chore(history): squash 59 commit(s) from 2026-09-23

- refactor(hud/weather/render): Scoreboard 931→240 + Rain 903→159 + UiMeshGeometry 956→99 (8.5.2)
- refactor(chams): Chams 985 → 129 + 4×≤195 (8.5.2)
- refactor(module): Module 1031 → 139 + 4×≤134 (8.5.2)
- refactor(tooltips): BetterTooltips 960 → 181 + 4×≤199 (8.5.2)
- refactor(clickgui): ClickGuiPickerState 970→166 + ModulesMenuScreen 966→145 (8.5.2)
- refactor(clickgui): CombatProtocolHeuristicsEditorState 932→79 + 4×≤192 (8.5.2)
- refactor(hud): CompactHudStatModel 894 → 125 + 4×≤175 (8.5.2)
- refactor(iris): ShaderPatchCompiler 843→66 + 4×≤174 (8.5.2)
- refactor(tab): CustomTabList 834→136 + 4×≤166 (8.5.2)
- refactor(relations): OnlineRelationPlayerPickerComponent 837→74 + 4×≤192 (8.5.2)
- refactor(hits): HitEffect 770→120 + 4×≤176 (8.5.2)
- refactor(predict): Predictions 815→150 + 4×≤195 (8.5.2)
- refactor(config): ConfigProfilesComponent 799→125 + 4×≤195 (8.5.2)
- refactor(sim): PlayerMovementSimulation 757→157 + 4×≤194 (8.5.2)
- refactor(predict): ProjectilePuncher 747→104 + 3×≤134 (8.5.2)
- refactor(crosshair): Crosshair 743→143 + 4×≤167 (8.5.2)
- refactor(mediaplayer): MediaPlayer 743→174 + 4×≤196 (8.5.2)
- refactor(hud): Itemizer 699→193 + Cooldowns 707→151 (8.5.2)
- refactor(hud): Potions 617→176 + 4×≤166 (8.5.2)
- refactor(hud): Admins 702→118 + 4×≤138 (8.5.2)
- refactor(hud): Keybinds 611→157 + 4×≤160 (8.5.2)
- refactor(hud): ModuleList 499→181 + 3×≤150 (8.5.2)
- refactor(hud): Radar 392→161 + 2×≤124 (8.5.2)
- refactor(hud): Fps 309→168 + 3×≤107 (8.5.2)
- refactor(hud): Memory 304→151 + 3×≤144 (8.5.2)
- refactor(hud): Tps 339→154 + 3×≤143 (8.5.2)
- refactor(hud): Ping 309→145 + 3×≤125 (8.5.2)
- refactor(hud): SpeedBps 334→156 + 3×≤142 (8.5.2)
- refactor(hud): GameTime 314→150 + 3×≤127 (8.5.2)
- refactor(hud): SystemTime 300→136 + 3×≤130 (8.5.2)
- refactor(hud): Coordinates 337→174 + 2×≤172 (8.5.2)
- refactor(hud): Armor 324→162 + 2×≤148 (8.5.2)
- refactor(hud): Inventory 476→153 + 3×≤181 (8.5.2)
- docs(todo): обновлён 8.5.2 — отмечены готовые гиганты (BetterButtons/InventorySwap/Svg/Темы/WorldParticles/Module/Chams и хвост)
- docs(api): папка combatant-client-26.2/docs + addons.md (API можно ломать при рефакторе)
- refactor(visuals): BlockHighlight 692→271 + 4×≤200 (8.5.2)
- refactor(mixins): GameRendererMixin 830→344 (хуки) + 4 хелпера (8.5.2)
- refactor(visuals): WorldParticles 821→260 + 6×≤200, режимы добиты (8.5.2)
- docs(todo): 8.5.2 — WorldParticles/GameRendererMixin/BlockHighlight отмечены ГОТОВО
- refactor(render): MeshBuilder 738→447 (фасад write-API) + 3 хелпера, FrameStats снесён (8.5.2)
- docs(todo): Фаза 10 — дизайн платформы (аккаунты/конфиги/аватарки), backlog RPC-чата
- docs(todo): Подсистема 3 — Аддоны 2.0 (скрипты/версии) + витрина аддонов + админка
- docs(todo): Подсистема 2 — RPC-чат/друзья/виджеты-телеметрия + рейт-лимиты, доп. правки Подсистемы 3
- chore: переименование combatant-client-26.2 -> mod, добавлены backend/ и frontend/
- docs(backend): implementation plan for accounts-service (Подсистема 1, часть 1)
- chore: пересоздать backend/frontend через cargo new и bun create vite + tailwind; убрать таблицу компонентов из TODO.md
- docs(backend): edition 2024 в плане вместо 2021
- chore: обновить версии до реально актуальных (проверено компиляцией)
- docs: правила платформы (лимит 250 строк, антипаттерны backend/frontend по итогам ресёрча) + frontend/ARCHITECTURE.md
- docs: добавить правило ≤4 файла на папку в правила платформы (backend/frontend)
- docs: commit messages in English from now on
- chore: gitignore .superpowers/ scratch directory
- docs: commit messages in English from now on (mod)
- feat(backend): scaffold accounts-service with health check
- chore(backend): remove target/ build artifacts from git, add gitignore
- chore: track docs/ in git (was accidentally gitignored, never committed)
- chore(backend): convert to Cargo workspace, plan full microservice layout
- feat(backend): accounts/avatars/device_links schema + migration
- feat(backend): argon2id password hashing
This commit is contained in:
loki5512344 2026-09-23 22:38:08 +02:00
parent ec10395511
commit 9d08fa910a
2239 changed files with 35265 additions and 28110 deletions

