1717
1818package com.lambda.module.modules.movement
1919
20+ import com.lambda.config.groups.RotationSettings
21+ import com.lambda.context.SafeContext
2022import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig
2123import com.lambda.config.applyEdits
2224import com.lambda.event.events.TickEvent
@@ -29,15 +31,17 @@ import com.lambda.threading.runSafe
2931import com.lambda.util.Communication.info
3032import com.lambda.util.NamedEnum
3133import com.lambda.util.SpeedUnit
32- import com.lambda.util.Timer
3334import com.lambda.util.world.fastEntitySearch
3435import net.minecraft.client.network.ClientPlayerEntity
36+ import net.minecraft.client.world.ClientWorld
3537import net.minecraft.entity.projectile.FireworkRocketEntity
3638import net.minecraft.text.Text.literal
39+ import net.minecraft.util.math.ChunkPos
3740import net.minecraft.util.math.Vec3d
3841import kotlin.time.Duration.Companion.seconds
3942import kotlin.time.TimeSource
4043
44+
4145object ElytraAltitudeControl : Module(
4246 name = " ElytraAttitudeControl" ,
4347 description = " Automatically control attitude or speed while elytra flying" ,
@@ -80,13 +84,19 @@ object ElytraAltitudeControl : Module(
8084 val pitch40SpeedThreshold by setting(" Speed Threshold" , 41f , 10f .. 100f , .5f , description = " Speed at which to start pitching up" ) { usePitch40OnHeight }.group(Group .Pitch40Control )
8185 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 )
8286
87+ 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 )
88+ 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 )
89+ 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 )
90+
91+ override val rotationConfig = RotationSettings (this , Group .Rotation )
92+
8393 var controlState = ControlState .AttitudeControl
8494 var state = Pitch40State .GainSpeed
8595 var lastAngle = pitch40UpStartAngle
8696 var lastCycleFinish = TimeSource .Monotonic .markNow()
8797 var lastY = 0.0
8898
89- val usageDelay = Timer ()
99+ val usageDelay = com.lambda.util. Timer ()
90100
91101 init {
92102 setDefaultAutomationConfig {
@@ -96,89 +106,14 @@ object ElytraAltitudeControl : Module(
96106 }
97107
98108 listen<TickEvent .Pre > {
99- if (! player.isGliding) return @listen
100- run {
109+ if (player.isGliding) {
101110 when (controlState) {
102- ControlState .AttitudeControl -> {
103- if (disableOnFirework && hasFirework) {
104- return @run
105- }
106- if (usePitch40OnHeight) {
107- if (player.y < minHeightForPitch40) {
108- controlState = ControlState .Pitch40Fly
109- lastY = player.pos.y
110- return @run
111- }
112- }
113- val outputPitch = when (controlValue) {
114- Mode .Speed -> {
115- speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
116- }
117- Mode .Altitude -> {
118- - 1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
119- }
120- }.coerceIn(- maxPitchAngle, maxPitchAngle)
121- rotationRequest { pitch(outputPitch) }.submit()
122-
123- if (usageDelay.timePassed(2 .seconds) && ! hasFirework) {
124- if (useFireworkOnHeight && minHeight > player.y) {
125- usageDelay.reset()
126- runSafe {
127- startFirework(true )
128- }
129- }
130- if (useFireworkOnSpeed && minSpeed > player.flySpeed()) {
131- usageDelay.reset()
132- runSafe {
133- startFirework(true )
134- }
135- }
136- }
137- }
138- ControlState .Pitch40Fly -> when (state) {
139- Pitch40State .GainSpeed -> {
140- rotationRequest { pitch(pitch40DownAngle) }.submit()
141- if (player.flySpeed() > pitch40SpeedThreshold) {
142- state = Pitch40State .PitchUp
143- }
144- }
145- Pitch40State .PitchUp -> {
146- lastAngle - = 5f
147- rotationRequest { pitch(lastAngle) }.submit()
148- if (lastAngle <= pitch40UpStartAngle) {
149- state = Pitch40State .FlyUp
150- if (pitch40UseFireworkOnUpTrajectory) {
151- runSafe {
152- startFirework(true )
153- }
154- }
155- }
156- }
157- Pitch40State .FlyUp -> {
158- lastAngle + = pitch40AngleChangeRate
159- rotationRequest { pitch(lastAngle) }.submit()
160- if (lastAngle >= 0f ) {
161- state = Pitch40State .GainSpeed
162- if (logHeightGain) {
163- var timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
164- var heightDelta = player.pos.y - lastY
165- var heightPerMinute = (heightDelta) / (timeDelta / 1000.0 ) * 60.0
166- info(literal(" Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)" .format(heightDelta, timeDelta / 1000.0 , heightPerMinute)))
167- }
168-
169- lastCycleFinish = TimeSource .Monotonic .markNow()
170- lastY = player.pos.y
171- if (pitch40ExitHeight < player.y) {
172- controlState = ControlState .AttitudeControl
173- speedController.reset()
174- altitudeController.reset()
175- }
176- }
177- }
178- }
111+ ControlState .AttitudeControl -> updateAltitudeControls()
112+ ControlState .Pitch40Fly -> updatePitch40Controls()
179113 }
114+ updateTimerUsage()
115+ lastPos = player.pos
180116 }
181- lastPos = player.pos
182117 }
183118
184119 onEnable {
@@ -189,11 +124,146 @@ object ElytraAltitudeControl : Module(
189124 controlState = ControlState .AttitudeControl
190125 lastAngle = pitch40UpStartAngle
191126 }
127+
128+ onDisable {
129+ if (useTimerOnChunkLoad) {
130+ Timer .timer = timerReturnValue.toDouble()
131+ }
132+ }
133+ }
134+
135+ private fun SafeContext.updateAltitudeControls () {
136+ if (disableOnFirework && hasFirework) {
137+ return
138+ }
139+ if (usePitch40OnHeight) {
140+ if (player.y < minHeightForPitch40) {
141+ controlState = ControlState .Pitch40Fly
142+ lastY = player.pos.y
143+ return
144+ }
145+ }
146+ val outputPitch = when (controlValue) {
147+ Mode .Speed -> {
148+ speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
149+ }
150+ Mode .Altitude -> {
151+ - 1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
152+ }
153+ }.coerceIn(- maxPitchAngle, maxPitchAngle)
154+ RotationRequest (Rotation (player.yaw, outputPitch.toFloat()), this @ElytraAltitudeControl).submit()
155+
156+ if (usageDelay.timePassed(2 .seconds) && ! hasFirework) {
157+ if (useFireworkOnHeight && minHeight > player.y) {
158+ usageDelay.reset()
159+ runSafe {
160+ startFirework(true )
161+ }
162+ }
163+ if (useFireworkOnSpeed && minSpeed > player.flySpeed()) {
164+ usageDelay.reset()
165+ runSafe {
166+ startFirework(true )
167+ }
168+ }
169+ }
170+ }
171+
172+ private fun SafeContext.updatePitch40Controls () {
173+ when (state) {
174+ Pitch40State .GainSpeed -> {
175+ rotationRequest { pitch(pitch40DownAngle) }.submit()
176+ if (player.flySpeed() > pitch40SpeedThreshold) {
177+ state = Pitch40State .PitchUp
178+ }
179+ }
180+ Pitch40State .PitchUp -> {
181+ lastAngle - = 5f
182+ rotationRequest { pitch(lastAngle) }.submit()
183+ if (lastAngle <= pitch40UpStartAngle) {
184+ state = Pitch40State .FlyUp
185+ if (pitch40UseFireworkOnUpTrajectory) {
186+ runSafe {
187+ startFirework(true )
188+ }
189+ }
190+ }
191+ }
192+ Pitch40State .FlyUp -> {
193+ lastAngle + = pitch40AngleChangeRate
194+ rotationRequest { pitch(lastAngle) }.submit()
195+ if (lastAngle >= 0f ) {
196+ state = Pitch40State .GainSpeed
197+ if (logHeightGain) {
198+ val timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
199+ val heightDelta = player.pos.y - lastY
200+ val heightPerMinute = (heightDelta) / (timeDelta / 1000.0 ) * 60.0
201+ info(literal(" Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)" .format(heightDelta, timeDelta / 1000.0 , heightPerMinute)))
202+ }
203+
204+ lastCycleFinish = TimeSource .Monotonic .markNow()
205+ lastY = player.pos.y
206+ if (pitch40ExitHeight < player.y) {
207+ controlState = ControlState .AttitudeControl
208+ speedController.reset()
209+ altitudeController.reset()
210+ }
211+ }
212+ }
213+ }
214+ }
215+
216+ private fun SafeContext.updateTimerUsage () {
217+ if (useTimerOnChunkLoad) {
218+ val nearestChunkDistance = getNearestUnloadedChunkDistance()
219+ if (nearestChunkDistance != - 1 && nearestChunkDistance / 16.0 <= timerMinChunkDistance) {
220+ val speedFactor = 0.1f + (nearestChunkDistance.toFloat() / timerMinChunkDistance.toFloat() * 16.0 ) * 0.9f
221+ Timer .enable()
222+ Timer .timer = speedFactor.coerceIn(0.1 , 1.0 )
223+ } else {
224+ if (Timer .isEnabled) {
225+ Timer .timer = timerReturnValue.toDouble()
226+ }
227+ }
228+ }
192229 }
193230
194231 val hasFirework: Boolean
195232 get() = runSafe { return fastEntitySearch<FireworkRocketEntity >(4.0 ) { it.shooter == player }.any() } ? : false
196233
234+ private fun SafeContext.getNearestUnloadedChunkDistance (): Int {
235+ val nearestChunk: ChunkPos ? = nearestUnloadedChunk(world, player)
236+ return if (nearestChunk != null ) distanceToChunk(nearestChunk, player).toInt() else - 1
237+ }
238+
239+ fun nearestUnloadedChunk (world : ClientWorld , player : ClientPlayerEntity ): ChunkPos ? {
240+ val scanRangeInt = 25
241+ var nearestChunk: ChunkPos ? = null
242+ var nearestDistance = Double .MAX_VALUE
243+ val playerChunk = player.chunkPos
244+
245+ for (x in - scanRangeInt.. < scanRangeInt) {
246+ for (z in - scanRangeInt.. < scanRangeInt) {
247+ val chunkPos = ChunkPos (playerChunk.x + x, playerChunk.z + z)
248+ if (world.chunkManager.isChunkLoaded(chunkPos.x, chunkPos.z)) {
249+ continue
250+ }
251+ val distance = distanceToChunk(chunkPos, player).toDouble()
252+ if (distance < nearestDistance) {
253+ nearestDistance = distance
254+ nearestChunk = chunkPos
255+ }
256+ }
257+ }
258+ return nearestChunk
259+ }
260+
261+ fun distanceToChunk (chunkPos : ChunkPos , player : ClientPlayerEntity ): Float {
262+ val playerPos = player.getPos()
263+ val chunkCenter = Vec3d ((chunkPos.startX + 8 ).toDouble(), playerPos.y, (chunkPos.startZ + 8 ).toDouble())
264+ return playerPos.distanceTo(chunkCenter).toFloat()
265+ }
266+
197267 class PIController (val valueP : () -> Double , val valueD : () -> Double , val valueI : () -> Double , val constant : () -> Double ) {
198268 var accumulator = 0.0 // Integral term accumulator
199269 var lastDiff = 0.0
@@ -238,7 +308,8 @@ object ElytraAltitudeControl : Module(
238308 SpeedControl (" Speed Control" ),
239309 AltitudeControl (" Altitude Control" ),
240310 Pitch40Control (" Pitch 40 Control" ),
241- Rotation (" Rotation" )
311+ Rotation (" Rotation" ),
312+ TimerControls (" Timer Controls" ),
242313 }
243314
244315 enum class Pitch40State {
0 commit comments