Merge branch 'master' into fix-movement-simulation

This commit is contained in:
hapily 2025-01-28 00:45:45 -08:00 committed by GitHub
commit 6211acf9d3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
75 changed files with 1444 additions and 502 deletions

61
.github/ISSUE_TEMPLATE/bug.yml vendored Normal file
View File

@ -0,0 +1,61 @@
name: Report a bug
description: You found a bug? Let us know!
title: "[Bug]: "
labels: [ "bug", "needs triage" ]
projects: ["minestom/6"]
body:
- type: markdown
attributes:
value: |
Thank you for reporting a bug to Minestom!
Please fill out the information below to help us understand the issue.
- type: markdown
attributes:
value: |
Before filling in the form fields, please consider the following:
- Ensure that you are using the latest version of Minestom.
- Search for existing issues in the [issue tracker](https://github.com/Minestom/Minestom/issues)
- type: textarea
attributes:
label: Affected Version
description: |
The unique version of the used dependency.
It can be found out via the Git class or in the dependency.
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: |
A clear and concise description of what the bug is.
If you have a screenshot of the bug, please attach it below.
validations:
required: true
- type: textarea
attributes:
label: Steps to reproduce the bug
description: Tell us exactly how to reproduce the bug you are experiencing
placeholder: |
1. ...
2. ...
3. ...
validations:
required: false
- type: textarea
attributes:
label: Code sample
description: |
Please create a reproducible sample to show us the bug in action and attach it below between the lines with the backticks.
This helps us to verify that the bug is valid and prevents us from having to ask you for a sample later.
Without this we will be unlikely to be able to progress on the issue, so
we'll regretfully have to close it.
**Note**: Please do not upload screenshots of text. Instead, use code blocks
or the above mentioned ways to upload your code sample.
value: |
```java
[Paste your code here]
```
validations:
required: false

6
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,6 @@
blank_issues_enabled: false
contact_links:
- name: Minestom Discord
url: https://discord.gg/minestom/
about: Please join our Discord server if you have any questions or concerns.
icon: https://github.com/simple-icons/simple-icons/blob/develop/icons/discord.svg

34
.github/ISSUE_TEMPLATE/feature.yml vendored Normal file
View File

@ -0,0 +1,34 @@
name: Feature Request
description: Suggest an idea for this project
title: "[Feature]: "
labels: [ "enhancement" ]
projects: ["minestom/6"]
body:
- type: markdown
attributes:
value: |
Thank you for suggesting an idea to make Minestom better!
Please fill out the information below to help us understand your idea.
- type: textarea
attributes:
label: Is your feature request related to a problem?
description: Please give some context for this request. Why do you want it added?
validations:
required: true
- type: textarea
attributes:
label: Describe the solution you'd like
description: Give us a clear description of what you want
validations:
required: true
- type: textarea
attributes:
label: Describe alternatives you've considered
description: A clear and concise description of any alternative solutions or features you've considered.
validations:
required: false
- type: markdown
attributes:
value: |
Before submitting your feature request, please make sure you have done the following:
- [ ] Searched for existing feature requests

41
.github/ISSUE_TEMPLATE/performance.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Performance Problem
description: Report a performance problem which is related to Minestom
labels: [ "needs triage", "performance" ]
projects: ["minestom/6"]
body:
- type: markdown
attributes:
value: |
Before creating an issue regarding to the performance, please reach out for support on our [Discord](https://discord.gg/minestom/)
or in the [Discussions](https://github.com/Minestom/Minestom/discussions)!
**Please be aware: Performance issues can sometimes depend on your specific implementation and not on Minestom itself. If the situation is clear and it's not a problem with the project, we will close the issue without any comment.**
- type: input
attributes:
label: Used Minestom version
description: Which version of Minestom are you using?
placeholder: 1.0.0
validations:
required: true
- type: textarea
attributes:
label: Describe the performance problem
description: If applicable, please describe your issue.
validations:
required: false
- type: textarea
attributes:
label: Other
description: |
Please include other helpful links below.
The more information we receive, the quicker and more effective we can be at finding the solution to the issue.
validations:
required: false
- type: markdown
attributes:
value: |
Before submitting your issue, please make sure you have done the following:
1. You are running the latest version of Minestom from [Release page](https://github.com/Minestom/Minestom)
2. You searched for and ensured there isn't already an open issue regarding this
3. Your version of Minecraft is supported by Minestom

View File

@ -1,31 +0,0 @@
name: Check PR code style
on:
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 21
uses: actions/setup-java@v1
with:
java-version: 21
- name: Run java checkstyle
uses: nikitasavinov/checkstyle-action@0.3.1
with:
# Report level for reviewdog [info,warning,error]
level: info
# Reporter of reviewdog command [github-pr-check,github-pr-review]
reporter: github-pr-check
# Filtering for the reviewdog command [added,diff_context,file,nofilter].
filter_mode: added
# Exit code for reviewdog when errors are found [true,false].
fail_on_error: false
# Checkstyle config file
checkstyle_config: minestom_checks.xml
checkstyle_version: "8.42"

View File

@ -1,67 +0,0 @@
# For most projects, this workflow file will not need changing; you simply need
# to commit it to your repository.
#
# You may wish to alter this file to override the set of languages analyzed,
# or to provide custom queries or build logic.
#
# ******** NOTE ********
# We have attempted to detect the languages in your repository. Please check
# the `language` matrix defined below to confirm you have the correct set of
# supported CodeQL languages.
#
name: "CodeQL"
on:
push:
branches: [ master ]
pull_request:
# The branches below must be a subset of the branches above
branches: [ master ]
schedule:
- cron: '27 19 * * 3'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
language: [ 'java' ]
# CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
# Learn more:
# https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
steps:
- name: Checkout repository
uses: actions/checkout@v2
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@v1
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1

29
.github/pull_request_template.md vendored Normal file
View File

@ -0,0 +1,29 @@
## Proposed changes
Describe the big picture of your changes here to communicate to the maintainers why we should accept this pull request.
If it fixes a bug or resolves a feature request, be sure to link to that issue.
## Types of changes
What types of changes does your code introduce to this project?
_Put an `x` in the boxes that apply_
- [ ] Bugfix (non-breaking change which fixes an issue)
- [ ] New feature (non-breaking change which adds functionality)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
- [ ] Documentation Update (if none of the other choices apply)
## Checklist
_Put an `x` in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of
them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before
merging your code._
- [ ] I have read the [CONTRIBUTING.md](CONTRIBUTING.md)
- [ ] I have added tests that prove my fix is effective or that my feature works
- [ ] I have added necessary documentation (if appropriate)
## Further comments
If this is a relatively large or complex change, kick off the discussion by explaining why you chose the solution you
did and what alternatives you considered, etc...

43
.github/workflows/build-pr.yml vendored Normal file
View File

@ -0,0 +1,43 @@
name: Build PR
on: [pull_request]
jobs:
build_pr:
name: Build PR
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v3
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 21
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- name: Build on ${{ matrix.os }}
run: ./gradlew test
publish:
name: Publish PR
if: contains(github.event.pull_request.labels.*.name, 'Publish Pull Request') && github.repository_owner == 'Minestom'
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- id: vars
run: echo "short_commit_hash=${GITHUB_SHA::10}" >> $GITHUB_OUTPUT
- name: Publish Artifacts
env:
MINESTOM_VERSION: ${{ github.head_ref }}-${{ steps.vars.outputs.short_commit_hash }}
MINESTOM_CHANNEL: snapshot
run: |
./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY

41
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,41 @@
name: Build
on:
push:
branches:
- master
jobs:
Build:
name: Build and Publish
# Run on all label events (won't be duplicated) or all push events or on PR syncs not from the same repo
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v3
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
cache: gradle
java-version: 21
- name: Cache Gradle packages
uses: actions/cache@v3
with:
path: ~/.gradle/caches
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }}
restore-keys: ${{ runner.os }}-gradle
- id: vars
run: echo "short_commit_hash=${GITHUB_SHA::10}" >> $GITHUB_OUTPUT
- name: Publish to Central via Build
if: github.repository_owner == 'Minestom'
run: |
./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY
env:
MINESTOM_VERSION: ${{ steps.vars.outputs.short_commit_hash }}
MINESTOM_CHANNEL: release
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSWORD }}

View File

@ -7,6 +7,7 @@ on:
jobs: jobs:
run: run:
name: Close invalid PRs
if: | if: |
github.repository != github.event.pull_request.head.repo.full_name && github.repository != github.event.pull_request.head.repo.full_name &&
( (

View File

@ -6,9 +6,8 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Build and deploy Javadoc
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- name: Set up JDK 21 - name: Set up JDK 21

View File

@ -1,59 +0,0 @@
name: Build and test Minestom
on:
pull_request:
branches: [ master ]
jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
distribution: 'zulu'
java-version: 21
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Setup gradle cache
uses: burrunan/gradle-cache-action@v1
with:
save-generated-gradle-jars: false
save-local-build-cache: false
save-gradle-dependencies-cache: true
save-maven-dependencies-cache: true
# Ignore some of the paths when caching Maven Local repository
maven-local-ignore-paths: |
net/minestom/
- name: Build Minestom
run: ./gradlew classes testClasses javadoc
- name: Run Minestom tests
run: ./gradlew test
publish:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
env:
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSWORD }}
steps:
- uses: actions/checkout@v2
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
java-version: '21'
distribution: 'temurin'
- name: Set outputs
id: vars
run: |
export ACTUAL=${{ github.event.pull_request.head.sha }}
echo "short_commit_hash=${ACTUAL::10}" >> $GITHUB_OUTPUT
- name: Publish to Sonatype
env:
MINESTOM_VERSION: ${{ github.head_ref }}-${{ steps.vars.outputs.short_commit_hash }}
MINESTOM_CHANNEL: snapshot
run: |
./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY

View File

@ -1,32 +0,0 @@
name: Gradle Publish to Maven Central
on:
push:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
env:
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSWORD }}
steps:
- uses: actions/checkout@v2
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
java-version: '21'
distribution: 'temurin'
- name: Set outputs
id: vars
run: |
echo "short_commit_hash=${GITHUB_SHA::10}" >> $GITHUB_OUTPUT
- name: Publish to Sonatype
env:
MINESTOM_VERSION: ${{ steps.vars.outputs.short_commit_hash }}
MINESTOM_CHANNEL: release
run: |
./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository
echo "Version: ${SHORT_COMMIT_HASH}" >> $GITHUB_STEP_SUMMARY

View File

@ -1,12 +0,0 @@
name: Trigger Jitpack Build
on:
push:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Trigger Jitpack Build
run: curl "https://jitpack.io/com/github/Minestom/Minestom/${GITHUB_SHA:0:10}/build.log"

View File

