1818package com.lambda.module.modules.movement
1919
2020import com.lambda.config.groups.RotationSettings
21+ import com.lambda.context.SafeContext
2122import com.lambda.event.events.TickEvent
2223import com.lambda.event.listener.SafeListener.Companion.listen
2324import com.lambda.interaction.managers.rotating.Rotation
@@ -29,15 +30,17 @@ import com.lambda.threading.runSafe
2930import com.lambda.util.Communication.info
3031import com.lambda.util.NamedEnum
3132import com.lambda.util.SpeedUnit
32- import com.lambda.util.Timer
3333import com.lambda.util.world.fastEntitySearch
3434import net.minecraft.client.network.ClientPlayerEntity
35+ import net.minecraft.client.world.ClientWorld
3536import net.minecraft.entity.projectile.FireworkRocketEntity
3637import net.minecraft.text.Text.literal
38+ import net.minecraft.util.math.ChunkPos
3739import net.minecraft.util.math.Vec3d
3840import kotlin.time.Duration.Companion.seconds
3941import kotlin.time.TimeSource
4042
43+
4144object ElytraAltitudeControl : Module(
4245 name = " ElytraAttitudeControl" ,
4346 description = " Automatically control attitude or speed while elytra flying" ,
@@ -80,6 +83,10 @@ object ElytraAltitudeControl : Module(
8083 val pitch40SpeedThreshold by setting(" Speed Threshold" , 41f , 10f .. 100f , .5f , description = " Speed at which to start pitching up" ) { usePitch40OnHeight }.group(Group .Pitch40Control )
8184 val pitch40UseFireworkOnUpTrajectory by setting(" Use Firework On Up Trajectory" , false , " Use fireworks when converting speed to altitude in the Pitch 40 maneuver" ) { usePitch40OnHeight }.group(Group .Pitch40Control )
8285
86+ val useTimerOnChunkLoad by setting(" Use Timer On Slow Chunk Loading" , false , " Slows down the game when chunks load slow to keep momentum" ).group(Group .TimerControls )
87+ val timerMinChunkDistance by setting(" Min Chunk Distance" , 4 , 1 .. 20 , 1 , " Min unloaded chunk distance to start timer effect" , unit = " chunks" ) { useTimerOnChunkLoad }.group(Group .TimerControls )
88+ val timerReturnValue by setting(" Timer Return Value" , 1.0f , 0.0f .. 1.0f , 0.05f , description = " Timer speed to return when above min chunk distance" ) { useTimerOnChunkLoad }.group(Group .TimerControls )
89+
8390 override val rotationConfig = RotationSettings (this , Group .Rotation )
8491
8592 var controlState = ControlState .AttitudeControl
@@ -88,93 +95,18 @@ object ElytraAltitudeControl : Module(
8895 var lastCycleFinish = TimeSource .Monotonic .markNow()
8996 var lastY = 0.0
9097
91- val usageDelay = Timer ()
98+ val usageDelay = com.lambda.util. Timer ()
9299
93100 init {
94101 listen<TickEvent .Pre > {
95- if (! player.isGliding) return @listen
96- run {
102+ if (player.isGliding) {
97103 when (controlState) {
98- ControlState .AttitudeControl -> {
99- if (disableOnFirework && player.hasFirework) {
100- return @run
101- }
102- if (usePitch40OnHeight) {
103- if (player.y < minHeightForPitch40) {
104- controlState = ControlState .Pitch40Fly
105- lastY = player.pos.y
106- return @run
107- }
108- }
109- val outputPitch = when (controlValue) {
110- Mode .Speed -> {
111- speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
112- }
113- Mode .Altitude -> {
114- - 1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
115- }
116- }.coerceIn(- maxPitchAngle, maxPitchAngle)
117- RotationRequest (Rotation (player.yaw, outputPitch.toFloat()), this @ElytraAltitudeControl).submit()
118-
119- if (usageDelay.timePassed(2 .seconds) && ! player.hasFirework) {
120- if (useFireworkOnHeight && minHeight > player.y) {
121- usageDelay.reset()
122- runSafe {
123- startFirework(true )
124- }
125- }
126- if (useFireworkOnSpeed && minSpeed > player.flySpeed()) {
127- usageDelay.reset()
128- runSafe {
129- startFirework(true )
130- }
131- }
132- }
133- }
134- ControlState .Pitch40Fly -> when (state) {
135- Pitch40State .GainSpeed -> {
136- RotationRequest (Rotation (player.yaw, pitch40DownAngle), this @ElytraAltitudeControl).submit()
137- if (player.flySpeed() > pitch40SpeedThreshold) {
138- state = Pitch40State .PitchUp
139- }
140- }
141- Pitch40State .PitchUp -> {
142- lastAngle - = 5f
143- RotationRequest (Rotation (player.yaw, lastAngle), this @ElytraAltitudeControl).submit()
144- if (lastAngle <= pitch40UpStartAngle) {
145- state = Pitch40State .FlyUp
146- if (pitch40UseFireworkOnUpTrajectory) {
147- runSafe {
148- startFirework(true )
149- }
150- }
151- }
152- }
153- Pitch40State .FlyUp -> {
154- lastAngle + = pitch40AngleChangeRate
155- RotationRequest (Rotation (player.yaw, lastAngle), this @ElytraAltitudeControl).submit()
156- if (lastAngle >= 0f ) {
157- state = Pitch40State .GainSpeed
158- if (logHeightGain) {
159- var timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
160- var heightDelta = player.pos.y - lastY
161- var heightPerMinute = (heightDelta) / (timeDelta / 1000.0 ) * 60.0
162- info(literal(" Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)" .format(heightDelta, timeDelta / 1000.0 , heightPerMinute)))
163- }
164-
165- lastCycleFinish = TimeSource .Monotonic .markNow()
166- lastY = player.pos.y
167- if (pitch40ExitHeight < player.y) {
168- controlState = ControlState .AttitudeControl
169- speedController.reset()
170- altitudeController.reset()
171- }
172- }
173- }
174- }
104+ ControlState .AttitudeControl -> updateAltitudeControls()
105+ ControlState .Pitch40Fly -> updatePitch40Controls()
175106 }
107+ updateTimerUsage()
108+ lastPos = player.pos
176109 }
177- lastPos = player.pos
178110 }
179111
180112 onEnable {
@@ -185,11 +117,146 @@ object ElytraAltitudeControl : Module(
185117 controlState = ControlState .AttitudeControl
186118 lastAngle = pitch40UpStartAngle
187119 }
120+
121+ onDisable {
122+ if (useTimerOnChunkLoad) {
123+ Timer .timer = timerReturnValue.toDouble()
124+ }
125+ }
188126 }
189127
190- val ClientPlayerEntity .hasFirework: Boolean
128+ private fun SafeContext.updateAltitudeControls () {
129+ if (disableOnFirework && hasFirework) {
130+ return
131+ }
132+ if (usePitch40OnHeight) {
133+ if (player.y < minHeightForPitch40) {
134+ controlState = ControlState .Pitch40Fly
135+ lastY = player.pos.y
136+ return
137+ }
138+ }
139+ val outputPitch = when (controlValue) {
140+ Mode .Speed -> {
141+ speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
142+ }
143+ Mode .Altitude -> {
144+ - 1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
145+ }
146+ }.coerceIn(- maxPitchAngle, maxPitchAngle)
147+ RotationRequest (Rotation (player.yaw, outputPitch.toFloat()), this @ElytraAltitudeControl).submit()
148+
149+ if (usageDelay.timePassed(2 .seconds) && ! hasFirework) {
150+ if (useFireworkOnHeight && minHeight > player.y) {
151+ usageDelay.reset()
152+ runSafe {
153+ startFirework(true )
154+ }
155+ }
156+ if (useFireworkOnSpeed && minSpeed > player.flySpeed()) {
157+ usageDelay.reset()
158+ runSafe {
159+ startFirework(true )
160+ }
161+ }
162+ }
163+ }
164+
165+ private fun SafeContext.updatePitch40Controls () {
166+ when (state) {
167+ Pitch40State .GainSpeed -> {
168+ RotationRequest (Rotation (player.yaw, pitch40DownAngle), this @ElytraAltitudeControl).submit()
169+ if (player.flySpeed() > pitch40SpeedThreshold) {
170+ state = Pitch40State .PitchUp
171+ }
172+ }
173+ Pitch40State .PitchUp -> {
174+ lastAngle - = 5f
175+ RotationRequest (Rotation (player.yaw, lastAngle), this @ElytraAltitudeControl).submit()
176+ if (lastAngle <= pitch40UpStartAngle) {
177+ state = Pitch40State .FlyUp
178+ if (pitch40UseFireworkOnUpTrajectory) {
179+ runSafe {
180+ startFirework(true )
181+ }
182+ }
183+ }
184+ }
185+ Pitch40State .FlyUp -> {
186+ lastAngle + = pitch40AngleChangeRate
187+ RotationRequest (Rotation (player.yaw, lastAngle), this @ElytraAltitudeControl).submit()
188+ if (lastAngle >= 0f ) {
189+ state = Pitch40State .GainSpeed
190+ if (logHeightGain) {
191+ val timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
192+ val heightDelta = player.pos.y - lastY
193+ val heightPerMinute = (heightDelta) / (timeDelta / 1000.0 ) * 60.0
194+ info(literal(" Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)" .format(heightDelta, timeDelta / 1000.0 , heightPerMinute)))
195+ }
196+
197+ lastCycleFinish = TimeSource .Monotonic .markNow()
198+ lastY = player.pos.y
199+ if (pitch40ExitHeight < player.y) {
200+ controlState = ControlState .AttitudeControl
201+ speedController.reset()
202+ altitudeController.reset()
203+ }
204+ }
205+ }
206+ }
207+ }
208+
209+ private fun SafeContext.updateTimerUsage () {
210+ if (useTimerOnChunkLoad) {
211+ val nearestChunkDistance = getNearestUnloadedChunkDistance()
212+ if (nearestChunkDistance != - 1 && nearestChunkDistance / 16.0 <= timerMinChunkDistance) {
213+ val speedFactor = 0.1f + (nearestChunkDistance.toFloat() / timerMinChunkDistance.toFloat() * 16.0 ) * 0.9f
214+ Timer .enable()
215+ Timer .timer = speedFactor.coerceIn(0.1 , 1.0 )
216+ } else {
217+ if (Timer .isEnabled) {
218+ Timer .timer = timerReturnValue.toDouble()
219+ }
220+ }
221+ }
222+ }
223+
224+ val hasFirework: Boolean
191225 get() = runSafe { return fastEntitySearch<FireworkRocketEntity >(4.0 ) { it.shooter == this .player }.any() } ? : false
192226
227+ private fun SafeContext.getNearestUnloadedChunkDistance (): Int {
228+ val nearestChunk: ChunkPos ? = nearestUnloadedChunk(world, player)
229+ return if (nearestChunk != null ) distanceToChunk(nearestChunk, player).toInt() else - 1
230+ }
231+
232+ fun nearestUnloadedChunk (world : ClientWorld , player : ClientPlayerEntity ): ChunkPos ? {
233+ val scanRangeInt = 25
234+ var nearestChunk: ChunkPos ? = null
235+ var nearestDistance = Double .MAX_VALUE
236+ val playerChunk = player.chunkPos
237+
238+ for (x in - scanRangeInt.. < scanRangeInt) {
239+ for (z in - scanRangeInt.. < scanRangeInt) {
240+ val chunkPos = ChunkPos (playerChunk.x + x, playerChunk.z + z)
241+ if (world.chunkManager.isChunkLoaded(chunkPos.x, chunkPos.z)) {
242+ continue
243+ }
244+ val distance = distanceToChunk(chunkPos, player).toDouble()
245+ if (distance < nearestDistance) {
246+ nearestDistance = distance
247+ nearestChunk = chunkPos
248+ }
249+ }
250+ }
251+ return nearestChunk
252+ }
253+
254+ fun distanceToChunk (chunkPos : ChunkPos , player : ClientPlayerEntity ): Float {
255+ val playerPos = player.getPos()
256+ val chunkCenter = Vec3d ((chunkPos.startX + 8 ).toDouble(), playerPos.y, (chunkPos.startZ + 8 ).toDouble())
257+ return playerPos.distanceTo(chunkCenter).toFloat()
258+ }
259+
193260 class PIController (val valueP : () -> Double , val valueD : () -> Double , val valueI : () -> Double , val constant : () -> Double ) {
194261 var accumulator = 0.0 // Integral term accumulator
195262 var lastDiff = 0.0
@@ -234,7 +301,8 @@ object ElytraAltitudeControl : Module(
234301 SpeedControl (" Speed Control" ),
235302 AltitudeControl (" Altitude Control" ),
236303 Pitch40Control (" Pitch 40 Control" ),
237- Rotation (" Rotation" )
304+ Rotation (" Rotation" ),
305+ TimerControls (" Timer Controls" ),
238306 }
239307
240308 enum class Pitch40State {
0 commit comments