|
| 1 | +/* |
| 2 | + * Copyright 2025 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.player |
| 19 | + |
| 20 | +import com.lambda.Lambda.mc |
| 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 = mc.gameRenderer.camera.lastTickProgress |
| 82 | + return prevPosition.interpolate(tickProgress, position) |
| 83 | + } |
| 84 | + |
| 85 | + private var rotation: Rotation = Rotation.ZERO |
| 86 | + private var velocity: Vec3d = Vec3d.ZERO |
| 87 | + private var loading = false |
| 88 | + |
| 89 | + @JvmStatic |
| 90 | + fun updateCam() { |
| 91 | + 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