@ -52,6 +52,7 @@ public class Generators {
generator.generate(resource("attributes.json"), "net.minestom.server.entity.attribute", "Attribute", "AttributeImpl", "Attributes"); generator.generate(resource("attributes.json"), "net.minestom.server.entity.attribute", "Attribute", "AttributeImpl", "Attributes");
generator.generate(resource("feature_flags.json"), "net.minestom.server", "FeatureFlag", "FeatureFlagImpl", "FeatureFlags"); generator.generate(resource("feature_flags.json"), "net.minestom.server", "FeatureFlag", "FeatureFlagImpl", "FeatureFlags");
generator.generate(resource("villager_professions.json"), "net.minestom.server.entity", "VillagerProfession", "VillagerProfessionImpl", "VillagerProfessions"); generator.generate(resource("villager_professions.json"), "net.minestom.server.entity", "VillagerProfession", "VillagerProfessionImpl", "VillagerProfessions");
generator.generate(resource("game_events.json"), "net.minestom.server.game", "GameEvent", "GameEventImpl", "GameEvents");
// Dynamic registries // Dynamic registries

View File

@ -141,7 +141,6 @@ public class PlayerInit {
inventory.addItemStack(getFoodItem(10000)); inventory.addItemStack(getFoodItem(10000));
inventory.addItemStack(getFoodItem(Integer.MAX_VALUE)); inventory.addItemStack(getFoodItem(Integer.MAX_VALUE));
if (event.isFirstSpawn()) { if (event.isFirstSpawn()) {
event.getPlayer().sendNotification(new Notification( event.getPlayer().sendNotification(new Notification(
Component.text("Welcome!"), Component.text("Welcome!"),

View File

@ -0,0 +1,127 @@
package net.minestom.server.game;
/**
* Code autogenerated, do not edit!
*/
@SuppressWarnings("unused")
interface GameEvents {
GameEvent BLOCK_ACTIVATE = GameEventImpl.get("minecraft:block_activate");
GameEvent BLOCK_ATTACH = GameEventImpl.get("minecraft:block_attach");
GameEvent BLOCK_CHANGE = GameEventImpl.get("minecraft:block_change");
GameEvent BLOCK_CLOSE = GameEventImpl.get("minecraft:block_close");
GameEvent BLOCK_DEACTIVATE = GameEventImpl.get("minecraft:block_deactivate");
GameEvent BLOCK_DESTROY = GameEventImpl.get("minecraft:block_destroy");
GameEvent BLOCK_DETACH = GameEventImpl.get("minecraft:block_detach");
GameEvent BLOCK_OPEN = GameEventImpl.get("minecraft:block_open");
GameEvent BLOCK_PLACE = GameEventImpl.get("minecraft:block_place");
GameEvent CONTAINER_CLOSE = GameEventImpl.get("minecraft:container_close");
GameEvent CONTAINER_OPEN = GameEventImpl.get("minecraft:container_open");
GameEvent DRINK = GameEventImpl.get("minecraft:drink");
GameEvent EAT = GameEventImpl.get("minecraft:eat");
GameEvent ELYTRA_GLIDE = GameEventImpl.get("minecraft:elytra_glide");
GameEvent ENTITY_DAMAGE = GameEventImpl.get("minecraft:entity_damage");
GameEvent ENTITY_DIE = GameEventImpl.get("minecraft:entity_die");
GameEvent ENTITY_DISMOUNT = GameEventImpl.get("minecraft:entity_dismount");
GameEvent ENTITY_INTERACT = GameEventImpl.get("minecraft:entity_interact");
GameEvent ENTITY_MOUNT = GameEventImpl.get("minecraft:entity_mount");
GameEvent ENTITY_PLACE = GameEventImpl.get("minecraft:entity_place");
GameEvent ENTITY_ACTION = GameEventImpl.get("minecraft:entity_action");
GameEvent EQUIP = GameEventImpl.get("minecraft:equip");
GameEvent EXPLODE = GameEventImpl.get("minecraft:explode");
GameEvent FLAP = GameEventImpl.get("minecraft:flap");
GameEvent FLUID_PICKUP = GameEventImpl.get("minecraft:fluid_pickup");
GameEvent FLUID_PLACE = GameEventImpl.get("minecraft:fluid_place");
GameEvent HIT_GROUND = GameEventImpl.get("minecraft:hit_ground");
GameEvent INSTRUMENT_PLAY = GameEventImpl.get("minecraft:instrument_play");
GameEvent ITEM_INTERACT_FINISH = GameEventImpl.get("minecraft:item_interact_finish");
GameEvent ITEM_INTERACT_START = GameEventImpl.get("minecraft:item_interact_start");
GameEvent JUKEBOX_PLAY = GameEventImpl.get("minecraft:jukebox_play");
GameEvent JUKEBOX_STOP_PLAY = GameEventImpl.get("minecraft:jukebox_stop_play");
GameEvent LIGHTNING_STRIKE = GameEventImpl.get("minecraft:lightning_strike");
GameEvent NOTE_BLOCK_PLAY = GameEventImpl.get("minecraft:note_block_play");
GameEvent PRIME_FUSE = GameEventImpl.get("minecraft:prime_fuse");
GameEvent PROJECTILE_LAND = GameEventImpl.get("minecraft:projectile_land");
GameEvent PROJECTILE_SHOOT = GameEventImpl.get("minecraft:projectile_shoot");
GameEvent SCULK_SENSOR_TENDRILS_CLICKING = GameEventImpl.get("minecraft:sculk_sensor_tendrils_clicking");
GameEvent SHEAR = GameEventImpl.get("minecraft:shear");
GameEvent SHRIEK = GameEventImpl.get("minecraft:shriek");
GameEvent SPLASH = GameEventImpl.get("minecraft:splash");
GameEvent STEP = GameEventImpl.get("minecraft:step");
GameEvent SWIM = GameEventImpl.get("minecraft:swim");
GameEvent TELEPORT = GameEventImpl.get("minecraft:teleport");
GameEvent UNEQUIP = GameEventImpl.get("minecraft:unequip");
GameEvent RESONATE_1 = GameEventImpl.get("minecraft:resonate_1");
GameEvent RESONATE_2 = GameEventImpl.get("minecraft:resonate_2");
GameEvent RESONATE_3 = GameEventImpl.get("minecraft:resonate_3");
GameEvent RESONATE_4 = GameEventImpl.get("minecraft:resonate_4");
GameEvent RESONATE_5 = GameEventImpl.get("minecraft:resonate_5");
GameEvent RESONATE_6 = GameEventImpl.get("minecraft:resonate_6");
GameEvent RESONATE_7 = GameEventImpl.get("minecraft:resonate_7");
GameEvent RESONATE_8 = GameEventImpl.get("minecraft:resonate_8");
GameEvent RESONATE_9 = GameEventImpl.get("minecraft:resonate_9");
GameEvent RESONATE_10 = GameEventImpl.get("minecraft:resonate_10");
GameEvent RESONATE_11 = GameEventImpl.get("minecraft:resonate_11");
GameEvent RESONATE_12 = GameEventImpl.get("minecraft:resonate_12");
GameEvent RESONATE_13 = GameEventImpl.get("minecraft:resonate_13");
GameEvent RESONATE_14 = GameEventImpl.get("minecraft:resonate_14");
GameEvent RESONATE_15 = GameEventImpl.get("minecraft:resonate_15");
}

View File

@ -27,6 +27,7 @@ public final class ServerFlag {
public static final int PLAYER_PACKET_QUEUE_SIZE = intProperty("minestom.packet-queue-size", 1000); public static final int PLAYER_PACKET_QUEUE_SIZE = intProperty("minestom.packet-queue-size", 1000);
public static final long KEEP_ALIVE_DELAY = longProperty("minestom.keep-alive-delay", 10_000); public static final long KEEP_ALIVE_DELAY = longProperty("minestom.keep-alive-delay", 10_000);
public static final long KEEP_ALIVE_KICK = longProperty("minestom.keep-alive-kick", 15_000); public static final long KEEP_ALIVE_KICK = longProperty("minestom.keep-alive-kick", 15_000);
public static final int PLAYER_CHUNK_UPDATE_LIMITER_HISTORY_SIZE = intProperty("minestom.player.chunk-update-limiter-history-size", 5, 0, Integer.MAX_VALUE);
// Network buffers // Network buffers
public static final int MAX_PACKET_SIZE = intProperty("minestom.max-packet-size", 2_097_151); // 3 bytes var-int public static final int MAX_PACKET_SIZE = intProperty("minestom.max-packet-size", 2_097_151); // 3 bytes var-int
@ -98,8 +99,19 @@ public final class ServerFlag {
return System.getProperty(name); return System.getProperty(name);
} }
private static int intProperty(String name, int defaultValue, int minValue, int maxValue) {
int value = Integer.getInteger(name, defaultValue);
if (value < minValue || value > maxValue) {
throw new IllegalArgumentException(String.format(
"Property '%s' value must be in range [%d..%d] but was %d",
name, minValue, maxValue, value
));
}
return value;
}
private static int intProperty(String name, int defaultValue) { private static int intProperty(String name, int defaultValue) {
return Integer.getInteger(name, defaultValue); return intProperty(name, defaultValue, Integer.MIN_VALUE, Integer.MAX_VALUE);
} }
private static long longProperty(String name, long defaultValue) { private static long longProperty(String name, long defaultValue) {

View File

@ -5,6 +5,7 @@ import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec; import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.EntityPose; import net.minestom.server.entity.EntityPose;
import net.minestom.server.instance.block.BlockFace; import net.minestom.server.instance.block.BlockFace;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
@ -21,11 +22,11 @@ public record BoundingBox(Vec relativeStart, Vec relativeEnd) implements Shape {
final static BoundingBox ZERO = new BoundingBox(Vec.ZERO, Vec.ZERO); final static BoundingBox ZERO = new BoundingBox(Vec.ZERO, Vec.ZERO);
public BoundingBox(double width, double height, double depth, Point offset) { public BoundingBox(double width, double height, double depth, Point offset) {
this(new Vec(-width / 2.0, 0.0, -depth / 2.0).add(offset), new Vec(width / 2.0, height, depth / 2.0).add(offset)); this(Vec.fromPoint(offset), new Vec(width, height, depth).add(offset));
} }
public BoundingBox(double width, double height, double depth) { public BoundingBox(double width, double height, double depth) {
this(width, height, depth, Vec.ZERO); this(width, height, depth, new Vec(-width / 2, 0, -depth / 2));
} }
@Override @Override
@ -94,6 +95,41 @@ public record BoundingBox(Vec relativeStart, Vec relativeEnd) implements Shape {
return new BoundingBox(width(), height(), depth(), offset); return new BoundingBox(width(), height(), depth(), offset);
} }
/**
* Creates a new {@link BoundingBox} with an expanded size from its center in every plane.
* <p>
* Equivalent to an expansion and an offset where the point is the three-axis offset.
* Particularly useful when you already use centered and aligned minY=0 position.
*
* @param x the X offset, this will be applied on both sides
* @param y the Y offset, this will be applied on both sides
* @param z the Z offset, this will be applied on both sides
* @return a new {@link BoundingBox} expanded and centered from the original minY
*/
@Contract(pure = true)
public @NotNull BoundingBox grow(double x, double y, double z) {
final double newWidth = width() + x, newDepth = depth() + z;
final Vec centerOffset = new Vec(-newWidth / 2, minY() - y / 2, -newDepth / 2);
return new BoundingBox(newWidth, height() + y, newDepth, centerOffset);
}
/**
* Creates a new {@link BoundingBox} with an expanded size from its center in every plane.
* <p>
* Equivalent to a double expansion and an offset where the point is the three-axis offset.
* Particularly useful when you already use centered and aligned minY=0 position.
*
* @param x the X offset, this will be applied on both sides
* @param y the Y offset, this will be applied on both sides
* @param z the Z offset, this will be applied on both sides
* @return a new {@link BoundingBox} expanded and centered from the original minY
*/
@Contract(pure = true)
public @NotNull BoundingBox growSymmetrically(double x, double y, double z) {
// Double all amounts to make it symmetric conformance to xyz
return grow(x * 2, y * 2, z * 2);
}
public double width() { public double width() {
return relativeEnd.x() - relativeStart.x(); return relativeEnd.x() - relativeStart.x();
} }

View File

@ -132,8 +132,8 @@ final class CommandParserImpl implements CommandParser {
int start = reader.cursor(); int start = reader.cursor();
if (reader.hasRemaining()) { if (reader.hasRemaining()) {
ArgumentResult<?> result = parseArgument(sender, argument, reader);
SuggestionCallback suggestionCallback = argument.getSuggestionCallback(); SuggestionCallback suggestionCallback = argument.getSuggestionCallback();
ArgumentResult<?> result = parseArgument(sender, argument, reader);
NodeResult nodeResult = new NodeResult(node, chain, (ArgumentResult<Object>) result, suggestionCallback); NodeResult nodeResult = new NodeResult(node, chain, (ArgumentResult<Object>) result, suggestionCallback);
chain.append(nodeResult); chain.append(nodeResult);
if (suggestionCallback != null) chain.suggestionCallback = suggestionCallback; if (suggestionCallback != null) chain.suggestionCallback = suggestionCallback;
@ -200,12 +200,27 @@ final class CommandParserImpl implements CommandParser {
if (reader.hasRemaining()) { if (reader.hasRemaining()) {
// Trailing data is a syntax error // Trailing data is a syntax error
return new NodeResult( // Can get to here if there's a default executor even if the user is still typing the command
node, // So let's supply the next argument's suggestion callback if it exists
Node returnNode = node;
SuggestionCallback suggestionCallback = argument.getSuggestionCallback();
List<Node> nextNodes = node.next();
if (!nextNodes.isEmpty()) {
returnNode = nextNodes.getFirst();
suggestionCallback = returnNode.argument().getSuggestionCallback();
}
NodeResult nodeResult = new NodeResult(
returnNode,
chain, chain,
new ArgumentResult.SyntaxError<>("Command has trailing data", "", -1), new ArgumentResult.SyntaxError<>("Command has trailing data", "", -1),
argument.getSuggestionCallback() suggestionCallback
); );
chain.suggestionCallback = suggestionCallback;
// prevent duplicates from being added (Fixes CommandParseTest#singleCommandWithMultipleSyntax() failure)
if (chain.getArgs().stream().noneMatch(arg -> arg.getId().equals(argument.getId()))) {
chain.append(nodeResult);
}
return nodeResult;
} }
// Command was successful! // Command was successful!

View File

@ -28,6 +28,21 @@ public record BlockVec(double x, double y, double z) implements Point {
this(point.x(), point.y(), point.z()); this(point.x(), point.y(), point.z());
} }
@Override
public int blockX() {
return (int) x;
}
@Override
public int blockY() {
return (int) y;
}
@Override
public int blockZ() {
return (int) z;
}
@Override @Override
public @NotNull Point withX(@NotNull DoubleUnaryOperator operator) { public @NotNull Point withX(@NotNull DoubleUnaryOperator operator) {
return new Vec(operator.applyAsDouble(x), y, z); return new Vec(operator.applyAsDouble(x), y, z);

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();
} }
@ -784,7 +802,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;
@ -1236,7 +1254,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) {
@ -1297,7 +1315,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);
} }
@ -1474,7 +1492,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

@ -191,7 +191,7 @@ public class Player extends LivingEntity implements CommandSender, HoverEventSou
// Game state (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Game_Event) // Game state (https://minecraft.wiki/w/Minecraft_Wiki:Projects/wiki.vg_merge/Protocol#Game_Event)
private boolean enableRespawnScreen; private boolean enableRespawnScreen;
private final ChunkUpdateLimitChecker chunkUpdateLimitChecker = new ChunkUpdateLimitChecker(6); private final ChunkUpdateLimitChecker chunkUpdateLimitChecker = new ChunkUpdateLimitChecker(ServerFlag.PLAYER_CHUNK_UPDATE_LIMITER_HISTORY_SIZE);
// Experience orb pickup // Experience orb pickup
protected Cooldown experiencePickupCooldown = new Cooldown(Duration.of(10, TimeUnit.SERVER_TICK)); protected Cooldown experiencePickupCooldown = new Cooldown(Duration.of(10, TimeUnit.SERVER_TICK));

View File

@ -58,7 +58,7 @@ public class WolfMeta extends TameableAnimalMeta {
} }
public sealed interface Variant extends ProtocolObject, WolfVariants permits VariantImpl { public sealed interface Variant extends ProtocolObject, WolfVariants permits VariantImpl {
@NotNull NetworkBuffer.Type<DynamicRegistry.Key<Variant>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::wolfVariant); @NotNull NetworkBuffer.Type<DynamicRegistry.Key<Variant>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::wolfVariant, true);
@NotNull BinaryTagSerializer<DynamicRegistry.Key<Variant>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::wolfVariant); @NotNull BinaryTagSerializer<DynamicRegistry.Key<Variant>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::wolfVariant);
static @NotNull Variant create( static @NotNull Variant create(

View File

@ -79,7 +79,7 @@ public class PaintingMeta extends EntityMeta implements ObjectDataProvider {
} }
public sealed interface Variant extends ProtocolObject, PaintingVariants permits VariantImpl { public sealed interface Variant extends ProtocolObject, PaintingVariants permits VariantImpl {
@NotNull NetworkBuffer.Type<DynamicRegistry.Key<Variant>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::paintingVariant); @NotNull NetworkBuffer.Type<DynamicRegistry.Key<Variant>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::paintingVariant, true);
@NotNull BinaryTagSerializer<DynamicRegistry.Key<Variant>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::paintingVariant); @NotNull BinaryTagSerializer<DynamicRegistry.Key<Variant>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::paintingVariant);
static @NotNull Variant create( static @NotNull Variant create(

View File

@ -95,7 +95,7 @@ public class PNode {
} }
@ApiStatus.Internal @ApiStatus.Internal
@NotNull Type getType() { public @NotNull Type getType() {
return type; return type;
} }

View File

@ -292,10 +292,21 @@ non-sealed class EventNodeImpl<T extends Event> implements EventNode<T> {
aClass -> new Handle<>((Class<T>) aClass)); aClass -> new Handle<>((Class<T>) aClass));
handle.invalidate(); handle.invalidate();
}); });
invalidateRecursiveSuperclasses(eventClass);
final EventNodeImpl<? super T> parent = this.parent; final EventNodeImpl<? super T> parent = this.parent;
if (parent != null) parent.invalidateEvent(eventClass); if (parent != null) parent.invalidateEvent(eventClass);
} }
private void invalidateRecursiveSuperclasses(@NotNull Class<?> eventClass) {
if (RecursiveEvent.class.isAssignableFrom(eventClass)) {
for (var cls : this.handleMap.keySet()) {
if (eventClass.isAssignableFrom(cls)) {
this.handleMap.get(cls).invalidate();
}
}
}
}
private ListenerEntry<T> getEntry(Class<? extends T> type) { private ListenerEntry<T> getEntry(Class<? extends T> type) {
return listenerMap.computeIfAbsent(type, aClass -> new ListenerEntry<>()); return listenerMap.computeIfAbsent(type, aClass -> new ListenerEntry<>());
} }

