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:
parent
ec10395511
commit
9d08fa910a
2239 changed files with 35265 additions and 28110 deletions
371
mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java
Normal file
371
mod/src/main/java/dev/loki/lovisual/util/entity/EagleUtil.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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; // якорь: в мире может существовать, но не рисуется
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue