From 3cb8800a617ce176f3ee060df86d906a76c4819f Mon Sep 17 00:00:00 2001 From: Essential CI Date: Tue, 28 May 2024 13:04:41 +0000 Subject: [PATCH] Version 1.3.2.5 --- build.gradle.kts | 3 + changelog/release-1.3.2.5.md | 10 + .../essential/cosmetics/WearablesManager.kt | 37 +++ .../cosmetics/state/WearableLocator.kt | 2 +- .../database/GitRepoCosmeticsDatabase.kt | 5 +- .../cosmetics/preview/PerspectiveCamera.kt | 1 + .../cosmetics/settings/CosmeticProperty.kt | 1 + .../kotlin/gg/essential/model/Animation.kt | 36 +- .../kotlin/gg/essential/model/BedrockModel.kt | 35 +- .../kotlin/gg/essential/model/ModelParser.kt | 2 + .../gg/essential/model/ParticleEffect.kt | 2 + .../gg/essential/model/ParticleSystem.kt | 56 +++- .../kotlin/gg/essential/model/SoundEffect.kt | 3 + .../essential/model/backend/RenderBackend.kt | 9 +- .../model/backend/atlas/TextureAtlas.kt | 221 +++++++++++++ .../gg/essential/model/file/AnimationFile.kt | 1 + .../gg/essential/model/file/ModelFile.kt | 1 + .../model/file/SoundDefinitionsFile.kt | 11 +- .../gg/essential/model/util/UMatrixStack.kt | 7 + features.properties | 1 + gradle.properties | 2 +- .../gg/essential/gui/common/LoadingIcon.kt | 3 +- .../gui/common/shadow/ShadowEffect.kt | 14 + .../gg/essential/gui/image/ImageFactory.kt | 26 -- .../gui/image/ResourceImageFactory.kt | 4 +- .../gg/essential/gui/layoutdsl/effects.kt | 3 +- immediatelyfast/build.gradle.kts | 27 ++ .../compat/ImmediatelyFastCompat.java | 57 ++++ .../cosmetics/model/CosmeticTier.java | 1 - loader | 2 +- settings.gradle.kts | 1 + src/main/java/gg/essential/Essential.java | 18 + .../cosmetics/EssentialModelRenderer.java | 32 +- .../essential/handlers/OnlineIndicator.java | 20 ++ src/main/java/gg/essential/mixins/Plugin.java | 38 ++- .../impl/client/audio/SoundSystemExt.java | 23 ++ .../entity/MixinAbstractClientPlayer.java | 3 +- ...tion.java => Mixin_UI3DPlayer_Camera.java} | 38 ++- .../Mixin_UpdateOffscreenPlayers.java | 2 +- .../Mixin_AddParticleSystemToClientWorld.java | 8 +- .../Mixin_FixPaulscodeChannelAllocation.java | 31 ++ ..._ISoundExt_isRelativeToListener_apply.java | 11 + .../Mixin_SoundSystemExt_SoundHandler.java | 40 +++ .../Mixin_SoundSystemExt_SoundManager.java | 94 ++++++ .../sound/Mixin_UpdateWhilePaused.java | 170 ++++++++++ .../gg/essential/model/PlayerMolangQuery.java | 7 + .../cosmetics/CosmeticsManager.java | 4 +- .../gui/common/CosmeticHoverOutlineEffect.kt | 15 +- .../gui/common/EmulatedUI3DPlayer.kt | 13 + .../gg/essential/gui/common/UI3DPlayer.kt | 310 +++++++++--------- .../gui/image/EssentialAssetImageFactory.kt | 2 - .../essential/gui/overlay/EphemeralLayer.kt | 21 -- .../gg/essential/gui/overlay/ModalManager.kt | 24 ++ .../essential/gui/overlay/ModalManagerImpl.kt | 58 ++++ .../essential/gui/overlay/OverlayManager.kt | 20 -- .../gui/overlay/OverlayManagerImpl.kt | 14 - .../components/ScreenshotBrowser.kt | 6 - .../kotlin/gg/essential/gui/wardrobe/Item.kt | 1 + .../wardrobe/components/outfitComponents.kt | 2 +- .../components/outfitItemFunctions.kt | 2 +- .../gui/wardrobe/components/previewWindow.kt | 20 +- .../RequiresUnlockActionConfiguration.kt | 8 +- .../gui/wardrobe/modals/CoinsPurchaseModal.kt | 8 +- .../gg/essential/gui/wardrobe/purchase.kt | 2 +- .../handlers/EssentialSoundManager.kt | 101 ++++-- .../minecraft/MinecraftRenderBackend.kt | 85 ++++- .../minecraft/legacyCameraPositioning.kt | 36 ++ .../CosmeticEquipVisibilityResponse.kt | 2 +- .../cosmetics/localCosmeticManagement.kt | 18 +- .../network/cosmetics/conversions.kt | 2 +- .../kotlin/gg/essential/util/GlFrameBuffer.kt | 15 +- src/main/kotlin/gg/essential/util/GuiUtil.kt | 21 +- .../kotlin/gg/essential/util/MojangAPI.kt | 2 +- .../kotlin/gg/essential/util/extensions.kt | 6 +- src/main/kotlin/gg/essential/util/helpers.kt | 2 + src/main/kotlin/gg/essential/util/iterable.kt | 1 + .../essential/account/login/microsoft.html | 28 +- .../essential/account/login/success.html | 30 +- .../assets/essential/textures/essential.png | Bin 1045 -> 6758 bytes src/main/resources/mixins.essential.init.json | 1 + src/main/resources/mixins.essential.json | 6 +- .../essential/model/util/ResourceCleaner.kt | 44 +++ versions/1.16.2-1.12.2.txt | 2 + .../MixinGuiInventory_UI3DPlayerOffset.java | 4 +- .../Mixin_FixPaulscodeChannelAllocation.java | 20 ++ .../sound/Mixin_UpdateWhilePaused.java | 69 ++++ versions/1.19.3-1.19.2.txt | 1 + .../sound/Mixin_ISoundExt_createAccessor.java | 22 +- 88 files changed, 1804 insertions(+), 363 deletions(-) create mode 100644 changelog/release-1.3.2.5.md create mode 100644 cosmetics/src/commonMain/kotlin/gg/essential/model/backend/atlas/TextureAtlas.kt create mode 100644 immediatelyfast/build.gradle.kts create mode 100644 immediatelyfast/src/main/java/gg/essential/compat/ImmediatelyFastCompat.java create mode 100644 src/main/java/gg/essential/mixins/impl/client/audio/SoundSystemExt.java rename src/main/java/gg/essential/mixins/transformers/client/gui/{Mixin_UI3DPlayer_FixNameTagRotation.java => Mixin_UI3DPlayer_Camera.java} (68%) create mode 100644 src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java create mode 100644 src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundHandler.java create mode 100644 src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundManager.java create mode 100644 src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java create mode 100644 src/main/kotlin/gg/essential/gui/overlay/ModalManager.kt create mode 100644 src/main/kotlin/gg/essential/gui/overlay/ModalManagerImpl.kt create mode 100644 src/main/kotlin/gg/essential/model/backend/minecraft/legacyCameraPositioning.kt create mode 100644 utils/src/jvmMain/kotlin/gg/essential/model/util/ResourceCleaner.kt create mode 100644 versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java create mode 100644 versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java diff --git a/build.gradle.kts b/build.gradle.kts index dc13bc0..b990efa 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -87,6 +87,9 @@ dependencies { implementation(bundle(project(":clipboard"))!!) implementation(bundle(project(":utils"))!!) implementation(bundle(project(":plasmo"))!!) + if (platform.mcVersion >= 11800) { + implementation(bundle(project(":immediatelyfast"))!!) + } testImplementation(kotlin("test")) diff --git a/changelog/release-1.3.2.5.md b/changelog/release-1.3.2.5.md new file mode 100644 index 0000000..e9b7c6f --- /dev/null +++ b/changelog/release-1.3.2.5.md @@ -0,0 +1,10 @@ +Title: Bug Patch +Summary: Minor bug fixes + +## Bug Fixes +- Fixed skins failing to upload in certain situations +- Fixed rendering of translucent cosmetics + +## Compatibility +- Fixed the tab list breaking when using ImmediatelyFast +- Fixed incompatibility with MixinBooter 8.9+ and some other mods on 1.12.2 diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt b/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt index 62de389..c38da6e 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt @@ -19,11 +19,13 @@ import gg.essential.model.ModelInstance import gg.essential.model.RenderMetadata import gg.essential.model.backend.PlayerPose import gg.essential.model.backend.RenderBackend +import gg.essential.model.backend.atlas.TextureAtlas import gg.essential.model.molang.MolangQueryEntity import gg.essential.model.util.UMatrixStack import gg.essential.network.cosmetics.Cosmetic class WearablesManager( + private val renderBackend: RenderBackend, private val entity: MolangQueryEntity, private val animationTargets: Set, private val onAnimation: (Cosmetic, String) -> Unit, @@ -34,8 +36,11 @@ class WearablesManager( var models: Map = emptyMap() private set + private var translucentTextureAtlas: TextureAtlas? = null + fun updateState(newState: CosmeticsState) { val oldModels = models + val oldTextures = oldModels.values.filter { it.model.translucent }.mapNotNull { it.model.texture }.distinct() val newModels = newState.bedrockModels @@ -48,8 +53,19 @@ class WearablesManager( wearable } } + .sortedBy { it.model.translucent } // render opaque models first .associateBy { it.cosmetic } + // If there's more than one translucent model, we need to render them all in a single (sorted) pass + val newTextures = newModels.values.filter { it.model.translucent }.mapNotNull { it.model.texture }.distinct() + if (oldTextures != newTextures) { + translucentTextureAtlas?.close() + translucentTextureAtlas = null + } + if (translucentTextureAtlas == null && newTextures.size > 1) { + translucentTextureAtlas = TextureAtlas.create(renderBackend, "cosmetics-${atlasCounter++}", newTextures) + } + for ((cosmetic, model) in models.entries) { if (newModels[cosmetic] != model) { model.locator.isValid = false @@ -71,8 +87,25 @@ class WearablesManager( parts: Set = EnumPart.values().toSet(), ) { for ((_, model) in models) { + if (model.model.translucent && translucentTextureAtlas != null) { + continue // will do these later in a single final pass + } render(matrixStack, vertexConsumerProvider, model, pose, skin, parts) } + + val atlas = translucentTextureAtlas + if (atlas != null) { + vertexConsumerProvider.provide(atlas.atlasTexture) { vertexConsumer -> + val atlasVertexConsumerProvider = RenderBackend.VertexConsumerProvider { texture, block -> + block(atlas.offsetVertexConsumer(texture, vertexConsumer)) + } + for ((_, model) in models) { + if (model.model.translucent) { + render(matrixStack, atlasVertexConsumerProvider, model, pose, skin, parts) + } + } + } + } } fun render( @@ -109,4 +142,8 @@ class WearablesManager( } } } + + companion object { + private var atlasCounter = 0 + } } \ No newline at end of file diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/WearableLocator.kt b/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/WearableLocator.kt index 8aba537..1b315df 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/WearableLocator.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/WearableLocator.kt @@ -14,7 +14,7 @@ package gg.essential.cosmetics.state import gg.essential.model.ParticleSystem /** A wrapper which becomes invalid when this particular cosmetic instance is unequipped. */ -class WearableLocator(private val parent: ParticleSystem.Locator) : ParticleSystem.Locator by parent { +class WearableLocator(override val parent: ParticleSystem.Locator) : ParticleSystem.Locator by parent { private var wearableIsValid = true override var isValid: Boolean get() = parent.isValid && wearableIsValid diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/database/GitRepoCosmeticsDatabase.kt b/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/database/GitRepoCosmeticsDatabase.kt index 6c24438..ed8478a 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/database/GitRepoCosmeticsDatabase.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/database/GitRepoCosmeticsDatabase.kt @@ -20,16 +20,15 @@ import gg.essential.cosmetics.CosmeticTypeId import gg.essential.cosmetics.FeaturedPageCollectionId import gg.essential.cosmetics.FeaturedPageWidth import gg.essential.mod.EssentialAsset -import gg.essential.mod.Skin import gg.essential.mod.cosmetics.CosmeticAssets import gg.essential.mod.cosmetics.CosmeticBundle import gg.essential.mod.cosmetics.CosmeticCategory -import gg.essential.mod.cosmetics.settings.CosmeticProperty import gg.essential.mod.cosmetics.CosmeticSlot import gg.essential.mod.cosmetics.CosmeticTier import gg.essential.mod.cosmetics.CosmeticType import gg.essential.mod.cosmetics.featured.FeaturedPage import gg.essential.mod.cosmetics.featured.FeaturedPageCollection +import gg.essential.mod.cosmetics.settings.CosmeticProperty import gg.essential.mod.cosmetics.settings.CosmeticSetting import gg.essential.model.Side import gg.essential.model.util.Instant @@ -458,7 +457,7 @@ class GitRepoCosmeticsDatabase( val existingMetadataFile = bundleByPath.entries.firstNotNullOfOrNull { if (it.value == id) it.key else null } val metadataFile = when { existingMetadataFile != null -> existingMetadataFile - bundle != null -> Path.of("store-bundles/${id.lowercase()}.store-bundle-metadata.json") + bundle != null -> Path.of("store_bundles/${id.lowercase()}.store-bundle-metadata.json") else -> return emptyMap() } diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/preview/PerspectiveCamera.kt b/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/preview/PerspectiveCamera.kt index 4d823d8..ab22af9 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/preview/PerspectiveCamera.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/preview/PerspectiveCamera.kt @@ -13,6 +13,7 @@ package gg.essential.mod.cosmetics.preview import dev.folomeev.kotgl.matrix.vectors.Vec3 import dev.folomeev.kotgl.matrix.vectors.mutables.minus +import dev.folomeev.kotgl.matrix.vectors.mutables.plus import dev.folomeev.kotgl.matrix.vectors.mutables.times import dev.folomeev.kotgl.matrix.vectors.vec3 import dev.folomeev.kotgl.matrix.vectors.vecUnitY diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/settings/CosmeticProperty.kt b/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/settings/CosmeticProperty.kt index e195991..b537291 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/settings/CosmeticProperty.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/settings/CosmeticProperty.kt @@ -213,6 +213,7 @@ sealed class CosmeticProperty { override val actionDescription: String, @SerialName("SERVER_ADDRESS") val serverAddress: String, ): Data() + } } diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/Animation.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/Animation.kt index 506d702..4bd10e9 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/Animation.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/Animation.kt @@ -48,7 +48,7 @@ class ModelAnimationState( animation.effects.values.forEach { list -> list.forEach { event -> - val name = (event as? Animation.ParticleEvent)?.locator?.boxName + val name = (event as? Animation.LocatableEvent)?.locator?.boxName if (name != null && name !in locators) { locators[name] = BoneLocator(vec3(), Quaternion.Identity, vec3()) } @@ -175,11 +175,12 @@ class ModelAnimationState( } /** Emits effect keyframes into [pendingEvents]. */ - fun updateEffects() { + fun updateEffects(untilLifeTime: Float = entity.lifeTime) { for (state in active) { while (true) { val (nextTime, effects) = state.animation.effects.higherEntry(state.lastEffectTime) ?: break - if (nextTime > state.animTime) { + val nextLifeTime = state.animStartTime + nextTime + if (nextLifeTime > untilLifeTime) { break } state.lastEffectTime = nextTime @@ -187,13 +188,16 @@ class ModelAnimationState( pendingEvents.add(when (event) { is Animation.ParticleEvent -> ParticleEvent( entity, + nextLifeTime, event.effect, event.locator?.boxName?.let { locators[it] } ?: parentLocator, event.preEffectScript, ) is Animation.SoundEvent -> SoundEvent( entity, + nextLifeTime, event.effect, + event.locator?.boxName?.let { locators[it] } ?: parentLocator, ) }) } @@ -205,7 +209,7 @@ class ModelAnimationState( val animation: Animation ) : MolangQueryAnimation, MolangQueryEntity by entity { val context = MolangContext(this) - private val animStartTime = entity.lifeTime + val animStartTime = entity.lifeTime override val animTime: Float get() = entity.lifeTime - animStartTime override val animLoopTime: Float @@ -219,10 +223,12 @@ class ModelAnimationState( sealed interface Event { val timeSource: MolangQueryTime + val time: Float } data class ParticleEvent( override val timeSource: MolangQueryTime, + override val time: Float, val effect: ParticleEffect, val locator: ParticleSystem.Locator, val preEffectScript: MolangExpression?, @@ -230,7 +236,9 @@ class ModelAnimationState( data class SoundEvent( override val timeSource: MolangQueryTime, - val effect: SoundEffect + override val time: Float, + val effect: SoundEffect, + val locator: ParticleSystem.Locator, ) : Event private inner class BoneLocator( @@ -238,6 +246,8 @@ class ModelAnimationState( override var rotation: Quaternion, override var velocity: Vec3 ) : ParticleSystem.Locator { + override val parent: ParticleSystem.Locator? + get() = parentLocator override val isValid: Boolean get() = parentLocator.isValid } @@ -279,7 +289,10 @@ data class Animation( file.soundEffects.forEach { (time, effects) -> val eventsAtTime = events.getOrPut(time, ::mutableListOf) for (config in effects) { - eventsAtTime.add(SoundEvent(soundEffects[config.effect] ?: continue)) + eventsAtTime.add(SoundEvent( + soundEffects[config.effect] ?: continue, + config.locator?.let { locatorName -> bones.find { it.boxName == locatorName } }, + )) } } }), @@ -290,15 +303,20 @@ data class Animation( sealed interface Event + sealed interface LocatableEvent : Event { + val locator: Bone? + } + data class ParticleEvent( val effect: ParticleEffect, - val locator: Bone?, + override val locator: Bone?, val preEffectScript: MolangExpression?, - ) : Event + ) : Event, LocatableEvent data class SoundEvent( val effect: SoundEffect, - ) : Event + override val locator: Bone?, + ) : Event, LocatableEvent companion object { private fun Map.calcAnimationLength(): Float { diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/BedrockModel.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/BedrockModel.kt index d074231..67cb10f 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/BedrockModel.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/BedrockModel.kt @@ -44,6 +44,7 @@ class BedrockModel( var boundingBoxes: List> var rootBone: Bone var textureFrameCount = 1 + var translucent = false var animations: List var animationEvents: List @@ -61,6 +62,7 @@ class BedrockModel( rootBone = parser.rootBone boundingBoxes = parser.boundingBoxes textureFrameCount = parser.textureFrameCount + translucent = parser.translucent } else { rootBone = Bone("_root") boundingBoxes = emptyList() @@ -69,23 +71,48 @@ class BedrockModel( sideOptions = getBones(rootBone).mapNotNull { it.side }.toSet() val particleEffects = mutableMapOf() + val soundEffects = mutableMapOf() + if (data != null) { for (config in particleData.map { it.particleEffect }) { val material = config.description.basicRenderParameters.material particleEffects[config.description.identifier] = - ParticleEffect(material, config.components, config.curves, config.events, texture, particleEffects) + ParticleEffect( + material, + config.components, + config.curves, + config.events, + texture, + particleEffects, + soundEffects, + ) } } - val soundEffects = mutableMapOf() if (soundData != null) { for ((identifier, definition) in soundData.definitions) { val sounds = definition.sounds.mapNotNull { sound -> val asset = cosmetic.assets(variant).files[sound.name + ".ogg"] ?: return@mapNotNull null - SoundEffect.Entry(asset, sound.stream, sound.volume, sound.pitch, sound.directional, sound.weight) + SoundEffect.Entry( + asset, + sound.stream, + sound.interruptible, + sound.volume, + sound.pitch, + sound.looping, + sound.directional, + sound.weight, + ) } soundEffects[identifier] = - SoundEffect(identifier, definition.category, definition.minDistance, definition.maxDistance, sounds) + SoundEffect( + identifier, + definition.category, + definition.minDistance, + definition.maxDistance, + definition.fixedPosition, + sounds, + ) } } diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelParser.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelParser.kt index 509e4e7..b4ba371 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelParser.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelParser.kt @@ -26,6 +26,7 @@ class ModelParser( val boundingBoxes = mutableListOf>() val rootBone = makeBone("_root") var textureFrameCount = 1 + var translucent = false private fun makeBone(name: String): Bone { val bone = Bone(name) @@ -41,6 +42,7 @@ class ModelParser( val geometry = file.geometries.firstOrNull() ?: return textureFrameCount = (textureHeight / geometry.description.textureHeight).coerceAtLeast(1) + translucent = geometry.description.textureTranslucent val extraInflate = when { cosmetic.type.id == "PLAYER" -> 0f diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleEffect.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleEffect.kt index 0883a27..5b8816c 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleEffect.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleEffect.kt @@ -23,6 +23,8 @@ class ParticleEffect( val texture: RenderBackend.Texture?, /** All effects referenced by events of this effect. May contain more events than actually referenced. */ val referencedEffects: Map, + /** All sounds referenced by events of this effect. May contain more sounds than actually referenced. */ + val referencedSounds: Map, ) { val renderPass = texture?.let { RenderPass(material, it) } data class RenderPass(val material: ParticlesFile.Material, val texture: RenderBackend.Texture) diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleSystem.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleSystem.kt index 28151cf..7952d77 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleSystem.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/ParticleSystem.kt @@ -88,6 +88,7 @@ class ParticleSystem( private val random: Random, private val collisionProvider: CollisionProvider, private val lightProvider: LightProvider, + private val playSound: (ModelAnimationState.SoundEvent) -> Unit, ) { private val universes = mutableMapOf() @@ -146,7 +147,19 @@ class ParticleSystem( ) event.preEffectScript?.eval(emitter.molang) universe.emitters.add(emitter) - emitter.startLoop(0f) + + val dt = (universe.lastUpdate - event.time).coerceAtLeast(0f) + emitter.startLoop(dt) + + // Spawn particles for at most the last few seconds, anything before that is probably already gone and we need + // *some* limit on the total simulation time to avoid effective live-locks on large values + val maxSimTime = 10f + if (dt > maxSimTime) { + emitter.skip(dt - maxSimTime) + emitter.update(maxSimTime) + } else { + emitter.update(dt) + } } fun update() { @@ -329,6 +342,10 @@ class ParticleSystem( nextTimelineEvent = components.emitterLifetimeEvents.timeline.lowestEntry() } + fun skip(dt: Float) { + age += dt + } + fun update(dt: Float): Boolean { val alive = doUpdate(dt) @@ -494,9 +511,28 @@ class ParticleSystem( } event.sound?.let { config -> - // TODO sound, eventually + val targetSound = effect.referencedSounds[config.event] ?: return@let + system.playSound(ModelAnimationState.SoundEvent( + universe.timeSource, + universe.lastUpdate - timeSince, + targetSound, + if (particle != null) Particle.LocatorFor(particle) else LocatorFor(this), + )) } } + + class LocatorFor(val emitter: Emitter) : Locator { + override val parent: Locator? + get() = emitter.locator + override val isValid: Boolean + get() = !emitter.firedExpirationEvents + override val position: Vec3 + get() = emitter.position + override val rotation: Quaternion + get() = emitter.rotation + override val velocity: Vec3 + get() = emitter.velocity + } } private class Particle( @@ -973,15 +1009,31 @@ class ParticleSystem( emitPoint(+sizeX, +sizeY, minUV.x, minUV.y) emitPoint(+sizeX, -sizeY, minUV.x, maxUV.y) } + + class LocatorFor(val particle: Particle) : Locator { + override val parent: Locator? + get() = particle.localSpace + override val isValid: Boolean + get() = !particle.firedExpirationEvents + override val position: Vec3 + get() = particle.globalPosition + override val rotation: Quaternion + get() = Quaternion.Identity + override val velocity: Vec3 + get() = particle.globalVelocity + } } interface Locator { + val parent: Locator? val isValid: Boolean val position: Vec3 val rotation: Quaternion val velocity: Vec3 object Zero : Locator { + override val parent: Locator? + get() = null override val isValid: Boolean get() = true override val position: Vec3 diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/SoundEffect.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/SoundEffect.kt index f51be93..e2b7d74 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/SoundEffect.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/SoundEffect.kt @@ -20,13 +20,16 @@ class SoundEffect( // Note: Min distance is not currently implemented. Minecraft uses a fixed 0 for all sounds. val minDistance: Float = 0f, val maxDistance: Float = 16f, + val fixedPosition: Boolean, val sounds: List, ) { class Entry( val asset: EssentialAsset, val stream: Boolean = false, + val interruptible: Boolean = false, val volume: Float = 1f, val pitch: Float = 1f, + val looping: Boolean = false, val directional: Boolean = true, val weight: Int = 1, ) diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/backend/RenderBackend.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/backend/RenderBackend.kt index 270e973..eb05441 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/backend/RenderBackend.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/backend/RenderBackend.kt @@ -14,6 +14,11 @@ package gg.essential.model.backend import gg.essential.model.util.UVertexConsumer interface RenderBackend { + fun createTexture(name: String, width: Int, height: Int): Texture + fun deleteTexture(texture: Texture) + + fun blitTexture(dst: Texture, ops: Iterable) + suspend fun readTexture(name: String, bytes: ByteArray): Texture interface Texture { @@ -21,7 +26,9 @@ interface RenderBackend { val height: Int } - interface VertexConsumerProvider { + fun interface VertexConsumerProvider { fun provide(texture: Texture, block: (UVertexConsumer) -> Unit) } + + data class BlitOp(val src: Texture, val srcX: Int, val srcY: Int, val destX: Int, val destY: Int, val width: Int, val height: Int) } diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/backend/atlas/TextureAtlas.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/backend/atlas/TextureAtlas.kt new file mode 100644 index 0000000..6a34315 --- /dev/null +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/backend/atlas/TextureAtlas.kt @@ -0,0 +1,221 @@ +/* + * 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.model.backend.atlas + +import gg.essential.model.backend.RenderBackend +import gg.essential.model.backend.RenderBackend.Texture +import gg.essential.model.util.ResourceCleaner +import gg.essential.model.util.UVertexConsumer +import java.util.* +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + +class TextureAtlas private constructor( + private val renderBackend: RenderBackend, + val atlasTexture: Texture, + private val textures: Map +) : AutoCloseable { + + init { + val renderBackend = renderBackend + val atlasTexture = atlasTexture + resourceCleaner.register(this) { renderBackend.deleteTexture(atlasTexture) } + resourceCleaner.runCleanups() + } + + override fun close() { + renderBackend.deleteTexture(atlasTexture) + } + + fun offsetVertexConsumer(texture: Texture, vertexConsumer: UVertexConsumer): UVertexConsumer { + val entry = textures.getValue(texture) + return object : UVertexConsumer by vertexConsumer { + override fun tex(u: Double, v: Double): UVertexConsumer { + vertexConsumer.tex(u * entry.uScale + entry.uOffset, v * entry.vScale + entry.vOffset) + return this + } + } + } + + private class Entry(val uScale: Double, val vScale: Double, val uOffset: Double, val vOffset: Double) + + companion object { + private val resourceCleaner = ResourceCleaner() + + fun create(renderBackend: RenderBackend, name: String, textures: Collection): TextureAtlas? { + val toBePlaced = textures.map { it to WH(it.width, it.height) } + + val sorted = toBePlaced.sortedByDescending { (_, size) -> with(size) { w * h * max(w, h) / min(w, h) } } + val packing = pack(sorted.map { it.first }, 4096) ?: return null + + val atlasTexture = renderBackend.createTexture("atlas/$name", packing.atlasWidth, packing.atlasHeight) + renderBackend.blitTexture(atlasTexture, packing.textures.map { (texture, x, y, w, h, flipped) -> + // TODO implement flipping, somehow + RenderBackend.BlitOp(texture, 0, 0, x, y, w, h) + }) + val texturesMap = packing.textures.associate { (texture, x, y, w, h, flipped) -> + // TODO implement flipping + texture to Entry( + w.toDouble() / packing.atlasWidth.toDouble(), + h.toDouble() / packing.atlasHeight.toDouble(), + x.toDouble() / packing.atlasWidth.toDouble(), + y.toDouble() / packing.atlasHeight.toDouble(), + ) + } + return TextureAtlas(renderBackend, atlasTexture, texturesMap) + } + } +} + +private data class WH(val w: Int, val h: Int) +private data class XYWH(val x: Int, val y: Int, val w: Int, val h: Int) +private class Packing( + val atlasWidth: Int, + val atlasHeight: Int, + val textures: List, +) +private data class Entry(val texture: Texture, val x: Int, val y: Int, val w: Int, val h: Int, val flipped: Boolean) + +// Packing algorithm very much based on https://github.com/TeamHypersomnia/rectpack2D#algorithm +private fun pack(textures: Iterable, maxAtlasSize: Int): Packing? { + val discardStep = 16 + val initialSize = 512 + + var bestPacking: Packing? = packWithSize(textures, initialSize, initialSize) + + var squareSize = initialSize + while (bestPacking == null) { + squareSize *= 2 + if (squareSize > maxAtlasSize) { + return null + } + bestPacking = packWithSize(textures, squareSize, squareSize) + } + + var step = -squareSize / 2 + if (squareSize > initialSize) step /= 2 + while (abs(step) >= discardStep) { + squareSize += step + val packing = packWithSize(textures, squareSize, squareSize) + step = if (packing == null) { + abs(step / 2) + } else { + -abs(step / 2) + } + bestPacking = packing ?: bestPacking + } + + bestPacking!! + var width = bestPacking.atlasWidth + var height = bestPacking.atlasHeight + + step = -width / 2 + while (abs(step) >= discardStep) { + width += step + val packing = packWithSize(textures, width, height) + step = if (packing == null) { + abs(step / 2) + } else { + -abs(step / 2) + } + bestPacking = packing ?: bestPacking + } + + bestPacking!! + width = bestPacking.atlasWidth + height = bestPacking.atlasHeight + + step = -height / 2 + while (abs(step) >= discardStep) { + height += step + val packing = packWithSize(textures, width, height) + step = if (packing == null) { + abs(step / 2) + } else { + -abs(step / 2) + } + bestPacking = packing ?: bestPacking + } + + return bestPacking +} + +private fun packWithSize(textures: Iterable, atlasWidth: Int, atlasHeight: Int): Packing? { + val packedTextures = mutableListOf() + val freeRects = mutableListOf(XYWH(0, 0, atlasWidth, atlasHeight)) + textures@for (texture in textures) { + // Search backwards through all free rects, so we try smaller ones first + for (i in freeRects.lastIndex downTo 0) { + val freeRect = freeRects[i] + + fun place(textureW: Int, textureH: Int, flipped: Boolean): Boolean { + val remainingW = freeRect.w - textureW + val remainingH = freeRect.h - textureH + + if (remainingW < 0 || remainingH < 0) { + return false // doesn't fit, try next one + } + + // Texture fits into this free rect, place it + packedTextures.add(Entry(texture, freeRect.x, freeRect.y, textureW, textureH, flipped)) + + // Fits, remove the free rect + // (by swapping with the last one so we don't need to shift the entire array) + freeRects[i] = freeRects.last() + freeRects.removeLast() + + when { + // Texture fills entire freeRect, nothing remains + remainingW == 0 && remainingH == 0 -> {} + // Texture fill entire width, add remaining height as new free rect + remainingW == 0 -> + freeRects.add(XYWH(freeRect.x, freeRect.y + textureH, freeRect.w, remainingH)) + // Texture fill entire height, add remaining width as new free rect + remainingH == 0 -> + freeRects.add(XYWH(freeRect.x + textureW, freeRect.y, remainingW, freeRect.h)) + // Texture fills neither width nor height, add remaining space as two free rects + else -> { + // Prefer one tiny and one huge free rect, assumption being that less space is wasted that way. + // Insert tiny one last so it is tried first for subsequent loops + if (remainingW > remainingH) { + // Large rect to the right of the texture + freeRects.add(XYWH(freeRect.x + textureW, freeRect.y, remainingW, freeRect.h)) + // Small rect directly below the texture + freeRects.add(XYWH(freeRect.x, freeRect.y + textureH, textureW, remainingH)) + } else { + // Large rect below the texture + freeRects.add(XYWH(freeRect.x, freeRect.y + textureH, freeRect.w, remainingH)) + // Small rect directly to the right of the texture + freeRects.add(XYWH(freeRect.x + textureW, freeRect.y, remainingW, textureH)) + } + } + } + + return true + } + + if (place(texture.width, texture.height, false)) { + continue@textures + } + /* TODO implement + if (place(texture.height, texture.width, true)) { + continue@textures + } + */ + } + + // No fitting free space found, give up + return null + } + return Packing(atlasWidth, atlasHeight, packedTextures) +} diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/file/AnimationFile.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/file/AnimationFile.kt index f79d5b5..54062db 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/file/AnimationFile.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/file/AnimationFile.kt @@ -68,6 +68,7 @@ data class AnimationFile( @Serializable data class SoundEffect( val effect: String, + val locator: String? = null, ) } diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/file/ModelFile.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/file/ModelFile.kt index bbbe38b..4467b44 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/file/ModelFile.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/file/ModelFile.kt @@ -46,6 +46,7 @@ data class ModelFile( val identifier: String, @SerialName("texture_width") val textureWidth: Int, @SerialName("texture_height") val textureHeight: Int, + @SerialName("texture_translucent") val textureTranslucent: Boolean = false, ) @Serializable diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/file/SoundDefinitionsFile.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/file/SoundDefinitionsFile.kt index eaafa29..e576d8b 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/file/SoundDefinitionsFile.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/file/SoundDefinitionsFile.kt @@ -14,6 +14,7 @@ package gg.essential.model.file import gg.essential.model.SoundCategory import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonObject @@ -34,6 +35,12 @@ class SoundDefinitionsFile( val minDistance: Float = 0f, @SerialName("max_distance") val maxDistance: Float = 16f, + /** + * When set to `true`, the sound will stay at the position it was emitted, otherwise it will follow the locator + * it is bound to (or its emitter if no explicit locator is set). + */ + @SerialName("fixed_position") + val fixedPosition: Boolean = false, val sounds: List<@Serializable(with = SoundObjectOrNameSerializer::class) Sound>, ) @@ -41,8 +48,10 @@ class SoundDefinitionsFile( class Sound( val name: String, val stream: Boolean = false, + val interruptible: Boolean = false, val volume: Float = 1f, val pitch: Float = 1f, + val looping: Boolean = false, @SerialName("is3D") val directional: Boolean = true, val weight: Int = 1, @@ -66,6 +75,6 @@ class SoundDefinitionsFile( } @JvmStatic - fun parse(str: String): SoundDefinitionsFile = SoundDefinitionsFile(emptyMap()) + fun parse(str: String): SoundDefinitionsFile = json.decodeFromString(str) } } diff --git a/cosmetics/src/commonMain/kotlin/gg/essential/model/util/UMatrixStack.kt b/cosmetics/src/commonMain/kotlin/gg/essential/model/util/UMatrixStack.kt index f87bd00..57edbc1 100644 --- a/cosmetics/src/commonMain/kotlin/gg/essential/model/util/UMatrixStack.kt +++ b/cosmetics/src/commonMain/kotlin/gg/essential/model/util/UMatrixStack.kt @@ -124,6 +124,13 @@ class UMatrixStack( rotate(2 * acos(q.w), q.x * n, q.y * n, q.z * n, degrees = false) } + fun multiply(other: UMatrixStack) { + val thisEntry = this.stack.last() + val otherEntry = other.stack.last() + thisEntry.model.timesSelf(otherEntry.model) + thisEntry.normal.timesSelf(otherEntry.normal) + } + fun fork() = UMatrixStack(mutableListOf(stack.last().deepCopy())) fun push() { diff --git a/features.properties b/features.properties index 652c3e5..29de899 100644 --- a/features.properties +++ b/features.properties @@ -1,3 +1,4 @@ always=true +cosmetic_sounds=true updated_gifting_modal=true new_toasts=true diff --git a/gradle.properties b/gradle.properties index f282238..b441302 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.2.4+g6b55293e12 +version=1.3.2.5+ge4fdbcd438 diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/common/LoadingIcon.kt b/gui/essential/src/main/kotlin/gg/essential/gui/common/LoadingIcon.kt index 7f557de..fce0295 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/common/LoadingIcon.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/common/LoadingIcon.kt @@ -20,7 +20,8 @@ import gg.essential.universal.UMatrixStack import java.awt.Color class LoadingIcon(val scale: Double) : UIComponent() { - private var time = 0f + var time = 0f + private set init { setX(CenterConstraint()) diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/common/shadow/ShadowEffect.kt b/gui/essential/src/main/kotlin/gg/essential/gui/common/shadow/ShadowEffect.kt index 18d765f..db6b924 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/common/shadow/ShadowEffect.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/common/shadow/ShadowEffect.kt @@ -19,6 +19,7 @@ import gg.essential.elementa.effects.Effect import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.State import gg.essential.gui.EssentialPalette +import gg.essential.gui.common.LoadingIcon import gg.essential.gui.common.SequenceAnimatedUIImage import gg.essential.universal.UGraphics import gg.essential.universal.UMatrixStack @@ -93,6 +94,19 @@ class ShadowEffect( shadowColorState.get() ) } + is LoadingIcon -> { + val xCenter = (boundComponent.getLeft() + boundComponent.getRight()) / 2 + val yCenter = (boundComponent.getTop() + boundComponent.getBottom()) / 2 + + LoadingIcon.draw( + matrixStack, + xCenter + boundComponent.scale.toInt(), + yCenter + boundComponent.scale.toInt(), + boundComponent.scale, + boundComponent.time, + shadowColorState.get() + ) + } else -> { throw UnsupportedOperationException("Shadow effect cannot be applied to ${getDebugInfo()}") } diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/image/ImageFactory.kt b/gui/essential/src/main/kotlin/gg/essential/gui/image/ImageFactory.kt index 3bab3fa..19d6164 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/image/ImageFactory.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/image/ImageFactory.kt @@ -41,21 +41,11 @@ data class ImageGeneratorSettings( abstract class ImageFactory( protected val settings: ImageGeneratorSettings = ImageGeneratorSettings() ) { - // Tracks whether this ImageFactory has loaded its resource at least once - private var loadedOnce = false - - /** - * Stores whether this ImageFactory supports the caching of its resource - * Calls to [preload] when supportsCaching is false will result in nothing - */ - abstract val supportsCaching: Boolean - /** * Produces a new [UIImage] and applies [settings] */ fun create(): UIImage { return generate().apply { - loadedOnce = true if (settings.autoSize) { supply(AutoImageSize(this)) } @@ -73,17 +63,6 @@ abstract class ImageFactory( */ protected abstract fun generate(): UIImage - /** - * Calls [create] if this ImageFactory supports caching and [create] - * has not been called at least once before. - */ - fun preload(): ImageFactory { - if (!loadedOnce && supportsCaching) { - create() - } - return this - } - /** * @return a clone of this ImageFactory with the color setting changed */ @@ -111,8 +90,6 @@ private class DelegatedImageImageFactory( settings: ImageGeneratorSettings ) : ImageFactory(settings) { - override val supportsCaching: Boolean by innerSupplier::supportsCaching - override fun generate(): UIImage { return innerSupplier.create() } @@ -130,9 +107,6 @@ private class DelegatedImageImageFactory( fun ImageFactory( generator: () -> UIImage, ): ImageFactory = object : ImageFactory() { - - override var supportsCaching: Boolean = false - override fun generate(): UIImage { return generator() } diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/image/ResourceImageFactory.kt b/gui/essential/src/main/kotlin/gg/essential/gui/image/ResourceImageFactory.kt index 06a3f5e..224c6e8 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/image/ResourceImageFactory.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/image/ResourceImageFactory.kt @@ -26,12 +26,10 @@ class ResourceImageFactory @LoadsResources("%resource%") constructor( init { if (preload) { - preload() + generate() } } - override val supportsCaching: Boolean = true - override fun generate(): UIImage { return UIImage.ofResourceCached(resource, cache) } diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/layoutdsl/effects.kt b/gui/essential/src/main/kotlin/gg/essential/gui/layoutdsl/effects.kt index 01d4a6a..d8c6a26 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/layoutdsl/effects.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/layoutdsl/effects.kt @@ -21,6 +21,7 @@ import gg.essential.elementa.effects.OutlineEffect import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.State import gg.essential.gui.EssentialPalette +import gg.essential.gui.common.LoadingIcon import gg.essential.gui.common.shadow.ShadowEffect import gg.essential.gui.common.shadow.ShadowIcon import java.awt.Color @@ -74,7 +75,7 @@ fun Modifier.shadow(color: Color? = null) = this then { } } - is UIImage, is UIBlock, is UIContainer -> { + is UIImage, is UIBlock, is UIContainer, is LoadingIcon -> { return@then Modifier.effect { ShadowEffect(color ?: EssentialPalette.BLACK) }.applyToComponent(this) } diff --git a/immediatelyfast/build.gradle.kts b/immediatelyfast/build.gradle.kts new file mode 100644 index 0000000..e892a08 --- /dev/null +++ b/immediatelyfast/build.gradle.kts @@ -0,0 +1,27 @@ +/* + * 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. + */ +import essential.modrinth + +plugins { + `java-library` +} + +java.toolchain.languageVersion.set(JavaLanguageVersion.of(17)) + +repositories { + modrinth() +} + +dependencies { + // Depend on a 1.20.4 version so that it is compiled with java 17 and not 21 + compileOnly("maven.modrinth:immediatelyfast:1.2.15+1.20.4-fabric") +} diff --git a/immediatelyfast/src/main/java/gg/essential/compat/ImmediatelyFastCompat.java b/immediatelyfast/src/main/java/gg/essential/compat/ImmediatelyFastCompat.java new file mode 100644 index 0000000..82818d3 --- /dev/null +++ b/immediatelyfast/src/main/java/gg/essential/compat/ImmediatelyFastCompat.java @@ -0,0 +1,57 @@ +/* + * 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.compat; + +import net.raphimc.immediatelyfastapi.BatchingAccess; +import net.raphimc.immediatelyfastapi.ImmediatelyFastApi; + +/** + * We must disable ImmediatelyFast's batching for any custom rendering we do. + * @see RaphiMC/ImmediatelyFast#213 + * @see + * MixinArmorChroma_GuiArmor + */ +public class ImmediatelyFastCompat { + private static final boolean IMMEDIATELYFAST_LOADED; + static { + boolean loaded; + try { + Class.forName("net.raphimc.immediatelyfastapi.ImmediatelyFastApi"); + loaded = true; + } catch (ClassNotFoundException e) { + loaded = false; + } + IMMEDIATELYFAST_LOADED = loaded; + } + + private static boolean wasHudBatching = false; + + public static void beforeHudDraw() { + if (!IMMEDIATELYFAST_LOADED) return; + + BatchingAccess access = ImmediatelyFastApi.getApiImpl().getBatching(); + if (access.isHudBatching()) { + access.endHudBatching(); + wasHudBatching = true; + } + } + + public static void afterHudDraw() { + if (!IMMEDIATELYFAST_LOADED) return; + + BatchingAccess access = ImmediatelyFastApi.getApiImpl().getBatching(); + if (wasHudBatching) { + access.beginHudBatching(); + wasHudBatching = false; + } + } +} diff --git a/infra/src/main/java/gg/essential/cosmetics/model/CosmeticTier.java b/infra/src/main/java/gg/essential/cosmetics/model/CosmeticTier.java index ea3c91b..99f2853 100644 --- a/infra/src/main/java/gg/essential/cosmetics/model/CosmeticTier.java +++ b/infra/src/main/java/gg/essential/cosmetics/model/CosmeticTier.java @@ -17,5 +17,4 @@ public enum CosmeticTier { RARE, EPIC, LEGENDARY, - } diff --git a/loader b/loader index 2f9e848..f1423fd 160000 --- a/loader +++ b/loader @@ -1 +1 @@ -Subproject commit 2f9e84827403189ee469cf66a6c3372584cfc719 +Subproject commit f1423fd87b591529bb3c6b24521e646d934ab98b diff --git a/settings.gradle.kts b/settings.gradle.kts index b86f1cb..34e5f3d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -54,6 +54,7 @@ include(":kdiscordipc") include(":clipboard") include(":utils") include(":plasmo") +include(":immediatelyfast") include(":api") project(":api").buildFileName = "root.gradle.kts" diff --git a/src/main/java/gg/essential/Essential.java b/src/main/java/gg/essential/Essential.java index ff1edfc..cc5de3d 100644 --- a/src/main/java/gg/essential/Essential.java +++ b/src/main/java/gg/essential/Essential.java @@ -31,6 +31,7 @@ import gg.essential.event.client.PostInitializationEvent; import gg.essential.event.client.PreInitializationEvent; import gg.essential.event.essential.TosAcceptedEvent; +import gg.essential.gui.EssentialPalette; import gg.essential.gui.account.factory.*; import gg.essential.gui.api.ComponentFactory; import gg.essential.gui.common.UI3DPlayer; @@ -98,6 +99,22 @@ public class Essential implements EssentialAPI { ctx.updateLoggers(conf); } + // Workaround for https://github.com/MinecraftForge/EventBus/issues/44 + // Specifically, we may use UIImage before the game is fully initialized. + // UIImage will use the common fork join pool to load the image via UGraphics, which will (via some chain not + // clear in the debugger) load RegisterShadersEvent on those threads. + // EventSubclassTransformer will then try to load its parent class via the context class loader (which on common + // fork join pool threads is set to the app class loader), thereby getting a different Event class than the + // Event class it expects, failing the `isAssignableFrom` check, and thereby silently failing to transform the + // class, which will later result in a hard NoSuchMethodException failure when the event bus tries to create the + // listener list for the event. + // To work around the issue, we explicitly load the relevant classes very early on the main thread (where it + // loads properly), such that it is then already loaded for any subsequent uses. + //#if FORGE && MC>=11400 + //$$ gg.essential.universal.UGraphics.getTexture( + //$$ new java.awt.image.BufferedImage(16, 16, java.awt.image.BufferedImage.TYPE_INT_ARGB)); + //#endif + dispatchIndependentStaticInitializers(); } @@ -184,6 +201,7 @@ private void dispatchStaticInitializers() { Multithreading.runAsync(() -> ElementaFonts.INSTANCE.getClass()); Multithreading.runAsync(() -> EssentialAPI.Companion.getClass()); Multithreading.runAsync(() -> AutoUpdate.INSTANCE.getClass()); + Multithreading.runAsync(() -> EssentialPalette.INSTANCE.getClass()); } @SuppressWarnings({ diff --git a/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java b/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java index 6df7727..f5cf7b3 100644 --- a/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java +++ b/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java @@ -13,6 +13,7 @@ import gg.essential.config.EssentialConfig; import gg.essential.gui.common.EmulatedUI3DPlayer; +import gg.essential.gui.elementa.state.v2.State; import gg.essential.handlers.EssentialSoundManager; import gg.essential.mixins.ext.client.ParticleSystemHolder; import gg.essential.model.EnumPart; @@ -40,12 +41,17 @@ import java.util.*; import static gg.essential.cosmetics.EssentialModelRendererKt.renderForHoverOutline; +import static gg.essential.gui.elementa.state.v2.StateKt.stateOf; import static gg.essential.util.ExtensionsKt.toCommon; //#if MC>=11400 //$$ import com.mojang.blaze3d.matrix.MatrixStack; //$$ import net.minecraft.client.renderer.IRenderTypeBuffer; //$$ import net.minecraft.client.renderer.entity.model.PlayerModel; +//#else +import dev.folomeev.kotgl.matrix.vectors.Vec3; +import gg.essential.universal.UGraphics; +import static gg.essential.model.backend.minecraft.LegacyCameraPositioningKt.getRelativeCameraPosFromGlState; //#endif //#if MC>=11400 @@ -131,6 +137,20 @@ public void render( ); matrixStack.push(); + + //#if MC<11400 + // Reposition our stack such that the camera is at 0/0/0, this is important for translucent geometry because + // those are sorted relative to 0/0/0. + // Modern versions have two separate stack and the passed one already fulfills this requirement, older versions + // however don't, so we need to create this split artificially. Luckily our renderer already uses an explicit + // matrix stack, so this is as simple as offsetting that in one direction and the global stack in the other to + // balance it out. + Vec3 relativeCamera = getRelativeCameraPosFromGlState(); + matrixStack.translate(-relativeCamera.getX(), -relativeCamera.getY(), -relativeCamera.getZ()); + UGraphics.GL.pushMatrix(); + UGraphics.GL.translate(relativeCamera.getX(), relativeCamera.getY(), relativeCamera.getZ()); + //#endif + //#if MC<11400 if (player.isSneaking() && parts == null) { matrixStack.translate(0.0F, 0.2F, 0.0F); // from LayerCustomHead @@ -147,6 +167,9 @@ public void render( renderForHoverOutline(wearablesManager, toCommon(matrixStack), vertexConsumerProvider, pose, skin, parts); matrixStack.pop(); + //#if MC<11400 + UGraphics.GL.popMatrix(); + //#endif //#if MC>=12000 //$$ World world = player.clientWorld; @@ -167,13 +190,20 @@ public void render( particleSystem.spawn((ModelAnimationState.ParticleEvent) event); } } else if (event instanceof ModelAnimationState.SoundEvent) { + boolean forceGlobal; + State volume; if (player instanceof EmulatedUI3DPlayer.EmulatedPlayer) { EmulatedUI3DPlayer component = ((EmulatedUI3DPlayer.EmulatedPlayer) player).getEmulatedUI3DPlayer(); if (!component.getSounds().getUntracked()) { return Unit.INSTANCE; } + forceGlobal = true; + volume = component.getSoundsVolume(); + } else { + forceGlobal = false; + volume = stateOf(1f); } - EssentialSoundManager.INSTANCE.playSound(player, ((ModelAnimationState.SoundEvent) event).getEffect()); + EssentialSoundManager.INSTANCE.playSound((ModelAnimationState.SoundEvent) event, forceGlobal, volume); } return Unit.INSTANCE; }); diff --git a/src/main/java/gg/essential/handlers/OnlineIndicator.java b/src/main/java/gg/essential/handlers/OnlineIndicator.java index 0d5e6bf..f932a50 100644 --- a/src/main/java/gg/essential/handlers/OnlineIndicator.java +++ b/src/main/java/gg/essential/handlers/OnlineIndicator.java @@ -33,6 +33,10 @@ import net.minecraft.entity.player.EntityPlayer; import net.minecraft.util.text.ITextComponent; +//#if MC>=11800 +//$$ import gg.essential.compat.ImmediatelyFastCompat; +//#endif + //#if MC>=11600 //$$ import net.minecraft.client.renderer.RenderType; //$$ import net.minecraft.util.ResourceLocation; @@ -217,6 +221,8 @@ private static void drawTabIndicator( ProfileStatus status = profileManager.getStatus(playerUuid); if (status == ProfileStatus.OFFLINE) return; + beforeTabDraw(); + BlendState prevBlendState = BlendState.active(); BlendState.NORMAL.activate(); @@ -243,6 +249,8 @@ private static void drawTabIndicator( matrixStack.pop(); prevBlendState.activate(); + + afterTabDraw(); } /** @@ -283,4 +291,16 @@ public static UUID findUUIDFromDisplayName(ITextComponent displayName) { return null; } + + public static void beforeTabDraw() { + //#if MC>=11800 + //$$ ImmediatelyFastCompat.beforeHudDraw(); + //#endif + } + + public static void afterTabDraw() { + //#if MC>=11800 + //$$ ImmediatelyFastCompat.afterHudDraw(); + //#endif + } } diff --git a/src/main/java/gg/essential/mixins/Plugin.java b/src/main/java/gg/essential/mixins/Plugin.java index 0de1496..b742dde 100644 --- a/src/main/java/gg/essential/mixins/Plugin.java +++ b/src/main/java/gg/essential/mixins/Plugin.java @@ -24,7 +24,10 @@ import gg.essential.util.MixinUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import org.objectweb.asm.Opcodes; import org.objectweb.asm.tree.ClassNode; +import org.objectweb.asm.tree.InsnNode; +import org.objectweb.asm.tree.MethodNode; import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; import org.spongepowered.asm.mixin.extensibility.IMixinInfo; import org.spongepowered.asm.service.MixinService; @@ -37,12 +40,6 @@ //$$ import java.lang.management.RuntimeMXBean; //#endif -//#if MC==11602 && FABRIC -//$$ import org.objectweb.asm.Opcodes; -//$$ import org.objectweb.asm.tree.InsnNode; -//$$ import org.objectweb.asm.tree.MethodNode; -//#endif - public class Plugin implements IMixinConfigPlugin { private static final Logger logger = LogManager.getLogger("Essential Logger - Plugin"); @@ -179,7 +176,7 @@ public void preApply(String targetClassName, ClassNode targetClass, String mixin transformer.transform(targetClass); } - //#if MC==11602 && FABRIC + //#if MC==11602 && FABRIC || MC==12004 && FABRIC //$$ if (inOurDevEnv && mixinClassName.endsWith("Mixin_RenderParticleSystemOfClientWorld")) { //$$ // Workaround for a mixin """feature""" where in a development environment it'll strip the descriptor from //$$ // target references and thereby match methods that were never meant to be matched. @@ -190,17 +187,26 @@ public void preApply(String targetClassName, ClassNode targetClass, String mixin //$$ // to match in 1.16, it'll find the vanilla target and subsequently complain that our method signature is //$$ // wrong. //$$ // To work around this, we'll add a dummy for the OF method, so it finds something on the first pass: - //$$ MethodNode dummyMethod = new MethodNode( - //$$ 0, - //$$ "renderParticles", - //$$ "(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;Lnet/minecraft/client/render/LightmapTextureManager;Lnet/minecraft/client/render/Camera;FLnet/minecraft/client/render/Frustum;)V", - //$$ null, - //$$ null - //$$ ); - //$$ dummyMethod.instructions.add(new InsnNode(Opcodes.RETURN)); - //$$ targetClass.methods.add(dummyMethod); + //$$ createDummyIfMissing(targetClass, "renderParticles", "(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider$Immediate;Lnet/minecraft/client/render/LightmapTextureManager;Lnet/minecraft/client/render/Camera;FLnet/minecraft/client/render/Frustum;)V"); //$$ } //#endif + //#if MC<11400 + if (inOurDevEnv && (mixinClassName.endsWith("Mixin_SoundSystemExt_SoundManager") || mixinClassName.endsWith("Mixin_UpdateWhilePaused"))) { + // Similar issue as above, one injector is for Forge pre-2646, one for 2646+. + createDummyIfMissing(targetClass, "setListener", "(Lnet/minecraft/entity/Entity;F)V"); + } + //#endif + } + + private void createDummyIfMissing(ClassNode targetClass, String name, String desc) { + for (MethodNode method : targetClass.methods) { + if (name.equals(method.name) && desc.equals(method.desc)) { + return; + } + } + MethodNode dummyMethod = new MethodNode(0, name, desc, null, null); + dummyMethod.instructions.add(new InsnNode(Opcodes.RETURN)); + targetClass.methods.add(dummyMethod); } @Override diff --git a/src/main/java/gg/essential/mixins/impl/client/audio/SoundSystemExt.java b/src/main/java/gg/essential/mixins/impl/client/audio/SoundSystemExt.java new file mode 100644 index 0000000..d688a6b --- /dev/null +++ b/src/main/java/gg/essential/mixins/impl/client/audio/SoundSystemExt.java @@ -0,0 +1,23 @@ +/* + * 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.impl.client.audio; + +import dev.folomeev.kotgl.matrix.vectors.Vec3; +import gg.essential.model.util.Quaternion; +import org.jetbrains.annotations.Nullable; + +public interface SoundSystemExt { + @Nullable + Vec3 essential$getListenerPosition(); + @Nullable + Quaternion essential$getListenerRotation(); +} diff --git a/src/main/java/gg/essential/mixins/transformers/client/entity/MixinAbstractClientPlayer.java b/src/main/java/gg/essential/mixins/transformers/client/entity/MixinAbstractClientPlayer.java index a3f9ace..386ab80 100644 --- a/src/main/java/gg/essential/mixins/transformers/client/entity/MixinAbstractClientPlayer.java +++ b/src/main/java/gg/essential/mixins/transformers/client/entity/MixinAbstractClientPlayer.java @@ -27,6 +27,7 @@ import gg.essential.mod.cosmetics.CosmeticSlot; import gg.essential.model.BedrockModel; import gg.essential.model.PlayerMolangQuery; +import gg.essential.model.backend.minecraft.MinecraftRenderBackend; import gg.essential.model.util.PlayerPoseManager; import gg.essential.network.connectionmanager.cosmetics.CosmeticsManager; import gg.essential.util.UUIDUtil; @@ -142,7 +143,7 @@ public void setCosmeticsSource(CosmeticsSource cosmeticsSource) { boolean sendsAnimationPackets = player.getUniqueID().equals(UUIDUtil.getClientUUID()) && !(player instanceof EmulatedUI3DPlayer.EmulatedPlayer); - this.wearablesManager = new WearablesManager(molangQuery, animationTargets, (cosmetic, event) -> { + this.wearablesManager = new WearablesManager(MinecraftRenderBackend.INSTANCE, molangQuery, animationTargets, (cosmetic, event) -> { if (sendsAnimationPackets) { CosmeticSlot slot = cosmetic.getType().getSlot(); Essential.getInstance().getConnectionManager() diff --git a/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UI3DPlayer_FixNameTagRotation.java b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UI3DPlayer_Camera.java similarity index 68% rename from src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UI3DPlayer_FixNameTagRotation.java rename to src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UI3DPlayer_Camera.java index b5959f7..9331076 100644 --- a/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UI3DPlayer_FixNameTagRotation.java +++ b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UI3DPlayer_Camera.java @@ -12,6 +12,7 @@ package gg.essential.mixins.transformers.client.gui; import gg.essential.gui.common.UI3DPlayer; +import gg.essential.universal.UMatrixStack; import net.minecraft.client.Minecraft; import net.minecraft.client.gui.inventory.GuiInventory; import org.spongepowered.asm.mixin.Mixin; @@ -19,8 +20,17 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +//#if MC>=12000 +//$$ import net.minecraft.client.gui.DrawContext; +//#endif + +//#if MC>=11400 +//$$ import com.llamalad7.mixinextras.sugar.Local; +//$$ import com.mojang.blaze3d.matrix.MatrixStack; +//#endif + @Mixin(GuiInventory.class) -public abstract class Mixin_UI3DPlayer_FixNameTagRotation { +public abstract class Mixin_UI3DPlayer_Camera { // Forge has renamed the original method and added a tiny wrapper method in its place //#if FORGE && MC>=11900 && MC<11904 @@ -51,12 +61,30 @@ public abstract class Mixin_UI3DPlayer_FixNameTagRotation { at = @At(value = "INVOKE", target = "Lnet/minecraft/client/renderer/entity/RenderManager;setPlayerViewY(F)V", shift = At.Shift.AFTER, remap = true) //#endif ) - private static void applyUI3DPlayerCameraRotation(CallbackInfo ci) { + private static void applyUI3DPlayerCamera( + CallbackInfo ci + //#if MC>=12000 + //$$ , @Local(argsOnly = true) DrawContext drawContext + //#elseif MC>=11904 + //$$ , @Local(argsOnly = true) MatrixStack matrixStack + //#elseif MC>=11700 + //$$ , @Local(ordinal = 1) MatrixStack matrixStack + //#elseif MC>=11400 + //$$ , @Local(ordinal = 0) MatrixStack matrixStack + //#endif + ) { UI3DPlayer component = UI3DPlayer.current; if (component != null) { - // This needs to be here because the method used to draw an entity into the gui (the one we inject into) - // overwrites the yaw (directly before our injection point), so we cannot set it from UI3DPlayer. - component.applyCameraRotation(Minecraft.getMinecraft().getRenderManager()); + UMatrixStack extraMatrixStack = component.applyCamera(Minecraft.getMinecraft().getRenderManager()); + //#if MC>=12000 + //$$ MatrixStack matrixStack = drawContext.getMatrices(); + //#endif + //#if MC>=11400 + //$$ matrixStack.getLast().getMatrix().mul(extraMatrixStack.peek().getModel()); + //$$ matrixStack.getLast().getNormal().mul(extraMatrixStack.peek().getNormal()); + //#else + extraMatrixStack.applyToGlobalState(); + //#endif } } } diff --git a/src/main/java/gg/essential/mixins/transformers/feature/cosmetics/Mixin_UpdateOffscreenPlayers.java b/src/main/java/gg/essential/mixins/transformers/feature/cosmetics/Mixin_UpdateOffscreenPlayers.java index 867bd4a..ece959c 100644 --- a/src/main/java/gg/essential/mixins/transformers/feature/cosmetics/Mixin_UpdateOffscreenPlayers.java +++ b/src/main/java/gg/essential/mixins/transformers/feature/cosmetics/Mixin_UpdateOffscreenPlayers.java @@ -75,7 +75,7 @@ public abstract class Mixin_UpdateOffscreenPlayers { particleSystem.spawn((ModelAnimationState.ParticleEvent) event); } } else if (event instanceof ModelAnimationState.SoundEvent) { - EssentialSoundManager.INSTANCE.playSound(player, ((ModelAnimationState.SoundEvent) event).getEffect()); + EssentialSoundManager.INSTANCE.playSound((ModelAnimationState.SoundEvent) event); } } pendingEvents.clear(); diff --git a/src/main/java/gg/essential/mixins/transformers/feature/particles/Mixin_AddParticleSystemToClientWorld.java b/src/main/java/gg/essential/mixins/transformers/feature/particles/Mixin_AddParticleSystemToClientWorld.java index 7372cf8..9187f25 100644 --- a/src/main/java/gg/essential/mixins/transformers/feature/particles/Mixin_AddParticleSystemToClientWorld.java +++ b/src/main/java/gg/essential/mixins/transformers/feature/particles/Mixin_AddParticleSystemToClientWorld.java @@ -11,10 +11,12 @@ */ package gg.essential.mixins.transformers.feature.particles; +import gg.essential.handlers.EssentialSoundManager; import gg.essential.mixins.ext.client.ParticleSystemHolder; import gg.essential.model.ParticleSystem; import gg.essential.model.backend.minecraft.WorldCollisionProvider; import gg.essential.model.backend.minecraft.WorldLightProvider; +import kotlin.Unit; import kotlin.random.Random; import net.minecraft.client.multiplayer.WorldClient; import org.jetbrains.annotations.NotNull; @@ -27,7 +29,11 @@ public abstract class Mixin_AddParticleSystemToClientWorld implements ParticleSy private final ParticleSystem particleSystem = new ParticleSystem( Random.Default, new WorldCollisionProvider((WorldClient) (Object) this), - new WorldLightProvider((WorldClient) (Object) this) + new WorldLightProvider((WorldClient) (Object) this), + soundEvent -> { + EssentialSoundManager.INSTANCE.playSound(soundEvent); + return Unit.INSTANCE; + } ); @NotNull diff --git a/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java new file mode 100644 index 0000000..045e725 --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java @@ -0,0 +1,31 @@ +/* + * 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.feature.sound; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import paulscode.sound.Library; +import paulscode.sound.Source; + +@Mixin(value = Library.class, remap = false) +public abstract class Mixin_FixPaulscodeChannelAllocation { + // By default this method will re-allocate paused channels as if they were unused. + // This is not desirable because it means that paused in-game sounds may disappear if you play enough sounds in the + // menu. + // This injector fixes that by treating paused channels the same way as playing ones. + @ModifyExpressionValue(method = "getNextChannel", at = @At(value = "INVOKE", target = "Lpaulscode/sound/Source;playing()Z")) + private boolean orPaused(boolean playing, @Local(name = "src") Source src) { + return playing || src.paused(); + } +} diff --git a/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_isRelativeToListener_apply.java b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_isRelativeToListener_apply.java index 81d705d..5f71b33 100644 --- a/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_isRelativeToListener_apply.java +++ b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_isRelativeToListener_apply.java @@ -39,6 +39,17 @@ private void setRelativeToListener(CallbackInfo ci) { } } + @Inject(method = "calculateDistance", at = @At("HEAD"), cancellable = true) + private void fixDistanceCalculationWhenRelativeToListener(CallbackInfo ci) { + if (sourcename.contains(SOUND_RELATIVE_MARKER)) { + float x = position.x; + float y = position.y; + float z = position.z; + distanceFromListener = (float) Math.sqrt(x*x + y*y + z*z); + ci.cancel(); + } + } + @SuppressWarnings("DataFlowIssue") public Mixin_ISoundExt_isRelativeToListener_apply() { super(null, null); } } diff --git a/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundHandler.java b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundHandler.java new file mode 100644 index 0000000..5dc294e --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundHandler.java @@ -0,0 +1,40 @@ +/* + * 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.feature.sound; + +import dev.folomeev.kotgl.matrix.vectors.Vec3; +import gg.essential.mixins.impl.client.audio.SoundSystemExt; +import gg.essential.model.util.Quaternion; +import net.minecraft.client.audio.SoundHandler; +import net.minecraft.client.audio.SoundManager; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(SoundHandler.class) +public abstract class Mixin_SoundSystemExt_SoundHandler implements SoundSystemExt { + + @Shadow @Final private SoundManager sndManager; + + @Nullable + @Override + public Vec3 essential$getListenerPosition() { + return ((SoundSystemExt) this.sndManager).essential$getListenerPosition(); + } + + @Nullable + @Override + public Quaternion essential$getListenerRotation() { + return ((SoundSystemExt) this.sndManager).essential$getListenerRotation(); + } +} diff --git a/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundManager.java b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundManager.java new file mode 100644 index 0000000..80e8588 --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_SoundSystemExt_SoundManager.java @@ -0,0 +1,94 @@ +/* + * 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.feature.sound; + +import dev.folomeev.kotgl.matrix.vectors.Vec3; +import gg.essential.mixins.impl.client.audio.SoundSystemExt; +import gg.essential.model.util.Quaternion; +import net.minecraft.client.audio.SoundManager; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Group; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import static dev.folomeev.kotgl.matrix.vectors.Vectors.vec3; +import static dev.folomeev.kotgl.matrix.vectors.Vectors.vecUnitY; + +//#if MC>=11600 +//$$ import net.minecraft.client.renderer.ActiveRenderInfo; +//#else +import net.minecraft.entity.Entity; +import net.minecraft.entity.player.EntityPlayer; +//#endif + +@Mixin(SoundManager.class) +public abstract class Mixin_SoundSystemExt_SoundManager implements SoundSystemExt { + @Unique + private Vec3 listenerPosition; + + @Unique + private Quaternion listenerRotation; + + @Nullable + @Override + public Vec3 essential$getListenerPosition() { + return listenerPosition; + } + + @Nullable + @Override + public Quaternion essential$getListenerRotation() { + return listenerRotation; + } + + //#if MC>=11600 + //$$ @Inject(method = "updateListener", at = @At("HEAD")) + //$$ private void recordListenerPosition(ActiveRenderInfo info, CallbackInfo ci) { + //$$ if (!info.isValid()) return; + //$$ + //$$ net.minecraft.util.math.vector.Vector3d vec = info.getProjectedView(); + //$$ this.listenerPosition = vec3((float) vec.x, (float) vec.y, (float) vec.z); + //$$ + //$$ net.minecraft.util.math.vector.Quaternion cameraRotMc = info.getRotation(); + //#if MC>=11903 + //$$ this.listenerRotation = new Quaternion(cameraRotMc.x, cameraRotMc.y, cameraRotMc.z, cameraRotMc.w).opposite(); + //#else + //$$ this.listenerRotation = new Quaternion(cameraRotMc.getX(), cameraRotMc.getY(), cameraRotMc.getZ(), cameraRotMc.getW()).opposite(); + //#endif + //$$ } + //#else + @Group(name = "setListener", min = 1) + @Inject(method = "setListener(Lnet/minecraft/entity/player/EntityPlayer;F)V", at = @At("HEAD")) + private void recordListenerPosition(EntityPlayer player, float partialTicks, CallbackInfo ci) { + recordListenerPosition((Entity) player, partialTicks, ci); + } + @Group(name = "setListener", min = 1) + @Inject(method = "setListener(Lnet/minecraft/entity/Entity;F)V", at = @At("HEAD"), remap = false) + @Dynamic("https://github.com/MinecraftForge/MinecraftForge/commit/6f642ba6ceb1978abdd5d63a5e4227f4cd1afa23") + private void recordListenerPosition(Entity player, float partialTicks, CallbackInfo ci) { + if (player == null) return; + + double x = player.prevPosX + (player.posX - player.prevPosX) * partialTicks; + double y = player.prevPosY + (player.posY - player.prevPosY) * partialTicks + player.getEyeHeight(); + double z = player.prevPosZ + (player.posZ - player.prevPosZ) * partialTicks; + this.listenerPosition = vec3((float) x, (float) y, (float) z); + + net.minecraft.util.math.Vec3d lookAtMc = player.getLook(partialTicks); + Vec3 lookAt = vec3((float) lookAtMc.x, (float) lookAtMc.y, (float) lookAtMc.z); + this.listenerRotation = Quaternion.Companion.fromLookAt(lookAt, vecUnitY()); + } + //#endif +} diff --git a/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java new file mode 100644 index 0000000..dd75867 --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java @@ -0,0 +1,170 @@ +/* + * 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.feature.sound; + +import com.google.common.collect.Multimap; +import gg.essential.mixins.impl.client.audio.ISoundExt; +import net.minecraft.client.Minecraft; +import net.minecraft.client.audio.ISound; +import net.minecraft.client.audio.ITickableSound; +import net.minecraft.client.audio.SoundManager; +import net.minecraft.entity.Entity; +import net.minecraft.util.SoundCategory; +import org.spongepowered.asm.mixin.Dynamic; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Coerce; +import org.spongepowered.asm.mixin.injection.Group; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import paulscode.sound.SoundSystem; + +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +//#if MC>=11200 +//#else +//$$ import net.minecraft.client.audio.SoundPoolEntry; +//#endif + +@Mixin(SoundManager.class) +public abstract class Mixin_UpdateWhilePaused { + // SoundSystem makes it impossible to know if a sound really has already stopped, or if it play command is simply + // still in the command queue.. + // MC works around this by assuming that any sound queued within the last second is hasn't stopped yet; it keeps + // track of this via the playingSoundsStopTime map, however it does so in ticks, and those don't advance while the + // game is paused, so they're no good for use in e.g. the Wardrobe. + // This may serves a similar purpose but stores millis since epoch of the time it was queued instead. + // Special value 0 indicates that it's been more than a second (so we can short-circuit some logic). + // Using a weak map to make sure we don't leak memory because MC might clean up our sounds as well. + @Unique private final Map commandQueueTime = new WeakHashMap<>(); + + // MC doesn't tick sounds when the game is paused, as such we can't cancel them or update their volume, both of + // which we need to be able to do. + // So instead of using MC's update method, we'll update our sounds from `setListener`, which is called every frame + // right before rendering. + @Group(name = "updateEssentialSounds", min = 1) + @Inject(method = "setListener", at = @At("RETURN")) + private void updateEssentialSounds(CallbackInfo ci) { + if (!this.loaded) return; + if (!Minecraft.getMinecraft().isGamePaused()) return; + + Iterator iterator = this.tickableSounds.iterator(); + while (iterator.hasNext()) { + ITickableSound sound = iterator.next(); + if (!(sound instanceof ISoundExt)) { + continue; + } + ISoundExt soundExt = (ISoundExt) sound; + String id = this.invPlayingSounds.get(sound); + + sound.update(); + + // Also have to clean up finished sounds while the game is paused, otherwise we'll relatively quickly run into + // the channel limit + boolean playing = this.soundSystem.playing(id); + // Hack because SoundSystem makes it impossible to know if the sound really has already stopped, or the play + // command is simply still in the queue.. + if (!playing) { + Long queueTime = this.commandQueueTime.get(soundExt); + if (queueTime == null) { + // First time we're seeing this sound, assume newly queued + this.commandQueueTime.put(soundExt, System.currentTimeMillis()); + } else if (queueTime == 0) { + // Known and been existing for a while, don't need to bother checking the current time + } else { + // Fresh, check how old exactly + if (queueTime + 1000 < System.currentTimeMillis()) { + // still fresh, assume it's merely stuck in queue + playing = true; + } else { + // older than a second now, update stored value so we can short-circuit in the future + this.commandQueueTime.put(soundExt, 0L); + } + } + } + + if (playing && !sound.isDonePlaying()) { + //#if MC>=11200 + this.soundSystem.setVolume(id, this.getClampedVolume(sound)); + this.soundSystem.setPitch(id, this.getClampedPitch(sound)); + //#else + //$$ this.soundSystem.setVolume(id, this.getNormalizedVolume(sound, this.playingSoundPoolEntries.get(sound), soundExt.essential$createAccessor().getSoundCategory())); + //$$ this.soundSystem.setPitch(id, this.getNormalizedPitch(sound, this.playingSoundPoolEntries.get(sound))); + //#endif + this.soundSystem.setPosition(id, sound.getXPosF(), sound.getYPosF(), sound.getZPosF()); + } else { + this.stopSound(sound); + + // Also need to immediately clean it up, because MC will only do that while the game is not paused + this.playingSounds.remove(id); + this.soundSystem.removeSource(id); + this.playingSoundsStopTime.remove(id); + this.commandQueueTime.remove(sound); + //#if MC<11200 + //$$ this.playingSoundPoolEntries.remove(sound); + //#endif + + try { + //#if MC>=11200 + this.categorySounds.remove(sound.getCategory(), id); + //#else + //$$ this.categorySounds.remove(soundExt.essential$createAccessor().getSoundCategory(), id); + //#endif + } catch (RuntimeException e) { // idk, ask Mojang + } + + iterator.remove(); + } + } + } + + @Group(name = "updateEssentialSounds", min = 1) + @Inject(method = "setListener(Lnet/minecraft/entity/Entity;F)V", at = @At("RETURN"), remap = false) + @Dynamic("https://github.com/MinecraftForge/MinecraftForge/commit/6f642ba6ceb1978abdd5d63a5e4227f4cd1afa23") + private void updateEssentialSounds(Entity player, float partialTicks, CallbackInfo ci) { + updateEssentialSounds(ci); + } + + // Hack to get access to `sndSystem` which has package private field type + @Unique private SoundSystem soundSystem; + @Inject(method = "access$102", at = @At("HEAD")) + private static void captureSoundSystem(SoundManager self, @Coerce SoundSystem soundSystem, CallbackInfoReturnable ci) { + ((Mixin_UpdateWhilePaused) (Object) self).soundSystem = soundSystem; + } + + @Shadow private boolean loaded; + @Shadow @Final private Map playingSounds; + @Shadow @Final private Map invPlayingSounds; + @Shadow @Final private List tickableSounds; + @Shadow @Final private Map playingSoundsStopTime; + @Shadow @Final private Multimap categorySounds; + //#if MC>=11200 + //#else + //$$ @Shadow private Map playingSoundPoolEntries; + //#endif + + @Shadow public abstract void stopSound(ISound sound); + //#if MC>=11200 + @Shadow protected abstract float getClampedVolume(ISound soundIn); + @Shadow protected abstract float getClampedPitch(ISound soundIn); + //#else + //$$ @Shadow protected abstract float getNormalizedVolume(ISound par1, SoundPoolEntry par2, SoundCategory par3); + //$$ @Shadow protected abstract float getNormalizedPitch(ISound par1, SoundPoolEntry par2); + //#endif +} diff --git a/src/main/java/gg/essential/model/PlayerMolangQuery.java b/src/main/java/gg/essential/model/PlayerMolangQuery.java index 94d2711..ea879cc 100644 --- a/src/main/java/gg/essential/model/PlayerMolangQuery.java +++ b/src/main/java/gg/essential/model/PlayerMolangQuery.java @@ -20,6 +20,7 @@ import gg.essential.event.render.RenderTickEvent; import gg.essential.model.molang.MolangQueryEntity; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import static dev.folomeev.kotgl.matrix.vectors.Vectors.vec3; import static dev.folomeev.kotgl.matrix.vectors.Vectors.vecUnitY; @@ -70,6 +71,12 @@ public ParticleSystem.Locator getLocator() { return this; } + @Nullable + @Override + public ParticleSystem.Locator getParent() { + return null; + } + @Override public boolean isValid() { //#if MC>=11700 diff --git a/src/main/java/gg/essential/network/connectionmanager/cosmetics/CosmeticsManager.java b/src/main/java/gg/essential/network/connectionmanager/cosmetics/CosmeticsManager.java index 00153fd..bd8f81b 100644 --- a/src/main/java/gg/essential/network/connectionmanager/cosmetics/CosmeticsManager.java +++ b/src/main/java/gg/essential/network/connectionmanager/cosmetics/CosmeticsManager.java @@ -424,13 +424,13 @@ public void setOwnCosmeticVisibility(boolean notification, final boolean visible if (GuiUtil.INSTANCE.openedScreen() == null) { // Show a notification when we're not in any menu, so it's less intrusive sendTosNotification(() -> { - Essential.getInstance().getOverlayManager().pushModal( + GuiUtil.INSTANCE.pushModal( new TOSModal(false, true, () -> Unit.INSTANCE, () -> Unit.INSTANCE) ); return Unit.INSTANCE; }); } else { - Essential.getInstance().getOverlayManager().pushModal( + GuiUtil.INSTANCE.pushModal( new TOSModal(false, true, () -> Unit.INSTANCE, () -> Unit.INSTANCE) ); } diff --git a/src/main/kotlin/gg/essential/gui/common/CosmeticHoverOutlineEffect.kt b/src/main/kotlin/gg/essential/gui/common/CosmeticHoverOutlineEffect.kt index 9a7e66d..9647f7c 100644 --- a/src/main/kotlin/gg/essential/gui/common/CosmeticHoverOutlineEffect.kt +++ b/src/main/kotlin/gg/essential/gui/common/CosmeticHoverOutlineEffect.kt @@ -13,6 +13,7 @@ package gg.essential.gui.common import gg.essential.elementa.effects.Effect import gg.essential.elementa.effects.ScissorEffect +import gg.essential.elementa.utils.withAlpha import gg.essential.gui.elementa.state.v2.State import gg.essential.gui.elementa.state.v2.mutableStateOf import gg.essential.network.cosmetics.Cosmetic @@ -29,9 +30,13 @@ import gg.essential.util.GlFrameBuffer import net.minecraft.client.renderer.GlStateManager import org.lwjgl.BufferUtils import org.lwjgl.opengl.GL11 +import java.awt.Color import kotlin.math.roundToInt -class CosmeticHoverOutlineEffect(private val outlineCosmetic: State>) : Effect() { +class CosmeticHoverOutlineEffect( + private val backgroundColor: Color, + private val outlineCosmetic: State>, +) : Effect() { private val mc = UMinecraft.getMinecraft() @@ -48,7 +53,7 @@ class CosmeticHoverOutlineEffect(private val outlineCosmetic: State = BasicState(null), renderNameTag: State = BasicState(false), sounds: StateV2 = stateOf(false), + soundsVolume: StateV2 = stateOf(1f), ) : UI3DPlayer( draggable = draggable, hideNameTags = renderNameTag.map { !it }, player = null, // to be created by [initPlayer] profile = profile, sounds = sounds, + soundsVolume = soundsVolume, ) { private val mcClient = UMinecraft.getMinecraft() @@ -160,6 +162,7 @@ class EmulatedUI3DPlayer( Random(0), PlaneCollisionProvider(vec3(0f, posY.toFloat(), 0f), vecUnitY()), LightProvider.FullBright, + ::playSound, ) //#if MC>=12002 @@ -242,6 +245,16 @@ class EmulatedUI3DPlayer( } } + override fun close() { + super.close() + + //#if MC>=11700 + //$$ player?.health = 0f + //#else + player?.isDead = true + //#endif + } + override fun draw(matrixStack: UMatrixStack) { if (errored) { return super.draw(matrixStack) diff --git a/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt b/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt index ef01e4a..9862c52 100644 --- a/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt +++ b/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt @@ -14,8 +14,8 @@ package gg.essential.gui.common import com.mojang.authlib.GameProfile import com.mojang.authlib.minecraft.MinecraftProfileTexture import dev.folomeev.kotgl.matrix.vectors.mutables.minus -import dev.folomeev.kotgl.matrix.vectors.mutables.mutableVec3 import dev.folomeev.kotgl.matrix.vectors.mutables.plus +import dev.folomeev.kotgl.matrix.vectors.vec3 import dev.folomeev.kotgl.matrix.vectors.vecUnitX import dev.folomeev.kotgl.matrix.vectors.vecUnitY import dev.folomeev.kotgl.matrix.vectors.vecUnitZ @@ -37,6 +37,7 @@ import gg.essential.elementa.dsl.pixels import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.State import gg.essential.event.render.RenderTickEvent +import gg.essential.gui.elementa.state.v2.mutableStateOf import gg.essential.gui.elementa.state.v2.stateOf import gg.essential.handlers.EssentialSoundManager import gg.essential.gui.elementa.state.v2.State as StateV2 @@ -63,7 +64,6 @@ import gg.essential.model.molang.MolangQueryEntity import gg.essential.model.util.PlayerPoseManager import gg.essential.model.util.Quaternion import gg.essential.model.util.rotateBy -import gg.essential.model.util.rotateSelfBy import gg.essential.network.connectionmanager.cosmetics.AssetLoader import gg.essential.model.util.UMatrixStack as CMatrixStack import gg.essential.universal.UGraphics @@ -78,6 +78,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.future.asDeferred import kotlinx.coroutines.launch import kotlinx.coroutines.plus @@ -112,7 +113,7 @@ import kotlin.random.Random //#endif //#if MC==11602 -//$$ import gg.essential.mixins.impl.util.math.Matrix4fExt +//$$ import dev.folomeev.kotgl.matrix.matrices.mutables.set //$$ import gg.essential.model.util.toMat4 //$$ import net.minecraft.client.renderer.GLAllocation //#endif @@ -136,9 +137,13 @@ open class UI3DPlayer( val draggable: State, player: EntityPlayer?, val sounds: StateV2 = stateOf(false), + soundsVolume: StateV2 = stateOf(1f), private val profile: State = BasicState(player?.gameProfile?.wrapped()), ) : UIComponent() { + private val closed = mutableStateOf(false) + val soundsVolume: StateV2 = StateV2 { if (closed()) 0f else soundsVolume() } + var player: EntityPlayer? = player set(value) { field = value @@ -175,6 +180,15 @@ open class UI3DPlayer( private var rotationAngleHorizontal = 30f private var rotationAngleVerticalFront = -10f private var rotationAngleVerticalSide = 0f + private val rotationAngleCamera: PerspectiveCamera + get() { + val target = vec3(0f, PLAYER_MODEL_HEIGHT / 2, 0f) + val rotHorizontal = Quaternion.fromAxisAngle(vecUnitY(), (180 - rotationAngleHorizontal) / 180f * PI.toFloat()) + val rotVertSide = Quaternion.fromAxisAngle(vecUnitZ(), rotationAngleVerticalSide / 180f * PI.toFloat()) + val rotVertFront = Quaternion.fromAxisAngle(vecUnitX(), rotationAngleVerticalFront / 180f * PI.toFloat()) + val cameraOffset = rotHorizontal * rotVertSide * rotVertFront * vec3(0f, 0f, ORTHOGRAPHIC_CAMERA_DISTANCE) + return PerspectiveCamera(target.plus(cameraOffset), target, 0f) + } private var prevX = -1f private var prevY = -1f @@ -188,6 +202,9 @@ open class UI3DPlayer( */ var perspectiveCamera: PerspectiveCamera? = null + val camera: PerspectiveCamera + get() = perspectiveCamera ?: rotationAngleCamera + protected var errored = false init { if (ModLoaderUtil.isModLoaded("figura")) { @@ -243,6 +260,12 @@ open class UI3DPlayer( prevY = mouseY } + open fun close() { + closed.set(true) + + fallbackPlayer.orNull?.close() + } + override fun animationFrame() { super.animationFrame() @@ -263,44 +286,37 @@ open class UI3DPlayer( } private fun drawWithOrthographicProjection(stack: UMatrixStack) { - var width = getWidth() - var height = getHeight() + // Center player within component + stack.translate(getLeft() + getWidth() / 2, getTop() + getHeight() / 2, 450f) - val widthPadding = width * PADDING_FACTOR - val heightPadding = height * PADDING_FACTOR + // Limit player size such that it'll always fit within our bounds (with a small bit of padding for style) + val playerHeightInPixels = min(getWidth() * 2, getHeight()) * (1 - PADDING_FACTOR) - width -= widthPadding - height -= heightPadding + // Scale from screen space where the player is 200px high and the y axis points down + // to world space where they are 2m high and the y axis points up + val scale = playerHeightInPixels / PLAYER_MODEL_HEIGHT + stack.scale(scale, -scale, scale) - val x = getLeft().toDouble() - val y = getTop().toDouble() - (heightPadding / 2.0f) + // With orthographic projection, we place the camera very far away, see [ORTHOGRAPHIC_CAMERA_DISTANCE]. + // This will however cause issues with the near/far plane because the resulting geometry will lie far + // outside those bounds. To counteract this, we offset the global state in the opposite direction. + // That way, the vertices that end up in the BufferBuilder (where MC will sort them) are still relative + // to the really distant camera (and therefore sorted correctly), but once OpenGL combines them with the + // global ModelViewMatrix, they'll end up close to zero / inside the near-far plane bounds again. + stack.translate(0f, 0f, ORTHOGRAPHIC_CAMERA_DISTANCE) - val boundingHeight = min(width * 2, height) - val scale = boundingHeight * MAGIC_HEIGHT_SCALING_FACTOR - - stack.translate(0.0, -getHeight() / 4.0, 0.0) - stack.translate(0f, 0f, 500f) - - stack.translate( - x + getWidth() / 2, - y + getHeight() / 2, - 0.0 - ) - - stack.translate(0f, getHeight() / 4, -50f) - stack.rotate(rotationAngleVerticalFront, 1.0f, 0.0f, 0.0f) - stack.rotate(rotationAngleVerticalSide, 0.0f, 0.0f, 1.0f) - stack.rotate(rotationAngleHorizontal, 0.0f, 1.0f, 0.0f) - stack.translate(0f, getHeight() / 2, -50f) + // Need this for normals to behave with orthographic projection + //#if MC<11700 + GlStateManager.enableRescaleNormal() + //#endif - // Note the extra posY shift in this call. If the player model is bounded by - // height, it will automatically be drawn in the center. If it is bounded by width, - // however, we need to vertically shift the model in order to get it vertically - // centered. The expression evaluates to zero when the model is bounded by height, - // and negative when bounded by width - stack.translate(0.0, -(height - boundingHeight) / 2.0, 0.0) + stack.runWithGlobalState { + drawPlayer() + } - drawPlayer(stack, scale.toInt()) + //#if MC<11700 + GlStateManager.disableRescaleNormal() + //#endif } private fun drawWithPerspectiveProjection(stack: UMatrixStack, camera: PerspectiveCamera) { @@ -321,12 +337,6 @@ open class UI3DPlayer( val scaleX = width / windowWidth val scaleY = height / windowHeight - val modelViewMatrix = camera.createModelViewMatrix().toUC() - // Undo all the flipping/rotating which GuiInventory.drawEntityOnScreen applies - modelViewMatrix.scale(-1f, -1f, -1f) - // Undo the Z offset from GuiInventory.drawEntityOnScreen - modelViewMatrix.translate(0.0, 0.0, -50.0) - // Compute our projection matrix (perspective projection but scaled and offset to fit into this component) val projectionMatrix = UMatrixStack() projectionMatrix.translate( @@ -361,8 +371,8 @@ open class UI3DPlayer( //#endif isRenderingPerspective = true - modelViewMatrix.runReplacingGlobalState { - drawPlayer(UMatrixStack(), 1) + UMatrixStack().runReplacingGlobalState { + drawPlayer() } isRenderingPerspective = false @@ -395,21 +405,14 @@ open class UI3DPlayer( return Pair(vec.x, vec.y) } - private fun drawPlayer(stack: UMatrixStack, scale: Int) { + private fun drawPlayer() { bindWhiteLightMapTexture() if (errored) { if (fallbackErrored) return try { - // GuiInventory does this and UI3DPlayer has been built around it, so we need to emulate it - stack.translate(0f, 0f, 50f) - stack.scale(-scale.toFloat(), scale.toFloat(), scale.toFloat()) - stack.rotate(180f, 0f, 0f, 1f) - - stack.runWithGlobalState { - doDrawFallbackPlayer(CMatrixStack()) - doDrawParticles(CMatrixStack(), fallbackPlayer.value.particleSystem) - } + doDrawFallbackPlayer() + doDrawParticles(fallbackPlayer.value.particleSystem) } catch (e: Exception) { Essential.logger.error("Error rendering fallback player", e) fallbackErrored = true @@ -419,7 +422,7 @@ open class UI3DPlayer( try { current = this - doDrawPlayer(stack, scale) + doDrawPlayer() // An emulated player has its own dedicated particle system which we need to manually update here (that is // after we have rendered the player, which updates any locators). @@ -431,13 +434,7 @@ open class UI3DPlayer( // in the UI (and with the correct billboard facing), we need to render it manually here. val particleSystem = dedicatedParticleSystem ?: (player?.world as? ParticleSystemHolder)?.particleSystem if (particleSystem != null) { - // GuiInventory does this and UI3DPlayer has been built around it, so we need to emulate it - stack.translate(0f, 0f, 50f) - stack.scale(-scale.toFloat(), scale.toFloat(), scale.toFloat()) - stack.rotate(180f, 0f, 0f, 1f) - stack.runWithGlobalState { - doDrawParticles(CMatrixStack(), particleSystem) - } + doDrawParticles(particleSystem) } } catch (e: Exception) { Essential.logger.error("Error rendering emulated player", e) @@ -447,7 +444,7 @@ open class UI3DPlayer( } } - private fun doDrawPlayer(stack: UMatrixStack, scale: Int) { + private fun doDrawPlayer() { val p = player ?: return val renderManager = Minecraft.getMinecraft().renderManager @@ -466,7 +463,31 @@ open class UI3DPlayer( UGraphics.directColor3f(1f, 1f, 1f) UGraphics.enableDepth() - stack.runWithGlobalState { + val stack = CMatrixStack() + // Undo parts of the flipping/rotating which GuiInventory.drawEntityOnScreen applies + // (specifically the parts that apply to the global matrix stack) + // (remainder is dealt with in [applyCamera]) + //#if MC>=11400 + //$$ stack.scale(1f, 1f, -1f) + //#else + stack.scale(1f, -1f, 1f) + //#endif + // Undo the Z offset from GuiInventory.drawEntityOnScreen + stack.translate(0f, 0f, -50f) + + //#if MC>=12000 + //$$ val context = + //$$ MinecraftClient.getInstance().let { mc -> DrawContext(mc, mc.bufferBuilders.entityVertexConsumers) } + //$$ context.matrices.multiplyPositionMatrix(stack.toUC().peek().model) + //$$ run { + //#else + stack.toUC().runWithGlobalState { + //#endif + //#if MC>=12005 + //$$ val scale = 1f + //#else + val scale = 1 + //#endif //#if MC>=12002 //$$ // As of 1.20.2, the simple vanilla method applies glScissor which we don't want (that's Elementa's job) //$$ // so we'll instead have to call the other overload and do all the save&restore ourselves. @@ -480,12 +501,7 @@ open class UI3DPlayer( //$$ p.bodyYaw = 180f //$$ p.headYaw = 180f //$$ p.prevHeadYaw = 180f - //$$ val context = - //$$ MinecraftClient.getInstance().let { mc -> DrawContext(mc, mc.bufferBuilders.entityVertexConsumers) } //$$ val rotation = Quaternionf().rotateZ(Math.PI.toFloat()) - //#if MC>=12005 - //$$ val scale = scale.toFloat() - //#endif //$$ InventoryScreen.drawEntity(context, 0f, 0f, scale, Vector3f(), rotation, Quaternionf(), p) //$$ p.pitch = orgPitch //$$ p.yaw = orgYaw @@ -495,7 +511,7 @@ open class UI3DPlayer( //#else GuiInventory.drawEntityOnScreen( //#if MC>=12000 - //$$ MinecraftClient.getInstance().let { mc -> DrawContext(mc, mc.bufferBuilders.entityVertexConsumers) }, + //$$ context, //#elseif MC>=11904 //$$ MatrixStack(), //#endif @@ -509,23 +525,8 @@ open class UI3DPlayer( UGraphics.disableDepth() } - fun applyCameraRotation(renderManager: RenderManager) { - //#if MC>=11700 - //$$ val matrix = Matrix4f() - //$$ matrix.loadIdentity() - //#if MC>=11903 - //$$ matrix.scale(1f, -1f, 1f) - //$$ matrix.rotate(RotationAxis.POSITIVE_Y.rotationDegrees(135f)) - //#else - //$$ matrix.multiply(Matrix4f.scale(1f, -1f, 1f)) - //$$ matrix.multiply(Vec3f.POSITIVE_Y.getDegreesQuaternion(135f)) - //#endif - //$$ DiffuseLighting.disableForLevel( - //#if MC<12005 - //$$ matrix - //#endif - //$$ ) - //#endif + fun applyCamera(renderManager: RenderManager): UMatrixStack { + setupPlayerLight() //#if MC>=11400 //$$ renderManager.cameraOrientation = renderManager.info.rotation @@ -533,19 +534,30 @@ open class UI3DPlayer( renderManager.playerViewX = -rotationAngleVerticalFront renderManager.playerViewY = rotationAngleHorizontal - 180 //#endif - } - private fun doDrawFallbackPlayer(stack: CMatrixStack) { - // Need this for normals to behave with orthographic projection - //#if MC<11700 - GlStateManager.enableRescaleNormal() - //#endif + val stack = CMatrixStack() + // Undo remainder of the rotating which GuiInventory.drawEntityOnScreen applies //#if MC>=11400 - //$$ val matrix = Matrix4f() - //#if MC>=11700 - //$$ matrix.loadIdentity() - //#else + //$$ stack.scale(-1f, -1f, 1f) + //#endif + + // Apply our camera + stack.multiply(camera.createModelViewMatrix()) + + //#if MC<11400 + // GuiInventory.drawEntityOnScreen uses entity yaw of 0, which means pointing towards positive z, we want it + // pointing towards negative z though, so rotate by 180 + stack.scale(-1f, 1f, -1f) + //#endif + + return stack.toUC() + } + + private fun setupPlayerLight() { + val stack = CMatrixStack() + + //#if MC==11602 //$$ // 1.16 is a weird mix of legacy gl stack and explicit pojo stack //$$ // The lighting setup method already ignores the gl stack but the renderer doesn't yet take the pojo stack //$$ // stack as a separate input, so one actually has to combine the two stacks when setting lighting for it @@ -554,30 +566,36 @@ open class UI3DPlayer( //$$ val buf = GLAllocation.createDirectFloatBuffer(16) //$$ GL11.glGetFloatv(GL11.GL_MODELVIEW_MATRIX, buf) //$$ val array = FloatArray(16).apply { buf.get(this) } - //$$ (matrix as Matrix4fExt).kotgl = array.toMat4() - //#endif - //#if MC>=11903 - //$$ matrix.rotate(RotationAxis.POSITIVE_Y.rotationDegrees(135f)) - //#else - //$$ matrix.mul(Vector3f.YP.rotationDegrees(135f)) + //$$ stack.peek().model.set(array.toMat4()) //#endif - //$$ // FIXME preprocessor bug: this looks like it should be automatically mapped - //#if MC>=11700 - //$$ DiffuseLighting.disableForLevel( - //#if MC<12005 - //$$ matrix - //#endif + + // Apply our camera + stack.rotate(camera.rotation.invert()) + + // Lighting as per GuiInventory + stack.rotate(135f, 0f, 1f, 0f, degrees = true) + + //#if MC>=11400 + //$$ val matrix = stack.toUC().peek().model + //#if MC>=12005 + //$$ RenderSystem.setupLevelDiffuseLighting( + //$$ Vector3f(0.2f, 1.0f, -0.7f).normalize().mulDirection(matrix), + //$$ Vector3f(-0.2f, 1.0f, 0.7f).normalize().mulDirection(matrix) //$$ ) + //#elseif MC>=11700 + //$$ // FIXME preprocessor bug: this looks like it should be automatically mapped + //$$ DiffuseLighting.disableForLevel(matrix) //#else //$$ RenderHelper.setupLevelDiffuseLighting(matrix) //#endif //#else - // Lighting as per GuiInventory - GlStateManager.rotate(135f, 0f, 1f, 0f) - RenderHelper.enableStandardItemLighting() - GlStateManager.rotate(-135f, 0f, 1f, 0f) + stack.toUC().runWithGlobalState { + RenderHelper.enableStandardItemLighting() + } //#endif + } + private fun doDrawFallbackPlayer() { //#if MC>=11400 //$$ val immediate = Minecraft.getInstance().renderTypeBuffers.bufferSource //$$ val vertexConsumerProvider = MinecraftRenderBackend.VertexConsumerProvider(immediate, 0xf000f0) @@ -586,8 +604,9 @@ open class UI3DPlayer( UGraphics.enableDepth() //#endif - // playerViewY is set to 180 by GuiInventory - stack.rotate(180f, 0f, 1f, 0f, degrees = true) + setupPlayerLight() + + val stack = camera.createModelViewMatrix() // See RenderLivingBase.prepareScale stack.scale(-1f, -1f, 1f) @@ -609,41 +628,27 @@ open class UI3DPlayer( //#else RenderHelper.disableStandardItemLighting() //#endif - - //#if MC<11700 - GlStateManager.disableRescaleNormal() - //#endif } - private fun doDrawParticles(stack: CMatrixStack, particleSystem: ParticleSystem) { + private fun doDrawParticles(particleSystem: ParticleSystem) { val vertexConsumerProvider = MinecraftRenderBackend.ParticleVertexConsumerProvider() UGraphics.enableDepth() bindWhiteLightMapTexture() + val stack = camera.createModelViewMatrix() + // The current stack has the player (and thereby implicitly also the world) oriented towards the camera // but the particle system expects absolute coordinates, so we need to offset the stack accordingly. - val renderedRotation = Quaternion.fromAxisAngle(vecUnitY(), PI.toFloat()) val realRotation = player.takeUnless { errored }?.let { PlayerMolangQuery(it).rotation } ?: Quaternion.Identity - stack.rotate(realRotation.invert() * renderedRotation) + stack.rotate(realRotation.invert()) // The current stack has the player at the origin, but the player isn't really at the world origin, // so we need to offset the stack accordingly. val realPosition = player.takeUnless { errored }?.let { PlayerMolangQuery(it).position } ?: vecZero() stack.translate(vecZero().minus(realPosition)) - val camera = perspectiveCamera ?: run { - var rotation = Quaternion.Identity - // The signs on these are a bit tricky due to all the flipping that goes on in UI3DPlayer and GuiInventory. - rotation *= Quaternion.fromAxisAngle(vecUnitY(), -rotationAngleHorizontal / 180f * PI.toFloat()) - rotation *= Quaternion.fromAxisAngle(vecUnitZ(), -rotationAngleVerticalSide / 180f * PI.toFloat()) - rotation *= Quaternion.fromAxisAngle(vecUnitX(), -rotationAngleVerticalFront / 180f * PI.toFloat()) - // We fake orthographic view by just putting the camera far away. We don't need to care about the actual - // position of the pivot point because that will be negligible compared to the orthographic offset. - val pos = mutableVec3(0f, 0f, -10000f) - pos.rotateSelfBy(rotation) - PerspectiveCamera(pos, vecZero(), 0f) - } + val camera = perspectiveCamera ?: rotationAngleCamera val cameraPos = camera.camera.rotateBy(realRotation).plus(realPosition) particleSystem.render(stack, cameraPos, realRotation * camera.rotation, vertexConsumerProvider) @@ -678,13 +683,13 @@ open class UI3DPlayer( private var playerModel: ModelInstance = ModelInstance(PlayerModel.steveBedrockModel, entity, subject.animationTargets) {} private val poseManager = PlayerPoseManager(entity) - val particleSystem = ParticleSystem(Random(0), PlaneCollisionProvider.PlaneXZ, LightProvider.FullBright) + val particleSystem = ParticleSystem(Random(0), PlaneCollisionProvider.PlaneXZ, LightProvider.FullBright, ::playSound) private var liveCosmeticsSource: CosmeticsSource? = null private val cosmeticsSource: CosmeticsSource get() = this@UI3DPlayer.cosmeticsSource ?: liveCosmeticsSource ?: CosmeticsSource.EMPTY private var cosmetics: Map = emptyMap() - val wearablesManager = WearablesManager(entity, subject.animationTargets) { _, _, -> } + val wearablesManager = WearablesManager(MinecraftRenderBackend, entity, subject.animationTargets) { _, _, -> } private var currentOutfitUpdate: Job? = null @@ -821,10 +826,6 @@ open class UI3DPlayer( ) playerModel.render(stack, vertexConsumerProvider, playerModel.model.rootBone, renderMetadata) - - wearablesManager.render(stack, vertexConsumerProvider, pose, skin) - wearablesManager.renderForHoverOutline(stack, vertexConsumerProvider, pose, skin) - if (!thirdPartyCapeErrored && CosmeticSlot.CAPE !in cosmetics) { try { renderThirdPartyCape(stack, vertexConsumerProvider, renderMetadata) @@ -834,14 +835,13 @@ open class UI3DPlayer( } } + wearablesManager.render(stack, vertexConsumerProvider, pose, skin) + wearablesManager.renderForHoverOutline(stack, vertexConsumerProvider, pose, skin) + wearablesManager.collectEvents { event -> when (event) { is ModelAnimationState.ParticleEvent -> particleSystem.spawn(event) - is ModelAnimationState.SoundEvent -> { - if (sounds.getUntracked()) { - EssentialSoundManager.playSound(null, event.effect) - } - } + is ModelAnimationState.SoundEvent -> playSound(event) } } @@ -886,14 +886,26 @@ open class UI3DPlayer( entity.lifeTime = newLifeTime } + + fun close() { + scope.cancel() + entity.isValid = false + } + } + + protected fun playSound(event: ModelAnimationState.SoundEvent) { + if (sounds.getUntracked()) { + EssentialSoundManager.playSound(event, forceGlobal = true, volume = soundsVolume) + } } private data class MolangQueryEntityImpl( override var lifeTime: Float, override var modifiedDistanceMoved: Float, override var modifiedMoveSpeed: Float - ) : MolangQueryEntity { - override val locator: ParticleSystem.Locator = ParticleSystem.Locator.Zero + ) : MolangQueryEntity, ParticleSystem.Locator by ParticleSystem.Locator.Zero { + override val locator: ParticleSystem.Locator = this + override var isValid: Boolean = true } companion object { @@ -901,6 +913,12 @@ open class UI3DPlayer( private const val playerWidth = 0.6F private const val MAGIC_HEIGHT_SCALING_FACTOR = 0.525 + private const val PLAYER_MODEL_HEIGHT = 1 / MAGIC_HEIGHT_SCALING_FACTOR.toFloat() + + // We put the logical camera far away such that any facing-towards-camera logic (e.g. billboard particles, + // transparency sorting, etc.) will behave close enough to correctly that we don't need to special case + // orthographic projection in all of them. + private const val ORTHOGRAPHIC_CAMERA_DISTANCE = 100f // Padding constant in terms of percentage of additional width and height. // This accounts for both the extra space needed for rotation, as well as diff --git a/src/main/kotlin/gg/essential/gui/image/EssentialAssetImageFactory.kt b/src/main/kotlin/gg/essential/gui/image/EssentialAssetImageFactory.kt index 100bb2d..ba70f40 100644 --- a/src/main/kotlin/gg/essential/gui/image/EssentialAssetImageFactory.kt +++ b/src/main/kotlin/gg/essential/gui/image/EssentialAssetImageFactory.kt @@ -28,8 +28,6 @@ data class EssentialAssetImageFactory( ) : ImageFactory() { private val assetLoader = Essential.getInstance().connectionManager.cosmeticsManager.assetLoader - override val supportsCaching: Boolean = true - fun primeCache(cachePriority: AssetLoader.Priority) { getCachedImage(cachePriority) } diff --git a/src/main/kotlin/gg/essential/gui/overlay/EphemeralLayer.kt b/src/main/kotlin/gg/essential/gui/overlay/EphemeralLayer.kt index 6262402..db8ae97 100644 --- a/src/main/kotlin/gg/essential/gui/overlay/EphemeralLayer.kt +++ b/src/main/kotlin/gg/essential/gui/overlay/EphemeralLayer.kt @@ -11,8 +11,6 @@ */ package gg.essential.gui.overlay -import gg.essential.gui.common.modal.Modal - /** * A layer which will automatically be disposed of when it no longer has any content. */ @@ -23,23 +21,4 @@ class EphemeralLayer(priority: LayerPriority) : Layer(priority) { * again, at which point the callback will be invoked again. */ var onClose: () -> Unit = {} - - /** - * Queues a [Modal] to be displayed once the layer becomes empty (via [onClose]). - * If the layer is already empty, displays the modal immediately. - * - * Note that this will delay the current [onClose] callback until both modals are closed. - */ - fun queueModal(modal: Modal) { - if (window.children.isEmpty()) { - pushModal(modal) - return - } - - val orgOnClose = onClose - onClose = { - onClose = orgOnClose - pushModal(modal) - } - } } \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/gui/overlay/ModalManager.kt b/src/main/kotlin/gg/essential/gui/overlay/ModalManager.kt new file mode 100644 index 0000000..ed1f762 --- /dev/null +++ b/src/main/kotlin/gg/essential/gui/overlay/ModalManager.kt @@ -0,0 +1,24 @@ +/* + * 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.overlay + +import gg.essential.gui.common.modal.Modal + +/** + * Queues [Modal]s to be displayed on a [Layer]. + */ +interface ModalManager { + /* + * Queues the given modal to be displayed after existing modals (or immediately if there are no active modals). + */ + fun queueModal(modal: Modal) +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/gui/overlay/ModalManagerImpl.kt b/src/main/kotlin/gg/essential/gui/overlay/ModalManagerImpl.kt new file mode 100644 index 0000000..bb24354 --- /dev/null +++ b/src/main/kotlin/gg/essential/gui/overlay/ModalManagerImpl.kt @@ -0,0 +1,58 @@ +/* + * 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.overlay + +import gg.essential.gui.common.modal.Modal + +/** + * Queues modals to be displayed onto a managed [EphemeralLayer]. + */ +class ModalManagerImpl(private val overlayManager: OverlayManager) : ModalManager { + private val modalQueue = mutableListOf() + + /** + * The [Layer] which [Modal]s will be displayed on. + */ + private var layer: EphemeralLayer? = null + set(value) { + field = value + if (value == null) return + + value.onClose = { + // When the layer is closed (i.e. it is about to be cleaned up due to having no children), + // we should attempt to pop from the queue. + val nextModal = modalQueue.removeFirstOrNull() + if (nextModal != null) { + value.pushModal(nextModal) + } else { + // If there are no modals left to push, we can set the backing layer to null, + // as it is about to be cleaned up by the OverlayManager. + this.layer = null + } + } + } + + override fun queueModal(modal: Modal) { + // If this.layer is null, that means that the layer was cleaned up by the OverlayManager, we should + // create a new one for this modal to be pushed on to. + val layer = layer ?: overlayManager.createEphemeralLayer(LayerPriority.Modal).apply { layer = this } + if (layer.window.children.isEmpty()) { + // If the layer currently doesn't have a modal on it, we can push one. + layer.pushModal(modal) + return + } + + // If the layer currently has a modal on it, the next one will be pushed + // when [EphemeralLayer.onClose] is called. + modalQueue.add(modal) + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/gui/overlay/OverlayManager.kt b/src/main/kotlin/gg/essential/gui/overlay/OverlayManager.kt index a103acf..b058427 100644 --- a/src/main/kotlin/gg/essential/gui/overlay/OverlayManager.kt +++ b/src/main/kotlin/gg/essential/gui/overlay/OverlayManager.kt @@ -11,8 +11,6 @@ */ package gg.essential.gui.overlay -import gg.essential.gui.common.modal.Modal - /** * Manages [Layer]s to be displayed above the vanilla screen. */ @@ -33,22 +31,4 @@ interface OverlayManager { * This will not call [EphemeralLayer.onClose] or any other cleanup methods. */ fun removeLayer(layer: Layer) - - /** - * Creates a new ephemeral layer with [LayerPriority.Modal] and displays the given modal on - * it. - * Returns the newly created layer (e.g. to queue further modals via [EphemeralLayer.queueModal]). - * - * If the modal does not need immediate attention, consider using [queueModal] instead, as to not interrupt the user - * in their current modal. - */ - fun pushModal(modal: Modal): Layer - - /** - * Queues the given modal to be displayed after existing modals (or immediately if there are no active modals). - * - * If a modal needs immediate attention and cannot wait until the user has dealt with the current modal, use - * [pushModal] instead to push the new modal on top of the existing ones. - */ - fun queueModal(modal: Modal) } \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/gui/overlay/OverlayManagerImpl.kt b/src/main/kotlin/gg/essential/gui/overlay/OverlayManagerImpl.kt index ccb4095..88ec035 100644 --- a/src/main/kotlin/gg/essential/gui/overlay/OverlayManagerImpl.kt +++ b/src/main/kotlin/gg/essential/gui/overlay/OverlayManagerImpl.kt @@ -23,7 +23,6 @@ import gg.essential.event.gui.GuiKeyTypedEvent import gg.essential.event.gui.GuiMouseReleaseEvent import gg.essential.event.gui.MouseScrollEvent import gg.essential.event.render.RenderTickEvent -import gg.essential.gui.common.modal.Modal import gg.essential.universal.UKeyboard import gg.essential.universal.UMatrixStack import gg.essential.universal.UMouse @@ -71,19 +70,6 @@ object OverlayManagerImpl : OverlayManager { layers.remove(layer) } - override fun pushModal(modal: Modal): Layer { - val layer = createEphemeralLayer(LayerPriority.Modal) - layer.pushModal(modal) - return layer - } - - override fun queueModal(modal: Modal) { - val layer = - layers.filterIsInstance().find { it.priority == LayerPriority.Modal } - ?: createEphemeralLayer(LayerPriority.Modal) - layer.queueModal(modal) - } - /** * Returns the layer which the mouse is currently hovering. * diff --git a/src/main/kotlin/gg/essential/gui/screenshot/components/ScreenshotBrowser.kt b/src/main/kotlin/gg/essential/gui/screenshot/components/ScreenshotBrowser.kt index c1fad84..9d2c8bb 100644 --- a/src/main/kotlin/gg/essential/gui/screenshot/components/ScreenshotBrowser.kt +++ b/src/main/kotlin/gg/essential/gui/screenshot/components/ScreenshotBrowser.kt @@ -129,12 +129,6 @@ class ScreenshotBrowser(editPath: Path? = null): InternalEssentialGUI( addLine(pair.first, pair.second) } }) - pushModal.window.keyTypedListeners.clear() // Don't pass through events - pushModal.window.onKeyType { _, keyCode -> - if (keyCode == UKeyboard.KEY_ESCAPE) { - pushModal.window.clearChildren() - } - } } private fun generateImageProperties(properties: ScreenshotProperties): List>> { diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt b/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt index 16ac7d4..e4ed4fb 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt @@ -183,6 +183,7 @@ sealed interface Item { CosmeticTier.RARE -> Tier.Rare CosmeticTier.EPIC -> Tier.Epic CosmeticTier.LEGENDARY -> Tier.Legendary + else -> Tier.Common } } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitComponents.kt b/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitComponents.kt index 49fc634..4cf43ba 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitComponents.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitComponents.kt @@ -60,7 +60,7 @@ fun LayoutScope.outfitAddButton(state: WardrobeState, modifier: Modifier = Modif if (newOutfit == null) { Notifications.push("Unable to add new outfit.", "Please try again later.") } else { - Notifications.push("Outfit created.", "") { + Notifications.push("Outfit created", "") { withCustomComponent(Slot.ICON, EssentialPalette.COSMETICS_10X7.create()) } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitItemFunctions.kt b/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitItemFunctions.kt index eec0b81..87629d2 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitItemFunctions.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/components/outfitItemFunctions.kt @@ -69,7 +69,7 @@ fun handleOutfitRightClick(item: Item.OutfitItem, wardrobeState: WardrobeState, if (it == null) { Notifications.push("Unable to add new outfit.", "Please try again later.") } else { - Notifications.push("Outfit created.", "") { + Notifications.push("Outfit created", "") { withCustomComponent(Slot.ICON, EssentialPalette.COSMETICS_10X7.create()) } 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 e36bbf4..ee2941e 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/components/previewWindow.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/components/previewWindow.kt @@ -272,7 +272,17 @@ fun LayoutScope.previewWindow(state: WardrobeState, modifier: Modifier, bottomDi } } -private fun LayoutScope.playerPreview(state: WardrobeState, modifier: Modifier): EmulatedUI3DPlayer { +private fun LayoutScope.playerPreview(state: WardrobeState, modifier: Modifier) { + var instance: EmulatedUI3DPlayer? = null + if_(state.screenOpen, cache = false) { + instance = playerPreviewInner(state, modifier) + } `else` { + instance?.close() + instance = null + } +} + +private fun LayoutScope.playerPreviewInner(state: WardrobeState, modifier: Modifier): EmulatedUI3DPlayer { val profile = state.selectedBundle.map { bundle -> bundle?.skin?.let { GameProfileManager.Overwrites( @@ -285,7 +295,11 @@ private fun LayoutScope.playerPreview(state: WardrobeState, modifier: Modifier): val sounds = stateOf(true) // TODO make toggleable - return EmulatedUI3DPlayer(profile = profile.toV1(stateScope), sounds = sounds).apply { + return EmulatedUI3DPlayer( + profile = profile.toV1(stateScope), + sounds = stateOf(true), + soundsVolume = sounds.map { if (it) 1f else 0f }, + ).apply { cosmeticsSource = ConfigurableCosmeticsSource().apply { effect(stateScope) { val bundle = state.selectedBundle() @@ -309,7 +323,7 @@ private fun LayoutScope.playerPreview(state: WardrobeState, modifier: Modifier): listOfNotNull(state.editingCosmetic()?.cosmetic, hovered().takeIf { !dragging() }) } - val cosmeticHoverEffect = CosmeticHoverOutlineEffect(outlineCosmetic) + val cosmeticHoverEffect = CosmeticHoverOutlineEffect(EssentialPalette.GUI_BACKGROUND, outlineCosmetic) bindEffect(cosmeticHoverEffect, state.selectedBundle.map { it == null }) hovered.rebind(cosmeticHoverEffect.hoveredCosmetic) diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/configuration/cosmetic/properties/RequiresUnlockActionConfiguration.kt b/src/main/kotlin/gg/essential/gui/wardrobe/configuration/cosmetic/properties/RequiresUnlockActionConfiguration.kt index df646f0..eccc45f 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/configuration/cosmetic/properties/RequiresUnlockActionConfiguration.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/configuration/cosmetic/properties/RequiresUnlockActionConfiguration.kt @@ -17,7 +17,7 @@ import gg.essential.gui.layoutdsl.* import gg.essential.gui.wardrobe.configuration.ConfigurationUtils.labeledListInputRow import gg.essential.gui.wardrobe.configuration.ConfigurationUtils.labeledStringInputRow import gg.essential.mod.cosmetics.settings.CosmeticProperty -import gg.essential.mod.cosmetics.settings.CosmeticProperty.RequiresUnlockAction.* +import gg.essential.mod.cosmetics.settings.CosmeticProperty.RequiresUnlockAction.Data import gg.essential.network.connectionmanager.cosmetics.* import gg.essential.network.cosmetics.Cosmetic @@ -32,10 +32,11 @@ class RequiresUnlockActionConfiguration( override fun LayoutScope.layout(property: CosmeticProperty.RequiresUnlockAction) { val data = property.data + labeledListInputRow( "Type:", data, - mutableListStateOf( + stateOf(listOfNotNull>( EssentialDropDown.Option( "Open Link", if (data is Data.OpenLink) data else Data.OpenLink("", "", "") @@ -48,7 +49,8 @@ class RequiresUnlockActionConfiguration( "Join SPS", if (data is Data.JoinSps) data else Data.JoinSps("", "") ), - ) + null, + )).toListState() ) { property.update(property.copy(data = it)) } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/modals/CoinsPurchaseModal.kt b/src/main/kotlin/gg/essential/gui/wardrobe/modals/CoinsPurchaseModal.kt index 807c301..9d6da77 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/modals/CoinsPurchaseModal.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/modals/CoinsPurchaseModal.kt @@ -161,8 +161,8 @@ class CoinsPurchaseModal(val state: WardrobeState, coinsNeeded: Int? = null) : M } } - container.contentContainer.layoutAsBox(Modifier.width(511f).childBasedHeight()) { - column(Modifier.fillWidth(padding = 17f)) { + container.contentContainer.layoutAsBox(Modifier.width(510f).childBasedHeight()) { + column(Modifier.fillWidth(padding = 16f)) { spacer(height = 11f) box(Modifier.fillWidth().height(17f)) { row(Modifier.fillHeight().alignHorizontal(Alignment.Start), Arrangement.spacedBy(5f)) { @@ -179,7 +179,7 @@ class CoinsPurchaseModal(val state: WardrobeState, coinsNeeded: Int? = null) : M row(Modifier.fillHeight(), Arrangement.spacedBy(5f)) { val minimumAmount = Essential.getInstance().connectionManager.cosmeticsManager.wardrobeSettings.youNeedMinimumAmount.get() - text("Essential Coins", Modifier.alignVertical(Alignment.Center(true)).shadow()) + text("Essential Coins", Modifier.alignVertical(Alignment.Center(true)).shadow(EssentialPalette.BLACK)) infoIcon("Unlock cosmetics and emotes with Essential Coins.\nMinimum coin package size of $minimumAmount coins.", position = EssentialTooltip.Position.ABOVE) } row(Modifier.fillHeight().alignHorizontal(Alignment.End), Arrangement.spacedBy(5f)) { @@ -193,7 +193,7 @@ class CoinsPurchaseModal(val state: WardrobeState, coinsNeeded: Int? = null) : M } } spacer(height = 13f) - row(Arrangement.spacedBy(10f)) { + row(Modifier.fillWidth(), Arrangement.SpaceBetween) { forEach(coinsManager.pricing) { bundleBox(it) } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/purchase.kt b/src/main/kotlin/gg/essential/gui/wardrobe/purchase.kt index 26918f0..a8911da 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/purchase.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/purchase.kt @@ -279,7 +279,7 @@ private fun WardrobeState.createOutfitForBundle(item: Item.Bundle, changeSelecte withCustomComponent(Slot.SMALL_PREVIEW, component) } - Notifications.push("Outfit created.", "") { + Notifications.push("Outfit created", "") { withCustomComponent(Slot.ICON, EssentialPalette.COSMETICS_10X7.create()) } diff --git a/src/main/kotlin/gg/essential/handlers/EssentialSoundManager.kt b/src/main/kotlin/gg/essential/handlers/EssentialSoundManager.kt index a31258d..9717956 100644 --- a/src/main/kotlin/gg/essential/handlers/EssentialSoundManager.kt +++ b/src/main/kotlin/gg/essential/handlers/EssentialSoundManager.kt @@ -12,19 +12,28 @@ package gg.essential.handlers import dev.folomeev.kotgl.matrix.vectors.Vec3 +import dev.folomeev.kotgl.matrix.vectors.mutables.minus import dev.folomeev.kotgl.matrix.vectors.sqrDistance import dev.folomeev.kotgl.matrix.vectors.vec3 +import dev.folomeev.kotgl.matrix.vectors.vecZero import gg.essential.config.LoadsResources -import gg.essential.gui.common.EmulatedUI3DPlayer +import gg.essential.gui.elementa.state.v2.State +import gg.essential.gui.elementa.state.v2.stateOf import gg.essential.mixins.impl.client.audio.ISoundExt +import gg.essential.mixins.impl.client.audio.SoundSystemExt +import gg.essential.model.ModelAnimationState +import gg.essential.model.ParticleSystem.Locator +import gg.essential.model.PlayerMolangQuery import gg.essential.model.SoundEffect +import gg.essential.model.util.Quaternion +import gg.essential.model.util.rotateSelfBy import net.minecraft.client.Minecraft import net.minecraft.client.audio.ISound +import net.minecraft.client.audio.ITickableSound import net.minecraft.client.audio.PositionedSoundRecord import net.minecraft.client.audio.Sound import net.minecraft.client.audio.SoundEventAccessor import net.minecraft.client.audio.SoundHandler -import net.minecraft.entity.Entity import net.minecraft.util.ResourceLocation import net.minecraft.util.SoundCategory import net.minecraft.util.SoundEvent @@ -77,13 +86,14 @@ object EssentialSoundManager { } - fun playSound(entity: Entity?, effect: SoundEffect) { + @JvmOverloads + fun playSound( + event: ModelAnimationState.SoundEvent, + forceGlobal: Boolean = false, + volume: State = stateOf(1f), + ) { val mc = Minecraft.getMinecraft() - val pos = entity - ?.takeUnless { it is EmulatedUI3DPlayer.EmulatedPlayer } - ?.let { vec3(it.posX.toFloat(), it.posY.toFloat(), it.posZ.toFloat()) } - //#if MC>=11600 //$$ val listener = with(mc.gameRenderer.activeRenderInfo.projectedView) { vec3(x.toFloat(), y.toFloat(), z.toFloat()) } //#else @@ -91,14 +101,21 @@ object EssentialSoundManager { val listener = mc.player?.let { vec3(it.posX.toFloat(), it.posY.toFloat(), it.posZ.toFloat()) } //#endif - if (pos != null && listener != null && listener.sqrDistance(pos) > effect.maxDistance * effect.maxDistance) { + val pos = event.locator.position + val maxDist = event.effect.maxDistance + if (!forceGlobal && listener != null && listener.sqrDistance(pos) > maxDist * maxDist) { return } - mc.soundHandler.playSound(EssentialSoundInstance(effect, pos)) + mc.soundHandler.playSound(EssentialSoundInstance(event.effect, event.locator, volume, forceGlobal)) } - private class EssentialSoundInstance(val effect: SoundEffect, val pos: Vec3?) : ISound, ISoundExt { + private class EssentialSoundInstance( + val effect: SoundEffect, + val loc: Locator, + private val volume: State, + forceGlobal: Boolean, + ) : ITickableSound, ISoundExt { private val identifier: ResourceLocation = effect.name.let { if (":" in it) ResourceLocation(it) else ResourceLocation("essential", it) } @@ -162,29 +179,38 @@ object EssentialSoundManager { //$$ override fun `essential$createAccessor`(): SoundEventAccessorComposite = soundSet //#endif - override fun canRepeat(): Boolean = false + override fun canRepeat(): Boolean = sound?.looping ?: false override fun getRepeatDelay(): Int = 0 - override fun getVolume(): Float = sound?.volume ?: 1f + // When the sound volume is zero during the initial playSound call, MC will completely skip submitting the + // sound. So even if the volume increases later, it won't be playing. + // To work around this, we never return 0 until the first `update` call. + private var mayReturnTrueVolume: Boolean = false + override fun getVolume(): Float = + ((sound?.volume ?: 1f) * volume.getUntracked()).coerceAtLeast(if (mayReturnTrueVolume) 0f else 1e-10f) override fun getPitch(): Float = sound?.pitch ?: 1f - private val isGlobal = pos == null || sound?.directional == false + private val isGlobal = forceGlobal || sound?.directional == false + private val isRelativeToListener = isGlobal || (loc.isRelativeToCamera() && !effect.fixedPosition) //#if MC>=11600 - //$$ override fun isGlobal(): Boolean = isGlobal + //$$ // Note: 1.16 forge and pre-1.18 yarn names for this method are bad, correct name is `isRelative`. + //$$ // The method is equivalent to our pre-1.16 mixin which sets `AL_SOURCE_RELATIVE`. + //$$ override fun isGlobal(): Boolean = isRelativeToListener //#else - override fun `essential$isRelativeToListener`(): Boolean = isGlobal + override fun `essential$isRelativeToListener`(): Boolean = isRelativeToListener //#endif + private var pos: Vec3 = if (isGlobal) vecZero() else loc.position.relativeToReference() //#if MC>=11600 - //$$ override fun getX(): Double = if (isGlobal) 0.0 else pos?.x?.toDouble() ?: 0.0 - //$$ override fun getY(): Double = if (isGlobal) 0.0 else pos?.y?.toDouble() ?: 0.0 - //$$ override fun getZ(): Double = if (isGlobal) 0.0 else pos?.z?.toDouble() ?: 0.0 + //$$ override fun getX(): Double = pos.x.toDouble() + //$$ override fun getY(): Double = pos.y.toDouble() + //$$ override fun getZ(): Double = pos.z.toDouble() //#else - override fun getXPosF(): Float = if (isGlobal) 0f else pos?.x ?: 0f - override fun getYPosF(): Float = if (isGlobal) 0f else pos?.y ?: 0f - override fun getZPosF(): Float = if (isGlobal) 0f else pos?.z ?: 0f + override fun getXPosF(): Float = pos.x + override fun getYPosF(): Float = pos.y + override fun getZPosF(): Float = pos.z //#endif //#if MC>=11600 @@ -195,5 +221,38 @@ object EssentialSoundManager { override fun getAttenuationType(): ISound.AttenuationType = if (isGlobal) ISound.AttenuationType.NONE else ISound.AttenuationType.LINEAR + + private var alive = loc.isValid + override fun isDonePlaying(): Boolean { + return !alive + } + + override fun update() { + mayReturnTrueVolume = true + + if (sound == null || sound.interruptible || sound.looping) { + alive = loc.isValid + } + if (alive && !isGlobal && !effect.fixedPosition) { + pos = loc.position.relativeToReference() + } + } + + private fun Vec3.relativeToReference(): Vec3 = + if (isRelativeToListener) { + val soundHandler = Minecraft.getMinecraft().soundHandler as SoundSystemExt + val origin = soundHandler.`essential$getListenerPosition`() ?: vecZero() + val rotation = soundHandler.`essential$getListenerRotation`() ?: Quaternion.Identity + this.minus(origin).rotateSelfBy(rotation.invert()) + } else { + this + } + } + + private fun Locator.isRelativeToCamera(): Boolean { + if (this is PlayerMolangQuery) { + return player == Minecraft.getMinecraft().player + } + return parent?.isRelativeToCamera() ?: false } } diff --git a/src/main/kotlin/gg/essential/model/backend/minecraft/MinecraftRenderBackend.kt b/src/main/kotlin/gg/essential/model/backend/minecraft/MinecraftRenderBackend.kt index 363799c..73cd703 100644 --- a/src/main/kotlin/gg/essential/model/backend/minecraft/MinecraftRenderBackend.kt +++ b/src/main/kotlin/gg/essential/model/backend/minecraft/MinecraftRenderBackend.kt @@ -30,10 +30,28 @@ import net.minecraft.client.renderer.GlStateManager import net.minecraft.client.renderer.vertex.DefaultVertexFormats import net.minecraft.util.ResourceLocation import org.lwjgl.opengl.GL11 +import org.lwjgl.opengl.GL30 import java.io.ByteArrayInputStream import gg.essential.model.util.UMatrixStack as CMatrixStack import gg.essential.model.util.UVertexConsumer as CVertexConsumer +//#if MC>=11700 +//$$ import org.lwjgl.opengl.GL30.glBindFramebuffer +//$$ import org.lwjgl.opengl.GL30.glDeleteFramebuffers +//$$ import org.lwjgl.opengl.GL30.glFramebufferTexture2D +//$$ import org.lwjgl.opengl.GL30.glGenFramebuffers +//#elseif MC>=11400 +//$$ import com.mojang.blaze3d.platform.GlStateManager.bindFramebuffer as glBindFramebuffer +//$$ import com.mojang.blaze3d.platform.GlStateManager.deleteFramebuffers as glDeleteFramebuffers +//$$ import com.mojang.blaze3d.platform.GlStateManager.framebufferTexture2D as glFramebufferTexture2D +//$$ import com.mojang.blaze3d.platform.GlStateManager.genFramebuffers as glGenFramebuffers +//#else +import net.minecraft.client.renderer.OpenGlHelper.glBindFramebuffer +import net.minecraft.client.renderer.OpenGlHelper.glDeleteFramebuffers +import net.minecraft.client.renderer.OpenGlHelper.glFramebufferTexture2D +import net.minecraft.client.renderer.OpenGlHelper.glGenFramebuffers +//#endif + //#if MC>=11600 //$$ import net.minecraft.client.renderer.RenderType //$$ import net.minecraft.client.renderer.texture.OverlayTexture @@ -43,6 +61,62 @@ import net.minecraft.client.renderer.vertex.VertexFormat //#endif object MinecraftRenderBackend : RenderBackend { + override fun createTexture(name: String, width: Int, height: Int): RenderBackend.Texture { + return DynamicTexture(ResourceLocation("essential", name), ReleasedDynamicTexture(width, height)) + } + + override fun deleteTexture(texture: RenderBackend.Texture) { + val identifier = (texture as DynamicTexture).identifier + + texture.texture.deleteGlTexture() + + val textureManager = getMinecraft().textureManager + //#if MC>=11700 + //$$ val registeredTexture = textureManager.getOrDefault(identifier, null) as? ReleasedDynamicTexture + //#else + val registeredTexture = textureManager.getTexture(identifier) as? ReleasedDynamicTexture + //#endif + if (registeredTexture == texture.texture) { + textureManager.deleteTexture(identifier) + } + } + + override fun blitTexture(dst: RenderBackend.Texture, ops: Iterable) { + val textureManager = getMinecraft().textureManager + fun RenderBackend.Texture.glId() = + textureManager.getTexture((this as MinecraftTexture).identifier)!!.glTextureId + + val prevScissor = GL11.glGetBoolean(GL11.GL_SCISSOR_TEST) + if (prevScissor) GL11.glDisable(GL11.GL_SCISSOR_TEST) + + val prevDrawFrameBufferBinding = GL11.glGetInteger(GL30.GL_DRAW_FRAMEBUFFER_BINDING) + val prevReadFrameBufferBinding = GL11.glGetInteger(GL30.GL_READ_FRAMEBUFFER_BINDING) + + val dstBuffer = glGenFramebuffers() + val srcBuffer = glGenFramebuffers() + + glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, dstBuffer) + glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, srcBuffer) + glFramebufferTexture2D(GL30.GL_DRAW_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, dst.glId(), 0) + + for ((src, srcX, srcY, destX, destY, width, height) in ops) { + glFramebufferTexture2D(GL30.GL_READ_FRAMEBUFFER, GL30.GL_COLOR_ATTACHMENT0, GL11.GL_TEXTURE_2D, src.glId(), 0) + GL30.glBlitFramebuffer( + srcX, srcY, srcX + width, srcY + height, + destX, destY, destX + width, destY + height, + GL11.GL_COLOR_BUFFER_BIT, GL11.GL_NEAREST + ) + } + + glBindFramebuffer(GL30.GL_DRAW_FRAMEBUFFER, prevDrawFrameBufferBinding) + glBindFramebuffer(GL30.GL_READ_FRAMEBUFFER, prevReadFrameBufferBinding) + + glDeleteFramebuffers(dstBuffer) + glDeleteFramebuffers(srcBuffer) + + if (prevScissor) GL11.glEnable(GL11.GL_SCISSOR_TEST) + } + override suspend fun readTexture(name: String, bytes: ByteArray): RenderBackend.Texture { return CosmeticTexture(name, UGraphics.getTexture(ByteArrayInputStream(bytes))) } @@ -65,15 +139,11 @@ object MinecraftRenderBackend : RenderBackend { get() = 32 } - data class CosmeticTexture( - val name: String, - val texture: ReleasedDynamicTexture, - ) : MinecraftTexture { + open class DynamicTexture(identifier: ResourceLocation, val texture: ReleasedDynamicTexture) : MinecraftTexture { override val width: Int = texture.width override val height: Int = texture.height override val identifier: ResourceLocation by lazy { - val identifier = ResourceLocation("essential", "textures/cosmetics/${name.lowercase()}") val textureManager = getMinecraft().textureManager //#if MC>=11700 //$$ (textureManager.getOrDefault(identifier, null) as? ReleasedDynamicTexture)?.clearGlId() @@ -85,6 +155,11 @@ object MinecraftRenderBackend : RenderBackend { } } + class CosmeticTexture( + val name: String, + texture: ReleasedDynamicTexture, + ) : DynamicTexture(ResourceLocation("essential", "textures/cosmetics/${name.lowercase()}"), texture) + //#if MC>=11600 //$$ class VertexConsumerProvider(val provider: net.minecraft.client.renderer.IRenderTypeBuffer, val light: Int) : RenderBackend.VertexConsumerProvider { //#else diff --git a/src/main/kotlin/gg/essential/model/backend/minecraft/legacyCameraPositioning.kt b/src/main/kotlin/gg/essential/model/backend/minecraft/legacyCameraPositioning.kt new file mode 100644 index 0000000..46203c3 --- /dev/null +++ b/src/main/kotlin/gg/essential/model/backend/minecraft/legacyCameraPositioning.kt @@ -0,0 +1,36 @@ +/* + * 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. + */ +//#if MC<11400 +package gg.essential.model.backend.minecraft + +import dev.folomeev.kotgl.matrix.vectors.Vec3 +import dev.folomeev.kotgl.matrix.vectors.vec3 +import org.lwjgl.BufferUtils +import org.lwjgl.opengl.GL11 +import org.lwjgl.util.glu.GLU + +fun getRelativeCameraPosFromGlState(): Vec3 { + val modelMatrix = BufferUtils.createFloatBuffer(16) + val projectionMatrix = BufferUtils.createFloatBuffer(16) + GL11.glGetFloat(GL11.GL_MODELVIEW_MATRIX, modelMatrix) + GL11.glGetFloat(GL11.GL_PROJECTION_MATRIX, projectionMatrix) + + val viewport = BufferUtils.createIntBuffer(16) + GL11.glGetInteger(GL11.GL_VIEWPORT, viewport) + val centerX = (viewport[0] + viewport[2]) / 2f + val centerY = (viewport[1] + viewport[3]) / 2f + + val objPos = BufferUtils.createFloatBuffer(3) + GLU.gluUnProject(centerX, centerY, 0.0f, modelMatrix, projectionMatrix, viewport, objPos) + return vec3(objPos[0], objPos[1], objPos[2]) +} +//#endif diff --git a/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/CosmeticEquipVisibilityResponse.kt b/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/CosmeticEquipVisibilityResponse.kt index cc0e279..e166a38 100644 --- a/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/CosmeticEquipVisibilityResponse.kt +++ b/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/CosmeticEquipVisibilityResponse.kt @@ -35,7 +35,7 @@ class CosmeticEquipVisibilityResponse( if (packet is ResponseActionPacket && packet.isSuccessful) { Essential.getInstance().connectionManager.cosmeticsManager.ownCosmeticsVisible = nextState if (notification) { - Notifications.push("Your cosmetics are ${if (nextState) "shown" else "hidden"}.", "") { + Notifications.push("Your cosmetics are ${if (nextState) "shown" else "hidden"}", "") { if (nextState) { withCustomComponent(Slot.ICON, EssentialPalette.COSMETICS_10X7.create()) } else { diff --git a/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/localCosmeticManagement.kt b/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/localCosmeticManagement.kt index 104ce36..36411b3 100644 --- a/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/localCosmeticManagement.kt +++ b/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/localCosmeticManagement.kt @@ -90,37 +90,37 @@ fun CosmeticsDataWithChanges.setCosmeticSingletonPropertyEnabled( } else { val newProperty = when (type) { CosmeticPropertyType.ARMOR_HANDLING -> CosmeticProperty.ArmorHandling( - null, + "UNUSED", enabled, CosmeticProperty.ArmorHandling.Data() ) CosmeticPropertyType.POSITION_RANGE -> CosmeticProperty.PositionRange( - null, + "UNUSED", enabled, CosmeticProperty.PositionRange.Data() ) CosmeticPropertyType.INTERRUPTS_EMOTE -> CosmeticProperty.InterruptsEmote( - null, + "UNUSED", enabled, CosmeticProperty.InterruptsEmote.Data() ) CosmeticPropertyType.LOCALIZATION -> CosmeticProperty.Localization( - null, + "UNUSED", enabled, CosmeticProperty.Localization.Data("Partner Name") ) CosmeticPropertyType.PREVIEW_RESET_TIME -> CosmeticProperty.PreviewResetTime( - null, + "UNUSED", enabled, CosmeticProperty.PreviewResetTime.Data(3.0) ) CosmeticPropertyType.REQUIRES_UNLOCK_ACTION -> CosmeticProperty.RequiresUnlockAction( - null, + "UNUSED", enabled, CosmeticProperty.RequiresUnlockAction.Data.OpenLink( "Descriptive Action", @@ -130,19 +130,19 @@ fun CosmeticsDataWithChanges.setCosmeticSingletonPropertyEnabled( ) CosmeticPropertyType.TRANSITION_DELAY -> CosmeticProperty.TransitionDelay( - null, + "UNUSED", enabled, CosmeticProperty.TransitionDelay.Data(0) ) CosmeticPropertyType.VARIANTS -> CosmeticProperty.Variants( - null, + "UNUSED", enabled, CosmeticProperty.Variants.Data(listOf()) ) CosmeticPropertyType.DEFAULT_SIDE -> CosmeticProperty.DefaultSide( - null, + "UNUSED", enabled, CosmeticProperty.DefaultSide.Data(Side.getDefaultSideOrNull(Side.values().toSet()) ?: Side.LEFT) ) diff --git a/src/main/kotlin/gg/essential/network/cosmetics/conversions.kt b/src/main/kotlin/gg/essential/network/cosmetics/conversions.kt index 897c1f8..3201400 100644 --- a/src/main/kotlin/gg/essential/network/cosmetics/conversions.kt +++ b/src/main/kotlin/gg/essential/network/cosmetics/conversions.kt @@ -92,7 +92,7 @@ fun InfraCosmeticTier?.toMod() = when (this) { InfraCosmeticTier.RARE -> CosmeticTier.RARE InfraCosmeticTier.EPIC -> CosmeticTier.EPIC InfraCosmeticTier.LEGENDARY -> CosmeticTier.LEGENDARY - null -> CosmeticTier.COMMON // Remove null option when removing flag + else -> CosmeticTier.COMMON // Remove null option when removing flag } fun InfraCosmeticCategory.toMod() = CosmeticCategory( diff --git a/src/main/kotlin/gg/essential/util/GlFrameBuffer.kt b/src/main/kotlin/gg/essential/util/GlFrameBuffer.kt index 7d8ee29..27dfa78 100644 --- a/src/main/kotlin/gg/essential/util/GlFrameBuffer.kt +++ b/src/main/kotlin/gg/essential/util/GlFrameBuffer.kt @@ -227,11 +227,18 @@ class GlFrameBuffer(width: Int, height: Int) { } } - fun clear() { + @JvmOverloads + fun clear( + clearColor: Color = Color(0, 0, 0, 0), + clearDepth: Double = 1.0, + clearStencil: Int = 0, + ) { use { - GlStateManager.clearColor(0f, 0f, 0f, 0f) - GlStateManager.clearDepth(1.0) - glClearStencil(0) + with(clearColor) { + GlStateManager.clearColor(red / 255f, green / 255f, blue / 255f, alpha / 255f) + } + GlStateManager.clearDepth(clearDepth) + glClearStencil(clearStencil) glClear(GL_COLOR_BUFFER_BIT or GL_DEPTH_BUFFER_BIT or GL_STENCIL_BUFFER_BIT) } } diff --git a/src/main/kotlin/gg/essential/util/GuiUtil.kt b/src/main/kotlin/gg/essential/util/GuiUtil.kt index 11a6956..62cca7b 100644 --- a/src/main/kotlin/gg/essential/util/GuiUtil.kt +++ b/src/main/kotlin/gg/essential/util/GuiUtil.kt @@ -19,9 +19,12 @@ import gg.essential.connectionmanager.common.packet.telemetry.ClientTelemetryPac import gg.essential.data.OnboardingData import gg.essential.elementa.WindowScreen import gg.essential.event.client.ClientTickEvent +import gg.essential.gui.common.modal.Modal import gg.essential.gui.friends.SocialMenu import gg.essential.gui.modals.* import gg.essential.gui.notification.sendTosNotification +import gg.essential.gui.overlay.ModalManager +import gg.essential.gui.overlay.ModalManagerImpl import gg.essential.gui.overlay.OverlayManager import gg.essential.gui.overlay.OverlayManagerImpl import gg.essential.gui.wardrobe.Wardrobe @@ -36,9 +39,25 @@ import net.minecraft.client.resources.I18n //$$ import gg.essential.universal.wrappers.message.UTextComponent //#endif -object GuiUtil : GuiUtil, OverlayManager by OverlayManagerImpl { +object GuiUtil : GuiUtil, OverlayManager by OverlayManagerImpl, ModalManager by ModalManagerImpl(OverlayManagerImpl) { private var display: (() -> GuiScreen?)? = null + /** + * Creates a new [ModalManager] and queues the modal on it (which will result in it being pushed immediately). + * + * Returns the newly created [ModalManager], which can be used to queue further modals if needed. + * + * If the modal does not need immediate attention, consider using [queueModal] instead, as to not + * interrupt the user in their current modal. + */ + fun pushModal(modal: Modal): ModalManager { + // If this modal needs to be pushed immediately, we will create a new modal manager for it. + val manager: ModalManager = ModalManagerImpl(this) + manager.queueModal(modal) + + return manager + } + inline fun openScreen(noinline screen: () -> T?) { // Guard against T not being inferred well enough. If there's a compile-time way to do this please tell me. when (T::class.java) { diff --git a/src/main/kotlin/gg/essential/util/MojangAPI.kt b/src/main/kotlin/gg/essential/util/MojangAPI.kt index a2a79bf..a2a2882 100644 --- a/src/main/kotlin/gg/essential/util/MojangAPI.kt +++ b/src/main/kotlin/gg/essential/util/MojangAPI.kt @@ -67,7 +67,7 @@ object MojangAPI : MojangAPI { val body: RequestBody = MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart( - "file", file.name, + "file", "skin.png", RequestBody.create(MediaType.parse("image/png"), file) ) .addFormDataPart( diff --git a/src/main/kotlin/gg/essential/util/extensions.kt b/src/main/kotlin/gg/essential/util/extensions.kt index 3279424..f1843c9 100644 --- a/src/main/kotlin/gg/essential/util/extensions.kt +++ b/src/main/kotlin/gg/essential/util/extensions.kt @@ -337,7 +337,11 @@ fun UMatrixStack.toCommon() = ) fun gg.essential.model.util.UMatrixStack.toUC() = - UMatrixStack().also { uc -> + UMatrixStack().apply { set(this@toUC) } + +fun UMatrixStack.set(value: gg.essential.model.util.UMatrixStack) = + value.run { + val uc = this@set //#if MC>=11400 //$$ uc.peek().model.kotgl = peek().model //$$ uc.peek().normal.kotgl = peek().normal diff --git a/src/main/kotlin/gg/essential/util/helpers.kt b/src/main/kotlin/gg/essential/util/helpers.kt index 0949d7d..d37cabc 100644 --- a/src/main/kotlin/gg/essential/util/helpers.kt +++ b/src/main/kotlin/gg/essential/util/helpers.kt @@ -24,6 +24,7 @@ import gg.essential.gui.screenshot.components.ScreenshotBrowser import gg.essential.gui.screenshot.components.ScreenshotProperties import gg.essential.universal.UDesktop import gg.essential.universal.UMinecraft +import gg.essential.util.resource.EssentialAssetResourcePack import net.minecraft.client.gui.GuiOptions import net.minecraft.client.resources.FileResourcePack import net.minecraft.client.resources.FolderResourcePack @@ -192,6 +193,7 @@ sealed interface CodeSource { } fun addEssentialResourcePack(consumer: Consumer) { + consumer.accept(EssentialAssetResourcePack(Essential.getInstance().connectionManager.cosmeticsManager.assetLoader)) //#if MC>=12005 //$$ val info = makePackInfo("essential") diff --git a/src/main/kotlin/gg/essential/util/iterable.kt b/src/main/kotlin/gg/essential/util/iterable.kt index 3ffe204..8654e37 100644 --- a/src/main/kotlin/gg/essential/util/iterable.kt +++ b/src/main/kotlin/gg/essential/util/iterable.kt @@ -18,3 +18,4 @@ inline fun Iterable.sumOf(selector: (T) -> Float): Float { } return sum } + diff --git a/src/main/resources/assets/essential/account/login/microsoft.html b/src/main/resources/assets/essential/account/login/microsoft.html index 7c0c83f..c1872f7 100644 --- a/src/main/resources/assets/essential/account/login/microsoft.html +++ b/src/main/resources/assets/essential/account/login/microsoft.html @@ -19,9 +19,31 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/essential/account/login/success.html b/src/main/resources/assets/essential/account/login/success.html index 40edc99..6e75595 100644 --- a/src/main/resources/assets/essential/account/login/success.html +++ b/src/main/resources/assets/essential/account/login/success.html @@ -18,11 +18,31 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/resources/assets/essential/textures/essential.png b/src/main/resources/assets/essential/textures/essential.png index 8e5031470ed16134e9a8a6906c800d5bac45d579..1d895aaf933d046db63ab91a6d78eb629f39cbde 100644 GIT binary patch literal 6758 zcmeAS@N?(olHy`uVBq!ia0y~yU;#2&7&zE~RK2WrGmzpe@Q5sCVBi)8VMc~ob0mO* zjKx9jP7LeL$-D%qPj~cnWMJ6X&;2Knm4QL>x2KC^NCo5D+xC5ag(3|PjX#O1h%A#f znrc7QOC#4|VG4h6%zO~$OAH6@J-k?E&G6^@Jo}kDYm*}`%$;p~U5}sJ>i4(oeSF-%exyEo`kFC; z)r|Ro)}Mbr_t$49T;*H$Uy|X$drMHt8*M+0HYtJ0fTSw(2G0RL8GBi=vJLhO;_u$^ zTK|o=u#|1!R%c+a{`lh|!v?1C^*3JrZ(?A0(**1~48oFlG|owlb9#5V8qPAjIWW8Y z@FBUovv-!IGU#2od41;h`Rom%0t^jvfyqtmTUPzEe`^_c1f<`8Q_`@t09=EPR+pgs zHd^7c0SC=SlhSB; z1xf`(mRAZOrK8!7_-r>CR-<7B%rc{e45;9sSt0X5c;zcgOYhAo;PEU^S3j3^P6F7`|M%#J;ubb^eoKmsAb~~1ycbJ(?G}m3l)n>Y8EBRY*L939101XW#b@k zYGYrLtj2AJ=?hWs5mXa?o#&=7Y=OjhH0( zE%u6agfnqOGc^#S)=r0umuHUMUYDc27o(C=nmfstv1oE5_7+uzegLG#O0b5S&VXf< z>uqpi*MgxuzV}fps!Zn1Q(fT?B&=O)rZ=6pK}x<{K-#!`cqzV;%h!zj`KdUpJcL!gf$>OiGLu%9 z8LHhX5w%-cZDfvNsTC~+TrF{3>L72Jk-x|U4TG(C-6L58{_!lq7x1dMt;L=A^?Mzh zfEQ*mtYg?Z_)OLX=HfR!cM?)${a?VGxiGHuQuKy z)Jwa2{fXQP2*e=PB`Au#_Tz=yxu&jxV2Yxv5vW=r6^=AyS0`70-vcH Ar2qf` diff --git a/src/main/resources/mixins.essential.init.json b/src/main/resources/mixins.essential.init.json index 91ddfdf..ea1a9aa 100644 --- a/src/main/resources/mixins.essential.init.json +++ b/src/main/resources/mixins.essential.init.json @@ -10,6 +10,7 @@ //#if MC<11400 "target": "@env(INIT)", //#endif + "plugin" : "gg.essential.mixins.InitPlugin", "mixins": [ "Mixin_MixIntoNonCoremods" ] diff --git a/src/main/resources/mixins.essential.json b/src/main/resources/mixins.essential.json index a3b4840..91f70c0 100644 --- a/src/main/resources/mixins.essential.json +++ b/src/main/resources/mixins.essential.json @@ -32,7 +32,7 @@ "client.gui.Mixin_PreventMovingOfServersInCustomTabs", "client.gui.Mixin_RecalculateMenuScale", "client.gui.Mixin_RemoveChatLimit", - "client.gui.Mixin_UI3DPlayer_FixNameTagRotation", + "client.gui.Mixin_UI3DPlayer_Camera", "client.gui.Mixin_UnfocusTextFieldWhileOverlayHasFocus", "client.gui.MixinGuiChat", "client.gui.MixinGuiInventory_UI3DPlayerOffset", @@ -176,11 +176,15 @@ "feature.per_server_privacy.Mixin_AddPerServerPrivacyButton", "feature.per_server_settings.Mixin_PreserveCustomServerData", "feature.skin_overwrites.Mixin_InstallTrustingServicesKeyInfo", + "feature.sound.Mixin_FixPaulscodeChannelAllocation", "feature.sound.Mixin_ISoundExt_createAccessor", "feature.sound.Mixin_ISoundExt_isRelativeToListener_apply", "feature.sound.Mixin_ISoundExt_isRelativeToListener_forward", "feature.sound.Mixin_ISoundExt_maxDistance", "feature.sound.Mixin_LoadFromEssentialAsset", + "feature.sound.Mixin_SoundSystemExt_SoundHandler", + "feature.sound.Mixin_SoundSystemExt_SoundManager", + "feature.sound.Mixin_UpdateWhilePaused", "feature.sps.Mixin_PlayerJoinSessionEvent", "feature.sps.Mixin_PlayerLeaveSessionEvent", "feature.sps.Mixin_IntegratedServerResourcePack", diff --git a/utils/src/jvmMain/kotlin/gg/essential/model/util/ResourceCleaner.kt b/utils/src/jvmMain/kotlin/gg/essential/model/util/ResourceCleaner.kt new file mode 100644 index 0000000..f317bf1 --- /dev/null +++ b/utils/src/jvmMain/kotlin/gg/essential/model/util/ResourceCleaner.kt @@ -0,0 +1,44 @@ +/* + * 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.model.util + +import java.io.Closeable +import java.lang.ref.PhantomReference +import java.lang.ref.ReferenceQueue +import java.util.* +import java.util.concurrent.ConcurrentHashMap + +class ResourceCleaner { + private val referenceQueue = ReferenceQueue() + private val toBeCleanedUp: MutableSet = Collections.newSetFromMap(ConcurrentHashMap()) + + fun register(owner: T, cleanup: Runnable) { + toBeCleanedUp.add(Resource(owner, cleanup)) + } + + fun runCleanups() { + while (true) { + ((referenceQueue.poll() ?: break) as Resource).close() + } + } + + private inner class Resource(owner: T, cleanup: Runnable) : PhantomReference(owner, referenceQueue), Closeable { + var cleanup: Runnable? = cleanup + + override fun close() { + toBeCleanedUp.remove(this) + + cleanup?.run() + cleanup = null + } + } +} \ No newline at end of file diff --git a/versions/1.16.2-1.12.2.txt b/versions/1.16.2-1.12.2.txt index 2db532e..dca3ccc 100644 --- a/versions/1.16.2-1.12.2.txt +++ b/versions/1.16.2-1.12.2.txt @@ -13,8 +13,10 @@ com.mojang.blaze3d.systems.RenderSystem translated() net.minecraft.client.render com.mojang.datafixers.DataFixer net.minecraft.util.datafix.DataFixer net.minecraft.client.GameSettings net.minecraft.client.settings.GameSettings net.minecraft.client.GameSettings gamma net.minecraft.client.settings.GameSettings gammaSetting +net.minecraft.client.audio.ITickableSound tick() update() net.minecraft.client.audio.SimpleSound net.minecraft.client.audio.PositionedSoundRecord net.minecraft.client.audio.SimpleSound master() func_194007_a() +net.minecraft.client.audio.SoundEngine net.minecraft.client.audio.SoundManager net.minecraft.client.entity.player.AbstractClientPlayerEntity net.minecraft.client.entity.AbstractClientPlayer net.minecraft.client.entity.player.ClientPlayerEntity net.minecraft.client.entity.EntityPlayerSP net.minecraft.client.entity.player.RemoteClientPlayerEntity net.minecraft.client.entity.EntityOtherPlayerMP diff --git a/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/client/gui/MixinGuiInventory_UI3DPlayerOffset.java b/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/client/gui/MixinGuiInventory_UI3DPlayerOffset.java index a1ef2f2..be184ea 100644 --- a/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/client/gui/MixinGuiInventory_UI3DPlayerOffset.java +++ b/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/client/gui/MixinGuiInventory_UI3DPlayerOffset.java @@ -44,7 +44,7 @@ public class MixinGuiInventory_UI3DPlayerOffset { @ModifyExpressionValue(method = DRAW_ENTITY, at = @At(value = "CONSTANT", args = "floatValue=1050"), remap = DRAW_ENTITY_REMAP) private static float essential$modifyZOffset1(float original) { //#endif - if (UI3DPlayer.isRenderingPerspective) return 50; + if (UI3DPlayer.current != null) return 50; return original; } @@ -58,7 +58,7 @@ public class MixinGuiInventory_UI3DPlayerOffset { @ModifyExpressionValue(method = DRAW_ENTITY, at = @At(value = "CONSTANT", args = "doubleValue=1000"), remap = DRAW_ENTITY_REMAP) private static double essential$modifyZOffset2(double original) { //#endif - if (UI3DPlayer.isRenderingPerspective) return 0; + if (UI3DPlayer.current != null) return 0; return original; } diff --git a/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java b/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java new file mode 100644 index 0000000..189662d --- /dev/null +++ b/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_FixPaulscodeChannelAllocation.java @@ -0,0 +1,20 @@ +/* + * 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.feature.sound; + +import gg.essential.mixins.DummyTarget; +import org.spongepowered.asm.mixin.Mixin; + +// 1.12.2 and below +@Mixin(DummyTarget.class) +public class Mixin_FixPaulscodeChannelAllocation { +} diff --git a/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java b/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java new file mode 100644 index 0000000..51a75e0 --- /dev/null +++ b/versions/1.16.2-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_UpdateWhilePaused.java @@ -0,0 +1,69 @@ +/* + * 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.feature.sound; + +import gg.essential.mixins.impl.client.audio.ISoundExt; +import net.minecraft.client.audio.ChannelManager; +import net.minecraft.client.audio.ISound; +import net.minecraft.client.audio.ITickableSound; +import net.minecraft.client.audio.SoundEngine; +import net.minecraft.util.math.vector.Vector3d; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; +import java.util.Map; + +@Mixin(SoundEngine.class) +public abstract class Mixin_UpdateWhilePaused { + // MC doesn't tick sounds when the game is paused, as such we can't cancel them or update their volume, both of + // which we need to be able to do. + @Inject(method = "tick", at = @At("RETURN")) + private void updateEssentialSounds(boolean isGamePaused, CallbackInfo ci) { + if (!isGamePaused) return; + + for (ITickableSound sound : this.tickableSounds) { + if (!(sound instanceof ISoundExt)) { + continue; + } + + sound.tick(); + + if (!sound.isDonePlaying()) { + float volume = this.getClampedVolume(sound); + float pitch = this.getClampedPitch(sound); + Vector3d pos = new Vector3d(sound.getX(), sound.getY(), sound.getZ()); + ChannelManager.Entry channel = this.playingSoundsChannel.get(sound); + if (channel != null) { + channel.runOnSoundExecutor((arg) -> { + arg.setGain(volume); + arg.setPitch(pitch); + arg.updateSource(pos); + }); + } + } else { + this.stop(sound); + } + } + } + + @Shadow @Final private Map playingSoundsChannel; + @Shadow @Final private List tickableSounds; + + @Shadow public abstract void stop(ISound sound); + @Shadow protected abstract float getClampedVolume(ISound soundIn); + @Shadow protected abstract float getClampedPitch(ISound soundIn); +} diff --git a/versions/1.19.3-1.19.2.txt b/versions/1.19.3-1.19.2.txt index 00ad67e..2299a62 100644 --- a/versions/1.19.3-1.19.2.txt +++ b/versions/1.19.3-1.19.2.txt @@ -2,6 +2,7 @@ org.joml.Matrix4f net.minecraft.util.math.Matrix4f org.joml.Matrix4f identity() net.minecraft.util.math.Matrix4f loadIdentity() org.joml.Matrix4f mul() net.minecraft.util.math.Matrix4f multiply() org.joml.Matrix3f net.minecraft.util.math.Matrix3f +org.joml.Matrix3f mul() net.minecraft.util.math.Matrix3f multiply() org.joml.Vector4f net.minecraft.util.math.Vector4f org.joml.Vector3f net.minecraft.util.math.Vec3f org.joml.Quaternionf net.minecraft.util.math.Quaternion diff --git a/versions/1.8.9-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_createAccessor.java b/versions/1.8.9-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_createAccessor.java index 96c2acd..8c31e3f 100644 --- a/versions/1.8.9-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_createAccessor.java +++ b/versions/1.8.9-forge/src/main/java/gg/essential/mixins/transformers/feature/sound/Mixin_ISoundExt_createAccessor.java @@ -12,8 +12,10 @@ package gg.essential.mixins.transformers.feature.sound; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.llamalad7.mixinextras.sugar.Local; import gg.essential.mixins.impl.client.audio.ISoundExt; import net.minecraft.client.audio.ISound; +import net.minecraft.client.audio.ITickableSound; import net.minecraft.client.audio.SoundEventAccessorComposite; import net.minecraft.client.audio.SoundManager; import org.spongepowered.asm.mixin.Mixin; @@ -21,11 +23,29 @@ @Mixin(SoundManager.class) public class Mixin_ISoundExt_createAccessor { - @ModifyExpressionValue(method = "playSound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/audio/SoundHandler;getSound(Lnet/minecraft/util/ResourceLocation;)Lnet/minecraft/client/audio/SoundEventAccessorComposite;")) + private static final String GET_SOUND = "Lnet/minecraft/client/audio/SoundHandler;getSound(Lnet/minecraft/util/ResourceLocation;)Lnet/minecraft/client/audio/SoundEventAccessorComposite;"; + + @ModifyExpressionValue(method = "playSound", at = @At(value = "INVOKE", target = GET_SOUND)) private SoundEventAccessorComposite resolveEssentialSoundAccessor(SoundEventAccessorComposite result, ISound sound) { if (result == null && sound instanceof ISoundExt) { result = ((ISoundExt) sound).essential$createAccessor(); } return result; } + + @ModifyExpressionValue(method = "updateAllSounds", at = @At(value = "INVOKE", target = GET_SOUND, ordinal = 0)) + private SoundEventAccessorComposite resolveEssentialSoundAccessor$0(SoundEventAccessorComposite result, @Local ITickableSound sound) { + if (result == null && sound instanceof ISoundExt) { + result = ((ISoundExt) sound).essential$createAccessor(); + } + return result; + } + + @ModifyExpressionValue(method = "updateAllSounds", at = @At(value = "INVOKE", target = GET_SOUND, ordinal = 1)) + private SoundEventAccessorComposite resolveEssentialSoundAccessor$1(SoundEventAccessorComposite result, @Local ISound sound) { + if (result == null && sound instanceof ISoundExt) { + result = ((ISoundExt) sound).essential$createAccessor(); + } + return result; + } }