View File

@ -0,0 +1,66 @@
package net.minestom.server.game;
import net.minestom.server.registry.Registry;
import net.minestom.server.registry.StaticProtocolObject;
import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
/**
* Represents a game event.
* Used for a wide variety of events, from weather to bed use to game mode to demo messages.
*/
public sealed interface GameEvent extends StaticProtocolObject permits GameEventImpl {
/**
* Returns the game event registry.
*
* @return the game event registry or null if not found
*/
@Contract(pure = true)
@Nullable
Registry.GameEventEntry registry();
/**
* Gets the namespace ID of this game event.
*
* @return the namespace ID
*/
@Override
@NotNull
NamespaceID namespace();
/**
* Gets the game events from the registry.
*
* @return the game events
*/
static @NotNull Collection<@NotNull GameEvent> values() {
return GameEventImpl.values();
}
/**
* Gets a game event by its namespace ID.
*
* @param namespaceID the namespace ID
* @return the game event or null if not found
*/
static @Nullable GameEvent fromNamespaceId(@NotNull String namespaceID) {
return GameEventImpl.getSafe(namespaceID);
}
/**
* Gets a game event by its namespace ID.
*
* @param namespaceID the namespace ID
* @return the game event or null if not found
*/
static @Nullable GameEvent fromNamespaceId(@NotNull NamespaceID namespaceID) {
return fromNamespaceId(namespaceID.asString());
}
}

View File

@ -0,0 +1,65 @@
package net.minestom.server.game;
import net.minestom.server.registry.Registry;
import net.minestom.server.utils.NamespaceID;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
/**
* Represents a game event implementation.
* Used for a wide variety of events, from weather to bed use to game mode to demo messages.
*/
record GameEventImpl(Registry.GameEventEntry registry, NamespaceID namespace, int id) implements GameEvent {
private static final Registry.Container<GameEvent> CONTAINER = Registry.createStaticContainer(Registry.Resource.GAME_EVENTS, GameEventImpl::createImpl);
/**
* Creates a new {@link GameEventImpl} with the given namespace and properties.
*
* @param namespace the namespace
* @param properties the properties
* @return a new {@link GameEventImpl}
*/
private static GameEventImpl createImpl(String namespace, Registry.Properties properties) {
return new GameEventImpl(Registry.gameEventEntry(namespace, properties));
}
/**
* Creates a new {@link GameEventImpl} with the given registry.
*
* @param registry the registry
*/
private GameEventImpl(Registry.GameEventEntry registry) {
this(registry, registry.namespace(), registry.main().getInt("id"));
}
/**
* Gets the game events from the registry.
*
* @return the game events
*/
static Collection<GameEvent> values() {
return CONTAINER.values();
}
/**
* Gets a game event by its namespace ID.
*
* @param namespace the namespace ID
* @return the game event or null if not found
*/
public static GameEvent get(@NotNull String namespace) {
return CONTAINER.get(namespace);
}
/**
* Gets a game event by its namespace ID.
*
* @param namespace the namespace ID
* @return the game event or null if not found
*/
static GameEvent getSafe(@NotNull String namespace) {
return CONTAINER.getSafe(namespace);
}
}

View File

