From 5e3a87e6c7ff2f7bb4a0349e4477611f014ac569 Mon Sep 17 00:00:00 2001 From: "Rian (Reza Mouna Hendrian)" <52914632+Rian8337@users.noreply.github.com> Date: Wed, 25 Dec 2024 23:11:04 +0800 Subject: [PATCH 01/87] Use "best PP" replay endpoint when opening a replay in pp beatmap leaderboard --- src/ru/nsu/ccfit/zuev/osu/online/OnlineManager.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/online/OnlineManager.java b/src/ru/nsu/ccfit/zuev/osu/online/OnlineManager.java index 834d40cc6..6ef71eaf9 100644 --- a/src/ru/nsu/ccfit/zuev/osu/online/OnlineManager.java +++ b/src/ru/nsu/ccfit/zuev/osu/online/OnlineManager.java @@ -63,7 +63,10 @@ public static OnlineManager getInstance() { } public static String getReplayURL(int playID) { - return endpoint + "upload/" + playID + ".odr"; + return switch (Config.getBeatmapLeaderboardScoringMode()) { + case SCORE -> endpoint + "upload/" + playID + ".odr"; + case PP -> endpoint + "bestpp/" + playID + ".odr"; + }; } public void init() { From 102dbcf518d539f1f22414a2786bb050250b317e Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 16:18:55 -0300 Subject: [PATCH 02/87] Document DepthInfo --- src/com/reco1l/andengine/DepthInfo.kt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/com/reco1l/andengine/DepthInfo.kt b/src/com/reco1l/andengine/DepthInfo.kt index 10e01097b..697bf103d 100644 --- a/src/com/reco1l/andengine/DepthInfo.kt +++ b/src/com/reco1l/andengine/DepthInfo.kt @@ -3,12 +3,24 @@ package com.reco1l.andengine import org.anddev.andengine.opengl.util.GLHelper import javax.microedition.khronos.opengles.GL10 +/** + * Information about how to behave with the depth buffer. + */ data class DepthInfo( + /** + * The depth function to use. + */ val function: DepthFunction, + /** + * Whether to write to the depth buffer. + */ val mask: Boolean, + /** + * Whether to clear the depth buffer. + */ val clear: Boolean ) { From 98d4df38ed0dbc950788bb91f9626a921884b5ba Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 16:19:24 -0300 Subject: [PATCH 03/87] Remove RoundedBox vertex buffer pre-initialization --- src/com/reco1l/andengine/shape/Box.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/shape/Box.kt b/src/com/reco1l/andengine/shape/Box.kt index 0450337fe..b8a30de39 100644 --- a/src/com/reco1l/andengine/shape/Box.kt +++ b/src/com/reco1l/andengine/shape/Box.kt @@ -59,7 +59,7 @@ open class Box : ExtendedEntity(vertexBuffer = BoxVertexBuffer()) { } -open class RoundedBox(segmentsPerArc: Int = 10) : ExtendedEntity(RoundedBoxVertexBuffer(segmentsPerArc)) { +open class RoundedBox : ExtendedEntity() { /** * The corner radius of the rectangle. From 7d8a720fd4b45a995dbb2b32b59afcbb2ff5217f Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 16:20:22 -0300 Subject: [PATCH 04/87] Register touch area to all `Scene` types --- src/com/reco1l/andengine/ExtendedEntity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index fc759ef0a..620906199 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -368,7 +368,7 @@ abstract class ExtendedEntity( override fun setParent(pEntity: IEntity?) { (parent as? Scene)?.unregisterTouchArea(this) super.setParent(pEntity) - (pEntity as? ExtendedScene)?.registerTouchArea(this) + (pEntity as? Scene)?.registerTouchArea(this) } From f37d0e3bb0700f7b8d90bf326bc1bb479e63f626 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 16:24:18 -0300 Subject: [PATCH 05/87] Inherit colors only if parents are of type `ExtendedEntity` --- src/com/reco1l/andengine/ExtendedEntity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 620906199..42285afec 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -472,7 +472,7 @@ abstract class ExtendedEntity( if (inheritColor) { var parent = parent - while (parent != null) { + while (parent is ExtendedEntity && parent.inheritColor) { red *= parent.red green *= parent.green From 11db3197cf9b53867f01d6fd1ccee7b880d815d2 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 16:25:09 -0300 Subject: [PATCH 06/87] Propagate input events along children in ExtendedEntity --- src/com/reco1l/andengine/ExtendedEntity.kt | 30 +++++++++++++++++++ .../reco1l/andengine/container/Container.kt | 19 ------------ 2 files changed, 30 insertions(+), 19 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 42285afec..e8b1eb74e 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -856,6 +856,36 @@ abstract class ExtendedEntity( return modifier } + + // Input + + override fun onAreaTouched( + event: TouchEvent, + localX: Float, + localY: Float + ): Boolean { + + try { + for (i in childCount - 1 downTo 0) { + val child = getChild(i) + + val transformedX = localX - child.getDrawX() + val transformedY = localY - child.getDrawY() + + if (child is ITouchArea && child.contains(transformedX, transformedY)) { + + if (child.onAreaTouched(event, transformedX, transformedY)) { + return true + } + } + } + } catch (e: IndexOutOfBoundsException) { + Log.e("ExtendedEntity", "A child entity was removed during touch event propagation.", e) + } + + return false + } + } diff --git a/src/com/reco1l/andengine/container/Container.kt b/src/com/reco1l/andengine/container/Container.kt index b320fe566..822f8452c 100644 --- a/src/com/reco1l/andengine/container/Container.kt +++ b/src/com/reco1l/andengine/container/Container.kt @@ -166,25 +166,6 @@ open class Container : ExtendedEntity() { override fun onUpdateVertexBuffer() {} override fun drawVertices(pGL: GL10, pCamera: Camera) {} - - // Input - - override fun onAreaTouched(event: TouchEvent, localX: Float, localY: Float): Boolean { - - if (mChildren != null) { - try { - mChildren.fastForEach { - if (it is ITouchArea && it.contains(localX, localY)) { - return it.onAreaTouched(event, localX, localY) - } - } - } catch (e: IndexOutOfBoundsException) { - Log.e("Container", "A child entity was removed during touch event propagation.") - } - } - - return false - } } From 5693ba355c601cccf846fbf84cb4bb13908b04f3 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 16:25:46 -0300 Subject: [PATCH 07/87] Use `Vector2` to represent entities anchor and origin properties --- src/com/reco1l/andengine/Anchor.kt | 34 +++-- src/com/reco1l/andengine/ExtendedEntity.kt | 142 +++++++++------------ 2 files changed, 83 insertions(+), 93 deletions(-) diff --git a/src/com/reco1l/andengine/Anchor.kt b/src/com/reco1l/andengine/Anchor.kt index e3eaa05d8..448d33638 100644 --- a/src/com/reco1l/andengine/Anchor.kt +++ b/src/com/reco1l/andengine/Anchor.kt @@ -1,26 +1,34 @@ package com.reco1l.andengine -/** - * The anchor points in the range of [0, 1]. - */ -enum class Anchor(val factorX: Float, val factorY: Float) { +import com.rian.osu.math.Vector2 - TopLeft(0f, 0f), +object Anchor { - TopCenter(0.5f, 0f), + @JvmField + val TopLeft = Vector2(0f, 0f) - TopRight(1f, 0f), + @JvmField + val TopCenter = Vector2(0.5f, 0f) - CenterLeft(0f, 0.5f), + @JvmField + val TopRight = Vector2(1f, 0f) - Center(0.5f, 0.5f), + @JvmField + val CenterLeft = Vector2(0f, 0.5f) - CenterRight(1f, 0.5f), + @JvmField + val Center = Vector2(0.5f, 0.5f) - BottomLeft(0f, 1f), + @JvmField + val CenterRight = Vector2(1f, 0.5f) - BottomCenter(0.5f, 1f), + @JvmField + val BottomLeft = Vector2(0f, 1f) - BottomRight(1f, 1f) + @JvmField + val BottomCenter = Vector2(0.5f, 1f) + + @JvmField + val BottomRight = Vector2(1f, 1f) } \ No newline at end of file diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index e8b1eb74e..2dde6a853 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -4,16 +4,18 @@ import android.util.* import com.reco1l.andengine.container.* import com.reco1l.andengine.modifier.* import com.reco1l.framework.* +import com.rian.osu.math.Vector2 import org.anddev.andengine.engine.camera.* import org.anddev.andengine.entity.* import org.anddev.andengine.entity.scene.CameraScene import org.anddev.andengine.entity.scene.Scene +import org.anddev.andengine.entity.scene.Scene.ITouchArea import org.anddev.andengine.entity.shape.* +import org.anddev.andengine.input.touch.TouchEvent import org.anddev.andengine.opengl.util.* import org.anddev.andengine.opengl.vertex.* import org.anddev.andengine.util.Transformation import javax.microedition.khronos.opengles.* -import javax.microedition.khronos.opengles.GL10.* /** @@ -31,7 +33,7 @@ abstract class ExtendedEntity( * Determines which axes the entity should automatically adjust its size to. * * In this case the size will equal to the content size of the entity. Some - * types of entities requieres the user to manually set the size, in those + * types of entities requires the user to manually set the size, in those * cases this property might be ignored. */ open var autoSizeAxes = Axes.None @@ -119,9 +121,9 @@ abstract class ExtendedEntity( } /** - * The origin factor of the entity in the X axis. + * Where the entity should be anchored in the parent. */ - open var originX = 0f + open var anchor = Anchor.TopLeft set(value) { if (field != value) { field = value @@ -130,37 +132,9 @@ abstract class ExtendedEntity( } /** - * The origin factor of the entity in the Y axis. + * Where the entity's origin should be. */ - open var originY = 0f - set(value) { - if (field != value) { - field = value - invalidateTransformations() - } - } - - /** - * The anchor factor of the entity in the X axis. - * This is used to determine the position of the entity in a container. - * - * Note: This will not take effect if the entity is not a child of a [Container]. - */ - open var anchorX = 0f - set(value) { - if (field != value) { - field = value - invalidateTransformations() - } - } - - /** - * The anchor factor of the entity in the Y axis. - * This is used to determine the position of the entity in a container. - * - * Note: This will not take effect if the entity is not a child of a [Container]. - */ - open var anchorY = 0f + open var origin = Anchor.TopLeft set(value) { if (field != value) { field = value @@ -319,42 +293,6 @@ abstract class ExtendedEntity( return y + totalOffsetY } - /** - * The offset applied to the X axis according to the origin factor. - */ - open val originOffsetX: Float - get() = -(drawWidth * originX) - - /** - * The offset applied to the Y axis according to the origin factor. - */ - open val originOffsetY: Float - get() = -(drawHeight * originY) - - /** - * The offset applied to the X axis according to the anchor factor. - */ - open val anchorOffsetX: Float - get() = getParentWidth() * anchorX - - /** - * The offset applied to the Y axis according to the anchor factor. - */ - open val anchorOffsetY: Float - get() = getParentHeight() * anchorY - - /** - * The total offset applied to the X axis. - */ - open val totalOffsetX - get() = originOffsetX + anchorOffsetX + translationX - - /** - * The total offset applied to the Y axis. - */ - open val totalOffsetY - get() = originOffsetY + anchorOffsetY + translationY - private var width = 0f @@ -374,14 +312,14 @@ abstract class ExtendedEntity( // Positions - open fun setAnchor(anchor: Anchor) { - anchorX = anchor.factorX - anchorY = anchor.factorY + open fun setAnchor(value: Vector2): ExtendedEntity { + anchor = value + return this } - open fun setOrigin(origin: Anchor) { - originX = origin.factorX - originY = origin.factorY + open fun setOrigin(value: Vector2): ExtendedEntity { + origin = value + return this } fun setRelativePosition(x: Float, y: Float) { @@ -443,8 +381,8 @@ abstract class ExtendedEntity( override fun applyRotation(pGL: GL10) { // This will ensure getSceneCenterCoordinates() applies the correct transformation. - mRotationCenterX = drawWidth * originX - mRotationCenterY = drawHeight * originY + mRotationCenterX = -originOffsetX + mRotationCenterY = -originOffsetY if (rotation != 0f) { pGL.glRotatef(rotation, 0f, 0f, 1f) @@ -454,8 +392,8 @@ abstract class ExtendedEntity( override fun applyScale(pGL: GL10) { // This will ensure getSceneCenterCoordinates() applies the correct transformation. - mScaleCenterX = drawWidth * originX - mScaleCenterY = drawHeight * originY + mScaleCenterX = -originOffsetX + mScaleCenterY = -originOffsetY if (scaleX != 1f || scaleY != 1f) { pGL.glScalef(scaleX, scaleY, 1f) @@ -889,6 +827,42 @@ abstract class ExtendedEntity( } +/** + * The total offset applied to the X axis. + */ +val ExtendedEntity.totalOffsetX + get() = originOffsetX + anchorOffsetX + translationX + +/** + * The total offset applied to the Y axis. + */ +val ExtendedEntity.totalOffsetY + get() = originOffsetY + anchorOffsetY + translationY + +/** + * The offset applied to the X axis according to the anchor factor. + */ +val ExtendedEntity.anchorOffsetX: Float + get() = getParentWidth() * anchor.x + +/** + * The offset applied to the Y axis according to the anchor factor. + */ +val ExtendedEntity.anchorOffsetY: Float + get() = getParentHeight() * anchor.y + +/** + * The offset applied to the X axis according to the origin factor. + */ +val ExtendedEntity.originOffsetX: Float + get() = -(drawWidth * origin.y) + +/** + * The offset applied to the Y axis according to the origin factor. + */ +val ExtendedEntity.originOffsetY: Float + get() = -(drawHeight * origin.y) + /** * Returns the width of the parent entity. */ @@ -945,3 +919,11 @@ fun IEntity.getDrawY(): Float = when (this) { else -> 0f } +/** + * Attaches the entity to a parent. + */ +infix fun T.attachTo(parent: IEntity): T { + parent.attachChild(this) + return this +} + From 88dc64e133b6ea03b16774f6326f2cd9f19ec683 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 16:38:34 -0300 Subject: [PATCH 08/87] Fix compilation errors --- src/com/reco1l/andengine/ExtendedEntity.kt | 12 +--------- .../container/ConstraintContainer.kt | 4 ++-- src/com/reco1l/osu/hitobjects/FollowPoints.kt | 2 +- src/com/reco1l/osu/hitobjects/SliderTicks.kt | 4 ++-- src/com/reco1l/osu/playfield/CirclePiece.kt | 17 +++++++------ .../osu/playfield/CircularSongProgress.kt | 20 ++++++++-------- src/com/reco1l/osu/playfield/Counters.kt | 24 +++++++++---------- src/com/reco1l/osu/playfield/HealthBar.kt | 6 ++--- 8 files changed, 39 insertions(+), 50 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 2dde6a853..9c5216e6c 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -312,16 +312,6 @@ abstract class ExtendedEntity( // Positions - open fun setAnchor(value: Vector2): ExtendedEntity { - anchor = value - return this - } - - open fun setOrigin(value: Vector2): ExtendedEntity { - origin = value - return this - } - fun setRelativePosition(x: Float, y: Float) { relativePositionAxes = Axes.Both setPosition(x, y) @@ -855,7 +845,7 @@ val ExtendedEntity.anchorOffsetY: Float * The offset applied to the X axis according to the origin factor. */ val ExtendedEntity.originOffsetX: Float - get() = -(drawWidth * origin.y) + get() = -(drawWidth * origin.x) /** * The offset applied to the Y axis according to the origin factor. diff --git a/src/com/reco1l/andengine/container/ConstraintContainer.kt b/src/com/reco1l/andengine/container/ConstraintContainer.kt index e13699c94..31d7f6f04 100644 --- a/src/com/reco1l/andengine/container/ConstraintContainer.kt +++ b/src/com/reco1l/andengine/container/ConstraintContainer.kt @@ -20,7 +20,7 @@ class ConstraintContainer : Container() { val target = constraints[child] ?: this val targetX = target.getDrawX() - val anchorOffsetX = target.getDrawWidth() * child.anchorX + val anchorOffsetX = target.getDrawWidth() * child.anchor.x var childX = child.x @@ -38,7 +38,7 @@ class ConstraintContainer : Container() { val target = constraints[child] ?: this val targetY = target.getDrawY() - val anchorOffsetY = target.getDrawHeight() * child.anchorY + val anchorOffsetY = target.getDrawHeight() * child.anchor.y var childY = child.y diff --git a/src/com/reco1l/osu/hitobjects/FollowPoints.kt b/src/com/reco1l/osu/hitobjects/FollowPoints.kt index 81d604db1..b604d6666 100644 --- a/src/com/reco1l/osu/hitobjects/FollowPoints.kt +++ b/src/com/reco1l/osu/hitobjects/FollowPoints.kt @@ -117,8 +117,8 @@ object FollowPointConnection { fp.clearEntityModifiers() fp.setPosition(pointStartX, pointStartY) - fp.setOrigin(Anchor.Center) fp.setScale(1.5f * scale) + fp.origin = Anchor.Center fp.rotation = rotation fp.alpha = 0f diff --git a/src/com/reco1l/osu/hitobjects/SliderTicks.kt b/src/com/reco1l/osu/hitobjects/SliderTicks.kt index 1b852a846..a87ac6d91 100644 --- a/src/com/reco1l/osu/hitobjects/SliderTicks.kt +++ b/src/com/reco1l/osu/hitobjects/SliderTicks.kt @@ -1,5 +1,6 @@ package com.reco1l.osu.hitobjects +import com.reco1l.andengine.Anchor import com.reco1l.andengine.container.* import com.reco1l.andengine.sprite.* import com.reco1l.framework.* @@ -43,8 +44,7 @@ class SliderTickSprite : ExtendedSprite() { init { textureRegion = ResourceManager.getInstance().getTexture("sliderscorepoint") - originX = 0.5f - originY = 0.5f + origin = Anchor.Center } override fun onDetached() { diff --git a/src/com/reco1l/osu/playfield/CirclePiece.kt b/src/com/reco1l/osu/playfield/CirclePiece.kt index b58a6d4c2..48c3d9afe 100644 --- a/src/com/reco1l/osu/playfield/CirclePiece.kt +++ b/src/com/reco1l/osu/playfield/CirclePiece.kt @@ -3,6 +3,7 @@ package com.reco1l.osu.playfield import com.reco1l.andengine.* import com.reco1l.andengine.container.* import com.reco1l.andengine.sprite.* +import com.rian.osu.math.Vector2 import ru.nsu.ccfit.zuev.osu.* import ru.nsu.ccfit.zuev.skins.* @@ -14,15 +15,13 @@ open class CirclePiece( ) : Container() { - override var originX = 0.5f - - override var originY = 0.5f + override var origin = Anchor.Center private val circle = ExtendedSprite().also { - it.setOrigin(Anchor.Center) - it.setAnchor(Anchor.Center) + it.origin = Anchor.Center + it.anchor = Anchor.Center it.textureRegion = ResourceManager.getInstance().getTexture(circleTexture) attachChild(it) @@ -30,8 +29,8 @@ open class CirclePiece( private val overlay = ExtendedSprite().also { - it.setOrigin(Anchor.Center) - it.setAnchor(Anchor.Center) + it.origin = Anchor.Center + it.anchor = Anchor.Center it.textureRegion = ResourceManager.getInstance().getTexture(overlayTexture) attachChild(it) @@ -48,8 +47,8 @@ class NumberedCirclePiece(circleTexture: String, overlayTexture: String) : Circl private val number = SpriteFont(OsuSkin.get().hitCirclePrefix).also { - it.setOrigin(Anchor.Center) - it.setAnchor(Anchor.Center) + it.origin = Anchor.Center + it.anchor = Anchor.Center it.spacing = -OsuSkin.get().hitCircleOverlap attachChild(it) diff --git a/src/com/reco1l/osu/playfield/CircularSongProgress.kt b/src/com/reco1l/osu/playfield/CircularSongProgress.kt index d955ab748..1da0373e3 100644 --- a/src/com/reco1l/osu/playfield/CircularSongProgress.kt +++ b/src/com/reco1l/osu/playfield/CircularSongProgress.kt @@ -21,8 +21,8 @@ class CircularSongProgress : Container() { Circle().also { clear -> clear.setSize(30f, 30f) - clear.setAnchor(Anchor.Center) - clear.setOrigin(Anchor.Center) + clear.anchor = Anchor.Center + clear.origin = Anchor.Center clear.color = ColorARGB.Transparent clear.depthInfo = DepthInfo.Clear @@ -32,8 +32,8 @@ class CircularSongProgress : Container() { Circle().also { background -> background.setSize(33f, 33f) - background.setAnchor(Anchor.Center) - background.setOrigin(Anchor.Center) + background.anchor = Anchor.Center + background.origin = Anchor.Center background.color = ColorARGB.White background.depthInfo = DepthInfo.Default @@ -43,8 +43,8 @@ class CircularSongProgress : Container() { circularProgress = Circle().also { progress -> progress.setSize(30f, 30f) - progress.setAnchor(Anchor.Center) - progress.setOrigin(Anchor.Center) + progress.anchor = Anchor.Center + progress.origin = Anchor.Center progress.alpha = 0.6f attachChild(progress) @@ -54,8 +54,8 @@ class CircularSongProgress : Container() { Circle().also { dot -> dot.setSize(4f, 4f) - dot.setAnchor(Anchor.Center) - dot.setOrigin(Anchor.Center) + dot.anchor = Anchor.Center + dot.origin = Anchor.Center dot.color = ColorARGB.White attachChild(dot) @@ -63,8 +63,8 @@ class CircularSongProgress : Container() { onMeasureContentSize() - setAnchor(Anchor.TopRight) - setOrigin(Anchor.CenterRight) + anchor = Anchor.TopRight + origin = Anchor.CenterRight } diff --git a/src/com/reco1l/osu/playfield/Counters.kt b/src/com/reco1l/osu/playfield/Counters.kt index 3d9e15cde..aa3aa380a 100644 --- a/src/com/reco1l/osu/playfield/Counters.kt +++ b/src/com/reco1l/osu/playfield/Counters.kt @@ -17,8 +17,8 @@ class ScoreCounter : SpriteFont(OsuSkin.get().scorePrefix) { init { - setAnchor(Anchor.TopRight) - setOrigin(Anchor.TopRight) + anchor = Anchor.TopRight + origin = Anchor.TopRight setScale(0.96f) x = -10f @@ -35,8 +35,8 @@ class ScoreCounter : SpriteFont(OsuSkin.get().scorePrefix) { class PPCounter(private val algorithm: DifficultyAlgorithm) : SpriteFont(OsuSkin.get().scorePrefix) { init { - setAnchor(Anchor.TopRight) - setOrigin(Anchor.TopRight) + anchor = Anchor.TopRight + origin = Anchor.TopRight setScale(0.6f * 0.96f) setValue(0.0) } @@ -55,8 +55,8 @@ class AccuracyCounter : SpriteFont(OsuSkin.get().scorePrefix) { init { - setAnchor(Anchor.TopRight) - setOrigin(Anchor.TopRight) + anchor = Anchor.TopRight + origin = Anchor.TopRight setScale(0.6f * 0.96f) setPosition(-17f, 9f) text = "100.00%" @@ -77,8 +77,8 @@ class ComboCounter : Container() { it.alpha = 0f it.text = "0x" - it.setAnchor(Anchor.BottomLeft) - it.setOrigin(Anchor.BottomLeft) + it.anchor = Anchor.BottomLeft + it.origin = Anchor.BottomLeft // In stable, the bigger pop out scales a bit to the left it.translationX = -3f @@ -94,8 +94,8 @@ class ComboCounter : Container() { private val displayedCountTextSprite = SpriteFont(OsuSkin.get().comboPrefix).also { it.text = "0x" - it.setAnchor(Anchor.BottomLeft) - it.setOrigin(Anchor.BottomLeft) + it.anchor = Anchor.BottomLeft + it.origin = Anchor.BottomLeft it.translationY = -(FONT_HEIGHT_RATIO * it.drawHeight + VERTICAL_OFFSET) @@ -114,8 +114,8 @@ class ComboCounter : Container() { init { - setAnchor(Anchor.BottomLeft) - setOrigin(Anchor.BottomLeft) + anchor = Anchor.BottomLeft + origin = Anchor.BottomLeft setPosition(10f, -10f) setScale(1.28f) } diff --git a/src/com/reco1l/osu/playfield/HealthBar.kt b/src/com/reco1l/osu/playfield/HealthBar.kt index 31849ed08..37f74bce9 100644 --- a/src/com/reco1l/osu/playfield/HealthBar.kt +++ b/src/com/reco1l/osu/playfield/HealthBar.kt @@ -49,7 +49,7 @@ class HealthBar(private val statistics: StatisticV2) : Container() { attachChild(ExtendedSprite().apply { textureRegion = backgroundTexture }) fillClear = Box() - fillClear.setOrigin(Anchor.TopRight) + fillClear.origin = Anchor.TopRight fillClear.depthInfo = DepthInfo.Clear fillClear.alpha = 0f attachChild(fillClear) @@ -60,11 +60,11 @@ class HealthBar(private val statistics: StatisticV2) : Container() { attachChild(fill) marker = ExtendedSprite() - marker.setOrigin(Anchor.Center) + marker.origin = Anchor.Center attachChild(marker) explode = ExtendedSprite() - explode.setOrigin(Anchor.Center) + explode.origin = Anchor.Center explode.blendingFunction = BlendingFunction.Additive explode.alpha = 0f attachChild(explode) From 964949e117a205c624f4119e76794ed07ca6ec3d Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 22:54:35 -0300 Subject: [PATCH 09/87] Always inherit colors --- src/com/reco1l/andengine/ExtendedEntity.kt | 44 +++++++++++++--------- 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 9c5216e6c..b721c9ff6 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -174,6 +174,21 @@ abstract class ExtendedEntity( */ open var depthInfo: DepthInfo? = null + /** + * The blending information of the entity. + */ + open var blendInfo: BlendInfo? = null + set(value) { + if (field != value) { + field = value + + if (value != null) { + mSourceBlendFunction = value.function.source + mDestinationBlendFunction = value.function.destination + } + } + } + /** * The color of the entity boxed in a [ColorARGB] object. */ @@ -186,10 +201,6 @@ abstract class ExtendedEntity( mAlpha = value.alpha } - /** - * Whether the color should be inherited from all the parents in the hierarchy. - */ - open var inheritColor = true /** * The color blending function. @@ -397,23 +408,20 @@ abstract class ExtendedEntity( var blue = mBlue var alpha = mAlpha - if (inheritColor) { + var parent = parent + while (parent != null) { - var parent = parent - while (parent is ExtendedEntity && parent.inheritColor) { + red *= parent.red + green *= parent.green + blue *= parent.blue + alpha *= parent.alpha - red *= parent.red - green *= parent.green - blue *= parent.blue - alpha *= parent.alpha - - // We'll assume at this point there's no need to keep multiplying. - if (red == 0f && green == 0f && blue == 0f && alpha == 0f) { - break - } - - parent = parent.parent + // We'll assume at this point there's no need to keep multiplying. + if (red == 0f && green == 0f && blue == 0f && alpha == 0f) { + break } + + parent = parent.parent } GLHelper.setColor(pGL, red, green, blue, alpha) From 12b2d4a1d372f356ecbd186464bcf62609e20f77 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 23:00:09 -0300 Subject: [PATCH 10/87] Introduce BlendInfo --- src/com/reco1l/andengine/BlendInfo.kt | 59 ++++++++++++++++++++++ src/com/reco1l/andengine/ExtendedEntity.kt | 37 +++----------- src/com/reco1l/osu/playfield/HealthBar.kt | 7 +-- 3 files changed, 70 insertions(+), 33 deletions(-) create mode 100644 src/com/reco1l/andengine/BlendInfo.kt diff --git a/src/com/reco1l/andengine/BlendInfo.kt b/src/com/reco1l/andengine/BlendInfo.kt new file mode 100644 index 000000000..e3776b639 --- /dev/null +++ b/src/com/reco1l/andengine/BlendInfo.kt @@ -0,0 +1,59 @@ +package com.reco1l.andengine + +import javax.microedition.khronos.opengles.GL10 + +data class BlendInfo( + + /** + * The blending function to use. + */ + var function: BlendingFunction, + + /** + * Whether to mask the red channel. + */ + var redMask: Boolean = true, + + /** + * Whether to mask the green channel. + */ + var greenMask: Boolean = true, + + /** + * Whether to mask the blue channel. + */ + var blueMask: Boolean = true, + + /** + * Whether to mask the alpha channel. + */ + var alphaMask: Boolean = true, + + /** + * Whether to clear the color buffer. + */ + var clear: Boolean = false + +) { + + fun apply(gl: GL10) { + + gl.glColorMask(redMask, greenMask, blueMask, alphaMask) + + if (function != BlendingFunction.Inherit) { + gl.glBlendFunc(function.source, function.destination) + } + + if (clear) { + gl.glClear(GL10.GL_COLOR_BUFFER_BIT) + } + } + + + companion object { + + val Inherit = BlendInfo(BlendingFunction.Inherit) + + } + +} \ No newline at end of file diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index b721c9ff6..43bdb7531 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -201,22 +201,6 @@ abstract class ExtendedEntity( mAlpha = value.alpha } - - /** - * The color blending function. - */ - open var blendingFunction: BlendingFunction? = null - set(value) { - if (field != value) { - field = value - - if (value != null) { - mSourceBlendFunction = value.source - mDestinationBlendFunction = value.destination - } - } - } - /** * The width of the content inside the entity. */ @@ -429,22 +413,13 @@ abstract class ExtendedEntity( protected open fun applyBlending(pGL: GL10) { - // If there's a blending function set, apply it instead of the engine's method. - val blendingFunction = blendingFunction - - if (blendingFunction != null) { + blendInfo?.apply(pGL) ?: GLHelper.blendFunction(pGL, mSourceBlendFunction, mDestinationBlendFunction) - val parent = parent as? ExtendedEntity - - // If the blending function is set to inherit, apply the parent's blending function. - if (blendingFunction == BlendingFunction.Inherit && parent != null) { + if (blendInfo?.function == BlendingFunction.Inherit) { + val parent = parent + if (parent is ExtendedEntity) { GLHelper.blendFunction(pGL, parent.mSourceBlendFunction, parent.mDestinationBlendFunction) - } else { - GLHelper.blendFunction(pGL, blendingFunction.source, blendingFunction.destination) } - - } else { - GLHelper.blendFunction(pGL, mSourceBlendFunction, mDestinationBlendFunction) } } @@ -778,7 +753,9 @@ abstract class ExtendedEntity( // Transformation override fun setBlendFunction(pSourceBlendFunction: Int, pDestinationBlendFunction: Int) { - blendingFunction = null + if (blendInfo != null) { + Log.w("ExtendedEntity", "BlendInfo is set, use blendInfo property to change the blending function.") + } super.setBlendFunction(pSourceBlendFunction, pDestinationBlendFunction) } diff --git a/src/com/reco1l/osu/playfield/HealthBar.kt b/src/com/reco1l/osu/playfield/HealthBar.kt index 37f74bce9..a764d516d 100644 --- a/src/com/reco1l/osu/playfield/HealthBar.kt +++ b/src/com/reco1l/osu/playfield/HealthBar.kt @@ -61,11 +61,12 @@ class HealthBar(private val statistics: StatisticV2) : Container() { marker = ExtendedSprite() marker.origin = Anchor.Center + marker.blendInfo = BlendInfo(BlendingFunction.Additive) attachChild(marker) explode = ExtendedSprite() explode.origin = Anchor.Center - explode.blendingFunction = BlendingFunction.Additive + explode.blendInfo = BlendInfo(BlendingFunction.Additive) explode.alpha = 0f attachChild(explode) @@ -116,7 +117,7 @@ class HealthBar(private val statistics: StatisticV2) : Container() { fill.color = color marker.color = color - marker.blendingFunction = if (statistics.hp < EPIC_CUTOFF) BlendingFunction.Inherit else BlendingFunction.Additive + marker.blendInfo?.function = if (statistics.hp < EPIC_CUTOFF) BlendingFunction.Inherit else BlendingFunction.Additive } else { @@ -139,7 +140,7 @@ class HealthBar(private val statistics: StatisticV2) : Container() { bulge() explode.clearEntityModifiers() - explode.blendingFunction = if (isEpic) BlendingFunction.Additive else BlendingFunction.Inherit + explode.blendInfo?.function = if (isEpic) BlendingFunction.Additive else BlendingFunction.Inherit explode.alpha = 1f explode.setScale(1f) From 4f3f39f659146950529451f3fae8e07102cb5427 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 23:18:38 -0300 Subject: [PATCH 11/87] Allow to use custom rotation and scale center / Reverting back old behavior --- src/com/reco1l/andengine/ExtendedEntity.kt | 109 +++++++++++---------- 1 file changed, 56 insertions(+), 53 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 43bdb7531..6f2ce5d1d 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -4,7 +4,6 @@ import android.util.* import com.reco1l.andengine.container.* import com.reco1l.andengine.modifier.* import com.reco1l.framework.* -import com.rian.osu.math.Vector2 import org.anddev.andengine.engine.camera.* import org.anddev.andengine.entity.* import org.anddev.andengine.entity.scene.CameraScene @@ -138,6 +137,12 @@ abstract class ExtendedEntity( set(value) { if (field != value) { field = value + + mRotationCenterX = origin.x + mRotationCenterY = origin.y + mScaleCenterX = origin.x + mScaleCenterY = origin.y + invalidateTransformations() } } @@ -365,22 +370,36 @@ abstract class ExtendedEntity( override fun applyRotation(pGL: GL10) { - // This will ensure getSceneCenterCoordinates() applies the correct transformation. - mRotationCenterX = -originOffsetX - mRotationCenterY = -originOffsetY + if (rotation == 0f) { + return + } + + val offsetX = drawWidth * mRotationCenterX + val offsetY = drawHeight * mRotationCenterY - if (rotation != 0f) { + if (offsetX > 0f || offsetY > 0f) { + pGL.glTranslatef(offsetX, offsetY, 0f) + pGL.glRotatef(rotation, 0f, 0f, 1f) + pGL.glTranslatef(-offsetX, -offsetY, 0f) + } else { pGL.glRotatef(rotation, 0f, 0f, 1f) } } override fun applyScale(pGL: GL10) { - // This will ensure getSceneCenterCoordinates() applies the correct transformation. - mScaleCenterX = -originOffsetX - mScaleCenterY = -originOffsetY + if (scaleX == 1f && scaleY == 1f) { + return + } + + val offsetX = drawWidth * mScaleCenterX + val offsetY = drawHeight * mScaleCenterY - if (scaleX != 1f || scaleY != 1f) { + if (offsetX > 0f || offsetY > 0f) { + pGL.glTranslatef(offsetX, offsetY, 0f) + pGL.glScalef(scaleX, scaleY, 1f) + pGL.glTranslatef(-offsetX, -offsetY, 0f) + } else { pGL.glScalef(scaleX, scaleY, 1f) } } @@ -425,14 +444,8 @@ abstract class ExtendedEntity( override fun onApplyTransformations(pGL: GL10, camera: Camera) { applyTranslation(pGL, camera) - - if (rotation != 0f || scaleX != 1f || scaleY != 1f) { - pGL.glTranslatef(-originOffsetX, -originOffsetY, 0f) - applyRotation(pGL) - applyScale(pGL) - pGL.glTranslatef(originOffsetX, originOffsetY, 0f) - } - + applyRotation(pGL) + applyScale(pGL) applyColor(pGL) applyBlending(pGL) } @@ -649,24 +662,6 @@ abstract class ExtendedEntity( @Deprecated("Base height is not preserved in ExtendedEntity, use getHeight() instead.") override fun getBaseHeight() = height - @Deprecated("Rotation center is determined by the entity's origin, use setOrigin() instead.") - final override fun setRotationCenter(pRotationCenterX: Float, pRotationCenterY: Float) {} - - @Deprecated("Rotation center is determined by the entity's origin, use setOrigin() instead.") - final override fun setRotationCenterX(pRotationCenterX: Float) {} - - @Deprecated("Rotation center is determined by the entity's origin, use setOrigin() instead.") - final override fun setRotationCenterY(pRotationCenterY: Float) {} - - @Deprecated("Scale center is determined by the entity's origin, use setOrigin() instead.") - final override fun setScaleCenter(pScaleCenterX: Float, pScaleCenterY: Float) {} - - @Deprecated("Scale center is determined by the entity's origin, use setOrigin() instead.") - final override fun setScaleCenterX(pScaleCenterX: Float) {} - - @Deprecated("Scale center is determined by the entity's origin, use setOrigin() instead.") - final override fun setScaleCenterY(pScaleCenterY: Float) {} - // Collision @@ -698,18 +693,22 @@ abstract class ExtendedEntity( if (mLocalToParentTransformationDirty) { mLocalToParentTransformation.setToIdentity() - if (scaleX != 1f || scaleY != 1f || rotation != 0f) { - mLocalToParentTransformation.postTranslate(originOffsetX, originOffsetY) + if (scaleX != 1f || scaleY != 1f) { + val offsetX = drawWidth * mScaleCenterX + val offsetY = drawHeight * mScaleCenterY - if (scaleX != 1f || scaleY != 1f) { - mLocalToParentTransformation.postScale(scaleX, scaleY) - } + mLocalToParentTransformation.postTranslate(-offsetX, -offsetY) + mLocalToParentTransformation.postScale(scaleX, scaleY) + mLocalToParentTransformation.postTranslate(offsetX, offsetY) + } - if (rotation != 0f) { - mLocalToParentTransformation.postRotate(rotation) - } + if (rotation != 0f) { + val offsetX = drawWidth * mRotationCenterX + val offsetY = drawHeight * mRotationCenterY - mLocalToParentTransformation.postTranslate(-originOffsetX, -originOffsetY) + mLocalToParentTransformation.postTranslate(-offsetX, -offsetY) + mLocalToParentTransformation.postRotate(rotation) + mLocalToParentTransformation.postTranslate(offsetX, offsetY) } mLocalToParentTransformation.postTranslate(drawX, drawY) @@ -729,18 +728,22 @@ abstract class ExtendedEntity( mParentToLocalTransformation.setToIdentity() mParentToLocalTransformation.postTranslate(-drawX, -drawY) - if (scaleX != 1f || scaleY != 1f || rotation != 0f) { - mParentToLocalTransformation.postTranslate(originOffsetX, originOffsetY) + if (scaleX != 1f || scaleY != 1f) { + val offsetX = drawWidth * mScaleCenterX + val offsetY = drawHeight * mScaleCenterY - if (rotation != 0f) { - mParentToLocalTransformation.postRotate(-rotation) - } + mParentToLocalTransformation.postTranslate(-offsetX, -offsetY) + mParentToLocalTransformation.postScale(1 / scaleX, 1 / scaleY) + mParentToLocalTransformation.postTranslate(offsetX, offsetY) + } - if (scaleX != 1f || scaleY != 1f) { - mParentToLocalTransformation.postScale(1 / scaleX, 1 / scaleY) - } + if (rotation != 0f) { + val offsetX = drawWidth * mRotationCenterX + val offsetY = drawHeight * mRotationCenterY - mParentToLocalTransformation.postTranslate(-originOffsetX, -originOffsetY) + mParentToLocalTransformation.postTranslate(-offsetX, -offsetY) + mParentToLocalTransformation.postRotate(-rotation) + mParentToLocalTransformation.postTranslate(offsetX, offsetY) } mParentToLocalTransformationDirty = false From 6821b776d1c93094a3f796792aac0cbee7f4b299 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 23:23:13 -0300 Subject: [PATCH 12/87] Revert "Register touch area to all `Scene` types" --- src/com/reco1l/andengine/ExtendedEntity.kt | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 6f2ce5d1d..e3df2a714 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -304,9 +304,17 @@ abstract class ExtendedEntity( // Attachment override fun setParent(pEntity: IEntity?) { - (parent as? Scene)?.unregisterTouchArea(this) + + val parent = parent + if (parent is Scene) { + parent.unregisterTouchArea(this) + } + super.setParent(pEntity) - (pEntity as? Scene)?.registerTouchArea(this) + + if (pEntity is ExtendedScene) { + pEntity.registerTouchArea(this) + } } From e28c63081f4fb496cd6bf2bfed224cf63437c4d8 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Wed, 25 Dec 2024 23:29:57 -0300 Subject: [PATCH 13/87] Fix scale center not being applied properly in CirclePiece --- src/com/reco1l/osu/playfield/CirclePiece.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/osu/playfield/CirclePiece.kt b/src/com/reco1l/osu/playfield/CirclePiece.kt index 48c3d9afe..85a6a17d7 100644 --- a/src/com/reco1l/osu/playfield/CirclePiece.kt +++ b/src/com/reco1l/osu/playfield/CirclePiece.kt @@ -15,7 +15,9 @@ open class CirclePiece( ) : Container() { - override var origin = Anchor.Center + init { + origin = Anchor.Center + } private val circle = ExtendedSprite().also { From 4d1776d2ce4c5a48f39efe06963dd7d0ebb59776 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Thu, 26 Dec 2024 22:09:11 +0800 Subject: [PATCH 14/87] Reword some KDocs --- src/com/reco1l/osu/UpdateManager.kt | 2 +- src/com/reco1l/osu/ui/PromptDialog.kt | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/com/reco1l/osu/UpdateManager.kt b/src/com/reco1l/osu/UpdateManager.kt index 646b74cdd..60ece52a0 100644 --- a/src/com/reco1l/osu/UpdateManager.kt +++ b/src/com/reco1l/osu/UpdateManager.kt @@ -62,7 +62,7 @@ object UpdateManager: IFileRequestObserver /** * Check for new game updates. * - * @param silently If `true` no announce will be shown unless there's new updates. + * @param silently If `true`, no prompts will be shown unless there's new updates. */ @JvmStatic fun checkNewUpdates(silently: Boolean) { diff --git a/src/com/reco1l/osu/ui/PromptDialog.kt b/src/com/reco1l/osu/ui/PromptDialog.kt index d7b702cbd..f24229b4d 100644 --- a/src/com/reco1l/osu/ui/PromptDialog.kt +++ b/src/com/reco1l/osu/ui/PromptDialog.kt @@ -41,7 +41,7 @@ open class PromptDialog : MessageDialog() { } /** - * The text to be show displayed in the input hint. + * The text to display in the input hint. */ var hint: String? = null set(value) { @@ -52,7 +52,7 @@ open class PromptDialog : MessageDialog() { } /** - * The function to be called when the text input is changed. + * The function to call when the text input is changed. */ var onTextChanged: ((PromptDialog) -> Unit)? = null @@ -92,7 +92,7 @@ open class PromptDialog : MessageDialog() { } /** - * The text to be show displayed in the input hint. + * The text to display in the input hint. */ fun setHint(text: String): PromptDialog { hint = text @@ -100,7 +100,7 @@ open class PromptDialog : MessageDialog() { } /** - * The function to be called when the text input is changed. + * The function to call when the text input is changed. */ fun setOnTextChanged(action: (PromptDialog) -> Unit): PromptDialog { onTextChanged = action From f5301c5085d21b297a954e77daf733152850e7ff Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 27 Dec 2024 00:35:27 +0800 Subject: [PATCH 15/87] Update some confusing and outdated comments --- src/com/reco1l/andengine/Axes.kt | 2 +- src/com/reco1l/andengine/ExtendedEntity.kt | 16 ++++++++-------- .../andengine/container/ConstraintContainer.kt | 4 ++-- .../andengine/container/ScrollableContainer.kt | 1 - src/com/reco1l/andengine/text/TextSprite.kt | 4 ++-- 5 files changed, 13 insertions(+), 14 deletions(-) diff --git a/src/com/reco1l/andengine/Axes.kt b/src/com/reco1l/andengine/Axes.kt index 55f049a43..4b9b82bf4 100644 --- a/src/com/reco1l/andengine/Axes.kt +++ b/src/com/reco1l/andengine/Axes.kt @@ -27,7 +27,7 @@ enum class Axes { /** - * Whether this axis [Y] or [Both]. + * Whether this axis is [Y] or [Both]. */ val isVertical: Boolean get() = this == Y || this == Both diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index e3df2a714..ee747de86 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -68,11 +68,11 @@ abstract class ExtendedEntity( * * Example: * - * If relativeSizeAxes is set to [Axes.Both] and we set the size to 0.5, the entity's + * If [relativeSizeAxes] is set to [Axes.Both] and we set the size to 0.5, the entity's * size will be half the size of the parent. * - * Note: autoSizeAxes has priority over relativeSizeAxes. As for example if autoSizeAxes - * is set to [Axes.Both] and relativeSizeAxes is set to [Axes.Both], relativeSizeAxes + * Note: [autoSizeAxes] has priority over [relativeSizeAxes]. For example, if [autoSizeAxes] + * is set to [Axes.Both] and [relativeSizeAxes] is set to [Axes.Both], [relativeSizeAxes] * will be ignored. */ open var relativeSizeAxes = Axes.None @@ -108,7 +108,7 @@ abstract class ExtendedEntity( * * Example: * - * If relativePositionAxes is set to [Axes.Both] and we set the position to 0.5 for both axes, + * If [relativePositionAxes] is set to [Axes.Both] and we set the position to 0.5 for both axes, * the entity's position will be at the center of the parent. */ open var relativePositionAxes = Axes.None @@ -578,7 +578,7 @@ abstract class ExtendedEntity( /** * Sets a relative size for the entity. - * This will set the [relativeSizeAxes] property to [Axes.Both] automaticall. + * This will set the [relativeSizeAxes] property to [Axes.Both] automatically. */ fun setRelativeSize(width: Float, height: Float) { relativeSizeAxes = Axes.Both @@ -590,7 +590,7 @@ abstract class ExtendedEntity( * * Note: This will change the [autoSizeAxes] property to [Axes.None] automatically. * - * @return Whether the size of the entity was changed or not, this depends on the [autoSizeAxes] property. + * @return Whether the size of the entity was changed or not. */ open fun setSize(newWidth: Float, newHeight: Float): Boolean { @@ -692,6 +692,8 @@ abstract class ExtendedEntity( || drawY > pCamera.maxY || drawY + drawHeight < pCamera.minY } + // Transformation + override fun getLocalToParentTransformation(): Transformation { if (mLocalToParentTransformation == null) { @@ -761,8 +763,6 @@ abstract class ExtendedEntity( } - // Transformation - override fun setBlendFunction(pSourceBlendFunction: Int, pDestinationBlendFunction: Int) { if (blendInfo != null) { Log.w("ExtendedEntity", "BlendInfo is set, use blendInfo property to change the blending function.") diff --git a/src/com/reco1l/andengine/container/ConstraintContainer.kt b/src/com/reco1l/andengine/container/ConstraintContainer.kt index 31d7f6f04..40811844e 100644 --- a/src/com/reco1l/andengine/container/ConstraintContainer.kt +++ b/src/com/reco1l/andengine/container/ConstraintContainer.kt @@ -24,7 +24,7 @@ class ConstraintContainer : Container() { var childX = child.x - // Relative positions will be multiplied by the remaining space since the + // Relative positions will be multiplied by the remaining space from the // target's position to the edge of the container. if (child.relativePositionAxes.isHorizontal) { childX *= drawWidth - targetX @@ -42,7 +42,7 @@ class ConstraintContainer : Container() { var childY = child.y - // Relative positions will be multiplied by the remaining space since the + // Relative positions will be multiplied by the remaining space from the // target's position to the edge of the container. if (child.relativePositionAxes.isVertical) { childY *= drawHeight - targetY diff --git a/src/com/reco1l/andengine/container/ScrollableContainer.kt b/src/com/reco1l/andengine/container/ScrollableContainer.kt index 2db646dbf..28d2a77b3 100644 --- a/src/com/reco1l/andengine/container/ScrollableContainer.kt +++ b/src/com/reco1l/andengine/container/ScrollableContainer.kt @@ -294,7 +294,6 @@ open class ScrollableContainer : Container() { ACTION_MOVE -> { isDragging = true - // Coerce the delta values to the width and height of the container because the user can't scroll more than that. var deltaX = localX - initialX var deltaY = localY - initialY diff --git a/src/com/reco1l/andengine/text/TextSprite.kt b/src/com/reco1l/andengine/text/TextSprite.kt index d9a2ae2d9..29d2187e4 100644 --- a/src/com/reco1l/andengine/text/TextSprite.kt +++ b/src/com/reco1l/andengine/text/TextSprite.kt @@ -26,8 +26,8 @@ import kotlin.math.max /** * A sprite that displays text. * - * Differently from the original [Text] class, this is a sprite that pre-renders the entire text - * to a texture, it is not as efficient as the original [Text] class, but it is more flexible and + * Unlike [org.anddev.andengine.entity.text.Text], this is a sprite that pre-renders the entire text to a texture. + * It is not as efficient as [org.anddev.andengine.entity.text.Text], but it is more flexible and * allows for more customization. * * It is not recommended to use this on places where the text changes frequently, as it will From a8af4b73e3c78ce062682bf8708246628e557805 Mon Sep 17 00:00:00 2001 From: Reco1l Date: Thu, 26 Dec 2024 14:43:40 -0300 Subject: [PATCH 16/87] Make relativeSizeAxes compatible with autoSizeAxes --- src/com/reco1l/andengine/ExtendedEntity.kt | 60 +++------------------- 1 file changed, 6 insertions(+), 54 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index ee747de86..3b0f118cc 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -39,23 +39,6 @@ abstract class ExtendedEntity( set(value) { if (field != value) { field = value - - // Setting the opposite value for relativeSizeAxes to avoid conflicts. - if (relativeSizeAxes != Axes.None) { - - if (value == Axes.Both) { - relativeSizeAxes = Axes.None - } - - if (value == Axes.X && relativeSizeAxes == Axes.Y) { - relativeSizeAxes = Axes.Y - } - - if (value == Axes.Y && relativeSizeAxes == Axes.X) { - relativeSizeAxes = Axes.X - } - } - onContentSizeMeasured() } } @@ -79,23 +62,6 @@ abstract class ExtendedEntity( set(value) { if (field != value) { field = value - - // Setting the opposite value for autoSizeAxes to avoid conflicts. - if (autoSizeAxes != Axes.None) { - - if (value == Axes.Both) { - autoSizeAxes = Axes.None - } - - if (value == Axes.X && autoSizeAxes == Axes.Y) { - autoSizeAxes = Axes.Y - } - - if (value == Axes.Y && autoSizeAxes == Axes.X) { - autoSizeAxes = Axes.X - } - } - onContentSizeMeasured() } } @@ -320,11 +286,6 @@ abstract class ExtendedEntity( // Positions - fun setRelativePosition(x: Float, y: Float) { - relativePositionAxes = Axes.Both - setPosition(x, y) - } - override fun setPosition(x: Float, y: Float) { if (mX != x || mY != y) { mX = x @@ -576,15 +537,6 @@ abstract class ExtendedEntity( } - /** - * Sets a relative size for the entity. - * This will set the [relativeSizeAxes] property to [Axes.Both] automatically. - */ - fun setRelativeSize(width: Float, height: Float) { - relativeSizeAxes = Axes.Both - setSize(width, height) - } - /** * Sets the size of the entity. * @@ -595,8 +547,8 @@ abstract class ExtendedEntity( open fun setSize(newWidth: Float, newHeight: Float): Boolean { if (autoSizeAxes != Axes.None) { - Log.w("ExtendedEntity", "autoSizeAxes is set to ${autoSizeAxes.name} while changing the size manually.") - autoSizeAxes = Axes.None + Log.e("ExtendedEntity", "Cannot change the size manually while autoSizeAxes is set to ${autoSizeAxes.name}.") + return false } if (width != newWidth || height != newHeight) { @@ -618,8 +570,8 @@ abstract class ExtendedEntity( open fun setWidth(value: Float) { if (autoSizeAxes.isHorizontal) { - Log.w("ExtendedEntity", "autoSizeAxes is set to ${autoSizeAxes.name} while changing the width manually.") - autoSizeAxes = if (autoSizeAxes == Axes.Both) Axes.Y else Axes.None + Log.e("ExtendedEntity", "Cannot change the width manually while autoSizeAxes is set to ${autoSizeAxes.name}.") + return } if (width != value) { @@ -633,8 +585,8 @@ abstract class ExtendedEntity( open fun setHeight(value: Float) { if (autoSizeAxes.isVertical) { - Log.w("ExtendedEntity", "autoSizeAxes is set to ${autoSizeAxes.name} while changing the height manually.") - autoSizeAxes = if (autoSizeAxes == Axes.Both) Axes.X else Axes.None + Log.e("ExtendedEntity", "Cannot change the height manually while autoSizeAxes is set to ${autoSizeAxes.name}.") + return } if (height != value) { From c4fd6bfff7ca1353f75a379acf5eb93dbc4c826e Mon Sep 17 00:00:00 2001 From: Reco1l Date: Thu, 26 Dec 2024 14:44:18 -0300 Subject: [PATCH 17/87] Call value instead --- src/com/reco1l/andengine/ExtendedEntity.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 3b0f118cc..e32abb2cb 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -104,10 +104,10 @@ abstract class ExtendedEntity( if (field != value) { field = value - mRotationCenterX = origin.x - mRotationCenterY = origin.y - mScaleCenterX = origin.x - mScaleCenterY = origin.y + mRotationCenterX = value.x + mRotationCenterY = value.y + mScaleCenterX = value.x + mScaleCenterY = value.y invalidateTransformations() } From 3a9b59d8b04689650d6f6a7ab2ac94294bafa75d Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Thu, 26 Dec 2024 22:18:23 -0300 Subject: [PATCH 18/87] Introduce Padding for ExtendedEntity --- src/com/reco1l/andengine/Entities.kt | 105 ++++++++++++++ src/com/reco1l/andengine/ExtendedEntity.kt | 133 +++--------------- .../reco1l/andengine/container/Container.kt | 14 +- src/com/reco1l/framework/math/Vector4.kt | 89 ++++++++++++ 4 files changed, 222 insertions(+), 119 deletions(-) create mode 100644 src/com/reco1l/andengine/Entities.kt create mode 100644 src/com/reco1l/framework/math/Vector4.kt diff --git a/src/com/reco1l/andengine/Entities.kt b/src/com/reco1l/andengine/Entities.kt new file mode 100644 index 000000000..30ec3752b --- /dev/null +++ b/src/com/reco1l/andengine/Entities.kt @@ -0,0 +1,105 @@ +package com.reco1l.andengine + +import com.reco1l.framework.math.Vector4 +import org.anddev.andengine.entity.IEntity +import org.anddev.andengine.entity.shape.IShape + + +fun IEntity?.getPadding() = when (this) { + is ExtendedEntity -> padding + else -> Vector4.Zero +} + +fun IEntity?.getPaddedWidth() = when (this) { + is ExtendedEntity -> drawWidth - padding.horizontal + is IShape -> width + else -> 0f +} + +fun IEntity?.getPaddedHeight() = when (this) { + is ExtendedEntity -> drawHeight - padding.vertical + is IShape -> height + else -> 0f +} + + +/** + * The total offset applied to the X axis. + */ +val ExtendedEntity.totalOffsetX + get() = originOffsetX + anchorOffsetX + translationX + +/** + * The total offset applied to the Y axis. + */ +val ExtendedEntity.totalOffsetY + get() = originOffsetY + anchorOffsetY + translationY + +/** + * The offset applied to the X axis according to the anchor factor. + */ +val ExtendedEntity.anchorOffsetX: Float + get() = parent.getPaddedWidth() * anchor.x + +/** + * The offset applied to the Y axis according to the anchor factor. + */ +val ExtendedEntity.anchorOffsetY: Float + get() = parent.getPaddedHeight() * anchor.y + +/** + * The offset applied to the X axis according to the origin factor. + */ +val ExtendedEntity.originOffsetX: Float + get() = -(drawWidth * origin.x) + +/** + * The offset applied to the Y axis according to the origin factor. + */ +val ExtendedEntity.originOffsetY: Float + get() = -(drawHeight * origin.y) + + +/** + * Returns the draw width of the entity. + */ +fun IEntity?.getDrawWidth(): Float = when (this) { + is ExtendedEntity -> drawWidth + is IShape -> width + else -> 0f +} + +/** + * Returns the draw height of the entity. + */ +fun IEntity?.getDrawHeight(): Float = when (this) { + is ExtendedEntity -> drawHeight + is IShape -> height + else -> 0f +} + +/** + * Returns the draw X position of the entity. + */ +fun IEntity?.getDrawX(): Float = when (this) { + is ExtendedEntity -> drawX + is IShape -> x + else -> 0f +} + +/** + * Returns the draw Y position of the entity. + */ +fun IEntity?.getDrawY(): Float = when (this) { + is ExtendedEntity -> drawY + is IShape -> y + else -> 0f +} + +/** + * Attaches the entity to a parent. + */ +infix fun T.attachTo(parent: IEntity): T { + parent.attachChild(this) + return this +} \ No newline at end of file diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index e32abb2cb..34b84f5ce 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -4,9 +4,9 @@ import android.util.* import com.reco1l.andengine.container.* import com.reco1l.andengine.modifier.* import com.reco1l.framework.* +import com.reco1l.framework.math.Vector4 import org.anddev.andengine.engine.camera.* import org.anddev.andengine.entity.* -import org.anddev.andengine.entity.scene.CameraScene import org.anddev.andengine.entity.scene.Scene import org.anddev.andengine.entity.scene.Scene.ITouchArea import org.anddev.andengine.entity.shape.* @@ -113,6 +113,17 @@ abstract class ExtendedEntity( } } + /** + * The padding of the entity. + */ + open var padding = Vector4.Zero + set(value) { + if (field != value) { + field = value + invalidateTransformations() + } + } + /** * The translation in the X axis. */ @@ -203,7 +214,7 @@ abstract class ExtendedEntity( open val drawWidth: Float get() { if (relativeSizeAxes.isHorizontal) { - return getParentWidth() * width + return parent.getPaddedWidth() * width } return width } @@ -217,7 +228,7 @@ abstract class ExtendedEntity( open val drawHeight: Float get() { if (relativeSizeAxes.isVertical) { - return getParentHeight() * height + return parent.getPaddedHeight() * height } return height } @@ -233,11 +244,12 @@ abstract class ExtendedEntity( return parent.getChildDrawX(this) } + var x = x if (relativePositionAxes.isHorizontal) { - return getParentWidth() * x + totalOffsetX + x *= parent.getPaddedWidth() } - return x + totalOffsetX + return parent.getPadding().left + x + totalOffsetX } @@ -252,11 +264,12 @@ abstract class ExtendedEntity( return parent.getChildDrawY(this) } + var y = y if (relativePositionAxes.isVertical) { - return getParentHeight() * y + totalOffsetY + y *= parent.getPaddedHeight() } - return y + totalOffsetY + return parent.getPadding().top + y + totalOffsetY } @@ -521,11 +534,11 @@ abstract class ExtendedEntity( if (contentWidth != width || contentHeight != height) { if (autoSizeAxes.isHorizontal) { - width = if (relativeSizeAxes.isHorizontal) contentWidth / getParentWidth() else contentWidth + width = if (relativeSizeAxes.isHorizontal) contentWidth / parent.getPaddedWidth() else contentWidth } if (autoSizeAxes.isVertical) { - height = if (relativeSizeAxes.isVertical) contentHeight / getParentHeight() else contentHeight + height = if (relativeSizeAxes.isVertical) contentHeight / parent.getPaddedHeight() else contentHeight } updateVertexBuffer() @@ -763,105 +776,3 @@ abstract class ExtendedEntity( } } - - -/** - * The total offset applied to the X axis. - */ -val ExtendedEntity.totalOffsetX - get() = originOffsetX + anchorOffsetX + translationX - -/** - * The total offset applied to the Y axis. - */ -val ExtendedEntity.totalOffsetY - get() = originOffsetY + anchorOffsetY + translationY - -/** - * The offset applied to the X axis according to the anchor factor. - */ -val ExtendedEntity.anchorOffsetX: Float - get() = getParentWidth() * anchor.x - -/** - * The offset applied to the Y axis according to the anchor factor. - */ -val ExtendedEntity.anchorOffsetY: Float - get() = getParentHeight() * anchor.y - -/** - * The offset applied to the X axis according to the origin factor. - */ -val ExtendedEntity.originOffsetX: Float - get() = -(drawWidth * origin.x) - -/** - * The offset applied to the Y axis according to the origin factor. - */ -val ExtendedEntity.originOffsetY: Float - get() = -(drawHeight * origin.y) - -/** - * Returns the width of the parent entity. - */ -fun ExtendedEntity.getParentWidth() = when (val parent = parent) { - is ExtendedEntity -> parent.drawWidth - is CameraScene -> parent.camera.widthRaw - is IShape -> parent.width - else -> 0f -} - -/** - * Returns the height of the parent entity. - */ -fun ExtendedEntity.getParentHeight() = when (val parent = parent) { - is ExtendedEntity -> parent.drawHeight - is CameraScene -> parent.camera.heightRaw - is IShape -> parent.height - else -> 0f -} - -/** - * Returns the draw width of the entity. - */ -fun IEntity.getDrawWidth(): Float = when (this) { - is ExtendedEntity -> drawWidth - is IShape -> width - else -> 0f -} - -/** - * Returns the draw height of the entity. - */ -fun IEntity.getDrawHeight(): Float = when (this) { - is ExtendedEntity -> drawHeight - is IShape -> height - else -> 0f -} - -/** - * Returns the draw X position of the entity. - */ -fun IEntity.getDrawX(): Float = when (this) { - is ExtendedEntity -> drawX - is IShape -> x - else -> 0f -} - -/** - * Returns the draw Y position of the entity. - */ -fun IEntity.getDrawY(): Float = when (this) { - is ExtendedEntity -> drawY - is IShape -> y - else -> 0f -} - -/** - * Attaches the entity to a parent. - */ -infix fun T.attachTo(parent: IEntity): T { - parent.attachChild(this) - return this -} - diff --git a/src/com/reco1l/andengine/container/Container.kt b/src/com/reco1l/andengine/container/Container.kt index 822f8452c..ec1ada9de 100644 --- a/src/com/reco1l/andengine/container/Container.kt +++ b/src/com/reco1l/andengine/container/Container.kt @@ -1,14 +1,10 @@ package com.reco1l.andengine.container -import android.util.* import com.reco1l.andengine.* import com.reco1l.toolkt.kotlin.* import org.anddev.andengine.engine.camera.* import org.anddev.andengine.entity.* import org.anddev.andengine.entity.IEntity.* -import org.anddev.andengine.entity.scene.Scene.ITouchArea -import org.anddev.andengine.entity.shape.IShape -import org.anddev.andengine.input.touch.* import org.anddev.andengine.util.* import javax.microedition.khronos.opengles.GL10 import kotlin.math.* @@ -63,20 +59,22 @@ open class Container : ExtendedEntity() { open fun getChildDrawX(child: ExtendedEntity): Float { + var x = child.x if (child.relativePositionAxes.isHorizontal) { - return child.x * drawWidth + child.totalOffsetX + x *= getPaddedWidth() } - return child.x + child.totalOffsetX + return getPadding().left + x + child.totalOffsetX } open fun getChildDrawY(child: ExtendedEntity): Float { + var y = child.y if (child.relativePositionAxes.isVertical) { - return child.y * drawHeight + child.totalOffsetY + y *= getPaddedHeight() } - return child.y + child.totalOffsetY + return getPadding().top + y + child.totalOffsetY } diff --git a/src/com/reco1l/framework/math/Vector4.kt b/src/com/reco1l/framework/math/Vector4.kt new file mode 100644 index 000000000..d8e25947b --- /dev/null +++ b/src/com/reco1l/framework/math/Vector4.kt @@ -0,0 +1,89 @@ +package com.reco1l.framework.math + +@JvmInline +value class Vector4(private val values: FloatArray = FloatArray(4)) { + + + constructor(x: Float, y: Float, z: Float, w: Float) : this(floatArrayOf(x, y, z, w)) + + + val x: Float + get() = values[0] + + val y: Float + get() = values[1] + + val z: Float + get() = values[2] + + val w: Float + get() = values[3] + + + val left: Float + get() = values[0] + + val top: Float + get() = values[1] + + val right: Float + get() = values[2] + + val bottom: Float + get() = values[3] + + + val total + get() = x + y + z + w + + val vertical + get() = y + w + + val horizontal + get() = x + z + + + operator fun plus(other: Vector4) = Vector4( + x + other.x, + y + other.y, + z + other.z, + w + other.w + ) + + operator fun minus(other: Vector4) = Vector4( + x - other.x, + y - other.y, + z - other.z, + w - other.w + ) + + operator fun times(scalar: Float) = Vector4( + x * scalar, + y * scalar, + z * scalar, + w * scalar + ) + + operator fun div(scalar: Float) = Vector4( + x / scalar, + y / scalar, + z / scalar, + w / scalar + ) + + operator fun unaryMinus() = Vector4( + -x, + -y, + -z, + -w + ) + + override fun toString() = "Vector4($x, $y, $z, $w)" + + + companion object { + val Zero = Vector4() + val One = Vector4(1f, 1f, 1f, 1f) + } + +} \ No newline at end of file From df8316d9b56cb383744a3f281c4bdddaa84c8471 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Thu, 26 Dec 2024 22:28:43 -0300 Subject: [PATCH 19/87] Make `Vec2` and `Vec4` immutable --- src/com/reco1l/andengine/Anchor.kt | 20 ++--- src/com/reco1l/andengine/Entities.kt | 4 +- src/com/reco1l/andengine/ExtendedEntity.kt | 4 +- src/com/reco1l/framework/math/Vec2.kt | 54 +++++++++++++ src/com/reco1l/framework/math/Vec4.kt | 78 +++++++++++++++++++ src/com/reco1l/framework/math/Vector4.kt | 89 ---------------------- 6 files changed, 146 insertions(+), 103 deletions(-) create mode 100644 src/com/reco1l/framework/math/Vec2.kt create mode 100644 src/com/reco1l/framework/math/Vec4.kt delete mode 100644 src/com/reco1l/framework/math/Vector4.kt diff --git a/src/com/reco1l/andengine/Anchor.kt b/src/com/reco1l/andengine/Anchor.kt index 448d33638..43c3e63e3 100644 --- a/src/com/reco1l/andengine/Anchor.kt +++ b/src/com/reco1l/andengine/Anchor.kt @@ -1,34 +1,34 @@ package com.reco1l.andengine -import com.rian.osu.math.Vector2 +import com.reco1l.framework.math.Vec2 object Anchor { @JvmField - val TopLeft = Vector2(0f, 0f) + val TopLeft = Vec2(0f, 0f) @JvmField - val TopCenter = Vector2(0.5f, 0f) + val TopCenter = Vec2(0.5f, 0f) @JvmField - val TopRight = Vector2(1f, 0f) + val TopRight = Vec2(1f, 0f) @JvmField - val CenterLeft = Vector2(0f, 0.5f) + val CenterLeft = Vec2(0f, 0.5f) @JvmField - val Center = Vector2(0.5f, 0.5f) + val Center = Vec2(0.5f, 0.5f) @JvmField - val CenterRight = Vector2(1f, 0.5f) + val CenterRight = Vec2(1f, 0.5f) @JvmField - val BottomLeft = Vector2(0f, 1f) + val BottomLeft = Vec2(0f, 1f) @JvmField - val BottomCenter = Vector2(0.5f, 1f) + val BottomCenter = Vec2(0.5f, 1f) @JvmField - val BottomRight = Vector2(1f, 1f) + val BottomRight = Vec2(1f, 1f) } \ No newline at end of file diff --git a/src/com/reco1l/andengine/Entities.kt b/src/com/reco1l/andengine/Entities.kt index 30ec3752b..f4e28e2f0 100644 --- a/src/com/reco1l/andengine/Entities.kt +++ b/src/com/reco1l/andengine/Entities.kt @@ -1,13 +1,13 @@ package com.reco1l.andengine -import com.reco1l.framework.math.Vector4 +import com.reco1l.framework.math.Vec4 import org.anddev.andengine.entity.IEntity import org.anddev.andengine.entity.shape.IShape fun IEntity?.getPadding() = when (this) { is ExtendedEntity -> padding - else -> Vector4.Zero + else -> Vec4.Zero } fun IEntity?.getPaddedWidth() = when (this) { diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 34b84f5ce..4bc548fc8 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -4,7 +4,7 @@ import android.util.* import com.reco1l.andengine.container.* import com.reco1l.andengine.modifier.* import com.reco1l.framework.* -import com.reco1l.framework.math.Vector4 +import com.reco1l.framework.math.Vec4 import org.anddev.andengine.engine.camera.* import org.anddev.andengine.entity.* import org.anddev.andengine.entity.scene.Scene @@ -116,7 +116,7 @@ abstract class ExtendedEntity( /** * The padding of the entity. */ - open var padding = Vector4.Zero + open var padding = Vec4.Zero set(value) { if (field != value) { field = value diff --git a/src/com/reco1l/framework/math/Vec2.kt b/src/com/reco1l/framework/math/Vec2.kt new file mode 100644 index 000000000..fe303f071 --- /dev/null +++ b/src/com/reco1l/framework/math/Vec2.kt @@ -0,0 +1,54 @@ +package com.reco1l.framework.math + +data class Vec2( + + val x: Float = 0f, + + val y: Float = 0f, + +) { + + val total + get() = x + y + + val vertical + get() = y + + val horizontal + get() = x + + + operator fun plus(other: Vec2) = Vec2( + x + other.x, + y + other.y + ) + + operator fun minus(other: Vec2) = Vec2( + x - other.x, + y - other.y + ) + + operator fun times(scalar: Float) = Vec2( + x * scalar, + y * scalar, + ) + + operator fun div(scalar: Float) = Vec2( + x / scalar, + y / scalar + ) + + operator fun unaryMinus() = Vec2( + -x, + -y + ) + + override fun toString() = "Vector2($x, $y)" + + + companion object { + val Zero = Vec2() + val One = Vec2(1f, 1f) + } + +} \ No newline at end of file diff --git a/src/com/reco1l/framework/math/Vec4.kt b/src/com/reco1l/framework/math/Vec4.kt new file mode 100644 index 000000000..675c25b49 --- /dev/null +++ b/src/com/reco1l/framework/math/Vec4.kt @@ -0,0 +1,78 @@ +package com.reco1l.framework.math + +data class Vec4( + + val x: Float = 0f, + val y: Float = 0f, + val z: Float = 0f, + val w: Float = 0f, + +) { + + val left: Float + get() = x + + val top: Float + get() = y + + val right: Float + get() = z + + val bottom: Float + get() = w + + + val total + get() = x + y + z + w + + val vertical + get() = y + w + + val horizontal + get() = x + z + + + operator fun plus(other: Vec4) = Vec4( + x + other.x, + y + other.y, + z + other.z, + w + other.w + ) + + operator fun minus(other: Vec4) = Vec4( + x - other.x, + y - other.y, + z - other.z, + w - other.w + ) + + operator fun times(scalar: Float) = Vec4( + x * scalar, + y * scalar, + z * scalar, + w * scalar + ) + + operator fun div(scalar: Float) = Vec4( + x / scalar, + y / scalar, + z / scalar, + w / scalar + ) + + operator fun unaryMinus() = Vec4( + -x, + -y, + -z, + -w + ) + + override fun toString() = "Vector4($x, $y, $z, $w)" + + + companion object { + val Zero = Vec4() + val One = Vec4(1f, 1f, 1f, 1f) + } + +} \ No newline at end of file diff --git a/src/com/reco1l/framework/math/Vector4.kt b/src/com/reco1l/framework/math/Vector4.kt deleted file mode 100644 index d8e25947b..000000000 --- a/src/com/reco1l/framework/math/Vector4.kt +++ /dev/null @@ -1,89 +0,0 @@ -package com.reco1l.framework.math - -@JvmInline -value class Vector4(private val values: FloatArray = FloatArray(4)) { - - - constructor(x: Float, y: Float, z: Float, w: Float) : this(floatArrayOf(x, y, z, w)) - - - val x: Float - get() = values[0] - - val y: Float - get() = values[1] - - val z: Float - get() = values[2] - - val w: Float - get() = values[3] - - - val left: Float - get() = values[0] - - val top: Float - get() = values[1] - - val right: Float - get() = values[2] - - val bottom: Float - get() = values[3] - - - val total - get() = x + y + z + w - - val vertical - get() = y + w - - val horizontal - get() = x + z - - - operator fun plus(other: Vector4) = Vector4( - x + other.x, - y + other.y, - z + other.z, - w + other.w - ) - - operator fun minus(other: Vector4) = Vector4( - x - other.x, - y - other.y, - z - other.z, - w - other.w - ) - - operator fun times(scalar: Float) = Vector4( - x * scalar, - y * scalar, - z * scalar, - w * scalar - ) - - operator fun div(scalar: Float) = Vector4( - x / scalar, - y / scalar, - z / scalar, - w / scalar - ) - - operator fun unaryMinus() = Vector4( - -x, - -y, - -z, - -w - ) - - override fun toString() = "Vector4($x, $y, $z, $w)" - - - companion object { - val Zero = Vector4() - val One = Vector4(1f, 1f, 1f, 1f) - } - -} \ No newline at end of file From 92c006f3cba6a0521092daed26edebee6ca10a66 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Thu, 26 Dec 2024 22:38:09 -0300 Subject: [PATCH 20/87] Fix getPaddedWidth() and getPaddedHeight() missing cases --- src/com/reco1l/andengine/Entities.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/com/reco1l/andengine/Entities.kt b/src/com/reco1l/andengine/Entities.kt index f4e28e2f0..b90979023 100644 --- a/src/com/reco1l/andengine/Entities.kt +++ b/src/com/reco1l/andengine/Entities.kt @@ -2,6 +2,7 @@ package com.reco1l.andengine import com.reco1l.framework.math.Vec4 import org.anddev.andengine.entity.IEntity +import org.anddev.andengine.entity.scene.CameraScene import org.anddev.andengine.entity.shape.IShape @@ -12,12 +13,14 @@ fun IEntity?.getPadding() = when (this) { fun IEntity?.getPaddedWidth() = when (this) { is ExtendedEntity -> drawWidth - padding.horizontal + is CameraScene -> camera.widthRaw is IShape -> width else -> 0f } fun IEntity?.getPaddedHeight() = when (this) { is ExtendedEntity -> drawHeight - padding.vertical + is CameraScene -> camera.heightRaw is IShape -> height else -> 0f } From 14260c191e43bf8015956e7fce5038a2fafb499f Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Thu, 26 Dec 2024 23:08:20 -0300 Subject: [PATCH 21/87] Override autoSizeAxes when changing manually the dimensions --- src/com/reco1l/andengine/ExtendedEntity.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 4bc548fc8..87b3cc41f 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -560,8 +560,7 @@ abstract class ExtendedEntity( open fun setSize(newWidth: Float, newHeight: Float): Boolean { if (autoSizeAxes != Axes.None) { - Log.e("ExtendedEntity", "Cannot change the size manually while autoSizeAxes is set to ${autoSizeAxes.name}.") - return false + autoSizeAxes = Axes.None } if (width != newWidth || height != newHeight) { @@ -583,8 +582,7 @@ abstract class ExtendedEntity( open fun setWidth(value: Float) { if (autoSizeAxes.isHorizontal) { - Log.e("ExtendedEntity", "Cannot change the width manually while autoSizeAxes is set to ${autoSizeAxes.name}.") - return + autoSizeAxes = if (autoSizeAxes == Axes.Both) Axes.Y else Axes.None } if (width != value) { @@ -598,8 +596,7 @@ abstract class ExtendedEntity( open fun setHeight(value: Float) { if (autoSizeAxes.isVertical) { - Log.e("ExtendedEntity", "Cannot change the height manually while autoSizeAxes is set to ${autoSizeAxes.name}.") - return + autoSizeAxes = if (autoSizeAxes == Axes.Both) Axes.X else Axes.None } if (height != value) { From 933cb9da951a72b032ce3e066046cbfb894b0214 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Thu, 26 Dec 2024 23:49:51 -0300 Subject: [PATCH 22/87] Support padding in Container sub-classes --- src/com/reco1l/andengine/ExtendedEntity.kt | 5 +-- .../container/ConstraintContainer.kt | 17 ++++++---- .../reco1l/andengine/container/Container.kt | 4 +-- .../andengine/container/LinearContainer.kt | 34 +++++++------------ .../container/ScrollableContainer.kt | 10 +++--- 5 files changed, 33 insertions(+), 37 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 87b3cc41f..507131659 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -15,6 +15,7 @@ import org.anddev.andengine.opengl.util.* import org.anddev.andengine.opengl.vertex.* import org.anddev.andengine.util.Transformation import javax.microedition.khronos.opengles.* +import kotlin.math.max /** @@ -249,7 +250,7 @@ abstract class ExtendedEntity( x *= parent.getPaddedWidth() } - return parent.getPadding().left + x + totalOffsetX + return max(parent.getPadding().left, x) + totalOffsetX } @@ -269,7 +270,7 @@ abstract class ExtendedEntity( y *= parent.getPaddedHeight() } - return parent.getPadding().top + y + totalOffsetY + return max(parent.getPadding().top, y) + totalOffsetY } diff --git a/src/com/reco1l/andengine/container/ConstraintContainer.kt b/src/com/reco1l/andengine/container/ConstraintContainer.kt index 40811844e..850c491d0 100644 --- a/src/com/reco1l/andengine/container/ConstraintContainer.kt +++ b/src/com/reco1l/andengine/container/ConstraintContainer.kt @@ -3,6 +3,7 @@ package com.reco1l.andengine.container import com.reco1l.andengine.* import org.anddev.andengine.entity.* import org.anddev.andengine.entity.shape.* +import kotlin.math.max /** * Container that allows to constrain nested entities to other entities in the same container. @@ -19,10 +20,12 @@ class ConstraintContainer : Container() { val target = constraints[child] ?: this - val targetX = target.getDrawX() - val anchorOffsetX = target.getDrawWidth() * child.anchor.x + val targetX = if (target == this) 0f else target.getDrawX() + val targetWidth = if (target == this) getPaddedWidth() else target.getDrawWidth() - var childX = child.x + val anchorOffsetX = targetWidth * child.anchor.x + + var childX = max(getPadding().left, child.x) // Relative positions will be multiplied by the remaining space from the // target's position to the edge of the container. @@ -37,10 +40,12 @@ class ConstraintContainer : Container() { val target = constraints[child] ?: this - val targetY = target.getDrawY() - val anchorOffsetY = target.getDrawHeight() * child.anchor.y + val targetY = if (target == this) 0f else target.getDrawY() + val targetHeight = if (target == this) getPaddedHeight() else target.getDrawHeight() + + val anchorOffsetY = targetHeight * child.anchor.y - var childY = child.y + var childY = max(getPadding().top, child.y) // Relative positions will be multiplied by the remaining space from the // target's position to the edge of the container. diff --git a/src/com/reco1l/andengine/container/Container.kt b/src/com/reco1l/andengine/container/Container.kt index ec1ada9de..efd1263ac 100644 --- a/src/com/reco1l/andengine/container/Container.kt +++ b/src/com/reco1l/andengine/container/Container.kt @@ -64,7 +64,7 @@ open class Container : ExtendedEntity() { x *= getPaddedWidth() } - return getPadding().left + x + child.totalOffsetX + return max(getPadding().left, x) + child.totalOffsetX } open fun getChildDrawY(child: ExtendedEntity): Float { @@ -74,7 +74,7 @@ open class Container : ExtendedEntity() { y *= getPaddedHeight() } - return getPadding().top + y + child.totalOffsetY + return max(getPadding().top, y) + child.totalOffsetY } diff --git a/src/com/reco1l/andengine/container/LinearContainer.kt b/src/com/reco1l/andengine/container/LinearContainer.kt index f09a24afb..266c61caf 100644 --- a/src/com/reco1l/andengine/container/LinearContainer.kt +++ b/src/com/reco1l/andengine/container/LinearContainer.kt @@ -30,9 +30,6 @@ open class LinearContainer : Container() { } - private var lastDrawOffset = 0f - - override fun onMeasureContentSize() { shouldMeasureSize = false @@ -44,9 +41,7 @@ open class LinearContainer : Container() { for (i in mChildren.indices) { val child = mChildren.getOrNull(i) ?: continue - - // Non-shape children are ignored as they doesn't have a size there's nothing to do. - if (child !is IShape) { + if (child !is ExtendedEntity) { continue } @@ -55,11 +50,13 @@ open class LinearContainer : Container() { when (orientation) { Horizontal -> { + child.x = contentWidth contentWidth += child.getDrawWidth() + spacing contentHeight = max(contentHeight, child.getDrawHeight()) } Vertical -> { + child.y = contentHeight contentWidth = max(contentWidth, child.getDrawWidth()) contentHeight += child.getDrawHeight() + spacing } @@ -71,33 +68,28 @@ open class LinearContainer : Container() { } - override fun onManagedDrawChildren(pGL: GL10, pCamera: Camera) { - lastDrawOffset = 0f - super.onManagedDrawChildren(pGL, pCamera) - } - override fun getChildDrawX(child: ExtendedEntity): Float { + val drawX = super.getChildDrawX(child) + if (orientation == Vertical) { - return super.getChildDrawX(child) + return drawX } - val drawX = lastDrawOffset + super.getChildDrawX(child) - lastDrawOffset += child.drawWidth + spacing - - return drawX + // Subtract the anchor offset for the X axis because it should be ignored in this case. + return drawX - child.anchorOffsetX } override fun getChildDrawY(child: ExtendedEntity): Float { + val drawY = super.getChildDrawY(child) + if (orientation == Horizontal) { - return super.getChildDrawY(child) + return drawY } - val drawY = lastDrawOffset + super.getChildDrawY(child) - lastDrawOffset += child.drawHeight + spacing - - return drawY + // Subtract the anchor offset for the Y axis because it should be ignored in this case. + return drawY - child.anchorOffsetY } } diff --git a/src/com/reco1l/andengine/container/ScrollableContainer.kt b/src/com/reco1l/andengine/container/ScrollableContainer.kt index 28d2a77b3..b656cd2ca 100644 --- a/src/com/reco1l/andengine/container/ScrollableContainer.kt +++ b/src/com/reco1l/andengine/container/ScrollableContainer.kt @@ -338,20 +338,20 @@ open class ScrollableContainer : Container() { override fun getChildDrawX(child: ExtendedEntity): Float { - if (child == indicatorX || child == indicatorY) { + if (child == indicatorX || child == indicatorY || !scrollAxes.isHorizontal) { return super.getChildDrawX(child) } - return -scrollX + child.x - child.originOffsetX + child.translationX + return -scrollX + max(getPadding().left, child.x) - child.originOffsetX + child.translationX } override fun getChildDrawY(child: ExtendedEntity): Float { - if (child == indicatorX || child == indicatorY) { + if (child == indicatorX || child == indicatorY || !scrollAxes.isVertical) { return super.getChildDrawY(child) } - return -scrollY + child.y - child.originOffsetY + child.translationY + return -scrollY + max(getPadding().top, child.y) - child.originOffsetY + child.translationY } @@ -359,7 +359,5 @@ open class ScrollableContainer : Container() { const val DEFAULT_DECELERATION = 0.98f - const val INSIGNIFICANT_DISTANCE = 0.05f - } } \ No newline at end of file From 9ecb8c7030189c53fe6f96d22a27ff7a83a4b1ee Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Fri, 27 Dec 2024 00:09:46 -0300 Subject: [PATCH 23/87] Fix padding not being applied properly in LinearContainer --- .../andengine/container/LinearContainer.kt | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/com/reco1l/andengine/container/LinearContainer.kt b/src/com/reco1l/andengine/container/LinearContainer.kt index 266c61caf..9f2dd9080 100644 --- a/src/com/reco1l/andengine/container/LinearContainer.kt +++ b/src/com/reco1l/andengine/container/LinearContainer.kt @@ -36,29 +36,42 @@ open class LinearContainer : Container() { contentWidth = 0f contentHeight = 0f - if (mChildren != null) { + for (i in 0 until childCount) { - for (i in mChildren.indices) { + val child = getChild(i) ?: continue + if (child !is ExtendedEntity) { + continue + } + + when (orientation) { + + Horizontal -> { + child.x = contentWidth + + contentWidth += child.getDrawWidth() + contentHeight = max(contentHeight, child.getDrawHeight()) - val child = mChildren.getOrNull(i) ?: continue - if (child !is ExtendedEntity) { - continue + if (i == 0) { + contentWidth += getPadding().left + } + + if (i < childCount - 1) { + contentWidth += spacing + } } - val spacing = if (i == mChildren.size - 1) 0f else spacing + Vertical -> { + child.y = contentHeight - when (orientation) { + contentWidth = max(contentWidth, child.getDrawWidth()) + contentHeight += child.getDrawHeight() - Horizontal -> { - child.x = contentWidth - contentWidth += child.getDrawWidth() + spacing - contentHeight = max(contentHeight, child.getDrawHeight()) + if (i == 0) { + contentHeight += getPadding().top } - Vertical -> { - child.y = contentHeight - contentWidth = max(contentWidth, child.getDrawWidth()) - contentHeight += child.getDrawHeight() + spacing + if (i < childCount - 1) { + contentHeight += spacing } } } From 85561f506ad95c6f65b4381387573641b1062e45 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Fri, 27 Dec 2024 00:29:55 -0300 Subject: [PATCH 24/87] Don't pre-multiply fonts --- src/com/reco1l/andengine/text/ExtendedText.kt | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/com/reco1l/andengine/text/ExtendedText.kt b/src/com/reco1l/andengine/text/ExtendedText.kt index 9951fdd6c..713feddfd 100644 --- a/src/com/reco1l/andengine/text/ExtendedText.kt +++ b/src/com/reco1l/andengine/text/ExtendedText.kt @@ -121,12 +121,6 @@ open class ExtendedText : ExtendedEntity() { shouldRebuildTextureBuffer = false textureBuffer = TextTextureBuffer(2 * VERTICES_PER_CHARACTER * maximumSize, GL_STATIC_DRAW, true) - - if (font!!.texture.textureOptions.mPreMultipyAlpha) { - setBlendFunction(BLENDFUNCTION_SOURCE_PREMULTIPLYALPHA_DEFAULT, BLENDFUNCTION_DESTINATION_PREMULTIPLYALPHA_DEFAULT) - } else { - setBlendFunction(BLENDFUNCTION_SOURCE_DEFAULT, BLENDFUNCTION_DESTINATION_DEFAULT) - } } val lines = text.split('\n').toTypedArray() From f991d58cd76d23f7ce504cfff4ddaedb93155156 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Fri, 27 Dec 2024 22:04:48 -0300 Subject: [PATCH 25/87] Optimize RoundedBox --- src/com/reco1l/andengine/shape/Box.kt | 35 +++++++++------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/com/reco1l/andengine/shape/Box.kt b/src/com/reco1l/andengine/shape/Box.kt index b8a30de39..d0593c221 100644 --- a/src/com/reco1l/andengine/shape/Box.kt +++ b/src/com/reco1l/andengine/shape/Box.kt @@ -124,7 +124,7 @@ open class RoundedBox : ExtendedEntity() { class RoundedBoxVertexBuffer(private val segmentsPerArc: Int) : VertexBuffer( - (5 /*Quads*/ * 4 + (segmentsPerArc + 2) /*Arcs*/ * 4) * 2, + (3 /*Quads*/ * 4 + (segmentsPerArc + 2) /*Arcs*/ * 4) * 2, GL11.GL_STATIC_DRAW, false ) { @@ -151,41 +151,28 @@ open class RoundedBox : ExtendedEntity() { } // Quads: - // [1] - // [4] [5] [2] - // [3] + // [ ] + // [1] [2] [3] + // [ ] // [1] addQuad( - fromX = cornerRadius, fromY = 0f, - toX = width - cornerRadius, toY = cornerRadius + fromX = 0f, fromY = cornerRadius, + toX = cornerRadius, toY = height - cornerRadius ) // [2] addQuad( - fromX = width - cornerRadius, fromY = cornerRadius, - toX = width, toY = height - cornerRadius - ) - - // [3] - addQuad( - fromX = cornerRadius, fromY = height - cornerRadius, + fromX = cornerRadius, fromY = 0f, toX = width - cornerRadius, toY = height ) - // [4] - addQuad( - fromX = 0f, fromY = cornerRadius, - toX = cornerRadius, toY = height - cornerRadius - ) - - // [5] + // [3] addQuad( - fromX = cornerRadius, fromY = cornerRadius, - toX = width - cornerRadius, toY = height - cornerRadius + fromX = width - cornerRadius, fromY = cornerRadius, + toX = width, toY = height - cornerRadius ) - // Arcs fun addArc(centerX: Float, centerY: Float, startAngle: Float, endAngle: Float) { @@ -234,7 +221,7 @@ open class RoundedBox : ExtendedEntity() { var offset = 0 // Quads - for (i in 0 until 5) { + for (i in 0 until 3) { gl.glDrawArrays(GL_TRIANGLE_STRIP, offset, 4) offset += 4 } From 6c1a2af831d26f7dd75c3de0b92d6957bc18ebc2 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Fri, 27 Dec 2024 22:15:13 -0300 Subject: [PATCH 26/87] Introduce size property --- src/com/reco1l/andengine/Entities.kt | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/com/reco1l/andengine/Entities.kt b/src/com/reco1l/andengine/Entities.kt index b90979023..29cadc323 100644 --- a/src/com/reco1l/andengine/Entities.kt +++ b/src/com/reco1l/andengine/Entities.kt @@ -4,6 +4,7 @@ import com.reco1l.framework.math.Vec4 import org.anddev.andengine.entity.IEntity import org.anddev.andengine.entity.scene.CameraScene import org.anddev.andengine.entity.shape.IShape +import kotlin.math.max fun IEntity?.getPadding() = when (this) { @@ -26,6 +27,20 @@ fun IEntity?.getPaddedHeight() = when (this) { } +/** + * The size of the entity. + * + * When using the getter this will return the maximum value between the width and height or the same. + * When using the setter this will set the width and height to the same value. + */ +var ExtendedEntity.size + get() = max(width, height) + set(value) { + width = value + height = value + } + + /** * The total offset applied to the X axis. */ From ee7b195ef0d5b4c756399f9c87d71a0c1719ad6a Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Fri, 27 Dec 2024 22:23:59 -0300 Subject: [PATCH 27/87] Improve Vec2 and Vec4 constructors --- src/com/reco1l/framework/math/Vec2.kt | 7 +++++-- src/com/reco1l/framework/math/Vec4.kt | 13 +++++++++---- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/com/reco1l/framework/math/Vec2.kt b/src/com/reco1l/framework/math/Vec2.kt index fe303f071..283e30167 100644 --- a/src/com/reco1l/framework/math/Vec2.kt +++ b/src/com/reco1l/framework/math/Vec2.kt @@ -2,12 +2,15 @@ package com.reco1l.framework.math data class Vec2( - val x: Float = 0f, + val x: Float, - val y: Float = 0f, + val y: Float, ) { + constructor(value: Float = 0f) : this(value, value) + + val total get() = x + y diff --git a/src/com/reco1l/framework/math/Vec4.kt b/src/com/reco1l/framework/math/Vec4.kt index 675c25b49..a4b11dade 100644 --- a/src/com/reco1l/framework/math/Vec4.kt +++ b/src/com/reco1l/framework/math/Vec4.kt @@ -2,13 +2,18 @@ package com.reco1l.framework.math data class Vec4( - val x: Float = 0f, - val y: Float = 0f, - val z: Float = 0f, - val w: Float = 0f, + val x: Float, + val y: Float, + val z: Float, + val w: Float, ) { + constructor(value: Float = 0f) : this(value, value, value, value) + + constructor(xz: Float, yw: Float) : this(xz, yw, xz, yw) + + val left: Float get() = x From 3e6ea8fda866cf3e3e83c5b33f477a6937ffaeac Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sat, 28 Dec 2024 17:14:00 -0300 Subject: [PATCH 28/87] Fix padding in ConstraintContainer --- src/com/reco1l/andengine/container/ConstraintContainer.kt | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/com/reco1l/andengine/container/ConstraintContainer.kt b/src/com/reco1l/andengine/container/ConstraintContainer.kt index 850c491d0..f92faaf94 100644 --- a/src/com/reco1l/andengine/container/ConstraintContainer.kt +++ b/src/com/reco1l/andengine/container/ConstraintContainer.kt @@ -23,9 +23,10 @@ class ConstraintContainer : Container() { val targetX = if (target == this) 0f else target.getDrawX() val targetWidth = if (target == this) getPaddedWidth() else target.getDrawWidth() + val paddingLeft = if (target == this) getPadding().left else 0f val anchorOffsetX = targetWidth * child.anchor.x - var childX = max(getPadding().left, child.x) + var childX = max(paddingLeft, child.x) // Relative positions will be multiplied by the remaining space from the // target's position to the edge of the container. @@ -43,9 +44,10 @@ class ConstraintContainer : Container() { val targetY = if (target == this) 0f else target.getDrawY() val targetHeight = if (target == this) getPaddedHeight() else target.getDrawHeight() + val paddingTop = if (target == this) getPadding().top else 0f val anchorOffsetY = targetHeight * child.anchor.y - var childY = max(getPadding().top, child.y) + var childY = max(paddingTop, child.y) // Relative positions will be multiplied by the remaining space from the // target's position to the edge of the container. From 443e0a8e173b8710b297d8f1232041faed9d453a Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sat, 28 Dec 2024 17:14:24 -0300 Subject: [PATCH 29/87] Use Vec2 for ExtendedEntity.size property --- src/com/reco1l/andengine/Entities.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/com/reco1l/andengine/Entities.kt b/src/com/reco1l/andengine/Entities.kt index 29cadc323..00a3f653e 100644 --- a/src/com/reco1l/andengine/Entities.kt +++ b/src/com/reco1l/andengine/Entities.kt @@ -1,10 +1,10 @@ package com.reco1l.andengine +import com.reco1l.framework.math.Vec2 import com.reco1l.framework.math.Vec4 import org.anddev.andengine.entity.IEntity import org.anddev.andengine.entity.scene.CameraScene import org.anddev.andengine.entity.shape.IShape -import kotlin.math.max fun IEntity?.getPadding() = when (this) { @@ -34,10 +34,10 @@ fun IEntity?.getPaddedHeight() = when (this) { * When using the setter this will set the width and height to the same value. */ var ExtendedEntity.size - get() = max(width, height) + get() = Vec2(width, height) set(value) { - width = value - height = value + width = value.x + height = value.y } From 5279392de77bf4d291b222e30332b111e578e758 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sat, 28 Dec 2024 18:18:18 -0300 Subject: [PATCH 30/87] Fix potential crash due to corner radius being 0 --- src/com/reco1l/andengine/shape/Box.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/shape/Box.kt b/src/com/reco1l/andengine/shape/Box.kt index d0593c221..041ef7d79 100644 --- a/src/com/reco1l/andengine/shape/Box.kt +++ b/src/com/reco1l/andengine/shape/Box.kt @@ -103,7 +103,8 @@ open class RoundedBox : ExtendedEntity() { override fun onUpdateVertexBuffer() { - val cornerRadius = cornerRadius.coerceIn(0f, min(drawWidth, drawHeight) / 2f) + val smallerSide = min(drawWidth, drawHeight) + val cornerRadius = cornerRadius.coerceAtMost(smallerSide / 2f).coerceAtLeast(0f) if (shouldRebuildVertexBuffer) { shouldRebuildVertexBuffer = false From 3e87067b1f839790e13806c6f4fa70b9758712a8 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sat, 28 Dec 2024 18:18:33 -0300 Subject: [PATCH 31/87] Introduce foreground and background --- src/com/reco1l/andengine/ExtendedEntity.kt | 49 ++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 507131659..8a85f7b40 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -4,6 +4,7 @@ import android.util.* import com.reco1l.andengine.container.* import com.reco1l.andengine.modifier.* import com.reco1l.framework.* +import com.reco1l.framework.math.Vec2 import com.reco1l.framework.math.Vec4 import org.anddev.andengine.engine.camera.* import org.anddev.andengine.entity.* @@ -147,6 +148,39 @@ abstract class ExtendedEntity( } } + /** + * The background entity. This entity will be drawn before the entity children and will not be + * affected by padding. + */ + open var background: ExtendedEntity? = null + set(value) { + if (field != value) { + if (value?.parent != null) { + Log.e("ExtendedEntity", "The background entity is already attached to another entity.") + return + } + + field = value + } + } + + /** + * The foreground entity. This entity will be drawn after the entity children and will not be + * affected by padding. + */ + open var foreground: ExtendedEntity? = null + set(value) { + if (field != value) { + if (value?.parent != null) { + Log.e("ExtendedEntity", "The foreground entity is already attached to another entity.") + return + } + + field = value + } + } + + /** * Whether the entity should clip its children. */ @@ -433,6 +467,21 @@ abstract class ExtendedEntity( applyBlending(pGL) } + override fun doDraw(gl: GL10, camera: Camera) { + + background?.setSize(drawWidth, drawHeight) + background?.onDraw(gl, camera) + + super.doDraw(gl, camera) + } + + override fun onDrawChildren(gl: GL10, camera: Camera) { + super.onDrawChildren(gl, camera) + + foreground?.setSize(drawWidth, drawHeight) + foreground?.onDraw(gl, camera) + } + override fun onManagedDraw(gl: GL10, camera: Camera) { if (isVertexBufferDirty) { From 32305dcfbaf2152ab8fabbc3d3cc974d4007f5cd Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sat, 28 Dec 2024 18:44:57 -0300 Subject: [PATCH 32/87] Improve Padding compatibility --- src/com/reco1l/andengine/ExtendedEntity.kt | 54 ++++++++++++------- .../container/ConstraintContainer.kt | 40 ++++++++------ .../reco1l/andengine/container/Container.kt | 4 +- .../andengine/container/LinearContainer.kt | 8 --- .../container/ScrollableContainer.kt | 4 +- 5 files changed, 64 insertions(+), 46 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 8a85f7b40..452803847 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -16,7 +16,6 @@ import org.anddev.andengine.opengl.util.* import org.anddev.andengine.opengl.vertex.* import org.anddev.andengine.util.Transformation import javax.microedition.khronos.opengles.* -import kotlin.math.max /** @@ -284,7 +283,7 @@ abstract class ExtendedEntity( x *= parent.getPaddedWidth() } - return max(parent.getPadding().left, x) + totalOffsetX + return x + totalOffsetX } @@ -304,7 +303,7 @@ abstract class ExtendedEntity( y *= parent.getPaddedHeight() } - return max(parent.getPadding().top, y) + totalOffsetY + return y + totalOffsetY } @@ -476,26 +475,20 @@ abstract class ExtendedEntity( } override fun onDrawChildren(gl: GL10, camera: Camera) { - super.onDrawChildren(gl, camera) - - foreground?.setSize(drawWidth, drawHeight) - foreground?.onDraw(gl, camera) - } - override fun onManagedDraw(gl: GL10, camera: Camera) { + val hasPaddingApplicable = padding.left > 0f || padding.top > 0f - if (isVertexBufferDirty) { - isVertexBufferDirty = false - onUpdateVertexBuffer() + if (hasPaddingApplicable) { + gl.glTranslatef(padding.left, padding.top, 0f) } if (clipChildren) { GLHelper.enableScissorTest(gl) var (bottomLeftX, bottomLeftY) = camera.getScreenSpaceCoordinates(convertLocalToSceneCoordinates(0f, 0f)) - var (topLeftX, topLeftY) = camera.getScreenSpaceCoordinates(convertLocalToSceneCoordinates(0f, drawHeight)) - var (topRightX, topRightY) = camera.getScreenSpaceCoordinates(convertLocalToSceneCoordinates(drawWidth, drawHeight)) - var (bottomRightX, bottomRightY) = camera.getScreenSpaceCoordinates(convertLocalToSceneCoordinates(drawWidth, 0f)) + var (topLeftX, topLeftY) = camera.getScreenSpaceCoordinates(convertLocalToSceneCoordinates(0f, getPaddedHeight())) + var (topRightX, topRightY) = camera.getScreenSpaceCoordinates(convertLocalToSceneCoordinates(getPaddedWidth(), getPaddedHeight())) + var (bottomRightX, bottomRightY) = camera.getScreenSpaceCoordinates(convertLocalToSceneCoordinates(getPaddedWidth(), 0f)) // Flip the Y axis to match the OpenGL coordinate system. bottomLeftY = camera.surfaceHeight - bottomLeftY @@ -517,11 +510,28 @@ abstract class ExtendedEntity( ) } - super.onManagedDraw(gl, camera) + super.onDrawChildren(gl, camera) if (clipChildren) { GLHelper.disableScissorTest(gl) } + + if (hasPaddingApplicable) { + gl.glTranslatef(-padding.right, -padding.top, 0f) + } + + foreground?.setSize(drawWidth, drawHeight) + foreground?.onDraw(gl, camera) + } + + override fun onManagedDraw(gl: GL10, camera: Camera) { + + if (isVertexBufferDirty) { + isVertexBufferDirty = false + onUpdateVertexBuffer() + } + + super.onManagedDraw(gl, camera) } override fun onInitDraw(pGL: GL10) { @@ -584,11 +594,19 @@ abstract class ExtendedEntity( if (contentWidth != width || contentHeight != height) { if (autoSizeAxes.isHorizontal) { - width = if (relativeSizeAxes.isHorizontal) contentWidth / parent.getPaddedWidth() else contentWidth + width = contentWidth + padding.horizontal + + if (relativeSizeAxes.isHorizontal) { + width /= parent.getPaddedWidth() + } } if (autoSizeAxes.isVertical) { - height = if (relativeSizeAxes.isVertical) contentHeight / parent.getPaddedHeight() else contentHeight + height = contentHeight + padding.vertical + + if (relativeSizeAxes.isVertical) { + height /= parent.getPaddedHeight() + } } updateVertexBuffer() diff --git a/src/com/reco1l/andengine/container/ConstraintContainer.kt b/src/com/reco1l/andengine/container/ConstraintContainer.kt index f92faaf94..7e1e7a71a 100644 --- a/src/com/reco1l/andengine/container/ConstraintContainer.kt +++ b/src/com/reco1l/andengine/container/ConstraintContainer.kt @@ -20,18 +20,22 @@ class ConstraintContainer : Container() { val target = constraints[child] ?: this - val targetX = if (target == this) 0f else target.getDrawX() - val targetWidth = if (target == this) getPaddedWidth() else target.getDrawWidth() + var targetX = target.getDrawX() + var targetWidth = target.getDrawWidth() - val paddingLeft = if (target == this) getPadding().left else 0f - val anchorOffsetX = targetWidth * child.anchor.x + if (target == this) { + targetX = 0f + targetWidth = getPaddedWidth() + } - var childX = max(paddingLeft, child.x) + val anchorOffsetX = targetWidth * child.anchor.x - // Relative positions will be multiplied by the remaining space from the - // target's position to the edge of the container. + var childX = child.x if (child.relativePositionAxes.isHorizontal) { - childX *= drawWidth - targetX + + // Relative positions will be multiplied by the remaining space from the + // target's position to the edge of the container. + childX *= getPaddedWidth() - targetX } return targetX + childX + child.originOffsetX + anchorOffsetX + child.translationX @@ -41,18 +45,22 @@ class ConstraintContainer : Container() { val target = constraints[child] ?: this - val targetY = if (target == this) 0f else target.getDrawY() - val targetHeight = if (target == this) getPaddedHeight() else target.getDrawHeight() + var targetY = target.getDrawY() + var targetHeight = target.getDrawHeight() - val paddingTop = if (target == this) getPadding().top else 0f - val anchorOffsetY = targetHeight * child.anchor.y + if (target == this) { + targetY = 0f + targetHeight = getPaddedHeight() + } - var childY = max(paddingTop, child.y) + val anchorOffsetY = targetHeight * child.anchor.y - // Relative positions will be multiplied by the remaining space from the - // target's position to the edge of the container. + var childY = child.y if (child.relativePositionAxes.isVertical) { - childY *= drawHeight - targetY + + // Relative positions will be multiplied by the remaining space from the + // target's position to the edge of the container. + childY *= getPaddedHeight() - targetY } return targetY + childY + child.originOffsetY + anchorOffsetY + child.translationY diff --git a/src/com/reco1l/andengine/container/Container.kt b/src/com/reco1l/andengine/container/Container.kt index efd1263ac..2364427d5 100644 --- a/src/com/reco1l/andengine/container/Container.kt +++ b/src/com/reco1l/andengine/container/Container.kt @@ -64,7 +64,7 @@ open class Container : ExtendedEntity() { x *= getPaddedWidth() } - return max(getPadding().left, x) + child.totalOffsetX + return x + child.totalOffsetX } open fun getChildDrawY(child: ExtendedEntity): Float { @@ -74,7 +74,7 @@ open class Container : ExtendedEntity() { y *= getPaddedHeight() } - return max(getPadding().top, y) + child.totalOffsetY + return y + child.totalOffsetY } diff --git a/src/com/reco1l/andengine/container/LinearContainer.kt b/src/com/reco1l/andengine/container/LinearContainer.kt index 9f2dd9080..3c4e4abc1 100644 --- a/src/com/reco1l/andengine/container/LinearContainer.kt +++ b/src/com/reco1l/andengine/container/LinearContainer.kt @@ -51,10 +51,6 @@ open class LinearContainer : Container() { contentWidth += child.getDrawWidth() contentHeight = max(contentHeight, child.getDrawHeight()) - if (i == 0) { - contentWidth += getPadding().left - } - if (i < childCount - 1) { contentWidth += spacing } @@ -66,10 +62,6 @@ open class LinearContainer : Container() { contentWidth = max(contentWidth, child.getDrawWidth()) contentHeight += child.getDrawHeight() - if (i == 0) { - contentHeight += getPadding().top - } - if (i < childCount - 1) { contentHeight += spacing } diff --git a/src/com/reco1l/andengine/container/ScrollableContainer.kt b/src/com/reco1l/andengine/container/ScrollableContainer.kt index b656cd2ca..ab6752f0d 100644 --- a/src/com/reco1l/andengine/container/ScrollableContainer.kt +++ b/src/com/reco1l/andengine/container/ScrollableContainer.kt @@ -342,7 +342,7 @@ open class ScrollableContainer : Container() { return super.getChildDrawX(child) } - return -scrollX + max(getPadding().left, child.x) - child.originOffsetX + child.translationX + return -scrollX + child.x - child.originOffsetX + child.translationX } override fun getChildDrawY(child: ExtendedEntity): Float { @@ -351,7 +351,7 @@ open class ScrollableContainer : Container() { return super.getChildDrawY(child) } - return -scrollY + max(getPadding().top, child.y) - child.originOffsetY + child.translationY + return -scrollY + child.y - child.originOffsetY + child.translationY } From a6f7722d58dc1ecc623fee4a81a044ec0a24ee58 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sat, 28 Dec 2024 20:31:27 -0300 Subject: [PATCH 33/87] Introduce touch area binding --- src/com/reco1l/andengine/ExtendedEntity.kt | 27 ++++++++++++++--- .../container/ScrollableContainer.kt | 30 +++++++++---------- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 452803847..9b0adb3d2 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -4,7 +4,6 @@ import android.util.* import com.reco1l.andengine.container.* import com.reco1l.andengine.modifier.* import com.reco1l.framework.* -import com.reco1l.framework.math.Vec2 import com.reco1l.framework.math.Vec4 import org.anddev.andengine.engine.camera.* import org.anddev.andengine.entity.* @@ -313,6 +312,8 @@ abstract class ExtendedEntity( private var isVertexBufferDirty = true + private var currentBoundEntity: ITouchArea? = null + // Attachment @@ -819,16 +820,34 @@ abstract class ExtendedEntity( localY: Float ): Boolean { + val boundEntity = currentBoundEntity + if (boundEntity != null) { + boundEntity as IEntity + + val transformedX = localX - boundEntity.getDrawX() + val transformedY = localY - boundEntity.getDrawY() + + boundEntity.onAreaTouched(event, transformedX, transformedY) + + if (event.isActionUp || event.isActionOutside || event.isActionCancel) { + currentBoundEntity = null + } + return true + } + try { for (i in childCount - 1 downTo 0) { val child = getChild(i) - val transformedX = localX - child.getDrawX() - val transformedY = localY - child.getDrawY() + if (child is ITouchArea && child.contains(localX, localY)) { - if (child is ITouchArea && child.contains(transformedX, transformedY)) { + val transformedX = localX - child.getDrawX() + val transformedY = localY - child.getDrawY() if (child.onAreaTouched(event, transformedX, transformedY)) { + if (event.isActionDown) { + currentBoundEntity = child + } return true } } diff --git a/src/com/reco1l/andengine/container/ScrollableContainer.kt b/src/com/reco1l/andengine/container/ScrollableContainer.kt index ab6752f0d..fe796b4b9 100644 --- a/src/com/reco1l/andengine/container/ScrollableContainer.kt +++ b/src/com/reco1l/andengine/container/ScrollableContainer.kt @@ -280,31 +280,33 @@ open class ScrollableContainer : Container() { when (event.action) { ACTION_DOWN -> { - isDragging = true - initialX = localX initialY = localY velocityX = 0f velocityY = 0f - - lastDragTimeSec = elapsedTimeSec } ACTION_MOVE -> { - isDragging = true - var deltaX = localX - initialX - var deltaY = localY - initialY + var deltaX = if (scrollAxes.isHorizontal) localX - initialX else 0f + var deltaY = if (scrollAxes.isVertical) localY - initialY else 0f + + if (!isDragging) { + isDragging = abs(deltaX) > 1f || abs(deltaY) > 1f + + if (!isDragging) { + return super.onAreaTouched(event, localX, localY) + } + } val length = hypot(deltaX, deltaY) - // Slow down the scroll when reaching the bounds. - if (scrollX + deltaX < 0 || scrollX + deltaX > maxScrollX) { + if (scrollX - deltaX < 0 || scrollX - deltaX > maxScrollX) { deltaX *= if (length <= 0) 0f else length.pow(0.7f) / length } - if (scrollY + deltaY < 0 || scrollY + deltaY > maxScrollY) { + if (scrollY - deltaY < 0 || scrollY - deltaY > maxScrollY) { deltaY *= if (length <= 0) 0f else length.pow(0.7f) / length } @@ -313,18 +315,16 @@ open class ScrollableContainer : Container() { if (abs(deltaX) > 0.1f) { scrollX -= deltaX velocityX = abs(deltaX / dragTimeSec) * sign(deltaX) - initialX = localX - lastDragTimeSec = elapsedTimeSec } if (abs(deltaY) > 0.1f) { scrollY -= deltaY velocityY = abs(deltaY / dragTimeSec) * sign(deltaY) - initialY = localY - lastDragTimeSec = elapsedTimeSec } + + lastDragTimeSec = elapsedTimeSec } else -> { @@ -332,7 +332,7 @@ open class ScrollableContainer : Container() { } } - return super.onAreaTouched(event, localX, localY) + return isDragging || super.onAreaTouched(event, localX, localY) } From f2cb224b4a3debfe3cea16a1f921b146f51f8332 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sat, 28 Dec 2024 20:48:42 -0300 Subject: [PATCH 34/87] Make ConstraintContainer open --- src/com/reco1l/andengine/container/ConstraintContainer.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/com/reco1l/andengine/container/ConstraintContainer.kt b/src/com/reco1l/andengine/container/ConstraintContainer.kt index 7e1e7a71a..159257aca 100644 --- a/src/com/reco1l/andengine/container/ConstraintContainer.kt +++ b/src/com/reco1l/andengine/container/ConstraintContainer.kt @@ -3,14 +3,13 @@ package com.reco1l.andengine.container import com.reco1l.andengine.* import org.anddev.andengine.entity.* import org.anddev.andengine.entity.shape.* -import kotlin.math.max /** * Container that allows to constrain nested entities to other entities in the same container. * * This is useful for creating complex layouts. */ -class ConstraintContainer : Container() { +open class ConstraintContainer : Container() { private val constraints = mutableMapOf() From 35a631feaba254375164c25da91ad5b5c3f0e96b Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sun, 29 Dec 2024 02:44:58 -0300 Subject: [PATCH 35/87] Change transform order --- src/com/reco1l/andengine/ExtendedEntity.kt | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 9b0adb3d2..9ec9f1514 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -769,21 +769,21 @@ abstract class ExtendedEntity( mParentToLocalTransformation.setToIdentity() mParentToLocalTransformation.postTranslate(-drawX, -drawY) - if (scaleX != 1f || scaleY != 1f) { - val offsetX = drawWidth * mScaleCenterX - val offsetY = drawHeight * mScaleCenterY + if (rotation != 0f) { + val offsetX = drawWidth * mRotationCenterX + val offsetY = drawHeight * mRotationCenterY mParentToLocalTransformation.postTranslate(-offsetX, -offsetY) - mParentToLocalTransformation.postScale(1 / scaleX, 1 / scaleY) + mParentToLocalTransformation.postRotate(-rotation) mParentToLocalTransformation.postTranslate(offsetX, offsetY) } - if (rotation != 0f) { - val offsetX = drawWidth * mRotationCenterX - val offsetY = drawHeight * mRotationCenterY + if (scaleX != 1f || scaleY != 1f) { + val offsetX = drawWidth * mScaleCenterX + val offsetY = drawHeight * mScaleCenterY mParentToLocalTransformation.postTranslate(-offsetX, -offsetY) - mParentToLocalTransformation.postRotate(-rotation) + mParentToLocalTransformation.postScale(1 / scaleX, 1 / scaleY) mParentToLocalTransformation.postTranslate(offsetX, offsetY) } @@ -855,7 +855,6 @@ abstract class ExtendedEntity( } catch (e: IndexOutOfBoundsException) { Log.e("ExtendedEntity", "A child entity was removed during touch event propagation.", e) } - return false } From 06ac2c806886158526cc60c400557c3e6f1d71a2 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sun, 29 Dec 2024 02:45:32 -0300 Subject: [PATCH 36/87] Use drawWidth and drawHeight to check whether a point it's on a Entity --- src/com/reco1l/andengine/ExtendedEntity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 9ec9f1514..6eb649fd2 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -711,7 +711,7 @@ abstract class ExtendedEntity( override fun contains(x: Float, y: Float): Boolean { - if (width == 0f || height == 0f) { + if (drawWidth == 0f || drawHeight == 0f) { return false } From 17e1741fbc49191d0234a5b7c8f51e75d866ca9f Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sun, 29 Dec 2024 02:46:24 -0300 Subject: [PATCH 37/87] Prevent event propagation if ScrollableContainer is scrolling --- src/com/reco1l/andengine/container/ScrollableContainer.kt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/container/ScrollableContainer.kt b/src/com/reco1l/andengine/container/ScrollableContainer.kt index fe796b4b9..d84d41190 100644 --- a/src/com/reco1l/andengine/container/ScrollableContainer.kt +++ b/src/com/reco1l/andengine/container/ScrollableContainer.kt @@ -332,7 +332,11 @@ open class ScrollableContainer : Container() { } } - return isDragging || super.onAreaTouched(event, localX, localY) + if (!isDragging) { + return super.onAreaTouched(event, localX, localY) + } + + return true } From 88fbc3bc32c5041e8a76a03f3c9006085128fd82 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sun, 29 Dec 2024 03:17:44 -0300 Subject: [PATCH 38/87] Update background and foreground --- src/com/reco1l/andengine/ExtendedEntity.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index 6eb649fd2..c7ea26f96 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -551,6 +551,17 @@ abstract class ExtendedEntity( } + // Update + + override fun onManagedUpdate(pSecondsElapsed: Float) { + + background?.onManagedUpdate(pSecondsElapsed) + foreground?.onManagedUpdate(pSecondsElapsed) + + super.onManagedUpdate(pSecondsElapsed) + } + + // Vertex buffer override fun updateVertexBuffer() { @@ -611,6 +622,7 @@ abstract class ExtendedEntity( } updateVertexBuffer() + invalidateTransformations() (parent as? Container)?.onChildSizeChanged(this) return true @@ -637,6 +649,7 @@ abstract class ExtendedEntity( height = newHeight updateVertexBuffer() + invalidateTransformations() val parent = parent if (parent is Container) { @@ -658,6 +671,7 @@ abstract class ExtendedEntity( width = value updateVertexBuffer() + invalidateTransformations() (parent as? Container)?.onChildSizeChanged(this) } } @@ -672,6 +686,7 @@ abstract class ExtendedEntity( height = value updateVertexBuffer() + invalidateTransformations() (parent as? Container)?.onChildSizeChanged(this) } } @@ -715,7 +730,7 @@ abstract class ExtendedEntity( return false } - return EntityCollision.contains(this, x, y) + return EntityCollision.contains(this, x, y, parent is Scene) } override fun isCulled(pCamera: Camera): Boolean { From c0ccd9a345239131b0cc4060e9a9042e70a29f33 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sun, 29 Dec 2024 03:18:04 -0300 Subject: [PATCH 39/87] Set traversal front to back by default for ExtendedScene --- src/com/reco1l/andengine/ExtendedScene.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/src/com/reco1l/andengine/ExtendedScene.kt b/src/com/reco1l/andengine/ExtendedScene.kt index f61155e20..5111da013 100644 --- a/src/com/reco1l/andengine/ExtendedScene.kt +++ b/src/com/reco1l/andengine/ExtendedScene.kt @@ -28,6 +28,7 @@ open class ExtendedScene : Scene(), IShape { init { super.setTouchAreaBindingEnabled(true) + super.setOnAreaTouchTraversalFrontToBack() } From 3fdc324843b14abdb6a2c715b80557419d5d3fd0 Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sun, 29 Dec 2024 03:18:28 -0300 Subject: [PATCH 40/87] Difference whether to apply transformation from scene or from parent entity --- src/com/reco1l/andengine/EntityCollision.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/com/reco1l/andengine/EntityCollision.kt b/src/com/reco1l/andengine/EntityCollision.kt index a6432d368..d96959fb3 100644 --- a/src/com/reco1l/andengine/EntityCollision.kt +++ b/src/com/reco1l/andengine/EntityCollision.kt @@ -10,7 +10,7 @@ object EntityCollision { private val vertices = FloatArray(8) - fun contains(entity: ExtendedEntity, x: Float, y: Float): Boolean { + fun contains(entity: ExtendedEntity, x: Float, y: Float, fromScene: Boolean): Boolean { val left = 0f val top = 0f @@ -29,7 +29,11 @@ object EntityCollision { vertices[6 + VERTEX_INDEX_X] = left vertices[6 + VERTEX_INDEX_Y] = bottom - entity.getLocalToSceneTransformation().transform(vertices) + if (fromScene) { + entity.localToSceneTransformation.transform(vertices) + } else { + entity.localToParentTransformation.transform(vertices) + } return ShapeCollisionChecker.checkContains(vertices, vertices.size, x, y) } From f4f7659a1a92378465caeb29a58adfadc1569a3c Mon Sep 17 00:00:00 2001 From: Geronimo Ferruccio Date: Sun, 29 Dec 2024 03:21:39 -0300 Subject: [PATCH 41/87] Draw indicators on top --- src/com/reco1l/andengine/container/ScrollableContainer.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/container/ScrollableContainer.kt b/src/com/reco1l/andengine/container/ScrollableContainer.kt index d84d41190..c6cf72417 100644 --- a/src/com/reco1l/andengine/container/ScrollableContainer.kt +++ b/src/com/reco1l/andengine/container/ScrollableContainer.kt @@ -270,9 +270,10 @@ open class ScrollableContainer : Container() { } override fun onManagedDrawChildren(pGL: GL10, pCamera: Camera) { + super.onManagedDrawChildren(pGL, pCamera) + indicatorX?.onDraw(pGL, pCamera) indicatorY?.onDraw(pGL, pCamera) - super.onManagedDrawChildren(pGL, pCamera) } override fun onAreaTouched(event: TouchEvent, localX: Float, localY: Float): Boolean { From a9cb858d47a21ca4d014cba971f987cb4565135a Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:03:58 +0800 Subject: [PATCH 42/87] Update copyright year --- src/ru/nsu/ccfit/zuev/osu/MainScene.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/MainScene.java b/src/ru/nsu/ccfit/zuev/osu/MainScene.java index c00878fbc..4de7105ed 100644 --- a/src/ru/nsu/ccfit/zuev/osu/MainScene.java +++ b/src/ru/nsu/ccfit/zuev/osu/MainScene.java @@ -194,7 +194,7 @@ public boolean onAreaTouched(TouchEvent event, float localX, float localY) { .setMessage( "

osu!droid

\n" + "
Version " + BuildConfig.VERSION_NAME + "
\n" + - "

Made by osu!droid team
osu! is © peppy 2007-2024

\n" + + "

Made by osu!droid team
osu! is © peppy 2007-2025

\n" + "
\n" + "Visit official osu! website ↗\n" + "
\n" + From 599b4ad9ac0fd288e28c73b50fca6268e6ece7a4 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:15:08 +0800 Subject: [PATCH 43/87] Fix ScoreV2 combo portion calculation incrementing combo on non-combo judgement --- src/ru/nsu/ccfit/zuev/osu/scoring/StatisticV2.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/scoring/StatisticV2.java b/src/ru/nsu/ccfit/zuev/osu/scoring/StatisticV2.java index 0fc3fb10a..ca5c6c6c6 100644 --- a/src/ru/nsu/ccfit/zuev/osu/scoring/StatisticV2.java +++ b/src/ru/nsu/ccfit/zuev/osu/scoring/StatisticV2.java @@ -244,7 +244,7 @@ private void addScore(final int amount, final boolean combo) { int currentMaxCombo = getScoreMaxCombo(); // At this point, the combo increment in registerHit has not happened, but it is necessary for ScoreV2 // calculation, so we do it locally here. - if (currentCombo == currentMaxCombo) { + if (combo && currentCombo == currentMaxCombo) { currentMaxCombo++; } From e6eb950f4464e736af6053ac7a5c9277a1f80da7 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:20:41 +0800 Subject: [PATCH 44/87] Add `maxPlayersChanged` event bridge to `FakeSocket` --- src/com/reco1l/BuildUtils.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/BuildUtils.kt b/src/com/reco1l/BuildUtils.kt index d4b42370b..ed973ea31 100644 --- a/src/com/reco1l/BuildUtils.kt +++ b/src/com/reco1l/BuildUtils.kt @@ -84,7 +84,10 @@ class FakeSocket(private val uid: Long, private val username: String) : Socket(n "playerModsChanged" -> "playerModsChanged" to arrayOf(uid.toString(), args[0]) "playerStatusChanged" -> "playerStatusChanged" to arrayOf(uid.toString(), args[0]) - // These events requires special handling + // In these events, the client expects to receive an integer as the first argument. + "maxPlayersChanged" -> "maxPlayersChanged" to arrayOf(args[0].toString()) + + // These events require special handling "roomGameplaySettingsChanged" -> "roomGameplaySettingsChanged" to arrayOf((args[0] as JSONObject).apply { Multiplayer.room!!.gameplaySettings.also { if (!has("isFreeMod")) put("isFreeMod", it.isFreeMod) From 1b6d7e5d8b4867d75de1332e79e36ff7b640bb1a Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:21:32 +0800 Subject: [PATCH 45/87] Add room max players seek bar setting --- res/values/strings_temporary_for_locals.xml | 3 +++ res/xml/multiplayer_room_settings.xml | 11 ++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/res/values/strings_temporary_for_locals.xml b/res/values/strings_temporary_for_locals.xml index 97d55c1e1..2f7023a06 100644 --- a/res/values/strings_temporary_for_locals.xml +++ b/res/values/strings_temporary_for_locals.xml @@ -16,4 +16,7 @@ 1 + Max Players + Set the maximum number of players allowed in the room + \ No newline at end of file diff --git a/res/xml/multiplayer_room_settings.xml b/res/xml/multiplayer_room_settings.xml index 3d82cccde..2fe0b5524 100644 --- a/res/xml/multiplayer_room_settings.xml +++ b/res/xml/multiplayer_room_settings.xml @@ -14,7 +14,16 @@ android:key="room_password" android:summary="@string/mp_room_password_summary" android:title="@string/mp_room_password_title" - app:layout="@layout/settings_preference_input_bottom" /> + app:layout="@layout/settings_preference_input" /> + + From 51a9a9314491ba765db59a515a04b3ccb944ba9f Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:22:39 +0800 Subject: [PATCH 46/87] Move `RoomPlayerList` initialization to invalidation operation The previous one assumes that max player count is fixed, while in reality it is not. --- .../reco1l/osu/multiplayer/RoomPlayerList.kt | 37 +++++++++++-------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/com/reco1l/osu/multiplayer/RoomPlayerList.kt b/src/com/reco1l/osu/multiplayer/RoomPlayerList.kt index 0d5ace363..69472e177 100644 --- a/src/com/reco1l/osu/multiplayer/RoomPlayerList.kt +++ b/src/com/reco1l/osu/multiplayer/RoomPlayerList.kt @@ -25,15 +25,7 @@ class RoomPlayerList(val room: Room) : ScrollableList(), IScrollDetectorListener init { - for (i in 0 until room.maxPlayers) { - camY = -146f - - val item = PlayerItem() - attachChild(item) - RoomScene.registerTouchArea(item) - - itemHeight = item.height - } + camY = -146f } @@ -54,15 +46,30 @@ class RoomPlayerList(val room: Room) : ScrollableList(), IScrollDetectorListener override fun onManagedUpdate(secondsElapsed: Float) { if (!isValid) { isValid = true - room.players.forEachIndexed { i, player -> - val item = getChild(i) as PlayerItem + for (i in childCount - 1 downTo 0) { + val child = getChild(i) + + RoomScene.unregisterTouchArea(child as ITouchArea) + detachChild(child) + } - item.room = room - item.player = player - item.isHost = player != null && player.id == room.host + for (i in 0 until room.maxPlayers) { + val item = PlayerItem() + attachChild(item) + RoomScene.registerTouchArea(item) - item.load() + itemHeight = item.height + + if (i < room.players.size) { + val player = room.players[i] + + item.room = room + item.player = player + item.isHost = player != null && player.id == room.host + + item.load() + } } } From 9f4e34024281d1d2ca30317d829a95b1ee9b2102 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:23:11 +0800 Subject: [PATCH 47/87] Make `Room.maxPlayers` mutable --- src/com/reco1l/ibancho/data/Room.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/ibancho/data/Room.kt b/src/com/reco1l/ibancho/data/Room.kt index 06be274ce..b7fb27ae6 100644 --- a/src/com/reco1l/ibancho/data/Room.kt +++ b/src/com/reco1l/ibancho/data/Room.kt @@ -22,7 +22,7 @@ data class Room( /** * The max amount of players. */ - val maxPlayers: Int, + var maxPlayers: Int, /** * The active player count. From 3dbb06fb2b69c28bce847ddca9942d9f5cafa2d6 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:41:22 +0800 Subject: [PATCH 48/87] Add room max player count setting event handler --- src/com/reco1l/ibancho/IRoomEventListener.kt | 5 +++++ src/com/reco1l/ibancho/RoomAPI.kt | 18 ++++++++++++++++++ src/com/reco1l/osu/multiplayer/RoomScene.kt | 8 ++++++++ src/com/reco1l/osu/ui/SettingsFragment.kt | 12 ++++++++++++ 4 files changed, 43 insertions(+) diff --git a/src/com/reco1l/ibancho/IRoomEventListener.kt b/src/com/reco1l/ibancho/IRoomEventListener.kt index aa8bee851..f305cd4eb 100644 --- a/src/com/reco1l/ibancho/IRoomEventListener.kt +++ b/src/com/reco1l/ibancho/IRoomEventListener.kt @@ -70,6 +70,11 @@ interface IRoomEventListener { */ fun onRoomNameChange(name: String) + /** + * Emit when the host changes the maximum amount of players allowed in the room. + */ + fun onRoomMaxPlayersChange(maxPlayers: Int) + /** * Emit when the host changes the beatmap. */ diff --git a/src/com/reco1l/ibancho/RoomAPI.kt b/src/com/reco1l/ibancho/RoomAPI.kt index 347e9ddb2..383c8e18b 100644 --- a/src/com/reco1l/ibancho/RoomAPI.kt +++ b/src/com/reco1l/ibancho/RoomAPI.kt @@ -117,6 +117,12 @@ object RoomAPI { roomEventListener?.onRoomNameChange(it[0] as String) } + private val maxPlayersChanged = Listener { + + Multiplayer.log("RECEIVED: maxPlayersChanged -> ${it.contentToString()}") + roomEventListener?.onRoomMaxPlayersChange((it[0] as String).toInt()) + } + private val playBeatmap = Listener { Multiplayer.log("RECEIVED: playBeatmap -> ${it.contentToString()}") @@ -180,6 +186,7 @@ object RoomAPI { on("winConditionChanged", winConditionChanged) on("teamChanged", teamChanged) on("roomNameChanged", roomNameChanged) + on("maxPlayersChanged", maxPlayersChanged) on("playBeatmap", playBeatmap) on("chatMessage", chatMessage) on("liveScoreData", liveScoreData) @@ -496,6 +503,17 @@ object RoomAPI { Multiplayer.log("EMITTED: roomNameChanged -> $name") } + /** + * Change room max players. + */ + fun setRoomMaxPlayers(maxPlayers: Int) { + socket?.emit("maxPlayersChanged", maxPlayers) ?: run { + Multiplayer.log("WARNING: Tried to emit event 'maxPlayersChanged' while socket is null.") + return + } + Multiplayer.log("EMITTED: maxPlayersChanged -> $maxPlayers") + } + /** * Change room password. */ diff --git a/src/com/reco1l/osu/multiplayer/RoomScene.kt b/src/com/reco1l/osu/multiplayer/RoomScene.kt index 7385d6540..ebb75a930 100644 --- a/src/com/reco1l/osu/multiplayer/RoomScene.kt +++ b/src/com/reco1l/osu/multiplayer/RoomScene.kt @@ -768,6 +768,14 @@ object RoomScene : Scene(), IRoomEventListener, IPlayerEventListener { updateInformation() } + override fun onRoomMaxPlayersChange(maxPlayers: Int) { + room!!.maxPlayers = maxPlayers + room!!.players = room!!.players.copyOf(maxPlayers) + + updateInformation() + playerList!!.invalidate() + } + override fun onRoomBeatmapChange(beatmap: RoomBeatmap?) { room!!.beatmap = beatmap diff --git a/src/com/reco1l/osu/ui/SettingsFragment.kt b/src/com/reco1l/osu/ui/SettingsFragment.kt index 861f352a1..65c4293de 100644 --- a/src/com/reco1l/osu/ui/SettingsFragment.kt +++ b/src/com/reco1l/osu/ui/SettingsFragment.kt @@ -21,6 +21,7 @@ import androidx.core.view.forEach import androidx.core.view.get import androidx.preference.CheckBoxPreference import androidx.preference.Preference +import androidx.preference.SeekBarPreference import com.osudroid.resources.R.* import com.edlplan.ui.fragment.LoadingFragment import com.google.android.material.snackbar.Snackbar @@ -53,6 +54,7 @@ import ru.nsu.ccfit.zuev.osu.online.OnlineManager import ru.nsu.ccfit.zuev.osuplus.R import ru.nsu.ccfit.zuev.skins.BeatmapSkinManager import java.io.File +import kotlin.math.max enum class Section(@XmlRes val xml: Int) { @@ -423,6 +425,16 @@ class SettingsFragment : com.edlplan.ui.fragment.SettingsFragment() { } } + findPreference("room_max_players")!!.apply { + min = max(2, Multiplayer.room!!.activePlayers.size) + value = Multiplayer.room!!.maxPlayers + + setOnPreferenceChangeListener { _, newValue -> + RoomAPI.setRoomMaxPlayers(newValue as Int) + true + } + } + findPreference("room_free_mods")!!.apply { isChecked = Multiplayer.room!!.gameplaySettings.isFreeMod From 70a26a2492fcc2e30e12f302a7091f32e57c09cd Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 13:41:44 +0800 Subject: [PATCH 49/87] Reword room max player count setting title --- res/values/strings_temporary_for_locals.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/res/values/strings_temporary_for_locals.xml b/res/values/strings_temporary_for_locals.xml index 2f7023a06..1d569eaa6 100644 --- a/res/values/strings_temporary_for_locals.xml +++ b/res/values/strings_temporary_for_locals.xml @@ -16,7 +16,7 @@ 1 - Max Players + Maximum Players Set the maximum number of players allowed in the room \ No newline at end of file From 0e176086bf0c631c02857ee59319110fd022cb51 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 14:29:45 +0800 Subject: [PATCH 50/87] Save sender username in room chat message --- src/com/reco1l/osu/multiplayer/RoomChat.kt | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/com/reco1l/osu/multiplayer/RoomChat.kt b/src/com/reco1l/osu/multiplayer/RoomChat.kt index de03c5524..7afa4f977 100644 --- a/src/com/reco1l/osu/multiplayer/RoomChat.kt +++ b/src/com/reco1l/osu/multiplayer/RoomChat.kt @@ -17,10 +17,8 @@ import android.widget.EditText import android.widget.LinearLayout import android.widget.TextView import android.widget.TextView.OnEditorActionListener -import androidx.core.view.isVisible import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.SimpleItemAnimator import com.edlplan.framework.easing.Easing import com.edlplan.ui.BaseAnimationListener import com.edlplan.ui.EasingHelper @@ -291,7 +289,11 @@ class RoomChat : BaseFragment(), OnEditorActionListener, OnKeyListener { } -data class Message(val sender: Long?, val text: String, val color: Int? = null) +data class Message(val senderUid: Long?, val text: String, val color: Int? = null) { + val senderUsername = + if (senderUid == null) "System" + else Multiplayer.room?.playersMap?.get(senderUid)?.name ?: "Unknown Player ($senderUid)" +} class MessageAdapter : RecyclerView.Adapter() { @@ -320,7 +322,7 @@ class MessageAdapter : RecyclerView.Adapter() { val msg = data[position] // The sender label will be shown if the previous message is not from the same sender - val showSender = msg.sender != null && (position == data.size - 1 || data[position + 1].sender != msg.sender) + val showSender = msg.senderUid != null && (position == data.size - 1 || data[position + 1].senderUid != msg.senderUid) val tintBackground = (data.size - position) % 2 == 0 holder.bind(msg, showSender, tintBackground) @@ -349,7 +351,7 @@ class MessageViewHolder(private val root: LinearLayout) : RecyclerView.ViewHolde senderText.visibility = if (showSender) View.VISIBLE else View.INVISIBLE // For system messages - if (msg.sender == null) { + if (msg.senderUid == null) { senderText.visibility = View.GONE messageText.text = msg.text @@ -362,10 +364,10 @@ class MessageViewHolder(private val root: LinearLayout) : RecyclerView.ViewHolde if (showSender) { - val isRoomHost = msg.sender == Multiplayer.room!!.host - val isDeveloper = msg.sender in DEVELOPERS + val isRoomHost = msg.senderUid == Multiplayer.room!!.host + val isDeveloper = msg.senderUid in DEVELOPERS - senderText.text = Multiplayer.room?.playersMap?.get(msg.sender)?.name ?: "Disconnected player" + senderText.text = msg.senderUsername val color = when { isRoomHost -> 0xFF00FFEA.toInt() From c0a2792b27736e4fbb5e7742a7a8efc2a1b5df41 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 15:01:38 +0800 Subject: [PATCH 51/87] Add timestamp to multiplayer chat messages --- res/layout/multiplayer_room_chat_item.xml | 12 ++++++++++++ src/com/reco1l/osu/multiplayer/RoomChat.kt | 15 ++++++++++++--- 2 files changed, 24 insertions(+), 3 deletions(-) diff --git a/res/layout/multiplayer_room_chat_item.xml b/res/layout/multiplayer_room_chat_item.xml index 06d72f87b..d25eb0590 100644 --- a/res/layout/multiplayer_room_chat_item.xml +++ b/res/layout/multiplayer_room_chat_item.xml @@ -7,6 +7,18 @@ android:gravity="right" android:paddingHorizontal="6dp"> + + () { class MessageViewHolder(private val root: LinearLayout) : RecyclerView.ViewHolder(root) { + private lateinit var timestampText: TextView + private lateinit var senderText: TextView private lateinit var messageText: TextView fun bind(msg: Message, showSender: Boolean, tintBackground: Boolean) { - + timestampText = root.findViewById(R.id.timestamp_text)!! senderText = root.findViewById(R.id.sender_text)!! messageText = root.findViewById(R.id.message_text)!! root.backgroundColor = if (tintBackground) 0xFF1A1A25.toInt() else Color.TRANSPARENT + timestampText.text = timestampFormatter.format(msg.timestamp) + + messageText.verticalPadding = 0 messageText.gravity = Gravity.LEFT messageText.fontColor = msg.color ?: Color.WHITE @@ -360,8 +368,6 @@ class MessageViewHolder(private val root: LinearLayout) : RecyclerView.ViewHolde return } - messageText.verticalPadding = 0.dp - if (showSender) { val isRoomHost = msg.senderUid == Multiplayer.room!!.host @@ -390,4 +396,7 @@ class MessageViewHolder(private val root: LinearLayout) : RecyclerView.ViewHolde messageText.text = msg.text } + companion object { + private val timestampFormatter = SimpleDateFormat("HH:mm:ss") + } } From be86a561a07ec38c00a17ff7f3103a5297f9b700 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 16:45:12 +0800 Subject: [PATCH 52/87] Bump multiplayer API version --- src/com/reco1l/ibancho/RoomAPI.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/ibancho/RoomAPI.kt b/src/com/reco1l/ibancho/RoomAPI.kt index 383c8e18b..1e4967015 100644 --- a/src/com/reco1l/ibancho/RoomAPI.kt +++ b/src/com/reco1l/ibancho/RoomAPI.kt @@ -15,7 +15,7 @@ object RoomAPI { /** * The API version. */ - private const val API_VERSION = 7 + private const val API_VERSION = 8 /** From 8b185fe24a554ff0694be03dfabc2e75cad6154f Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sun, 12 Jan 2025 17:22:44 +0800 Subject: [PATCH 53/87] Send current multiplayer API version when creating a multiplayer room --- src/com/reco1l/ibancho/LobbyAPI.kt | 1 + src/com/reco1l/ibancho/RoomAPI.kt | 2 +- src/com/reco1l/osu/multiplayer/LobbyCreateRoom.kt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/ibancho/LobbyAPI.kt b/src/com/reco1l/ibancho/LobbyAPI.kt index 7ca91367a..8c639c7bd 100644 --- a/src/com/reco1l/ibancho/LobbyAPI.kt +++ b/src/com/reco1l/ibancho/LobbyAPI.kt @@ -115,6 +115,7 @@ object LobbyAPI { put("password", password) } + put("version", RoomAPI.API_VERSION) put("sign", sign) } diff --git a/src/com/reco1l/ibancho/RoomAPI.kt b/src/com/reco1l/ibancho/RoomAPI.kt index 1e4967015..7d20c97ab 100644 --- a/src/com/reco1l/ibancho/RoomAPI.kt +++ b/src/com/reco1l/ibancho/RoomAPI.kt @@ -15,7 +15,7 @@ object RoomAPI { /** * The API version. */ - private const val API_VERSION = 8 + const val API_VERSION = 8 /** diff --git a/src/com/reco1l/osu/multiplayer/LobbyCreateRoom.kt b/src/com/reco1l/osu/multiplayer/LobbyCreateRoom.kt index 4ec027113..f37ff3a35 100644 --- a/src/com/reco1l/osu/multiplayer/LobbyCreateRoom.kt +++ b/src/com/reco1l/osu/multiplayer/LobbyCreateRoom.kt @@ -94,6 +94,7 @@ class LobbyCreateRoom : BaseFragment() { if (password != null) { signStr += "_${password}" } + signStr += "_${RoomAPI.API_VERSION}" try { From dc3698a1bae2153777ab9859746ce9613e18e03b Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 01:43:57 +0800 Subject: [PATCH 54/87] Revert unranked sprite persistence logic to semi-old behavior Having it persist in the center throughout gameplay is an anti-design. However, I remember that the sprite was changed to persist permanently as many players did not realize that their play was unranked. To find a good middle ground, I allowed the sprite to exist until the first object appears, with a cap of 1.5 seconds. In addition to that, the sprite expands as it disappears, giving more signal to the player. --- src/ru/nsu/ccfit/zuev/osu/game/GameScene.java | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java b/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java index 7180f54ba..1f71f7c91 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java @@ -174,6 +174,7 @@ public class GameScene implements IUpdateHandler, GameObjectListener, private SliderPath[] sliderPaths = null; private LinePath[] sliderRenderPaths = null; private int sliderIndex = 0; + private ExtendedSprite unrankedSprite; private StoryboardSprite storyboardSprite; @@ -942,11 +943,6 @@ protected void onManagedDraw(GL10 pGL, Camera pCamera) { timeOffset += 0.25f; } - Sprite unranked = new Sprite(0, 0, ResourceManager.getInstance().getTexture("play-unranked")); - unranked.setPosition((float) Config.getRES_WIDTH() / 2 - unranked.getWidth() / 2, 80); - unranked.setVisible(false); - fgScene.attachChild(unranked); - boolean hasUnrankedMod = SmartIterator.wrap(stat.getMod().iterator()) .applyFilter(m -> m.unranked).hasNext(); if (hasUnrankedMod @@ -956,7 +952,10 @@ protected void onManagedDraw(GL10 pGL, Camera pCamera) { || ModMenu.getInstance().isCustomCS() || ModMenu.getInstance().isCustomHP() || !ModMenu.getInstance().isDefaultFLFollowDelay()) { - unranked.setVisible(true); + unrankedSprite = new ExtendedSprite(ResourceManager.getInstance().getTexture("play-unranked")); + unrankedSprite.setPosition(Config.getRES_WIDTH() / 2f, 80); + unrankedSprite.setOrigin(Anchor.Center); + fgScene.attachChild(unrankedSprite); } String playname = Config.getOnlineUsername(); @@ -1321,6 +1320,7 @@ public void onUpdate(final float pSecondsElapsed) { totalLength = GlobalManager.getInstance().getSongService().getLength(); musicStarted = true; elapsedTime = totalOffset; + return; } @@ -1331,6 +1331,21 @@ public void onUpdate(final float pSecondsElapsed) { gameStarted = true; final var obj = objects.poll(); + if (unrankedSprite != null) { + unrankedSprite.registerEntityModifier( + Modifiers.sequence(IEntity::detachSelf, + Modifiers.delay(1.5f - elapsedTime), + Modifiers.parallel( + Modifiers.scale(0.5f, 1, 1.5f), + Modifiers.fadeOut(0.5f) + ) + ) + ); + + // Make it null to avoid multiple entity modifier registration + unrankedSprite = null; + } + if (obj.startTime > totalLength) { shouldBePunished = true; break; From e4329bdae755a9f80d801e7db47464d8a6c7a324 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 01:47:05 +0800 Subject: [PATCH 55/87] Use anchor instead of manually adjusting position --- src/ru/nsu/ccfit/zuev/osu/game/GameScene.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java b/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java index 1f71f7c91..a0ad6f268 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameScene.java @@ -953,8 +953,9 @@ protected void onManagedDraw(GL10 pGL, Camera pCamera) { || ModMenu.getInstance().isCustomHP() || !ModMenu.getInstance().isDefaultFLFollowDelay()) { unrankedSprite = new ExtendedSprite(ResourceManager.getInstance().getTexture("play-unranked")); - unrankedSprite.setPosition(Config.getRES_WIDTH() / 2f, 80); + unrankedSprite.setAnchor(Anchor.TopCenter); unrankedSprite.setOrigin(Anchor.Center); + unrankedSprite.setPosition(0, 80); fgScene.attachChild(unrankedSprite); } From c43ec878b749a3ae939e7dcd717c959c7c4fbdc1 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:37:26 +0800 Subject: [PATCH 56/87] Restrict unnecessary `SliderHitObject` properties to its inheritors --- src/com/rian/osu/beatmap/hitobject/Slider.kt | 13 +++---------- .../beatmap/hitobject/sliderobject/SliderHead.kt | 5 +++-- .../hitobject/sliderobject/SliderHitObject.kt | 14 +------------- .../beatmap/hitobject/sliderobject/SliderRepeat.kt | 10 +++------- .../beatmap/hitobject/sliderobject/SliderTail.kt | 14 ++------------ .../beatmap/hitobject/sliderobject/SliderTick.kt | 6 +++--- 6 files changed, 15 insertions(+), 47 deletions(-) diff --git a/src/com/rian/osu/beatmap/hitobject/Slider.kt b/src/com/rian/osu/beatmap/hitobject/Slider.kt index c0bc2650e..d1a82bff0 100644 --- a/src/com/rian/osu/beatmap/hitobject/Slider.kt +++ b/src/com/rian/osu/beatmap/hitobject/Slider.kt @@ -423,20 +423,13 @@ class Slider( if (span < spanCount - 1) { val repeatPosition = position + path.positionAt(((span + 1) % 2).toDouble()) - nestedHitObjects.add( - SliderRepeat( - spanStartTime + spanDuration, - repeatPosition, - span, - spanStartTime - ) - ) + nestedHitObjects.add(SliderRepeat(spanStartTime + spanDuration, repeatPosition, span)) } } } tail = when (mode) { - GameMode.Droid -> SliderTail(endTime, endPosition, repeatCount, startTime + repeatCount * spanDuration) + GameMode.Droid -> SliderTail(endTime, endPosition) GameMode.Standard -> { // Okay, I'll level with you. I made a mistake. It was 2007. @@ -453,7 +446,7 @@ class Slider( finalSpanStartTime + spanDuration - LEGACY_LAST_TICK_OFFSET ) - SliderTail(finalSpanEndTime, endPosition, finalSpanIndex, finalSpanStartTime) + SliderTail(finalSpanEndTime, endPosition) } } diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHead.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHead.kt index 603e8f368..4cf0d1801 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHead.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHead.kt @@ -1,9 +1,10 @@ package com.rian.osu.beatmap.hitobject.sliderobject +import com.rian.osu.beatmap.hitobject.Slider import com.rian.osu.math.Vector2 /** - * Represents the head of a slider. + * Represents the head of a [Slider]. */ class SliderHead( /** @@ -15,4 +16,4 @@ class SliderHead( * The position of this [SliderHead] relative to the play field. */ position: Vector2 -) : SliderHitObject(startTime, position, 0, startTime) +) : SliderHitObject(startTime, position) diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHitObject.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHitObject.kt index 83723c8b4..da2788b8d 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHitObject.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderHitObject.kt @@ -16,17 +16,5 @@ abstract class SliderHitObject( /** * The position of this [SliderHitObject] relative to the play field. */ - position: Vector2, - - /** - * The index of the span at which this [SliderHitObject] lies. - */ - @JvmField - val spanIndex: Int, - - /** - * The start time of the span at which this [SliderHitObject] lies, in milliseconds. - */ - @JvmField - val spanStartTime: Double + position: Vector2 ) : HitObject(startTime, position, false, 0) diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt index 7fe74f818..c4d884e5a 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt @@ -19,10 +19,6 @@ class SliderRepeat( /** * The index of the span at which this [SliderRepeat] lies. */ - spanIndex: Int, - - /** - * The start time of the span at which this [SliderRepeat] lies, in milliseconds. - */ - spanStartTime: Double -) : SliderHitObject(startTime, position, spanIndex, spanStartTime) + @JvmField + val spanIndex: Int +) : SliderHitObject(startTime, position) diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt index 5e0cd9661..d39e3599f 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt @@ -14,15 +14,5 @@ class SliderTail( /** * The position of this [SliderTail] relative to the play field. */ - position: Vector2, - - /** - * The index of the span at which this [SliderTail] lies. - */ - spanIndex: Int, - - /** - * The start time of the span at which this [SliderTail] lies, in milliseconds. - */ - spanStartTime: Double -) : SliderHitObject(startTime, position, spanIndex, spanStartTime) + position: Vector2 +) : SliderHitObject(startTime, position) diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt index 7a377f055..9e8e27fb4 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt @@ -19,10 +19,10 @@ class SliderTick( /** * The index of the span at which this [SliderTick] lies. */ - spanIndex: Int, + private val spanIndex: Int, /** * The start time of the span at which this [SliderTick] lies, in milliseconds. */ - spanStartTime: Double -) : SliderHitObject(startTime, position, spanIndex, spanStartTime) + private val spanStartTime: Double +) : SliderHitObject(startTime, position) From 10edb58ab04f4687e40a689e88a24059d357428e Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:43:00 +0800 Subject: [PATCH 57/87] Add `SliderTick` specific `timePreempt` --- .../hitobject/sliderobject/SliderTick.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt index 9e8e27fb4..2c9c42524 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTick.kt @@ -1,6 +1,10 @@ package com.rian.osu.beatmap.hitobject.sliderobject +import com.rian.osu.GameMode +import com.rian.osu.beatmap.sections.BeatmapControlPoints +import com.rian.osu.beatmap.sections.BeatmapDifficulty import com.rian.osu.math.Vector2 +import kotlinx.coroutines.CoroutineScope /** * Represents a slider tick. @@ -25,4 +29,19 @@ class SliderTick( * The start time of the span at which this [SliderTick] lies, in milliseconds. */ private val spanStartTime: Double -) : SliderHitObject(startTime, position) +) : SliderHitObject(startTime, position) { + override fun applyDefaults( + controlPoints: BeatmapControlPoints, + difficulty: BeatmapDifficulty, + mode: GameMode, + scope: CoroutineScope? + ) { + super.applyDefaults(controlPoints, difficulty, mode, scope) + + // Adding 200 to include the offset stable used. + // This is so on repeats ticks don't appear too late to be visually processed by the player. + val offset = if (spanIndex > 0) 200.0 else timePreempt * 0.66 + + timePreempt = (startTime - spanStartTime) / 2 + offset + } +} From f062d2a651b41d5c531d786e3fde8d34efc79562 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 22:05:02 +0800 Subject: [PATCH 58/87] Add `SliderRepeat` and `SliderTail` specific `timeFadeIn` and `timePreempt` --- src/com/rian/osu/beatmap/hitobject/Slider.kt | 8 ++- .../hitobject/sliderobject/SliderEndCircle.kt | 50 +++++++++++++++++++ .../hitobject/sliderobject/SliderRepeat.kt | 16 ++---- .../hitobject/sliderobject/SliderTail.kt | 14 +++--- 4 files changed, 66 insertions(+), 22 deletions(-) create mode 100644 src/com/rian/osu/beatmap/hitobject/sliderobject/SliderEndCircle.kt diff --git a/src/com/rian/osu/beatmap/hitobject/Slider.kt b/src/com/rian/osu/beatmap/hitobject/Slider.kt index d1a82bff0..e2abe1d31 100644 --- a/src/com/rian/osu/beatmap/hitobject/Slider.kt +++ b/src/com/rian/osu/beatmap/hitobject/Slider.kt @@ -421,15 +421,13 @@ class Slider( nestedHitObjects.addAll(sliderTicks) if (span < spanCount - 1) { - val repeatPosition = position + path.positionAt(((span + 1) % 2).toDouble()) - - nestedHitObjects.add(SliderRepeat(spanStartTime + spanDuration, repeatPosition, span)) + nestedHitObjects.add(SliderRepeat(this, span)) } } } tail = when (mode) { - GameMode.Droid -> SliderTail(endTime, endPosition) + GameMode.Droid -> SliderTail(this) GameMode.Standard -> { // Okay, I'll level with you. I made a mistake. It was 2007. @@ -446,7 +444,7 @@ class Slider( finalSpanStartTime + spanDuration - LEGACY_LAST_TICK_OFFSET ) - SliderTail(finalSpanEndTime, endPosition) + SliderTail(this, finalSpanEndTime) } } diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderEndCircle.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderEndCircle.kt new file mode 100644 index 000000000..d63c840ee --- /dev/null +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderEndCircle.kt @@ -0,0 +1,50 @@ +package com.rian.osu.beatmap.hitobject.sliderobject + +import com.rian.osu.GameMode +import com.rian.osu.beatmap.hitobject.Slider +import com.rian.osu.beatmap.sections.BeatmapControlPoints +import com.rian.osu.beatmap.sections.BeatmapDifficulty +import kotlinx.coroutines.CoroutineScope + +/** + * Represents a [SliderHitObject] that is at the end of a slider path. + */ +abstract class SliderEndCircle( + /** + * The [Slider] to which [SliderEndCircle] belongs to. + */ + protected val slider: Slider, + + /** + * The index of the span at which this [SliderEndCircle] lies. + */ + val spanIndex: Int, + + /** + * An optional start time to override this [SliderEndCircle]'s [startTime]. + */ + startTime: Double = slider.startTime + slider.spanDuration * (spanIndex + 1) +) : SliderHitObject( + startTime, + if (spanIndex % 2 == 0) slider.endPosition else slider.position +) { + override fun applyDefaults( + controlPoints: BeatmapControlPoints, + difficulty: BeatmapDifficulty, + mode: GameMode, + scope: CoroutineScope? + ) { + super.applyDefaults(controlPoints, difficulty, mode, scope) + + if (spanIndex > 0) { + // Repeat points after the first span should appear behind the still-visible one. + timeFadeIn = 0.0 + + // The next end circle should appear exactly after the previous circle (on the same end) is hit. + timePreempt = slider.spanDuration * 2 + } else { + // The first end circle should fade in with the slider. + timePreempt += startTime - slider.startTime + } + } +} \ No newline at end of file diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt index c4d884e5a..c20b2b7fb 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderRepeat.kt @@ -1,24 +1,18 @@ package com.rian.osu.beatmap.hitobject.sliderobject -import com.rian.osu.math.Vector2 +import com.rian.osu.beatmap.hitobject.Slider /** * Represents a slider repeat. */ class SliderRepeat( /** - * The time at which this [SliderRepeat] starts, in milliseconds. + * The slider to which this [SliderRepeat] belongs. */ - startTime: Double, - - /** - * The position of this [SliderRepeat] relative to the play field. - */ - position: Vector2, + slider: Slider, /** * The index of the span at which this [SliderRepeat] lies. */ - @JvmField - val spanIndex: Int -) : SliderHitObject(startTime, position) + spanIndex: Int +) : SliderEndCircle(slider, spanIndex) diff --git a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt index d39e3599f..338714a1d 100644 --- a/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt +++ b/src/com/rian/osu/beatmap/hitobject/sliderobject/SliderTail.kt @@ -1,18 +1,20 @@ package com.rian.osu.beatmap.hitobject.sliderobject -import com.rian.osu.math.Vector2 +import com.rian.osu.beatmap.hitobject.Slider /** * Represents a slider tail. */ class SliderTail( /** - * The time at which this [SliderTail] starts, in milliseconds. + * The [Slider] to which this [SliderTail] belongs. */ - startTime: Double, + slider: Slider, /** - * The position of this [SliderTail] relative to the play field. + * An optional start time for this [SliderTail] to override this [SliderTail]'s [startTime]. + * + * Used for osu!standard's legacy slider tail. */ - position: Vector2 -) : SliderHitObject(startTime, position) + startTime: Double = slider.endTime +) : SliderEndCircle(slider, slider.repeatCount, startTime) From 705454bbc80e220097c439825b1cb6b1942cbfa4 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:26:01 +0800 Subject: [PATCH 59/87] Add slider tick fade in animation --- src/com/reco1l/osu/hitobjects/SliderTicks.kt | 59 ++++++++++++++++++- .../ccfit/zuev/osu/game/GameplaySlider.java | 23 ++------ 2 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/com/reco1l/osu/hitobjects/SliderTicks.kt b/src/com/reco1l/osu/hitobjects/SliderTicks.kt index 1b852a846..82bfa95b4 100644 --- a/src/com/reco1l/osu/hitobjects/SliderTicks.kt +++ b/src/com/reco1l/osu/hitobjects/SliderTicks.kt @@ -1,5 +1,7 @@ package com.reco1l.osu.hitobjects +import com.edlplan.framework.easing.Easing +import com.reco1l.andengine.Modifiers import com.reco1l.andengine.container.* import com.reco1l.andengine.sprite.* import com.reco1l.framework.* @@ -8,8 +10,11 @@ import com.rian.osu.beatmap.hitobject.sliderobject.* import ru.nsu.ccfit.zuev.osu.* class SliderTickContainer : Container() { + private var slider: Slider? = null + private val animDuration = 0.15f - fun init(beatmapSlider: Slider) { + fun init(currentTimeSec: Double, beatmapSlider: Slider) { + slider = beatmapSlider detachChildren() @@ -21,13 +26,60 @@ class SliderTickContainer : Container() { val tickPosition = tick.gameplayStackedPosition val sprite = SliderTickSprite.pool.obtain() - sprite.alpha = 0f - // We're substracting the position of the slider because the tick container is + // We're subtracting the position of the slider because the tick container is // already at the position of the slider since it's a child of the slider's body. sprite.setPosition(tickPosition.x - position.x, tickPosition.y - position.y) attachChild(sprite) + applyTickAnimation(currentTimeSec, tick, sprite) + } + } + + fun onNewSpan(currentTimeSec: Double, newSpanIndex: Int) { + if (slider == null) { + return + } + + val spanStartIndex = + // Amount of slider ticks passed. + newSpanIndex * childCount + + // Amount of slider repeats passed. + newSpanIndex + + // The slider head. + 1 + + for (i in spanStartIndex until spanStartIndex + childCount) { + val tick = slider!!.nestedHitObjects[i] as? SliderTick ?: break + + // For reverse sliders, the ticks are in the opposite order. + val sprite = getChild( + if (newSpanIndex % 2 != 0) childCount - (i - spanStartIndex) - 1 else i - spanStartIndex + ) as? SliderTickSprite ?: break + + applyTickAnimation(currentTimeSec, tick, sprite) + } + } + + private fun applyTickAnimation(currentTimeSec: Double, tick: SliderTick, sprite: SliderTickSprite) { + val fadeInStartTime = (tick.startTime - tick.timePreempt) / 1000 + + sprite.apply { + clearEntityModifiers() + + alpha = 0f + setScale(0.5f) + + registerEntityModifier( + Modifiers.sequence(null, + // Delay up to fadeInStartTime + Modifiers.delay((fadeInStartTime - currentTimeSec).toFloat()), + Modifiers.parallel(null, + Modifiers.scale(animDuration * 4, 0.5f, 1f, easing = Easing.OutElasticHalf), + Modifiers.fadeIn(animDuration) + ) + ) + ) } } @@ -49,6 +101,7 @@ class SliderTickSprite : ExtendedSprite() { override fun onDetached() { super.onDetached() + clearEntityModifiers() pool.free(this) } diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java index 0dcd69ee1..adf2fa39f 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java @@ -314,7 +314,7 @@ public void init(final GameObjectListener listener, final Scene scene, sliderBody.setHintVisible(false); } - tickContainer.init(beatmapSlider); + tickContainer.init(secPassed, beatmapSlider); sliderBody.attachChild(tickContainer); scene.attachChild(sliderBody, 0); @@ -569,10 +569,7 @@ private void onSpanFinish() { } // Restore ticks - for (int i = 0, size = tickContainer.getChildCount(); i < size; i++) { - tickContainer.getChild(i).setAlpha(1f); - } - + tickContainer.onNewSpan(getGameplayPassedTimeMilliseconds() / 1000, completedSpanCount); currentTickSpriteIndex = reverse ? tickContainer.getChildCount() - 1 : 0; // Setting visibility of repeat arrows @@ -779,12 +776,6 @@ public void update(final float dt) { // Following core doing a very cute show animation ^_^" percentage = Math.min(1, percentage * 2); - for (int i = 0, size = tickContainer.getChildCount(); i < size; i++) { - if (percentage > (float) (i + 1) / size) { - tickContainer.getChild(i).setAlpha(1f); - } - } - if (beatmapSlider.getSpanCount() > 1) { endArrow.setAlpha(percentage); } @@ -802,11 +793,6 @@ public void update(final float dt) { endArrow.setPosition(position.x, position.y); } } else if (percentage - dt / timePreempt <= 0.5f) { - - for (int i = 0, size = tickContainer.getChildCount(); i < size; i++) { - tickContainer.getChild(i).setAlpha(1f); - } - if (beatmapSlider.getSpanCount() > 1) { endArrow.setAlpha(1); } @@ -1115,7 +1101,10 @@ private void judgeSliderTicks() { listener.onSliderHit(id, isTracking ? 10 : -1, tmpPoint, false, bodyColor, GameObjectListener.SLIDER_TICK, isTracking); currentNestedObjectIndex++; - tickContainer.getChild(currentTickSpriteIndex).setAlpha(0); + var tickSprite = tickContainer.getChild(currentTickSpriteIndex); + tickSprite.clearEntityModifiers(); + tickSprite.setAlpha(0); + if (reverse && currentTickSpriteIndex > 0) { currentTickSpriteIndex--; } else if (!reverse && currentTickSpriteIndex < tickContainer.getChildCount() - 1) { From 097337e54057ac90f2eeae2cc679e587189c2c0d Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:32:49 +0800 Subject: [PATCH 60/87] Add slider tick hit animation --- src/com/reco1l/osu/hitobjects/SliderTicks.kt | 12 ++++++++++++ src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java | 6 +++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/com/reco1l/osu/hitobjects/SliderTicks.kt b/src/com/reco1l/osu/hitobjects/SliderTicks.kt index 82bfa95b4..607bef85e 100644 --- a/src/com/reco1l/osu/hitobjects/SliderTicks.kt +++ b/src/com/reco1l/osu/hitobjects/SliderTicks.kt @@ -61,6 +61,18 @@ class SliderTickContainer : Container() { } } + fun onHit(sprite: SliderTickSprite, isHit: Boolean) { + sprite.apply { + clearEntityModifiers() + + registerEntityModifier(Modifiers.fadeOut(animDuration, easing = Easing.OutQuint)) + + if (isHit) { + registerEntityModifier(Modifiers.scale(animDuration, 1.5f, 1f, easing = Easing.Out)) + } + } + } + private fun applyTickAnimation(currentTimeSec: Double, tick: SliderTick, sprite: SliderTickSprite) { val fadeInStartTime = (tick.startTime - tick.timePreempt) / 1000 diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java index adf2fa39f..88193cee1 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java @@ -11,6 +11,7 @@ import com.reco1l.andengine.sprite.ExtendedSprite; import com.reco1l.andengine.Modifiers; import com.reco1l.andengine.Anchor; +import com.reco1l.osu.hitobjects.SliderTickSprite; import com.reco1l.osu.playfield.CirclePiece; import com.reco1l.osu.playfield.NumberedCirclePiece; import com.reco1l.osu.hitobjects.SliderTickContainer; @@ -1101,9 +1102,8 @@ private void judgeSliderTicks() { listener.onSliderHit(id, isTracking ? 10 : -1, tmpPoint, false, bodyColor, GameObjectListener.SLIDER_TICK, isTracking); currentNestedObjectIndex++; - var tickSprite = tickContainer.getChild(currentTickSpriteIndex); - tickSprite.clearEntityModifiers(); - tickSprite.setAlpha(0); + var tickSprite = (SliderTickSprite) tickContainer.getChild(currentTickSpriteIndex); + tickContainer.onHit(tickSprite, isTracking); if (reverse && currentTickSpriteIndex > 0) { currentTickSpriteIndex--; From 1c1be365ab9db4bb7795f389e844641ddfee0741 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:39:40 +0800 Subject: [PATCH 61/87] Fix slider ticks potentially being judged incorrectly in replay Caused by slider tracking state not updated after `replayTickIndex` was modified. --- src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java index 88193cee1..7f29295e1 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java @@ -1073,7 +1073,6 @@ private void judgeSliderTicks() { } float scale = beatmapSlider.getGameplayScale(); - boolean isTracking = isTracking(); var nestedObjects = beatmapSlider.getNestedHitObjects(); var nestedObjectToJudge = nestedObjects.get(currentNestedObjectIndex); @@ -1083,6 +1082,8 @@ private void judgeSliderTicks() { float followCircleExpandDuration = Math.min((float) spanDuration / (tickContainer.getChildCount() + 1), 0.2f); while (nestedObjectToJudge instanceof SliderTick && currentTime >= nestedObjectToJudge.startTime) { + boolean isTracking = isTracking(); + if (isTracking) { if (Config.isAnimateFollowCircle() && !isFollowCircleAnimating) { followCircle.clearEntityModifiers(); From 5de877dedbbf1d036e33257609d1a94244953da8 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:56:36 +0800 Subject: [PATCH 62/87] Move tick animation logic directly to `SliderTickSprite` rather than `SliderTickContainer` --- src/com/reco1l/osu/hitobjects/SliderTicks.kt | 80 ++++++++++--------- .../ccfit/zuev/osu/game/GameplaySlider.java | 2 +- 2 files changed, 44 insertions(+), 38 deletions(-) diff --git a/src/com/reco1l/osu/hitobjects/SliderTicks.kt b/src/com/reco1l/osu/hitobjects/SliderTicks.kt index 607bef85e..c6ea5382f 100644 --- a/src/com/reco1l/osu/hitobjects/SliderTicks.kt +++ b/src/com/reco1l/osu/hitobjects/SliderTicks.kt @@ -1,6 +1,7 @@ package com.reco1l.osu.hitobjects import com.edlplan.framework.easing.Easing +import com.reco1l.andengine.Anchor import com.reco1l.andengine.Modifiers import com.reco1l.andengine.container.* import com.reco1l.andengine.sprite.* @@ -11,7 +12,6 @@ import ru.nsu.ccfit.zuev.osu.* class SliderTickContainer : Container() { private var slider: Slider? = null - private val animDuration = 0.15f fun init(currentTimeSec: Double, beatmapSlider: Slider) { slider = beatmapSlider @@ -30,9 +30,9 @@ class SliderTickContainer : Container() { // We're subtracting the position of the slider because the tick container is // already at the position of the slider since it's a child of the slider's body. sprite.setPosition(tickPosition.x - position.x, tickPosition.y - position.y) + sprite.init(currentTimeSec, tick) attachChild(sprite) - applyTickAnimation(currentTimeSec, tick, sprite) } } @@ -57,58 +57,63 @@ class SliderTickContainer : Container() { if (newSpanIndex % 2 != 0) childCount - (i - spanStartIndex) - 1 else i - spanStartIndex ) as? SliderTickSprite ?: break - applyTickAnimation(currentTimeSec, tick, sprite) + sprite.init(currentTimeSec, tick) } } - fun onHit(sprite: SliderTickSprite, isHit: Boolean) { - sprite.apply { - clearEntityModifiers() + override fun onDetached() { + super.onDetached() + detachChildren() + } - registerEntityModifier(Modifiers.fadeOut(animDuration, easing = Easing.OutQuint)) +} - if (isHit) { - registerEntityModifier(Modifiers.scale(animDuration, 1.5f, 1f, easing = Easing.Out)) - } - } + +class SliderTickSprite : ExtendedSprite() { + + init { + textureRegion = ResourceManager.getInstance().getTexture("sliderscorepoint") + setOrigin(Anchor.Center) } - private fun applyTickAnimation(currentTimeSec: Double, tick: SliderTick, sprite: SliderTickSprite) { + /** + * Initializes this [SliderTickSprite] with the given [SliderTick]. + * + * @param currentTimeSec The current time in seconds. + * @param tick The [SliderTick] represented by this [SliderTickSprite]. + */ + fun init(currentTimeSec: Double, tick: SliderTick) { val fadeInStartTime = (tick.startTime - tick.timePreempt) / 1000 - sprite.apply { - clearEntityModifiers() + clearEntityModifiers() - alpha = 0f - setScale(0.5f) + alpha = 0f + setScale(0.5f) - registerEntityModifier( - Modifiers.sequence(null, - // Delay up to fadeInStartTime - Modifiers.delay((fadeInStartTime - currentTimeSec).toFloat()), - Modifiers.parallel(null, - Modifiers.scale(animDuration * 4, 0.5f, 1f, easing = Easing.OutElasticHalf), - Modifiers.fadeIn(animDuration) - ) + registerEntityModifier( + Modifiers.sequence(null, + Modifiers.delay((fadeInStartTime - currentTimeSec).toFloat()), + Modifiers.parallel(null, + Modifiers.scale(ANIM_DURATION * 4, 0.5f, 1f, easing = Easing.OutElasticHalf), + Modifiers.fadeIn(ANIM_DURATION) ) ) - } + ) } - override fun onDetached() { - super.onDetached() - detachChildren() - } - -} - + /** + * Called when the [SliderTick] that this [SliderTickSprite] represents is hit. + * + * @param isSuccessful Whether the hit resulted in a successful hit. + */ + fun onHit(isSuccessful: Boolean) { + clearEntityModifiers() -class SliderTickSprite : ExtendedSprite() { + registerEntityModifier(Modifiers.fadeOut(ANIM_DURATION, easing = Easing.OutQuint)) - init { - textureRegion = ResourceManager.getInstance().getTexture("sliderscorepoint") - originX = 0.5f - originY = 0.5f + if (isSuccessful) { + registerEntityModifier(Modifiers.scale(ANIM_DURATION, 1.5f, 1f, easing = Easing.Out)) + } } override fun onDetached() { @@ -118,6 +123,7 @@ class SliderTickSprite : ExtendedSprite() { } companion object { + private const val ANIM_DURATION = 0.15f @JvmStatic val pool = Pool { SliderTickSprite() } diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java index 7f29295e1..0374e4f81 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java @@ -1104,7 +1104,7 @@ private void judgeSliderTicks() { currentNestedObjectIndex++; var tickSprite = (SliderTickSprite) tickContainer.getChild(currentTickSpriteIndex); - tickContainer.onHit(tickSprite, isTracking); + tickSprite.onHit(isTracking); if (reverse && currentTickSpriteIndex > 0) { currentTickSpriteIndex--; From 1d02a0d6d0b030cf755c7d3b1a9063f308fee8bf Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Tue, 21 Jan 2025 23:58:45 +0800 Subject: [PATCH 63/87] Fix wrong initial scale when a slider tick is hit --- src/com/reco1l/osu/hitobjects/SliderTicks.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/osu/hitobjects/SliderTicks.kt b/src/com/reco1l/osu/hitobjects/SliderTicks.kt index c6ea5382f..4e4847a96 100644 --- a/src/com/reco1l/osu/hitobjects/SliderTicks.kt +++ b/src/com/reco1l/osu/hitobjects/SliderTicks.kt @@ -112,7 +112,7 @@ class SliderTickSprite : ExtendedSprite() { registerEntityModifier(Modifiers.fadeOut(ANIM_DURATION, easing = Easing.OutQuint)) if (isSuccessful) { - registerEntityModifier(Modifiers.scale(ANIM_DURATION, 1.5f, 1f, easing = Easing.Out)) + registerEntityModifier(Modifiers.scale(ANIM_DURATION, 1f, 1.5f, easing = Easing.Out)) } } From 03c71beafa0ec0ea26d0b076a13dfe8e0b7437e9 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Thu, 23 Jan 2025 00:29:50 +0800 Subject: [PATCH 64/87] Do not rebuild slider vertices when lengths are the same --- src/com/edlplan/osu/support/slider/SliderBody.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/com/edlplan/osu/support/slider/SliderBody.java b/src/com/edlplan/osu/support/slider/SliderBody.java index f331733df..f1a05ca5a 100644 --- a/src/com/edlplan/osu/support/slider/SliderBody.java +++ b/src/com/edlplan/osu/support/slider/SliderBody.java @@ -159,11 +159,19 @@ protected void onManagedUpdate(float pSecondsElapsed) { public void setStartLength(float length) { + if (length == startLength) { + return; + } + startLength = length; shouldRebuildVertices = true; } public void setEndLength(float length) { + if (length == endLength) { + return; + } + endLength = length; shouldRebuildVertices = true; } From eea96e3e182bf2f0a643344d4384a086f5427ebc Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Thu, 23 Jan 2025 01:42:29 +0800 Subject: [PATCH 65/87] Match slider snaking in speed and slider tail circle fade animation with osu!stable --- .../ccfit/zuev/osu/game/GameplaySlider.java | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java index 0374e4f81..51726ce01 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java @@ -250,8 +250,12 @@ public void init(final GameObjectListener listener, final Scene scene, timePreempt = (float) beatmapSlider.timePreempt / 1000; float fadeInDuration = (float) beatmapSlider.timeFadeIn / 1000; + // When snaking in is enabled, the first repeat or tail needs to be delayed until the snaking completes. + float fadeInDelay = Config.isSnakingInSliders() ? timePreempt / 3 : 0; + if (GameHelper.isHidden()) { float fadeOutDuration = timePreempt * (float) ModHidden.FADE_OUT_DURATION_MULTIPLIER; + float finalTailAlpha = (fadeInDuration - fadeInDelay) / fadeInDuration; headCirclePiece.registerEntityModifier(Modifiers.sequence( Modifiers.fadeIn(fadeInDuration), @@ -259,13 +263,18 @@ public void init(final GameObjectListener listener, final Scene scene, )); tailCirclePiece.registerEntityModifier(Modifiers.sequence( - Modifiers.fadeIn(fadeInDuration), - Modifiers.fadeOut(fadeOutDuration) + Modifiers.delay(fadeInDelay), + Modifiers.alpha(fadeInDuration - fadeInDelay, 0, finalTailAlpha), + Modifiers.alpha(fadeOutDuration, finalTailAlpha, 0) )); } else { headCirclePiece.registerEntityModifier(Modifiers.fadeIn(fadeInDuration)); - tailCirclePiece.registerEntityModifier(Modifiers.fadeIn(fadeInDuration)); + + tailCirclePiece.registerEntityModifier(Modifiers.sequence( + Modifiers.delay(fadeInDelay), + Modifiers.fadeIn(fadeInDuration) + )); } if (approachCircle.isVisible()) { @@ -289,6 +298,11 @@ public void init(final GameObjectListener listener, final Scene scene, endArrow.setPosition(pathEndPosition.x, pathEndPosition.y); } + endArrow.registerEntityModifier(Modifiers.sequence( + Modifiers.delay(fadeInDelay), + Modifiers.fadeIn(fadeInDuration) + )); + scene.attachChild(endArrow, 0); } scene.attachChild(tailCirclePiece, 0); @@ -771,17 +785,10 @@ public void update(final float dt) { approachCircle.setAlpha(0); } - float percentage = (float) (1 + elapsedSpanTime / timePreempt); - - if (percentage <= 0.5f) { - // Following core doing a very cute show animation ^_^" - percentage = Math.min(1, percentage * 2); - - if (beatmapSlider.getSpanCount() > 1) { - endArrow.setAlpha(percentage); - } + if (Config.isSnakingInSliders()) { + float percentage = FMath.clamp((float) (timePreempt + elapsedSpanTime) / (timePreempt / 3), 0, 1); - if (Config.isSnakingInSliders()) { + if (percentage < 1) { if (superPath != null && sliderBody != null) { float l = superPath.getMeasurer().maxLength() * percentage; @@ -792,12 +799,7 @@ public void update(final float dt) { tailCirclePiece.setPosition(position.x, position.y); endArrow.setPosition(position.x, position.y); - } - } else if (percentage - dt / timePreempt <= 0.5f) { - if (beatmapSlider.getSpanCount() > 1) { - endArrow.setAlpha(1); - } - if (Config.isSnakingInSliders()) { + } else { if (!preStageFinish && superPath != null && sliderBody != null) { sliderBody.setEndLength(superPath.getMeasurer().maxLength()); preStageFinish = true; From 5e25c072387a4f2e9676a6606c521df74320e532 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Thu, 23 Jan 2025 11:05:14 +0800 Subject: [PATCH 66/87] Detach tick container from slider body --- src/com/reco1l/osu/hitobjects/SliderTicks.kt | 2 ++ src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/osu/hitobjects/SliderTicks.kt b/src/com/reco1l/osu/hitobjects/SliderTicks.kt index 4e4847a96..648c58359 100644 --- a/src/com/reco1l/osu/hitobjects/SliderTicks.kt +++ b/src/com/reco1l/osu/hitobjects/SliderTicks.kt @@ -20,6 +20,8 @@ class SliderTickContainer : Container() { val position = beatmapSlider.gameplayStackedPosition + setPosition(position.x, position.y) + for (i in 1 until beatmapSlider.nestedHitObjects.size) { val tick = beatmapSlider.nestedHitObjects[i] as? SliderTick ?: break diff --git a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java index 51726ce01..b75bd96f4 100644 --- a/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java +++ b/src/ru/nsu/ccfit/zuev/osu/game/GameplaySlider.java @@ -330,8 +330,8 @@ public void init(final GameObjectListener listener, final Scene scene, } tickContainer.init(secPassed, beatmapSlider); - sliderBody.attachChild(tickContainer); + scene.attachChild(tickContainer, 0); scene.attachChild(sliderBody, 0); if (Config.isDimHitObjects()) { From b78bb678141e1c0d16b3e3e6456ac55e56542210 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Thu, 23 Jan 2025 12:32:06 +0800 Subject: [PATCH 67/87] Add slider tick fade out animation with Hidden mod --- src/com/reco1l/osu/hitobjects/SliderTicks.kt | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/com/reco1l/osu/hitobjects/SliderTicks.kt b/src/com/reco1l/osu/hitobjects/SliderTicks.kt index 648c58359..a1d2a6faf 100644 --- a/src/com/reco1l/osu/hitobjects/SliderTicks.kt +++ b/src/com/reco1l/osu/hitobjects/SliderTicks.kt @@ -8,7 +8,9 @@ import com.reco1l.andengine.sprite.* import com.reco1l.framework.* import com.rian.osu.beatmap.hitobject.* import com.rian.osu.beatmap.hitobject.sliderobject.* +import kotlin.math.min import ru.nsu.ccfit.zuev.osu.* +import ru.nsu.ccfit.zuev.osu.game.GameHelper class SliderTickContainer : Container() { private var slider: Slider? = null @@ -85,7 +87,10 @@ class SliderTickSprite : ExtendedSprite() { * @param tick The [SliderTick] represented by this [SliderTickSprite]. */ fun init(currentTimeSec: Double, tick: SliderTick) { - val fadeInStartTime = (tick.startTime - tick.timePreempt) / 1000 + val startTime = (tick.startTime / 1000).toFloat() + val timePreempt = (tick.timePreempt / 1000).toFloat() + + val fadeInStartTime = startTime - timePreempt clearEntityModifiers() @@ -94,13 +99,25 @@ class SliderTickSprite : ExtendedSprite() { registerEntityModifier( Modifiers.sequence(null, - Modifiers.delay((fadeInStartTime - currentTimeSec).toFloat()), + Modifiers.delay(fadeInStartTime - currentTimeSec.toFloat()), Modifiers.parallel(null, Modifiers.scale(ANIM_DURATION * 4, 0.5f, 1f, easing = Easing.OutElasticHalf), Modifiers.fadeIn(ANIM_DURATION) ) ) ) + + if (GameHelper.isHidden()) { + val fadeOutDuration = min(timePreempt - ANIM_DURATION, 1f) + val fadeOutStartTime = startTime - fadeOutDuration + + registerEntityModifier( + Modifiers.sequence(null, + Modifiers.delay(fadeOutStartTime - currentTimeSec.toFloat()), + Modifiers.fadeOut(fadeOutDuration) + ) + ) + } } /** @@ -111,7 +128,7 @@ class SliderTickSprite : ExtendedSprite() { fun onHit(isSuccessful: Boolean) { clearEntityModifiers() - registerEntityModifier(Modifiers.fadeOut(ANIM_DURATION, easing = Easing.OutQuint)) + registerEntityModifier(Modifiers.alpha(ANIM_DURATION, alpha, 0f, easing = Easing.OutQuint)) if (isSuccessful) { registerEntityModifier(Modifiers.scale(ANIM_DURATION, 1f, 1.5f, easing = Easing.Out)) From c8c68878d51e744a7157238ebc1f20e02cd3bfe8 Mon Sep 17 00:00:00 2001 From: Acivev <37246611+Acivev@users.noreply.github.com> Date: Thu, 23 Jan 2025 13:45:23 +0100 Subject: [PATCH 68/87] diable snowflake effect --- src/ru/nsu/ccfit/zuev/osu/MainScene.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ru/nsu/ccfit/zuev/osu/MainScene.java b/src/ru/nsu/ccfit/zuev/osu/MainScene.java index 4de7105ed..8c9e55e9f 100644 --- a/src/ru/nsu/ccfit/zuev/osu/MainScene.java +++ b/src/ru/nsu/ccfit/zuev/osu/MainScene.java @@ -345,7 +345,7 @@ public boolean onAreaTouched(final TouchEvent pSceneTouchEvent, final TextureRegion nptex = ResourceManager.getInstance().getTexture("music_np"); music_nowplay = new Sprite(Utils.toRes(Config.getRES_WIDTH() - 500), 0, (float) (40 * nptex.getWidth()) / nptex.getHeight(), 40, nptex); - addSnowfall(scene, context); +// addSnowfall(scene, context); for (int i = 0; i < 120; i++) { final float pX = (float) Config.getRES_WIDTH() / 2; From e6a4ea2efdfa91ac369fbc6d7d4bcd0ecbcf154f Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:23:29 +0800 Subject: [PATCH 69/87] Fix spinners not increasing cumulative strain time in Flashlight difficulty calculation --- .../osu/difficulty/evaluators/DroidFlashlightEvaluator.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/rian/osu/difficulty/evaluators/DroidFlashlightEvaluator.kt b/src/com/rian/osu/difficulty/evaluators/DroidFlashlightEvaluator.kt index dda68c177..33f59b9a9 100644 --- a/src/com/rian/osu/difficulty/evaluators/DroidFlashlightEvaluator.kt +++ b/src/com/rian/osu/difficulty/evaluators/DroidFlashlightEvaluator.kt @@ -52,13 +52,13 @@ object DroidFlashlightEvaluator { for (i in 0 until min(current.index, 10)) { val currentObject = current.previous(i)!! + cumulativeStrainTime += last.strainTime + // Exclude overlapping objects that can be tapped at once. if (currentObject.obj !is Spinner) { val jumpDistance = current.obj.difficultyStackedPosition .getDistance(currentObject.obj.difficultyStackedEndPosition) - cumulativeStrainTime += last.strainTime - // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) { smallDistNerf = min(1.0, jumpDistance / 75.0) From 5974b4b796c4a7ae7e4070749daeb838662e9eee Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:24:56 +0800 Subject: [PATCH 70/87] Use `logistic` function in repeated island nerf --- .../rian/osu/difficulty/evaluators/DroidRhythmEvaluator.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/com/rian/osu/difficulty/evaluators/DroidRhythmEvaluator.kt b/src/com/rian/osu/difficulty/evaluators/DroidRhythmEvaluator.kt index 50cbb3991..758795ccf 100644 --- a/src/com/rian/osu/difficulty/evaluators/DroidRhythmEvaluator.kt +++ b/src/com/rian/osu/difficulty/evaluators/DroidRhythmEvaluator.kt @@ -3,6 +3,7 @@ package com.rian.osu.difficulty.evaluators import com.rian.osu.beatmap.hitobject.Slider import com.rian.osu.beatmap.hitobject.Spinner import com.rian.osu.difficulty.DroidDifficultyHitObject +import com.rian.osu.difficulty.utils.DifficultyCalculationUtils import kotlin.math.* /** @@ -137,7 +138,9 @@ object DroidRhythmEvaluator { // Repeated island (ex: triplet -> triplet) effectiveRatio *= min( 3.0 / islandCounts[island]!!, - (1.0 / islandCounts[island]!!).pow(2.75 / (1 + exp(14 - 0.24 * island.delta))) + (1.0 / islandCounts[island]!!).pow( + DifficultyCalculationUtils.logistic(island.delta.toDouble(), 58.33, 0.24, 2.75) + ) ) } else { islandCounts[island] = 1 From 877057e249fccc08ae6b90aee23e3cbc88ee7b08 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:34:29 +0800 Subject: [PATCH 71/87] Extract `HitWindow` conversion to a separate method --- .../calculator/DroidPerformanceCalculator.kt | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt b/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt index 8ab98b431..b7f9e3842 100644 --- a/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt @@ -1,6 +1,7 @@ package com.rian.osu.difficulty.calculator import com.rian.osu.beatmap.DroidHitWindow +import com.rian.osu.beatmap.HitWindow import com.rian.osu.beatmap.PreciseDroidHitWindow import com.rian.osu.difficulty.attributes.DroidDifficultyAttributes import com.rian.osu.difficulty.attributes.DroidPerformanceAttributes @@ -321,17 +322,7 @@ class DroidPerformanceCalculator( return@run Double.POSITIVE_INFINITY } - var od = overallDifficulty.toFloat() - var hitWindow = if (isPrecise) PreciseDroidHitWindow(od) else DroidHitWindow(od) - val realGreatWindow = hitWindow.greatWindow * clockRate.toFloat() - - // Obtain the good and meh hit window for osu!droid. - od = - if (isPrecise) PreciseDroidHitWindow.hitWindow300ToOverallDifficulty(realGreatWindow) - else DroidHitWindow.hitWindow300ToOverallDifficulty(realGreatWindow) - - hitWindow = if (isPrecise) PreciseDroidHitWindow(od) else DroidHitWindow(od) - + val hitWindow = getConvertedHitWindow() val greatWindow = hitWindow.greatWindow / clockRate val okWindow = hitWindow.okWindow / clockRate val mehWindow = hitWindow.mehWindow / clockRate @@ -405,17 +396,7 @@ class DroidPerformanceCalculator( return@run Double.POSITIVE_INFINITY } - var od = overallDifficulty.toFloat() - var hitWindow = if (isPrecise) PreciseDroidHitWindow(od) else DroidHitWindow(od) - val realGreatWindow = hitWindow.greatWindow * clockRate.toFloat() - - // Obtain the good and meh hit window for osu!droid. - od = - if (isPrecise) PreciseDroidHitWindow.hitWindow300ToOverallDifficulty(realGreatWindow) - else DroidHitWindow.hitWindow300ToOverallDifficulty(realGreatWindow) - - hitWindow = if (isPrecise) PreciseDroidHitWindow(od) else DroidHitWindow(od) - + val hitWindow = getConvertedHitWindow() val greatWindow = hitWindow.greatWindow / clockRate val okWindow = hitWindow.okWindow / clockRate val mehWindow = hitWindow.mehWindow / clockRate @@ -462,6 +443,19 @@ class DroidPerformanceCalculator( Double.POSITIVE_INFINITY } + private fun getConvertedHitWindow(): HitWindow { + var od = difficultyAttributes.overallDifficulty.toFloat() + var hitWindow = if (isPrecise) PreciseDroidHitWindow(od) else DroidHitWindow(od) + val realGreatWindow = hitWindow.greatWindow * difficultyAttributes.clockRate.toFloat() + + // Obtain the good and meh hit window for osu!droid. + od = + if (isPrecise) PreciseDroidHitWindow.hitWindow300ToOverallDifficulty(realGreatWindow) + else DroidHitWindow.hitWindow300ToOverallDifficulty(realGreatWindow) + + return if (isPrecise) PreciseDroidHitWindow(od) else DroidHitWindow(od) + } + companion object { const val FINAL_MULTIPLIER = 1.24 } From a7c04d4fa9cc4650ac4e83077109f5eb290ec66c Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:38:23 +0800 Subject: [PATCH 72/87] Replace sliderend miss penalty difficult slider assumption with strain-based one --- .../attributes/DroidDifficultyAttributes.kt | 6 +++++ .../calculator/DroidDifficultyCalculator.kt | 1 + .../calculator/DroidPerformanceCalculator.kt | 20 +++++++------- .../calculator/PerformanceCalculator.kt | 6 +++++ .../rian/osu/difficulty/skills/DroidAim.kt | 27 +++++++++++++++++++ 5 files changed, 49 insertions(+), 11 deletions(-) diff --git a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt index 1ea7303e8..050745352 100644 --- a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt +++ b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt @@ -19,6 +19,12 @@ class DroidDifficultyAttributes : DifficultyAttributes() { @JvmField var clockRate = 1.0 + /** + * The amount of sliders weighted by difficulty. + */ + @JvmField + var aimDifficultSliderCount = 0.0 + /** * The difficulty corresponding to the tap skill. */ diff --git a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt index 70f711a88..bc899c77e 100644 --- a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt @@ -45,6 +45,7 @@ class DroidDifficultyCalculator : DifficultyCalculator s.difficultyRating } diff --git a/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt b/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt index b7f9e3842..df26b9525 100644 --- a/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/DroidPerformanceCalculator.kt @@ -107,17 +107,15 @@ class DroidPerformanceCalculator( // Scale the aim value with estimated full combo deviation. aimValue *= calculateDeviationBasedLengthScaling() - // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. - val estimateDifficultSliders = sliderCount * 0.15 - if (estimateDifficultSliders > 0) { - val estimateSliderEndsDropped = - min( - countOk + countMeh + countMiss, - maxCombo - scoreMaxCombo - ).toDouble().coerceIn(0.0, estimateDifficultSliders) - - val sliderNerfFactor = (1 - aimSliderFactor) * - (1 - estimateSliderEndsDropped / estimateDifficultSliders).pow(3) + aimSliderFactor + if (aimDifficultSliderCount > 0) { + // Consider all missing combo to be dropped difficult sliders. + val estimateImproperlyFollowedDifficultSliders = + min(totalImperfectHits, maxCombo - scoreMaxCombo).toDouble().coerceIn(0.0, aimDifficultSliderCount) + + val sliderNerfFactor = + (1 - aimSliderFactor) * + (1 - estimateImproperlyFollowedDifficultSliders / aimDifficultSliderCount).pow(3) + + aimSliderFactor aimValue *= sliderNerfFactor } diff --git a/src/com/rian/osu/difficulty/calculator/PerformanceCalculator.kt b/src/com/rian/osu/difficulty/calculator/PerformanceCalculator.kt index 06afd9b4b..95162922e 100644 --- a/src/com/rian/osu/difficulty/calculator/PerformanceCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/PerformanceCalculator.kt @@ -39,6 +39,12 @@ abstract class PerformanceCalculator< protected val totalHits: Int get() = difficultyAttributes.let { it.hitCircleCount + it.sliderCount + it.spinnerCount } + /** + * The total imperfect hits that were done. + */ + protected val totalImperfectHits + get() = countOk + countMeh + countMiss + /** * The total hits that were successfully done. */ diff --git a/src/com/rian/osu/difficulty/skills/DroidAim.kt b/src/com/rian/osu/difficulty/skills/DroidAim.kt index 8021d57d7..053946324 100644 --- a/src/com/rian/osu/difficulty/skills/DroidAim.kt +++ b/src/com/rian/osu/difficulty/skills/DroidAim.kt @@ -1,9 +1,11 @@ package com.rian.osu.difficulty.skills +import com.rian.osu.beatmap.hitobject.Slider import com.rian.osu.difficulty.DroidDifficultyHitObject import com.rian.osu.difficulty.attributes.DifficultSlider import com.rian.osu.difficulty.evaluators.DroidAimEvaluator import com.rian.osu.mods.Mod +import kotlin.math.exp import kotlin.math.pow /** @@ -23,11 +25,32 @@ class DroidAim( override val starsPerDouble = 1.05 val sliderVelocities = mutableListOf() + private val sliderStrains = mutableListOf() private var currentStrain = 0.0 private val skillMultiplier = 24.55 private val strainDecayBase = 0.15 + /** + * Obtains the amount of sliders that are considered difficult in terms of relative strain. + */ + fun countDifficultSliders(): Double { + if (sliderStrains.isEmpty()) { + return 0.0 + } + + val sortedStrains = sliderStrains.sortedDescending() + val maxStrain = sortedStrains[0] + + if (maxStrain == 0.0) { + return 0.0 + } + + return sortedStrains.reduce { total, strain -> + total + 1 / (1 + exp(-((strain / maxStrain) * 12 - 6))) + } + } + override fun strainValueAt(current: DroidDifficultyHitObject): Double { currentStrain *= strainDecay(current.deltaTime) currentStrain += DroidAimEvaluator.evaluateDifficultyOf(current, withSliders) * skillMultiplier @@ -38,6 +61,10 @@ class DroidAim( sliderVelocities.add(DifficultSlider(current.index + 1, velocity)) } + if (current.obj is Slider) { + sliderStrains.add(currentStrain) + } + objectStrains.add(currentStrain) return currentStrain } From 29caf3a9f9c7da2de86306b9f807fc4874419ddc Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:09:41 +0800 Subject: [PATCH 73/87] Add `DifficultyCalculationUtils` --- .../utils/DifficultyCalculationUtils.kt | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt diff --git a/src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt b/src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt new file mode 100644 index 000000000..80d70d1e6 --- /dev/null +++ b/src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt @@ -0,0 +1,81 @@ +package com.rian.osu.difficulty.utils + +import androidx.annotation.IntRange +import com.rian.osu.math.Interpolation +import kotlin.math.exp + +/** + * Utilities for difficulty calculation. + */ +object DifficultyCalculationUtils { + /** + * Converts a BPM value to milliseconds. + * + * @param bpm The BPM value. + * @param delimiter The denominator of the time signature. Defaults to 4. + * @return The BPM value in milliseconds. + */ + @JvmStatic + @JvmOverloads + fun bpmToMilliseconds(bpm: Double, @IntRange(from = 1, to = 4) delimiter: Int = 4) = + 60000 / bpm / delimiter + + /** + * Converts milliseconds to a BPM value. + * + * @param milliseconds The milliseconds value. + * @param delimiter The denominator of the time signature. Defaults to 4. + * @return The milliseconds value in BPM. + */ + @JvmStatic + @JvmOverloads + fun millisecondsToBPM(milliseconds: Double, @IntRange(from = 1, to = 4) delimiter: Int = 4) = + 60000 / milliseconds * delimiter + + /** + * Calculates an S-shaped [logistic function](https://en.wikipedia.org/wiki/Logistic_function) + * with offset at [x]. + * + * @param x The value to calculate the function for. + * @param midpointOffset How much the function midpoint is offset from zero [x]. + * @param multiplier The growth rate of the function. + * @param maxValue Maximum value returnable by the function. + * @returns The output of the logistic function calculated at [x]. + */ + @JvmStatic + @JvmOverloads + fun logistic(x: Double, midpointOffset: Double, multiplier: Double, maxValue: Double = 1.0) = + maxValue / (1 + exp(-multiplier * (x - midpointOffset))) + + /** + * Calculates the [smoothstep](https://en.wikipedia.org/wiki/Smoothstep) function at [x]. + * + * @param x The value to calculate the function for. + * @param start The [x] value at which the function returns 0. + * @param end The [x] value at which the function returns 1. + * @return The output of the smoothstep function calculated at [x]. + */ + @JvmStatic + fun smoothstep(x: Double, start: Double, end: Double): Double { + val t = Interpolation.reverseLinear(x, start, end) + + return t * t * (3 - 2 * t) + } + + /** + * Calculates the [smootherstep](https://en.wikipedia.org/wiki/Smoothstep#Variations) function at [x]. + * + * @param x The value to calculate the function for. + * @param start The [x] value at which the function returns 0. + * @param end The [x] value at which the function returns 1. + * @return The output of the smootherstep function calculated at [x]. + */ + @JvmStatic + fun smootherstep(x: Double, start: Double, end: Double): Double { + val t = Interpolation.reverseLinear(x, start, end) + + return t * t * t * (t * (t * 6 - 15) + 10) + } + + +} \ No newline at end of file From 54e5617f672804ed2e091e68b20d3aeebcbb1a56 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:09:52 +0800 Subject: [PATCH 74/87] Add angle conversion extension methods --- src/com/rian/osu/math/AngleConversions.kt | 42 +++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 src/com/rian/osu/math/AngleConversions.kt diff --git a/src/com/rian/osu/math/AngleConversions.kt b/src/com/rian/osu/math/AngleConversions.kt new file mode 100644 index 000000000..4dc49e6f4 --- /dev/null +++ b/src/com/rian/osu/math/AngleConversions.kt @@ -0,0 +1,42 @@ +@file:JvmName("AngleConversions") +package com.rian.osu.math + +/** + * Converts a value from degrees to radians. + */ +fun Double.toRadians() = this * Math.PI / 180 + +/** + * Converts a value from radians to degrees. + */ +fun Float.toRadians() = this * Math.PI.toFloat() / 180 + +/** + * Converts a value from degrees to radians. + */ +fun Long.toRadians() = this.toDouble().toRadians() + +/** + * Converts a value from degrees to radians. + */ +fun Int.toRadians() = this.toFloat().toRadians() + +/** + * Converts a value from radians to degrees. + */ +fun Double.toDegrees() = this * 180 / Math.PI + +/** + * Converts a value from degrees to radians. + */ +fun Float.toDegrees() = this * 180 / Math.PI.toFloat() + +/** + * Converts a value from degrees to radians. + */ +fun Long.toDegrees() = this.toDouble().toDegrees() + +/** + * Converts a value from degrees to radians. + */ +fun Int.toDegrees() = this.toFloat().toDegrees() \ No newline at end of file From 1ba63f4eabb251b87ec9f29a69a53a4691ba3cd3 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:10:13 +0800 Subject: [PATCH 75/87] Add reverse linear interpolation method --- src/com/rian/osu/math/Interpolation.kt | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/com/rian/osu/math/Interpolation.kt b/src/com/rian/osu/math/Interpolation.kt index a9a5020d4..101710f0d 100644 --- a/src/com/rian/osu/math/Interpolation.kt +++ b/src/com/rian/osu/math/Interpolation.kt @@ -42,6 +42,19 @@ object Interpolation { linear(start.y, end.y, amount) ) + /** + * Calculates the reverse [linear interpolation](https://en.wikipedia.org/wiki/Linear_interpolation) + * function at [x]. + * + * @param x The value to calculate the function for. + * @param start The [x] value at which the function returns 0. + * @param end The [x] value at which the function returns 1. + * @return The output of the reverse linear interpolation function calculated at [x]. + */ + @JvmStatic + fun reverseLinear(x: Double, start: Double, end: Double) = + ((x - start) / (end - start)).coerceIn(0.0, 1.0) + /** * Interpolates between [start] and [end] using a given [base] and [exponent]. * From 433181b769aa68fbd86dc05e9fcae35a4e3fdef2 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:10:42 +0800 Subject: [PATCH 76/87] Rework angle bonuses and wiggle bonus in droid aim skill --- .../osu/difficulty/DifficultyHitObject.kt | 8 ++- .../evaluators/DroidAimEvaluator.kt | 64 ++++++++++++------- .../rian/osu/difficulty/skills/DroidAim.kt | 2 +- 3 files changed, 50 insertions(+), 24 deletions(-) diff --git a/src/com/rian/osu/difficulty/DifficultyHitObject.kt b/src/com/rian/osu/difficulty/DifficultyHitObject.kt index 1b8066d47..650cdc8d4 100644 --- a/src/com/rian/osu/difficulty/DifficultyHitObject.kt +++ b/src/com/rian/osu/difficulty/DifficultyHitObject.kt @@ -382,7 +382,13 @@ abstract class DifficultyHitObject( * A distance by which all distances should be scaled in order to assume a uniform circle size. */ @JvmStatic - protected val NORMALIZED_RADIUS = 50f + val NORMALIZED_RADIUS = 50f + + /** + * The normalized diameter of a circle. + */ + @JvmStatic + val NORMALIZED_DIAMETER = NORMALIZED_RADIUS * 2 /** * The minimum delta time between hit objects. diff --git a/src/com/rian/osu/difficulty/evaluators/DroidAimEvaluator.kt b/src/com/rian/osu/difficulty/evaluators/DroidAimEvaluator.kt index abc12832e..fcb5a885d 100644 --- a/src/com/rian/osu/difficulty/evaluators/DroidAimEvaluator.kt +++ b/src/com/rian/osu/difficulty/evaluators/DroidAimEvaluator.kt @@ -2,17 +2,22 @@ package com.rian.osu.difficulty.evaluators import com.rian.osu.beatmap.hitobject.Slider import com.rian.osu.beatmap.hitobject.Spinner +import com.rian.osu.difficulty.DifficultyHitObject import com.rian.osu.difficulty.DroidDifficultyHitObject +import com.rian.osu.difficulty.utils.DifficultyCalculationUtils +import com.rian.osu.math.Interpolation +import com.rian.osu.math.toRadians import kotlin.math.* /** * An evaluator for calculating osu!droid aim difficulty. */ object DroidAimEvaluator { - private const val WIDE_ANGLE_MULTIPLIER = 1.65 - private const val ACUTE_ANGLE_MULTIPLIER = 1.95 - private const val SLIDER_MULTIPLIER = 1.5 - private const val VELOCITY_CHANGE_MULTIPLIER = 0.85 + private const val WIDE_ANGLE_MULTIPLIER = 1.5 + private const val ACUTE_ANGLE_MULTIPLIER = 2.6 + private const val SLIDER_MULTIPLIER = 1.35 + private const val VELOCITY_CHANGE_MULTIPLIER = 0.75 + private const val WIGGLE_MULTIPLIER = 1.02 private const val SINGLE_SPACING_THRESHOLD = 100.0 @@ -54,6 +59,9 @@ object DroidAimEvaluator { val last = current.previous(0)!! val lastLast = current.previous(1)!! + val radius = DifficultyHitObject.NORMALIZED_RADIUS + val diameter = DifficultyHitObject.NORMALIZED_DIAMETER + // Calculate the velocity to the current hit object, which starts with a base distance / time assuming the last object is a circle. var currentVelocity = current.lazyJumpDistance / current.strainTime @@ -71,6 +79,7 @@ object DroidAimEvaluator { // As above, do the same for the previous hit object. var prevVelocity = last.lazyJumpDistance / last.strainTime + if (lastLast.obj is Slider && withSliders) { val travelVelocity = lastLast.travelDistance / lastLast.travelTime val movementVelocity = last.minimumJumpDistance / last.minimumJumpTime @@ -82,6 +91,7 @@ object DroidAimEvaluator { var acuteAngleBonus = 0.0 var sliderBonus = 0.0 var velocityChangeBonus = 0.0 + var wiggleBonus = 0.0 // Start strain with regular velocity. var strain = currentVelocity @@ -89,31 +99,38 @@ object DroidAimEvaluator { if ( // If rhythms are the same. max(current.strainTime, last.strainTime) < 1.25 * min(current.strainTime, last.strainTime) && - current.angle != null && last.angle != null && lastLast.angle != null + current.angle != null && last.angle != null ) { val currentAngle = current.angle!! val lastAngle = last.angle!! - val lastLastAngle = lastLast.angle!! // Rewarding angles, take the smaller velocity as base. val angleBonus = min(currentVelocity, prevVelocity) wideAngleBonus = calculateWideAngleBonus(currentAngle) acuteAngleBonus = calculateAcuteAngleBonus(currentAngle) - // Only buff deltaTime exceeding 300 BPM 1/2. - if (current.strainTime > 100) { - acuteAngleBonus = 0.0 - } else { - acuteAngleBonus *= - calculateAcuteAngleBonus(lastAngle) * min(angleBonus, 125 / current.strainTime) * - sin(Math.PI / 2 * min(1.0, (100 - current.strainTime) / 25)).pow(2.0) * - sin(Math.PI / 2 * current.lazyJumpDistance.coerceIn(50.0, 100.0) - 50 / 50).pow(2.0) - } - - // Penalize wide angles if they're repeated, reducing the penalty as last.angle gets more acute. - wideAngleBonus *= angleBonus * (1 - min(wideAngleBonus, calculateWideAngleBonus(lastAngle).pow(3.0))) - // Penalize acute angles if they're repeated, reducing the penalty as lastLast.angle gets more obtuse. - acuteAngleBonus *= 0.5 + 0.5 * (1 - min(acuteAngleBonus, calculateAcuteAngleBonus(lastLastAngle).pow(3.0))) + // Penalize angle repetition. + wideAngleBonus *= 1 - min(wideAngleBonus, calculateWideAngleBonus(lastAngle).pow(3)) + acuteAngleBonus *= 0.08 + 0.92 * (1 - min(acuteAngleBonus, calculateAcuteAngleBonus(lastAngle).pow(3))) + + // Apply full wide angle bonus for distance more than one diameter. + wideAngleBonus *= angleBonus * DifficultyCalculationUtils.smootherstep(current.lazyJumpDistance, 0.0, diameter.toDouble()) + + // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter + acuteAngleBonus *= + angleBonus * + DifficultyCalculationUtils.smootherstep(DifficultyCalculationUtils.millisecondsToBPM(current.strainTime, 2), 300.0, 400.0) * + DifficultyCalculationUtils.smootherstep(current.lazyJumpDistance, diameter.toDouble(), diameter * 2.0) + + // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle + // https://www.desmos.com/calculator/dp0v0nvowc + wiggleBonus = angleBonus * + DifficultyCalculationUtils.smootherstep(current.lazyJumpDistance, radius.toDouble(), diameter.toDouble()) * + Interpolation.reverseLinear(current.lazyJumpDistance, diameter * 3.0, diameter.toDouble()).pow(1.8) * + DifficultyCalculationUtils.smootherstep(currentAngle, 110.0.toRadians(), 60.0.toRadians()) * + DifficultyCalculationUtils.smootherstep(last.lazyJumpDistance, radius.toDouble(), diameter.toDouble()) * + Interpolation.reverseLinear(last.lazyJumpDistance, diameter * 3.0, diameter.toDouble()).pow(1.8) * + DifficultyCalculationUtils.smootherstep(lastAngle, 110.0.toRadians(), 60.0.toRadians()) } if (max(prevVelocity, currentVelocity) != 0.0) { @@ -141,6 +158,8 @@ object DroidAimEvaluator { sliderBonus = last.travelDistance / last.travelTime } + strain += wiggleBonus * WIGGLE_MULTIPLIER + // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. strain += max( acuteAngleBonus * ACUTE_ANGLE_MULTIPLIER, @@ -178,10 +197,11 @@ object DroidAimEvaluator { * Calculates the bonus of wide angles. */ private fun calculateWideAngleBonus(angle: Double) = - sin(3.0 / 4 * (angle.coerceIn(Math.PI / 6, 5.0 / 6 * Math.PI) - Math.PI / 6)).pow(2.0) + DifficultyCalculationUtils.smoothstep(angle, 40.0.toRadians(), 140.0.toRadians()) /** * Calculates the bonus of acute angles. */ - private fun calculateAcuteAngleBonus(angle: Double) = 1 - calculateWideAngleBonus(angle) + private fun calculateAcuteAngleBonus(angle: Double) = + DifficultyCalculationUtils.smoothstep(angle, 140.0.toRadians(), 40.0.toRadians()) } \ No newline at end of file diff --git a/src/com/rian/osu/difficulty/skills/DroidAim.kt b/src/com/rian/osu/difficulty/skills/DroidAim.kt index 053946324..decc89242 100644 --- a/src/com/rian/osu/difficulty/skills/DroidAim.kt +++ b/src/com/rian/osu/difficulty/skills/DroidAim.kt @@ -28,7 +28,7 @@ class DroidAim( private val sliderStrains = mutableListOf() private var currentStrain = 0.0 - private val skillMultiplier = 24.55 + private val skillMultiplier = 25.6 private val strainDecayBase = 0.15 /** From 80c5d190caf7a2617e3629991a20aae73d018f9e Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:12:13 +0800 Subject: [PATCH 77/87] Move tap skill multiplier normalization to tap evaluator level --- src/com/rian/osu/difficulty/evaluators/DroidTapEvaluator.kt | 2 +- src/com/rian/osu/difficulty/skills/DroidTap.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/com/rian/osu/difficulty/evaluators/DroidTapEvaluator.kt b/src/com/rian/osu/difficulty/evaluators/DroidTapEvaluator.kt index 92d069164..08af49062 100644 --- a/src/com/rian/osu/difficulty/evaluators/DroidTapEvaluator.kt +++ b/src/com/rian/osu/difficulty/evaluators/DroidTapEvaluator.kt @@ -51,6 +51,6 @@ object DroidTapEvaluator { speedBonus += 0.75 * ErrorFunction.erf((MIN_SPEED_BONUS - strainTime) / 40).pow(2) } - return speedBonus * doubletapness.pow(1.5) / strainTime + return speedBonus * doubletapness.pow(1.5) * 1000 / strainTime } } \ No newline at end of file diff --git a/src/com/rian/osu/difficulty/skills/DroidTap.kt b/src/com/rian/osu/difficulty/skills/DroidTap.kt index 9d2209b17..6eda9c7ae 100644 --- a/src/com/rian/osu/difficulty/skills/DroidTap.kt +++ b/src/com/rian/osu/difficulty/skills/DroidTap.kt @@ -30,7 +30,7 @@ class DroidTap( private var currentStrain = 0.0 private var currentRhythm = 0.0 - private val skillMultiplier = 1375.0 + private val skillMultiplier = 1.375 private val strainDecayBase = 0.3 private val objectDeltaTimes = mutableListOf() From 8826443113918cef3115b4892adfa0523b664466 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Fri, 24 Jan 2025 20:20:07 +0800 Subject: [PATCH 78/87] Replace vibro nerf with a faster (and still accurate) alternative Should reduce loading time of real-time dpp calculation with minimal impact towards the final dpp. --- .../attributes/DroidDifficultyAttributes.kt | 16 ------ .../calculator/DroidDifficultyCalculator.kt | 11 +--- .../calculator/DroidPerformanceCalculator.kt | 52 +++++++++++++------ .../evaluators/DroidTapEvaluator.kt | 16 ++---- .../rian/osu/difficulty/skills/DroidTap.kt | 34 +----------- 5 files changed, 41 insertions(+), 88 deletions(-) diff --git a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt index 050745352..36c8725ad 100644 --- a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt +++ b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt @@ -61,12 +61,6 @@ class DroidDifficultyAttributes : DifficultyAttributes() { @JvmField var visualDifficultStrainCount = 0.0 - /** - * The average delta time of speed objects. - */ - @JvmField - var averageSpeedDeltaTime = 0.0 - /** * Possible sections at which the player can use three fingers on. */ @@ -98,14 +92,4 @@ class DroidDifficultyAttributes : DifficultyAttributes() { */ @JvmField var visualSliderFactor = 1.0 - - /** - * Describes how much of tap difficulty is contributed by notes that are "vibroable". - * - * A value closer to 1 indicates most tap difficulty is contributed by notes that are not "vibroable". - * - * A value closer to 0 indicates most tap difficulty is contributed by notes that are "vibroable". - */ - @JvmField - var vibroFactor = 1.0 } \ No newline at end of file diff --git a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt index bc899c77e..76c0ef1c0 100644 --- a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt @@ -72,15 +72,6 @@ class DroidDifficultyCalculator : DifficultyCalculator 0) { - val tapSkillVibro = DroidTap(mods, true, averageSpeedDeltaTime) - - objects.forEach { o -> tapSkillVibro.process(o) } - - vibroFactor = calculateRating(tapSkillVibro) / tapDifficulty - } } var firstObjectIndex = 0 @@ -262,7 +253,7 @@ class DroidDifficultyCalculator : DifficultyCalculator() - /** * Gets the amount of notes that are relevant to the difficulty. */ @@ -51,38 +44,15 @@ class DroidTap( fold(0.0) { acc, d -> acc + 1 / (1 + exp(-(d / maxStrain * 12 - 6))) } } - /** - * Gets the delta time relevant to the difficulty. - */ - fun relevantDeltaTime() = objectStrains.run { - if (isEmpty()) { - return 0.0 - } - - val maxStrain = max() - if (maxStrain == 0.0) { - return 0.0 - } - - objectDeltaTimes.foldIndexed(0.0) { i, acc, d -> - acc + d / (1 + exp(-(this[i] / maxStrain * 25 - 20))) - } / fold(0.0) { acc, d -> - acc + 1 / (1 + exp(-(d / maxStrain * 25 - 20))) - } - } - override fun strainValueAt(current: DroidDifficultyHitObject): Double { currentStrain *= strainDecay(current.strainTime) - currentStrain += DroidTapEvaluator.evaluateDifficultyOf( - current, considerCheesability, strainTimeCap - ) * skillMultiplier + currentStrain += DroidTapEvaluator.evaluateDifficultyOf(current, considerCheesability) * skillMultiplier currentRhythm = current.rhythmMultiplier val totalStrain = currentStrain * currentRhythm objectStrains.add(totalStrain) - objectDeltaTimes.add(current.deltaTime) return totalStrain } From 6b9d109c9f6648efad44adf72b66f6b0519ee20f Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 01:53:05 +0800 Subject: [PATCH 79/87] Revert "Replace vibro nerf with a faster (and still accurate) alternative" This reverts commit 8826443113918cef3115b4892adfa0523b664466. --- .../attributes/DroidDifficultyAttributes.kt | 16 ++++++ .../calculator/DroidDifficultyCalculator.kt | 11 +++- .../calculator/DroidPerformanceCalculator.kt | 52 ++++++------------- .../evaluators/DroidTapEvaluator.kt | 16 ++++-- .../rian/osu/difficulty/skills/DroidTap.kt | 34 +++++++++++- 5 files changed, 88 insertions(+), 41 deletions(-) diff --git a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt index 36c8725ad..050745352 100644 --- a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt +++ b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt @@ -61,6 +61,12 @@ class DroidDifficultyAttributes : DifficultyAttributes() { @JvmField var visualDifficultStrainCount = 0.0 + /** + * The average delta time of speed objects. + */ + @JvmField + var averageSpeedDeltaTime = 0.0 + /** * Possible sections at which the player can use three fingers on. */ @@ -92,4 +98,14 @@ class DroidDifficultyAttributes : DifficultyAttributes() { */ @JvmField var visualSliderFactor = 1.0 + + /** + * Describes how much of tap difficulty is contributed by notes that are "vibroable". + * + * A value closer to 1 indicates most tap difficulty is contributed by notes that are not "vibroable". + * + * A value closer to 0 indicates most tap difficulty is contributed by notes that are "vibroable". + */ + @JvmField + var vibroFactor = 1.0 } \ No newline at end of file diff --git a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt index 76c0ef1c0..bc899c77e 100644 --- a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt @@ -72,6 +72,15 @@ class DroidDifficultyCalculator : DifficultyCalculator 0) { + val tapSkillVibro = DroidTap(mods, true, averageSpeedDeltaTime) + + objects.forEach { o -> tapSkillVibro.process(o) } + + vibroFactor = calculateRating(tapSkillVibro) / tapDifficulty + } } var firstObjectIndex = 0 @@ -253,7 +262,7 @@ class DroidDifficultyCalculator : DifficultyCalculator() + /** * Gets the amount of notes that are relevant to the difficulty. */ @@ -44,15 +51,38 @@ class DroidTap( fold(0.0) { acc, d -> acc + 1 / (1 + exp(-(d / maxStrain * 12 - 6))) } } + /** + * Gets the delta time relevant to the difficulty. + */ + fun relevantDeltaTime() = objectStrains.run { + if (isEmpty()) { + return 0.0 + } + + val maxStrain = max() + if (maxStrain == 0.0) { + return 0.0 + } + + objectDeltaTimes.foldIndexed(0.0) { i, acc, d -> + acc + d / (1 + exp(-(this[i] / maxStrain * 25 - 20))) + } / fold(0.0) { acc, d -> + acc + 1 / (1 + exp(-(d / maxStrain * 25 - 20))) + } + } + override fun strainValueAt(current: DroidDifficultyHitObject): Double { currentStrain *= strainDecay(current.strainTime) - currentStrain += DroidTapEvaluator.evaluateDifficultyOf(current, considerCheesability) * skillMultiplier + currentStrain += DroidTapEvaluator.evaluateDifficultyOf( + current, considerCheesability, strainTimeCap + ) * skillMultiplier currentRhythm = current.rhythmMultiplier val totalStrain = currentStrain * currentRhythm objectStrains.add(totalStrain) + objectDeltaTimes.add(current.deltaTime) return totalStrain } From cdf0d2545318fc7abf5dfd01ef05f924f5458eb5 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 13:28:47 +0800 Subject: [PATCH 80/87] Replace index-based skill access with `skills.first` --- .../calculator/DroidDifficultyCalculator.kt | 314 ++++++++++-------- .../StandardDifficultyCalculator.kt | 66 ++-- .../rian/osu/difficulty/skills/DroidAim.kt | 2 +- .../osu/difficulty/skills/DroidFlashlight.kt | 3 +- .../rian/osu/difficulty/skills/DroidTap.kt | 3 +- .../rian/osu/difficulty/skills/DroidVisual.kt | 3 +- .../rian/osu/difficulty/skills/StandardAim.kt | 3 +- 7 files changed, 235 insertions(+), 159 deletions(-) diff --git a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt index bc899c77e..883617b4c 100644 --- a/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/DroidDifficultyCalculator.kt @@ -42,126 +42,11 @@ class DroidDifficultyCalculator : DifficultyCalculator s.difficultyRating } - - for (slider in it.sliderVelocities) { - val difficultyRating = slider.difficultyRating / velocitySum - - // Only consider sliders that are fast enough. - if (difficultyRating > 0.02) { - difficultSliders.add(slider.copy(difficultyRating = difficultyRating)) - } - } - - difficultSliders.sortByDescending { s -> s.difficultyRating } - - // Take the top 15% most difficult sliders. - difficultSliders.dropLastWhile { difficultSliders.size > ceil(0.15 * sliderCount) } - } - - aimSliderFactor = if (aimDifficulty > 0) calculateRating(skills[1]) / aimDifficulty else 1.0 - - rhythmDifficulty = calculateRating(skills[2]) - - (skills[3] as DroidTap).let { - tapDifficulty = calculateRating(it) - tapDifficultStrainCount = it.countDifficultStrains() - speedNoteCount = it.relevantNoteCount() - averageSpeedDeltaTime = it.relevantDeltaTime() - - if (tapDifficulty > 0) { - val tapSkillVibro = DroidTap(mods, true, averageSpeedDeltaTime) - - objects.forEach { o -> tapSkillVibro.process(o) } - - vibroFactor = calculateRating(tapSkillVibro) / tapDifficulty - } - } - - var firstObjectIndex = 0 - val sectionBoundaries = mutableListOf>() - - for (i in 0 until objects.size - 1) { - val current = objects[i].obj - val next = objects[i + 1].obj - val deltaTime = next.startTime - current.endTime - - if (deltaTime >= maximumSectionDeltaTime) { - // Ignore sections that do not meet object count requirement. - if (i - firstObjectIndex >= minimumSectionObjectCount) { - sectionBoundaries.add(Pair(firstObjectIndex, i)) - } - - firstObjectIndex = i + 1 - } - } - - // Do not forget to manually add the last beatmap section, which would otherwise be ignored. - if (objects.size - firstObjectIndex >= minimumSectionObjectCount) { - sectionBoundaries.add(Pair(firstObjectIndex, objects.size - 1)) - } - - // Re-filter with tap strain in mind. - (skills[4] as DroidTap).objectStrains.let { - for (section in sectionBoundaries) { - var inSpeedSection = false - var newFirstObjectIndex = section.first - - for (i in section.first until section.second) { - val strain = it[i] - - if (!inSpeedSection && strain >= threeFingerStrainThreshold) { - inSpeedSection = true - newFirstObjectIndex = i - continue - } - - if (inSpeedSection && strain < threeFingerStrainThreshold) { - inSpeedSection = false - - // Ignore sections that do not meet object count requirement. - if (i - newFirstObjectIndex < minimumSectionObjectCount) { - continue - } - - possibleThreeFingeredSections.add(HighStrainSection( - newFirstObjectIndex, - i, - calculateThreeFingerSummedStrain(it.subList(newFirstObjectIndex, i)) - )) - } - } - - // Do not forget to manually add the last beatmap section, which would otherwise be ignored. - // Ignore sections that don't meet object count requirement. - if (inSpeedSection && section.second - newFirstObjectIndex >= minimumSectionObjectCount) { - possibleThreeFingeredSections.add(HighStrainSection( - newFirstObjectIndex, - section.second, - calculateThreeFingerSummedStrain(it.subList(newFirstObjectIndex, section.second)) - )) - } - } - } - - (skills[5] as DroidFlashlight).let { - flashlightDifficulty = calculateRating(it) - flashlightDifficultStrainCount = it.countDifficultStrains() - } - - flashlightSliderFactor = if (flashlightDifficulty > 0) calculateRating(skills[6]) / flashlightDifficulty else 1.0 - - (skills[7] as DroidVisual).let { - visualDifficulty = calculateRating(it) - visualDifficultStrainCount = it.countDifficultStrains() - } - - visualSliderFactor = if (visualDifficulty > 0) calculateRating(skills[8]) / visualDifficulty else 1.0 + populateAimAttributes(skills) + populateTapAttributes(skills, objects) + populateRhythmAttributes(skills) + populateFlashlightAttributes(skills) + populateVisualAttributes(skills) if (mods.any { it is ModRelax }) { aimDifficulty *= 0.9 @@ -179,7 +64,7 @@ class DroidDifficultyCalculator : DifficultyCalculator> { val mods = beatmap.mods?.toList() ?: emptyList() - return arrayOf( - DroidAim(mods, true), - DroidAim(mods, false), - // Tap and visual skills depend on rhythm skill, so we put it first - DroidRhythm(mods), - DroidTap(mods, true), - DroidTap(mods, false), - DroidFlashlight(mods, true), - DroidFlashlight(mods, false), - DroidVisual(mods, true), - DroidVisual(mods, false) - ) + val aim = DroidAim(mods, true) + val aimNoSlider = DroidAim(mods, false) + // Tap and visual skills depend on rhythm skill, so we will put it first + val rhythm = DroidRhythm(mods) + val tapCheese = DroidTap(mods, true) + val tapNoCheese = DroidTap(mods, false) + val visual = DroidVisual(mods, true) + val visualNoSlider = DroidVisual(mods, false) + + if (mods.any { it is ModFlashlight }) { + val flashlight = DroidFlashlight(mods, true) + val flashlightNoSlider = DroidFlashlight(mods, false) + + return arrayOf( + aim, aimNoSlider, rhythm, tapCheese, tapNoCheese, + flashlight, flashlightNoSlider, visual, visualNoSlider + ) + } + + return arrayOf(aim, aimNoSlider, rhythm, tapCheese, tapNoCheese, visual, visualNoSlider) } @Suppress("UNCHECKED_CAST") @@ -262,10 +155,163 @@ class DroidDifficultyCalculator : DifficultyCalculator>) { + val aim = skills.first { it is DroidAim && it.withSliders } as DroidAim + aimDifficulty = calculateRating(aim) + aimDifficultStrainCount = aim.countDifficultStrains() + aimDifficultSliderCount = aim.countDifficultSliders() + + val velocitySum = aim.sliderVelocities.sumOf { s -> s.difficultyRating } + + for (slider in aim.sliderVelocities) { + val difficultyRating = slider.difficultyRating / velocitySum + + // Only consider sliders that are fast enough. + if (difficultyRating > 0.02) { + difficultSliders.add(slider.copy(difficultyRating = difficultyRating)) + } + } + + difficultSliders.sortByDescending { s -> s.difficultyRating } + + // Take the top 15% most difficult sliders. + difficultSliders.dropLastWhile { difficultSliders.size > ceil(0.15 * sliderCount) } + + if (aimDifficulty > 0) { + val aimNoSlider = skills.first { it is DroidAim && !it.withSliders } as DroidAim + + aimSliderFactor = calculateRating(aimNoSlider) / aimDifficulty + } else { + aimSliderFactor = 1.0 + } + } + + private fun DroidDifficultyAttributes.populateTapAttributes( + skills: Array>, + objects: Array + ) { + val tap = skills.first { it is DroidTap && it.considerCheesability } as DroidTap + + tapDifficulty = calculateRating(tap) + tapDifficultStrainCount = tap.countDifficultStrains() + speedNoteCount = tap.relevantNoteCount() + averageSpeedDeltaTime = tap.relevantDeltaTime() + + if (tapDifficulty > 0) { + val tapSkillVibro = DroidTap(mods, true, averageSpeedDeltaTime) + + objects.forEach { tapSkillVibro.process(it) } + + vibroFactor = calculateRating(tapSkillVibro) / tapDifficulty + } + + var firstObjectIndex = 0 + val sectionBoundaries = mutableListOf>() + + for (i in 0 until objects.size - 1) { + val current = objects[i].obj + val next = objects[i + 1].obj + val deltaTime = next.startTime - current.endTime + + if (deltaTime >= maximumSectionDeltaTime) { + // Ignore sections that do not meet object count requirement. + if (i - firstObjectIndex >= minimumSectionObjectCount) { + sectionBoundaries.add(Pair(firstObjectIndex, i)) + } + + firstObjectIndex = i + 1 + } + } + + // Do not forget to manually add the last beatmap section, which would otherwise be ignored. + if (objects.size - firstObjectIndex >= minimumSectionObjectCount) { + sectionBoundaries.add(Pair(firstObjectIndex, objects.size - 1)) + } + + val tapNoCheese = skills.first { it is DroidTap && !it.considerCheesability } as DroidTap + + // Re-filter with tap strain in mind. + for (section in sectionBoundaries) { + var inSpeedSection = false + var newFirstObjectIndex = section.first + + for (i in section.first until section.second) { + val strain = tapNoCheese.objectStrains[i] + + if (!inSpeedSection && strain >= threeFingerStrainThreshold) { + inSpeedSection = true + newFirstObjectIndex = i + continue + } + + if (inSpeedSection && strain < threeFingerStrainThreshold) { + inSpeedSection = false + + // Ignore sections that do not meet object count requirement. + if (i - newFirstObjectIndex < minimumSectionObjectCount) { + continue + } + + possibleThreeFingeredSections.add(HighStrainSection( + newFirstObjectIndex, + i, + calculateThreeFingerSummedStrain(tapNoCheese.objectStrains.subList(newFirstObjectIndex, i)) + )) + } + } + + // Do not forget to manually add the last beatmap section, which would otherwise be ignored. + // Ignore sections that don't meet object count requirement. + if (inSpeedSection && section.second - newFirstObjectIndex >= minimumSectionObjectCount) { + possibleThreeFingeredSections.add(HighStrainSection( + newFirstObjectIndex, + section.second, + calculateThreeFingerSummedStrain(tapNoCheese.objectStrains.subList(newFirstObjectIndex, section.second)) + )) + } + } + } + + private fun DroidDifficultyAttributes.populateRhythmAttributes(skills: Array>) { + val rhythm = skills.first { it is DroidRhythm } as DroidRhythm + + rhythmDifficulty = calculateRating(rhythm) + } + + private fun DroidDifficultyAttributes.populateFlashlightAttributes(skills: Array>) { + val flashlight = skills.firstOrNull { it is DroidFlashlight && it.withSliders } as? DroidFlashlight ?: return + + flashlightDifficulty = calculateRating(flashlight) + flashlightDifficultStrainCount = flashlight.countDifficultStrains() + + if (flashlightDifficulty > 0) { + val flashlightNoSlider = skills.first { it is DroidFlashlight && !it.withSliders } as DroidFlashlight + + flashlightSliderFactor = calculateRating(flashlightNoSlider) / flashlightDifficulty + } else { + flashlightSliderFactor = 1.0 + } + } + + private fun DroidDifficultyAttributes.populateVisualAttributes(skills: Array>) { + val visual = skills.first { it is DroidVisual && it.withSliders } as DroidVisual + + visualDifficulty = calculateRating(visual) + visualDifficultStrainCount = visual.countDifficultStrains() + + if (visualDifficulty > 0) { + val visualNoSlider = skills.first { it is DroidVisual && !it.withSliders } as DroidVisual + + visualSliderFactor = calculateRating(visualNoSlider) / visualDifficulty + } else { + visualSliderFactor = 1.0 + } + } + private fun calculateThreeFingerSummedStrain(strains: List) = strains.fold(0.0) { acc, d -> acc + d / threeFingerStrainThreshold }.pow(0.75) } \ No newline at end of file diff --git a/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt b/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt index 7fb608349..3ed391628 100644 --- a/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt @@ -33,15 +33,9 @@ class StandardDifficultyCalculator : DifficultyCalculator 0) calculateRating(skills[1]) / aimDifficulty else 1.0 - - aimDifficultStrainCount = (skills[0] as StandardAim).countDifficultStrains() - speedDifficultStrainCount = (skills[2] as StandardSpeed).countDifficultStrains() + populateAimAttributes(skills) + populateSpeedAttributes(skills) + populateFlashlightAttributes(skills) if (mods.any { it is ModRelax }) { aimDifficulty *= 0.9 @@ -51,13 +45,13 @@ class StandardDifficultyCalculator : DifficultyCalculator> { val mods = beatmap.mods?.toList() ?: emptyList() - return arrayOf( - StandardAim(mods, true), - StandardAim(mods, false), - StandardSpeed(mods), - StandardFlashlight(mods) - ) + val aim = StandardAim(mods, true) + val aimNoSlider = StandardAim(mods, false) + val speed = StandardSpeed(mods) + + if (mods.any { it is ModFlashlight }) { + return arrayOf(aim, aimNoSlider, speed, StandardFlashlight(mods)) + } + + return arrayOf(aim, aimNoSlider, speed) } @Suppress("UNCHECKED_CAST") @@ -127,4 +124,33 @@ class StandardDifficultyCalculator : DifficultyCalculator>) { + val aim = skills.first { it is StandardAim && it.withSliders } as StandardAim + + aimDifficulty = calculateRating(aim) + aimDifficultStrainCount = aim.countDifficultStrains() + + if (aimDifficulty > 0) { + val aimNoSlider = skills.first { it is StandardAim && !it.withSliders } as StandardAim + + aimSliderFactor = calculateRating(aimNoSlider) / aimDifficulty + } else { + aimSliderFactor = 1.0 + } + } + + private fun StandardDifficultyAttributes.populateSpeedAttributes(skills: Array>) { + val speed = skills.first { it is StandardSpeed } as StandardSpeed + + speedDifficulty = calculateRating(speed) + speedNoteCount = speed.relevantNoteCount() + speedDifficultStrainCount = speed.countDifficultStrains() + } + + private fun StandardDifficultyAttributes.populateFlashlightAttributes(skills: Array>) { + val flashlight = skills.firstOrNull { it is StandardFlashlight } as? StandardFlashlight ?: return + + flashlightDifficulty = calculateRating(flashlight) + } } \ No newline at end of file diff --git a/src/com/rian/osu/difficulty/skills/DroidAim.kt b/src/com/rian/osu/difficulty/skills/DroidAim.kt index decc89242..c6864053e 100644 --- a/src/com/rian/osu/difficulty/skills/DroidAim.kt +++ b/src/com/rian/osu/difficulty/skills/DroidAim.kt @@ -20,7 +20,7 @@ class DroidAim( /** * Whether to consider sliders in the calculation. */ - private val withSliders: Boolean + val withSliders: Boolean ) : DroidStrainSkill(mods) { override val starsPerDouble = 1.05 diff --git a/src/com/rian/osu/difficulty/skills/DroidFlashlight.kt b/src/com/rian/osu/difficulty/skills/DroidFlashlight.kt index 484945226..b9e16395d 100644 --- a/src/com/rian/osu/difficulty/skills/DroidFlashlight.kt +++ b/src/com/rian/osu/difficulty/skills/DroidFlashlight.kt @@ -18,7 +18,8 @@ class DroidFlashlight( /** * Whether to consider sliders in the calculation. */ - private val withSliders: Boolean + @JvmField + val withSliders: Boolean ) : DroidStrainSkill(mods) { override val starsPerDouble = 1.06 diff --git a/src/com/rian/osu/difficulty/skills/DroidTap.kt b/src/com/rian/osu/difficulty/skills/DroidTap.kt index 6eda9c7ae..f623b700d 100644 --- a/src/com/rian/osu/difficulty/skills/DroidTap.kt +++ b/src/com/rian/osu/difficulty/skills/DroidTap.kt @@ -18,7 +18,8 @@ class DroidTap( /** * Whether to consider cheesability. */ - private val considerCheesability: Boolean, + @JvmField + val considerCheesability: Boolean, /** * The strain time to cap to. diff --git a/src/com/rian/osu/difficulty/skills/DroidVisual.kt b/src/com/rian/osu/difficulty/skills/DroidVisual.kt index c241e2690..121ed196a 100644 --- a/src/com/rian/osu/difficulty/skills/DroidVisual.kt +++ b/src/com/rian/osu/difficulty/skills/DroidVisual.kt @@ -18,7 +18,8 @@ class DroidVisual( /** * Whether to consider sliders in the calculation. */ - private val withSliders: Boolean + @JvmField + val withSliders: Boolean ) : DroidStrainSkill(mods) { override val starsPerDouble = 1.025 diff --git a/src/com/rian/osu/difficulty/skills/StandardAim.kt b/src/com/rian/osu/difficulty/skills/StandardAim.kt index 88055333c..fe4d1ecaa 100644 --- a/src/com/rian/osu/difficulty/skills/StandardAim.kt +++ b/src/com/rian/osu/difficulty/skills/StandardAim.kt @@ -17,7 +17,8 @@ class StandardAim( /** * Whether to consider sliders in the calculation. */ - private val withSliders: Boolean + @JvmField + val withSliders: Boolean ) : StandardStrainSkill(mods) { private var currentStrain = 0.0 private val skillMultiplier = 25.18 From e992270ec9c94739df56207f144fa6a5d52ef1d2 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:50:19 +0800 Subject: [PATCH 81/87] Use `fold` instead of `reduce` to retain initial value --- src/com/rian/osu/difficulty/skills/DroidAim.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/rian/osu/difficulty/skills/DroidAim.kt b/src/com/rian/osu/difficulty/skills/DroidAim.kt index c6864053e..789e8e018 100644 --- a/src/com/rian/osu/difficulty/skills/DroidAim.kt +++ b/src/com/rian/osu/difficulty/skills/DroidAim.kt @@ -46,7 +46,7 @@ class DroidAim( return 0.0 } - return sortedStrains.reduce { total, strain -> + return sortedStrains.fold(0.0) { total, strain -> total + 1 / (1 + exp(-((strain / maxStrain) * 12 - 6))) } } From 30b787f08b09343927d61409e79454a36c6e3d7b Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 14:59:29 +0800 Subject: [PATCH 82/87] Fix wrong milliseconds to BPM conversion --- src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt b/src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt index 80d70d1e6..78bffdde7 100644 --- a/src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt +++ b/src/com/rian/osu/difficulty/utils/DifficultyCalculationUtils.kt @@ -30,7 +30,7 @@ object DifficultyCalculationUtils { @JvmStatic @JvmOverloads fun millisecondsToBPM(milliseconds: Double, @IntRange(from = 1, to = 4) delimiter: Int = 4) = - 60000 / milliseconds * delimiter + 60000 / (milliseconds * delimiter) /** * Calculates an S-shaped [logistic function](https://en.wikipedia.org/wiki/Logistic_function) From 51b4a882428660054742b55c273848bbdc1cafea Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 15:01:19 +0800 Subject: [PATCH 83/87] Port osu!standard pp changes These may still change, but only moderately, so it's fine to port now. --- .../attributes/DifficultyAttributes.kt | 12 + .../attributes/DroidDifficultyAttributes.kt | 12 - .../StandardDifficultyCalculator.kt | 7 + .../StandardPerformanceCalculator.kt | 233 ++++++++++++++---- .../evaluators/StandardAimEvaluator.kt | 57 +++-- .../evaluators/StandardFlashlightEvaluator.kt | 4 +- .../evaluators/StandardSpeedEvaluator.kt | 10 +- .../rian/osu/difficulty/skills/StandardAim.kt | 30 ++- .../osu/difficulty/skills/StandardSpeed.kt | 4 +- 9 files changed, 280 insertions(+), 89 deletions(-) diff --git a/src/com/rian/osu/difficulty/attributes/DifficultyAttributes.kt b/src/com/rian/osu/difficulty/attributes/DifficultyAttributes.kt index 39647bb45..33446aaab 100644 --- a/src/com/rian/osu/difficulty/attributes/DifficultyAttributes.kt +++ b/src/com/rian/osu/difficulty/attributes/DifficultyAttributes.kt @@ -9,6 +9,12 @@ import com.rian.osu.mods.Mod * Holds data that can be used to calculate performance points. */ abstract class DifficultyAttributes { + /** + * The overall clock rate that was applied to the beatmap. + */ + @JvmField + var clockRate = 1.0 + /** * The mods which were applied to the beatmap. */ @@ -63,6 +69,12 @@ abstract class DifficultyAttributes { @JvmField var aimDifficultStrainCount = 0.0 + /** + * The amount of sliders weighted by difficulty. + */ + @JvmField + var aimDifficultSliderCount = 0.0 + /** * The perceived overall difficulty inclusive of rate-adjusting [Mod]s (DT/HT/etc.). * diff --git a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt index 050745352..e43e92d45 100644 --- a/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt +++ b/src/com/rian/osu/difficulty/attributes/DroidDifficultyAttributes.kt @@ -13,18 +13,6 @@ class DroidDifficultyAttributes : DifficultyAttributes() { @JvmField var customSpeedMultiplier = 1f - /** - * The overall clock rate that was applied to the beatmap. - */ - @JvmField - var clockRate = 1.0 - - /** - * The amount of sliders weighted by difficulty. - */ - @JvmField - var aimDifficultSliderCount = 0.0 - /** * The difficulty corresponding to the tap skill. */ diff --git a/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt b/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt index 3ed391628..e4d52697e 100644 --- a/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/StandardDifficultyCalculator.kt @@ -12,6 +12,7 @@ import com.rian.osu.difficulty.skills.Skill import com.rian.osu.difficulty.skills.StandardAim import com.rian.osu.difficulty.skills.StandardFlashlight import com.rian.osu.difficulty.skills.StandardSpeed +import com.rian.osu.mods.ModAutopilot import com.rian.osu.mods.ModFlashlight import com.rian.osu.mods.ModRelax import kotlin.math.cbrt @@ -32,6 +33,7 @@ class StandardDifficultyCalculator : DifficultyCalculator ) = StandardDifficultyAttributes().apply { mods = beatmap.mods?.toList() ?: mods + clockRate = beatmap.overallSpeedMultiplier.toDouble() populateAimAttributes(skills) populateSpeedAttributes(skills) @@ -41,6 +43,10 @@ class StandardDifficultyCalculator : DifficultyCalculator 0) { diff --git a/src/com/rian/osu/difficulty/calculator/StandardPerformanceCalculator.kt b/src/com/rian/osu/difficulty/calculator/StandardPerformanceCalculator.kt index 9e1ef6a33..b0bc25416 100644 --- a/src/com/rian/osu/difficulty/calculator/StandardPerformanceCalculator.kt +++ b/src/com/rian/osu/difficulty/calculator/StandardPerformanceCalculator.kt @@ -1,17 +1,23 @@ package com.rian.osu.difficulty.calculator +import com.rian.osu.beatmap.StandardHitWindow import com.rian.osu.difficulty.attributes.StandardDifficultyAttributes import com.rian.osu.difficulty.attributes.StandardPerformanceAttributes +import com.rian.osu.math.ErrorFunction +import com.rian.osu.math.Interpolation +import com.rian.osu.mods.ModAutopilot import com.rian.osu.mods.ModFlashlight import com.rian.osu.mods.ModHidden import com.rian.osu.mods.ModNoFail import com.rian.osu.mods.ModRelax import com.rian.osu.mods.ModScoreV2 +import kotlin.math.exp import kotlin.math.ln import kotlin.math.log10 import kotlin.math.max import kotlin.math.min import kotlin.math.pow +import kotlin.math.sqrt /** * A performance calculator for calculating osu!standard performance points. @@ -26,6 +32,8 @@ class StandardPerformanceCalculator( StandardPerformanceAttributes, PerformanceCalculationParameters >(difficultyAttributes) { + private var speedDeviation = 0.0 + override fun createPerformanceAttributes() = StandardPerformanceAttributes().also { var multiplier = FINAL_MULTIPLIER @@ -55,6 +63,8 @@ class StandardPerformanceCalculator( } } + speedDeviation = calculateSpeedDeviation() + it.effectiveMissCount = effectiveMissCount it.aim = calculateAimValue() it.speed = calculateSpeedValue() @@ -69,7 +79,11 @@ class StandardPerformanceCalculator( } private fun calculateAimValue(): Double { - var aimValue = (5 * max(1.0, difficultyAttributes.aimDifficulty / 0.0675) - 4).pow(3.0) / 100000 + if (difficultyAttributes.mods.any { it is ModAutopilot }) { + return 0.0 + } + + var aimValue = baseValue(difficultyAttributes.aimDifficulty) // Longer maps are worth more val lengthBonus = 0.95 + 0.4 * min(1.0, totalHits / 2000.0) + @@ -97,18 +111,15 @@ class StandardPerformanceCalculator( aimValue *= 1 + 0.04 * (12 - approachRate) } - // We assume 15% of sliders in a map are difficult since there's no way to tell from the performance calculator. - val estimateDifficultSliders = sliderCount * 0.15 - if (estimateDifficultSliders > 0) { - val estimateSliderEndsDropped = - min( - countOk + countMeh + countMiss, - maxCombo - scoreMaxCombo - ).toDouble().coerceIn(0.0, estimateDifficultSliders) + if (aimDifficultSliderCount > 0) { + // Consider all missing combo to be dropped difficult sliders. + val estimateImproperlyFollowedDifficultSliders = + min(totalImperfectHits, maxCombo - scoreMaxCombo).toDouble().coerceIn(0.0, aimDifficultSliderCount) val sliderNerfFactor = (1 - aimSliderFactor) * - (1 - estimateSliderEndsDropped / estimateDifficultSliders).pow(3.0) + aimSliderFactor + (1 - estimateImproperlyFollowedDifficultSliders / aimDifficultSliderCount).pow(3) + + aimSliderFactor aimValue *= sliderNerfFactor } @@ -118,16 +129,16 @@ class StandardPerformanceCalculator( aimValue *= accuracy // It is also important to consider accuracy difficulty when doing that. - aimValue *= 0.98 + difficultyAttributes.overallDifficulty.pow(2.0) / 2500 + aimValue *= 0.98 + max(0.0, difficultyAttributes.overallDifficulty).pow(2.0) / 2500 return aimValue } private fun calculateSpeedValue(): Double { - if (difficultyAttributes.mods.any { it is ModRelax }) { + if (difficultyAttributes.mods.any { it is ModRelax } || speedDeviation == Double.POSITIVE_INFINITY) { return 0.0 } - var speedValue = (5 * max(1.0, difficultyAttributes.speedDifficulty / 0.0675) - 4).pow(3.0) / 100000 + var speedValue = baseValue(difficultyAttributes.speedDifficulty) // Longer maps are worth more val lengthBonus = 0.95 + 0.4 * min(1.0, totalHits / 2000.0) + @@ -136,32 +147,30 @@ class StandardPerformanceCalculator( speedValue *= lengthBonus speedValue *= calculateMissPenalty(difficultyAttributes.speedDifficultStrainCount) - difficultyAttributes.apply { - // AR scaling - if (approachRate > 10.33) { - // Buff for longer maps with high AR. - speedValue *= 1 + 0.3 * (approachRate - 10.33) * lengthBonus - } - if (mods.any { it is ModHidden }) { - speedValue *= 1 + 0.04 * (12 - approachRate) - } - - // Calculate accuracy assuming the worst case scenario. - val relevantTotalDiff = totalHits - speedNoteCount - val relevantCountGreat = max(0.0, countGreat - relevantTotalDiff) - val relevantCountOk = max(0.0, countOk - max(0.0, relevantTotalDiff - countGreat)) - val relevantCountMeh = max(0.0, countMeh - max(0.0, relevantTotalDiff - countGreat - countOk)) - val relevantAccuracy = - if (speedNoteCount == 0.0) 0.0 - else (relevantCountGreat * 6 + relevantCountOk * 2 + relevantCountMeh) / (speedNoteCount * 6) + // AR scaling + if (difficultyAttributes.approachRate > 10.33 && difficultyAttributes.mods.none { it is ModAutopilot }) { + // Buff for longer maps with high AR. + speedValue *= 1 + 0.3 * (difficultyAttributes.approachRate - 10.33) * lengthBonus + } - // Scale the speed value with accuracy and OD. - speedValue *= (0.95 + overallDifficulty.pow(2.0) / 750) * - ((accuracy + relevantAccuracy) / 2).pow((14.5 - overallDifficulty) / 2) + if (difficultyAttributes.mods.any { it is ModHidden }) { + speedValue *= 1 + 0.04 * (12 - difficultyAttributes.approachRate) } - // Scale the speed value with # of 50s to punish double-tapping. - speedValue *= 0.99.pow(max(0.0, countMeh - totalHits / 500.0)) + // Calculate accuracy assuming the worst case scenario. + val relevantTotalDiff = totalHits - difficultyAttributes.speedNoteCount + val relevantCountGreat = max(0.0, countGreat - relevantTotalDiff) + val relevantCountOk = max(0.0, countOk - max(0.0, relevantTotalDiff - countGreat)) + val relevantCountMeh = max(0.0, countMeh - max(0.0, relevantTotalDiff - countGreat - countOk)) + val relevantAccuracy = + if (difficultyAttributes.speedNoteCount == 0.0) 0.0 + else (relevantCountGreat * 6 + relevantCountOk * 2 + relevantCountMeh) / (difficultyAttributes.speedNoteCount * 6) + + speedValue *= calculateSpeedHighDeviationNerf() + + // Scale the speed value with accuracy and OD. + speedValue *= (0.95 + max(0.0, difficultyAttributes.overallDifficulty).pow(2.0) / 750) * + ((accuracy + relevantAccuracy) / 2).pow((14.5 - difficultyAttributes.overallDifficulty) / 2) return speedValue } @@ -182,24 +191,22 @@ class StandardPerformanceCalculator( ) else 0.0 - return difficultyAttributes.run { - // Lots of arbitrary values from testing. - // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution - var accuracyValue = - 1.52163.pow(overallDifficulty) * betterAccuracyPercentage.pow(24.0) * 2.83 + // Lots of arbitrary values from testing. + // Considering to use derivation from perfect accuracy in a probabilistic manner - assume normal distribution + var accuracyValue = 1.52163.pow(difficultyAttributes.overallDifficulty) * betterAccuracyPercentage.pow(24.0) * 2.83 - // Bonus for many hit circles - it's harder to keep good accuracy up for longer - accuracyValue *= min(1.15, (hitObjectWithAccuracyCount / 1000.0).pow(0.3)) + // Bonus for many hit circles - it's harder to keep good accuracy up for longer + accuracyValue *= min(1.15, (hitObjectWithAccuracyCount / 1000.0).pow(0.3)) - if (mods.any { it is ModHidden }) { - accuracyValue *= 1.08 - } - if (mods.any { it is ModFlashlight }) { - accuracyValue *= 1.02 - } + if (difficultyAttributes.mods.any { it is ModHidden }) { + accuracyValue *= 1.08 + } - accuracyValue + if (difficultyAttributes.mods.any { it is ModFlashlight }) { + accuracyValue *= 1.02 } + + return accuracyValue } private fun calculateFlashlightValue(): Double { @@ -232,6 +239,132 @@ class StandardPerformanceCalculator( return flashlightValue } + /** + * Estimates a player's deviation on speed notes using [calculateDeviation], assuming worst-case. + * + * Treats all speed notes as hit circles. + */ + private fun calculateSpeedDeviation(): Double { + if (totalSuccessfulHits == 0) { + return Double.POSITIVE_INFINITY + } + + // Calculate accuracy assuming the worst case scenario + val speedNoteCount = difficultyAttributes.speedNoteCount + + (totalHits - difficultyAttributes.speedNoteCount) * 0.1 + + // Assume worst case: all mistakes were on speed notes + val relevantCountMiss = min(countMiss.toDouble(), speedNoteCount) + val relevantCountMeh = min(countMeh.toDouble(), speedNoteCount - relevantCountMiss) + val relevantCountOk = min(countOk.toDouble(), speedNoteCount - relevantCountMiss - relevantCountMeh) + val relevantCountGreat = max(0.0, speedNoteCount - relevantCountMiss - relevantCountMeh - relevantCountOk) + + return calculateDeviation(relevantCountGreat, relevantCountOk, relevantCountMeh, relevantCountMiss) + } + + /** + * Estimates the player's tap deviation based on the OD, given number of greats, oks, mehs and misses, + * assuming the player's mean hit error is 0. The estimation is consistent in that two SS scores on the + * same map with the same settings will always return the same deviation. + * + * Misses are ignored because they are usually due to misaiming. + * + * Greats and oks are assumed to follow a normal distribution, whereas mehs are assumed to follow a uniform distribution. + */ + private fun calculateDeviation(relevantCountGreat: Double, relevantCountOk: Double, relevantCountMeh: Double, relevantCountMiss: Double): Double { + if (relevantCountGreat + relevantCountOk + relevantCountMeh <= 0) { + return Double.POSITIVE_INFINITY + } + + val objectCount = relevantCountGreat + relevantCountOk + relevantCountMeh + relevantCountMiss + + // Obtain the great, ok, and meh windows. + val hitWindow = StandardHitWindow( + StandardHitWindow.hitWindow300ToOverallDifficulty( + // Convert current OD to non clock rate-adjusted OD. + StandardHitWindow(difficultyAttributes.overallDifficulty.toFloat()).greatWindow * + difficultyAttributes.clockRate.toFloat() + ) + ) + + val greatWindow = hitWindow.greatWindow + val okWindow = hitWindow.okWindow + val mehWindow = hitWindow.mehWindow + + // The probability that a player hits a circle is unknown, but we can estimate it to be + // the number of greats on circles divided by the number of circles, and then add one + // to the number of circles as a bias correction. + val n = max(1.0, objectCount - relevantCountMiss - relevantCountMeh) + + // 99% critical value for the normal distribution (one-tailed). + val z = 2.32634787404 + + // Proportion of greats hit on circles, ignoring misses and 50s. + val p = relevantCountGreat / n + + // We can be 99% confident that p is at least this value. + val pLowerBound = (n * p + z * z / 2) / (n + z * z) - z / (n + z * z) * sqrt(n * p * (1 - p) + z * z / 4) + + // Compute the deviation assuming greats and oks are normally distributed, and mehs are uniformly distributed. + // Begin with greats and oks first. Ignoring mehs, we can be 99% confident that the deviation is not higher than: + var deviation = greatWindow / (sqrt(2.0) * ErrorFunction.erfInv(pLowerBound)) + + val randomValue = sqrt(2 / Math.PI) * okWindow * exp(-0.5 * (okWindow / deviation).pow(2)) / + (deviation * ErrorFunction.erf(okWindow / (sqrt(2.0) * deviation))) + + deviation *= sqrt(1 - randomValue) + + // Value deviation approach as greatCount approaches 0 + val limitValue = okWindow / sqrt(3.0) + + // If precision is not enough to compute true deviation - use limit value + if (pLowerBound == 0.0 || randomValue >= 1 || deviation > limitValue) { + deviation = limitValue + } + + // Then compute the variance for mehs. + val mehVariance = (mehWindow.pow(2) + okWindow * mehWindow + okWindow.pow(2)) / 3 + + // Find the total deviation. + deviation = sqrt( + ((relevantCountGreat + relevantCountOk) * deviation.pow(2) + relevantCountMeh * mehVariance) / (relevantCountGreat + relevantCountOk + relevantCountMeh) + ) + + return deviation + } + + /** + * Calculates multiplier for speed to account for improper tapping based on the deviation and speed difficulty. + * + * [Graph](https://www.desmos.com/calculator/dmogdhzofn) + */ + private fun calculateSpeedHighDeviationNerf(): Double { + if (speedDeviation == Double.POSITIVE_INFINITY) { + return 0.0 + } + + val speedValue = baseValue(difficultyAttributes.speedDifficulty) + + // Decide a point where the PP value achieved compared to the speed deviation is assumed to be tapped + // improperly. Any PP above this point is considered "excess" speed difficulty. This is used to cause + // PP above the cutoff to scale logarithmically towards the original speed value thus nerfing the value. + val excessSpeedDifficultyCutoff = 100 + 220 * (22 / speedDeviation).pow(6.5) + + if (speedValue <= excessSpeedDifficultyCutoff) { + return 1.0 + } + + val scale = 50 + val adjustedSpeedValue = scale * (ln((speedValue - excessSpeedDifficultyCutoff) / scale + 1) + excessSpeedDifficultyCutoff / scale) + + // 220 UR and less are considered tapped correctly to ensure that normal scores will be punished as little as possible + val t = 1 - Interpolation.reverseLinear(speedDeviation, 22.0, 27.0) + + return Interpolation.linear(adjustedSpeedValue, speedValue, t) / speedValue + } + + private fun baseValue(rating: Double) = (5 * max(1.0, rating / 0.0675) - 4).pow(3) / 100000 + // Miss penalty assumes that a player will miss on the hardest parts of a map, // so we use the amount of relatively difficult sections to adjust miss penalty // to make it more punishing on maps with lower amount of hard sections. diff --git a/src/com/rian/osu/difficulty/evaluators/StandardAimEvaluator.kt b/src/com/rian/osu/difficulty/evaluators/StandardAimEvaluator.kt index 2a5bb11cf..99ab76d46 100644 --- a/src/com/rian/osu/difficulty/evaluators/StandardAimEvaluator.kt +++ b/src/com/rian/osu/difficulty/evaluators/StandardAimEvaluator.kt @@ -2,7 +2,11 @@ package com.rian.osu.difficulty.evaluators import com.rian.osu.beatmap.hitobject.Slider import com.rian.osu.beatmap.hitobject.Spinner +import com.rian.osu.difficulty.DifficultyHitObject import com.rian.osu.difficulty.StandardDifficultyHitObject +import com.rian.osu.difficulty.utils.DifficultyCalculationUtils +import com.rian.osu.math.Interpolation +import com.rian.osu.math.toRadians import kotlin.math.* /** @@ -10,9 +14,10 @@ import kotlin.math.* */ object StandardAimEvaluator { private const val WIDE_ANGLE_MULTIPLIER = 1.5 - private const val ACUTE_ANGLE_MULTIPLIER = 1.95 + private const val ACUTE_ANGLE_MULTIPLIER = 2.6 private const val SLIDER_MULTIPLIER = 1.35 private const val VELOCITY_CHANGE_MULTIPLIER = 0.75 + private const val WIGGLE_MULTIPLIER = 1.02 /** * Evaluates the difficulty of aiming the current object, based on: @@ -34,6 +39,9 @@ object StandardAimEvaluator { val last = current.previous(0)!! val lastLast = current.previous(1)!! + val radius = DifficultyHitObject.NORMALIZED_RADIUS + val diameter = DifficultyHitObject.NORMALIZED_DIAMETER + // Calculate the velocity to the current hit object, which starts with a base distance / time assuming the last object is a circle. var currentVelocity = current.lazyJumpDistance / current.strainTime @@ -62,6 +70,7 @@ object StandardAimEvaluator { var acuteAngleBonus = 0.0 var sliderBonus = 0.0 var velocityChangeBonus = 0.0 + var wiggleBonus = 0.0 // Start strain with regular velocity. var strain = currentVelocity @@ -69,31 +78,38 @@ object StandardAimEvaluator { if ( // If rhythms are the same. max(current.strainTime, last.strainTime) < 1.25 * min(current.strainTime, last.strainTime) && - current.angle != null && last.angle != null && lastLast.angle != null + current.angle != null && last.angle != null ) { val currentAngle = current.angle!! val lastAngle = last.angle!! - val lastLastAngle = lastLast.angle!! // Rewarding angles, take the smaller velocity as base. val angleBonus = min(currentVelocity, prevVelocity) wideAngleBonus = calculateWideAngleBonus(currentAngle) acuteAngleBonus = calculateAcuteAngleBonus(currentAngle) - // Only buff deltaTime exceeding 300 BPM 1/2. - if (current.strainTime > 100) { - acuteAngleBonus = 0.0 - } else { - acuteAngleBonus *= - calculateAcuteAngleBonus(lastAngle) * min(angleBonus, 125 / current.strainTime) * - sin(Math.PI / 2 * min(1.0, (100 - current.strainTime) / 25)).pow(2.0) * - sin(Math.PI / 2 * current.lazyJumpDistance.coerceIn(50.0, 100.0) - 50 / 50).pow(2.0) - } - - // Penalize wide angles if they're repeated, reducing the penalty as last.angle gets more acute. - wideAngleBonus *= angleBonus * (1 - min(wideAngleBonus, calculateWideAngleBonus(lastAngle).pow(3.0))) - // Penalize acute angles if they're repeated, reducing the penalty as lastLast.angle gets more obtuse. - acuteAngleBonus *= 0.5 + 0.5 * (1 - min(acuteAngleBonus, calculateAcuteAngleBonus(lastLastAngle).pow(3.0))) + // Penalize angle repetition. + wideAngleBonus *= 1 - min(wideAngleBonus, calculateWideAngleBonus(lastAngle).pow(3)) + acuteAngleBonus *= 0.08 + 0.92 * (1 - min(acuteAngleBonus, calculateAcuteAngleBonus(lastAngle).pow(3))) + + // Apply full wide angle bonus for distance more than one diameter. + wideAngleBonus *= angleBonus * DifficultyCalculationUtils.smootherstep(current.lazyJumpDistance, 0.0, diameter.toDouble()) + + // Apply acute angle bonus for BPM above 300 1/2 and distance more than one diameter + acuteAngleBonus *= + angleBonus * + DifficultyCalculationUtils.smootherstep(DifficultyCalculationUtils.millisecondsToBPM(current.strainTime, 2), 300.0, 400.0) * + DifficultyCalculationUtils.smootherstep(current.lazyJumpDistance, diameter.toDouble(), diameter * 2.0) + + // Apply wiggle bonus for jumps that are [radius, 3*diameter] in distance, with < 110 angle + // https://www.desmos.com/calculator/dp0v0nvowc + wiggleBonus = angleBonus * + DifficultyCalculationUtils.smootherstep(current.lazyJumpDistance, radius.toDouble(), diameter.toDouble()) * + Interpolation.reverseLinear(current.lazyJumpDistance, diameter * 3.0, diameter.toDouble()).pow(1.8) * + DifficultyCalculationUtils.smootherstep(currentAngle, 110.0.toRadians(), 60.0.toRadians()) * + DifficultyCalculationUtils.smootherstep(last.lazyJumpDistance, radius.toDouble(), diameter.toDouble()) * + Interpolation.reverseLinear(last.lazyJumpDistance, diameter * 3.0, diameter.toDouble()).pow(1.8) * + DifficultyCalculationUtils.smootherstep(lastAngle, 110.0.toRadians(), 60.0.toRadians()) } if (max(prevVelocity, currentVelocity) != 0.0) { @@ -121,6 +137,8 @@ object StandardAimEvaluator { sliderBonus = last.travelDistance / last.travelTime } + strain += wiggleBonus * WIGGLE_MULTIPLIER + // Add in acute angle bonus or wide angle bonus + velocity change bonus, whichever is larger. strain += max( acuteAngleBonus * ACUTE_ANGLE_MULTIPLIER, @@ -139,10 +157,11 @@ object StandardAimEvaluator { * Calculates the bonus of wide angles. */ private fun calculateWideAngleBonus(angle: Double) = - sin(3.0 / 4 * (angle.coerceIn(Math.PI / 6, 5.0 / 6 * Math.PI) - Math.PI / 6)).pow(2.0) + DifficultyCalculationUtils.smoothstep(angle, 40.0.toRadians(), 140.0.toRadians()) /** * Calculates the bonus of acute angles. */ - private fun calculateAcuteAngleBonus(angle: Double) = 1 - calculateWideAngleBonus(angle) + private fun calculateAcuteAngleBonus(angle: Double) = + DifficultyCalculationUtils.smoothstep(angle, 140.0.toRadians(), 40.0.toRadians()) } diff --git a/src/com/rian/osu/difficulty/evaluators/StandardFlashlightEvaluator.kt b/src/com/rian/osu/difficulty/evaluators/StandardFlashlightEvaluator.kt index fe959b4fa..272414a19 100644 --- a/src/com/rian/osu/difficulty/evaluators/StandardFlashlightEvaluator.kt +++ b/src/com/rian/osu/difficulty/evaluators/StandardFlashlightEvaluator.kt @@ -41,13 +41,13 @@ object StandardFlashlightEvaluator { for (i in 0 until min(current.index, 10)) { val currentObject = current.previous(i)!! + cumulativeStrainTime += last.strainTime + // Exclude overlapping objects that can be tapped at once. if (currentObject.obj !is Spinner) { val jumpDistance = current.obj.difficultyStackedPosition .getDistance(currentObject.obj.endPosition) - cumulativeStrainTime += last.strainTime - // We want to nerf objects that can be easily seen within the Flashlight circle radius. if (i == 0) { smallDistNerf = min(1.0, jumpDistance / 75.0) diff --git a/src/com/rian/osu/difficulty/evaluators/StandardSpeedEvaluator.kt b/src/com/rian/osu/difficulty/evaluators/StandardSpeedEvaluator.kt index 03a0b067f..d524ba9cb 100644 --- a/src/com/rian/osu/difficulty/evaluators/StandardSpeedEvaluator.kt +++ b/src/com/rian/osu/difficulty/evaluators/StandardSpeedEvaluator.kt @@ -2,6 +2,8 @@ package com.rian.osu.difficulty.evaluators import com.rian.osu.beatmap.hitobject.Spinner import com.rian.osu.difficulty.StandardDifficultyHitObject +import com.rian.osu.mods.Mod +import com.rian.osu.mods.ModAutopilot import kotlin.math.min import kotlin.math.pow @@ -11,7 +13,7 @@ import kotlin.math.pow object StandardSpeedEvaluator { private const val SINGLE_SPACING_THRESHOLD = 125.0 // 1.25 circles distance between centers private const val MIN_SPEED_BONUS = 75.0 // 200 1/4 BPM - private const val DISTANCE_MULTIPLIER = 0.94 + private const val DISTANCE_MULTIPLIER = 0.9 /** * Evaluates the difficulty of tapping the current object, based on: @@ -22,7 +24,7 @@ object StandardSpeedEvaluator { * * @param current The current object. */ - fun evaluateDifficultyOf(current: StandardDifficultyHitObject): Double { + fun evaluateDifficultyOf(current: StandardDifficultyHitObject, mods: List): Double { if (current.obj is Spinner) { return 0.0 } @@ -51,7 +53,9 @@ object StandardSpeedEvaluator { val distance = min(SINGLE_SPACING_THRESHOLD, travelDistance + current.minimumJumpDistance) // Max distance bonus is 1 * `distance_multiplier` at single_spacing_threshold - val distanceBonus = (distance / SINGLE_SPACING_THRESHOLD).pow(3.95) * DISTANCE_MULTIPLIER + val distanceBonus = + if (mods.any { it is ModAutopilot }) 0.0 + else (distance / SINGLE_SPACING_THRESHOLD).pow(3.95) * DISTANCE_MULTIPLIER // Base difficulty with all bonuses val difficulty = (1 + speedBonus + distanceBonus) * 1000 / strainTime diff --git a/src/com/rian/osu/difficulty/skills/StandardAim.kt b/src/com/rian/osu/difficulty/skills/StandardAim.kt index fe4d1ecaa..e0e9d9715 100644 --- a/src/com/rian/osu/difficulty/skills/StandardAim.kt +++ b/src/com/rian/osu/difficulty/skills/StandardAim.kt @@ -1,8 +1,10 @@ package com.rian.osu.difficulty.skills +import com.rian.osu.beatmap.hitobject.Slider import com.rian.osu.difficulty.StandardDifficultyHitObject import com.rian.osu.difficulty.evaluators.StandardAimEvaluator.evaluateDifficultyOf import com.rian.osu.mods.Mod +import kotlin.math.exp import kotlin.math.pow /** @@ -21,13 +23,39 @@ class StandardAim( val withSliders: Boolean ) : StandardStrainSkill(mods) { private var currentStrain = 0.0 - private val skillMultiplier = 25.18 + private val skillMultiplier = 25.6 private val strainDecayBase = 0.15 + private val sliderStrains = mutableListOf() + + /** + * Obtains the amount of sliders that are considered difficult in terms of relative strain. + */ + fun countDifficultSliders(): Double { + if (sliderStrains.isEmpty()) { + return 0.0 + } + + val sortedStrains = sliderStrains.sortedDescending() + val maxStrain = sortedStrains[0] + + if (maxStrain == 0.0) { + return 0.0 + } + + return sortedStrains.fold(0.0) { total, strain -> + total + 1 / (1 + exp(-((strain / maxStrain) * 12 - 6))) + } + } + override fun strainValueAt(current: StandardDifficultyHitObject): Double { currentStrain *= strainDecay(current.deltaTime) currentStrain += evaluateDifficultyOf(current, withSliders) * skillMultiplier + if (current.obj is Slider) { + sliderStrains.add(currentStrain) + } + objectStrains.add(currentStrain) return currentStrain } diff --git a/src/com/rian/osu/difficulty/skills/StandardSpeed.kt b/src/com/rian/osu/difficulty/skills/StandardSpeed.kt index b4f59e836..4aaffb8b4 100644 --- a/src/com/rian/osu/difficulty/skills/StandardSpeed.kt +++ b/src/com/rian/osu/difficulty/skills/StandardSpeed.kt @@ -20,7 +20,7 @@ class StandardSpeed( private var currentStrain = 0.0 private var currentRhythm = 0.0 - private val skillMultiplier = 1.43 + private val skillMultiplier = 1.46 private val strainDecayBase = 0.3 /** @@ -41,7 +41,7 @@ class StandardSpeed( override fun strainValueAt(current: StandardDifficultyHitObject): Double { currentStrain *= strainDecay(current.strainTime) - currentStrain += StandardSpeedEvaluator.evaluateDifficultyOf(current) * skillMultiplier + currentStrain += StandardSpeedEvaluator.evaluateDifficultyOf(current, mods) * skillMultiplier currentRhythm = StandardRhythmEvaluator.evaluateDifficultyOf(current) val totalStrain = currentStrain * currentRhythm From b5bc690140c4607f7723b8cd489e7b8d9a28052e Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:27:59 +0800 Subject: [PATCH 84/87] Resolve... weird "syntax" error? See https://github.com/osudroid/osu-droid/actions/runs/12964055528/job/36162349793?pr=475. --- src/com/reco1l/andengine/text/ExtendedText.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/com/reco1l/andengine/text/ExtendedText.kt b/src/com/reco1l/andengine/text/ExtendedText.kt index 713feddfd..e899652a3 100644 --- a/src/com/reco1l/andengine/text/ExtendedText.kt +++ b/src/com/reco1l/andengine/text/ExtendedText.kt @@ -30,10 +30,12 @@ open class ExtendedText : ExtendedEntity() { set(value) { if (field != value) { field = value + if (value.length > maximumSize) { shouldRebuildVertexBuffer = true shouldRebuildTextureBuffer = true } + updateVertexBuffer() } } From e2e3605fadf4fae9cf1f119fb7f4c0ba4e573d06 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 18:35:17 +0800 Subject: [PATCH 85/87] Reformat line to fix weird indentation issue --- src/com/reco1l/andengine/text/ExtendedText.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/com/reco1l/andengine/text/ExtendedText.kt b/src/com/reco1l/andengine/text/ExtendedText.kt index e899652a3..7c42670cf 100644 --- a/src/com/reco1l/andengine/text/ExtendedText.kt +++ b/src/com/reco1l/andengine/text/ExtendedText.kt @@ -29,7 +29,7 @@ open class ExtendedText : ExtendedEntity() { var text: String = "" set(value) { if (field != value) { - field = value + field = value if (value.length > maximumSize) { shouldRebuildVertexBuffer = true @@ -37,7 +37,7 @@ open class ExtendedText : ExtendedEntity() { } updateVertexBuffer() - } + } } /** @@ -47,17 +47,17 @@ open class ExtendedText : ExtendedEntity() { var font: Font? = null set(value) { if (field != value) { - field = value + field = value shouldRebuildTextureBuffer = true updateVertexBuffer() - } + } } /** * The horizontal alignment of the text. */ var horizontalAlign = HorizontalAlign.LEFT - set(value) { + set(value) { if (field != value) { field = value updateVertexBuffer() @@ -108,8 +108,8 @@ open class ExtendedText : ExtendedEntity() { currentSize = text.length if (text.length > maximumSize) { - shouldRebuildVertexBuffer = true - shouldRebuildTextureBuffer = true + shouldRebuildVertexBuffer = true + shouldRebuildTextureBuffer = true maximumSize = text.length } From 3ce2c138c901215c6720099a0660ea049746d65e Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 19:19:03 +0800 Subject: [PATCH 86/87] Make `invalidateTransformations` `protected` --- src/com/reco1l/andengine/ExtendedEntity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/ExtendedEntity.kt b/src/com/reco1l/andengine/ExtendedEntity.kt index c7ea26f96..1575dac2d 100644 --- a/src/com/reco1l/andengine/ExtendedEntity.kt +++ b/src/com/reco1l/andengine/ExtendedEntity.kt @@ -367,7 +367,7 @@ abstract class ExtendedEntity( } } - open fun invalidateTransformations() { + protected open fun invalidateTransformations() { mLocalToParentTransformationDirty = true mParentToLocalTransformationDirty = true } From 7c1e3cf6a90ed70d4ebcc1e9fedd7cf95065b395 Mon Sep 17 00:00:00 2001 From: Rian8337 <52914632+Rian8337@users.noreply.github.com> Date: Sat, 25 Jan 2025 19:43:23 +0800 Subject: [PATCH 87/87] Update `GradientCircle` KDoc --- src/com/reco1l/andengine/shape/GradientCircle.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/com/reco1l/andengine/shape/GradientCircle.kt b/src/com/reco1l/andengine/shape/GradientCircle.kt index 2a8c66f82..388651741 100644 --- a/src/com/reco1l/andengine/shape/GradientCircle.kt +++ b/src/com/reco1l/andengine/shape/GradientCircle.kt @@ -10,7 +10,9 @@ import javax.microedition.khronos.opengles.* /** - * A circle shape. + * A circle that supports gradients. + * + * The gradient is applied from [startAngle] to [endAngle]. * * @author Reco1l */