Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright 2025 Lambda
* Copyright 2026 Lambda
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
Expand All @@ -17,6 +17,8 @@

package com.lambda.module.modules.movement

import com.lambda.config.groups.RotationSettings
import com.lambda.context.SafeContext
import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig
import com.lambda.config.applyEdits
import com.lambda.event.events.TickEvent
Expand All @@ -25,15 +27,15 @@ import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotat
import com.lambda.module.Module
import com.lambda.module.modules.movement.BetterFirework.startFirework
import com.lambda.module.tag.ModuleTag
import com.lambda.threading.runSafe
import com.lambda.util.Communication.info
import com.lambda.util.NamedEnum
import com.lambda.util.SpeedUnit
import com.lambda.util.Timer
import com.lambda.util.math.distCenter
import com.lambda.util.world.fastEntitySearch
import net.minecraft.client.network.ClientPlayerEntity
import net.minecraft.client.world.ClientWorld
import net.minecraft.entity.projectile.FireworkRocketEntity
import net.minecraft.text.Text.literal
import net.minecraft.util.math.ChunkPos
import net.minecraft.util.math.Vec3d
import kotlin.time.Duration.Companion.seconds
import kotlin.time.TimeSource
Expand Down Expand Up @@ -80,13 +82,22 @@ object ElytraAltitudeControl : Module(
val pitch40SpeedThreshold by setting("Speed Threshold", 41f, 10f..100f, .5f, description = "Speed at which to start pitching up") { usePitch40OnHeight }.group(Group.Pitch40Control)
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)

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)
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)
val timerReturnValue by setting("Timer Return Value", 1.0, 0.0..1.0, 0.05, description = "Timer speed to return when above min chunk distance") { useTimerOnChunkLoad }.group(Group.TimerControls)

override val rotationConfig = RotationSettings(this, Group.Rotation)

var controlState = ControlState.AttitudeControl
var state = Pitch40State.GainSpeed
var lastAngle = pitch40UpStartAngle
var lastCycleFinish = TimeSource.Monotonic.markNow()
var lastY = 0.0

val usageDelay = Timer()
val usageDelay = com.lambda.util.Timer()

val SafeContext.hasFirework: Boolean
get() = fastEntitySearch<FireworkRocketEntity>(4.0) { it.shooter == this.player }.any()

