Merge branch 'Minestom:master' into instance-based-threading

This commit is contained in:
Leon 2025-01-15 18:52:51 +00:00 committed by GitHub
commit 4f00d7a2cd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 281 additions and 112 deletions

View File

@ -46,6 +46,7 @@ import net.minestom.server.timer.Schedulable;
import net.minestom.server.timer.Scheduler; import net.minestom.server.timer.Scheduler;
import net.minestom.server.timer.TaskSchedule; import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.ArrayUtils; import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.PacketViewableUtils; import net.minestom.server.utils.PacketViewableUtils;
import net.minestom.server.utils.async.AsyncUtils; import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.block.BlockIterator; import net.minestom.server.utils.block.BlockIterator;
@ -79,6 +80,10 @@ import java.util.function.UnaryOperator;
*/ */
public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, EventHandler<EntityEvent>, Taggable, public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, EventHandler<EntityEvent>, Taggable,
HoverEventSource<ShowEntity>, Sound.Emitter, Shape, AcquirableSource<Entity> { HoverEventSource<ShowEntity>, Sound.Emitter, Shape, AcquirableSource<Entity> {
// This is somewhat arbitrary, but we don't want to hit the max int ever because it is very easy to
// overflow while working with a position at the max int (for example, looping over a bounding box)
private static final int MAX_COORDINATE = 2_000_000_000;
private static final AtomicInteger LAST_ENTITY_ID = new AtomicInteger(); private static final AtomicInteger LAST_ENTITY_ID = new AtomicInteger();
// Certain entities should only have their position packets sent during synchronization // Certain entities should only have their position packets sent during synchronization
@ -96,7 +101,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
protected Instance instance; protected Instance instance;
protected Chunk currentChunk; protected Chunk currentChunk;
protected Pos position; protected Pos position; // Should be updated by setPositionInternal only.
protected Pos previousPosition; protected Pos previousPosition;
protected Pos lastSyncedPosition; protected Pos lastSyncedPosition;
protected boolean onGround; protected boolean onGround;
@ -202,6 +207,19 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
this(entityType, UUID.randomUUID()); this(entityType, UUID.randomUUID());
} }
protected void setPositionInternal(@NotNull Pos newPosition) {
if (newPosition.x() >= MAX_COORDINATE || newPosition.x() <= -MAX_COORDINATE ||
newPosition.y() >= MAX_COORDINATE || newPosition.y() <= -MAX_COORDINATE ||
newPosition.z() >= MAX_COORDINATE || newPosition.z() <= -MAX_COORDINATE) {
newPosition = newPosition.withCoord(
MathUtils.clamp(newPosition.x(), -MAX_COORDINATE, MAX_COORDINATE),
MathUtils.clamp(newPosition.y(), -MAX_COORDINATE, MAX_COORDINATE),
MathUtils.clamp(newPosition.z(), -MAX_COORDINATE, MAX_COORDINATE)
);
}
this.position = newPosition;
}
/** /**
* Schedules a task to be run during the next entity tick. * Schedules a task to be run during the next entity tick.
* *
@ -325,7 +343,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
final Runnable endCallback = () -> { final Runnable endCallback = () -> {
this.previousPosition = this.position; this.previousPosition = this.position;
this.position = globalPosition; setPositionInternal(globalPosition);
this.velocity = globalVelocity; this.velocity = globalVelocity;
refreshCoordinate(globalPosition); refreshCoordinate(globalPosition);
if (this instanceof Player player) player.synchronizePositionAfterTeleport(position, velocity, flags, shouldConfirm); if (this instanceof Player player) player.synchronizePositionAfterTeleport(position, velocity, flags, shouldConfirm);
@ -356,7 +374,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
public void setView(float yaw, float pitch) { public void setView(float yaw, float pitch) {
final Pos currentPosition = this.position; final Pos currentPosition = this.position;
if (currentPosition.sameView(yaw, pitch)) return; if (currentPosition.sameView(yaw, pitch)) return;
this.position = currentPosition.withView(yaw, pitch); setPositionInternal(currentPosition.withView(yaw, pitch));
synchronizeView(); synchronizeView();
} }
@ -785,7 +803,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
if (previousInstance != null) removeFromInstance(previousInstance); if (previousInstance != null) removeFromInstance(previousInstance);
this.isActive = true; this.isActive = true;
this.position = spawnPosition; setPositionInternal(spawnPosition);
this.previousPosition = spawnPosition; this.previousPosition = spawnPosition;
this.lastSyncedPosition = spawnPosition; this.lastSyncedPosition = spawnPosition;
this.previousPhysicsResult = null; this.previousPhysicsResult = null;
@ -1239,7 +1257,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
final var previousPosition = this.position; final var previousPosition = this.position;
final Pos position = ignoreView ? previousPosition.withCoord(newPosition) : newPosition; final Pos position = ignoreView ? previousPosition.withCoord(newPosition) : newPosition;
if (position.equals(lastSyncedPosition)) return; if (position.equals(lastSyncedPosition)) return;
this.position = position; setPositionInternal(position);
this.previousPosition = previousPosition; this.previousPosition = previousPosition;
if (!position.samePoint(previousPosition)) refreshCoordinate(position); if (!position.samePoint(previousPosition)) refreshCoordinate(position);
if (nextSynchronizationTick <= ticks + 1 || !sendPackets) { if (nextSynchronizationTick <= ticks + 1 || !sendPackets) {
@ -1300,7 +1318,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
final Pos newPassengerPos = oldPassengerPos.withCoord(newPosition.x(), final Pos newPassengerPos = oldPassengerPos.withCoord(newPosition.x(),
newPosition.y() + EntityUtils.getPassengerHeightOffset(this, passenger), newPosition.y() + EntityUtils.getPassengerHeightOffset(this, passenger),
newPosition.z()); newPosition.z());
passenger.position = newPassengerPos; passenger.setPositionInternal(newPassengerPos);
passenger.previousPosition = oldPassengerPos; passenger.previousPosition = oldPassengerPos;
passenger.refreshCoordinate(newPassengerPos); passenger.refreshCoordinate(newPassengerPos);
} }
@ -1477,7 +1495,7 @@ public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, Ev
this.removed = true; this.removed = true;
if (!permanent) { if (!permanent) {
// Reset some state to be ready for re-use // Reset some state to be ready for re-use
this.position = Pos.ZERO; setPositionInternal(Pos.ZERO);
this.previousPosition = Pos.ZERO; this.previousPosition = Pos.ZERO;
this.lastSyncedPosition = Pos.ZERO; this.lastSyncedPosition = Pos.ZERO;
} }

View File

@ -115,6 +115,7 @@ public interface Palette {
if (bitsPerEntry == 0) { if (bitsPerEntry == 0) {
// Single valued 0-0 // Single valued 0-0
final int value = buffer.read(VAR_INT); final int value = buffer.read(VAR_INT);
buffer.read(VAR_INT); // Skip size
return new PaletteSingle((byte) dimension, value); return new PaletteSingle((byte) dimension, value);
} else if (bitsPerEntry >= minIndirect && bitsPerEntry <= maxIndirect) { } else if (bitsPerEntry >= minIndirect && bitsPerEntry <= maxIndirect) {
// Indirect palette // Indirect palette

View File

@ -45,33 +45,33 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
private void writeInnerComponent(@NotNull NetworkBuffer buffer, @NotNull Component component) { private void writeInnerComponent(@NotNull NetworkBuffer buffer, @NotNull Component component) {
buffer.write(BYTE, TAG_STRING); // Start first tag (always the type) buffer.write(BYTE, TAG_STRING); // Start first tag (always the type)
writeUtf(buffer, "type"); buffer.write(STRING_IO_UTF8, "type");
switch (component) { switch (component) {
case TextComponent text -> { case TextComponent text -> {
writeUtf(buffer, "text"); buffer.write(STRING_IO_UTF8, "text");
buffer.write(BYTE, TAG_STRING); // Start "text" tag buffer.write(BYTE, TAG_STRING); // Start "text" tag
writeUtf(buffer, "text"); buffer.write(STRING_IO_UTF8, "text");
writeUtf(buffer, text.content()); buffer.write(STRING_IO_UTF8, text.content());
} }
case TranslatableComponent translatable -> { case TranslatableComponent translatable -> {
writeUtf(buffer, "translatable"); buffer.write(STRING_IO_UTF8, "translatable");
buffer.write(BYTE, TAG_STRING); // Start "translate" tag buffer.write(BYTE, TAG_STRING); // Start "translate" tag
writeUtf(buffer, "translate"); buffer.write(STRING_IO_UTF8, "translate");
writeUtf(buffer, translatable.key()); buffer.write(STRING_IO_UTF8, translatable.key());
final String fallback = translatable.fallback(); final String fallback = translatable.fallback();
if (fallback != null) { if (fallback != null) {
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "fallback"); buffer.write(STRING_IO_UTF8, "fallback");
writeUtf(buffer, fallback); buffer.write(STRING_IO_UTF8, fallback);
} }
final List<TranslationArgument> args = translatable.arguments(); final List<TranslationArgument> args = translatable.arguments();
if (!args.isEmpty()) { if (!args.isEmpty()) {
buffer.write(BYTE, TAG_LIST); buffer.write(BYTE, TAG_LIST);
writeUtf(buffer, "with"); buffer.write(STRING_IO_UTF8, "with");
buffer.write(BYTE, TAG_COMPOUND); // List type buffer.write(BYTE, TAG_COMPOUND); // List type
buffer.write(INT, args.size()); buffer.write(INT, args.size());
for (final TranslationArgument arg : args) for (final TranslationArgument arg : args)
@ -79,42 +79,42 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
} }
} }
case ScoreComponent score -> { case ScoreComponent score -> {
writeUtf(buffer, "score"); buffer.write(STRING_IO_UTF8, "score");
buffer.write(BYTE, TAG_COMPOUND); // Start "score" tag buffer.write(BYTE, TAG_COMPOUND); // Start "score" tag
writeUtf(buffer, "score"); buffer.write(STRING_IO_UTF8, "score");
{ {
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "name"); buffer.write(STRING_IO_UTF8, "name");
writeUtf(buffer, score.name()); buffer.write(STRING_IO_UTF8, score.name());
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "objective"); buffer.write(STRING_IO_UTF8, "objective");
writeUtf(buffer, score.objective()); buffer.write(STRING_IO_UTF8, score.objective());
} }
buffer.write(BYTE, TAG_END); // End "score" tag buffer.write(BYTE, TAG_END); // End "score" tag
} }
case SelectorComponent selector -> { case SelectorComponent selector -> {
writeUtf(buffer, "selector"); buffer.write(STRING_IO_UTF8, "selector");
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "selector"); buffer.write(STRING_IO_UTF8, "selector");
writeUtf(buffer, selector.pattern()); buffer.write(STRING_IO_UTF8, selector.pattern());
final Component separator = selector.separator(); final Component separator = selector.separator();
if (separator != null) { if (separator != null) {
buffer.write(BYTE, TAG_COMPOUND); buffer.write(BYTE, TAG_COMPOUND);
writeUtf(buffer, "separator"); buffer.write(STRING_IO_UTF8, "separator");
writeInnerComponent(buffer, separator); writeInnerComponent(buffer, separator);
} }
} }
case KeybindComponent keybind -> { case KeybindComponent keybind -> {
writeUtf(buffer, "keybind"); buffer.write(STRING_IO_UTF8, "keybind");
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "keybind"); buffer.write(STRING_IO_UTF8, "keybind");
writeUtf(buffer, keybind.keybind()); buffer.write(STRING_IO_UTF8, keybind.keybind());
} }
case NBTComponent<?, ?> nbt -> { case NBTComponent<?, ?> nbt -> {
//todo //todo
@ -126,7 +126,7 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
// Children // Children
if (!component.children().isEmpty()) { if (!component.children().isEmpty()) {
buffer.write(BYTE, TAG_LIST); buffer.write(BYTE, TAG_LIST);
writeUtf(buffer, "extra"); buffer.write(STRING_IO_UTF8, "extra");
buffer.write(BYTE, TAG_COMPOUND); // List type buffer.write(BYTE, TAG_COMPOUND); // List type
buffer.write(INT, component.children().size()); buffer.write(INT, component.children().size());
@ -144,59 +144,59 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
final TextColor color = style.color(); final TextColor color = style.color();
if (color != null) { if (color != null) {
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "color"); buffer.write(STRING_IO_UTF8, "color");
if (color instanceof NamedTextColor namedColor) if (color instanceof NamedTextColor namedColor)
writeUtf(buffer, namedColor.toString()); buffer.write(STRING_IO_UTF8, namedColor.toString());
else writeUtf(buffer, color.asHexString()); else buffer.write(STRING_IO_UTF8, color.asHexString());
} }
final Key font = style.font(); final Key font = style.font();
if (font != null) { if (font != null) {
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "font"); buffer.write(STRING_IO_UTF8, "font");
writeUtf(buffer, font.asString()); buffer.write(STRING_IO_UTF8, font.asString());
} }
final TextDecoration.State bold = style.decoration(TextDecoration.BOLD); final TextDecoration.State bold = style.decoration(TextDecoration.BOLD);
if (bold != TextDecoration.State.NOT_SET) { if (bold != TextDecoration.State.NOT_SET) {
buffer.write(BYTE, TAG_BYTE); buffer.write(BYTE, TAG_BYTE);
writeUtf(buffer, "bold"); buffer.write(STRING_IO_UTF8, "bold");
buffer.write(BYTE, bold == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0); buffer.write(BYTE, bold == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0);
} }
final TextDecoration.State italic = style.decoration(TextDecoration.ITALIC); final TextDecoration.State italic = style.decoration(TextDecoration.ITALIC);
if (italic != TextDecoration.State.NOT_SET) { if (italic != TextDecoration.State.NOT_SET) {
buffer.write(BYTE, TAG_BYTE); buffer.write(BYTE, TAG_BYTE);
writeUtf(buffer, "italic"); buffer.write(STRING_IO_UTF8, "italic");
buffer.write(BYTE, italic == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0); buffer.write(BYTE, italic == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0);
} }
final TextDecoration.State underlined = style.decoration(TextDecoration.UNDERLINED); final TextDecoration.State underlined = style.decoration(TextDecoration.UNDERLINED);
if (underlined != TextDecoration.State.NOT_SET) { if (underlined != TextDecoration.State.NOT_SET) {
buffer.write(BYTE, TAG_BYTE); buffer.write(BYTE, TAG_BYTE);
writeUtf(buffer, "underlined"); buffer.write(STRING_IO_UTF8, "underlined");
buffer.write(BYTE, underlined == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0); buffer.write(BYTE, underlined == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0);
} }
final TextDecoration.State strikethrough = style.decoration(TextDecoration.STRIKETHROUGH); final TextDecoration.State strikethrough = style.decoration(TextDecoration.STRIKETHROUGH);
if (strikethrough != TextDecoration.State.NOT_SET) { if (strikethrough != TextDecoration.State.NOT_SET) {
buffer.write(BYTE, TAG_BYTE); buffer.write(BYTE, TAG_BYTE);
writeUtf(buffer, "strikethrough"); buffer.write(STRING_IO_UTF8, "strikethrough");
buffer.write(BYTE, strikethrough == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0); buffer.write(BYTE, strikethrough == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0);
} }
final TextDecoration.State obfuscated = style.decoration(TextDecoration.OBFUSCATED); final TextDecoration.State obfuscated = style.decoration(TextDecoration.OBFUSCATED);
if (obfuscated != TextDecoration.State.NOT_SET) { if (obfuscated != TextDecoration.State.NOT_SET) {
buffer.write(BYTE, TAG_BYTE); buffer.write(BYTE, TAG_BYTE);
writeUtf(buffer, "obfuscated"); buffer.write(STRING_IO_UTF8, "obfuscated");
buffer.write(BYTE, obfuscated == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0); buffer.write(BYTE, obfuscated == TextDecoration.State.TRUE ? (byte) 1 : (byte) 0);
} }
final String insertion = style.insertion(); final String insertion = style.insertion();
if (insertion != null) { if (insertion != null) {
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "insertion"); buffer.write(STRING_IO_UTF8, "insertion");
writeUtf(buffer, insertion); buffer.write(STRING_IO_UTF8, insertion);
} }
final ClickEvent clickEvent = style.clickEvent(); final ClickEvent clickEvent = style.clickEvent();
@ -208,15 +208,15 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
private void writeClickEvent(@NotNull NetworkBuffer buffer, @NotNull ClickEvent clickEvent) { private void writeClickEvent(@NotNull NetworkBuffer buffer, @NotNull ClickEvent clickEvent) {
buffer.write(BYTE, TAG_COMPOUND); buffer.write(BYTE, TAG_COMPOUND);
writeUtf(buffer, "clickEvent"); buffer.write(STRING_IO_UTF8, "clickEvent");
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "action"); buffer.write(STRING_IO_UTF8, "action");
writeUtf(buffer, clickEvent.action().name().toLowerCase(Locale.ROOT)); buffer.write(STRING_IO_UTF8, clickEvent.action().name().toLowerCase(Locale.ROOT));
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "value"); buffer.write(STRING_IO_UTF8, "value");
writeUtf(buffer, clickEvent.value()); buffer.write(STRING_IO_UTF8, clickEvent.value());
buffer.write(BYTE, TAG_END); buffer.write(BYTE, TAG_END);
} }
@ -224,29 +224,29 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private void writeHoverEvent(@NotNull NetworkBuffer buffer, @NotNull HoverEvent<?> hoverEvent) { private void writeHoverEvent(@NotNull NetworkBuffer buffer, @NotNull HoverEvent<?> hoverEvent) {
buffer.write(BYTE, TAG_COMPOUND); buffer.write(BYTE, TAG_COMPOUND);
writeUtf(buffer, "hoverEvent"); buffer.write(STRING_IO_UTF8, "hoverEvent");
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "action"); buffer.write(STRING_IO_UTF8, "action");
writeUtf(buffer, hoverEvent.action().toString().toLowerCase(Locale.ROOT)); buffer.write(STRING_IO_UTF8, hoverEvent.action().toString().toLowerCase(Locale.ROOT));
buffer.write(BYTE, TAG_COMPOUND); // Start contents tag buffer.write(BYTE, TAG_COMPOUND); // Start contents tag
writeUtf(buffer, "contents"); buffer.write(STRING_IO_UTF8, "contents");
if (hoverEvent.action() == HoverEvent.Action.SHOW_TEXT) { if (hoverEvent.action() == HoverEvent.Action.SHOW_TEXT) {
writeInnerComponent(buffer, (Component) hoverEvent.value()); writeInnerComponent(buffer, (Component) hoverEvent.value());
} else if (hoverEvent.action() == HoverEvent.Action.SHOW_ITEM) { } else if (hoverEvent.action() == HoverEvent.Action.SHOW_ITEM) {
var value = ((HoverEvent<HoverEvent.ShowItem>) hoverEvent).value(); var value = ((HoverEvent<HoverEvent.ShowItem>) hoverEvent).value();
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "id"); buffer.write(STRING_IO_UTF8, "id");
writeUtf(buffer, value.item().asString()); buffer.write(STRING_IO_UTF8, value.item().asString());
buffer.write(BYTE, TAG_INT); buffer.write(BYTE, TAG_INT);
writeUtf(buffer, "count"); buffer.write(STRING_IO_UTF8, "count");
buffer.write(INT, value.count()); buffer.write(INT, value.count());
buffer.write(BYTE, TAG_COMPOUND); buffer.write(BYTE, TAG_COMPOUND);
writeUtf(buffer, "components"); buffer.write(STRING_IO_UTF8, "components");
//todo item components //todo item components
buffer.write(BYTE, TAG_END); buffer.write(BYTE, TAG_END);
@ -257,17 +257,17 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
final Component name = value.name(); final Component name = value.name();
if (name != null) { if (name != null) {
buffer.write(BYTE, TAG_COMPOUND); buffer.write(BYTE, TAG_COMPOUND);
writeUtf(buffer, "name"); buffer.write(STRING_IO_UTF8, "name");
writeInnerComponent(buffer, name); writeInnerComponent(buffer, name);
} }
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "type"); buffer.write(STRING_IO_UTF8, "type");
writeUtf(buffer, value.type().asString()); buffer.write(STRING_IO_UTF8, value.type().asString());
buffer.write(BYTE, TAG_STRING); buffer.write(BYTE, TAG_STRING);
writeUtf(buffer, "id"); buffer.write(STRING_IO_UTF8, "id");
writeUtf(buffer, value.id().toString()); buffer.write(STRING_IO_UTF8, value.id().toString());
buffer.write(BYTE, TAG_END); // End contents tag buffer.write(BYTE, TAG_END); // End contents tag
} else { } else {
@ -276,53 +276,4 @@ record ComponentNetworkBufferTypeImpl() implements NetworkBufferTypeImpl<Compone
buffer.write(BYTE, TAG_END); buffer.write(BYTE, TAG_END);
} }
/**
* This is a very gross version of {@link java.io.DataOutputStream#writeUTF(String)}. We need the data in the java
* modified utf-8 format, and I couldnt find a method without creating a new buffer for it.
*
* @param buffer the buffer to write to
* @param str the string to write
*/
private static void writeUtf(@NotNull NetworkBuffer buffer, @NotNull String str) {
final int strlen = str.length();
int utflen = strlen; // optimized for ASCII
for (int i = 0; i < strlen; i++) {
int c = str.charAt(i);
if (c >= 0x80 || c == 0)
utflen += (c >= 0x800) ? 2 : 1;
}
if (utflen > 65535 || /* overflow */ utflen < strlen)
throw new RuntimeException("UTF-8 string too long");
buffer.write(SHORT, (short) utflen);
buffer.ensureWritable(utflen);
var impl = (NetworkBufferImpl) buffer;
int i;
for (i = 0; i < strlen; i++) { // optimized for initial run of ASCII
int c = str.charAt(i);
if (c >= 0x80 || c == 0) break;
impl._putByte(buffer.writeIndex(), (byte) c);
impl.advanceWrite(1);
}
for (; i < strlen; i++) {
int c = str.charAt(i);
if (c < 0x80 && c != 0) {
impl._putByte(buffer.writeIndex(), (byte) c);
impl.advanceWrite(1);
} else if (c >= 0x800) {
impl._putByte(buffer.writeIndex(), (byte) (0xE0 | ((c >> 12) & 0x0F)));
impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 6) & 0x3F)));
impl._putByte(buffer.writeIndex() + 2, (byte) (0x80 | ((c >> 0) & 0x3F)));
impl.advanceWrite(3);
} else {
impl._putByte(buffer.writeIndex(), (byte) (0xC0 | ((c >> 6) & 0x1F)));
impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 0) & 0x3F)));
impl.advanceWrite(2);
}
}
}
} }