View file

@ -0,0 +1,371 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.entity;
import net.minecraft.client.player.LocalPlayer;
import net.minecraft.world.entity.EntityDimensions;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import dev.loki.lovisual.events.impl.movement.MovementInputEvent;
import dev.loki.lovisual.render.helpers.util.TickDelta;
public enum EagleUtil {
;
private static final double STEP_HEIGHT = 0.5;
private static final double MIN_INPUT_LENGTH_SQUARED = 1.0E-6;
private static final double MIN_AXIS_COMPONENT = 0.18;
private static final double MAX_RESCUE_LOOKAHEAD = 0.42;
private static final double MIN_TICK_DELTA_LEAD = 0.25;
private static final double CENTER_DEAD_ANGLE_COS = 0.34;
private static final int DIAGONAL_RESCUE_TICKS = 2;
public static EdgeCheck checkEdge(LocalPlayer player, MovementInputEvent event, double edgeDistance) {
return checkEdge(player, event, edgeDistance, TickDelta.get());
}
public static EdgeCheck checkEdge(LocalPlayer player, MovementInputEvent event, double edgeDistance, double tickDelta) {
if (player == null || event == null) {
return EdgeCheck.empty();
}
Vec3 inputDirection = movementInputToWorld(player, event);
if (inputDirection.lengthSqr() < MIN_INPUT_LENGTH_SQUARED) {
return EdgeCheck.empty();
}
Vec3 movementDirection = inputDirection.normalize();
Vec3 horizontalVelocity = new Vec3(player.getDeltaMovement().x, 0.0, player.getDeltaMovement().z);
double velocityLength = horizontalVelocity.horizontalDistance();
double distance = Math.max(0.0, edgeDistance);
double lookAhead = Math.min(MAX_RESCUE_LOOKAHEAD, distance + velocityLength);
double firstLead = Math.max(MIN_TICK_DELTA_LEAD, clamp01(tickDelta));
Vec3 partialPos = player.position().add(horizontalVelocity.scale(firstLead));
Vec3 nextPartialPos = player.position().add(horizontalVelocity.scale(firstLead + 0.5));
boolean closeToEdge = wouldFallAlong(player, player.position(), movementDirection, distance)
|| wouldFallAlong(player, partialPos, movementDirection, distance)
|| wouldFallAlong(player, nextPartialPos, movementDirection, distance);
boolean diagonalRescue = isDiagonal(event)
&& wouldDiagonalFall(player, movementDirection, horizontalVelocity, Math.max(distance, lookAhead), firstLead);
return new EdgeCheck(closeToEdge || diagonalRescue, diagonalRescue, movementDirection);
}
public static boolean isCloseToEdge(LocalPlayer player, MovementInputEvent event, double edgeDistance) {
return checkEdge(player, event, edgeDistance).closeToEdge();
}
public static boolean shouldDiagonalRescue(LocalPlayer player, MovementInputEvent event, double edgeDistance) {
return checkEdge(player, event, edgeDistance).diagonalRescue();
}
public static double horizontalSpeed(LocalPlayer player) {
if (player == null) {
return 0.0;
}
Vec3 velocity = player.getDeltaMovement();
return Math.hypot(velocity.x, velocity.z);
}
public static Vec3 movementInputToWorld(LocalPlayer player, MovementInputEvent event) {
if (player == null || event == null) {
return Vec3.ZERO;
}
float forward = impulse(event.isForward(), event.isBackward());
float sideways = impulse(event.isRight(), event.isLeft());
if (forward == 0.0f && sideways == 0.0f) {
return Vec3.ZERO;
}
double rad = Math.toRadians(player.getYRot());
double sin = Math.sin(rad);
double cos = Math.cos(rad);
double x = sideways * cos - forward * sin;
double z = forward * cos + sideways * sin;
return new Vec3(x, 0.0, z);
}
public static boolean wouldBeCloseToFallOff(LocalPlayer player, Vec3 position) {
if (player == null || position == null) {
return false;
}
EntityDimensions dimensions = player.getDimensions(player.getPose());
AABB hitbox = dimensions.makeBoundingBox(position)
.inflate(-0.05, 0.0, -0.05)
.move(0.0, player.fallDistance - STEP_HEIGHT, 0.0);
return player.level().noCollision(player, hitbox);
}
private static boolean wouldDiagonalFall(
LocalPlayer player,
Vec3 movementDirection,
Vec3 horizontalVelocity,
double distance,
double firstLead
) {
Vec3 base = player.position().add(horizontalVelocity.scale(firstLead));
Vec3 predicted = player.position().add(horizontalVelocity.scale(firstLead + 0.5));
Vec3 predictedSecond = player.position().add(horizontalVelocity.scale(firstLead + 1.0));
if (wouldFallAlong(player, base, movementDirection, distance)
|| wouldFallAlong(player, predicted, movementDirection, distance)
|| wouldFallAlong(player, predictedSecond, movementDirection, distance)) {
return true;
}
Vec3 xAxis = Math.abs(movementDirection.x) > MIN_AXIS_COMPONENT
? new Vec3(Math.signum(movementDirection.x), 0.0, 0.0)
: Vec3.ZERO;
Vec3 zAxis = Math.abs(movementDirection.z) > MIN_AXIS_COMPONENT
? new Vec3(0.0, 0.0, Math.signum(movementDirection.z))
: Vec3.ZERO;
return (xAxis != Vec3.ZERO && wouldFallAlong(player, predicted, xAxis, distance))
|| (zAxis != Vec3.ZERO && wouldFallAlong(player, predicted, zAxis, distance))
|| (xAxis != Vec3.ZERO && zAxis != Vec3.ZERO
&& wouldFallAlong(player, predicted, xAxis.add(zAxis).normalize(), distance));
}
private static boolean wouldFallAlong(LocalPlayer player, Vec3 base, Vec3 direction, double distance) {
if (player == null || base == null || direction == null || direction.lengthSqr() < MIN_INPUT_LENGTH_SQUARED) {
return false;
}
Vec3 normalized = direction.normalize();
double clampedDistance = Math.max(0.0, distance);
if (clampedDistance <= 1.0E-7) {
return wouldBeCloseToFallOff(player, base);
}
for (int i = 1; i <= 3; i++) {
double scale = clampedDistance * i / 3.0;
Vec3 checkedPos = base.add(normalized.x * scale, 0.0, normalized.z * scale);
if (wouldBeCloseToFallOff(player, checkedPos)) {
return true;
}
}
return false;
}
private static boolean isDiagonal(MovementInputEvent event) {
return event != null
&& event.isForward() != event.isBackward()
&& event.isLeft() != event.isRight();
}
private static float impulse(boolean positive, boolean negative) {
if (positive == negative) return 0.0f;
return positive ? 1.0f : -1.0f;
}
private static double clamp01(double value) {
if (Double.isNaN(value)) return 0.0;
return Math.max(0.0, Math.min(1.0, value));
}
public enum RecoveryMode {
STOP,
INVERT,
CENTER
}
public static final class EdgeRecovery {
private Vec3 center;
private int overwriteTicks;
private int sneakTicks;
private static Vec3 blockBottomCenter(LocalPlayer player) {
return new Vec3(
player.blockPosition().getX() + 0.5,
player.blockPosition().getY(),
player.blockPosition().getZ() + 0.5
);
}
private static double horizontalDistanceSquared(Vec3 a, Vec3 b) {
double x = a.x - b.x;
double z = a.z - b.z;
return x * x + z * z;
}
public boolean handleDiagonalRescue(LocalPlayer player, MovementInputEvent event, double edgeDistance) {
if (player == null || event == null) {
return false;
}
updateSafeCenter(player);
EdgeCheck edgeCheck = checkEdge(player, event, edgeDistance);
if (edgeCheck.diagonalRescue() && shouldOverwriteInput(player)) {
overwriteTicks = Math.max(overwriteTicks, DIAGONAL_RESCUE_TICKS);
}
if (overwriteTicks <= 0) {
return false;
}
overwriteTicks--;
applyInputTowardsCenter(player, event);
event.setJump(false);
event.setSneak(true);
event.setSprint(false);
updateSafeCenter(player);
return true;
}
public boolean handleOnEdge(
LocalPlayer player,
MovementInputEvent event,
double edgeDistance,
RecoveryMode mode,
int keepTicks,
int requestedSneakTicks,
boolean jump
) {
if (player == null || event == null) {
return false;
}
updateSafeCenter(player);
double distance = Math.min(horizontalSpeed(player), Math.max(0.0, edgeDistance));
EdgeCheck edgeCheck = checkEdge(player, event, distance);
if (edgeCheck.closeToEdge() && shouldOverwriteInput(player)) {
overwriteTicks = Math.max(overwriteTicks, Math.max(1, keepTicks));
sneakTicks = Math.max(sneakTicks, Math.max(0, requestedSneakTicks));
}
boolean applied = false;
if (overwriteTicks > 0) {
overwriteTicks--;
applyRecoveryInput(player, event, mode);
event.setJump(jump);
event.setSprint(false);
applied = true;
}
if (sneakTicks > 0) {
sneakTicks--;
event.setSneak(true);
applied = true;
}
updateSafeCenter(player);
return applied;
}
public void reset() {
center = null;
overwriteTicks = 0;
sneakTicks = 0;
}
private boolean shouldOverwriteInput(LocalPlayer player) {
Vec3 center = this.center;
if (center == null) {
return true;
}
Vec3 horizontalVelocity = new Vec3(player.getDeltaMovement().x, 0.0, player.getDeltaMovement().z);
if (horizontalVelocity.horizontalDistanceSqr() <= 0.02 * 0.02) {
return true;
}
double currentDistance = horizontalDistanceSquared(center, player.position());
double nextDistance = horizontalDistanceSquared(center, player.position().add(horizontalVelocity));
return nextDistance > currentDistance;
}
private void applyRecoveryInput(LocalPlayer player, MovementInputEvent event, RecoveryMode mode) {
RecoveryMode resolvedMode = mode != null ? mode : RecoveryMode.CENTER;
if (resolvedMode == RecoveryMode.INVERT) {
boolean forward = event.isForward();
boolean backward = event.isBackward();
boolean left = event.isLeft();
boolean right = event.isRight();
event.setForward(backward);
event.setBackward(forward);
event.setLeft(right);
event.setRight(left);
event.setJump(false);
return;
}
if (resolvedMode == RecoveryMode.STOP && horizontalSpeed(player) <= 0.05) {
event.setForward(false);
event.setBackward(false);
event.setLeft(false);
event.setRight(false);
event.setJump(false);
return;
}
applyInputTowardsCenter(player, event);
}
private void applyInputTowardsCenter(LocalPlayer player, MovementInputEvent event) {
Vec3 center = this.center != null ? this.center : blockBottomCenter(player);
Vec3 toCenter = center.subtract(player.position());
Vec3 horizontal = new Vec3(toCenter.x, 0.0, toCenter.z);
if (horizontal.lengthSqr() < 0.015 * 0.015) {
event.setForward(false);
event.setBackward(false);
event.setLeft(false);
event.setRight(false);
return;
}
Vec3 direction = horizontal.normalize();
double rad = Math.toRadians(player.getYRot());
double sin = Math.sin(rad);
double cos = Math.cos(rad);
double localForward = direction.z * cos - direction.x * sin;
double localSideways = direction.x * cos + direction.z * sin;
boolean forward = localForward > CENTER_DEAD_ANGLE_COS;
boolean backward = localForward < -CENTER_DEAD_ANGLE_COS;
boolean left = localSideways < -CENTER_DEAD_ANGLE_COS;
boolean right = localSideways > CENTER_DEAD_ANGLE_COS;
if (!forward && !backward && !left && !right) {
if (Math.abs(localForward) >= Math.abs(localSideways)) {
forward = localForward > 0.0;
backward = localForward < 0.0;
} else {
left = localSideways < 0.0;
right = localSideways > 0.0;
}
}
event.setForward(forward);
event.setBackward(backward);
event.setLeft(left);
event.setRight(right);
}
private void updateSafeCenter(LocalPlayer player) {
Vec3 blockCenter = blockBottomCenter(player);
if (!wouldBeCloseToFallOff(player, blockCenter)) {
center = blockCenter;
}
}
}
public record EdgeCheck(boolean closeToEdge, boolean diagonalRescue, Vec3 movementDirection) {
private static EdgeCheck empty() {
return new EdgeCheck(false, false, Vec3.ZERO);
}
}
}

