From 9980c1561d14c022932aacfed47a74110535358c Mon Sep 17 00:00:00 2001
From: sasheg dev <152582704+SashegDev@users.noreply.github.com>
Date: Wed, 21 Oct 2026 22:31:16 +0300
Subject: [PATCH] =?UTF-8?q?=D0=98=D0=B3=D1=80=D0=BE=D0=BA:=20=D1=85=D0=B8?=
=?UTF-8?q?=D1=82=D0=B1=D0=BE=D0=BA=D1=81=201.45=20=D0=B2=20=D1=88=D0=B8?=
=?UTF-8?q?=D1=84=D1=82=D0=B5=20=D0=B8=20=D0=BF=D1=80=D1=8B=D0=B6=D0=BE?=
=?UTF-8?q?=D0=BA=20=D1=81=20=D0=BC=D0=B5=D1=81=D1=82=D0=B0=201.5?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- RsxPlayerTuning: высота при подкрадывании 1.45 (ванилла 1.65), прыжок с
места 1.5, в движении ванильные 1.25, высота глаз в шифте 1.37.
- RsxPlayer: прыжок по наличию горизонтального ввода, глаза при подкрадывании.
- Патч в EntityPlayer.updateSize: высота при подкрадывании из нашего класса.
- Импульс прыжка считается обратным счетом по физике ваниллы.
AABB: редактор хитбоксов и их отрисовка
- Протокол: Hitbox (имя, зона, min/max, поворот), пакеты Hitboxes,
HitboxInstances и HitboxesReload.
- Сервер: HitboxRegistry читает JSON-файл (world/hitboxes.json, можно
указать --hitboxes), перечитывает его по запросу клиента и шлет наборы
игрокам. Сломанный файл не стирает рабочие коробки, а объясняет причину.
- Клиент: RsxHitboxDebug рисует коробки через RenderGlobal.drawBoundingBox
с подписями DebugRenderer, красит зоны (голова красная, тело желтое, ноги
синие). Команды: /rsx hitboxes on|off|reload|list|show NAME.
- Новый модуль rsx-plugin-api с интерфейсом Hitboxes: плагин может
зарегистрировать свои коробки. У сервера появился скрипт сборки.
Тесты: протокол 34, сервер 80 (было 74). Форк собирается чисто, 3279 классов.
---
game-fork/patches/decompiler-fixes.txt | 14 +
.../net/rustnotmc/client/RsxBootstrap.java | 9 +
.../net/rustnotmc/client/RsxHitboxDebug.java | 263 +++++++++++++++++
.../net/rustnotmc/client/RsxPlayer.java | 38 ++-
.../net/rustnotmc/client/RsxPlayerTuning.java | 37 +++
.../client/net/RsxNetworkManager.java | 24 ++
rsx-plugin-api/scripts/build.ps1 | 73 +++++
.../net/rustnotmc/api/server/Hitboxes.java | 30 ++
.../java/net/rustnotmc/protocol/Hitbox.java | 80 +++++
.../java/net/rustnotmc/protocol/Packets.java | 162 ++++++++++
rsx-server-core/scripts/build.ps1 | 9 +-
rsx-server-core/scripts/test.ps1 | 8 +-
.../net/rustnotmc/server/ClientSession.java | 33 ++-
.../java/net/rustnotmc/server/Server.java | 35 +++
.../java/net/rustnotmc/server/ServerMain.java | 7 +
.../server/hitbox/HitboxRegistry.java | 259 ++++++++++++++++
.../java/net/rustnotmc/server/util/Json.java | 278 ++++++++++++++++++
.../rustnotmc/server/hitbox/HitboxTest.java | 183 ++++++++++++
18 files changed, 1528 insertions(+), 14 deletions(-)
create mode 100644 game-fork/src-overlay/net/rustnotmc/client/RsxHitboxDebug.java
create mode 100644 rsx-plugin-api/scripts/build.ps1
create mode 100644 rsx-plugin-api/src/main/java/net/rustnotmc/api/server/Hitboxes.java
create mode 100644 rsx-protocol/src/main/java/net/rustnotmc/protocol/Hitbox.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/hitbox/HitboxRegistry.java
create mode 100644 rsx-server-core/src/main/java/net/rustnotmc/server/util/Json.java
create mode 100644 rsx-server-core/src/test/java/net/rustnotmc/server/hitbox/HitboxTest.java
diff --git a/game-fork/patches/decompiler-fixes.txt b/game-fork/patches/decompiler-fixes.txt
index dcb6f6c..11ce877 100644
--- a/game-fork/patches/decompiler-fixes.txt
+++ b/game-fork/patches/decompiler-fixes.txt
@@ -1263,3 +1263,17 @@ import org.lwjgl.opengl.ContextCapabilities;
--- AFTER
this.player.movementInput = new net.rustnotmc.client.RsxMovementInput(this.gameSettings);
+# Our crouch is lower than the vanilla one, so the player fits through a hole
+# of one and a half blocks. The number lives in RsxPlayerTuning.
+=== FILE net/minecraft/entity/player/EntityPlayer.java
+--- BEFORE
+ } else if (this.isSneaking()) {
+ f = 0.6F;
+ f1 = 1.65F;
+ } else {
+--- AFTER
+ } else if (this.isSneaking()) {
+ f = 0.6F;
+ f1 = net.rustnotmc.client.RsxPlayerTuning.crouchHeight();
+ } else {
+
diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java b/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java
index 3863b12..1bb38b3 100644
--- a/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java
+++ b/game-fork/src-overlay/net/rustnotmc/client/RsxBootstrap.java
@@ -114,6 +114,14 @@ public final class RsxBootstrap {
* that has to see them and decide.
*/
public static boolean handleChatCommand(String line) {
+ if (line == null || !line.startsWith("/")) {
+ return false;
+ }
+ // our own commands come first: they are not mod commands, and a mod
+ // should not be able to take a name that the game itself uses
+ if (RsxHitboxDebug.command(line)) {
+ return true;
+ }
if (client == null || !ClientHost.present()) {
return false;
}
@@ -203,6 +211,7 @@ public final class RsxBootstrap {
if (client != null) {
client.advanceFrame();
}
+ RsxHitboxDebug.draw(minecraft);
drawOverlay(minecraft);
}
diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxHitboxDebug.java b/game-fork/src-overlay/net/rustnotmc/client/RsxHitboxDebug.java
new file mode 100644
index 0000000..414e7b8
--- /dev/null
+++ b/game-fork/src-overlay/net/rustnotmc/client/RsxHitboxDebug.java
@@ -0,0 +1,263 @@
+package net.rustnotmc.client;
+
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import net.minecraft.client.Minecraft;
+import net.minecraft.client.entity.EntityPlayerSP;
+import net.minecraft.client.renderer.GlStateManager;
+import net.minecraft.client.renderer.RenderGlobal;
+import net.minecraft.client.renderer.debug.DebugRenderer;
+import net.minecraft.entity.Entity;
+import net.rustnotmc.protocol.Hitbox;
+import net.rustnotmc.protocol.Packets;
+
+/**
+ * Draws the boxes of the models.
+ *
+ * The boxes are the editor: the server reads them from a file and sends them
+ * here, and they are drawn around the things they belong to. A model that
+ * does not fit is changed in the file, the file is read again, and the new
+ * numbers are on the screen.
+ *
+ * The command is:
+ *
+ *
+ * /rsx hitboxes on show the boxes
+ * /rsx hitboxes off hide them
+ * /rsx hitboxes reload ask the server to read its file again
+ * /rsx hitboxes list what the server knows
+ * /rsx hitboxes show NAME put one model in front of the player
+ *
+ */
+public final class RsxHitboxDebug {
+
+ /** The model whose boxes describe a player. */
+ private static final String PLAYER_MODEL = "player";
+
+ /** The height the player model is written for. */
+ private static final float PLAYER_MODEL_HEIGHT = 1.8F;
+
+ private static final Map MODELS =
+ new LinkedHashMap();
+ private static final List INSTANCES =
+ new ArrayList();
+
+ private static boolean visible;
+ private static String preview;
+
+ private RsxHitboxDebug() {
+ }
+
+ /** Takes the boxes of the models from the server. */
+ public static void accept(Packets.Hitboxes boxes) {
+ MODELS.clear();
+ for (int i = 0; i < boxes.models.length; i++) {
+ MODELS.put(boxes.models[i].name, boxes.models[i]);
+ }
+ }
+
+ /** Takes the places where the models stand. */
+ public static void accept(Packets.HitboxInstances instances) {
+ INSTANCES.clear();
+ for (int i = 0; i < instances.instances.length; i++) {
+ INSTANCES.add(instances.instances[i]);
+ }
+ }
+
+ /** True when the boxes are being drawn. */
+ public static boolean visible() {
+ return visible;
+ }
+
+ /** How many models the server has sent. */
+ public static int modelCount() {
+ return MODELS.size();
+ }
+
+ /**
+ * Carries out one of our commands.
+ *
+ * Returns true when the line was ours, so that the mods do not see it.
+ */
+ public static boolean command(String line) {
+ if (line == null || !line.startsWith("/")) {
+ return false;
+ }
+ String[] parts = line.substring(1).trim().split("\\s+");
+ if (parts.length < 2 || !"rsx".equalsIgnoreCase(parts[0])
+ || !"hitboxes".equalsIgnoreCase(parts[1])) {
+ return false;
+ }
+ String what = parts.length > 2 ? parts[2].toLowerCase() : "";
+
+ if ("on".equals(what)) {
+ visible = true;
+ say("hitboxes on (" + MODELS.size() + " models)");
+ return true;
+ }
+ if ("off".equals(what)) {
+ visible = false;
+ say("hitboxes off");
+ return true;
+ }
+ if ("reload".equals(what)) {
+ net.rustnotmc.client.net.RsxNetworkManager.sendHitboxesReload();
+ say("the server was asked to read its file again");
+ return true;
+ }
+ if ("list".equals(what)) {
+ StringBuilder names = new StringBuilder();
+ for (String name : MODELS.keySet()) {
+ if (names.length() > 0) {
+ names.append(", ");
+ }
+ names.append(name);
+ }
+ say("models: " + (names.length() == 0 ? "none" : names.toString()));
+ return true;
+ }
+ if ("show".equals(what)) {
+ if (parts.length < 4) {
+ say("usage: /rsx hitboxes show NAME");
+ return true;
+ }
+ preview = parts[3];
+ visible = true;
+ say("showing " + preview);
+ return true;
+ }
+ say("usage: /rsx hitboxes on|off|reload|list|show NAME");
+ return true;
+ }
+
+ private static void say(String message) {
+ net.rustnotmc.loader.Log.info("hitboxes: " + message);
+ Minecraft mc = Minecraft.getMinecraft();
+ if (mc.ingameGUI != null) {
+ mc.ingameGUI.addChatMessage(net.minecraft.util.text.ChatType.CHAT,
+ new net.minecraft.util.text.TextComponentString(message));
+ }
+ }
+
+ /**
+ * Draws the boxes.
+ *
+ * Called from the frame hook, so this is after the world and after the
+ * interface: the lines are drawn over both, which is what a debug view
+ * should do.
+ */
+ public static void draw(Minecraft mc) {
+ if (!visible || mc.player == null || mc.world == null) {
+ return;
+ }
+ Entity view = mc.getRenderViewEntity();
+ if (view == null) {
+ return;
+ }
+ float partial = mc.getRenderPartialTicks();
+ double camX = view.lastTickPosX + (view.posX - view.lastTickPosX) * partial;
+ double camY = view.lastTickPosY + (view.posY - view.lastTickPosY) * partial;
+ double camZ = view.lastTickPosZ + (view.posZ - view.lastTickPosZ) * partial;
+
+ GlStateManager.enableBlend();
+ GlStateManager.tryBlendFuncSeparate(GlStateManager.SourceFactor.SRC_ALPHA,
+ GlStateManager.DestFactor.ONE_MINUS_SRC_ALPHA,
+ GlStateManager.SourceFactor.ONE, GlStateManager.DestFactor.ZERO);
+ GlStateManager.glLineWidth(2.0F);
+ GlStateManager.disableTexture2D();
+ GlStateManager.depthMask(false);
+
+ for (int i = 0; i < INSTANCES.size(); i++) {
+ Packets.HitboxInstance instance = INSTANCES.get(i);
+ Packets.HitboxModel model = MODELS.get(instance.model);
+ if (model == null) {
+ continue;
+ }
+ drawModel(model, instance.x, instance.y, instance.z, instance.yaw,
+ camX, camY, camZ, 1.0F);
+ }
+
+ if (preview != null) {
+ Packets.HitboxModel model = MODELS.get(preview);
+ if (model != null) {
+ EntityPlayerSP player = mc.player;
+ drawModel(model, player.posX + 1.0D, player.posY, player.posZ,
+ player.rotationYaw, camX, camY, camZ, 1.0F);
+ }
+ }
+
+ // and the players, with the zones the damage is counted by
+ Packets.HitboxModel player = MODELS.get(PLAYER_MODEL);
+ if (player != null) {
+ List players = mc.world.getLoadedEntityList();
+ for (int i = 0; i < players.size(); i++) {
+ Entity entity = players.get(i);
+ if (!(entity instanceof EntityPlayerSP)) {
+ continue;
+ }
+ float scale = entity.height / PLAYER_MODEL_HEIGHT;
+ drawModel(player, entity.posX, entity.posY, entity.posZ,
+ entity.rotationYaw, camX, camY, camZ, scale);
+ }
+ }
+
+ GlStateManager.depthMask(true);
+ GlStateManager.enableTexture2D();
+ GlStateManager.disableBlend();
+ }
+
+ /**
+ * Draws one model.
+ *
+ * The boxes of the model are turned by the yaw of the thing and moved to
+ * where it stands, and then drawn relative to the camera, because that is
+ * how the world is drawn too.
+ */
+ private static void drawModel(Packets.HitboxModel model, double x, double y, double z,
+ float yaw, double camX, double camY, double camZ, float scale) {
+ double cos = Math.cos(Math.toRadians(yaw));
+ double sin = Math.sin(Math.toRadians(yaw));
+ for (int i = 0; i < model.boxes.length; i++) {
+ Hitbox box = model.boxes[i];
+ float[] colour = colourOf(box.part);
+ double minX = turn(box.minX * scale, box.minZ * scale, cos, sin);
+ double minZ = turn(box.minZ * scale, -box.minX * scale, cos, sin);
+ double maxX = turn(box.maxX * scale, box.maxZ * scale, cos, sin);
+ double maxZ = turn(box.maxZ * scale, -box.maxX * scale, cos, sin);
+ double minY = box.minY * scale;
+ double maxY = box.maxY * scale;
+
+ RenderGlobal.drawBoundingBox(
+ x + minX - camX, y + minY - camY, z + minZ - camZ,
+ x + maxX - camX, y + maxY - camY, z + maxZ - camZ,
+ colour[0], colour[1], colour[2], 0.9F);
+
+ if (!box.name.isEmpty()) {
+ DebugRenderer.renderDebugText(box.name,
+ x + minX, y + maxY, z + minZ, 0.02F, 0xFFFFFFFF);
+ }
+ }
+ }
+
+ /** Turns a point around the vertical axis. */
+ private static double turn(double along, double across, double cos, double sin) {
+ return along * cos + across * sin;
+ }
+
+ /** The colour of a box: the head is red, the body yellow, the legs blue. */
+ private static float[] colourOf(String part) {
+ if (Hitbox.PART_HEAD.equals(part)) {
+ return new float[]{1.0F, 0.25F, 0.25F};
+ }
+ if (Hitbox.PART_BODY.equals(part)) {
+ return new float[]{1.0F, 0.9F, 0.2F};
+ }
+ if (Hitbox.PART_LEGS.equals(part)) {
+ return new float[]{0.3F, 0.55F, 1.0F};
+ }
+ return new float[]{0.3F, 1.0F, 0.4F};
+ }
+}
diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java
index 3e0902b..ff66e30 100644
--- a/game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java
+++ b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayer.java
@@ -10,10 +10,11 @@ import net.minecraft.world.World;
/**
* Our player.
*
- * The vanilla player with two things changed: the crouch is a little faster
- * and the jump is a little higher. Everything else is the vanilla behaviour,
- * so the player still walks, swims, rides, fights and dies the way the game
- * always did.
+ * The vanilla player with three things changed: the crouch is low enough to
+ * squeeze through a hole of one and a half blocks, the hop from a standstill
+ * is higher than the vanilla jump, and the eyes follow the lower crouch.
+ * Everything else is the vanilla behaviour, so the player still walks, swims,
+ * rides, fights and dies the way the game always did.
*
* The class exists so that the numbers of our movement live in our code and
* not in the patches of the vanilla tree: the patches only have to say "the
@@ -30,11 +31,38 @@ public class RsxPlayer extends EntityPlayerSP {
/**
* How high the player jumps.
*
+ * Jumping on the spot is the Rust-like hop and clears more than the
+ * vanilla jump does. Jumping while moving keeps the vanilla height, so a
+ * running jump stays the same as it always was.
+ *
* The override lives here and not in EntityLivingBase, because the mobs
* have to keep jumping the way they always did.
*/
@Override
protected float getJumpUpwardsMotion() {
- return RsxPlayerTuning.jumpMotion();
+ if (this.movementInput == null || !isMoving()) {
+ return RsxPlayerTuning.jumpMotion(RsxPlayerTuning.jumpHeightStanding());
+ }
+ return RsxPlayerTuning.jumpMotion(RsxPlayerTuning.jumpHeightMoving());
+ }
+
+ /** True when the player is asking to move sideways or forward. */
+ private boolean isMoving() {
+ return this.movementInput.moveStrafe != 0.0F || this.movementInput.moveForward != 0.0F;
+ }
+
+ /**
+ * Where the eyes are.
+ *
+ * The vanilla crouch lowers the player to 1.65 and puts the eyes at 1.54.
+ * Ours is lower, so the eyes have to come down with it, or they would sit
+ * above the head.
+ */
+ @Override
+ public float getEyeHeight() {
+ if (this.isSneaking() && !this.isPlayerSleeping() && !this.isElytraFlying()) {
+ return RsxPlayerTuning.crouchHeight() - RsxPlayerTuning.CROUCH_EYE_OFFSET;
+ }
+ return super.getEyeHeight();
}
}
diff --git a/game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java
index 3068e32..721a18d 100644
--- a/game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java
+++ b/game-fork/src-overlay/net/rustnotmc/client/RsxPlayerTuning.java
@@ -30,6 +30,28 @@ public final class RsxPlayerTuning {
/** How high the player jumps, in blocks. The vanilla jumps 1.25. */
public static final float JUMP_HEIGHT = 1.35F;
+ /**
+ * How high the player jumps when it is standing still, in blocks.
+ *
+ * Jumping on the spot is the Rust-like hop: it clears a block that the
+ * walking jump does not. Jumping while moving keeps the vanilla height.
+ */
+ public static final float JUMP_HEIGHT_STANDING = 1.5F;
+
+ /** How high the player jumps while moving, in blocks. The vanilla. */
+ public static final float JUMP_HEIGHT_MOVING = 1.25F;
+
+ /**
+ * How tall the player is while crouching, in blocks.
+ *
+ * The vanilla crouches to 1.65, which does not fit through a hole of one
+ * and a half blocks. Ours is low enough to squeeze through it.
+ */
+ public static final float CROUCH_HEIGHT = 1.45F;
+
+ /** How far below the top of the head the eyes sit while crouching. */
+ public static final float CROUCH_EYE_OFFSET = 0.08F;
+
/** The crouch never goes below this, in blocks per second. */
public static final float SNEAK_SPEED_FLOOR = 1.5F;
@@ -61,6 +83,21 @@ public final class RsxPlayerTuning {
return setting("rsx.jumpHeight", JUMP_HEIGHT, JUMP_HEIGHT_FLOOR);
}
+ /** The height of the hop from a standstill, in blocks. */
+ public static float jumpHeightStanding() {
+ return setting("rsx.jumpHeightStanding", JUMP_HEIGHT_STANDING, JUMP_HEIGHT_FLOOR);
+ }
+
+ /** The height of the jump while moving, in blocks. */
+ public static float jumpHeightMoving() {
+ return setting("rsx.jumpHeightMoving", JUMP_HEIGHT_MOVING, JUMP_HEIGHT_FLOOR);
+ }
+
+ /** How tall the player is while crouching, in blocks. */
+ public static float crouchHeight() {
+ return setting("rsx.crouchHeight", CROUCH_HEIGHT, CROUCH_HEIGHT);
+ }
+
/** The factor the crouch slows the movement down by. */
public static float sneakFactor() {
return sneakSpeed() / WALK_SPEED;
diff --git a/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java b/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java
index 6158337..9e2a21c 100644
--- a/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java
+++ b/game-fork/src-overlay/net/rustnotmc/client/net/RsxNetworkManager.java
@@ -249,11 +249,35 @@ public final class RsxNetworkManager extends NetworkManager {
RsxClientMode.set(gameMode.mode);
return;
}
+ case Packets.HITBOXES: {
+ Packets.Hitboxes boxes = Packets.Hitboxes.read(new Buf(frame.body()));
+ net.rustnotmc.client.RsxHitboxDebug.accept(boxes);
+ return;
+ }
+ case Packets.HITBOX_INSTANCES: {
+ Packets.HitboxInstances instances =
+ Packets.HitboxInstances.read(new Buf(frame.body()));
+ net.rustnotmc.client.RsxHitboxDebug.accept(instances);
+ return;
+ }
default:
onPlay(frame);
}
}
+ /**
+ * Asks the server to read its box file again.
+ *
+ * The file is the editor, so after a change the client wants the new
+ * numbers without the server having to be restarted.
+ */
+ public static void sendHitboxesReload() {
+ RsxNetworkManager manager = active();
+ if (manager != null) {
+ manager.send(Packets.HITBOXES_RELOAD, new Packets.HitboxesReload());
+ }
+ }
+
private void onHelloAck(Frame frame) {
Packets.HelloAck hello = Packets.HelloAck.read(new Buf(frame.body()));
this.nonce = hello.nonce;
diff --git a/rsx-plugin-api/scripts/build.ps1 b/rsx-plugin-api/scripts/build.ps1
new file mode 100644
index 0000000..fae5496
--- /dev/null
+++ b/rsx-plugin-api/scripts/build.ps1
@@ -0,0 +1,73 @@
+# Builds rsx-plugin-api into dist\.
+#
+# The API is the way a plugin talks to the server. It is a separate module on
+# purpose: the server and the plugin both compile against it, and neither
+# needs the other.
+#
+# NOTE: ASCII on purpose. Windows PowerShell 5.1 reads a .ps1 with no byte order
+# mark using the system ANSI code page, which mangles UTF-8 and breaks the
+# parser. Keep script messages English.
+
+$ErrorActionPreference = 'Stop'
+
+$Repo = Split-Path -Parent $PSScriptRoot
+if (-not $Repo) { $Repo = $PSCommandPath | Split-Path -Parent | Split-Path -Parent }
+$Workspace = Split-Path -Parent $Repo
+$Dist = Join-Path $Repo 'dist'
+$Out = Join-Path $Repo 'build'
+
+function Say($text) { Write-Host " $text" -ForegroundColor Cyan }
+
+if (-not (Get-Command javac -ErrorAction SilentlyContinue)) {
+ Write-Host ' A JDK (javac) is required.' -ForegroundColor Red
+ exit 1
+}
+
+New-Item -ItemType Directory -Force -Path $Dist, $Out | Out-Null
+
+# The API speaks about items and boxes, so it is compiled against the
+# protocol: the same jar the server and the client use.
+$Protocol = Join-Path $Workspace 'rsx-protocol\build\classes'
+if (-not (Test-Path $Protocol)) {
+ Write-Host " Missing dependency: $Protocol" -ForegroundColor Red
+ Write-Host ' Build rsx-protocol first.' -ForegroundColor Red
+ exit 1
+}
+
+$mainOut = Join-Path $Out 'classes'
+Remove-Item $mainOut -Recurse -Force -ErrorAction SilentlyContinue
+New-Item -ItemType Directory -Force -Path $mainOut | Out-Null
+
+$mainFiles = @(Get-ChildItem (Join-Path $Repo 'src\main\java') -Recurse -Filter '*.java' |
+ ForEach-Object { $_.FullName })
+if ($mainFiles.Count -eq 0) {
+ Write-Host ' No sources found.' -ForegroundColor Red
+ exit 1
+}
+
+& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -cp $Protocol -d $mainOut $mainFiles
+if ($LASTEXITCODE -ne 0) {
+ Write-Host ' The plugin API failed to compile.' -ForegroundColor Red
+ exit 1
+}
+Say "plugin API compiled ($($mainFiles.Count) files)"
+
+$manifest = Join-Path $Out 'manifest.txt'
+@"
+Manifest-Version: 1.0
+Implementation-Title: RustNotMC plugin API
+Implementation-Version: 0.1.0
+"@ | Set-Content -LiteralPath $manifest -Encoding ASCII
+
+$jar = Join-Path $Dist 'rsx-plugin-api.jar'
+if (Test-Path -LiteralPath $jar) { Remove-Item -LiteralPath $jar -Force }
+& jar cfm $jar $manifest -C $mainOut .
+if ($LASTEXITCODE -ne 0) {
+ Write-Host ' Could not package the jar.' -ForegroundColor Red
+ exit 1
+}
+Say "rsx-plugin-api.jar ($((Get-Item $jar).Length) bytes)"
+
+Write-Host ''
+Write-Host ' Done:' -ForegroundColor Green
+Write-Host " $jar"
diff --git a/rsx-plugin-api/src/main/java/net/rustnotmc/api/server/Hitboxes.java b/rsx-plugin-api/src/main/java/net/rustnotmc/api/server/Hitboxes.java
new file mode 100644
index 0000000..f6e5805
--- /dev/null
+++ b/rsx-plugin-api/src/main/java/net/rustnotmc/api/server/Hitboxes.java
@@ -0,0 +1,30 @@
+package net.rustnotmc.api.server;
+
+import net.rustnotmc.protocol.Hitbox;
+
+/**
+ * The boxes of the models, on the server.
+ *
+ * A plugin that brings its own object says where its boxes are, and the game
+ * uses them: for the collision, for the damage, and for the drawing that the
+ * editor does.
+ *
+ * The boxes of our own models are in a file that the server reads, so that
+ * they can be changed without touching any code.
+ */
+public interface Hitboxes {
+
+ /**
+ * Says where the boxes of a model are.
+ *
+ * A model that is already known is replaced, so a plugin may take a
+ * model over.
+ */
+ void register(String model, Hitbox[] boxes);
+
+ /** The boxes of a model, or null when it has none. */
+ Hitbox[] of(String model);
+
+ /** True when the model has boxes. */
+ boolean has(String model);
+}
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Hitbox.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Hitbox.java
new file mode 100644
index 0000000..4811794
--- /dev/null
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Hitbox.java
@@ -0,0 +1,80 @@
+package net.rustnotmc.protocol;
+
+/**
+ * One box around a thing.
+ *
+ * A hitbox is not a block: it is a box in the space of a model, and it says
+ * what it is for. The name tells the two boxes of the same model apart, and
+ * the part tells which part of a player the box belongs to, because the damage
+ * of a hit depends on where it landed.
+ *
+ * The boxes live in a file, so that a model can be fitted by hand: the
+ * server reads the file, the client draws what it read, and changing a number
+ * and reloading shows the result right away.
+ */
+public final class Hitbox {
+
+ /** The box is not on a player, so it takes no part in the damage. */
+ public static final String PART_NONE = "";
+
+ /** The head of a player: a hit here hurts twice as much. */
+ public static final String PART_HEAD = "head";
+
+ /** The torso of a player. */
+ public static final String PART_BODY = "body";
+
+ /** The legs of a player. */
+ public static final String PART_LEGS = "legs";
+
+ public final String name;
+ public final String part;
+ public final float minX;
+ public final float minY;
+ public final float minZ;
+ public final float maxX;
+ public final float maxY;
+ public final float maxZ;
+
+ /** The turn of the box around the vertical axis, in degrees. */
+ public final float yaw;
+
+ public Hitbox(String name, String part, float minX, float minY, float minZ,
+ float maxX, float maxY, float maxZ, float yaw) {
+ this.name = name == null ? "" : name;
+ this.part = part == null ? PART_NONE : part;
+ this.minX = minX;
+ this.minY = minY;
+ this.minZ = minZ;
+ this.maxX = maxX;
+ this.maxY = maxY;
+ this.maxZ = maxZ;
+ this.yaw = yaw;
+ }
+
+ /** True when the box belongs to a part of a player. */
+ public boolean onPlayer() {
+ return !PART_NONE.equals(this.part);
+ }
+
+ /** The width of the box. */
+ public float width() {
+ return this.maxX - this.minX;
+ }
+
+ /** The height of the box. */
+ public float height() {
+ return this.maxY - this.minY;
+ }
+
+ /** The depth of the box. */
+ public float depth() {
+ return this.maxZ - this.minZ;
+ }
+
+ @Override
+ public String toString() {
+ return "Hitbox[" + this.name + (this.part.isEmpty() ? "" : "/" + this.part)
+ + " " + this.minX + "," + this.minY + "," + this.minZ
+ + " .. " + this.maxX + "," + this.maxY + "," + this.maxZ + "]";
+ }
+}
diff --git a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java
index d49b420..4f31fc5 100644
--- a/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java
+++ b/rsx-protocol/src/main/java/net/rustnotmc/protocol/Packets.java
@@ -149,6 +149,15 @@ public final class Packets {
/** The client learns a blueprint. */
public static final int LEARN_BLUEPRINT = 0x29;
+ /** The boxes of every model the server knows. */
+ public static final int HITBOXES = 0x2A;
+
+ /** The places where those models stand. */
+ public static final int HITBOX_INSTANCES = 0x2B;
+
+ /** The client asks the server to read its box file again. */
+ public static final int HITBOXES_RELOAD = 0x2C;
+
/**
* One slot of the inventory and what is in it now.
*
@@ -1372,6 +1381,156 @@ public final class Packets {
}
}
+ /** The boxes of one model. */
+ public static final class HitboxModel {
+
+ public final String name;
+ public final Hitbox[] boxes;
+
+ public HitboxModel(String name, Hitbox[] boxes) {
+ this.name = name;
+ this.boxes = boxes;
+ }
+ }
+
+ /**
+ * The boxes of every model the server knows.
+ *
+ * The file with the boxes is the editor: it is read by the server, sent
+ * to the client and drawn there, so that a change in the file can be
+ * seen without anything else being touched.
+ */
+ public static final class Hitboxes implements Packet {
+
+ public final HitboxModel[] models;
+
+ public Hitboxes(HitboxModel[] models) {
+ this.models = models;
+ }
+
+ public Buf write() {
+ Buf buf = new Buf(256 + this.models.length * 64);
+ buf.writeVarInt(this.models.length);
+ for (int i = 0; i < this.models.length; i++) {
+ HitboxModel model = this.models[i];
+ buf.writeString(model.name);
+ buf.writeVarInt(model.boxes.length);
+ for (int j = 0; j < model.boxes.length; j++) {
+ Hitbox box = model.boxes[j];
+ buf.writeString(box.name);
+ buf.writeString(box.part);
+ buf.writeFloat(box.minX);
+ buf.writeFloat(box.minY);
+ buf.writeFloat(box.minZ);
+ buf.writeFloat(box.maxX);
+ buf.writeFloat(box.maxY);
+ buf.writeFloat(box.maxZ);
+ buf.writeFloat(box.yaw);
+ }
+ }
+ return buf;
+ }
+
+ public static Hitboxes read(Buf in) {
+ int count = in.readVarInt();
+ if (count < 0 || count > 4096) {
+ throw new ProtocolException("that many box models is not real");
+ }
+ HitboxModel[] models = new HitboxModel[count];
+ for (int i = 0; i < count; i++) {
+ String name = in.readString(MAX_TEXT);
+ int boxes = in.readVarInt();
+ if (boxes < 0 || boxes > 256) {
+ throw new ProtocolException("that many boxes is not real");
+ }
+ Hitbox[] list = new Hitbox[boxes];
+ for (int j = 0; j < boxes; j++) {
+ String boxName = in.readString(MAX_TEXT);
+ String part = in.readString(MAX_TEXT);
+ list[j] = new Hitbox(boxName, part,
+ in.readFloat(), in.readFloat(), in.readFloat(),
+ in.readFloat(), in.readFloat(), in.readFloat(),
+ in.readFloat());
+ }
+ models[i] = new HitboxModel(name, list);
+ }
+ return new Hitboxes(models);
+ }
+ }
+
+ /** One model standing somewhere in the world. */
+ public static final class HitboxInstance {
+
+ public final String model;
+ public final float x;
+ public final float y;
+ public final float z;
+ public final float yaw;
+
+ public HitboxInstance(String model, float x, float y, float z, float yaw) {
+ this.model = model;
+ this.x = x;
+ this.y = y;
+ this.z = z;
+ this.yaw = yaw;
+ }
+ }
+
+ /** Every place where a model with boxes stands. */
+ public static final class HitboxInstances implements Packet {
+
+ public final HitboxInstance[] instances;
+
+ public HitboxInstances(HitboxInstance[] instances) {
+ this.instances = instances;
+ }
+
+ public Buf write() {
+ Buf buf = new Buf(32 + this.instances.length * 32);
+ buf.writeVarInt(this.instances.length);
+ for (int i = 0; i < this.instances.length; i++) {
+ HitboxInstance instance = this.instances[i];
+ buf.writeString(instance.model);
+ buf.writeFloat(instance.x);
+ buf.writeFloat(instance.y);
+ buf.writeFloat(instance.z);
+ buf.writeFloat(instance.yaw);
+ }
+ return buf;
+ }
+
+ public static HitboxInstances read(Buf in) {
+ int count = in.readVarInt();
+ if (count < 0 || count > 65536) {
+ throw new ProtocolException("that many box instances is not real");
+ }
+ HitboxInstance[] instances = new HitboxInstance[count];
+ for (int i = 0; i < count; i++) {
+ String model = in.readString(MAX_TEXT);
+ instances[i] = new HitboxInstance(model, in.readFloat(), in.readFloat(),
+ in.readFloat(), in.readFloat());
+ }
+ return new HitboxInstances(instances);
+ }
+ }
+
+ /**
+ * The client asks the server to read its box file again.
+ *
+ * The file is the editor, so after a change the client wants the new
+ * numbers without the server having to be restarted.
+ */
+ public static final class HitboxesReload implements Packet {
+
+ public Buf write() {
+ return new Buf(4);
+ }
+
+ public static HitboxesReload read(Buf in) {
+ return new HitboxesReload();
+ }
+ }
+
/**
* Every packet id this build knows, and what it decodes into.
*
@@ -1412,6 +1571,9 @@ public final class Packets {
map.put(Integer.valueOf(CONTAINER_OPEN), "ContainerOpen");
map.put(Integer.valueOf(CONTAINER_TAKE), "ContainerTake");
map.put(Integer.valueOf(LEARN_BLUEPRINT), "LearnBlueprint");
+ map.put(Integer.valueOf(HITBOXES), "Hitboxes");
+ map.put(Integer.valueOf(HITBOX_INSTANCES), "HitboxInstances");
+ map.put(Integer.valueOf(HITBOXES_RELOAD), "HitboxesReload");
map.put(Integer.valueOf(DISCONNECT), "Disconnect");
return map;
}
diff --git a/rsx-server-core/scripts/build.ps1 b/rsx-server-core/scripts/build.ps1
index 4f4a377..2337c16 100644
--- a/rsx-server-core/scripts/build.ps1
+++ b/rsx-server-core/scripts/build.ps1
@@ -14,10 +14,11 @@ function Say($text) { Write-Host " $text" -ForegroundColor Cyan }
$Protocol = Join-Path $Workspace 'rsx-protocol\build\classes'
$Auth = Join-Path $Workspace 'rsx-auth\build\classes'
-foreach ($dep in @($Protocol, $Auth)) {
+$PluginApi = Join-Path $Workspace 'rsx-plugin-api\build\classes'
+foreach ($dep in @($Protocol, $Auth, $PluginApi)) {
if (-not (Test-Path $dep)) {
Write-Host " Missing dependency: $dep" -ForegroundColor Red
- Write-Host ' Build rsx-protocol and rsx-auth first.' -ForegroundColor Red
+ Write-Host ' Build rsx-protocol, rsx-auth and rsx-plugin-api first.' -ForegroundColor Red
exit 1
}
}
@@ -32,7 +33,7 @@ $mainFiles = @(Get-ChildItem (Join-Path $Repo 'src\main\java') -Recurse -Filter
ForEach-Object { $_.FullName })
if ($mainFiles.Count -eq 0) { Write-Host ' No sources found.' -ForegroundColor Red; exit 1 }
-$cp = "$Protocol;$Auth"
+$cp = "$Protocol;$Auth;$PluginApi"
& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -cp $cp -d $mainOut $mainFiles
if ($LASTEXITCODE -ne 0) { Write-Host ' Server failed to compile.' -ForegroundColor Red; exit 1 }
Say "server compiled ($($mainFiles.Count) files)"
@@ -42,7 +43,7 @@ $manifest = Join-Path $Out 'manifest.txt'
Manifest-Version: 1.0
Implementation-Title: RustNotMC server
Implementation-Version: 0.1.0
-Class-Path: rsx-protocol.jar rsx-auth.jar
+Class-Path: rsx-protocol.jar rsx-auth.jar rsx-plugin-api.jar
"@ | Set-Content -LiteralPath $manifest -Encoding ASCII
$jar = Join-Path $Dist 'rsx-server-core.jar'
diff --git a/rsx-server-core/scripts/test.ps1 b/rsx-server-core/scripts/test.ps1
index fc05bc0..acdb7a9 100644
--- a/rsx-server-core/scripts/test.ps1
+++ b/rsx-server-core/scripts/test.ps1
@@ -16,10 +16,11 @@ function Say($text) { Write-Host " $text" -ForegroundColor Cyan }
$Protocol = Join-Path $Workspace 'rsx-protocol\build\classes'
$Auth = Join-Path $Workspace 'rsx-auth\build\classes'
-foreach ($dep in @($Protocol, $Auth)) {
+$PluginApi = Join-Path $Workspace 'rsx-plugin-api\build\classes'
+foreach ($dep in @($Protocol, $Auth, $PluginApi)) {
if (-not (Test-Path $dep)) {
Write-Host " Missing dependency: $dep" -ForegroundColor Red
- Write-Host ' Build rsx-protocol and rsx-auth first.' -ForegroundColor Red
+ Write-Host ' Build rsx-protocol, rsx-auth and rsx-plugin-api first.' -ForegroundColor Red
exit 1
}
}
@@ -31,7 +32,7 @@ Remove-Item $mainOut -Recurse -Force -ErrorAction SilentlyContinue
New-Item -ItemType Directory -Force -Path $mainOut | Out-Null
$mainFiles = @(Get-ChildItem (Join-Path $Repo 'src\main\java') -Recurse -Filter '*.java' |
ForEach-Object { $_.FullName })
-$cp = "$Protocol;$Auth"
+$cp = "$Protocol;$Auth;$PluginApi"
& javac -encoding UTF-8 --release 17 -Xlint:-options -nowarn -cp $cp -d $mainOut $mainFiles
if ($LASTEXITCODE -ne 0) { Write-Host ' Main failed.' -ForegroundColor Red; exit 1 }
@@ -50,6 +51,7 @@ $suites = @(
'net.rustnotmc.server.world.NodeTest',
'net.rustnotmc.server.item.InventoryTest',
'net.rustnotmc.server.craft.CraftTest',
+ 'net.rustnotmc.server.hitbox.HitboxTest',
'net.rustnotmc.server.PlayTest'
)
foreach ($suite in $suites) {
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java
index 1fdd424..31de3d2 100644
--- a/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/ClientSession.java
@@ -229,6 +229,9 @@ public final class ClientSession implements Runnable {
case Packets.LEARN_BLUEPRINT:
onLearnBlueprint(frame);
return true;
+ case Packets.HITBOXES_RELOAD:
+ onHitboxesReload(frame);
+ return true;
case Packets.PLAYER_MOVE:
onPlayerMove(frame);
return true;
@@ -472,6 +475,8 @@ public final class ClientSession implements Runnable {
new Packets.KnownRecipes(this.player.knowledge().knownRecipes()));
send(Packets.TECH_TREE_STATE,
new Packets.TechTreeState(this.player.knowledge().unlockedNodes()));
+ // the boxes of the models, so that the client can draw them
+ this.sendHitboxes();
this.player.streamChunks(this.server.world());
}
@@ -632,8 +637,32 @@ public final class ClientSession implements Runnable {
send(Packets.INVENTORY, this.player.inventory().toPacket());
}
- /**
- * The player learned a blueprint.
+ /**
+ * The client wants the box file read again.
+ *
+ * The file is the editor, so after a change the client asks for the new
+ * numbers. Everybody is told, not only the one who asked.
+ */
+ private void onHitboxesReload(Frame frame) {
+ if (this.state != SessionState.PLAYING || this.player == null) {
+ return;
+ }
+ String error = this.server.reloadHitboxes();
+ if (!error.isEmpty()) {
+ this.server.log("hitboxes: " + error);
+ }
+ }
+
+ /** Sends the boxes of the models to the client. */
+ void sendHitboxes() {
+ if (this.player == null) {
+ return;
+ }
+ send(Packets.HITBOXES, this.server.hitboxes().toPacket());
+ }
+
+ /**
+ * The client learned a blueprint.
*
* The blueprint is used up and the recipe opens for good.
*/
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java
index aba7687..4af764c 100644
--- a/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/Server.java
@@ -18,6 +18,7 @@ import net.rustnotmc.auth.Passwords;
import net.rustnotmc.auth.TokenService;
import net.rustnotmc.protocol.Packets;
import net.rustnotmc.server.world.ChunkPos;
+import net.rustnotmc.server.hitbox.HitboxRegistry;
import net.rustnotmc.server.world.ChunkStorage;
import net.rustnotmc.server.world.World;
@@ -56,6 +57,7 @@ public final class Server {
private final World world;
private volatile LauncherAuth launcherAuth;
private volatile boolean localAccounts = true;
+ private volatile HitboxRegistry hitboxes = new HitboxRegistry(null);
private volatile ServerSocket listener;
private volatile Thread acceptThread;
@@ -85,6 +87,39 @@ public final class Server {
return this.world;
}
+ /**
+ * The boxes of the models.
+ *
+ * The file with the boxes is the editor: it is read by the server and
+ * sent to the client, which draws them, so that a model can be fitted by
+ * changing a number and looking at the result.
+ */
+ public HitboxRegistry hitboxes() {
+ return this.hitboxes;
+ }
+
+ public void hitboxes(HitboxRegistry hitboxes) {
+ this.hitboxes = hitboxes == null ? new HitboxRegistry(null) : hitboxes;
+ }
+
+ /**
+ * Reads the box file again and tells everybody about it.
+ *
+ * Returns the reason when the file could not be read.
+ */
+ public String reloadHitboxes() {
+ HitboxRegistry registry = this.hitboxes;
+ registry.reload();
+ String error = registry.lastError();
+ if (!error.isEmpty()) {
+ return error;
+ }
+ for (ClientSession session : this.sessions.values()) {
+ session.sendHitboxes();
+ }
+ return "";
+ }
+
/** The players in the world, by player id. */
public Map players() {
return this.players;
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java b/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java
index 9c05060..97035ab 100644
--- a/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/ServerMain.java
@@ -45,6 +45,7 @@ public final class ServerMain {
String gameMode = "survival";
String register = null;
String password = null;
+ Path hitboxes = worldDir.resolve("hitboxes.json");
for (int i = 0; i < args.length - 1; i++) {
String flag = args[i];
@@ -57,6 +58,8 @@ public final class ServerMain {
accounts = Path.of(value);
} else if ("--world".equals(flag)) {
worldDir = Path.of(value);
+ } else if ("--hitboxes".equals(flag)) {
+ hitboxes = Path.of(value);
} else if ("--seed".equals(flag)) {
seed = Long.parseLong(value);
} else if ("--launcher-api".equals(flag)) {
@@ -78,6 +81,10 @@ public final class ServerMain {
World world = new World(seed, new ChunkStorage(worldDir));
Server server = new Server(name, motd, capacity, port, world);
+ server.hitboxes(new net.rustnotmc.server.hitbox.HitboxRegistry(hitboxes));
+ if (!server.hitboxes().lastError().isEmpty()) {
+ System.out.println("[rsx] hitboxes: " + server.hitboxes().lastError());
+ }
server.gameMode("creative".equalsIgnoreCase(gameMode)
? net.rustnotmc.protocol.Packets.GAMEMODE_CREATIVE
: net.rustnotmc.protocol.Packets.GAMEMODE_SURVIVAL);
diff --git a/rsx-server-core/src/main/java/net/rustnotmc/server/hitbox/HitboxRegistry.java b/rsx-server-core/src/main/java/net/rustnotmc/server/hitbox/HitboxRegistry.java
new file mode 100644
index 0000000..0451d68
--- /dev/null
+++ b/rsx-server-core/src/main/java/net/rustnotmc/server/hitbox/HitboxRegistry.java
@@ -0,0 +1,259 @@
+package net.rustnotmc.server.hitbox;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import net.rustnotmc.protocol.Hitbox;
+import net.rustnotmc.protocol.Packets;
+import net.rustnotmc.server.util.Json;
+
+/**
+ * The boxes of every model the server knows.
+ *
+ * The boxes are in a file, because they are the editor: a model is fitted by
+ * changing a number in the file, and the result can be seen right away in the
+ * game. The server reads the file, hands the numbers to the client, and the
+ * client draws them.
+ *
+ * The file looks like this:
+ *
+ *