View File

@ -45,6 +45,7 @@ public sealed interface NetworkBuffer permits NetworkBufferImpl {
Type<byte[]> RAW_BYTES = new NetworkBufferTypeImpl.RawBytesType(-1); Type<byte[]> RAW_BYTES = new NetworkBufferTypeImpl.RawBytesType(-1);
Type<String> STRING = new NetworkBufferTypeImpl.StringType(); Type<String> STRING = new NetworkBufferTypeImpl.StringType();
Type<String> STRING_TERMINATED = new NetworkBufferTypeImpl.StringTerminatedType(); Type<String> STRING_TERMINATED = new NetworkBufferTypeImpl.StringTerminatedType();
Type<String> STRING_IO_UTF8 = new NetworkBufferTypeImpl.IOUTF8StringType();
Type<BinaryTag> NBT = new NetworkBufferTypeImpl.NbtType(); Type<BinaryTag> NBT = new NetworkBufferTypeImpl.NbtType();
@SuppressWarnings({"unchecked", "rawtypes"}) @SuppressWarnings({"unchecked", "rawtypes"})
Type<CompoundBinaryTag> NBT_COMPOUND = (Type) new NetworkBufferTypeImpl.NbtType(); Type<CompoundBinaryTag> NBT_COMPOUND = (Type) new NetworkBufferTypeImpl.NbtType();

View File

@ -819,6 +819,123 @@ interface NetworkBufferTypeImpl<T> extends NetworkBuffer.Type<T> {
} }
} }
/**
* This is a very gross version of {@link java.io.DataOutputStream#writeUTF(String)} & ${@link DataInputStream#readUTF()}. We need the data in the java
* modified utf-8 format for Component, and I couldnt find a method without creating a new buffer for it.
*/
record IOUTF8StringType() implements NetworkBufferTypeImpl<String> {
@Override
public void write(@NotNull NetworkBuffer buffer, String value) {
final int strlen = value.length();
int utflen = strlen; // optimized for ASCII
for (int i = 0; i < strlen; i++) {
int c = value.charAt(i);
if (c >= 0x80 || c == 0)
utflen += (c >= 0x800) ? 2 : 1;
}
if (utflen > 65535 || /* overflow */ utflen < strlen)
throw new RuntimeException("UTF-8 string too long");
buffer.write(SHORT, (short) utflen);
buffer.ensureWritable(utflen);
var impl = (NetworkBufferImpl) buffer;
int i;
for (i = 0; i < strlen; i++) { // optimized for initial run of ASCII
int c = value.charAt(i);
if (c >= 0x80 || c == 0) break;
impl._putByte(buffer.writeIndex(), (byte) c);
impl.advanceWrite(1);
}
for (; i < strlen; i++) {
int c = value.charAt(i);
if (c < 0x80 && c != 0) {
impl._putByte(buffer.writeIndex(), (byte) c);
impl.advanceWrite(1);
} else if (c >= 0x800) {
impl._putByte(buffer.writeIndex(), (byte) (0xE0 | ((c >> 12) & 0x0F)));
impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 6) & 0x3F)));
impl._putByte(buffer.writeIndex() + 2, (byte) (0x80 | ((c >> 0) & 0x3F)));
impl.advanceWrite(3);
} else {
impl._putByte(buffer.writeIndex(), (byte) (0xC0 | ((c >> 6) & 0x1F)));
impl._putByte(buffer.writeIndex() + 1, (byte) (0x80 | ((c >> 0) & 0x3F)));
impl.advanceWrite(2);
}
}
}
@Override
public String read(@NotNull NetworkBuffer buffer) {
int utflen = buffer.read(UNSIGNED_SHORT);
if (buffer.readableBytes() < utflen) throw new IllegalArgumentException("Invalid String size.");
byte[] bytearr = buffer.read(RAW_BYTES);
final char[] chararr = new char[utflen];
int c, char2, char3;
int count = 0;
int chararr_count = 0;
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
if (c > 127) break;
count++;
chararr[chararr_count++] = (char) c;
}
while (count < utflen) {
c = (int) bytearr[count] & 0xff;
try { // Surround in try catch to throw a runtime exception instead of a checked one
switch (c >> 4) {
case 0, 1, 2, 3, 4, 5, 6, 7 -> {
/* 0xxxxxxx*/
count++;
chararr[chararr_count++] = (char) c;
}
case 12, 13 -> {
/* 110x xxxx 10xx xxxx*/
count += 2;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = bytearr[count - 1];
if ((char2 & 0xC0) != 0x80)
throw new UTFDataFormatException(
"malformed input around byte " + count);
chararr[chararr_count++] = (char) (((c & 0x1F) << 6) |
(char2 & 0x3F));
}
case 14 -> {
/* 1110 xxxx 10xx xxxx 10xx xxxx */
count += 3;
if (count > utflen)
throw new UTFDataFormatException(
"malformed input: partial character at end");
char2 = bytearr[count - 2];
char3 = bytearr[count - 1];
if (((char2 & 0xC0) != 0x80) || ((char3 & 0xC0) != 0x80))
throw new UTFDataFormatException(
"malformed input around byte " + (count - 1));
chararr[chararr_count++] = (char) (((c & 0x0F) << 12) |
((char2 & 0x3F) << 6) |
((char3 & 0x3F) << 0));
}
default ->
/* 10xx xxxx, 1111 xxxx */
throw new UTFDataFormatException(
"malformed input around byte " + count);
}
} catch (UTFDataFormatException e) {
throw new IllegalArgumentException(e);
}
}
// The number of chars produced may be less than utflen
return new String(chararr, 0, chararr_count);
}
}
static <T> long sizeOf(Type<T> type, T value, Registries registries) { static <T> long sizeOf(Type<T> type, T value, Registries registries) {
NetworkBuffer buffer = NetworkBufferImpl.dummy(registries); NetworkBuffer buffer = NetworkBufferImpl.dummy(registries);
type.write(buffer, value); type.write(buffer, value);

View File

@ -202,6 +202,7 @@ public final class PacketWriting {
if (written < minWrite) { if (written < minWrite) {
// Try again with a bigger buffer // Try again with a bigger buffer
final long newSize = Math.min(buffer.capacity() * 2, ServerFlag.MAX_PACKET_SIZE); final long newSize = Math.min(buffer.capacity() * 2, ServerFlag.MAX_PACKET_SIZE);
if (newSize == buffer.capacity()) break; // We reached the maximum size
buffer.resize(newSize); buffer.resize(newSize);
} else { } else {
// At least one packet has been written // At least one packet has been written

View File

@ -141,9 +141,9 @@ public abstract class PlayerConnection {
*/ */
public void disconnect() { public void disconnect() {
this.online = false; this.online = false;
MinecraftServer.getConnectionManager().removePlayer(this); final Player player = MinecraftServer.getConnectionManager().getPlayer(this);
final Player player = getPlayer();
if (player != null) { if (player != null) {
MinecraftServer.getConnectionManager().removePlayer(this);
if (connectionState == ConnectionState.PLAY && !player.isRemoved()) if (connectionState == ConnectionState.PLAY && !player.isRemoved())
player.scheduleNextTick(Entity::remove); player.scheduleNextTick(Entity::remove);
else { else {

View File

@ -8,6 +8,11 @@ import net.minestom.testing.Env;
import net.minestom.testing.EnvTest; import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
@EnvTest @EnvTest
@ -99,4 +104,21 @@ public class EntityTeleportIntegrationTest {
player.teleport(new Pos(5, 10, 2, 5, 5), null, RelativeFlags.VIEW).join(); player.teleport(new Pos(5, 10, 2, 5, 5), null, RelativeFlags.VIEW).join();
assertEquals(player.getPosition(), new Pos(5, 10, 2, 95, 5)); assertEquals(player.getPosition(), new Pos(5, 10, 2, 95, 5));
} }
@Test
public void entityTeleportToInfinity(Env env) throws ExecutionException, InterruptedException, TimeoutException {
var instance = env.createFlatInstance();
var entity = new Entity(EntityTypes.ZOMBIE);
entity.setInstance(instance, new Pos(0, 42, 0)).join();
assertEquals(instance, entity.getInstance());
assertEquals(new Pos(0, 42, 0), entity.getPosition());
entity.teleport(new Pos(Double.POSITIVE_INFINITY, 42, 52)).join();
CompletableFuture.runAsync(() -> entity.tick(System.currentTimeMillis()))
.get(10, TimeUnit.SECONDS);
// This should not hang forever
// The position should have been capped at 2 billion.
assertEquals(new Pos(2_000_000_000, 42, 52), entity.getPosition());
}
} }

View File

@ -11,6 +11,8 @@ import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability; import org.jetbrains.annotations.UnknownNullability;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.io.UTFDataFormatException;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.*; import java.util.*;
import java.util.function.Consumer; import java.util.function.Consumer;
@ -474,6 +476,62 @@ public class NetworkBufferTest {
assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING)); // oom assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING)); // oom
} }
@Test
public void oomStringUtf8Regression() {
var buffer = NetworkBuffer.resizableBuffer(100);
buffer.write(UNSIGNED_SHORT, 65535); // String length
buffer.write(RAW_BYTES, "Hello".getBytes(StandardCharsets.UTF_8)); // String data
assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING)); // oom
}
@Test
public void testStringUtf8ModifiedWrite() throws IOException {
var stream = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream out = new java.io.DataOutputStream(stream);
out.writeUTF("Hello");
assertBufferType(STRING_IO_UTF8, "Hello", stream.toByteArray());
}
@Test
public void testStringUtf8ModifiedRead() throws IOException {
var stream = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream out = new java.io.DataOutputStream(stream);
out.writeUTF("Hello");
var buffer = NetworkBuffer.wrap(stream.toByteArray(), 0, stream.size());
assertEquals("Hello", buffer.read(STRING_IO_UTF8));
}
@Test
public void oomStringUtf8ModfiedRegression() throws IOException {
var buffer = NetworkBuffer.resizableBuffer(100);
buffer.write(UNSIGNED_SHORT, 65535); // String length
// Write the raw bytes that are invalid
buffer.write(RAW_BYTES, new byte[]{(byte) 0xC0, (byte) 0x80}); // Invalid UTF-8
assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING_IO_UTF8)); // oom
buffer.clear();
var stream = new java.io.ByteArrayOutputStream();
java.io.DataOutputStream out = new java.io.DataOutputStream(stream);
out.writeUTF("Hello");
var byteArray = stream.toByteArray();
// Mess with the length to 0
byteArray[0] = (byte) 0x00;
byteArray[1] = (byte) 0x00;
assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING_IO_UTF8)); // oom
buffer.clear();
buffer.write(UNSIGNED_SHORT, 5);
buffer.write(RAW_BYTES, new byte[]{(byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF, (byte) 0xFF}); // Invalid utf8
assertThrows(IllegalArgumentException.class, () -> buffer.read(STRING_IO_UTF8)); // oom
}
static <T> void assertBufferType(NetworkBuffer.@NotNull Type<T> type, @UnknownNullability T value, byte[] expected, @NotNull Action<T> action) { static <T> void assertBufferType(NetworkBuffer.@NotNull Type<T> type, @UnknownNullability T value, byte[] expected, @NotNull Action<T> action) {
var buffer = NetworkBuffer.resizableBuffer(MinecraftServer.process()); var buffer = NetworkBuffer.resizableBuffer(MinecraftServer.process());
action.write(buffer, type, value); action.write(buffer, type, value);