View file

@ -0,0 +1,51 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.entity;
import com.mojang.authlib.GameProfile;
import net.minecraft.client.multiplayer.ClientLevel;
import net.minecraft.client.player.RemotePlayer;
import java.util.UUID;
public class FreecamEntity extends RemotePlayer {
private static final int FREECAM_ID_BASE = -1_100_000;
private static final int FREECAM_ID_RANGE = 10_000;
private static int nextFreecamEntityId = FREECAM_ID_BASE;
public FreecamEntity(ClientLevel world) {
super(world, new GameProfile(UUID.randomUUID(), "freecam"));
this.setId(allocateFreecamEntityId(world));
this.noPhysics = true;
this.setInvisible(true);
this.setInvulnerable(true);
}
private static int allocateFreecamEntityId(ClientLevel world) {
for (int i = 0; i < FREECAM_ID_RANGE; i++) {
int candidate = nextFreecamEntityId--;
if (nextFreecamEntityId < FREECAM_ID_BASE - FREECAM_ID_RANGE) {
nextFreecamEntityId = FREECAM_ID_BASE;
}
if (world.getEntity(candidate) == null) {
return candidate;
}
}
return FREECAM_ID_BASE;
}
@Override
public boolean shouldShowName() {
return false;
}
@Override
public boolean shouldRenderAtSqrDistance(double distance) {
return false; // якорь: в мире может существовать, но не рисуется
}
}

