Skip to content

Commit c07f597

Browse files
committed
feat(ElytraAltitudeControl): Add feature to use timer when chunks near are loading slowly.
1 parent 4bbda8a commit c07f597

File tree

2 files changed

+164
-92
lines changed

2 files changed

+164
-92
lines changed

src/main/kotlin/com/lambda/module/modules/movement/ElytraAltitudeControl.kt

Lines changed: 154 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717

1818
package com.lambda.module.modules.movement
1919

20+
import com.lambda.config.groups.RotationSettings
21+
import com.lambda.context.SafeContext
2022
import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig
2123
import com.lambda.config.applyEdits
2224
import com.lambda.event.events.TickEvent
@@ -29,15 +31,17 @@ import com.lambda.threading.runSafe
2931
import com.lambda.util.Communication.info
3032
import com.lambda.util.NamedEnum
3133
import com.lambda.util.SpeedUnit
32-
import com.lambda.util.Timer
3334
import com.lambda.util.world.fastEntitySearch
3435
import net.minecraft.client.network.ClientPlayerEntity
36+
import net.minecraft.client.world.ClientWorld
3537
import net.minecraft.entity.projectile.FireworkRocketEntity
3638
import net.minecraft.text.Text.literal
39+
import net.minecraft.util.math.ChunkPos
3740
import net.minecraft.util.math.Vec3d
3841
import kotlin.time.Duration.Companion.seconds
3942
import kotlin.time.TimeSource
4043

44+
4145
object 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 {

src/main/kotlin/com/lambda/module/modules/movement/Timer.kt

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,16 @@ import com.lambda.module.Module
2323
import com.lambda.module.tag.ModuleTag
2424

2525
object Timer : Module(
26-
name = "Timer",
27-
description = "Modify client tick speed.",
28-
tag = ModuleTag.MOVEMENT,
26+
name = "Timer",
27+
description = "Modify client tick speed.",
28+
tag = ModuleTag.MOVEMENT,
2929
) {
30-
private val timer by setting("Timer", 1.0, 0.0..10.0, 0.01)
30+
@JvmStatic
31+
var timer by setting("Timer", 1.0, 0.0..10.0, 0.01)
3132

32-
init {
33-
listen<ClientEvent.TimerUpdate> {
34-
it.speed = timer.coerceAtLeast(0.05)
35-
}
36-
}
33+
init {
34+
listen<ClientEvent.TimerUpdate> {
35+
it.speed = timer.coerceAtLeast(0.05)
36+
}
37+
}
3738
}

0 commit comments

Comments
 (0)