mirror of
https://github.com/MCCTeam/Minecraft-Console-Client
synced 2026-08-15 13:04:36 +00:00
Merge pull request #2953 from BruceChenQAQ/master
feat: implement physics engine for player movement and collision detection
This commit is contained in:
commit
38e86eb209
20 changed files with 2174 additions and 76 deletions
3
.cursorindexingignore
Normal file
3
.cursorindexingignore
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
|
||||
# Don't index SpecStory auto-save files, but allow explicit context inclusion via @ references
|
||||
.specstory/**
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
|
|
@ -423,10 +423,14 @@ FodyWeavers.xsd
|
|||
/docs/.vuepress/public/MCC-README/
|
||||
|
||||
# Floder to store the decompiled Minecraft official source code
|
||||
MinecraftOfficial/
|
||||
/MinecraftOfficial/
|
||||
|
||||
# Possible debug files
|
||||
/lang/*
|
||||
/mcc_input.txt
|
||||
/MinecraftClient.ini
|
||||
/MinecraftClient.backup.ini
|
||||
|
||||
# SpecStory files
|
||||
/.specstory/
|
||||
/.vscode/settings.json
|
||||
|
|
|
|||
|
|
@ -10,13 +10,12 @@ Systematic workflow for updating Minecraft Console Client to support a new Minec
|
|||
## Prerequisites
|
||||
|
||||
- Decompiled server source for both the old and new MC versions in `$MCC_REPO/MinecraftOfficial/<version>-decompiled/`
|
||||
- If missing, decompile first:
|
||||
- If missing, decompile and download server.jar:
|
||||
```bash
|
||||
cd $MCC_REPO/MinecraftOfficial
|
||||
java -jar MinecraftDecompiler.jar --version <ver> --side SERVER \
|
||||
--decompile --output <ver>-remapped.jar --decompiled-output <ver>-decompiled
|
||||
$MCC_REPO/tools/decompile.sh --version <ver>
|
||||
```
|
||||
- A test server of the target version in `$MCC_SERVERS/` (see `mcc-dev-workflow` skill)
|
||||
This auto-downloads `MinecraftDecompiler.jar` if needed, produces the decompiled source, and downloads `server.jar` into `$MCC_SERVERS/<ver>/`.
|
||||
- A test server of the target version in `$MCC_SERVERS/<version>/` (see `mcc-dev-workflow` skill)
|
||||
|
||||
## Step 0: Generate Server Reports (CRITICAL since 1.21.9)
|
||||
|
||||
|
|
@ -24,7 +23,7 @@ Systematic workflow for updating Minecraft Console Client to support a new Minec
|
|||
|
||||
```bash
|
||||
cd /tmp && java -DbundlerMainClass=net.minecraft.data.Main \
|
||||
-jar $MCC_SERVERS/<version>-Vanilla/server.jar \
|
||||
-jar $MCC_SERVERS/<version>/server.jar \
|
||||
--reports --output /tmp/mc_reports
|
||||
```
|
||||
|
||||
|
|
@ -186,7 +185,37 @@ Compare key packet codec classes between versions. Known changes:
|
|||
|
||||
When in doubt, compare the relevant packet class (e.g. `ClientboundAddEntityPacket.java`) between versions.
|
||||
|
||||
## Step 8: Compile and Verify
|
||||
## Step 8: Update Block Collision Shapes (Physics Engine)
|
||||
|
||||
MCC's physics engine uses block collision shape data from PrismarineJS `minecraft-data` to perform accurate AABB collision detection (stored in `MinecraftClient/Physics/BlockShapeData.json`, embedded as a resource).
|
||||
|
||||
When a new MC version introduces new blocks or changes block shapes, update this data:
|
||||
|
||||
```bash
|
||||
# Download and compact collision shapes for the target version
|
||||
python3 $MCC_REPO/tools/gen_block_shapes.py <version>
|
||||
# e.g. python3 tools/gen_block_shapes.py 1.21.11
|
||||
```
|
||||
|
||||
If network is slow or unreliable, download the file manually and convert:
|
||||
```bash
|
||||
# Manual download
|
||||
curl -L -o /tmp/bcs.json \
|
||||
"https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/<version>/blockCollisionShapes.json"
|
||||
|
||||
# Then compact from local file
|
||||
python3 $MCC_REPO/tools/gen_block_shapes.py --from-file /tmp/bcs.json
|
||||
```
|
||||
|
||||
Output: `MinecraftClient/Physics/BlockShapeData.json` (embedded via `MinecraftClient.csproj`)
|
||||
|
||||
The JSON maps block names (snake_case) → collision shape IDs → AABB coordinates. At runtime, `BlockShapes.cs` maps MCC's block state IDs to these AABBs using the block palette.
|
||||
|
||||
**When to update**: Whenever new blocks are added that have non-trivial collision shapes (e.g., new slab variants, stairs, fences). If only items or entities changed, this step can be skipped.
|
||||
|
||||
**Data source**: PrismarineJS `minecraft-data` repo, path: `data/pc/<version>/blockCollisionShapes.json`. Version availability can be checked via `data/dataPaths.json`.
|
||||
|
||||
## Step 9: Compile and Verify
|
||||
|
||||
```bash
|
||||
dotnet build $MCC_REPO/MinecraftClient.sln -c Release
|
||||
|
|
@ -244,3 +273,4 @@ All scripts are in `$MCC_REPO/tools/`. See `tools/README.md` for detailed usage.
|
|||
| `gen_block_palette.py` | Generate BlockPalette C# | blocks.json |
|
||||
| `gen_entity_palette.py` | Generate EntityPalette C# | registries.json |
|
||||
| `gen_entity_metadata_palette.py` | Generate EntityMetadataPalette C# | Decompiled source |
|
||||
| `gen_block_shapes.py` | Download & compact block collision shapes | PrismarineJS minecraft-data |
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ using MinecraftClient.Commands;
|
|||
using MinecraftClient.Inventory;
|
||||
using MinecraftClient.Logger;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Physics;
|
||||
using MinecraftClient.Protocol;
|
||||
using MinecraftClient.Protocol.Handlers;
|
||||
using MinecraftClient.Protocol.Handlers.Forge;
|
||||
|
|
@ -68,6 +69,10 @@ namespace MinecraftClient
|
|||
private float playerYaw;
|
||||
private float playerPitch;
|
||||
private double motionY;
|
||||
private readonly PlayerPhysics playerPhysics = new();
|
||||
private readonly MovementInput physicsInput = new();
|
||||
private bool physicsInitialized = false;
|
||||
private Location? pathTarget; // Current waypoint for physics-driven pathfinding
|
||||
public enum MovementType { Sneak, Walk, Sprint }
|
||||
private int sequenceId; // User for player block synchronization (Aka. digging, placing blocks, etc..)
|
||||
private bool CanSendMessage = false;
|
||||
|
|
@ -495,33 +500,47 @@ namespace MinecraftClient
|
|||
{
|
||||
lock (locationLock)
|
||||
{
|
||||
for (int i = 0; i < Config.Main.Advanced.MovementSpeed; i++) //Needs to run at 20 tps; MCC runs at 10 tps
|
||||
if (!physicsInitialized)
|
||||
{
|
||||
if (_yaw == null || _pitch == null)
|
||||
{
|
||||
if (steps != null && steps.Count > 0)
|
||||
{
|
||||
location = steps.Dequeue();
|
||||
}
|
||||
else if (path != null && path.Count > 0)
|
||||
{
|
||||
Location next = path.Dequeue();
|
||||
steps = Movement.Move2Steps(location, next, ref motionY);
|
||||
|
||||
if (Config.Main.Advanced.MoveHeadWhileWalking) // Disable head movements to avoid anti-cheat triggers
|
||||
UpdateLocation(location, next + new Location(0, 1, 0)); // Update yaw and pitch to look at next step
|
||||
}
|
||||
else
|
||||
{
|
||||
location = Movement.HandleGravity(world, location, ref motionY);
|
||||
}
|
||||
}
|
||||
playerYaw = _yaw == null ? playerYaw : _yaw.Value;
|
||||
playerPitch = _pitch == null ? playerPitch : _pitch.Value;
|
||||
handler.SendLocationUpdate(location, Movement.IsOnGround(world, location), _yaw, _pitch);
|
||||
BlockShapes.Initialize();
|
||||
playerPhysics.SetPosition(location.X, location.Y, location.Z);
|
||||
playerPhysics.Yaw = playerYaw;
|
||||
playerPhysics.Pitch = playerPitch;
|
||||
physicsInitialized = true;
|
||||
}
|
||||
// First 2 updates must be player position AND look, and player must not move (to conform with vanilla)
|
||||
// Once yaw and pitch have been sent, switch back to location-only updates (without yaw and pitch)
|
||||
|
||||
// Run 2 physics ticks per OnUpdate call (10 Hz * 2 = 20 TPS)
|
||||
for (int tick = 0; tick < 2; tick++)
|
||||
{
|
||||
// Navigate pathfinding: set input based on current path
|
||||
UpdatePathfindingInput();
|
||||
|
||||
// Sync yaw/pitch if explicitly set (by commands/bots)
|
||||
if (_yaw != null) playerPhysics.Yaw = _yaw.Value;
|
||||
if (_pitch != null) playerPhysics.Pitch = _pitch.Value;
|
||||
|
||||
// Update environment flags (water, lava, climbable)
|
||||
playerPhysics.UpdateEnvironment(world);
|
||||
|
||||
// Apply movement input
|
||||
playerPhysics.ApplyInput(physicsInput);
|
||||
|
||||
// Run one physics tick
|
||||
playerPhysics.Tick(world);
|
||||
|
||||
// Sync back to MCC location
|
||||
location = new Location(
|
||||
playerPhysics.Position.X,
|
||||
playerPhysics.Position.Y,
|
||||
playerPhysics.Position.Z);
|
||||
|
||||
playerYaw = _yaw ?? playerYaw;
|
||||
playerPitch = _pitch ?? playerPitch;
|
||||
|
||||
// Send position packet
|
||||
handler.SendLocationUpdate(location, playerPhysics.OnGround, _yaw, _pitch);
|
||||
}
|
||||
|
||||
_yaw = null;
|
||||
_pitch = null;
|
||||
}
|
||||
|
|
@ -1335,7 +1354,7 @@ namespace MinecraftClient
|
|||
}
|
||||
else
|
||||
{
|
||||
// Calculate path through pathfinding. Path contains a list of 1-block movement that will be divided into steps
|
||||
pathTarget = null;
|
||||
path = Movement.CalculatePath(world, location, goal, allowUnsafe, maxOffset, minOffset, timeout ?? TimeSpan.FromSeconds(5));
|
||||
return path != null;
|
||||
}
|
||||
|
|
@ -2684,6 +2703,87 @@ namespace MinecraftClient
|
|||
DispatchBotEvent(bot => bot.OnRespawn());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Drive the physics engine input based on the current A* path.
|
||||
/// Converts discrete waypoint pathfinding into continuous movement input.
|
||||
/// </summary>
|
||||
private void UpdatePathfindingInput()
|
||||
{
|
||||
physicsInput.Reset();
|
||||
|
||||
// Still heading toward a target (even if path queue is empty)
|
||||
if (pathTarget != null && ReachedWaypoint(pathTarget.Value))
|
||||
{
|
||||
// Arrived at current waypoint — advance to next, or finish
|
||||
if (path != null && path.Count > 0)
|
||||
{
|
||||
pathTarget = path.Dequeue();
|
||||
if (Config.Main.Advanced.MoveHeadWhileWalking)
|
||||
UpdateLocation(location, pathTarget.Value + new Location(0, 1, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
pathTarget = null;
|
||||
path = null;
|
||||
}
|
||||
}
|
||||
|
||||
// Need a first target from a fresh path
|
||||
if (pathTarget == null && path != null && path.Count > 0)
|
||||
{
|
||||
pathTarget = path.Dequeue();
|
||||
if (Config.Main.Advanced.MoveHeadWhileWalking)
|
||||
UpdateLocation(location, pathTarget.Value + new Location(0, 1, 0));
|
||||
}
|
||||
|
||||
if (pathTarget != null)
|
||||
{
|
||||
SetInputToward(pathTarget.Value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the player has approximately reached a waypoint.
|
||||
/// </summary>
|
||||
private bool ReachedWaypoint(Location target)
|
||||
{
|
||||
double dx = target.X - location.X;
|
||||
double dz = target.Z - location.Z;
|
||||
return dx * dx + dz * dz < 0.25; // within ~0.5 blocks horizontally
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set movement input to walk toward a target location.
|
||||
/// Calculates the yaw needed and sets Forward + Sprint.
|
||||
/// </summary>
|
||||
private void SetInputToward(Location target)
|
||||
{
|
||||
double dx = target.X - location.X;
|
||||
double dz = target.Z - location.Z;
|
||||
double dy = target.Y - location.Y;
|
||||
double distSqr = dx * dx + dz * dz;
|
||||
|
||||
if (distSqr < 0.01) return; // Close enough horizontally
|
||||
|
||||
// Calculate yaw to face target
|
||||
float targetYaw = (float)(-Math.Atan2(dx, dz) / Math.PI * 180.0);
|
||||
if (targetYaw < 0) targetYaw += 360;
|
||||
playerPhysics.Yaw = targetYaw;
|
||||
playerYaw = targetYaw;
|
||||
|
||||
physicsInput.Forward = true;
|
||||
|
||||
// Jump if target is above and we're on ground
|
||||
if (dy > 0.5 && playerPhysics.OnGround)
|
||||
physicsInput.Jump = true;
|
||||
|
||||
// Map MovementSpeed setting: 1=sneak, 2-4=walk, 5=sprint
|
||||
if (Config.Main.Advanced.MovementSpeed >= 5)
|
||||
physicsInput.Sprint = true;
|
||||
else if (Config.Main.Advanced.MovementSpeed <= 1)
|
||||
physicsInput.Sneak = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if the client is currently processing a Movement.
|
||||
/// </summary>
|
||||
|
|
@ -2752,6 +2852,12 @@ namespace MinecraftClient
|
|||
}
|
||||
else this.location = location;
|
||||
locationReceived = true;
|
||||
|
||||
// Sync physics engine position
|
||||
if (physicsInitialized)
|
||||
{
|
||||
playerPhysics.Teleport(this.location.X, this.location.Y, this.location.Z);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@
|
|||
<PropertyGroup>
|
||||
<StartupObject>MinecraftClient.Program</StartupObject>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Physics\BlockShapeData.json" LogicalName="BlockShapeData.json" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Remove="Protocol\Handlers\Compression\**" />
|
||||
<EmbeddedResource Remove="Protocol\Handlers\Compression\**" />
|
||||
|
|
|
|||
195
MinecraftClient/Physics/Aabb.cs
Normal file
195
MinecraftClient/Physics/Aabb.cs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MinecraftClient.Physics
|
||||
{
|
||||
/// <summary>
|
||||
/// Axis-aligned bounding box, mirrors net.minecraft.world.phys.AABB.
|
||||
/// Immutable — mutating methods return new instances.
|
||||
/// </summary>
|
||||
public readonly struct Aabb : IEquatable<Aabb>
|
||||
{
|
||||
public static readonly Aabb Empty = new(0, 0, 0, 0, 0, 0);
|
||||
|
||||
public readonly double MinX, MinY, MinZ;
|
||||
public readonly double MaxX, MaxY, MaxZ;
|
||||
|
||||
public Aabb(double x1, double y1, double z1, double x2, double y2, double z2)
|
||||
{
|
||||
MinX = Math.Min(x1, x2);
|
||||
MinY = Math.Min(y1, y2);
|
||||
MinZ = Math.Min(z1, z2);
|
||||
MaxX = Math.Max(x1, x2);
|
||||
MaxY = Math.Max(y1, y2);
|
||||
MaxZ = Math.Max(z1, z2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Create a player-style AABB centered on feetX/Z with given width and height
|
||||
/// </summary>
|
||||
public static Aabb OfSize(double centerX, double feetY, double centerZ, double width, double height)
|
||||
{
|
||||
double hw = width / 2.0;
|
||||
return new Aabb(centerX - hw, feetY, centerZ - hw, centerX + hw, feetY + height, centerZ + hw);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Full block AABB at given integer position
|
||||
/// </summary>
|
||||
public static Aabb BlockAt(int x, int y, int z) =>
|
||||
new(x, y, z, x + 1.0, y + 1.0, z + 1.0);
|
||||
|
||||
public double XSize => MaxX - MinX;
|
||||
public double YSize => MaxY - MinY;
|
||||
public double ZSize => MaxZ - MinZ;
|
||||
|
||||
public double Min(int axis) => axis switch { 0 => MinX, 1 => MinY, _ => MinZ };
|
||||
public double Max(int axis) => axis switch { 0 => MaxX, 1 => MaxY, _ => MaxZ };
|
||||
|
||||
/// <summary>
|
||||
/// Expand toward a movement direction (vanilla expandTowards)
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Aabb ExpandTowards(double dx, double dy, double dz)
|
||||
{
|
||||
double minX = MinX, minY = MinY, minZ = MinZ;
|
||||
double maxX = MaxX, maxY = MaxY, maxZ = MaxZ;
|
||||
if (dx < 0) minX += dx; else if (dx > 0) maxX += dx;
|
||||
if (dy < 0) minY += dy; else if (dy > 0) maxY += dy;
|
||||
if (dz < 0) minZ += dz; else if (dz > 0) maxZ += dz;
|
||||
return new Aabb(minX, minY, minZ, maxX, maxY, maxZ);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Aabb ExpandTowards(Vec3d v) => ExpandTowards(v.X, v.Y, v.Z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Aabb Inflate(double x, double y, double z) =>
|
||||
new(MinX - x, MinY - y, MinZ - z, MaxX + x, MaxY + y, MaxZ + z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Aabb Inflate(double v) => Inflate(v, v, v);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Aabb Deflate(double x, double y, double z) => Inflate(-x, -y, -z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Aabb Move(double dx, double dy, double dz) =>
|
||||
new(MinX + dx, MinY + dy, MinZ + dz, MaxX + dx, MaxY + dy, MaxZ + dz);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Aabb Move(Vec3d v) => Move(v.X, v.Y, v.Z);
|
||||
|
||||
/// <summary>
|
||||
/// Strict overlap test (vanilla uses < and >, not <=)
|
||||
/// </summary>
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Intersects(Aabb other) =>
|
||||
MinX < other.MaxX && MaxX > other.MinX &&
|
||||
MinY < other.MaxY && MaxY > other.MinY &&
|
||||
MinZ < other.MaxZ && MaxZ > other.MinZ;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Intersects(double x1, double y1, double z1, double x2, double y2, double z2) =>
|
||||
MinX < x2 && MaxX > x1 && MinY < y2 && MaxY > y1 && MinZ < z2 && MaxZ > z1;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public bool Contains(double x, double y, double z) =>
|
||||
x >= MinX && x < MaxX && y >= MinY && y < MaxY && z >= MinZ && z < MaxZ;
|
||||
|
||||
/// <summary>
|
||||
/// Collide this AABB along a single axis against another AABB.
|
||||
/// Returns the clamped movement distance.
|
||||
/// </summary>
|
||||
/// <summary>
|
||||
/// Clip entity movement along X against a block shape (other).
|
||||
/// Vanilla semantics: VoxelShape.collide(Axis.X, entityBox, movement).
|
||||
/// </summary>
|
||||
public double CollideX(Aabb other, double movement)
|
||||
{
|
||||
if (other.MaxY <= MinY || other.MinY >= MaxY || other.MaxZ <= MinZ || other.MinZ >= MaxZ)
|
||||
return movement;
|
||||
if (movement > 0.0 && other.MinX >= MaxX)
|
||||
{
|
||||
double d = other.MinX - MaxX;
|
||||
if (d < movement) movement = d;
|
||||
}
|
||||
else if (movement < 0.0 && other.MaxX <= MinX)
|
||||
{
|
||||
double d = other.MaxX - MinX;
|
||||
if (d > movement) movement = d;
|
||||
}
|
||||
return movement;
|
||||
}
|
||||
|
||||
public double CollideY(Aabb other, double movement)
|
||||
{
|
||||
if (other.MaxX <= MinX || other.MinX >= MaxX || other.MaxZ <= MinZ || other.MinZ >= MaxZ)
|
||||
return movement;
|
||||
if (movement > 0.0 && other.MinY >= MaxY)
|
||||
{
|
||||
double d = other.MinY - MaxY;
|
||||
if (d < movement) movement = d;
|
||||
}
|
||||
else if (movement < 0.0 && other.MaxY <= MinY)
|
||||
{
|
||||
double d = other.MaxY - MinY;
|
||||
if (d > movement) movement = d;
|
||||
}
|
||||
return movement;
|
||||
}
|
||||
|
||||
public double CollideZ(Aabb other, double movement)
|
||||
{
|
||||
if (other.MaxX <= MinX || other.MinX >= MaxX || other.MaxY <= MinY || other.MinY >= MaxY)
|
||||
return movement;
|
||||
if (movement > 0.0 && other.MinZ >= MaxZ)
|
||||
{
|
||||
double d = other.MinZ - MaxZ;
|
||||
if (d < movement) movement = d;
|
||||
}
|
||||
else if (movement < 0.0 && other.MaxZ <= MinZ)
|
||||
{
|
||||
double d = other.MaxZ - MinZ;
|
||||
if (d > movement) movement = d;
|
||||
}
|
||||
return movement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collide along an axis (0=X, 1=Y, 2=Z) against another AABB
|
||||
/// </summary>
|
||||
public double Collide(int axis, Aabb other, double movement)
|
||||
{
|
||||
return axis switch
|
||||
{
|
||||
0 => CollideX(other, movement),
|
||||
1 => CollideY(other, movement),
|
||||
2 => CollideZ(other, movement),
|
||||
_ => movement
|
||||
};
|
||||
}
|
||||
|
||||
public Vec3d GetCenter() => new(
|
||||
(MinX + MaxX) * 0.5,
|
||||
(MinY + MaxY) * 0.5,
|
||||
(MinZ + MaxZ) * 0.5);
|
||||
|
||||
public Vec3d GetBottomCenter() => new(
|
||||
(MinX + MaxX) * 0.5,
|
||||
MinY,
|
||||
(MinZ + MaxZ) * 0.5);
|
||||
|
||||
public bool Equals(Aabb other) =>
|
||||
MinX == other.MinX && MinY == other.MinY && MinZ == other.MinZ &&
|
||||
MaxX == other.MaxX && MaxY == other.MaxY && MaxZ == other.MaxZ;
|
||||
|
||||
public override bool Equals(object? obj) => obj is Aabb a && Equals(a);
|
||||
public override int GetHashCode() => HashCode.Combine(MinX, MinY, MinZ, MaxX, MaxY, MaxZ);
|
||||
public override string ToString() => $"AABB[{MinX:F3},{MinY:F3},{MinZ:F3} -> {MaxX:F3},{MaxY:F3},{MaxZ:F3}]";
|
||||
|
||||
public static bool operator ==(Aabb a, Aabb b) => a.Equals(b);
|
||||
public static bool operator !=(Aabb a, Aabb b) => !a.Equals(b);
|
||||
}
|
||||
}
|
||||
1
MinecraftClient/Physics/BlockShapeData.json
Normal file
1
MinecraftClient/Physics/BlockShapeData.json
Normal file
File diff suppressed because one or more lines are too long
233
MinecraftClient/Physics/BlockShapes.cs
Normal file
233
MinecraftClient/Physics/BlockShapes.cs
Normal file
|
|
@ -0,0 +1,233 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using MinecraftClient.Mapping;
|
||||
using MinecraftClient.Mapping.BlockPalettes;
|
||||
|
||||
namespace MinecraftClient.Physics
|
||||
{
|
||||
/// <summary>
|
||||
/// Registry of block collision shapes. Maps block state IDs to collision AABBs.
|
||||
/// Data sourced from PrismarineJS/minecraft-data blockCollisionShapes.json.
|
||||
/// </summary>
|
||||
public static class BlockShapes
|
||||
{
|
||||
private static readonly Aabb FullBlock = new(0, 0, 0, 1, 1, 1);
|
||||
private static readonly Aabb[] FullBlockArray = { FullBlock };
|
||||
private static readonly Aabb[] EmptyArray = Array.Empty<Aabb>();
|
||||
|
||||
private static Dictionary<int, Aabb[]>? stateToShape;
|
||||
private static Dictionary<string, object>? prismarineBlocks;
|
||||
private static Dictionary<int, Aabb[]>? prismarineShapes;
|
||||
|
||||
/// <summary>
|
||||
/// Initialize the shape registry from embedded data + current palette.
|
||||
/// Call once after the block palette is set.
|
||||
/// </summary>
|
||||
public static void Initialize()
|
||||
{
|
||||
LoadPrismarineData();
|
||||
BuildStateMap();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get collision shapes for a block state ID.
|
||||
/// Returns empty array for air/passable blocks, single full-block for solid cubes, etc.
|
||||
/// </summary>
|
||||
public static Aabb[] GetShapes(int blockStateId)
|
||||
{
|
||||
if (stateToShape != null && stateToShape.TryGetValue(blockStateId, out var shapes))
|
||||
return shapes;
|
||||
return FallbackShape(blockStateId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get collision shapes for a Block at a specific position (state-aware)
|
||||
/// </summary>
|
||||
public static Aabb[] GetShapes(Block block) => GetShapes(block.BlockId);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a block state is effectively empty (no collision)
|
||||
/// </summary>
|
||||
public static bool IsEmpty(int blockStateId)
|
||||
{
|
||||
var shapes = GetShapes(blockStateId);
|
||||
return shapes.Length == 0;
|
||||
}
|
||||
|
||||
private static Aabb[] FallbackShape(int blockStateId)
|
||||
{
|
||||
Material mat = Block.Palette.FromId(blockStateId);
|
||||
if (mat == Material.Air) return EmptyArray;
|
||||
if (mat.IsLiquid()) return EmptyArray;
|
||||
if (mat.IsSolid()) return FullBlockArray;
|
||||
return EmptyArray;
|
||||
}
|
||||
|
||||
private static void LoadPrismarineData()
|
||||
{
|
||||
prismarineBlocks = new Dictionary<string, object>();
|
||||
prismarineShapes = new Dictionary<int, Aabb[]>();
|
||||
|
||||
try
|
||||
{
|
||||
var assembly = Assembly.GetExecutingAssembly();
|
||||
using var stream = assembly.GetManifestResourceStream("BlockShapeData.json");
|
||||
if (stream == null)
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted("§e[Physics] BlockShapeData.json not found as embedded resource");
|
||||
return;
|
||||
}
|
||||
using var doc = JsonDocument.Parse(stream);
|
||||
var root = doc.RootElement;
|
||||
|
||||
// Parse shapes: shapeId -> list of AABB boxes
|
||||
if (root.TryGetProperty("shapes", out var shapesEl))
|
||||
{
|
||||
foreach (var prop in shapesEl.EnumerateObject())
|
||||
{
|
||||
if (int.TryParse(prop.Name, out int shapeId))
|
||||
{
|
||||
var boxes = new List<Aabb>();
|
||||
foreach (var boxEl in prop.Value.EnumerateArray())
|
||||
{
|
||||
var coords = new double[6];
|
||||
int idx = 0;
|
||||
foreach (var c in boxEl.EnumerateArray())
|
||||
{
|
||||
if (idx < 6) coords[idx++] = c.GetDouble();
|
||||
}
|
||||
if (idx == 6)
|
||||
boxes.Add(new Aabb(coords[0], coords[1], coords[2], coords[3], coords[4], coords[5]));
|
||||
}
|
||||
prismarineShapes[shapeId] = boxes.ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Parse blocks: blockName -> shapeId (int) or list of shapeIds
|
||||
if (root.TryGetProperty("blocks", out var blocksEl))
|
||||
{
|
||||
foreach (var prop in blocksEl.EnumerateObject())
|
||||
{
|
||||
string blockName = prop.Name;
|
||||
if (prop.Value.ValueKind == JsonValueKind.Number)
|
||||
{
|
||||
prismarineBlocks[blockName] = prop.Value.GetInt32();
|
||||
}
|
||||
else if (prop.Value.ValueKind == JsonValueKind.Array)
|
||||
{
|
||||
var ids = new List<int>();
|
||||
foreach (var el in prop.Value.EnumerateArray())
|
||||
ids.Add(el.GetInt32());
|
||||
prismarineBlocks[blockName] = ids;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ConsoleInteractive.ConsoleWriter.WriteLineFormatted($"§e[Physics] Failed to load BlockShapeData.json: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void BuildStateMap()
|
||||
{
|
||||
stateToShape = new Dictionary<int, Aabb[]>();
|
||||
|
||||
if (prismarineBlocks == null || prismarineShapes == null)
|
||||
return;
|
||||
|
||||
var palette = Block.Palette;
|
||||
var dict = GetPaletteDict(palette);
|
||||
if (dict == null) return;
|
||||
|
||||
// Group consecutive state IDs by Material to find state ranges per block
|
||||
var materialRanges = new Dictionary<Material, List<(int start, int end)>>();
|
||||
int? rangeStart = null;
|
||||
Material? currentMat = null;
|
||||
|
||||
foreach (var kvp in dict.OrderBy(k => k.Key))
|
||||
{
|
||||
if (currentMat == kvp.Value && rangeStart.HasValue && kvp.Key == (materialRanges[currentMat.Value].Last().end + 1))
|
||||
{
|
||||
var ranges = materialRanges[currentMat.Value];
|
||||
ranges[ranges.Count - 1] = (ranges.Last().start, kvp.Key);
|
||||
}
|
||||
else
|
||||
{
|
||||
currentMat = kvp.Value;
|
||||
if (!materialRanges.ContainsKey(currentMat.Value))
|
||||
materialRanges[currentMat.Value] = new List<(int, int)>();
|
||||
materialRanges[currentMat.Value].Add((kvp.Key, kvp.Key));
|
||||
}
|
||||
}
|
||||
|
||||
// Map each Material to PrismarineJS block name
|
||||
foreach (var kvp in materialRanges)
|
||||
{
|
||||
string snakeName = MaterialToSnakeCase(kvp.Key);
|
||||
if (!prismarineBlocks.TryGetValue(snakeName, out var blockShapeData))
|
||||
continue;
|
||||
|
||||
foreach (var (start, end) in kvp.Value)
|
||||
{
|
||||
int stateCount = end - start + 1;
|
||||
|
||||
if (blockShapeData is int singleShapeId)
|
||||
{
|
||||
var shapes = prismarineShapes.GetValueOrDefault(singleShapeId, EmptyArray);
|
||||
for (int sid = start; sid <= end; sid++)
|
||||
stateToShape[sid] = shapes;
|
||||
}
|
||||
else if (blockShapeData is List<int> shapeIdList)
|
||||
{
|
||||
for (int i = 0; i < stateCount && i < shapeIdList.Count; i++)
|
||||
{
|
||||
int shapeId = shapeIdList[i];
|
||||
stateToShape[start + i] = prismarineShapes.GetValueOrDefault(shapeId, EmptyArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert Material enum name (PascalCase) to snake_case block name
|
||||
/// </summary>
|
||||
private static string MaterialToSnakeCase(Material mat)
|
||||
{
|
||||
string name = mat.ToString();
|
||||
var sb = new System.Text.StringBuilder(name.Length + 5);
|
||||
for (int i = 0; i < name.Length; i++)
|
||||
{
|
||||
char c = name[i];
|
||||
if (char.IsUpper(c) && i > 0)
|
||||
sb.Append('_');
|
||||
sb.Append(char.ToLowerInvariant(c));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Access the internal dictionary of a palette via reflection (all palettes store it the same way)
|
||||
/// </summary>
|
||||
private static Dictionary<int, Material>? GetPaletteDict(BlockPalette palette)
|
||||
{
|
||||
try
|
||||
{
|
||||
var method = palette.GetType().GetMethod("GetDict",
|
||||
BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.FlattenHierarchy);
|
||||
return method?.Invoke(palette, null) as Dictionary<int, Material>;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
204
MinecraftClient/Physics/CollisionDetector.cs
Normal file
204
MinecraftClient/Physics/CollisionDetector.cs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Physics
|
||||
{
|
||||
/// <summary>
|
||||
/// Performs AABB collision detection against the block world.
|
||||
/// Mirrors Entity.collide(), collideBoundingBox(), collideWithShapes() from vanilla MC.
|
||||
/// </summary>
|
||||
public static class CollisionDetector
|
||||
{
|
||||
/// <summary>
|
||||
/// Resolve movement with full collision detection including step-up.
|
||||
/// This is the main entry point, equivalent to Entity.collide(Vec3).
|
||||
/// </summary>
|
||||
public static Vec3d Collide(World world, Aabb entityBox, Vec3d movement, bool onGround, float maxUpStep)
|
||||
{
|
||||
if (movement.LengthSqr() == 0.0)
|
||||
return movement;
|
||||
|
||||
// Collect block collision shapes in the movement path
|
||||
var colliders = CollectBlockColliders(world, entityBox.ExpandTowards(movement));
|
||||
Vec3d resolved = CollideWithShapes(movement, entityBox, colliders);
|
||||
|
||||
bool blockedX = movement.X != resolved.X;
|
||||
bool blockedZ = movement.Z != resolved.Z;
|
||||
bool blockedY = movement.Y != resolved.Y;
|
||||
bool hitGroundDuringMove = blockedY && movement.Y < 0.0;
|
||||
|
||||
// Step-up logic: if blocked horizontally and on ground or just landed
|
||||
if (maxUpStep > 0.0f && (hitGroundDuringMove || onGround) && (blockedX || blockedZ))
|
||||
{
|
||||
// Try stepping up
|
||||
Aabb stepBase = hitGroundDuringMove ? entityBox.Move(0, resolved.Y, 0) : entityBox;
|
||||
Aabb expanded = stepBase.ExpandTowards(movement.X, maxUpStep, movement.Z)
|
||||
.ExpandTowards(0, hitGroundDuringMove ? 0 : -1.0E-5, 0);
|
||||
|
||||
var stepColliders = CollectBlockColliders(world, expanded);
|
||||
|
||||
// Try various step heights
|
||||
float[] candidateHeights = CollectCandidateStepHeights(stepBase, stepColliders, maxUpStep, (float)resolved.Y);
|
||||
|
||||
foreach (float stepY in candidateHeights)
|
||||
{
|
||||
Vec3d stepMovement = new Vec3d(movement.X, stepY, movement.Z);
|
||||
Vec3d stepResolved = CollideWithShapes(stepMovement, stepBase, stepColliders);
|
||||
|
||||
if (stepResolved.HorizontalDistanceSqr() > resolved.HorizontalDistanceSqr())
|
||||
{
|
||||
double yOffset = entityBox.MinY - stepBase.MinY;
|
||||
return stepResolved.Subtract(0, yOffset, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collide movement against a list of shapes using axis-separated resolution.
|
||||
/// Matches Entity.collideWithShapes() — processes axes in order of smallest movement first.
|
||||
/// </summary>
|
||||
private static Vec3d CollideWithShapes(Vec3d movement, Aabb entityBox, List<Aabb> colliders)
|
||||
{
|
||||
if (colliders.Count == 0)
|
||||
return movement;
|
||||
|
||||
Vec3d accumulated = Vec3d.Zero;
|
||||
int[] axisOrder = GetAxisStepOrder(movement);
|
||||
|
||||
foreach (int axis in axisOrder)
|
||||
{
|
||||
double dist = movement.Get(axis);
|
||||
if (dist == 0.0) continue;
|
||||
|
||||
double resolved = CollideAxis(axis, entityBox.Move(accumulated), colliders, dist);
|
||||
accumulated = accumulated.With(axis, resolved);
|
||||
}
|
||||
|
||||
return accumulated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get axis processing order: Y first if moving down, otherwise smallest absolute movement first.
|
||||
/// Vanilla uses Direction.axisStepOrder(Vec3) which returns axes sorted by absolute movement.
|
||||
/// </summary>
|
||||
private static int[] GetAxisStepOrder(Vec3d movement)
|
||||
{
|
||||
double absX = Math.Abs(movement.X);
|
||||
double absY = Math.Abs(movement.Y);
|
||||
double absZ = Math.Abs(movement.Z);
|
||||
|
||||
if (absX > absZ)
|
||||
{
|
||||
if (absZ > absY)
|
||||
return new[] { 1, 2, 0 }; // Y Z X
|
||||
if (absX > absY)
|
||||
return new[] { 1, 0, 2 }; // Y X Z
|
||||
return new[] { 0, 1, 2 }; // X Y Z
|
||||
}
|
||||
else
|
||||
{
|
||||
if (absX > absY)
|
||||
return new[] { 1, 0, 2 }; // Y X Z
|
||||
if (absZ > absY)
|
||||
return new[] { 1, 2, 0 }; // Y Z X
|
||||
return new[] { 2, 1, 0 }; // Z Y X
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collide along a single axis against all block shapes.
|
||||
/// Equivalent to Shapes.collide(axis, box, shapes, distance).
|
||||
/// </summary>
|
||||
private static double CollideAxis(int axis, Aabb entityBox, List<Aabb> colliders, double movement)
|
||||
{
|
||||
foreach (var collider in colliders)
|
||||
{
|
||||
if (Math.Abs(movement) < PhysicsConsts.CollisionEpsilon)
|
||||
return 0.0;
|
||||
movement = entityBox.Collide(axis, collider, movement);
|
||||
}
|
||||
return movement;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect all block collision AABBs that overlap the given search area.
|
||||
/// Equivalent to BlockCollisions iterator in vanilla.
|
||||
/// </summary>
|
||||
public static List<Aabb> CollectBlockColliders(World world, Aabb searchBox)
|
||||
{
|
||||
var result = new List<Aabb>();
|
||||
|
||||
int minBX = (int)Math.Floor(searchBox.MinX - PhysicsConsts.CollisionEpsilon) - 1;
|
||||
int maxBX = (int)Math.Floor(searchBox.MaxX + PhysicsConsts.CollisionEpsilon) + 1;
|
||||
int minBY = (int)Math.Floor(searchBox.MinY - PhysicsConsts.CollisionEpsilon) - 1;
|
||||
int maxBY = (int)Math.Floor(searchBox.MaxY + PhysicsConsts.CollisionEpsilon) + 1;
|
||||
int minBZ = (int)Math.Floor(searchBox.MinZ - PhysicsConsts.CollisionEpsilon) - 1;
|
||||
int maxBZ = (int)Math.Floor(searchBox.MaxZ + PhysicsConsts.CollisionEpsilon) + 1;
|
||||
|
||||
for (int bx = minBX; bx <= maxBX; bx++)
|
||||
{
|
||||
for (int bz = minBZ; bz <= maxBZ; bz++)
|
||||
{
|
||||
for (int by = minBY; by <= maxBY; by++)
|
||||
{
|
||||
Block block = world.GetBlock(new Location(bx, by, bz));
|
||||
Aabb[] shapes = BlockShapes.GetShapes(block);
|
||||
|
||||
foreach (var shape in shapes)
|
||||
{
|
||||
Aabb worldShape = shape.Move(bx, by, bz);
|
||||
if (worldShape.Intersects(searchBox))
|
||||
result.Add(worldShape);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Collect candidate step-up heights, matching Entity.collectCandidateStepUpHeights().
|
||||
/// Returns sorted distinct step heights between current resolved Y and maxUpStep.
|
||||
/// </summary>
|
||||
private static float[] CollectCandidateStepHeights(Aabb stepBase, List<Aabb> colliders, float maxUpStep, float currentY)
|
||||
{
|
||||
var heights = new SortedSet<float>();
|
||||
|
||||
foreach (var collider in colliders)
|
||||
{
|
||||
float h = (float)(collider.MaxY - stepBase.MinY);
|
||||
if (h > currentY && h <= maxUpStep)
|
||||
heights.Add(h);
|
||||
}
|
||||
|
||||
if (heights.Count == 0)
|
||||
return new[] { maxUpStep };
|
||||
|
||||
var result = new float[heights.Count];
|
||||
heights.CopyTo(result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a position is on ground by testing for vertical collision below.
|
||||
/// </summary>
|
||||
public static bool IsOnGround(World world, Aabb entityBox)
|
||||
{
|
||||
Aabb testBox = entityBox.ExpandTowards(0, -0.06, 0);
|
||||
return CollectBlockColliders(world, testBox).Count > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Check if a given position has no collision (for checking if player fits somewhere).
|
||||
/// </summary>
|
||||
public static bool NoCollision(World world, Aabb entityBox)
|
||||
{
|
||||
return CollectBlockColliders(world, entityBox).Count == 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
55
MinecraftClient/Physics/MovementInput.cs
Normal file
55
MinecraftClient/Physics/MovementInput.cs
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
using System;
|
||||
|
||||
namespace MinecraftClient.Physics
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents movement input state, equivalent to vanilla ClientInput / KeyboardInput.
|
||||
/// </summary>
|
||||
public class MovementInput
|
||||
{
|
||||
public bool Forward;
|
||||
public bool Back;
|
||||
public bool Left;
|
||||
public bool Right;
|
||||
public bool Jump;
|
||||
public bool Sneak;
|
||||
public bool Sprint;
|
||||
|
||||
/// <summary>
|
||||
/// Get the raw input vector (xxa, zza) before rotation.
|
||||
/// Forward = +zza, Back = -zza, Left = +xxa, Right = -xxa.
|
||||
/// Then normalized if magnitude > 1.
|
||||
/// </summary>
|
||||
public (float xxa, float zza) GetMoveVector()
|
||||
{
|
||||
float xxa = 0;
|
||||
float zza = 0;
|
||||
|
||||
if (Forward) zza += 1.0f;
|
||||
if (Back) zza -= 1.0f;
|
||||
if (Left) xxa += 1.0f;
|
||||
if (Right) xxa -= 1.0f;
|
||||
|
||||
float lenSqr = xxa * xxa + zza * zza;
|
||||
if (lenSqr > 1.0f)
|
||||
{
|
||||
float len = MathF.Sqrt(lenSqr);
|
||||
xxa /= len;
|
||||
zza /= len;
|
||||
}
|
||||
|
||||
return (xxa, zza);
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
Forward = false;
|
||||
Back = false;
|
||||
Left = false;
|
||||
Right = false;
|
||||
Jump = false;
|
||||
Sneak = false;
|
||||
Sprint = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
MinecraftClient/Physics/PhysicsConsts.cs
Normal file
87
MinecraftClient/Physics/PhysicsConsts.cs
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
namespace MinecraftClient.Physics
|
||||
{
|
||||
/// <summary>
|
||||
/// All physics constants matching vanilla Minecraft 1.21.11.
|
||||
/// Values sourced from Entity.java, LivingEntity.java, Player.java, LocalPlayer.java.
|
||||
/// </summary>
|
||||
public static class PhysicsConsts
|
||||
{
|
||||
// --- Player dimensions ---
|
||||
public const double PlayerWidth = 0.6;
|
||||
public const double PlayerHeight = 1.8;
|
||||
public const double PlayerSneakHeight = 1.5;
|
||||
public const double PlayerSwimHeight = 0.6;
|
||||
public const double PlayerEyeHeight = 1.62;
|
||||
|
||||
// --- Gravity ---
|
||||
public const double DefaultGravity = 0.08;
|
||||
public const double SlowFallingCap = 0.01;
|
||||
|
||||
// --- Step height ---
|
||||
public const float StepHeight = 0.6f;
|
||||
|
||||
// --- Friction / drag ---
|
||||
public const float FrictionMultiplier = 0.91f;
|
||||
public const float DragY = 0.98f;
|
||||
public const float InputFriction = 0.98f;
|
||||
public const float GroundAccelerationFactor = 0.21600002f; // 0.216 / (f^3)
|
||||
public const float AirAcceleration = 0.02f;
|
||||
|
||||
// --- Default block friction ---
|
||||
public const float DefaultBlockFriction = 0.6f;
|
||||
public const float IceFriction = 0.98f;
|
||||
public const float PackedIceFriction = 0.98f;
|
||||
public const float BlueIceFriction = 0.989f;
|
||||
public const float SlimeBlockFriction = 0.8f;
|
||||
|
||||
// --- Speed factors ---
|
||||
public const float DefaultSpeedFactor = 1.0f;
|
||||
public const float SoulSandSpeedFactor = 0.4f;
|
||||
public const float HoneySpeedFactor = 0.4f;
|
||||
|
||||
// --- Water ---
|
||||
public const float WaterSlowDown = 0.8f;
|
||||
public const float WaterSprintSlowDown = 0.9f;
|
||||
public const float DolphinsGraceSlowDown = 0.96f;
|
||||
public const float WaterBaseSpeed = 0.02f;
|
||||
public const float WaterYDamping = 0.8f;
|
||||
public const float WaterFloatImpulse = 0.04f;
|
||||
|
||||
// --- Lava ---
|
||||
public const float LavaSpeed = 0.02f;
|
||||
public const double LavaHorizontalDamping = 0.5;
|
||||
public const double LavaVerticalDamping = 0.8;
|
||||
|
||||
// --- Jump ---
|
||||
public const float BaseJumpPower = 0.42f;
|
||||
public const double SprintJumpHorizontalBoost = 0.2;
|
||||
|
||||
// --- Climb ---
|
||||
public const float ClimbMaxSpeed = 0.15f;
|
||||
public const double ClimbWallBump = 0.2;
|
||||
|
||||
// --- Velocity zeroing thresholds (from LivingEntity.aiStep) ---
|
||||
public const double PlayerHorizontalVelocityThresholdSqr = 9.0E-6; // < 0.003 length
|
||||
public const double NonPlayerVelocityThreshold = 0.003;
|
||||
public const double VerticalVelocityThreshold = 0.003;
|
||||
|
||||
// --- Collision epsilon ---
|
||||
public const double CollisionEpsilon = 1.0E-7;
|
||||
|
||||
// --- Position packet sending (from LocalPlayer.sendPosition) ---
|
||||
public const double PositionSendThresholdSqr = 4.0E-8; // (2e-4)^2
|
||||
public const int PositionReminderInterval = 20;
|
||||
|
||||
// --- Flying detection ---
|
||||
public const double FloatingYThreshold = -0.03125;
|
||||
|
||||
// --- Elytra ---
|
||||
public const double ElytraXZDrag = 0.99;
|
||||
public const double ElytraYDrag = 0.98;
|
||||
|
||||
// --- Creative/spectator fly ---
|
||||
public const float DefaultFlySpeed = 0.05f;
|
||||
public const double FlyVerticalDamping = 0.6;
|
||||
public const double FlyVerticalBoostScale = 3.0;
|
||||
}
|
||||
}
|
||||
579
MinecraftClient/Physics/PlayerPhysics.cs
Normal file
579
MinecraftClient/Physics/PlayerPhysics.cs
Normal file
|
|
@ -0,0 +1,579 @@
|
|||
using System;
|
||||
using MinecraftClient.Mapping;
|
||||
|
||||
namespace MinecraftClient.Physics
|
||||
{
|
||||
/// <summary>
|
||||
/// Core physics tick engine for the player, faithfully replicating vanilla 1.21.11 physics.
|
||||
/// Mirrors the combined logic of Entity.move(), LivingEntity.aiStep()/travel()/travelInAir(),
|
||||
/// Player.travel(), and LocalPlayer.aiStep().
|
||||
/// </summary>
|
||||
public class PlayerPhysics
|
||||
{
|
||||
// --- State ---
|
||||
public Vec3d Position;
|
||||
public Vec3d DeltaMovement;
|
||||
public float Yaw;
|
||||
public float Pitch;
|
||||
public bool OnGround;
|
||||
public bool HorizontalCollision;
|
||||
public bool VerticalCollision;
|
||||
public bool VerticalCollisionBelow;
|
||||
public double FallDistance;
|
||||
public Vec3d StuckSpeedMultiplier = Vec3d.Zero;
|
||||
|
||||
// Movement input
|
||||
public float Xxa; // strafe
|
||||
public float Zza; // forward
|
||||
public float Yya; // vertical (creative fly)
|
||||
public bool Jumping;
|
||||
|
||||
// Movement mode flags
|
||||
public bool Sprinting;
|
||||
public bool Sneaking;
|
||||
public bool CreativeFlying;
|
||||
public bool InWater;
|
||||
public bool InLava;
|
||||
public bool OnClimbable;
|
||||
public bool HasSlowFalling;
|
||||
public bool HasLevitation;
|
||||
public int LevitationAmplifier;
|
||||
|
||||
// Player dimensions
|
||||
public double PlayerWidth = PhysicsConsts.PlayerWidth;
|
||||
public double PlayerHeight = PhysicsConsts.PlayerHeight;
|
||||
|
||||
// Anti-jump-spam
|
||||
private int noJumpDelay;
|
||||
|
||||
// Tick counter for position packet timing
|
||||
public int TickCount;
|
||||
|
||||
// Movement speed attribute (base = 0.1 for players)
|
||||
public float MovementSpeed = 0.1f;
|
||||
|
||||
/// <summary>
|
||||
/// Get the player's bounding box at current position
|
||||
/// </summary>
|
||||
public Aabb GetBoundingBox()
|
||||
{
|
||||
return Aabb.OfSize(Position.X, Position.Y, Position.Z, PlayerWidth, PlayerHeight);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Run one physics tick. Call at 20 TPS.
|
||||
/// </summary>
|
||||
public void Tick(World world)
|
||||
{
|
||||
TickCount++;
|
||||
|
||||
// Velocity threshold zeroing (LivingEntity.aiStep)
|
||||
ZeroTinyVelocity();
|
||||
|
||||
// Jump handling
|
||||
HandleJumping(world);
|
||||
|
||||
// Build travel input
|
||||
Vec3d travelInput = new(Xxa, Yya, Zza);
|
||||
|
||||
// Travel (dispatches to air/water/lava/fly)
|
||||
Travel(world, travelInput);
|
||||
|
||||
if (noJumpDelay > 0)
|
||||
noJumpDelay--;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply the movement input from MovementInput to xxa/zza.
|
||||
/// Call before Tick() each frame.
|
||||
/// </summary>
|
||||
public void ApplyInput(MovementInput input)
|
||||
{
|
||||
var (rawXxa, rawZza) = input.GetMoveVector();
|
||||
|
||||
// Scale by INPUT_FRICTION (0.98) — this matches LocalPlayer.modifyInput
|
||||
rawXxa *= PhysicsConsts.InputFriction;
|
||||
rawZza *= PhysicsConsts.InputFriction;
|
||||
|
||||
// Sneak slowdown
|
||||
if (input.Sneak)
|
||||
{
|
||||
rawXxa *= 0.3f;
|
||||
rawZza *= 0.3f;
|
||||
}
|
||||
|
||||
Xxa = rawXxa;
|
||||
Zza = rawZza;
|
||||
Yya = 0;
|
||||
Jumping = input.Jump;
|
||||
Sneaking = input.Sneak;
|
||||
Sprinting = input.Sprint;
|
||||
|
||||
// Creative/spectator fly vertical
|
||||
if (CreativeFlying)
|
||||
{
|
||||
if (input.Jump)
|
||||
Yya += (float)(PhysicsConsts.DefaultFlySpeed * PhysicsConsts.FlyVerticalBoostScale);
|
||||
if (input.Sneak)
|
||||
Yya -= (float)(PhysicsConsts.DefaultFlySpeed * PhysicsConsts.FlyVerticalBoostScale);
|
||||
}
|
||||
}
|
||||
|
||||
private void ZeroTinyVelocity()
|
||||
{
|
||||
double dx = DeltaMovement.X;
|
||||
double dy = DeltaMovement.Y;
|
||||
double dz = DeltaMovement.Z;
|
||||
|
||||
// Player-specific: zero horizontal if combined length < 0.003
|
||||
if (dx * dx + dz * dz < PhysicsConsts.PlayerHorizontalVelocityThresholdSqr)
|
||||
{
|
||||
dx = 0;
|
||||
dz = 0;
|
||||
}
|
||||
if (Math.Abs(dy) < PhysicsConsts.VerticalVelocityThreshold)
|
||||
dy = 0;
|
||||
|
||||
DeltaMovement = new Vec3d(dx, dy, dz);
|
||||
}
|
||||
|
||||
private void HandleJumping(World world)
|
||||
{
|
||||
if (!Jumping) { noJumpDelay = 0; return; }
|
||||
|
||||
if (InWater || InLava)
|
||||
{
|
||||
// Jump in fluid: add upward impulse
|
||||
DeltaMovement = DeltaMovement.Add(0, PhysicsConsts.WaterFloatImpulse, 0);
|
||||
}
|
||||
else if (OnGround && noJumpDelay == 0)
|
||||
{
|
||||
JumpFromGround();
|
||||
noJumpDelay = 10;
|
||||
}
|
||||
}
|
||||
|
||||
private void JumpFromGround()
|
||||
{
|
||||
float jumpPower = PhysicsConsts.BaseJumpPower;
|
||||
if (jumpPower <= 1.0E-5f) return;
|
||||
|
||||
DeltaMovement = new Vec3d(
|
||||
DeltaMovement.X,
|
||||
Math.Max(jumpPower, DeltaMovement.Y),
|
||||
DeltaMovement.Z);
|
||||
|
||||
if (Sprinting)
|
||||
{
|
||||
float yawRad = Yaw * (MathF.PI / 180.0f);
|
||||
DeltaMovement = DeltaMovement.Add(
|
||||
-MathF.Sin(yawRad) * PhysicsConsts.SprintJumpHorizontalBoost,
|
||||
0,
|
||||
MathF.Cos(yawRad) * PhysicsConsts.SprintJumpHorizontalBoost);
|
||||
}
|
||||
}
|
||||
|
||||
private void Travel(World world, Vec3d input)
|
||||
{
|
||||
if (InWater && !CreativeFlying)
|
||||
{
|
||||
TravelInWater(world, input);
|
||||
}
|
||||
else if (InLava && !CreativeFlying)
|
||||
{
|
||||
TravelInLava(world, input);
|
||||
}
|
||||
else
|
||||
{
|
||||
TravelInAir(world, input);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ground/air travel — LivingEntity.travelInAir(Vec3)
|
||||
/// </summary>
|
||||
private void TravelInAir(World world, Vec3d input)
|
||||
{
|
||||
// Get block friction at feet
|
||||
float blockFriction = OnGround ? GetBlockFriction(world) : 1.0f;
|
||||
float f = blockFriction * PhysicsConsts.FrictionMultiplier;
|
||||
|
||||
// Apply input → velocity (handleRelativeFrictionAndCalculateMovement)
|
||||
float speed = GetFrictionInfluencedSpeed(blockFriction);
|
||||
MoveRelative(speed, input);
|
||||
|
||||
// Handle climbable
|
||||
HandleOnClimbable();
|
||||
|
||||
// Execute collision
|
||||
Move(world, DeltaMovement);
|
||||
|
||||
Vec3d postMoveVel = DeltaMovement;
|
||||
double vy = postMoveVel.Y;
|
||||
|
||||
// Climbing wall bump
|
||||
if ((HorizontalCollision || Jumping) && OnClimbable)
|
||||
{
|
||||
vy = PhysicsConsts.ClimbWallBump;
|
||||
}
|
||||
|
||||
// Apply gravity
|
||||
if (HasLevitation)
|
||||
{
|
||||
vy += (0.05 * (LevitationAmplifier + 1) - vy) * 0.2;
|
||||
}
|
||||
else
|
||||
{
|
||||
vy -= GetEffectiveGravity();
|
||||
}
|
||||
|
||||
// Apply drag/friction
|
||||
if (CreativeFlying)
|
||||
{
|
||||
// Player.travel override: creative fly preserves horizontal from parent, damps Y
|
||||
DeltaMovement = new Vec3d(postMoveVel.X * f, vy * PhysicsConsts.FlyVerticalDamping, postMoveVel.Z * f);
|
||||
}
|
||||
else
|
||||
{
|
||||
DeltaMovement = new Vec3d(postMoveVel.X * f, vy * PhysicsConsts.DragY, postMoveVel.Z * f);
|
||||
}
|
||||
|
||||
// Block speed factor (soul sand, honey, etc.)
|
||||
ApplyBlockSpeedFactor(world);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Water travel — LivingEntity.travelInWater(Vec3, ...)
|
||||
/// </summary>
|
||||
private void TravelInWater(World world, Vec3d input)
|
||||
{
|
||||
float slowDown = Sprinting ? PhysicsConsts.WaterSprintSlowDown : PhysicsConsts.WaterSlowDown;
|
||||
float speed = PhysicsConsts.WaterBaseSpeed;
|
||||
|
||||
MoveRelative(speed, input);
|
||||
Move(world, DeltaMovement);
|
||||
|
||||
Vec3d vel = DeltaMovement;
|
||||
|
||||
// Climbing bump in water
|
||||
if (HorizontalCollision && OnClimbable)
|
||||
vel = new Vec3d(vel.X, PhysicsConsts.ClimbWallBump, vel.Z);
|
||||
|
||||
vel = vel.Multiply(slowDown, PhysicsConsts.WaterYDamping, slowDown);
|
||||
|
||||
// Gravity adjustment in water
|
||||
double gravity = GetEffectiveGravity();
|
||||
if (gravity != 0.0)
|
||||
{
|
||||
double adjustedY = vel.Y;
|
||||
bool falling = vel.Y <= 0.0;
|
||||
if (falling && Math.Abs(vel.Y - 0.005) >= PhysicsConsts.VerticalVelocityThreshold)
|
||||
{
|
||||
adjustedY -= gravity / 16.0;
|
||||
}
|
||||
|
||||
if (!OnGround)
|
||||
adjustedY -= gravity / 16.0;
|
||||
|
||||
vel = new Vec3d(vel.X, adjustedY, vel.Z);
|
||||
}
|
||||
|
||||
DeltaMovement = vel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Lava travel — LivingEntity.travelInLava(Vec3, ...)
|
||||
/// </summary>
|
||||
private void TravelInLava(World world, Vec3d input)
|
||||
{
|
||||
MoveRelative(PhysicsConsts.LavaSpeed, input);
|
||||
Move(world, DeltaMovement);
|
||||
|
||||
double gravity = GetEffectiveGravity();
|
||||
Vec3d vel = DeltaMovement;
|
||||
vel = vel.Multiply(PhysicsConsts.LavaHorizontalDamping, PhysicsConsts.LavaVerticalDamping, PhysicsConsts.LavaHorizontalDamping);
|
||||
|
||||
if (gravity != 0.0)
|
||||
{
|
||||
vel = vel.Add(0, -gravity / 4.0, 0);
|
||||
}
|
||||
|
||||
DeltaMovement = vel;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Add input vector rotated by yaw to deltaMovement.
|
||||
/// Equivalent to Entity.moveRelative(float, Vec3) + getInputVector().
|
||||
/// </summary>
|
||||
private void MoveRelative(float speed, Vec3d input)
|
||||
{
|
||||
Vec3d rotated = GetInputVector(input, speed, Yaw);
|
||||
DeltaMovement = DeltaMovement.Add(rotated);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rotate input by yaw and scale by speed. Equivalent to Entity.getInputVector().
|
||||
/// </summary>
|
||||
private static Vec3d GetInputVector(Vec3d input, float speed, float yaw)
|
||||
{
|
||||
double lenSqr = input.LengthSqr();
|
||||
if (lenSqr < 1.0E-7)
|
||||
return Vec3d.Zero;
|
||||
|
||||
Vec3d scaled = (lenSqr > 1.0 ? input.Normalize() : input).Scale(speed);
|
||||
float sinYaw = MathF.Sin(yaw * (MathF.PI / 180.0f));
|
||||
float cosYaw = MathF.Cos(yaw * (MathF.PI / 180.0f));
|
||||
|
||||
return new Vec3d(
|
||||
scaled.X * cosYaw - scaled.Z * sinYaw,
|
||||
scaled.Y,
|
||||
scaled.Z * cosYaw + scaled.X * sinYaw);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Execute movement with collision detection.
|
||||
/// Equivalent to Entity.move(MoverType.SELF, delta).
|
||||
/// </summary>
|
||||
private void Move(World world, Vec3d movement)
|
||||
{
|
||||
if (StuckSpeedMultiplier.LengthSqr() > 1.0E-7)
|
||||
{
|
||||
movement = movement.Multiply(StuckSpeedMultiplier);
|
||||
StuckSpeedMultiplier = Vec3d.Zero;
|
||||
DeltaMovement = Vec3d.Zero;
|
||||
}
|
||||
|
||||
// Sneak edge back-off
|
||||
if (Sneaking && OnGround)
|
||||
movement = MaybeBackOffFromEdge(world, movement);
|
||||
|
||||
Aabb box = GetBoundingBox();
|
||||
Vec3d resolved = CollisionDetector.Collide(world, box, movement, OnGround, PhysicsConsts.StepHeight);
|
||||
|
||||
double resolvedLenSqr = resolved.LengthSqr();
|
||||
if (resolvedLenSqr > 1.0E-7 || movement.LengthSqr() - resolvedLenSqr < 1.0E-7)
|
||||
{
|
||||
// Fall distance reset via trace (simplified: reset on hitting ground)
|
||||
if (FallDistance != 0.0 && resolvedLenSqr >= 1.0)
|
||||
{
|
||||
// Simplified: just check vertical collision
|
||||
}
|
||||
|
||||
Position = Position.Add(resolved);
|
||||
}
|
||||
|
||||
// Collision flags
|
||||
bool blockedX = !MthEqual(movement.X, resolved.X);
|
||||
bool blockedZ = !MthEqual(movement.Z, resolved.Z);
|
||||
HorizontalCollision = blockedX || blockedZ;
|
||||
VerticalCollision = movement.Y != resolved.Y;
|
||||
VerticalCollisionBelow = VerticalCollision && movement.Y < 0.0;
|
||||
OnGround = VerticalCollisionBelow;
|
||||
|
||||
// Fall distance tracking
|
||||
if (OnGround)
|
||||
FallDistance = 0;
|
||||
else if (resolved.Y < 0)
|
||||
FallDistance -= resolved.Y;
|
||||
|
||||
// Zero velocity on blocked axes
|
||||
if (HorizontalCollision)
|
||||
{
|
||||
DeltaMovement = new Vec3d(
|
||||
blockedX ? 0 : DeltaMovement.X,
|
||||
DeltaMovement.Y,
|
||||
blockedZ ? 0 : DeltaMovement.Z);
|
||||
}
|
||||
|
||||
if (VerticalCollision)
|
||||
{
|
||||
// Slime block bounce would go here; for now just zero Y
|
||||
DeltaMovement = new Vec3d(DeltaMovement.X, 0, DeltaMovement.Z);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sneak edge detection: prevent walking off edges while sneaking.
|
||||
/// Equivalent to Player.maybeBackOffFromEdge(Vec3, MoverType).
|
||||
/// </summary>
|
||||
private Vec3d MaybeBackOffFromEdge(World world, Vec3d movement)
|
||||
{
|
||||
if (movement.Y > 0) return movement;
|
||||
|
||||
double step = 0.05;
|
||||
double dx = movement.X;
|
||||
double dz = movement.Z;
|
||||
Aabb box = GetBoundingBox();
|
||||
|
||||
while (dx != 0.0 && CollisionDetector.CollectBlockColliders(world,
|
||||
box.Move(dx, -1.0, 0)).Count == 0)
|
||||
{
|
||||
dx = dx < step && dx >= -step ? 0.0 : (dx > 0.0 ? dx - step : dx + step);
|
||||
}
|
||||
|
||||
while (dz != 0.0 && CollisionDetector.CollectBlockColliders(world,
|
||||
box.Move(0, -1.0, dz)).Count == 0)
|
||||
{
|
||||
dz = dz < step && dz >= -step ? 0.0 : (dz > 0.0 ? dz - step : dz + step);
|
||||
}
|
||||
|
||||
while (dx != 0.0 && dz != 0.0 && CollisionDetector.CollectBlockColliders(world,
|
||||
box.Move(dx, -1.0, dz)).Count == 0)
|
||||
{
|
||||
dx = dx < step && dx >= -step ? 0.0 : (dx > 0.0 ? dx - step : dx + step);
|
||||
dz = dz < step && dz >= -step ? 0.0 : (dz > 0.0 ? dz - step : dz + step);
|
||||
}
|
||||
|
||||
return new Vec3d(dx, movement.Y, dz);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clamp velocity for climbable blocks.
|
||||
/// Equivalent to LivingEntity.handleOnClimbable(Vec3).
|
||||
/// </summary>
|
||||
private void HandleOnClimbable()
|
||||
{
|
||||
if (!OnClimbable) return;
|
||||
|
||||
FallDistance = 0;
|
||||
double vx = Math.Clamp(DeltaMovement.X, -PhysicsConsts.ClimbMaxSpeed, PhysicsConsts.ClimbMaxSpeed);
|
||||
double vz = Math.Clamp(DeltaMovement.Z, -PhysicsConsts.ClimbMaxSpeed, PhysicsConsts.ClimbMaxSpeed);
|
||||
double vy = Math.Max(DeltaMovement.Y, -PhysicsConsts.ClimbMaxSpeed);
|
||||
|
||||
// Sneaking on ladder prevents sliding down
|
||||
if (vy < 0.0 && Sneaking)
|
||||
vy = 0.0;
|
||||
|
||||
DeltaMovement = new Vec3d(vx, vy, vz);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get effective gravity considering slow falling effect.
|
||||
/// </summary>
|
||||
private double GetEffectiveGravity()
|
||||
{
|
||||
double gravity = PhysicsConsts.DefaultGravity;
|
||||
if (HasSlowFalling && DeltaMovement.Y <= 0.0)
|
||||
return Math.Min(gravity, PhysicsConsts.SlowFallingCap);
|
||||
return gravity;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get speed based on friction: ground uses attribute speed * 0.216/(f^3), air uses 0.02.
|
||||
/// Equivalent to LivingEntity.getFrictionInfluencedSpeed(float).
|
||||
/// </summary>
|
||||
private float GetFrictionInfluencedSpeed(float friction)
|
||||
{
|
||||
if (OnGround)
|
||||
{
|
||||
return MovementSpeed * (PhysicsConsts.GroundAccelerationFactor / (friction * friction * friction));
|
||||
}
|
||||
else
|
||||
{
|
||||
return CreativeFlying ? MovementSpeed * 0.1f : PhysicsConsts.AirAcceleration;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get the friction of the block below the player's feet.
|
||||
/// </summary>
|
||||
private float GetBlockFriction(World world)
|
||||
{
|
||||
Location belowFeet = new(Position.X, Position.Y - 0.5000010, Position.Z);
|
||||
Material mat = world.GetBlock(belowFeet).Type;
|
||||
return GetMaterialFriction(mat);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Apply block speed factor (soul sand, honey, etc.)
|
||||
/// Equivalent to Entity.getBlockSpeedFactor().
|
||||
/// </summary>
|
||||
private void ApplyBlockSpeedFactor(World world)
|
||||
{
|
||||
Location atFeet = new(Position.X, Position.Y, Position.Z);
|
||||
Material mat = world.GetBlock(atFeet).Type;
|
||||
float factor = GetMaterialSpeedFactor(mat);
|
||||
|
||||
if (factor == 1.0f)
|
||||
{
|
||||
Location belowFeet = new(Position.X, Position.Y - 0.5000010, Position.Z);
|
||||
mat = world.GetBlock(belowFeet).Type;
|
||||
factor = GetMaterialSpeedFactor(mat);
|
||||
}
|
||||
|
||||
if (factor != 1.0f)
|
||||
{
|
||||
DeltaMovement = DeltaMovement.Multiply(factor, 1.0, factor);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get friction value for a material. Default 0.6, special blocks differ.
|
||||
/// </summary>
|
||||
public static float GetMaterialFriction(Material mat)
|
||||
{
|
||||
return mat switch
|
||||
{
|
||||
Material.Ice or Material.PackedIce => PhysicsConsts.IceFriction,
|
||||
Material.BlueIce => PhysicsConsts.BlueIceFriction,
|
||||
Material.SlimeBlock => PhysicsConsts.SlimeBlockFriction,
|
||||
Material.FrostedIce => PhysicsConsts.IceFriction,
|
||||
_ => PhysicsConsts.DefaultBlockFriction
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get speed factor for a material.
|
||||
/// </summary>
|
||||
public static float GetMaterialSpeedFactor(Material mat)
|
||||
{
|
||||
return mat switch
|
||||
{
|
||||
Material.SoulSand or Material.SoulSoil => PhysicsConsts.SoulSandSpeedFactor,
|
||||
Material.HoneyBlock => PhysicsConsts.HoneySpeedFactor,
|
||||
_ => PhysicsConsts.DefaultSpeedFactor
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Update environmental state flags (in water, in lava, on climbable, etc.)
|
||||
/// Call before each Tick().
|
||||
/// </summary>
|
||||
public void UpdateEnvironment(World world)
|
||||
{
|
||||
Location feetLoc = new(Position.X, Position.Y, Position.Z);
|
||||
Location headLoc = new(Position.X, Position.Y + PlayerHeight * 0.5, Position.Z);
|
||||
|
||||
Material feetBlock = world.GetBlock(feetLoc).Type;
|
||||
Material headBlock = world.GetBlock(headLoc).Type;
|
||||
|
||||
InWater = feetBlock == Material.Water || headBlock == Material.Water
|
||||
|| feetBlock == Material.BubbleColumn;
|
||||
InLava = feetBlock == Material.Lava || headBlock == Material.Lava;
|
||||
OnClimbable = feetBlock.CanBeClimbedOn();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set position from server teleport / initial spawn.
|
||||
/// </summary>
|
||||
public void SetPosition(double x, double y, double z)
|
||||
{
|
||||
Position = new Vec3d(x, y, z);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Set position and reset velocity (for teleports).
|
||||
/// </summary>
|
||||
public void Teleport(double x, double y, double z)
|
||||
{
|
||||
Position = new Vec3d(x, y, z);
|
||||
DeltaMovement = Vec3d.Zero;
|
||||
FallDistance = 0;
|
||||
}
|
||||
|
||||
private static bool MthEqual(double a, double b)
|
||||
{
|
||||
return Math.Abs(a - b) < 1.0E-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
102
MinecraftClient/Physics/Vec3d.cs
Normal file
102
MinecraftClient/Physics/Vec3d.cs
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace MinecraftClient.Physics
|
||||
{
|
||||
/// <summary>
|
||||
/// Immutable 3D double vector, mirrors net.minecraft.world.phys.Vec3
|
||||
/// </summary>
|
||||
public readonly struct Vec3d : IEquatable<Vec3d>
|
||||
{
|
||||
public static readonly Vec3d Zero = new(0, 0, 0);
|
||||
|
||||
public readonly double X;
|
||||
public readonly double Y;
|
||||
public readonly double Z;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d(double x, double y, double z)
|
||||
{
|
||||
X = x;
|
||||
Y = y;
|
||||
Z = z;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d Add(double x, double y, double z) => new(X + x, Y + y, Z + z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d Add(Vec3d other) => new(X + other.X, Y + other.Y, Z + other.Z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d Subtract(Vec3d other) => new(X - other.X, Y - other.Y, Z - other.Z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d Subtract(double x, double y, double z) => new(X - x, Y - y, Z - z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d Scale(double factor) => new(X * factor, Y * factor, Z * factor);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d Multiply(double x, double y, double z) => new(X * x, Y * y, Z * z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public Vec3d Multiply(Vec3d other) => new(X * other.X, Y * other.Y, Z * other.Z);
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double LengthSqr() => X * X + Y * Y + Z * Z;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double Length() => Math.Sqrt(LengthSqr());
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public double HorizontalDistanceSqr() => X * X + Z * Z;
|
||||
|
||||
public Vec3d Normalize()
|
||||
{
|
||||
double len = Length();
|
||||
return len < 1.0E-7 ? Zero : new Vec3d(X / len, Y / len, Z / len);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Get component by axis index: 0=X, 1=Y, 2=Z
|
||||
/// </summary>
|
||||
public double Get(int axis) => axis switch
|
||||
{
|
||||
0 => X,
|
||||
1 => Y,
|
||||
2 => Z,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(axis))
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Return a new Vec3d with one axis replaced
|
||||
/// </summary>
|
||||
public Vec3d With(int axis, double value) => axis switch
|
||||
{
|
||||
0 => new Vec3d(value, Y, Z),
|
||||
1 => new Vec3d(X, value, Z),
|
||||
2 => new Vec3d(X, Y, value),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(axis))
|
||||
};
|
||||
|
||||
public bool Equals(Vec3d other) =>
|
||||
X == other.X && Y == other.Y && Z == other.Z;
|
||||
|
||||
public override bool Equals(object? obj) =>
|
||||
obj is Vec3d other && Equals(other);
|
||||
|
||||
public override int GetHashCode() =>
|
||||
HashCode.Combine(X, Y, Z);
|
||||
|
||||
public override string ToString() =>
|
||||
$"({X:F4}, {Y:F4}, {Z:F4})";
|
||||
|
||||
public static bool operator ==(Vec3d a, Vec3d b) => a.Equals(b);
|
||||
public static bool operator !=(Vec3d a, Vec3d b) => !a.Equals(b);
|
||||
public static Vec3d operator +(Vec3d a, Vec3d b) => a.Add(b);
|
||||
public static Vec3d operator -(Vec3d a, Vec3d b) => a.Subtract(b);
|
||||
public static Vec3d operator *(Vec3d a, double s) => a.Scale(s);
|
||||
public static Vec3d operator -(Vec3d a) => new(-a.X, -a.Y, -a.Z);
|
||||
}
|
||||
}
|
||||
|
|
@ -90,6 +90,10 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
private bool isOnlineMode = false;
|
||||
private readonly BlockingCollection<Tuple<int, Queue<byte>>> packetQueue = new();
|
||||
private float LastYaw, LastPitch;
|
||||
private double lastSentX, lastSentY, lastSentZ;
|
||||
private float lastSentYaw, lastSentPitch;
|
||||
private bool lastSentOnGround;
|
||||
private int positionReminder;
|
||||
private long chunkBatchStartTime;
|
||||
private double aggregatedNanosPerChunk = 2000000.0;
|
||||
private int oldSamplesWeight = 1;
|
||||
|
|
@ -4061,47 +4065,92 @@ namespace MinecraftClient.Protocol.Handlers
|
|||
{
|
||||
if (handler.GetTerrainEnabled())
|
||||
{
|
||||
var yawPitch = Array.Empty<byte>();
|
||||
var packetType = PacketTypesOut.PlayerPosition;
|
||||
// Vanilla-like packet selection (LocalPlayer.sendPosition):
|
||||
// Send position if delta > (2e-4)^2 or every 20 ticks
|
||||
// Send rotation if yaw/pitch changed
|
||||
// Send StatusOnly if only onGround changed
|
||||
|
||||
if (Config.Main.Advanced.TemporaryFixBadpacket)
|
||||
{
|
||||
if (yaw.HasValue && pitch.HasValue &&
|
||||
(forceUpdate || yaw.Value != LastYaw || pitch.Value != LastPitch))
|
||||
{
|
||||
yawPitch = dataTypes.ConcatBytes(dataTypes.GetFloat(yaw.Value),
|
||||
dataTypes.GetFloat(pitch.Value));
|
||||
packetType = PacketTypesOut.PlayerPositionAndRotation;
|
||||
double dx = location.X - lastSentX;
|
||||
double dy = location.Y - lastSentY;
|
||||
double dz = location.Z - lastSentZ;
|
||||
double distSqr = dx * dx + dy * dy + dz * dz;
|
||||
|
||||
LastYaw = yaw.Value;
|
||||
LastPitch = pitch.Value;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (yaw.HasValue && pitch.HasValue)
|
||||
{
|
||||
yawPitch = dataTypes.ConcatBytes(dataTypes.GetFloat(yaw.Value),
|
||||
dataTypes.GetFloat(pitch.Value));
|
||||
packetType = PacketTypesOut.PlayerPositionAndRotation;
|
||||
bool positionChanged = distSqr > 4.0E-8 || positionReminder >= 20;
|
||||
bool rotationChanged = false;
|
||||
if (yaw.HasValue && pitch.HasValue)
|
||||
rotationChanged = forceUpdate || yaw.Value != lastSentYaw || pitch.Value != lastSentPitch;
|
||||
bool groundChanged = onGround != lastSentOnGround;
|
||||
|
||||
LastYaw = yaw.Value;
|
||||
LastPitch = pitch.Value;
|
||||
}
|
||||
}
|
||||
positionReminder++;
|
||||
|
||||
if (!positionChanged && !rotationChanged && !groundChanged)
|
||||
return true; // Nothing to send
|
||||
|
||||
try
|
||||
{
|
||||
SendPacket(packetType, dataTypes.ConcatBytes(
|
||||
dataTypes.GetDouble(location.X),
|
||||
dataTypes.GetDouble(location.Y),
|
||||
protocolVersion < MC_1_8_Version
|
||||
? dataTypes.GetDouble(location.Y + 1.62)
|
||||
: Array.Empty<byte>(),
|
||||
dataTypes.GetDouble(location.Z),
|
||||
yawPitch,
|
||||
new byte[] { onGround ? (byte)1 : (byte)0 })
|
||||
);
|
||||
PacketTypesOut packetType;
|
||||
byte[] payload;
|
||||
byte flags = (byte)(onGround ? 1 : 0);
|
||||
|
||||
if (positionChanged && rotationChanged && yaw.HasValue && pitch.HasValue)
|
||||
{
|
||||
packetType = PacketTypesOut.PlayerPositionAndRotation;
|
||||
payload = dataTypes.ConcatBytes(
|
||||
dataTypes.GetDouble(location.X),
|
||||
dataTypes.GetDouble(location.Y),
|
||||
protocolVersion < MC_1_8_Version
|
||||
? dataTypes.GetDouble(location.Y + 1.62)
|
||||
: Array.Empty<byte>(),
|
||||
dataTypes.GetDouble(location.Z),
|
||||
dataTypes.GetFloat(yaw.Value),
|
||||
dataTypes.GetFloat(pitch.Value),
|
||||
new[] { flags });
|
||||
lastSentYaw = yaw.Value;
|
||||
lastSentPitch = pitch.Value;
|
||||
LastYaw = yaw.Value;
|
||||
LastPitch = pitch.Value;
|
||||
}
|
||||
else if (positionChanged)
|
||||
{
|
||||
packetType = PacketTypesOut.PlayerPosition;
|
||||
payload = dataTypes.ConcatBytes(
|
||||
dataTypes.GetDouble(location.X),
|
||||
dataTypes.GetDouble(location.Y),
|
||||
protocolVersion < MC_1_8_Version
|
||||
? dataTypes.GetDouble(location.Y + 1.62)
|
||||
: Array.Empty<byte>(),
|
||||
dataTypes.GetDouble(location.Z),
|
||||
new[] { flags });
|
||||
}
|
||||
else if (rotationChanged && yaw.HasValue && pitch.HasValue)
|
||||
{
|
||||
packetType = PacketTypesOut.PlayerRotation;
|
||||
payload = dataTypes.ConcatBytes(
|
||||
dataTypes.GetFloat(yaw.Value),
|
||||
dataTypes.GetFloat(pitch.Value),
|
||||
new[] { flags });
|
||||
lastSentYaw = yaw.Value;
|
||||
lastSentPitch = pitch.Value;
|
||||
LastYaw = yaw.Value;
|
||||
LastPitch = pitch.Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Only onGround changed — send StatusOnly (PlayerMovement)
|
||||
packetType = PacketTypesOut.PlayerMovement;
|
||||
payload = new[] { flags };
|
||||
}
|
||||
|
||||
if (positionChanged)
|
||||
{
|
||||
lastSentX = location.X;
|
||||
lastSentY = location.Y;
|
||||
lastSentZ = location.Z;
|
||||
positionReminder = 0;
|
||||
}
|
||||
lastSentOnGround = onGround;
|
||||
|
||||
SendPacket(packetType, payload);
|
||||
return true;
|
||||
}
|
||||
catch (SocketException)
|
||||
|
|
|
|||
|
|
@ -18,17 +18,21 @@ Two types of data can be used as input:
|
|||
### Decompiling a new MC version
|
||||
|
||||
```bash
|
||||
cd MinecraftOfficial
|
||||
java -jar MinecraftDecompiler.jar --version 1.21.9 --side SERVER \
|
||||
--decompile --output 1.21.9-remapped.jar --decompiled-output 1.21.9-decompiled
|
||||
# Server side (default) — also downloads server.jar into MinecraftOfficial/downloads/<ver>/
|
||||
tools/decompile.sh --version 1.21.9
|
||||
|
||||
# Client side
|
||||
tools/decompile.sh --version 1.21.9 --side CLIENT
|
||||
```
|
||||
|
||||
The script auto-downloads `MinecraftDecompiler.jar` from GitHub releases if it doesn't exist.
|
||||
|
||||
### Generating server data reports
|
||||
|
||||
```bash
|
||||
cd /tmp
|
||||
java -DbundlerMainClass=net.minecraft.data.Main \
|
||||
-jar /path/to/server.jar \
|
||||
-jar $MCC_SERVERS/<version>/server.jar \
|
||||
--reports --output /tmp/mc_reports
|
||||
```
|
||||
|
||||
|
|
@ -110,6 +114,25 @@ python3 tools/gen_command_argument_registry.py 1.20.6 1.21.5 1.21.6
|
|||
|
||||
Reads `ArgumentTypeInfos.java`, skips the `SharedConstants.IS_RUNNING_IN_IDE` block, and prints C# array initializers for the runtime `COMMAND_ARGUMENT_TYPE` registry order. Use this when Mojang inserts new command argument types and the modern `DeclareCommands` parser needs updated ID routing.
|
||||
|
||||
## gen_block_shapes.py — Download & compact block collision shapes
|
||||
|
||||
Downloads block collision shapes from PrismarineJS `minecraft-data` and compacts them into a single JSON for MCC's physics engine.
|
||||
|
||||
```bash
|
||||
# Auto-download for a specific MC version
|
||||
python3 tools/gen_block_shapes.py 1.21.11
|
||||
# → MinecraftClient/Physics/BlockShapeData.json
|
||||
|
||||
# From a local file (if network is slow)
|
||||
python3 tools/gen_block_shapes.py --from-file /path/to/blockCollisionShapes.json
|
||||
```
|
||||
|
||||
Output: `MinecraftClient/Physics/BlockShapeData.json` (embedded as a resource via `.csproj`).
|
||||
|
||||
Data source: `https://raw.githubusercontent.com/PrismarineJS/minecraft-data/master/data/pc/<version>/blockCollisionShapes.json`
|
||||
|
||||
Uses `curl` with resume (`-C -`) for reliable download over slow connections. Falls back to manual download if retries are exhausted.
|
||||
|
||||
## Recommended workflow
|
||||
|
||||
1. Generate server reports (Step 0)
|
||||
|
|
@ -119,6 +142,7 @@ Reads `ArgumentTypeInfos.java`, skips the `SharedConstants.IS_RUNNING_IN_IDE` bl
|
|||
- Blocks: `gen_block_palette.py`
|
||||
- Entities: `gen_entity_palette.py`
|
||||
- Metadata: `gen_entity_metadata_palette.py`
|
||||
4. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs`
|
||||
5. Update version routing (see SKILL.md)
|
||||
6. Build and test
|
||||
4. Update block collision shapes: `gen_block_shapes.py`
|
||||
5. Add any missing enum values to `ItemType.cs`, `Material.cs`, `EntityType.cs`, `EntityMetaDataType.cs`
|
||||
6. Update version routing (see SKILL.md)
|
||||
7. Build and test
|
||||
|
|
|
|||
138
tools/decompile.sh
Normal file
138
tools/decompile.sh
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
#!/bin/bash
|
||||
# Download (if needed) and run MinecraftDecompiler to produce decompiled source
|
||||
# and server.jar for a given Minecraft version.
|
||||
#
|
||||
# Usage:
|
||||
# ./tools/decompile.sh --version <ver> [--side SERVER|CLIENT]
|
||||
#
|
||||
# Examples:
|
||||
# ./tools/decompile.sh --version 1.21.11
|
||||
# ./tools/decompile.sh --version 1.21.11 --side CLIENT
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
MC_OFFICIAL="$REPO_ROOT/MinecraftOfficial"
|
||||
DECOMPILER_JAR="$MC_OFFICIAL/MinecraftDecompiler.jar"
|
||||
DECOMPILER_REPO="MaxPixelStudios/MinecraftDecompiler"
|
||||
|
||||
VERSION=""
|
||||
SIDE="SERVER"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--version) VERSION="$2"; shift 2 ;;
|
||||
--side) SIDE="$(echo "$2" | tr '[:lower:]' '[:upper:]')"; shift 2 ;;
|
||||
-h|--help)
|
||||
echo "Usage: $0 --version <ver> [--side SERVER|CLIENT]"
|
||||
echo ""
|
||||
echo "Options:"
|
||||
echo " --version <ver> Minecraft version (e.g. 1.21.11)"
|
||||
echo " --side <env> SERVER (default) or CLIENT"
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1"; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$VERSION" ]]; then
|
||||
echo "Error: --version is required"
|
||||
echo "Usage: $0 --version <ver> [--side SERVER|CLIENT]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SIDE" != "SERVER" && "$SIDE" != "CLIENT" ]]; then
|
||||
echo "Error: --side must be SERVER or CLIENT (got: $SIDE)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# --- Ensure MinecraftDecompiler.jar exists ---
|
||||
if [[ ! -f "$DECOMPILER_JAR" ]]; then
|
||||
echo "MinecraftDecompiler.jar not found, downloading latest release..."
|
||||
DOWNLOAD_URL=$(curl -sL "https://api.github.com/repos/$DECOMPILER_REPO/releases/latest" \
|
||||
| python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for a in data['assets']:
|
||||
if a['name'] == 'MinecraftDecompiler.jar':
|
||||
print(a['browser_download_url'])
|
||||
break
|
||||
")
|
||||
if [[ -z "$DOWNLOAD_URL" ]]; then
|
||||
echo "Error: could not find MinecraftDecompiler.jar in latest release"
|
||||
exit 1
|
||||
fi
|
||||
echo "Downloading from $DOWNLOAD_URL ..."
|
||||
curl -L -o "$DECOMPILER_JAR" "$DOWNLOAD_URL"
|
||||
echo "Downloaded MinecraftDecompiler.jar"
|
||||
fi
|
||||
|
||||
# --- Build output paths ---
|
||||
SIDE_LOWER="$(echo "$SIDE" | tr '[:upper:]' '[:lower:]')"
|
||||
|
||||
if [[ "$SIDE" == "SERVER" ]]; then
|
||||
REMAPPED_JAR="$MC_OFFICIAL/remapped_jar/${VERSION}-remapped.jar"
|
||||
DECOMPILED_DIR="$MC_OFFICIAL/${VERSION}-decompiled"
|
||||
else
|
||||
REMAPPED_JAR="$MC_OFFICIAL/remapped_jar/${VERSION}-${SIDE_LOWER}-remapped.jar"
|
||||
DECOMPILED_DIR="$MC_OFFICIAL/${VERSION}-${SIDE_LOWER}-decompiled"
|
||||
fi
|
||||
|
||||
if [[ -d "$DECOMPILED_DIR" ]]; then
|
||||
echo "Decompiled source already exists: $DECOMPILED_DIR"
|
||||
echo "Delete it first if you want to re-decompile."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
mkdir -p "$MC_OFFICIAL/remapped_jar"
|
||||
|
||||
echo "=== Decompiling Minecraft $VERSION ($SIDE) ==="
|
||||
echo " Remapped JAR: $REMAPPED_JAR"
|
||||
echo " Decompiled: $DECOMPILED_DIR"
|
||||
echo ""
|
||||
|
||||
cd "$MC_OFFICIAL"
|
||||
java -jar "$DECOMPILER_JAR" \
|
||||
--version "$VERSION" \
|
||||
--side "$SIDE" \
|
||||
--decompile \
|
||||
--output "$REMAPPED_JAR" \
|
||||
--decompiled-output "$DECOMPILED_DIR"
|
||||
|
||||
echo ""
|
||||
echo "=== Done ==="
|
||||
echo "Decompiled source: $DECOMPILED_DIR"
|
||||
|
||||
# --- For SERVER side, also ensure downloads/<ver>/server.jar exists ---
|
||||
if [[ "$SIDE" == "SERVER" ]]; then
|
||||
DOWNLOADS_DIR="$MC_OFFICIAL/downloads/$VERSION"
|
||||
if [[ ! -f "$DOWNLOADS_DIR/server.jar" ]]; then
|
||||
mkdir -p "$DOWNLOADS_DIR"
|
||||
# MinecraftDecompiler downloads the original jar into its cache;
|
||||
# extract it from the bundled remapped jar or re-download via manifest.
|
||||
echo ""
|
||||
echo "Downloading server.jar for $VERSION into $DOWNLOADS_DIR ..."
|
||||
MANIFEST_URL="https://launchermeta.mojang.com/mc/game/version_manifest_v2.json"
|
||||
VERSION_URL=$(curl -sL "$MANIFEST_URL" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
for v in data['versions']:
|
||||
if v['id'] == '$VERSION':
|
||||
print(v['url'])
|
||||
break
|
||||
")
|
||||
if [[ -n "$VERSION_URL" ]]; then
|
||||
SERVER_JAR_URL=$(curl -sL "$VERSION_URL" | python3 -c "
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
print(data['downloads']['server']['url'])
|
||||
")
|
||||
curl -L -o "$DOWNLOADS_DIR/server.jar" "$SERVER_JAR_URL"
|
||||
echo "Downloaded server.jar"
|
||||
else
|
||||
echo "Warning: could not find version $VERSION in Mojang manifest; server.jar not downloaded."
|
||||
fi
|
||||
else
|
||||
echo "server.jar already exists: $DOWNLOADS_DIR/server.jar"
|
||||
fi
|
||||
fi
|
||||
167
tools/gen_block_shapes.py
Normal file
167
tools/gen_block_shapes.py
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
#!/usr/bin/env python3
|
||||
"""
|
||||
Download block collision shapes from PrismarineJS minecraft-data and compact
|
||||
them into a single JSON file for embedding in MCC's physics engine.
|
||||
|
||||
Usage:
|
||||
python3 tools/gen_block_shapes.py <mc_version>
|
||||
python3 tools/gen_block_shapes.py --from-file /path/to/blockCollisionShapes.json
|
||||
# e.g. python3 tools/gen_block_shapes.py 1.21.11
|
||||
|
||||
Output:
|
||||
MinecraftClient/Physics/BlockShapeData.json
|
||||
|
||||
The output JSON has two top-level keys:
|
||||
- "shapes": { shapeId -> [[x0,y0,z0,x1,y1,z1], ...] }
|
||||
- "blocks": { blockName -> shapeId | [shapeId, ...] }
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
import os
|
||||
import subprocess
|
||||
import tempfile
|
||||
|
||||
REPO = "PrismarineJS/minecraft-data"
|
||||
BRANCH = "master"
|
||||
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
REPO_ROOT = os.path.dirname(SCRIPT_DIR)
|
||||
OUTPUT_PATH = os.path.join(REPO_ROOT, "MinecraftClient", "Physics", "BlockShapeData.json")
|
||||
|
||||
|
||||
def resolve_version_path(version: str) -> str:
|
||||
"""Resolve actual data path using PrismarineJS dataPaths.json."""
|
||||
url = f"https://raw.githubusercontent.com/{REPO}/{BRANCH}/data/dataPaths.json"
|
||||
tmp = tempfile.mktemp(suffix=".json")
|
||||
try:
|
||||
subprocess.run(
|
||||
["curl", "-sL", "--connect-timeout", "10", "--max-time", "30",
|
||||
"-o", tmp, url],
|
||||
check=True, timeout=35
|
||||
)
|
||||
with open(tmp) as f:
|
||||
data = json.load(f)
|
||||
pc = data.get("pc", {})
|
||||
if version in pc:
|
||||
entry = pc[version]
|
||||
bcs_path = entry.get("blockCollisionShapes", "")
|
||||
if bcs_path:
|
||||
return bcs_path # e.g. "pc/1.21.11"
|
||||
return f"pc/{version}"
|
||||
except Exception as e:
|
||||
print(f" Warning: could not resolve version path ({e}), using default")
|
||||
return f"pc/{version}"
|
||||
finally:
|
||||
if os.path.exists(tmp):
|
||||
os.remove(tmp)
|
||||
|
||||
|
||||
def download_collision_shapes(version: str) -> dict:
|
||||
"""Download blockCollisionShapes.json with curl and resume support."""
|
||||
ver_path = resolve_version_path(version)
|
||||
url = f"https://raw.githubusercontent.com/{REPO}/{BRANCH}/data/{ver_path}/blockCollisionShapes.json"
|
||||
print(f"Downloading: {url}")
|
||||
|
||||
tmp = tempfile.mktemp(suffix=".json")
|
||||
max_retries = 5
|
||||
|
||||
for attempt in range(1, max_retries + 1):
|
||||
print(f" Attempt {attempt}/{max_retries}...")
|
||||
result = subprocess.run(
|
||||
["curl", "-sL", "-C", "-",
|
||||
"--connect-timeout", "15", "--max-time", "180",
|
||||
"--retry", "3", "--retry-delay", "2",
|
||||
"-o", tmp, url],
|
||||
timeout=200
|
||||
)
|
||||
|
||||
if not os.path.exists(tmp):
|
||||
print(f" No file downloaded")
|
||||
continue
|
||||
|
||||
size = os.path.getsize(tmp)
|
||||
print(f" Downloaded {size:,} bytes")
|
||||
|
||||
try:
|
||||
with open(tmp) as f:
|
||||
data = json.load(f)
|
||||
os.remove(tmp)
|
||||
return data
|
||||
except json.JSONDecodeError as e:
|
||||
print(f" Incomplete/corrupt JSON ({e}), retrying...")
|
||||
# Don't delete tmp, curl -C - will resume
|
||||
|
||||
if os.path.exists(tmp):
|
||||
os.remove(tmp)
|
||||
print(f"ERROR: Failed to download complete file after {max_retries} attempts.")
|
||||
print(f"You can manually download from: {url}")
|
||||
print(f"Then run: {sys.argv[0]} --from-file /path/to/blockCollisionShapes.json")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
def compact(raw: dict) -> dict:
|
||||
"""Convert PrismarineJS format to compacted format for embedding."""
|
||||
shapes_raw = raw.get("shapes", {})
|
||||
blocks_raw = raw.get("blocks", {})
|
||||
|
||||
if not shapes_raw:
|
||||
raise ValueError("Could not find 'shapes' key in input JSON")
|
||||
if not blocks_raw:
|
||||
raise ValueError("Could not find 'blocks' key in input JSON")
|
||||
|
||||
shapes = {}
|
||||
for sid, boxes in shapes_raw.items():
|
||||
compacted = []
|
||||
for box in boxes:
|
||||
compacted.append([round(c, 6) for c in box])
|
||||
shapes[sid] = compacted
|
||||
|
||||
blocks = {}
|
||||
for name, data in blocks_raw.items():
|
||||
blocks[name] = data
|
||||
|
||||
return {"shapes": shapes, "blocks": blocks}
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(f"Usage: {sys.argv[0]} <mc_version>")
|
||||
print(f" {sys.argv[0]} --from-file <path/to/blockCollisionShapes.json>")
|
||||
print()
|
||||
print(f"Example: {sys.argv[0]} 1.21.11")
|
||||
sys.exit(1)
|
||||
|
||||
if sys.argv[1] == "--from-file":
|
||||
if len(sys.argv) < 3:
|
||||
print("Error: --from-file requires a file path")
|
||||
sys.exit(1)
|
||||
input_path = sys.argv[2]
|
||||
print(f"Reading from: {input_path}")
|
||||
with open(input_path) as f:
|
||||
raw = json.load(f)
|
||||
else:
|
||||
version = sys.argv[1]
|
||||
print(f"Fetching block collision shapes for MC {version}...")
|
||||
raw = download_collision_shapes(version)
|
||||
|
||||
result = compact(raw)
|
||||
|
||||
shape_count = len(result["shapes"])
|
||||
block_count = len(result["blocks"])
|
||||
print(f" Shapes: {shape_count}")
|
||||
print(f" Blocks: {block_count}")
|
||||
|
||||
with open(OUTPUT_PATH, "w") as f:
|
||||
json.dump(result, f, separators=(",", ":"))
|
||||
|
||||
file_size = os.path.getsize(OUTPUT_PATH)
|
||||
print(f" Written to: {OUTPUT_PATH}")
|
||||
print(f" File size: {file_size:,} bytes")
|
||||
print()
|
||||
print("Done. The file is embedded as a resource via MinecraftClient.csproj.")
|
||||
print("Rebuild MCC to include updated collision data.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
46
tools/mc-rcon.sh
Normal file
46
tools/mc-rcon.sh
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/bash
|
||||
# Send an RCON command to a Minecraft server
|
||||
# Usage: mc-rcon.sh <command> [port] [password]
|
||||
set -euo pipefail
|
||||
|
||||
CMD="${1:?Usage: mc-rcon.sh <command> [port] [password]}"
|
||||
PORT="${2:-25575}"
|
||||
PW="${3:-test123}"
|
||||
|
||||
python3 -c "
|
||||
import socket, struct, sys
|
||||
|
||||
s = socket.socket()
|
||||
s.settimeout(5)
|
||||
try:
|
||||
s.connect(('localhost', $PORT))
|
||||
except Exception as e:
|
||||
print(f'Connection failed: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
def send(req_id, pkt_type, body):
|
||||
body = body.encode()
|
||||
s.send(struct.pack('<iii', 10 + len(body), req_id, pkt_type) + body + b'\x00\x00')
|
||||
|
||||
def recv():
|
||||
length = struct.unpack('<i', s.recv(4))[0]
|
||||
data = b''
|
||||
while len(data) < length:
|
||||
data += s.recv(length - len(data))
|
||||
return data
|
||||
|
||||
send(1, 3, '$PW')
|
||||
r = recv()
|
||||
rid = struct.unpack('<i', r[:4])[0]
|
||||
if rid == -1:
|
||||
print('Auth failed', file=sys.stderr)
|
||||
s.close()
|
||||
sys.exit(1)
|
||||
|
||||
send(2, 2, \"\"\"$CMD\"\"\")
|
||||
r = recv()
|
||||
body = r[8:-2].decode(errors='replace')
|
||||
if body:
|
||||
print(body)
|
||||
s.close()
|
||||
"
|
||||
33
tools/mcc-env.sh
Normal file
33
tools/mcc-env.sh
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#!/bin/bash
|
||||
# MCC (Minecraft Console Client) Development Utilities
|
||||
# Source this file to get helper functions: source $MCC_REPO/tools/mcc-env.sh
|
||||
# Or add to ~/.bashrc: source "$HOME/Minecraft/Minecraft-Console-Client-milutinke/tools/mcc-env.sh"
|
||||
|
||||
TOOLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
export MCC_REPO="$(cd "$TOOLS_DIR/.." && pwd)"
|
||||
export MCC_SERVERS="$MCC_REPO/MinecraftOfficial/downloads"
|
||||
|
||||
# Helper: convert version to tmux session name (dots -> underscores)
|
||||
_mc-session() { echo "mc-${1//\./_}"; }
|
||||
|
||||
# --- Minecraft Server Management ---
|
||||
mc-start() { "$MCC_REPO/tools/start-server.sh" "${1:-1.20.6}"; }
|
||||
mc-stop() { local v="${1:-1.20.6}"; echo "stop" > "$MCC_SERVERS/$v/stdin.pipe"; }
|
||||
mc-cmd() { local v="${2:-1.20.6}"; echo "$1" > "$MCC_SERVERS/$v/stdin.pipe"; }
|
||||
mc-log() { local s; s=$(_mc-session "${1:-1.20.6}"); tmux capture-pane -t "$s" -p -S "-${2:-50}"; }
|
||||
mc-kill() { local v="${1:-1.20.6}" s; s=$(_mc-session "$v"); tmux kill-session -t "$s" 2>/dev/null; rm -f "$MCC_SERVERS/$v/stdin.pipe"; echo "Killed $s"; }
|
||||
mc-list() { tmux list-sessions 2>/dev/null | grep "^mc-" || echo "No running MC servers"; }
|
||||
|
||||
# --- RCON ---
|
||||
mc-rcon() { "$MCC_REPO/tools/mc-rcon.sh" "$@"; }
|
||||
|
||||
# --- MCC Build/Run ---
|
||||
mcc-build() { dotnet build "$MCC_REPO/MinecraftClient.sln" -c Release; }
|
||||
mcc-run() { cd "$MCC_REPO" && MCC_FILE_INPUT=1 dotnet run --project MinecraftClient -c Release -- CursorBot - "localhost:${1:-25565}" 2>&1; }
|
||||
mcc-cmd() { echo "$1" >> "$MCC_REPO/mcc_input.txt"; }
|
||||
mcc-kill() { pkill -f "MinecraftClient" 2>/dev/null && echo "MCC killed" || echo "No MCC process found"; }
|
||||
mcc-reload() {
|
||||
mcc-kill
|
||||
sleep 1
|
||||
mcc-build && mcc-run
|
||||
}
|
||||
39
tools/start-server.sh
Normal file
39
tools/start-server.sh
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/bin/bash
|
||||
# Start a Minecraft server in a tmux session with named pipe for stdin
|
||||
# Servers live in MinecraftOfficial/downloads/<version>/ alongside the downloaded server.jar
|
||||
VERSION="${1}"
|
||||
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
DOWNLOADS="$REPO_ROOT/MinecraftOfficial/downloads"
|
||||
DIR="$DOWNLOADS/$VERSION"
|
||||
PIPE="$DIR/stdin.pipe"
|
||||
SESSION="mc-${VERSION//\./_}"
|
||||
|
||||
if [ -z "$VERSION" ] || [ ! -d "$DIR" ]; then
|
||||
echo "Error: Server directory not found${VERSION:+: $DIR}"
|
||||
echo "Available versions:"
|
||||
ls "$DOWNLOADS" | grep -E '^[0-9]' | sort -V
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$DIR/server.jar" ]; then
|
||||
echo "Error: No server.jar in $DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if tmux has-session -t "$SESSION" 2>/dev/null; then
|
||||
echo "Server $VERSION already running in tmux session '$SESSION'"
|
||||
echo "View output: tmux capture-pane -t '$SESSION' -p -S -50"
|
||||
echo "Send command: echo 'say hello' > $PIPE"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
rm -f "$DIR/world/session.lock"
|
||||
|
||||
[ -p "$PIPE" ] || mkfifo "$PIPE"
|
||||
|
||||
tmux new-session -d -s "$SESSION" -c "$DIR" \
|
||||
"tail -f $PIPE | java -Xmx2G -Xms2G -jar server.jar nogui 2>&1"
|
||||
|
||||
echo "Server $VERSION started in tmux session '$SESSION'"
|
||||
echo "Send commands: echo 'say hello' > $PIPE"
|
||||
echo "View output: tmux capture-pane -t '$SESSION' -p -S -50"
|
||||
Loading…
Add table
Add a link
Reference in a new issue