View file

@ -0,0 +1,311 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.entity.simulation;
import net.minecraft.core.BlockPos;
import net.minecraft.tags.FluidTags;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
import net.minecraft.world.level.block.LilyPadBlock;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.material.FluidState;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.BooleanOp;
import net.minecraft.world.phys.shapes.Shapes;
import net.minecraft.world.phys.shapes.VoxelShape;
import dev.loki.lovisual.events.meta.EventHandler;
import dev.loki.lovisual.events.impl.render.GameTickEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.WeakHashMap;
public final class BoatSimulationCache {
public static final BoatSimulationCache INSTANCE = new BoatSimulationCache();
private static final double BOAT_GRAVITY = 0.04;
private final Map<AbstractBoat, SimulatedBoatCache> cache = new WeakHashMap<>();
private BoatSimulationCache() {
}
public static SimulatedBoatCache getSimulation(AbstractBoat boat) {
return INSTANCE.cache.computeIfAbsent(boat, SimulatedBoatCache::new);
}
@EventHandler
private void onGameTick(GameTickEvent event) {
cache.clear();
}
private enum Location {
IN_WATER,
UNDER_WATER,
UNDER_FLOWING_WATER,
ON_LAND,
IN_AIR
}
public static final class SimulatedBoatCache {
private final SimulatedBoat simulator;
private final List<BoatSnapshot> snapshots = new ArrayList<>();
private int simulatedTicks;
private SimulatedBoatCache(AbstractBoat boat) {
this.simulator = new SimulatedBoat(boat);
this.snapshots.add(new BoatSnapshot(simulator.pos, simulator.yaw));
}
public BoatSnapshot getSnapshotAt(int ticks) {
while (simulatedTicks < ticks) {
simulator.tick();
snapshots.add(new BoatSnapshot(simulator.pos, simulator.yaw));
simulatedTicks++;
}
return snapshots.get(ticks);
}
}
public record BoatSnapshot(Vec3 pos, float yaw) {
}
private static final class SimulatedBoat {
private final AbstractBoat boat;
private final float yaw;
private Vec3 pos;
private Vec3 velocity;
private AABB boundingBox;
private float yawVelocity;
private double waterLevel;
private float nearbySlipperiness;
private Location location;
private Location lastLocation;
private double fallVelocity;
private SimulatedBoat(AbstractBoat boat) {
this.boat = boat;
this.pos = boat.position();
this.velocity = boat.getDeltaMovement();
this.boundingBox = boat.getBoundingBox();
this.yaw = boat.getYRot();
this.yawVelocity = 0.0f;
this.location = checkLocation();
this.lastLocation = location;
}
private void tick() {
lastLocation = location;
location = checkLocation();
updateVelocity();
move();
}
private void move() {
Vec3 adjusted = Entity.collideBoundingBox(boat, velocity, boundingBox, boat.level(), List.of());
pos = pos.add(adjusted);
boundingBox = boat.getBoundingBox().move(pos.subtract(boat.position()));
velocity = adjusted;
}
private Location checkLocation() {
Location underWater = getUnderWaterLocation();
if (underWater != null) {
waterLevel = boundingBox.maxY;
return underWater;
}
if (checkBoatInWater()) {
return Location.IN_WATER;
}
float slipperiness = getNearbySlipperiness();
if (slipperiness > 0.0f) {
nearbySlipperiness = slipperiness;
return Location.ON_LAND;
}
return Location.IN_AIR;
}
private void updateVelocity() {
double gravity = -boat.getGravity();
double buoyancy = 0.0;
float drag = 0.05f;
if (lastLocation == Location.IN_AIR && location != Location.IN_AIR && location != Location.ON_LAND) {
waterLevel = pos.y + boat.getBbHeight();
double waterHeightBelow = getWaterHeightBelow() - boat.getBbHeight() + 0.101;
AABB movedBox = boundingBox.move(0.0, waterHeightBelow - pos.y, 0.0);
if (boat.level().noCollision(boat, movedBox)) {
pos = new Vec3(pos.x, waterHeightBelow, pos.z);
boundingBox = movedBox;
velocity = new Vec3(velocity.x, 0.0, velocity.z);
fallVelocity = 0.0;
}
location = Location.IN_WATER;
} else {
if (location == Location.IN_WATER) {
buoyancy = (waterLevel - pos.y) / boat.getBbHeight();
drag = 0.9f;
} else if (location == Location.UNDER_FLOWING_WATER) {
gravity = -7.0E-4;
drag = 0.9f;
} else if (location == Location.UNDER_WATER) {
buoyancy = 0.01f;
drag = 0.45f;
} else if (location == Location.IN_AIR) {
drag = 0.9f;
} else if (location == Location.ON_LAND) {
drag = nearbySlipperiness;
if (boat.getControllingPassenger() instanceof Player) {
nearbySlipperiness /= 2.0f;
}
}
velocity = new Vec3(velocity.x * drag, velocity.y + gravity, velocity.z * drag);
yawVelocity *= drag;
if (buoyancy > 0.0) {
velocity = new Vec3(
velocity.x,
(velocity.y + buoyancy * (BOAT_GRAVITY / 0.65)) * 0.75,
velocity.z
);
}
}
}
private float getWaterHeightBelow() {
AABB box = boundingBox;
int minX = Mth.floor(box.minX);
int maxX = Mth.ceil(box.maxX);
int minY = Mth.floor(box.maxY);
int maxY = Mth.ceil(box.maxY - fallVelocity);
int minZ = Mth.floor(box.minZ);
int maxZ = Mth.ceil(box.maxZ);
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (int y = minY; y < maxY; y++) {
float fluidHeight = 0.0f;
for (int x = minX; x < maxX; x++) {
for (int z = minZ; z < maxZ; z++) {
mutable.set(x, y, z);
FluidState fluidState = boat.level().getFluidState(mutable);
if (fluidState.is(FluidTags.WATER)) {
fluidHeight = Math.max(fluidHeight, fluidState.getHeight(boat.level(), mutable));
}
if (fluidHeight >= 1.0f) {
break;
}
}
}
if (fluidHeight < 1.0f) {
return mutable.getY() + fluidHeight;
}
}
return maxY + 1.0f;
}
private float getNearbySlipperiness() {
AABB box = boundingBox;
AABB sample = new AABB(box.minX, box.minY - 0.001, box.minZ, box.maxX, box.minY, box.maxZ);
int minX = Mth.floor(sample.minX) - 1;
int maxX = Mth.ceil(sample.maxX) + 1;
int minY = Mth.floor(sample.minY) - 1;
int maxY = Mth.ceil(sample.maxY) + 1;
int minZ = Mth.floor(sample.minZ) - 1;
int maxZ = Mth.ceil(sample.maxZ) + 1;
VoxelShape shape = Shapes.create(sample);
float slipperiness = 0.0f;
int count = 0;
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (int x = minX; x < maxX; x++) {
for (int z = minZ; z < maxZ; z++) {
int edge = (x != minX && x != maxX - 1 ? 0 : 1) + (z != minZ && z != maxZ - 1 ? 0 : 1);
if (edge == 2) {
continue;
}
for (int y = minY; y < maxY; y++) {
if (edge <= 0 || y != minY && y != maxY - 1) {
mutable.set(x, y, z);
BlockState state = boat.level().getBlockState(mutable);
if (!(state.getBlock() instanceof LilyPadBlock)
&& Shapes.joinIsNotEmpty(state.getCollisionShape(boat.level(), mutable).move(mutable), shape, BooleanOp.AND)) {
slipperiness += state.getBlock().getFriction();
count++;
}
}
}
}
}
return count == 0 ? 0.0f : slipperiness / count;
}
private boolean checkBoatInWater() {
AABB box = boundingBox;
int minX = Mth.floor(box.minX);
int maxX = Mth.ceil(box.maxX);
int minY = Mth.floor(box.minY);
int maxY = Mth.ceil(box.minY + 0.001);
int minZ = Mth.floor(box.minZ);
int maxZ = Mth.ceil(box.maxZ);
boolean inWater = false;
waterLevel = -Double.MAX_VALUE;
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (int x = minX; x < maxX; x++) {
for (int y = minY; y < maxY; y++) {
for (int z = minZ; z < maxZ; z++) {
mutable.set(x, y, z);
FluidState fluidState = boat.level().getFluidState(mutable);
if (fluidState.is(FluidTags.WATER)) {
float height = y + fluidState.getHeight(boat.level(), mutable);
waterLevel = Math.max(height, waterLevel);
inWater |= box.minY < height;
}
}
}
}
return inWater;
}
private Location getUnderWaterLocation() {
AABB box = boundingBox;
double top = box.maxY + 0.001;
int minX = Mth.floor(box.minX);
int maxX = Mth.ceil(box.maxX);
int minY = Mth.floor(box.maxY);
int maxY = Mth.ceil(top);
int minZ = Mth.floor(box.minZ);
int maxZ = Mth.ceil(box.maxZ);
boolean still = false;
BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos();
for (int x = minX; x < maxX; x++) {
for (int y = minY; y < maxY; y++) {
for (int z = minZ; z < maxZ; z++) {
mutable.set(x, y, z);
FluidState fluidState = boat.level().getFluidState(mutable);
if (fluidState.is(FluidTags.WATER) && top < mutable.getY() + fluidState.getHeight(boat.level(), mutable)) {
if (!fluidState.isSource()) {
return Location.UNDER_FLOWING_WATER;
}
still = true;
}
}
}
}
return still ? Location.UNDER_WATER : null;
}
}
}

