1+ /*
2+ * Copyright 2026 Lambda
3+ *
4+ * This program is free software: you can redistribute it and/or modify
5+ * it under the terms of the GNU General Public License as published by
6+ * the Free Software Foundation, either version 3 of the License, or
7+ * (at your option) any later version.
8+ *
9+ * This program is distributed in the hope that it will be useful,
10+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
11+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+ * GNU General Public License for more details.
13+ *
14+ * You should have received a copy of the GNU General Public License
15+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
16+ */
17+
18+ package com.lambda.module.modules.render
19+
20+ import com.lambda.Lambda
21+ import com.lambda.event.events.MovementEvent
22+ import com.lambda.event.events.PlayerEvent
23+ import com.lambda.event.events.RenderEvent
24+ import com.lambda.event.events.TickEvent
25+ import com.lambda.event.listener.SafeListener.Companion.listen
26+ import com.lambda.interaction.managers.rotating.IRotationRequest.Companion.rotationRequest
27+ import com.lambda.interaction.managers.rotating.Rotation
28+ import com.lambda.interaction.managers.rotating.RotationConfig
29+ import com.lambda.interaction.managers.rotating.RotationMode
30+ import com.lambda.interaction.managers.rotating.visibilty.lookAt
31+ import com.lambda.module.Module
32+ import com.lambda.module.tag.ModuleTag
33+ import com.lambda.threading.runSafeAutomated
34+ import com.lambda.util.Communication.info
35+ import com.lambda.util.Describable
36+ import com.lambda.util.NamedEnum
37+ import com.lambda.util.extension.rotation
38+ import com.lambda.util.math.interpolate
39+ import com.lambda.util.math.plus
40+ import com.lambda.util.math.times
41+ import com.lambda.util.player.MovementUtils.calcMoveRad
42+ import com.lambda.util.player.MovementUtils.cancel
43+ import com.lambda.util.player.MovementUtils.handledByBaritone
44+ import com.lambda.util.player.MovementUtils.isInputting
45+ import com.lambda.util.player.MovementUtils.movementVector
46+ import com.lambda.util.player.MovementUtils.newMovementInput
47+ import com.lambda.util.player.MovementUtils.roundedForward
48+ import com.lambda.util.player.MovementUtils.roundedStrafing
49+ import com.lambda.util.player.MovementUtils.verticalMovement
50+ import com.lambda.util.world.raycast.RayCastUtils.orMiss
51+ import net.minecraft.client.option.Perspective
52+ import net.minecraft.util.hit.BlockHitResult
53+ import net.minecraft.util.hit.HitResult
54+ import net.minecraft.util.math.BlockPos
55+ import net.minecraft.util.math.Direction
56+ import net.minecraft.util.math.Vec3d
57+
58+ object Freecam : Module(
59+ name = " Freecam" ,
60+ description = " Move your camera freely" ,
61+ tag = ModuleTag .RENDER ,
62+ autoDisable = true ,
63+ ) {
64+ private val speed by setting(" Speed" , 0.5 , 0.1 .. 1.0 , 0.1 , " Freecam movement speed" , unit = " m/s" )
65+ 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." )
66+ private val reach by setting(" Reach" , 10.0 , 1.0 .. 100.0 , 1.0 , " Freecam reach distance" )
67+ private val rotateMode by setting(" Rotate Mode" , FreecamRotationMode .None , " Rotation mode" )
68+ .onValueChange { _, it -> if (it == FreecamRotationMode .LookAtTarget ) mc.crosshairTarget = BlockHitResult .createMissed(Vec3d .ZERO , Direction .UP , BlockPos .ORIGIN ) }
69+ private val relative by setting(" Relative" , false , " Moves freecam relative to player position" )
70+ .onValueChange { _, it -> if (it) lastPlayerPosition = player.pos }
71+ private val keepYLevel by setting(" Keep Y Level" , false , " Don't change the camera y-level on player movement" ) { relative }
72+
73+ override val rotationConfig = RotationConfig .Instant (RotationMode .Lock )
74+
75+ private var lastPerspective = Perspective .FIRST_PERSON
76+ private var lastPlayerPosition: Vec3d = Vec3d .ZERO
77+ private var prevPosition: Vec3d = Vec3d .ZERO
78+ private var position: Vec3d = Vec3d .ZERO
79+ private val lerpPos: Vec3d
80+ get() {
81+ val tickProgress = Lambda .mc.gameRenderer.camera.lastTickProgress
82+ return prevPosition.interpolate(tickProgress, position)
83+ }
84+
85+ private var rotation: Rotation = Rotation .Companion .ZERO
86+ private var velocity: Vec3d = Vec3d .ZERO
87+ private var loading = false
88+
89+ @JvmStatic
90+ fun updateCam () {
91+ Lambda .mc.gameRenderer.apply {
92+ camera.setRotation(rotation.yawF, rotation.pitchF)
93+ camera.setPos(lerpPos.x, lerpPos.y, lerpPos.z)
94+ }
95+ }
96+
97+ /* *
98+ * @see net.minecraft.entity.Entity.changeLookDirection
99+ */
100+ private const val SENSITIVITY_FACTOR = 0.15
101+
102+ init {
103+ onEnable {
104+ lastPerspective = mc.options.perspective
105+ position = player.eyePos
106+ rotation = player.rotation
107+ velocity = Vec3d .ZERO
108+ lastPlayerPosition = player.pos
109+ }
110+
111+ onDisable {
112+ mc.options.perspective = lastPerspective
113+ }
114+
115+ listen<TickEvent .Pre > {
116+ when (rotateMode) {
117+ FreecamRotationMode .None -> return @listen
118+ FreecamRotationMode .KeepRotation -> rotationRequest { rotation(rotation) }.submit()
119+ FreecamRotationMode .LookAtTarget ->
120+ mc.crosshairTarget?.let {
121+ runSafeAutomated {
122+ rotationRequest { rotation(lookAt(it.pos)) }.submit()
123+ }
124+ }
125+ }
126+ }
127+
128+ listen<PlayerEvent .World .Respawn > {
129+ loading = true
130+ info(" Respawned, waiting for position look packet to update freecam position..." )
131+ }
132+
133+ listen<PlayerEvent .World .SetPosition > {
134+ info(" Received position look packet, updating freecam position" )
135+ info(" New position: ${it.position} " )
136+ if (loading) {
137+ loading = false
138+ position = player.eyePos
139+ rotation = player.rotation
140+ }
141+ }
142+
143+ listen<PlayerEvent .ChangeLookDirection > {
144+ rotation = rotation.withDelta(
145+ it.deltaYaw * SENSITIVITY_FACTOR ,
146+ it.deltaPitch * SENSITIVITY_FACTOR
147+ )
148+ it.cancel()
149+ }
150+
151+ listen<MovementEvent .InputUpdate > { event ->
152+ mc.options.perspective = Perspective .FIRST_PERSON
153+
154+ // Don't block baritone from working
155+ if (! event.input.handledByBaritone) {
156+ // Reset actual input
157+ event.input.cancel()
158+ }
159+
160+ // Create new input for freecam
161+ val input = newMovementInput(assumeBaritone = false , slowdownCheck = false )
162+ val sprintModifier = if (mc.options.sprintKey.isPressed) sprint else 1.0
163+ val moveDir = calcMoveRad(rotation.yawF, input.roundedForward, input.roundedStrafing)
164+ var moveVec = movementVector(moveDir, input.verticalMovement) * speed * sprintModifier
165+ if (! input.isInputting) moveVec * = Vec3d (0.0 , 1.0 , 0.0 )
166+
167+ // Apply movement
168+ velocity + moveVec
169+ velocity * = 0.6
170+
171+ // Update position
172+ prevPosition = position
173+ position + = velocity
174+
175+ if (relative) {
176+ val delta = player.pos.subtract(lastPlayerPosition)
177+ position + = if (keepYLevel) Vec3d (delta.x, 0.0 , delta.z) else delta
178+ lastPlayerPosition = player.pos
179+ }
180+ }
181+
182+ listen<RenderEvent .UpdateTarget >({ 1 }) { event -> // Higher priority then RotationManager to run before RotationManager modifies mc.crosshairTarget
183+ mc.crosshairTarget = rotation
184+ .rayCast(reach, lerpPos)
185+ .orMiss // Can't be null (otherwise mc will spam "Null returned as 'hitResult', this shouldn't happen!")
186+
187+ mc.crosshairTarget?.let { if (it.type != HitResult .Type .MISS ) event.cancel() }
188+ }
189+ }
190+
191+ enum class FreecamRotationMode (override val displayName : String , override val description : String ) : NamedEnum, Describable {
192+ None (" None" , " No rotation changes" ),
193+ LookAtTarget (" Look At Target" , " Look at the block or entity under your crosshair" ),
194+ KeepRotation (" Keep Rotation" , " Look in the same direction as the camera" );
195+ }
196+ }
0 commit comments