@ -2,8 +2,8 @@ package net.minestom.server.gamedata.tags;
import net.kyori.adventure.key.Key; import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.Keyed; import net.kyori.adventure.key.Keyed;
import net.minestom.server.MinecraftServer;
import net.minestom.server.entity.EntityType; import net.minestom.server.entity.EntityType;
import net.minestom.server.game.GameEvent;
import net.minestom.server.instance.block.Block; import net.minestom.server.instance.block.Block;
import net.minestom.server.item.Material; import net.minestom.server.item.Material;
import net.minestom.server.registry.*; import net.minestom.server.registry.*;
@ -14,10 +14,9 @@ import org.jetbrains.annotations.Nullable;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Objects; import java.util.Optional;
import java.util.Set; import java.util.Set;
import java.util.function.BiFunction; import java.util.function.BiFunction;
import java.util.function.Function;
/** /**
* Represents a group of items, blocks, fluids, entity types or function. * Represents a group of items, blocks, fluids, entity types or function.
@ -92,32 +91,35 @@ public final class Tag implements ProtocolObject, Keyed {
public enum BasicType { public enum BasicType {
BLOCKS("minecraft:block", Registry.Resource.BLOCK_TAGS, BLOCKS("minecraft:block", Registry.Resource.BLOCK_TAGS,
(name, registries) -> Objects.requireNonNull(Block.fromNamespaceId(name)).id()), (blockName, registries) -> Optional.ofNullable(Block.fromNamespaceId(blockName)).map(Block::id)),
ITEMS("minecraft:item", Registry.Resource.ITEM_TAGS, ITEMS("minecraft:item", Registry.Resource.ITEM_TAGS,
(name, registries) -> Objects.requireNonNull(Material.fromNamespaceId(name)).id()), (itemName, registries) -> Optional.ofNullable(Material.fromNamespaceId(itemName)).map(Material::id)),
FLUIDS("minecraft:fluid", Registry.Resource.FLUID_TAGS, FLUIDS("minecraft:fluid", Registry.Resource.FLUID_TAGS,
(name, registries) -> FluidRegistries.getFluid(name).ordinal()), (name, registries) -> Optional.of(name).map(FluidRegistries::getFluid).map(Enum::ordinal)),
ENTITY_TYPES("minecraft:entity_type", Registry.Resource.ENTITY_TYPE_TAGS, ENTITY_TYPES("minecraft:entity_type", Registry.Resource.ENTITY_TYPE_TAGS,
(name, registries) -> Objects.requireNonNull(EntityType.fromNamespaceId(name)).id()), (entityName, registries) -> Optional.ofNullable(EntityType.fromNamespaceId(entityName)).map(EntityType::id)),
GAME_EVENTS("minecraft:game_event", Registry.Resource.GAMEPLAY_TAGS, GAME_EVENTS("minecraft:game_event", Registry.Resource.GAMEPLAY_TAGS,
(name, registries) -> FluidRegistries.getFluid(name).ordinal()), (eventName, registries) -> Optional.ofNullable(GameEvent.fromNamespaceId(eventName)).map(GameEvent::id)),
SOUND_EVENTS("minecraft:sound_event", null, null), // Seems not to be included in server data SOUND_EVENTS("minecraft:sound_event", null, null), // Seems not to be included in server data
POTION_EFFECTS("minecraft:potion_effect", null, null), // Seems not to be included in server data POTION_EFFECTS("minecraft:potion_effect", null, null), // Seems not to be included in server data
//todo this is cursed. it does not update as the registry changes. Fix later.
ENCHANTMENTS("minecraft:enchantment", Registry.Resource.ENCHANTMENT_TAGS, ENCHANTMENTS("minecraft:enchantment", Registry.Resource.ENCHANTMENT_TAGS,
(name, registries) -> registries.enchantment().getId(DynamicRegistry.Key.of(name))), (name, registries) -> Optional.of(DynamicRegistry.Key.of(name))
.map(DynamicRegistry.Key::namespace)
.map(registries.enchantment()::getId)),
BIOMES("minecraft:worldgen/biome", Registry.Resource.BIOME_TAGS, BIOMES("minecraft:worldgen/biome", Registry.Resource.BIOME_TAGS,
(name, registries) -> registries.biome().getId(DynamicRegistry.Key.of(name))); (name, registries) -> Optional.of(DynamicRegistry.Key.of(name))
.map(DynamicRegistry.Key::namespace)
.map(registries.biome()::getId));
private final static BasicType[] VALUES = values(); private static final BasicType[] VALUES = values();
private final String identifier; private final String identifier;
private final Registry.Resource resource; private final Registry.Resource resource;
private final BiFunction<String, Registries, Integer> function; private final BiFunction<String, Registries, Optional<Integer>> function;
BasicType(@NotNull String identifier, BasicType(@NotNull String identifier,
@Nullable Registry.Resource resource, @Nullable Registry.Resource resource,
@Nullable BiFunction<String, Registries, Integer> function) { @Nullable BiFunction<String, Registries, Optional<Integer>> function) {
this.identifier = identifier; this.identifier = identifier;
this.resource = resource; this.resource = resource;
this.function = function; this.function = function;
@ -131,7 +133,7 @@ public final class Tag implements ProtocolObject, Keyed {
return resource; return resource;
} }
public BiFunction<String, Registries, Integer> getFunction() { public BiFunction<String, Registries, Optional<Integer>> getFunction() {
return function; return function;
} }

View File

@ -30,8 +30,8 @@ public final class TagManager {
public @Nullable Tag getTag(Tag.BasicType type, String namespace) { public @Nullable Tag getTag(Tag.BasicType type, String namespace) {
final var tags = tagMap.get(type); final var tags = tagMap.get(type);
for (var tag : tags) { for (final var tag : tags) {
if (tag.getName().asString().equals(namespace)) if (tag.name().equals(namespace))
return tag; return tag;
} }
return null; return null;
@ -46,10 +46,10 @@ public final class TagManager {
for (Map.Entry<Tag.BasicType, List<Tag>> entry : tagMap.entrySet()) { for (Map.Entry<Tag.BasicType, List<Tag>> entry : tagMap.entrySet()) {
final Tag.BasicType type = entry.getKey(); final Tag.BasicType type = entry.getKey();
final String registry = type.getIdentifier(); final String registry = type.getIdentifier();
List<TagsPacket.Tag> tags = new ArrayList<>(); final List<TagsPacket.Tag> tags = new ArrayList<>();
for (Tag tag : entry.getValue()) { for (final Tag tag : entry.getValue()) {
final String identifier = tag.getName().asString(); final String identifier = tag.name();
final int[] values = tag.getValues().stream().mapToInt(value -> type.getFunction().apply(value.asString(), registries)).toArray(); final int[] values = tag.getValues().stream().mapToInt(value -> type.getFunction().apply(value.asString(), registries).orElse(null)).filter(Objects::nonNull).toArray();
tags.add(new TagsPacket.Tag(identifier, values)); tags.add(new TagsPacket.Tag(identifier, values));
} }
registryList.add(new TagsPacket.Registry(registry, tags)); registryList.add(new TagsPacket.Registry(registry, tags));

View File

@ -211,7 +211,7 @@ public class DynamicChunk extends Chunk {
final Section section = getSectionAt(y); final Section section = getSectionAt(y);
final int blockStateId = section.blockPalette() final int blockStateId = section.blockPalette()
.get(globalToSectionRelative(x), globalToSectionRelative(y), globalToSectionRelative(z)); .get(globalToSectionRelative(x), globalToSectionRelative(y), globalToSectionRelative(z));
return Objects.requireNonNullElse(Block.fromStateId((short) blockStateId), Block.AIR); return Objects.requireNonNullElse(Block.fromStateId(blockStateId), Block.AIR);
} }
@Override @Override

View File

@ -103,7 +103,7 @@ public abstract class Instance implements Block.Getter, Block.Setter,
private final ChunkCache blockRetriever = new ChunkCache(this, null, null); private final ChunkCache blockRetriever = new ChunkCache(this, null, null);
// the uuid of this instance // the uuid of this instance
protected UUID uniqueId; protected UUID uuid;
// instance custom data // instance custom data
protected TagHandler tagHandler = TagHandler.newHandler(); protected TagHandler tagHandler = TagHandler.newHandler();
@ -119,31 +119,31 @@ public abstract class Instance implements Block.Getter, Block.Setter,
/** /**
* Creates a new instance. * Creates a new instance.
* *
* @param uniqueId the {@link UUID} of the instance * @param uuid the {@link UUID} of the instance
* @param dimensionType the {@link DimensionType} of the instance * @param dimensionType the {@link DimensionType} of the instance
*/ */
public Instance(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key<DimensionType> dimensionType) { public Instance(@NotNull UUID uuid, @NotNull DynamicRegistry.Key<DimensionType> dimensionType) {
this(uniqueId, dimensionType, dimensionType.namespace()); this(uuid, dimensionType, dimensionType.namespace());
} }
/** /**
* Creates a new instance. * Creates a new instance.
* *
* @param uniqueId the {@link UUID} of the instance * @param uuid the {@link UUID} of the instance
* @param dimensionType the {@link DimensionType} of the instance * @param dimensionType the {@link DimensionType} of the instance
*/ */
public Instance(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @NotNull NamespaceID dimensionName) { public Instance(@NotNull UUID uuid, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @NotNull NamespaceID dimensionName) {
this(MinecraftServer.getDimensionTypeRegistry(), uniqueId, dimensionType, dimensionName); this(MinecraftServer.getDimensionTypeRegistry(), uuid, dimensionType, dimensionName);
} }
/** /**
* Creates a new instance. * Creates a new instance.
* *
* @param uniqueId the {@link UUID} of the instance * @param uuid the {@link UUID} of the instance
* @param dimensionType the {@link DimensionType} of the instance * @param dimensionType the {@link DimensionType} of the instance
*/ */
public Instance(@NotNull DynamicRegistry<DimensionType> dimensionTypeRegistry, @NotNull UUID uniqueId, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @NotNull NamespaceID dimensionName) { public Instance(@NotNull DynamicRegistry<DimensionType> dimensionTypeRegistry, @NotNull UUID uuid, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @NotNull NamespaceID dimensionName) {
this.uniqueId = uniqueId; this.uuid = uuid;
this.dimensionType = dimensionType; this.dimensionType = dimensionType;
this.cachedDimensionType = dimensionTypeRegistry.get(dimensionType); this.cachedDimensionType = dimensionTypeRegistry.get(dimensionType);
Check.argCondition(cachedDimensionType == null, "The dimension " + dimensionType + " is not registered! Please add it to the registry (`MinecraftServer.getDimensionTypeRegistry().registry(dimensionType)`)."); Check.argCondition(cachedDimensionType == null, "The dimension " + dimensionType + " is not registered! Please add it to the registry (`MinecraftServer.getDimensionTypeRegistry().registry(dimensionType)`).");
@ -153,7 +153,7 @@ public abstract class Instance implements Block.Getter, Block.Setter,
targetBorderDiameter = this.worldBorder.diameter(); targetBorderDiameter = this.worldBorder.diameter();
this.pointers = Pointers.builder() this.pointers = Pointers.builder()
.withDynamic(Identity.UUID, this::getUniqueId) .withDynamic(Identity.UUID, this::getUuid)
.build(); .build();
final ServerProcess process = MinecraftServer.process(); final ServerProcess process = MinecraftServer.process();
@ -750,8 +750,19 @@ public abstract class Instance implements Block.Getter, Block.Setter,
* *
* @return the instance unique id * @return the instance unique id
*/ */
public @NotNull UUID getUuid() {
return uuid;
}
/**
* Gets the instance unique id.
*
* @return the instance unique id
* @deprecated Replace with {@link Instance#getUuid()}
*/
@Deprecated(forRemoval = true)
public @NotNull UUID getUniqueId() { public @NotNull UUID getUniqueId() {
return uniqueId; return uuid;
} }
/** /**

View File

@ -89,30 +89,30 @@ public class InstanceContainer extends Instance {
protected InstanceContainer srcInstance; // only present if this instance has been created using a copy protected InstanceContainer srcInstance; // only present if this instance has been created using a copy
private long lastBlockChangeTime; // Time at which the last block change happened (#setBlock) private long lastBlockChangeTime; // Time at which the last block change happened (#setBlock)
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key<DimensionType> dimensionType) { public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key<DimensionType> dimensionType) {
this(uniqueId, dimensionType, null, dimensionType.namespace()); this(uuid, dimensionType, null, dimensionType.namespace());
} }
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @NotNull NamespaceID dimensionName) { public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @NotNull NamespaceID dimensionName) {
this(uniqueId, dimensionType, null, dimensionName); this(uuid, dimensionType, null, dimensionName);
} }
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @Nullable IChunkLoader loader) { public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @Nullable IChunkLoader loader) {
this(uniqueId, dimensionType, loader, dimensionType.namespace()); this(uuid, dimensionType, loader, dimensionType.namespace());
} }
public InstanceContainer(@NotNull UUID uniqueId, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @Nullable IChunkLoader loader, @NotNull NamespaceID dimensionName) { public InstanceContainer(@NotNull UUID uuid, @NotNull DynamicRegistry.Key<DimensionType> dimensionType, @Nullable IChunkLoader loader, @NotNull NamespaceID dimensionName) {
this(MinecraftServer.getDimensionTypeRegistry(), uniqueId, dimensionType, loader, dimensionName); this(MinecraftServer.getDimensionTypeRegistry(), uuid, dimensionType, loader, dimensionName);
} }
public InstanceContainer( public InstanceContainer(
@NotNull DynamicRegistry<DimensionType> dimensionTypeRegistry, @NotNull DynamicRegistry<DimensionType> dimensionTypeRegistry,
@NotNull UUID uniqueId, @NotNull UUID uuid,
@NotNull DynamicRegistry.Key<DimensionType> dimensionType, @NotNull DynamicRegistry.Key<DimensionType> dimensionType,
@Nullable IChunkLoader loader, @Nullable IChunkLoader loader,
@NotNull NamespaceID dimensionName @NotNull NamespaceID dimensionName
) { ) {
super(dimensionTypeRegistry, uniqueId, dimensionType, dimensionName); super(dimensionTypeRegistry, uuid, dimensionType, dimensionName);
setChunkSupplier(DynamicChunk::new); setChunkSupplier(DynamicChunk::new);
setChunkLoader(Objects.requireNonNullElse(loader, DEFAULT_LOADER)); setChunkLoader(Objects.requireNonNullElse(loader, DEFAULT_LOADER));
this.chunkLoader.loadInstance(this); this.chunkLoader.loadInstance(this);

View File

@ -151,7 +151,7 @@ public final class InstanceManager {
public @Nullable Instance getInstance(@NotNull UUID uuid) { public @Nullable Instance getInstance(@NotNull UUID uuid) {
Optional<Instance> instance = getInstances() Optional<Instance> instance = getInstances()
.stream() .stream()
.filter(someInstance -> someInstance.getUniqueId().equals(uuid)) .filter(someInstance -> someInstance.getUuid().equals(uuid))
.findFirst(); .findFirst();
return instance.orElse(null); return instance.orElse(null);
} }

View File

@ -241,97 +241,98 @@ public class LightingChunk extends DynamicChunk {
@Override @Override
protected LightData createLightData(boolean requiredFullChunk) { protected LightData createLightData(boolean requiredFullChunk) {
packetGenerationLock.lock(); packetGenerationLock.lock();
if (requiredFullChunk) { try {
if (fullLightData != null) { if (requiredFullChunk) {
packetGenerationLock.unlock(); if (fullLightData != null) {
return fullLightData; return fullLightData;
}
} else {
if (partialLightData != null) {
packetGenerationLock.unlock();
return partialLightData;
}
}
BitSet skyMask = new BitSet();
BitSet blockMask = new BitSet();
BitSet emptySkyMask = new BitSet();
BitSet emptyBlockMask = new BitSet();
List<byte[]> skyLights = new ArrayList<>();
List<byte[]> blockLights = new ArrayList<>();
int chunkMin = instance.getCachedDimensionType().minY();
int highestNeighborBlock = instance.getCachedDimensionType().minY();
for (int i = -1; i <= 1; i++) {
for (int j = -1; j <= 1; j++) {
Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j);
if (neighborChunk == null) continue;
if (neighborChunk instanceof LightingChunk light) {
light.getOcclusionMap();
highestNeighborBlock = Math.max(highestNeighborBlock, light.highestBlock);
} }
} } else {
} if (partialLightData != null) {
return partialLightData;
int index = 0;
for (Section section : sections) {
boolean wasUpdatedBlock = false;
boolean wasUpdatedSky = false;
if (section.blockLight().requiresUpdate()) {
relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.BLOCK);
wasUpdatedBlock = true;
} else if (requiredFullChunk || section.blockLight().requiresSend()) {
wasUpdatedBlock = true;
}
if (section.skyLight().requiresUpdate()) {
relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.SKY);
wasUpdatedSky = true;
} else if (requiredFullChunk || section.skyLight().requiresSend()) {
wasUpdatedSky = true;
}
final int sectionMinY = index * 16 + chunkMin;
index++;
if ((wasUpdatedSky) && this.instance.getCachedDimensionType().hasSkylight() && sectionMinY <= (highestNeighborBlock + 16)) {
final byte[] skyLight = section.skyLight().array();
if (skyLight.length != 0 && skyLight != EMPTY_CONTENT) {
skyLights.add(skyLight);
skyMask.set(index);
} else {
emptySkyMask.set(index);
} }
} }
if (wasUpdatedBlock) { BitSet skyMask = new BitSet();
final byte[] blockLight = section.blockLight().array(); BitSet blockMask = new BitSet();
BitSet emptySkyMask = new BitSet();
BitSet emptyBlockMask = new BitSet();
List<byte[]> skyLights = new ArrayList<>();
List<byte[]> blockLights = new ArrayList<>();
if (blockLight.length != 0 && blockLight != EMPTY_CONTENT) { int chunkMin = instance.getCachedDimensionType().minY();
blockLights.add(blockLight); int highestNeighborBlock = instance.getCachedDimensionType().minY();
blockMask.set(index); for (int i = -1; i <= 1; i++) {
} else { for (int j = -1; j <= 1; j++) {
emptyBlockMask.set(index); Chunk neighborChunk = instance.getChunk(chunkX + i, chunkZ + j);
if (neighborChunk == null) continue;
if (neighborChunk instanceof LightingChunk light) {
light.getOcclusionMap();
highestNeighborBlock = Math.max(highestNeighborBlock, light.highestBlock);
}
} }
} }
int index = 0;
for (Section section : sections) {
boolean wasUpdatedBlock = false;
boolean wasUpdatedSky = false;
if (section.blockLight().requiresUpdate()) {
relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.BLOCK);
wasUpdatedBlock = true;
} else if (requiredFullChunk || section.blockLight().requiresSend()) {
wasUpdatedBlock = true;
}
if (section.skyLight().requiresUpdate()) {
relightSection(instance, this.chunkX, index + minSection, chunkZ, LightType.SKY);
wasUpdatedSky = true;
} else if (requiredFullChunk || section.skyLight().requiresSend()) {
wasUpdatedSky = true;
}
final int sectionMinY = index * 16 + chunkMin;
index++;
if ((wasUpdatedSky) && this.instance.getCachedDimensionType().hasSkylight() && sectionMinY <= (highestNeighborBlock + 16)) {
final byte[] skyLight = section.skyLight().array();
if (skyLight.length != 0 && skyLight != EMPTY_CONTENT) {
skyLights.add(skyLight);
skyMask.set(index);
} else {
emptySkyMask.set(index);
}
}
if (wasUpdatedBlock) {
final byte[] blockLight = section.blockLight().array();
if (blockLight.length != 0 && blockLight != EMPTY_CONTENT) {
blockLights.add(blockLight);
blockMask.set(index);
} else {
emptyBlockMask.set(index);
}
}
}
LightData lightData = new LightData(skyMask, blockMask,
emptySkyMask, emptyBlockMask,
skyLights, blockLights);
if (requiredFullChunk) {
this.fullLightData = lightData;
} else {
this.partialLightData = lightData;
}
return lightData;
} finally {
packetGenerationLock.unlock();
} }
LightData lightData = new LightData(skyMask, blockMask,
emptySkyMask, emptyBlockMask,
skyLights, blockLights);
if (requiredFullChunk) {
this.fullLightData = lightData;
} else {
this.partialLightData = lightData;
}
packetGenerationLock.unlock();
return lightData;
} }
@Override @Override

View File

@ -21,8 +21,8 @@ import java.util.concurrent.CompletableFuture;
public class SharedInstance extends Instance { public class SharedInstance extends Instance {
private final InstanceContainer instanceContainer; private final InstanceContainer instanceContainer;
public SharedInstance(@NotNull UUID uniqueId, @NotNull InstanceContainer instanceContainer) { public SharedInstance(@NotNull UUID uuid, @NotNull InstanceContainer instanceContainer) {
super(uniqueId, instanceContainer.getDimensionType()); super(uuid, instanceContainer.getDimensionType());
this.instanceContainer = instanceContainer; this.instanceContainer = instanceContainer;
} }

View File

@ -106,7 +106,7 @@ public class ChunkBatch implements Batch<ChunkCallback> {
final Chunk chunk = instance.getChunk(chunkX, chunkZ); final Chunk chunk = instance.getChunk(chunkX, chunkZ);
if (chunk == null) { if (chunk == null) {
LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.", LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.",
chunkX, chunkZ, instance.getUniqueId()); chunkX, chunkZ, instance.getUuid());
return null; return null;
} }
return apply(instance, chunk, callback); return apply(instance, chunk, callback);
@ -165,7 +165,7 @@ public class ChunkBatch implements Batch<ChunkCallback> {
try { try {
if (!chunk.isLoaded()) { if (!chunk.isLoaded()) {
LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.", LOGGER.warn("Unable to apply ChunkBatch to unloaded chunk ({}, {}) in {}.",
chunk.getChunkX(), chunk.getChunkZ(), instance.getUniqueId()); chunk.getChunkX(), chunk.getChunkZ(), instance.getUuid());
return; return;
} }

View File

@ -13,7 +13,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public sealed interface BannerPattern extends ProtocolObject, BannerPatterns permits BannerPatternImpl { public sealed interface BannerPattern extends ProtocolObject, BannerPatterns permits BannerPatternImpl {
@NotNull NetworkBuffer.Type<DynamicRegistry.Key<BannerPattern>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::bannerPattern); @NotNull NetworkBuffer.Type<DynamicRegistry.Key<BannerPattern>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::bannerPattern, true);
@NotNull BinaryTagSerializer<DynamicRegistry.Key<BannerPattern>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::bannerPattern); @NotNull BinaryTagSerializer<DynamicRegistry.Key<BannerPattern>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::bannerPattern);
static @NotNull BannerPattern create( static @NotNull BannerPattern create(

View File

@ -14,7 +14,7 @@ import org.jetbrains.annotations.Nullable;
public sealed interface JukeboxSong extends ProtocolObject, JukeboxSongs permits JukeboxSongImpl { public sealed interface JukeboxSong extends ProtocolObject, JukeboxSongs permits JukeboxSongImpl {
@NotNull NetworkBuffer.Type<DynamicRegistry.Key<JukeboxSong>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::jukeboxSong); @NotNull NetworkBuffer.Type<DynamicRegistry.Key<JukeboxSong>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::jukeboxSong, false);
@NotNull BinaryTagSerializer<DynamicRegistry.Key<JukeboxSong>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::jukeboxSong); @NotNull BinaryTagSerializer<DynamicRegistry.Key<JukeboxSong>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::jukeboxSong);
static @NotNull JukeboxSong create( static @NotNull JukeboxSong create(

View File

@ -48,7 +48,7 @@ public interface UnitModifier extends Block.Setter, Biome.Setter {
void fill(@NotNull Point start, @NotNull Point end, @NotNull Block block); void fill(@NotNull Point start, @NotNull Point end, @NotNull Block block);
/** /**
* Fills the 3d rectangular area with the given biome. * Fills the 3d rectangular area with the given block.
* *
* @param minHeight the minimum height of the area * @param minHeight the minimum height of the area
* @param maxHeight the maximum height of the area * @param maxHeight the maximum height of the area

View File

@ -33,7 +33,7 @@ final class BlockLight implements Light {
ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue(); ShortArrayFIFOQueue lightSources = new ShortArrayFIFOQueue();
// Apply section light // Apply section light
blockPalette.getAllPresent((x, y, z, stateId) -> { blockPalette.getAllPresent((x, y, z, stateId) -> {
final Block block = Block.fromStateId((short) stateId); final Block block = Block.fromStateId(stateId);
assert block != null; assert block != null;
final byte lightEmission = (byte) block.registry().lightEmission(); final byte lightEmission = (byte) block.registry().lightEmission();

View File

@ -112,7 +112,7 @@ public final class LightCompute {
} }
public static Block getBlock(Palette palette, int x, int y, int z) { public static Block getBlock(Palette palette, int x, int y, int z) {
return Block.fromStateId((short) palette.get(x, y, z)); return Block.fromStateId(palette.get(x, y, z));
} }
public static byte[] bake(byte[] content1, byte[] content2) { public static byte[] bake(byte[] content1, byte[] content2) {

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

@ -49,8 +49,6 @@ final class PaletteIndirect implements SpecializedPalette, Cloneable {
this.paletteToValueList.add(palette[i]); this.paletteToValueList.add(palette[i]);
this.valueToPaletteMap.put(palette[i], i); this.valueToPaletteMap.put(palette[i], i);
} }
this.values = new long[arrayLength(dimension(), bitsPerEntry)];
} }
PaletteIndirect(int dimension, int maxBitsPerEntry, byte bitsPerEntry) { PaletteIndirect(int dimension, int maxBitsPerEntry, byte bitsPerEntry) {

View File

@ -77,7 +77,7 @@ public final class ItemComponent {
public static final DataComponent<WritableBookContent> WRITABLE_BOOK_CONTENT = register("writable_book_content", WritableBookContent.NETWORK_TYPE, WritableBookContent.NBT_TYPE); public static final DataComponent<WritableBookContent> WRITABLE_BOOK_CONTENT = register("writable_book_content", WritableBookContent.NETWORK_TYPE, WritableBookContent.NBT_TYPE);
public static final DataComponent<WrittenBookContent> WRITTEN_BOOK_CONTENT = register("written_book_content", WrittenBookContent.NETWORK_TYPE, WrittenBookContent.NBT_TYPE); public static final DataComponent<WrittenBookContent> WRITTEN_BOOK_CONTENT = register("written_book_content", WrittenBookContent.NETWORK_TYPE, WrittenBookContent.NBT_TYPE);
public static final DataComponent<ArmorTrim> TRIM = register("trim", ArmorTrim.NETWORK_TYPE, ArmorTrim.NBT_TYPE); public static final DataComponent<ArmorTrim> TRIM = register("trim", ArmorTrim.NETWORK_TYPE, ArmorTrim.NBT_TYPE);
public static final DataComponent<DebugStickState> DEBUG_STICK_STATE = register("debug_stick_state", null, DebugStickState.NBT_TYPE); public static final DataComponent<DebugStickState> DEBUG_STICK_STATE = register("debug_stick_state", DebugStickState.NETWORK_TYPE, DebugStickState.NBT_TYPE);
public static final DataComponent<CustomData> ENTITY_DATA = register("entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE); public static final DataComponent<CustomData> ENTITY_DATA = register("entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE);
public static final DataComponent<CustomData> BUCKET_ENTITY_DATA = register("bucket_entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE); public static final DataComponent<CustomData> BUCKET_ENTITY_DATA = register("bucket_entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE);
public static final DataComponent<CustomData> BLOCK_ENTITY_DATA = register("block_entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE); public static final DataComponent<CustomData> BLOCK_ENTITY_DATA = register("block_entity_data", CustomData.NETWORK_TYPE, CustomData.NBT_TYPE);

View File

@ -17,7 +17,7 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
public sealed interface TrimMaterial extends ProtocolObject permits TrimMaterialImpl { public sealed interface TrimMaterial extends ProtocolObject permits TrimMaterialImpl {
@NotNull NetworkBuffer.Type<DynamicRegistry.Key<TrimMaterial>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimMaterial); @NotNull NetworkBuffer.Type<DynamicRegistry.Key<TrimMaterial>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimMaterial, true);
@NotNull BinaryTagSerializer<DynamicRegistry.Key<TrimMaterial>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::trimMaterial); @NotNull BinaryTagSerializer<DynamicRegistry.Key<TrimMaterial>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::trimMaterial);
static @NotNull TrimMaterial create( static @NotNull TrimMaterial create(

View File

@ -15,7 +15,7 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public sealed interface TrimPattern extends ProtocolObject permits TrimPatternImpl { public sealed interface TrimPattern extends ProtocolObject permits TrimPatternImpl {
@NotNull NetworkBuffer.Type<DynamicRegistry.Key<TrimPattern>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimPattern); @NotNull NetworkBuffer.Type<DynamicRegistry.Key<TrimPattern>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::trimPattern, true);
@NotNull BinaryTagSerializer<DynamicRegistry.Key<TrimPattern>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::trimPattern); @NotNull BinaryTagSerializer<DynamicRegistry.Key<TrimPattern>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::trimPattern);
static @NotNull TrimPattern create( static @NotNull TrimPattern create(

View File

@ -3,6 +3,7 @@ package net.minestom.server.item.component;
import net.kyori.adventure.nbt.BinaryTag; import net.kyori.adventure.nbt.BinaryTag;
import net.kyori.adventure.nbt.CompoundBinaryTag; import net.kyori.adventure.nbt.CompoundBinaryTag;
import net.kyori.adventure.nbt.StringBinaryTag; import net.kyori.adventure.nbt.StringBinaryTag;
import net.minestom.server.network.NetworkBuffer;
import net.minestom.server.utils.nbt.BinaryTagSerializer; import net.minestom.server.utils.nbt.BinaryTagSerializer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -30,6 +31,7 @@ public record DebugStickState(@NotNull Map<String, String> state) {
return builder.build(); return builder.build();
} }
); );
public static final NetworkBuffer.Type<DebugStickState> NETWORK_TYPE = NetworkBuffer.TypedNBT(NBT_TYPE);
public DebugStickState { public DebugStickState {
state = Map.copyOf(state); state = Map.copyOf(state);

View File

@ -16,7 +16,7 @@ import org.jetbrains.annotations.Nullable;
import java.util.List; import java.util.List;
public sealed interface Enchantment extends ProtocolObject, Enchantments permits EnchantmentImpl { public sealed interface Enchantment extends ProtocolObject, Enchantments permits EnchantmentImpl {
@NotNull NetworkBuffer.Type<DynamicRegistry.Key<Enchantment>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::enchantment); @NotNull NetworkBuffer.Type<DynamicRegistry.Key<Enchantment>> NETWORK_TYPE = NetworkBuffer.RegistryKey(Registries::enchantment, false);
@NotNull BinaryTagSerializer<DynamicRegistry.Key<Enchantment>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::enchantment); @NotNull BinaryTagSerializer<DynamicRegistry.Key<Enchantment>> NBT_TYPE = BinaryTagSerializer.registryKey(Registries::enchantment);
static @NotNull Builder builder() { static @NotNull Builder builder() {

View File

@ -35,7 +35,7 @@ public class SpectateListener {
// Ignore if they're not in the same instance. Vanilla actually allows for // Ignore if they're not in the same instance. Vanilla actually allows for
// cross-instance spectating, but it's not really a good idea for Minestom. // cross-instance spectating, but it's not really a good idea for Minestom.
if (targetInstance.getUniqueId() != playerInstance.getUniqueId()) { if (targetInstance.getUuid() != playerInstance.getUuid()) {
return; return;
} }

View File

@ -1,7 +1,9 @@
package net.minestom.server.listener; package net.minestom.server.listener;
import net.minestom.server.ServerFlag; import net.minestom.server.ServerFlag;
import net.minestom.server.collision.BoundingBox;
import net.minestom.server.coordinate.Point; import net.minestom.server.coordinate.Point;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec; import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.Entity; import net.minestom.server.entity.Entity;
import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.LivingEntity;
@ -20,8 +22,11 @@ public class UseEntityListener {
return; return;
if (ServerFlag.ENFORCE_INTERACTION_LIMIT) { if (ServerFlag.ENFORCE_INTERACTION_LIMIT) {
double range = Math.pow(player.getAttributeValue(Attribute.ENTITY_INTERACTION_RANGE) + 1, 2); // Add 1 additional block for people with less than stellar ping final double maxDistanceSquared = Math.pow(player.getAttributeValue(Attribute.ENTITY_INTERACTION_RANGE) + 1, 2);
if (player.getDistanceSquared(entity) > range) {
final double distSquared = getDistSquared(player, entity);
if (distSquared > maxDistanceSquared) {
return; return;
} }
} }
@ -36,4 +41,33 @@ public class UseEntityListener {
EventDispatcher.call(new PlayerEntityInteractEvent(player, entity, interactAt.hand(), interactPosition)); EventDispatcher.call(new PlayerEntityInteractEvent(player, entity, interactAt.hand(), interactPosition));
} }
} }
}
private static double getDistSquared(Player player, Entity entity) {
final Pos playerPos = player.getPosition();
final double eyeHeight = player.getEyeHeight();
final double px = playerPos.x();
final double py = playerPos.y() + eyeHeight;
final double pz = playerPos.z();
final BoundingBox box = entity.getBoundingBox();
final double halfWidth = box.width() / 2;
final double height = box.height();
final Pos entityPos = entity.getPosition();
final double minX = entityPos.x() - halfWidth;
final double maxX = entityPos.x() + halfWidth;
final double minY = entityPos.y();
final double maxY = entityPos.y() + height;
final double minZ = entityPos.z() - halfWidth;
final double maxZ = entityPos.z() + halfWidth;
final double clampX = Math.max(minX, Math.min(px, maxX));
final double clampY = Math.max(minY, Math.min(py, maxY));
final double clampZ = Math.max(minZ, Math.min(pz, maxZ));
final double dx = px - clampX;
final double dy = py - clampY;
final double dz = pz - clampZ;
return dx * dx + dy * dy + dz * dz;
}
}

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

@ -333,6 +333,7 @@ public final class ConnectionManager {
public void updateWaitingPlayers() { public void updateWaitingPlayers() {
this.waitingPlayers.drain(player -> { this.waitingPlayers.drain(player -> {
if (!player.isOnline()) return; // Player disconnected while in queued to join if (!player.isOnline()) return; // Player disconnected while in queued to join
configurationPlayers.remove(player);
playPlayers.add(player); playPlayers.add(player);
keepAlivePlayers.add(player); keepAlivePlayers.add(player);

View File

@ -12,6 +12,7 @@ import net.minestom.server.registry.Registries;
import net.minestom.server.utils.Direction; import net.minestom.server.utils.Direction;
import net.minestom.server.utils.Unit; import net.minestom.server.utils.Unit;
import net.minestom.server.utils.crypto.KeyUtils; import net.minestom.server.utils.crypto.KeyUtils;
import net.minestom.server.utils.nbt.BinaryTagSerializer;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability; import org.jetbrains.annotations.UnknownNullability;
@ -45,6 +46,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();
@ -63,8 +65,8 @@ public sealed interface NetworkBuffer permits NetworkBufferImpl {
Type<Instant> INSTANT_MS = LONG.transform(Instant::ofEpochMilli, Instant::toEpochMilli); Type<Instant> INSTANT_MS = LONG.transform(Instant::ofEpochMilli, Instant::toEpochMilli);
Type<PublicKey> PUBLIC_KEY = BYTE_ARRAY.transform(KeyUtils::publicRSAKeyFrom, PublicKey::getEncoded); Type<PublicKey> PUBLIC_KEY = BYTE_ARRAY.transform(KeyUtils::publicRSAKeyFrom, PublicKey::getEncoded);
static <T extends ProtocolObject> @NotNull Type<DynamicRegistry.Key<T>> RegistryKey(@NotNull Function<Registries, DynamicRegistry<T>> selector) { static <T extends ProtocolObject> @NotNull Type<DynamicRegistry.Key<T>> RegistryKey(@NotNull Function<Registries, DynamicRegistry<T>> selector, boolean holder) {
return new NetworkBufferTypeImpl.RegistryTypeType<>(selector); return new NetworkBufferTypeImpl.RegistryTypeType<>(selector, holder);
} }
// METADATA // METADATA
@ -104,6 +106,10 @@ public sealed interface NetworkBuffer permits NetworkBufferImpl {
return new NetworkBufferTypeImpl.LazyType<>(supplier); return new NetworkBufferTypeImpl.LazyType<>(supplier);
} }
static <T> @NotNull Type<T> TypedNBT(@NotNull BinaryTagSerializer<T> serializer) {
return new NetworkBufferTypeImpl.TypedNbtType<>(serializer);
}
<T> void write(@NotNull Type<T> type, @UnknownNullability T value) throws IndexOutOfBoundsException; <T> void write(@NotNull Type<T> type, @UnknownNullability T value) throws IndexOutOfBoundsException;
<T> @UnknownNullability T read(@NotNull Type<T> type) throws IndexOutOfBoundsException; <T> @UnknownNullability T read(@NotNull Type<T> type) throws IndexOutOfBoundsException;

View File

@ -13,6 +13,7 @@ import net.minestom.server.registry.ProtocolObject;
import net.minestom.server.registry.Registries; import net.minestom.server.registry.Registries;
import net.minestom.server.utils.Unit; import net.minestom.server.utils.Unit;
import net.minestom.server.utils.nbt.BinaryTagReader; import net.minestom.server.utils.nbt.BinaryTagReader;
import net.minestom.server.utils.nbt.BinaryTagSerializer;
import net.minestom.server.utils.nbt.BinaryTagWriter; import net.minestom.server.utils.nbt.BinaryTagWriter;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -708,6 +709,23 @@ interface NetworkBufferTypeImpl<T> extends NetworkBuffer.Type<T> {
} }
} }
record TypedNbtType<T>(@NotNull BinaryTagSerializer<T> nbtType) implements NetworkBufferTypeImpl<T> {
@Override
public void write(@NotNull NetworkBuffer buffer, T value) {
final Registries registries = impl(buffer).registries;
Check.stateCondition(registries == null, "Buffer does not have registries");
buffer.write(NBT, nbtType.write(new BinaryTagSerializer.ContextWithRegistries(registries), value));
}
@Override
public T read(@NotNull NetworkBuffer buffer) {
final Registries registries = impl(buffer).registries;
Check.stateCondition(registries == null, "Buffer does not have registries");
final BinaryTag tag = buffer.read(NBT);
return nbtType.read(new BinaryTagSerializer.ContextWithRegistries(registries), tag);
}
}
record TransformType<T, S>(@NotNull Type<T> parent, @NotNull Function<T, S> to, record TransformType<T, S>(@NotNull Type<T> parent, @NotNull Function<T, S> to,
@NotNull Function<S, T> from) implements NetworkBufferTypeImpl<S> { @NotNull Function<S, T> from) implements NetworkBufferTypeImpl<S> {
@Override @Override
@ -794,15 +812,18 @@ interface NetworkBufferTypeImpl<T> extends NetworkBuffer.Type<T> {
} }
record RegistryTypeType<T extends ProtocolObject>( record RegistryTypeType<T extends ProtocolObject>(
@NotNull Function<Registries, DynamicRegistry<T>> selector) implements NetworkBufferTypeImpl<DynamicRegistry.Key<T>> { @NotNull Function<Registries, DynamicRegistry<T>> selector,
boolean holder
) implements NetworkBufferTypeImpl<DynamicRegistry.Key<T>> {
@Override @Override
public void write(@NotNull NetworkBuffer buffer, DynamicRegistry.Key<T> value) { public void write(@NotNull NetworkBuffer buffer, DynamicRegistry.Key<T> value) {
final Registries registries = impl(buffer).registries; final Registries registries = impl(buffer).registries;
Check.stateCondition(registries == null, "Buffer does not have registries"); Check.stateCondition(registries == null, "Buffer does not have registries");
final DynamicRegistry<T> registry = selector.apply(registries); final DynamicRegistry<T> registry = selector.apply(registries);
// Painting variants may be sent in their entirety rather than a registry reference so the ID is offset by 1 to indicate this. // "Holder" references can either be a registry entry or the entire object itself. The id is zero if the
// entire object follows, but we only support registry objects currently so always offset by 1.
// FIXME: Support sending the entire registry object instead of an ID reference. // FIXME: Support sending the entire registry object instead of an ID reference.
final int id = registry.id().equals("minecraft:painting_variant") ? registry.getId(value) + 1 : registry.getId(value); final int id = registry.getId(value) + (holder ? 1 : 0);
Check.argCondition(id == -1, "Key is not registered: {0} > {1}", registry, value); Check.argCondition(id == -1, "Key is not registered: {0} > {1}", registry, value);
buffer.write(VAR_INT, id); buffer.write(VAR_INT, id);
} }
@ -812,13 +833,131 @@ interface NetworkBufferTypeImpl<T> extends NetworkBuffer.Type<T> {
final Registries registries = impl(buffer).registries; final Registries registries = impl(buffer).registries;
Check.stateCondition(registries == null, "Buffer does not have registries"); Check.stateCondition(registries == null, "Buffer does not have registries");
DynamicRegistry<T> registry = selector.apply(registries); DynamicRegistry<T> registry = selector.apply(registries);
final int id = buffer.read(VAR_INT); // See note above about holder references.
final int id = buffer.read(VAR_INT) + (holder ? -1 : 0);
final DynamicRegistry.Key<T> key = registry.getKey(id); final DynamicRegistry.Key<T> key = registry.getKey(id);
Check.argCondition(key == null, "No such ID in registry: {0} > {1}", registry, id); Check.argCondition(key == null, "No such ID in registry: {0} > {1}", registry, id);
return key; return key;
} }
} }
/**
* 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

@ -10,16 +10,15 @@ import net.minestom.server.network.player.GameProfile;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import java.util.EnumSet; import java.util.*;
import java.util.List; import java.util.function.UnaryOperator;
import java.util.UUID;
import static net.minestom.server.network.NetworkBuffer.*; import static net.minestom.server.network.NetworkBuffer.*;
public record PlayerInfoUpdatePacket( public record PlayerInfoUpdatePacket(
@NotNull EnumSet<@NotNull Action> actions, @NotNull EnumSet<@NotNull Action> actions,
@NotNull List<@NotNull Entry> entries @NotNull List<@NotNull Entry> entries
) implements ServerPacket.Play { ) implements ServerPacket.Play, ServerPacket.ComponentHolding {
public static final int MAX_ENTRIES = 1024; public static final int MAX_ENTRIES = 1024;
public PlayerInfoUpdatePacket(@NotNull Action action, @NotNull Entry entry) { public PlayerInfoUpdatePacket(@NotNull Action action, @NotNull Entry entry) {
@ -46,6 +45,33 @@ public record PlayerInfoUpdatePacket(
} }
}; };
@Override
public @NotNull Collection<Component> components() {
final List<Component> components = new ArrayList<>();
for (final Entry entry : entries) {
if (entry.displayName() == null) continue;
components.add(entry.displayName());
}
return components;
}
@Override
public @NotNull ServerPacket copyWithOperator(@NotNull UnaryOperator<Component> operator) {
final List<Entry> newEntries = new ArrayList<>();
for (final Entry entry : entries) {
final Component displayName = entry.displayName();
if (displayName != null) {
newEntries.add(new Entry(entry.uuid, entry.username,
entry.properties, entry.listed, entry.latency,
entry.gameMode, operator.apply(displayName),
entry.chatSession, entry.listOrder));
} else {
newEntries.add(entry);
}
}
return new PlayerInfoUpdatePacket(actions, newEntries);
}
public record Entry(UUID uuid, String username, List<Property> properties, public record Entry(UUID uuid, String username, List<Property> properties,
boolean listed, int latency, GameMode gameMode, boolean listed, int latency, GameMode gameMode,
@Nullable Component displayName, @Nullable ChatSession chatSession, @Nullable Component displayName, @Nullable ChatSession chatSession,

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

@ -137,6 +137,10 @@ public final class Registry {
return new JukeboxSongEntry(namespace, main, null); return new JukeboxSongEntry(namespace, main, null);
} }
public static GameEventEntry gameEventEntry(String namespace, Properties properties) {
return new GameEventEntry(namespace, properties, null);
}
public static @NotNull InputStream loadRegistryFile(@NotNull Resource resource) throws IOException { public static @NotNull InputStream loadRegistryFile(@NotNull Resource resource) throws IOException {
// 1. Try to load from jar resources // 1. Try to load from jar resources
InputStream resourceStream = Registry.class.getClassLoader().getResourceAsStream(resource.name); InputStream resourceStream = Registry.class.getClassLoader().getResourceAsStream(resource.name);
@ -245,6 +249,7 @@ public final class Registry {
ENTITY_TYPE_TAGS("tags/entity_type.json"), ENTITY_TYPE_TAGS("tags/entity_type.json"),
FLUID_TAGS("tags/fluid.json"), FLUID_TAGS("tags/fluid.json"),
GAMEPLAY_TAGS("tags/game_event.json"), GAMEPLAY_TAGS("tags/game_event.json"),
GAME_EVENTS("game_events.json"),
ITEM_TAGS("tags/item.json"), ITEM_TAGS("tags/item.json"),
ENCHANTMENT_TAGS("tags/enchantment.json"), ENCHANTMENT_TAGS("tags/enchantment.json"),
BIOME_TAGS("tags/biome.json"), BIOME_TAGS("tags/biome.json"),
@ -270,6 +275,12 @@ public final class Registry {
} }
} }
public record GameEventEntry(NamespaceID namespace, Properties main, Properties custom) implements Entry {
public GameEventEntry(String namespace, Properties main, Properties custom) {
this(NamespaceID.from(namespace), main, custom);
}
}
public static final class BlockEntry implements Entry { public static final class BlockEntry implements Entry {
private final NamespaceID namespace; private final NamespaceID namespace;
private final int id; private final int id;

View File

@ -5,7 +5,7 @@ import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.entity.LivingEntity; import net.minestom.server.entity.LivingEntity;
import net.minestom.server.entity.Player; import net.minestom.server.entity.Player;
import net.minestom.server.utils.PacketSendingUtils; import net.minestom.server.utils.PacketSendingUtils;
import net.minestom.server.utils.UniqueIdUtils; import net.minestom.server.utils.UUIDUtils;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import java.util.ArrayList; import java.util.ArrayList;
@ -158,7 +158,7 @@ public final class TeamManager {
public List<String> getPlayers(Team team) { public List<String> getPlayers(Team team) {
List<String> players = new ArrayList<>(); List<String> players = new ArrayList<>();
for (String member : team.getMembers()) { for (String member : team.getMembers()) {
boolean match = UniqueIdUtils.isUniqueId(member); boolean match = UUIDUtils.isUuid(member);
if (!match) players.add(member); if (!match) players.add(member);
} }
@ -176,7 +176,7 @@ public final class TeamManager {
public List<String> getEntities(Team team) { public List<String> getEntities(Team team) {
List<String> entities = new ArrayList<>(); List<String> entities = new ArrayList<>();
for (String member : team.getMembers()) { for (String member : team.getMembers()) {
boolean match = UniqueIdUtils.isUniqueId(member); boolean match = UUIDUtils.isUuid(member);
if (match) entities.add(member); if (match) entities.add(member);
} }

View File

@ -97,7 +97,7 @@ public final class SnapshotImpl {
final Section section = sections[globalToChunk(y) - minSection]; final Section section = sections[globalToChunk(y) - minSection];
final int blockStateId = section.blockPalette() final int blockStateId = section.blockPalette()
.get(globalToSectionRelative(x), globalToSectionRelative(y), globalToSectionRelative(z)); .get(globalToSectionRelative(x), globalToSectionRelative(y), globalToSectionRelative(z));
return Objects.requireNonNullElse(Block.fromStateId((short) blockStateId), Block.AIR); return Objects.requireNonNullElse(Block.fromStateId(blockStateId), Block.AIR);
} }
@Override @Override

View File

@ -5,7 +5,7 @@ import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer; import net.kyori.adventure.text.serializer.gson.GsonComponentSerializer;
import net.minestom.server.ServerFlag; import net.minestom.server.ServerFlag;
import net.minestom.server.item.ItemStack; import net.minestom.server.item.ItemStack;
import net.minestom.server.utils.UniqueIdUtils; import net.minestom.server.utils.UUIDUtils;
import java.util.function.Function; import java.util.function.Function;
@ -23,7 +23,7 @@ final class Serializers {
static final Entry<String, StringBinaryTag> STRING = new Entry<>(BinaryTagTypes.STRING, StringBinaryTag::value, StringBinaryTag::stringBinaryTag); static final Entry<String, StringBinaryTag> STRING = new Entry<>(BinaryTagTypes.STRING, StringBinaryTag::value, StringBinaryTag::stringBinaryTag);
static final Entry<BinaryTag, BinaryTag> NBT_ENTRY = new Entry<>(null, Function.identity(), Function.identity()); static final Entry<BinaryTag, BinaryTag> NBT_ENTRY = new Entry<>(null, Function.identity(), Function.identity());
static final Entry<java.util.UUID, IntArrayBinaryTag> UUID = new Entry<>(BinaryTagTypes.INT_ARRAY, UniqueIdUtils::fromNbt, UniqueIdUtils::toNbt); static final Entry<java.util.UUID, IntArrayBinaryTag> UUID = new Entry<>(BinaryTagTypes.INT_ARRAY, UUIDUtils::fromNbt, UUIDUtils::toNbt);
static final Entry<ItemStack, CompoundBinaryTag> ITEM = new Entry<>(BinaryTagTypes.COMPOUND, ItemStack::fromItemNBT, ItemStack::toItemNBT); static final Entry<ItemStack, CompoundBinaryTag> ITEM = new Entry<>(BinaryTagTypes.COMPOUND, ItemStack::fromItemNBT, ItemStack::toItemNBT);
static final Entry<Component, StringBinaryTag> COMPONENT = new Entry<>(BinaryTagTypes.STRING, input -> GsonComponentSerializer.gson().deserialize(input.value()), static final Entry<Component, StringBinaryTag> COMPONENT = new Entry<>(BinaryTagTypes.STRING, input -> GsonComponentSerializer.gson().deserialize(input.value()),
component -> StringBinaryTag.stringBinaryTag(GsonComponentSerializer.gson().serialize(component))); component -> StringBinaryTag.stringBinaryTag(GsonComponentSerializer.gson().serialize(component)));

View File

@ -11,7 +11,7 @@ import java.util.regex.Pattern;
* An utilities class for {@link UUID}. * An utilities class for {@link UUID}.
*/ */
@ApiStatus.Internal @ApiStatus.Internal
public final class UniqueIdUtils { public final class UUIDUtils {
public static final Pattern UNIQUE_ID_PATTERN = Pattern.compile("\\b[0-9a-f]{8}\\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\\b[0-9a-f]{12}\\b"); public static final Pattern UNIQUE_ID_PATTERN = Pattern.compile("\\b[0-9a-f]{8}\\b-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-\\b[0-9a-f]{12}\\b");
/** /**
@ -20,8 +20,8 @@ public final class UniqueIdUtils {
* @param input The input string to be checked * @param input The input string to be checked
* @return {@code true} if the input an unique identifier, otherwise {@code false} * @return {@code true} if the input an unique identifier, otherwise {@code false}
*/ */
public static boolean isUniqueId(String input) { public static boolean isUuid(String input) {
return input.matches(UNIQUE_ID_PATTERN.pattern()); return UNIQUE_ID_PATTERN.matcher(input).matches();
} }
public static @NotNull UUID fromNbt(@NotNull IntArrayBinaryTag tag) { public static @NotNull UUID fromNbt(@NotNull IntArrayBinaryTag tag) {

View File

@ -6,17 +6,28 @@ import org.jetbrains.annotations.ApiStatus;
import java.util.Arrays; import java.util.Arrays;
/**
* Allows to limit operations with recently operated chunks
* <p>
* {@link ChunkUpdateLimitChecker#historySize} defines how many last chunks will be remembered
* to skip operations with them via {@link ChunkUpdateLimitChecker#addToHistory(Chunk)} returning {@code false}
*/
@ApiStatus.Internal @ApiStatus.Internal
public final class ChunkUpdateLimitChecker { public final class ChunkUpdateLimitChecker {
private final int historySize; private final int historySize;
private final long[] chunkHistory; private final long[] chunkHistory;
public ChunkUpdateLimitChecker(int historySize) { public ChunkUpdateLimitChecker(int historySize) {
this.historySize = historySize; this.historySize = Math.max(0, historySize);
this.chunkHistory = new long[historySize]; this.chunkHistory = new long[this.historySize];
this.clearHistory(); this.clearHistory();
} }
public boolean isEnabled() {
return historySize > 0;
}
/** /**
* Adds the chunk to the history * Adds the chunk to the history
* *
@ -24,14 +35,19 @@ public final class ChunkUpdateLimitChecker {
* @return {@code true} if it's a new chunk in the history * @return {@code true} if it's a new chunk in the history
*/ */
public boolean addToHistory(Chunk chunk) { public boolean addToHistory(Chunk chunk) {
if (!isEnabled()) {
return true;
}
final long index = CoordConversion.chunkIndex(chunk.getChunkX(), chunk.getChunkZ()); final long index = CoordConversion.chunkIndex(chunk.getChunkX(), chunk.getChunkZ());
boolean result = true; boolean result = true;
final int lastIndex = historySize - 1; final int lastIndex = historySize - 1;
for (int i = 0; i < lastIndex; i++) { for (int i = 0; i <= lastIndex; i++) {
if (chunkHistory[i] == index) { if (chunkHistory[i] == index) {
result = false; result = false;
} }
chunkHistory[i] = chunkHistory[i + 1]; if (i != lastIndex) {
chunkHistory[i] = chunkHistory[i + 1];
}
} }
chunkHistory[lastIndex] = index; chunkHistory[lastIndex] = index;
return result; return result;

View File

@ -14,7 +14,7 @@ import net.minestom.server.registry.DynamicRegistry;
import net.minestom.server.registry.ProtocolObject; import net.minestom.server.registry.ProtocolObject;
import net.minestom.server.registry.Registries; import net.minestom.server.registry.Registries;
import net.minestom.server.utils.NamespaceID; import net.minestom.server.utils.NamespaceID;
import net.minestom.server.utils.UniqueIdUtils; import net.minestom.server.utils.UUIDUtils;
import net.minestom.server.utils.Unit; import net.minestom.server.utils.Unit;
import net.minestom.server.utils.validate.Check; import net.minestom.server.utils.validate.Check;
import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.ApiStatus;
@ -257,7 +257,7 @@ public interface BinaryTagSerializer<T> {
BinaryTagSerializer<UUID> UUID = new BinaryTagSerializer<>() { BinaryTagSerializer<UUID> UUID = new BinaryTagSerializer<>() {
@Override @Override
public @NotNull BinaryTag write(java.util.@NotNull UUID value) { public @NotNull BinaryTag write(java.util.@NotNull UUID value) {
return UniqueIdUtils.toNbt(value); return UUIDUtils.toNbt(value);
} }
@Override @Override
@ -265,7 +265,7 @@ public interface BinaryTagSerializer<T> {
if (!(tag instanceof IntArrayBinaryTag intArrayTag)) { if (!(tag instanceof IntArrayBinaryTag intArrayTag)) {
throw new IllegalArgumentException("unexpected uuid type: " + tag.type()); throw new IllegalArgumentException("unexpected uuid type: " + tag.type());
} }
return UniqueIdUtils.fromNbt(intArrayTag); return UUIDUtils.fromNbt(intArrayTag);
} }
}; };

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

@ -124,6 +124,33 @@ public class EventNodeTest {
assertTrue(result1.get(), "Recursive1 should be called due to the RecursiveEvent interface"); assertTrue(result1.get(), "Recursive1 should be called due to the RecursiveEvent interface");
} }
@Test
public void testRecursiveChild() {
var called1 = new AtomicBoolean(false);
var called2 = new AtomicBoolean(false);
var child1 = EventNode.all("child1");
var child2 = EventNode.all("child2");
child1.addListener(Recursive1.class, event -> called1.set(true));
child2.addListener(Recursive1.class, event -> called2.set(true));
var node = EventNode.all("main");
node.addChild(child1);
node.call(new Recursive2());
assertTrue(called1.get());
assertFalse(called2.get());
called1.set(false);
node.removeChild(child1);
node.addChild(child2);
node.call(new Recursive2());
assertFalse(called1.get());
assertTrue(called2.get());
}
// FIXME: nodes are currently unable to retrieve sub handles // FIXME: nodes are currently unable to retrieve sub handles
//@Test //@Test
//public void recursiveSuper() { //public void recursiveSuper() {

View File

@ -99,7 +99,7 @@ public class InstanceUnregisterIntegrationTest {
private void tmp(InstanceContainer instanceContainer) { private void tmp(InstanceContainer instanceContainer) {
instanceContainer.eventNode().addListener(InstanceTickEvent.class, (e) -> { instanceContainer.eventNode().addListener(InstanceTickEvent.class, (e) -> {
var uuid = instanceContainer.getUniqueId(); var uuid = instanceContainer.getUuid();
}); });
} }
} }

View File

@ -0,0 +1,17 @@
package net.minestom.server.instance.palette;
import org.junit.jupiter.api.Test;
public class PaletteIndirectTest {
@Test
public void constructor() {
var palette = new PaletteIndirect(16, 8, (byte) 4);
palette.set(0, 0, 1, 1);
var otherPalette = new PaletteIndirect(palette.dimension(), palette.maxBitsPerEntry(), (byte) palette.bitsPerEntry(), palette.count(), palette.paletteToValueList.toIntArray(), palette.values);
palette.getAll((x, y, z, value) -> {
assert value == otherPalette.get(x, y, z);
});
}
}

View File

@ -1,5 +1,6 @@
package net.minestom.server.item.component; package net.minestom.server.item.component;
import net.minestom.server.MinecraftServer;
import net.minestom.server.component.DataComponent; import net.minestom.server.component.DataComponent;
import net.minestom.server.item.ItemComponent; import net.minestom.server.item.ItemComponent;
import net.minestom.server.network.NetworkBuffer; import net.minestom.server.network.NetworkBuffer;
@ -25,6 +26,10 @@ public class UnitTest extends AbstractItemComponentTest<Unit> {
ItemComponent.GLIDER ItemComponent.GLIDER
); );
static {
MinecraftServer.init();
}
@Override @Override
protected @NotNull DataComponent<Unit> component() { protected @NotNull DataComponent<Unit> component() {
return UNIT_COMPONENTS.getFirst(); return UNIT_COMPONENTS.getFirst();
@ -46,7 +51,7 @@ public class UnitTest extends AbstractItemComponentTest<Unit> {
// Try to write as a Unit and if it fails we can ignore that type // Try to write as a Unit and if it fails we can ignore that type
try { try {
//noinspection unchecked //noinspection unchecked
((DataComponent<Unit>) component).write(NetworkBuffer.resizableBuffer(), Unit.INSTANCE); ((DataComponent<Unit>) component).write(NetworkBuffer.resizableBuffer(MinecraftServer.process()), Unit.INSTANCE);
} catch (ClassCastException ignored) { } catch (ClassCastException ignored) {
continue; continue;
} }

View File

@ -0,0 +1,106 @@
package net.minestom.server.listener;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.PlayerHand;
import net.minestom.server.entity.attribute.Attribute;
import net.minestom.server.event.player.PlayerEntityInteractEvent;
import net.minestom.server.instance.Instance;
import net.minestom.server.network.packet.client.play.ClientInteractEntityPacket;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@EnvTest
public class UseEntityListenerTest {
private Player player;
private Entity targetEntity;
private boolean eventWasCalled;
@BeforeEach
public void setup(Env env) {
Instance instance = env.createFlatInstance();
player = env.createPlayer(instance, new Pos(0, 0, 0));
player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(5.0);
targetEntity = new Entity(EntityType.SLIME);
targetEntity.setInstance(instance, new Pos(2, 0, 2)).join();
eventWasCalled = false;
player.eventNode().addListener(PlayerEntityInteractEvent.class, event -> {
if (event.getPlayer().equals(player) && event.getTarget().equals(targetEntity)) {
eventWasCalled = true;
}
});
}
@Test
public void testInteractionWithinRange() {
ClientInteractEntityPacket packet = new ClientInteractEntityPacket(
targetEntity.getEntityId(),
new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN),
false
);
UseEntityListener.useEntityListener(packet, player);
assertTrue(eventWasCalled, "Expected PlayerEntityInteractEvent to be called for nearby target");
}
@Test
public void testInteractionOutOfRange() {
player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(1.0);
targetEntity.teleport(new Pos(10, 0, 10)).join();
ClientInteractEntityPacket packet = new ClientInteractEntityPacket(
targetEntity.getEntityId(),
new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN),
false
);
eventWasCalled = false;
UseEntityListener.useEntityListener(packet, player);
assertFalse(eventWasCalled, "Expected PlayerEntityInteractEvent NOT to be called for out-of-range target");
}
@Test
public void testInteractionConsideringHitboxAndEyePosition() {
player.getAttribute(Attribute.ENTITY_INTERACTION_RANGE).setBaseValue(1.5);
targetEntity.teleport(new Pos(1.6, 0, 0)).join();
ClientInteractEntityPacket packet = new ClientInteractEntityPacket(
targetEntity.getEntityId(),
new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN),
false
);
eventWasCalled = false;
UseEntityListener.useEntityListener(packet, player);
assertTrue(eventWasCalled, "Expected PlayerEntityInteractEvent to be called considering hitbox size and eye position");
}
@Test
public void testInteractionConsideringEyeHeight() {
player.teleport(new Pos(0, 1.6, 0)).join();
targetEntity.teleport(new Pos(0, 1.6, 2)).join();
ClientInteractEntityPacket packet = new ClientInteractEntityPacket(
targetEntity.getEntityId(),
new ClientInteractEntityPacket.InteractAt(0, 0, 0, PlayerHand.MAIN),
false
);
eventWasCalled = false;
UseEntityListener.useEntityListener(packet, player);
assertTrue(eventWasCalled, "Expected PlayerEntityInteractEvent to be called considering eye height");
}
}

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);

View File

@ -0,0 +1,34 @@
package net.minestom.server.utils;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
class UUIDUtilsTest {
private static final UUID TEST_UUID = UUID.fromString("d2ac7139-76a6-435b-b659-7852d34dd7a3");
private static final int[] TEST_INT_ARRAY = new int[]{
0xd2ac7139,
0x76a6435b,
0xb6597852,
0xd34dd7a3
};
@Test
void isUuid() {
assertTrue(UUIDUtils.isUuid("d2ac7139-76a6-435b-b659-7852d34dd7a3"));
assertFalse(UUIDUtils.isUuid("This is not a UUID"));
assertFalse(UUIDUtils.isUuid("d2acL139-76a6-435b-b659-7852d34dd7a3"));
}
@Test
void uuidToIntArray() {
assertArrayEquals(TEST_INT_ARRAY, UUIDUtils.uuidToIntArray(TEST_UUID));
}
@Test
void intArrayToUuid() {
assertEquals(TEST_UUID, UUIDUtils.intArrayToUuid(TEST_INT_ARRAY));
}
}

View File

@ -0,0 +1,54 @@
package net.minestom.server.utils.chunk;
import net.minestom.server.instance.DynamicChunk;
import net.minestom.testing.Env;
import net.minestom.testing.EnvTest;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
@EnvTest
public class ChunkUpdateLimitCheckerTest {
@Test
public void testHistory(Env env) {
var instance = env.createFlatInstance();
var limiter = new ChunkUpdateLimitChecker(3);
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1)));
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 2)));
// history : 0, 1, 2
assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
// history : 1, 2, 0
assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 1)));
// history : 2, 0, 1
assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 2)));
// history : 0, 1, 2
assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 2)));
// history : 1, 2, 2
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
}
@Test
public void testOneSlotHistory(Env env) {
var instance = env.createFlatInstance();
var limiter = new ChunkUpdateLimitChecker(1);
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
assertFalse(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1)));
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
}
@Test
public void testDisabling(Env env) {
var instance = env.createFlatInstance();
var limiter = new ChunkUpdateLimitChecker(0);
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 0)));
assertTrue(limiter.addToHistory(new DynamicChunk(instance, 0, 1)));
}
}