View file

@ -0,0 +1,79 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.entity.simulation;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.entity.vehicle.boat.AbstractBoat;
import net.minecraft.world.phys.Vec3;
import dev.loki.lovisual.util.player.simulation.PlayerSimulationCache;
/**
* Predicts passengers by following the predicted vehicle position and rotating
* the current passenger offset by the vehicle yaw delta.
*/
public enum MountedEntityPrediction {
;
public static Vec3 predictMountedPosition(Entity entity, double ticks) {
if (entity == null) {
return Vec3.ZERO;
}
Entity vehicle = entity.getVehicle();
if (vehicle == null) {
return entity.position();
}
Vec3 predictedVehiclePos = predictVehiclePosition(vehicle, ticks);
float currentVehicleYaw = vehicle.getYRot();
float predictedVehicleYaw = predictVehicleYaw(vehicle, ticks);
Vec3 currentOffset = entity.position().subtract(vehicle.position());
Vec3 localOffset = rotateY(currentOffset, -currentVehicleYaw);
Vec3 rotatedOffset = rotateY(localOffset, predictedVehicleYaw);
return predictedVehiclePos.add(rotatedOffset);
}
private static Vec3 predictVehiclePosition(Entity vehicle, double ticks) {
if (vehicle.isPassenger()) {
return predictMountedPosition(vehicle, ticks);
}
if (vehicle instanceof AbstractBoat boat) {
return BoatSimulationCache.getSimulation(boat).getSnapshotAt((int) Math.ceil(ticks)).pos();
}
if (vehicle instanceof Player player) {
return PlayerSimulationCache.getSimulationForOtherPlayers(player).getSnapshotAt((int) Math.ceil(ticks)).pos();
}
return PositionPredictor.predictEntityPos(vehicle, ticks);
}
private static float predictVehicleYaw(Entity vehicle, double ticks) {
if (vehicle instanceof AbstractBoat boat) {
return BoatSimulationCache.getSimulation(boat).getSnapshotAt((int) Math.ceil(ticks)).yaw();
}
float yaw = vehicle.getYRot();
float yawDelta = Mth.wrapDegrees(yaw - vehicle.yRotO);
return yaw + yawDelta * (float) ticks;
}
private static Vec3 rotateY(Vec3 vec, float yawDegrees) {
double rad = Math.toRadians(yawDegrees);
double sin = Math.sin(rad);
double cos = Math.cos(rad);
double x = vec.x * cos - vec.z * sin;
double z = vec.x * sin + vec.z * cos;
return new Vec3(x, vec.y, z);
}
}

View file

@ -0,0 +1,44 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.entity.simulation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
public enum PositionExtrapolation {
;
public static Extrapolation getBestForEntity(Entity entity) {
return new Extrapolation(entity);
}
public static final class Extrapolation {
private final Entity entity;
private Extrapolation(Entity entity) {
this.entity = entity;
}
public Vec3 getPositionInTicks(double ticks) {
return PositionPredictor.predictEntityPos(entity, ticks);
}
public Vec3 getEyePositionInTicks(double ticks) {
if (entity instanceof LivingEntity living) {
return PositionPredictor.predictEyePos(living, ticks);
}
return getPositionInTicks(ticks);
}
public AABB getBoxInTicks(double ticks) {
return PositionPredictor.predictBox(entity, ticks);
}
}
}

View file

@ -0,0 +1,100 @@
/*
* This file is part of the LoVisual Client distribution.
* Copyright (c) 2026 loki5512344.
*
* Licensed under the GNU General Public License v3.0.
*/
package dev.loki.lovisual.util.entity.simulation;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.Vec3;
import java.lang.ref.WeakReference;
import java.util.Map;
import java.util.WeakHashMap;
/**
* Simple position extrapolation based on current velocity.
*/
public enum PositionPredictor {
;
private static final Map<Entity, State> STATES = new WeakHashMap<>();
public static Vec3 predictEntityPos(Entity entity, double ticks) {
if (entity == null || ticks <= 0) {
return entity != null ? entity.position() : Vec3.ZERO;
}
State state = updateState(entity);
Vec3 base = pickBaseMovement(entity, state);
Vec3 accel = state.prevMovement != null
? state.lastMovement.subtract(state.prevMovement)
: Vec3.ZERO;
double t = ticks;
Vec3 predicted = state.lastPos.add(base.scale(t));
if (state.prevMovement != null) {
predicted = predicted.add(accel.scale(0.5 * t * t));
}
return predicted;
}
public static Vec3 predictEyePos(LivingEntity entity, double ticks) {
if (entity == null) return Vec3.ZERO;
Vec3 pos = predictEntityPos(entity, ticks);
Vec3 eyeOffset = entity.getEyePosition().subtract(entity.position());
return pos.add(eyeOffset);
}
public static AABB predictBox(Entity entity, double ticks) {
if (entity == null) return new AABB(0, 0, 0, 0, 0, 0);
Vec3 predicted = predictEntityPos(entity, ticks);
Vec3 current = entity.position();
Vec3 delta = predicted.subtract(current);
return entity.getBoundingBox().move(delta);
}
private static State updateState(Entity entity) {
State state = STATES.get(entity);
if (state == null) {
state = new State();
state.ref = new WeakReference<>(entity);
STATES.put(entity, state);
}
int age = entity.tickCount;
if (state.lastAge != age) {
state.lastAge = age;
state.lastPos = entity.position();
state.prevMovement = state.lastMovement;
state.lastMovement = entity.getKnownMovement();
state.lastVelocity = entity.getDeltaMovement();
}
return state;
}
private static Vec3 pickBaseMovement(Entity entity, State state) {
Vec3 movement = state.lastMovement != null ? state.lastMovement : entity.getKnownMovement();
Vec3 velocity = state.lastVelocity != null ? state.lastVelocity : entity.getDeltaMovement();
double moveLen = movement.lengthSqr();
double velLen = velocity.lengthSqr();
if (moveLen < 1.0e-6 && velLen > moveLen) {
return velocity;
}
return movement;
}
private static final class State {
private WeakReference<Entity> ref;
private int lastAge = Integer.MIN_VALUE;
private Vec3 lastPos = Vec3.ZERO;
private Vec3 lastMovement = Vec3.ZERO;
private Vec3 prevMovement;
private Vec3 lastVelocity = Vec3.ZERO;
}
}