Skip to content

Commit 340d967

Browse files
authored
Freecam improvements including follow player and track player (#247)
1 parent a27497e commit 340d967

2 files changed

Lines changed: 203 additions & 122 deletions

File tree

src/main/kotlin/com/lambda/module/modules/player/Freecam.kt

Lines changed: 193 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -18,23 +18,28 @@
1818
package com.lambda.module.modules.player
1919

2020
import com.lambda.Lambda.mc
21+
import com.lambda.config.AutomationConfig.Companion.setDefaultAutomationConfig
22+
import com.lambda.config.applyEdits
23+
import com.lambda.context.SafeContext
2124
import com.lambda.event.events.MovementEvent
2225
import com.lambda.event.events.PlayerEvent
2326
import com.lambda.event.events.RenderEvent
2427
import com.lambda.event.events.TickEvent
2528
import com.lambda.event.listener.SafeListener.Companion.listen
2629
import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
2730
import com.lambda.interaction.managers.rotating.Rotation
28-
import com.lambda.interaction.managers.rotating.RotationConfig
2931
import com.lambda.interaction.managers.rotating.RotationMode
3032
import com.lambda.interaction.managers.rotating.visibilty.lookAt
3133
import com.lambda.module.Module
3234
import com.lambda.module.tag.ModuleTag
33-
import com.lambda.threading.runSafeAutomated
35+
import com.lambda.threading.runSafe
3436
import com.lambda.util.Describable
3537
import com.lambda.util.NamedEnum
3638
import com.lambda.util.extension.rotation
39+
import com.lambda.util.math.Vec2d
40+
import com.lambda.util.math.dist
3741
import com.lambda.util.math.interpolate
42+
import com.lambda.util.math.normal
3843
import com.lambda.util.math.plus
3944
import com.lambda.util.math.times
4045
import com.lambda.util.player.MovementUtils.calcMoveRad
@@ -47,133 +52,199 @@ import com.lambda.util.player.MovementUtils.roundedForward
4752
import com.lambda.util.player.MovementUtils.roundedStrafing
4853
import com.lambda.util.player.MovementUtils.verticalMovement
4954
import com.lambda.util.world.raycast.RayCastUtils.orMiss
55+
import net.minecraft.client.network.ClientPlayerEntity
5056
import net.minecraft.client.option.Perspective
57+
import net.minecraft.entity.player.PlayerEntity
5158
import net.minecraft.util.hit.BlockHitResult
5259
import net.minecraft.util.hit.HitResult
5360
import net.minecraft.util.math.BlockPos
5461
import net.minecraft.util.math.Direction
5562
import net.minecraft.util.math.Vec3d
63+
import net.minecraft.world.GameMode
64+
import kotlin.math.abs
65+
import kotlin.math.atan2
66+
import kotlin.math.hypot
67+
import kotlin.math.sign
5668

5769
object Freecam : Module(
58-
name = "Freecam",
59-
description = "Move your camera freely",
60-
tag = ModuleTag.RENDER,
61-
autoDisable = true,
70+
name = "Freecam",
71+
description = "Move your camera freely",
72+
tag = ModuleTag.RENDER,
73+
autoDisable = true,
6274
) {
63-
private val speed by setting("Speed", 0.5, 0.1..1.0, 0.1, "Freecam movement speed", unit = "m/s")
64-
private val sprint by setting("Sprint Multiplier", 3.0, 0.1..10.0, 0.1, description = "Set below 1.0 to fly slower on sprint.")
65-
private val reach by setting("Reach", 10.0, 1.0..100.0, 1.0, "Freecam reach distance")
66-
private val rotateMode by setting("Rotate Mode", FreecamRotationMode.None, "Rotation mode")
67-
.onValueChange { _, it -> if (it == FreecamRotationMode.LookAtTarget) mc.crosshairTarget = BlockHitResult.createMissed(Vec3d.ZERO, Direction.UP, BlockPos.ORIGIN) }
68-
private val relative by setting("Relative", false, "Moves freecam relative to player position")
69-
.onValueChange { _, it -> if (it) lastPlayerPosition = player.pos }
70-
private val keepYLevel by setting("Keep Y Level", false, "Don't change the camera y-level on player movement") { relative }
71-
72-
override val rotationConfig = RotationConfig.Instant(RotationMode.Lock)
73-
74-
private var lastPerspective = Perspective.FIRST_PERSON
75-
private var lastPlayerPosition: Vec3d = Vec3d.ZERO
76-
private var prevPosition: Vec3d = Vec3d.ZERO
77-
private var position: Vec3d = Vec3d.ZERO
78-
private val lerpPos: Vec3d
79-
get() {
80-
val tickProgress = mc.gameRenderer.camera.lastTickProgress
81-
return prevPosition.interpolate(tickProgress, position)
82-
}
83-
84-
private var rotation: Rotation = Rotation.ZERO
85-
private var velocity: Vec3d = Vec3d.ZERO
86-
87-
@JvmStatic
88-
fun updateCam() {
89-
mc.gameRenderer.apply {
90-
camera.setRotation(rotation.yawF, rotation.pitchF)
91-
camera.setPos(lerpPos.x, lerpPos.y, lerpPos.z)
92-
}
93-
}
94-
95-
/**
96-
* @see net.minecraft.entity.Entity.changeLookDirection
97-
*/
98-
private const val SENSITIVITY_FACTOR = 0.15
99-
100-
init {
101-
onEnable {
102-
lastPerspective = mc.options.perspective
103-
position = player.eyePos
104-
rotation = player.rotation
105-
velocity = Vec3d.ZERO
106-
lastPlayerPosition = player.pos
107-
}
108-
109-
onDisable {
110-
mc.options.perspective = lastPerspective
111-
}
112-
113-
listen<TickEvent.Pre> {
114-
when (rotateMode) {
115-
FreecamRotationMode.None -> return@listen
116-
FreecamRotationMode.KeepRotation -> rotationRequest { rotation(rotation) }.submit()
117-
FreecamRotationMode.LookAtTarget ->
118-
mc.crosshairTarget?.let {
119-
runSafeAutomated {
120-
rotationRequest { rotation(lookAt(it.pos)) }.submit()
121-
}
122-
}
123-
}
124-
}
125-
126-
listen<PlayerEvent.ChangeLookDirection> {
127-
rotation = rotation.withDelta(
128-
it.deltaYaw * SENSITIVITY_FACTOR,
129-
it.deltaPitch * SENSITIVITY_FACTOR
130-
)
131-
it.cancel()
132-
}
133-
134-
listen<MovementEvent.InputUpdate> { event ->
135-
mc.options.perspective = Perspective.FIRST_PERSON
136-
137-
// Don't block baritone from working
138-
if (!event.input.handledByBaritone) {
139-
// Reset actual input
140-
event.input.cancel()
141-
}
142-
143-
// Create new input for freecam
144-
val input = newMovementInput(assumeBaritone = false, slowdownCheck = false)
145-
val sprintModifier = if (mc.options.sprintKey.isPressed) sprint else 1.0
146-
val moveDir = calcMoveRad(rotation.yawF, input.roundedForward, input.roundedStrafing)
147-
var moveVec = movementVector(moveDir, input.verticalMovement) * speed * sprintModifier
148-
if (!input.isInputting) moveVec *= Vec3d(0.0, 1.0, 0.0)
149-
150-
// Apply movement
151-
velocity += moveVec
152-
velocity *= 0.6
153-
154-
// Update position
155-
prevPosition = position
156-
position += velocity
157-
158-
if (relative) {
159-
val delta = player.pos.subtract(lastPlayerPosition)
160-
position += if (keepYLevel) Vec3d(delta.x, 0.0, delta.z) else delta
161-
lastPlayerPosition = player.pos
162-
}
163-
}
164-
165-
listen<RenderEvent.UpdateTarget>({ 1 }) { event -> // Higher priority then RotationManager to run before RotationManager modifies mc.crosshairTarget
166-
mc.crosshairTarget = rotation
167-
.rayCast(reach, lerpPos)
168-
.orMiss // Can't be null (otherwise mc will spam "Null returned as 'hitResult', this shouldn't happen!")
169-
170-
mc.crosshairTarget?.let { if (it.type != HitResult.Type.MISS) event.cancel() }
171-
}
172-
}
173-
174-
enum class FreecamRotationMode(override val displayName: String, override val description: String) : NamedEnum, Describable {
175-
None("None", "No rotation changes"),
176-
LookAtTarget("Look At Target", "Look at the block or entity under your crosshair"),
177-
KeepRotation("Keep Rotation", "Look in the same direction as the camera");
178-
}
75+
private val mode by setting("Mode", Mode.Free, "Freecam movement mode")
76+
private val speed by setting("Speed", 0.5, 0.1..1.0, 0.1, "Freecam movement speed", unit = "m/s") { mode == Mode.Free }
77+
private val sprint by setting("Sprint Multiplier", 3.0, 0.1..10.0, 0.1, description = "Set below 1.0 to fly slower on sprint.") { mode == Mode.Free }
78+
private val reach by setting("Reach", 10.0, 1.0..100.0, 1.0, "Freecam reach distance")
79+
private val rotateMode by setting("Rotate Mode", FreecamRotationMode.None, "Rotation mode").onValueChange { _, it -> if (it == FreecamRotationMode.LookAtTarget) mc.crosshairTarget = BlockHitResult.createMissed(Vec3d.ZERO, Direction.UP, BlockPos.ORIGIN) }
80+
private val relative by setting("Relative", false, "Moves freecam relative to player position") { mode == Mode.Free }.onValueChange { _, it -> if (it) lastPlayerPosition = player.pos }
81+
private val keepYLevel by setting("Keep Y Level", false, "Don't change the camera y-level on player movement") { mode == Mode.Free && relative }
82+
83+
// Follow Player settings
84+
private val followMaxDistance by setting("String Length", 10.0, 2.0..50.0, 0.5, "Maximum distance before the string pulls the camera", unit = "m") { mode == Mode.FollowPlayer }
85+
private val followTrackPlayer by setting("Track Player", false, "Keeps looking at the followed player") { mode == Mode.FollowPlayer }
86+
87+
88+
private var lastPerspective = Perspective.FIRST_PERSON
89+
private var lastPlayerPosition: Vec3d = Vec3d.ZERO
90+
private var prevPosition: Vec3d = Vec3d.ZERO
91+
private var position: Vec3d = Vec3d.ZERO
92+
private val lerpPos: Vec3d
93+
get() {
94+
val tickProgress = mc.gameRenderer.camera.lastTickProgress
95+
return prevPosition.interpolate(tickProgress, position)
96+
}
97+
98+
private var rotation: Rotation = Rotation.ZERO
99+
private var velocity: Vec3d = Vec3d.ZERO
100+
101+
@JvmStatic
102+
fun updateCam() {
103+
if (mode == Mode.FollowPlayer && followTrackPlayer) {
104+
runSafe {
105+
findFollowTarget()?.let {
106+
val vec = it.getLerpedPos(mc.gameRenderer.camera.lastTickProgress).add(.0, it.standingEyeHeight.toDouble(), .0).subtract(lerpPos) // look from lerp pos to target's eye pos
107+
val yaw = Math.toDegrees(atan2(vec.z, vec.x)) - 90.0
108+
val pitch = -Math.toDegrees(atan2(vec.y, hypot(vec.x, vec.z)))
109+
rotation = Rotation(yaw, pitch)
110+
}
111+
}
112+
}
113+
mc.gameRenderer.apply {
114+
camera.setRotation(rotation.yawF, rotation.pitchF)
115+
camera.setPos(lerpPos.x, lerpPos.y, lerpPos.z)
116+
}
117+
}
118+
119+
/**
120+
* @see net.minecraft.entity.Entity.changeLookDirection
121+
*/
122+
private const val SENSITIVITY_FACTOR = 0.15
123+
124+
init {
125+
setDefaultAutomationConfig {
126+
applyEdits {
127+
rotationConfig::rotationMode.edit {
128+
defaultValue(RotationMode.Lock)
129+
}
130+
hideAllGroupsExcept(rotationConfig)
131+
}
132+
}
133+
134+
onEnable {
135+
lastPerspective = mc.options.perspective
136+
position = player.eyePos
137+
rotation = player.rotation
138+
velocity = Vec3d.ZERO
139+
lastPlayerPosition = player.pos
140+
}
141+
142+
onDisable {
143+
mc.options.perspective = lastPerspective
144+
}
145+
146+
listen<TickEvent.Pre> {
147+
when (rotateMode) {
148+
FreecamRotationMode.None -> return@listen
149+
FreecamRotationMode.KeepRotation -> rotationRequest { rotation(rotation) }.submit()
150+
FreecamRotationMode.LookAtTarget -> mc.crosshairTarget?.let { rotationRequest { rotation(lookAt(it.pos)) }.submit() }
151+
}
152+
}
153+
154+
listen<PlayerEvent.ChangeLookDirection> {
155+
rotation = rotation.withDelta(it.deltaYaw * SENSITIVITY_FACTOR, it.deltaPitch * SENSITIVITY_FACTOR)
156+
it.cancel()
157+
}
158+
159+
listen<MovementEvent.InputUpdate> { event ->
160+
mc.options.perspective = Perspective.FIRST_PERSON
161+
162+
// Don't block baritone from working
163+
if (!event.input.handledByBaritone) { // Reset actual input
164+
event.input.cancel()
165+
}
166+
167+
when (mode) {
168+
Mode.Free -> {
169+
// Create new input for freecam
170+
val input = newMovementInput(assumeBaritone = false, slowdownCheck = false)
171+
val sprintModifier = if (mc.options.sprintKey.isPressed) sprint else 1.0
172+
val moveDir = calcMoveRad(rotation.yawF, input.roundedForward, input.roundedStrafing)
173+
var moveVec = movementVector(moveDir, input.verticalMovement) * speed * sprintModifier
174+
if (!input.isInputting) moveVec *= Vec3d(0.0, 1.0, 0.0)
175+
// Apply movement
176+
velocity += moveVec
177+
velocity *= 0.6
178+
// Update position
179+
prevPosition = position
180+
position += velocity
181+
182+
if (relative) {
183+
val delta = player.pos.subtract(lastPlayerPosition)
184+
position += if (keepYLevel) Vec3d(delta.x, 0.0, delta.z) else delta
185+
lastPlayerPosition = player.pos
186+
}
187+
}
188+
189+
Mode.FollowPlayer -> {
190+
// Allow manual camera nudges via input
191+
val input = newMovementInput(assumeBaritone = false, slowdownCheck = false)
192+
val moveDir = calcMoveRad(rotation.yawF, input.roundedForward, input.roundedStrafing)
193+
var moveVec = movementVector(moveDir, input.verticalMovement) * speed
194+
if (!input.isInputting) moveVec *= Vec3d(0.0, 1.0, 0.0)
195+
// Apply movement
196+
velocity += moveVec
197+
velocity *= 0.6
198+
// Update position
199+
prevPosition = position
200+
201+
findFollowTarget()?.let { target ->
202+
val targetPos2d = Vec2d(target.eyePos.x, target.eyePos.z)
203+
val cameraPos2d = Vec2d(position.x, position.z)
204+
val distance2d = targetPos2d.dist(cameraPos2d)
205+
if (distance2d > followMaxDistance) {
206+
val excess2d = distance2d - followMaxDistance
207+
val pullDirection2d = Vec2d(targetPos2d.x - position.x, targetPos2d.y - position.z).normal()
208+
val pullVec2d = pullDirection2d * excess2d
209+
position = Vec3d(position.x + pullVec2d.x, position.y, position.z + pullVec2d.y)
210+
}
211+
212+
if (abs(target.eyePos.y - position.y) > followMaxDistance) {
213+
val excessY = abs(target.eyePos.y - position.y) - followMaxDistance
214+
val pullDirectionY = sign(target.eyePos.y - position.y)
215+
position = Vec3d(position.x, position.y + pullDirectionY * excessY, position.z)
216+
}
217+
val delta = player.pos.subtract(lastPlayerPosition)
218+
position += Vec3d(0.0, delta.y, 0.0)
219+
}
220+
position += velocity
221+
lastPlayerPosition = player.pos
222+
}
223+
}
224+
}
225+
226+
listen<RenderEvent.UpdateTarget>({ 1 }) { event -> // Higher priority then RotationManager to run before RotationManager modifies mc.crosshairTarget
227+
mc.crosshairTarget = rotation.rayCast(reach, lerpPos).orMiss // Can't be null (otherwise mc will spam "Null returned as 'hitResult', this shouldn't happen!")
228+
mc.crosshairTarget?.let { if (it.type != HitResult.Type.MISS) event.cancel() }
229+
}
230+
}
231+
232+
private enum class FreecamRotationMode(override val displayName: String, override val description: String) : NamedEnum, Describable {
233+
None("None", "No rotation changes"),
234+
LookAtTarget("Look At Target", "Look at the block or entity under your crosshair"),
235+
KeepRotation("Keep Rotation", "Look in the same direction as the camera");
236+
}
237+
238+
private enum class Mode(override val displayName: String, override val description: String) : NamedEnum, Describable {
239+
Free("Free", "Move the camera freely with keyboard input"),
240+
FollowPlayer("Follow Player", "Camera follows a player as if attached by an invisible string");
241+
}
242+
243+
private fun SafeContext.findFollowTarget(): PlayerEntity? {
244+
if (player.gameMode != GameMode.SPECTATOR) {
245+
return player
246+
}
247+
val players = world.players.filter { it !is ClientPlayerEntity }
248+
return players.minByOrNull { it.eyePos.squaredDistanceTo(position) }
249+
}
179250
}

src/main/kotlin/com/lambda/util/math/Vectors.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,16 @@ infix operator fun Vec3i.times(other: Int): Vec3i = multiply(other)
184184
infix operator fun Vec3i.div(other: Vec3i): Vec3i = Vec3i(x / other.x, y / other.y, z / other.z)
185185
infix operator fun Vec3i.div(other: Int): Vec3i = times(1 / other)
186186

187+
infix fun Vec2d.dist(other: Vec2d): Double = sqrt(this distSq other)
188+
infix fun Vec2d.dist(other: Vec2f): Double = sqrt(this distSq other)
189+
infix fun Vec2d.distSq(other: Vec2d): Double = (other.x - x).pow(2) + (other.y - y).pow(2)
190+
infix fun Vec2d.distSq(other: Vec2f): Double = (other.x - x).pow(2) + (other.y - y).pow(2)
191+
192+
fun Vec2d.normal(): Vec2d {
193+
val length = sqrt(x * x + y * y)
194+
return if (length != 0.0) Vec2d(x / length, y / length) else Vec2d.ZERO
195+
}
196+
187197
/* Entity */
188198
infix fun Entity.dist(other: Vec3d): Double = pos dist other
189199
infix fun Entity.dist(other: Vec3i): Double = blockPos dist other

0 commit comments

Comments
 (0)