init {
setDefaultAutomationConfig {
Expand All @@ -96,89 +107,33 @@ object ElytraAltitudeControl : Module(
}

listen<TickEvent.Pre> {
if (!player.isGliding) return@listen
run {
if (player.isGliding) {
when (controlState) {
ControlState.AttitudeControl -> {
if (disableOnFirework && hasFirework) {
return@run
}
if (usePitch40OnHeight) {
if (player.y < minHeightForPitch40) {
controlState = ControlState.Pitch40Fly
lastY = player.pos.y
return@run
}
}
val outputPitch = when (controlValue) {
Mode.Speed -> {
speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
}
Mode.Altitude -> {
-1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
}
}.coerceIn(-maxPitchAngle, maxPitchAngle)
rotationRequest { pitch(outputPitch) }.submit()

if (usageDelay.timePassed(2.seconds) && !hasFirework) {
if (useFireworkOnHeight && minHeight > player.y) {
usageDelay.reset()
runSafe {
startFirework(true)
}
}
if (useFireworkOnSpeed && minSpeed > player.flySpeed()) {
usageDelay.reset()
runSafe {
startFirework(true)
}
}
}
}
ControlState.Pitch40Fly -> when (state) {
Pitch40State.GainSpeed -> {
rotationRequest { pitch(pitch40DownAngle) }.submit()
if (player.flySpeed() > pitch40SpeedThreshold) {
state = Pitch40State.PitchUp
}
}
Pitch40State.PitchUp -> {
lastAngle -= 5f
rotationRequest { pitch(lastAngle) }.submit()
if (lastAngle <= pitch40UpStartAngle) {
state = Pitch40State.FlyUp
if (pitch40UseFireworkOnUpTrajectory) {
runSafe {
startFirework(true)
}
}
ControlState.AttitudeControl -> updateAltitudeControls()
ControlState.Pitch40Fly -> updatePitch40Controls()
}

if (useTimerOnChunkLoad) {
nearestUnloadedChunk(world, player)
?.distCenter(player.pos)
?.let {
if (it <= timerMinChunkDistance * 16.0) {
val speedFactor = 0.1f + (it / (timerMinChunkDistance * 16.0)) * 0.9f
Timer.enable()
Timer.timer = speedFactor.coerceIn(0.1, 1.0)
}
}
Pitch40State.FlyUp -> {
lastAngle += pitch40AngleChangeRate
rotationRequest { pitch(lastAngle) }.submit()
if (lastAngle >= 0f) {
state = Pitch40State.GainSpeed
if (logHeightGain) {
var timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
var heightDelta = player.pos.y - lastY
var heightPerMinute = (heightDelta) / (timeDelta / 1000.0) * 60.0
info(literal("Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)".format(heightDelta, timeDelta / 1000.0, heightPerMinute)))
}

lastCycleFinish = TimeSource.Monotonic.markNow()
lastY = player.pos.y
if (pitch40ExitHeight < player.y) {
controlState = ControlState.AttitudeControl
speedController.reset()
altitudeController.reset()
}
}
?: run {
// FixMe:
// When the timer is changed in an unloaded chunk and the player stop gliding,
// the timer value is never set back.
if (Timer.isEnabled)
Timer.timer = timerReturnValue
}
}
}

lastPos = player.pos
}
lastPos = player.pos
}

onEnable {
Expand All @@ -189,10 +144,117 @@ object ElytraAltitudeControl : Module(
controlState = ControlState.AttitudeControl
lastAngle = pitch40UpStartAngle
}

onDisable {
if (useTimerOnChunkLoad)
Timer.timer = timerReturnValue
}
}

private fun SafeContext.updateAltitudeControls() {
if (disableOnFirework && hasFirework) return

if (usePitch40OnHeight) {
if (player.y < minHeightForPitch40) {
controlState = ControlState.Pitch40Fly
lastY = player.pos.y
return
}
}

val outputPitch = when (controlValue) {
Mode.Speed -> speedController.getOutput(targetSpeed, player.flySpeed(horizontalSpeed).toDouble())
Mode.Altitude -> -1 * altitudeController.getOutput(targetAltitude.toDouble(), player.y) // Negative because in minecraft pitch > 0 is looking down not up
}.coerceIn(-maxPitchAngle, maxPitchAngle)

rotationRequest {
yaw(player.yaw)
pitch(outputPitch)
}.submit()

if (usageDelay.delayIfPassed(2.seconds) && !hasFirework) {
if (useFireworkOnHeight && minHeight > player.y)
startFirework(true)

if (useFireworkOnSpeed && minSpeed > player.flySpeed())
startFirework(true)
}
}

val hasFirework: Boolean
get() = runSafe { return fastEntitySearch<FireworkRocketEntity>(4.0) { it.shooter == player }.any() } ?: false
private fun SafeContext.updatePitch40Controls() {
when (state) {
Pitch40State.GainSpeed -> {
rotationRequest { pitch(pitch40DownAngle) }.submit()

if (player.flySpeed() > pitch40SpeedThreshold)
state = Pitch40State.PitchUp
}
Pitch40State.PitchUp -> {
lastAngle -= 5f
rotationRequest { pitch(lastAngle) }.submit()
if (lastAngle <= pitch40UpStartAngle) {
state = Pitch40State.FlyUp

if (pitch40UseFireworkOnUpTrajectory)
startFirework(true)
}
}
Pitch40State.FlyUp -> {
lastAngle += pitch40AngleChangeRate
rotationRequest { pitch(lastAngle) }.submit()
if (lastAngle >= 0f) {
state = Pitch40State.GainSpeed

if (logHeightGain) {
val timeDelta = lastCycleFinish.elapsedNow().inWholeMilliseconds
val heightDelta = player.pos.y - lastY
val heightPerMinute = (heightDelta) / (timeDelta / 1000.0) * 60.0

info("Height gained this cycle: %.2f in %.2f seconds (%.2f blocks/min)".format(heightDelta, timeDelta / 1000.0, heightPerMinute))
}

lastCycleFinish = TimeSource.Monotonic.markNow()
lastY = player.pos.y

if (pitch40ExitHeight < player.y) {
controlState = ControlState.AttitudeControl
speedController.reset()
altitudeController.reset()
}
}
}
}
}

fun nearestUnloadedChunk(world: ClientWorld, player: ClientPlayerEntity): ChunkPos? {
val scanRangeInt = 25
var nearestChunk: ChunkPos? = null
var nearestDistance = Double.MAX_VALUE
val playerChunk = player.chunkPos

for (x in -scanRangeInt..<scanRangeInt) {
for (z in -scanRangeInt..<scanRangeInt) {
val chunkPos = ChunkPos(playerChunk.x + x, playerChunk.z + z)

if (world.chunkManager.isChunkLoaded(chunkPos.x, chunkPos.z))
continue

val distance = distanceToChunk(chunkPos, player).toDouble()
if (distance < nearestDistance) {
nearestDistance = distance
nearestChunk = chunkPos
}
}
}

return nearestChunk
}

fun distanceToChunk(chunkPos: ChunkPos, player: ClientPlayerEntity): Float {
val playerPos = player.pos
val chunkCenter = Vec3d((chunkPos.startX + 8).toDouble(), playerPos.y, (chunkPos.startZ + 8).toDouble())
return playerPos.distanceTo(chunkCenter).toFloat()
}

class PIController(val valueP: () -> Double, val valueD: () -> Double, val valueI: () -> Double, val constant: () -> Double) {
var accumulator = 0.0 // Integral term accumulator
Expand Down Expand Up @@ -238,12 +300,13 @@ object ElytraAltitudeControl : Module(
SpeedControl("Speed Control"),
AltitudeControl("Altitude Control"),
Pitch40Control("Pitch 40 Control"),
Rotation("Rotation")
Rotation("Rotation"),
TimerControls("Timer Controls"),
}

enum class Pitch40State {
GainSpeed,
PitchUp,
FlyUp,
}
}
}
19 changes: 10 additions & 9 deletions src/main/kotlin/com/lambda/module/modules/movement/Timer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -23,15 +23,16 @@ import com.lambda.module.Module
import com.lambda.module.tag.ModuleTag

object Timer : Module(
name = "Timer",
description = "Modify client tick speed.",
tag = ModuleTag.MOVEMENT,
name = "Timer",
description = "Modify client tick speed.",
tag = ModuleTag.MOVEMENT,
) {
private val timer by setting("Timer", 1.0, 0.0..10.0, 0.01)
@JvmStatic
var timer by setting("Timer", 1.0, 0.0..10.0, 0.01)

init {
listen<ClientEvent.TimerUpdate> {
it.speed = timer.coerceAtLeast(0.05)
}
}
init {
listen<ClientEvent.TimerUpdate> {
it.speed = timer.coerceAtLeast(0.05)
}
}
}