Compare commits

...
Author SHA1 Message Date
david 8f1fad8687 No API records 2026-10-25 10:38:10 +01:00
david 431b4245ea Rebase 2026-10-25 10:22:03 +01:00
david 8f5f29305d AT instead of patch 2026-10-25 10:22:03 +01:00
david 4e8df4a990 Not experimental anymore :) 2026-10-25 10:22:03 +01:00
david 3612df6d9c Add world clock API 2026-10-25 10:22:00 +01:00
Creeam e3ba7b1dc8 Regen missing worldgen settings file from server.properties (#14233) 2026-10-24 10:18:07 -03:00
metabrix 05c2798ec0 Fix cancelled running scheduled task state (#14109) 2026-10-24 10:13:14 -03:00
TiggerTakkyTTTandDoc 02378ab582 Fire PrepareItemCraftEvent for Crafter preview (#14320)
Co-authored-by: Doc <git@mrdoc.dev>
2026-10-24 09:52:52 -03:00
Warrior 67912d0827 [ci skip] Re-enable using JBR for local run tasks again (#14318) 2026-10-24 09:13:46 -03:00
personalized-advertising e8a1f8fd68 Configurable plugin message channel limit (#14160) 2026-10-24 09:07:27 -03:00
personalized-advertising 1b9bf5c8e9 Add EntityPotentSulfurLaunchEvent (#14237) 2026-10-23 10:47:23 -03:00
Marco N. 325f9071f0 Clear scoreboard team entries on unregister (#14159) 2026-10-23 10:21:43 -03:00
PedroandWarrior d86d427405 Call PlayerBucketEmptyEvent for Sulfur Cube in Bucket (#14150)
Co-authored-by: Warrior <50800980+Warriorrrr@users.noreply.github.com>
2026-10-23 09:45:41 -03:00
Gotze 2f1ce01e87 Improve swing hand API to respect item ATTACK_ANIMATION component (#14363) 2026-10-23 09:38:43 -03:00
Bjarne Koll 0d9bf08a23 [ci skip] Fix broken test plugin refs 2026-10-22 18:10:53 +01:00
Mickey42302 e65c0765c6 Add permissions for reset and sequence arguments in /random (#14128) 2026-10-22 15:30:22 +02:00
H. Boyd ac9a1b2c72 Make Color implement adventure ARGBLike (#14349) 2026-10-22 09:59:01 -03:00
Strokkur24andJason Penilla 0db5c93a3d Add helper methods for vanilla-style command feedback (#14331)
Co-authored-by: Jason Penilla <11360596+jpenilla@users.noreply.github.com>
2026-10-22 09:54:34 -03:00
Shane Freeder 2d602d68c1 Split TreeType.POPLAR into per-color poplar tree types (#14416) 2026-10-21 14:12:43 +01:00
TiggerTakkyTTT 2766dd30b1 Add API for explorer maps using specific structures (#14290) 2026-10-21 09:31:41 -03:00
Warrior 03af8b4291 Fix shutdown thread deadlock if it tries to close async (#14410) 2026-10-20 21:49:26 +00:00
Jason Penilla 45fac22871 Sync latest Moonrise changes (#14409) 2026-10-20 21:41:39 +00:00
86 changed files with 2500 additions and 1353 deletions

No files matched your search

+1
View File
@@ -654,6 +654,7 @@ public net.minecraft.world.level.storage.SavedDataStorage cache
public net.minecraft.world.level.storage.TagValueInput input
public net.minecraft.world.level.storage.loot.providers.number.ints.ResolvableInt$Reference getProvider(Lnet/minecraft/world/level/storage/loot/LootContext;)Ljava/util/Optional;
public net.minecraft.world.scores.criteria.ObjectiveCriteria CRITERIA_CACHE
public net.minecraft.world.timeline.Timeline tracks
public-f net.minecraft.server.MinecraftServer storageSource
public-f net.minecraft.server.ServerAdvancementManager advancements
public-f net.minecraft.server.dedicated.DedicatedServer serverLinks
@@ -0,0 +1,68 @@
package io.papermc.paper.registry.keys;
import static net.kyori.adventure.key.Key.key;
import io.papermc.paper.annotation.GeneratedClass;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.TypedKey;
import io.papermc.paper.world.Timeline;
import net.kyori.adventure.key.Key;
import org.jspecify.annotations.NullMarked;
/**
* Vanilla keys for {@link RegistryKey#TIMELINE}.
*
* @apiNote The fields provided here are a direct representation of
* what is available from the vanilla game source. They may be
* changed (including removals) on any Minecraft version
* bump, so cross-version compatibility is not provided on the
* same level as it is on most of the other API.
*/
@SuppressWarnings({
"unused",
"SpellCheckingInspection"
})
@NullMarked
@GeneratedClass
public final class TimelineKeys {
/**
* {@code minecraft:day}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TypedKey<Timeline> DAY = create(key("day"));
/**
* {@code minecraft:early_game}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TypedKey<Timeline> EARLY_GAME = create(key("early_game"));
/**
* {@code minecraft:moon}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TypedKey<Timeline> MOON = create(key("moon"));
/**
* {@code minecraft:villager_schedule}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TypedKey<Timeline> VILLAGER_SCHEDULE = create(key("villager_schedule"));
private TimelineKeys() {
}
/**
* Creates a typed key for {@link Timeline} in the registry {@code minecraft:timeline}.
*
* @param key the value's key in the registry
* @return a new typed key
*/
public static TypedKey<Timeline> create(final Key key) {
return TypedKey.create(RegistryKey.TIMELINE, key);
}
}
@@ -0,0 +1,54 @@
package io.papermc.paper.registry.keys;
import static net.kyori.adventure.key.Key.key;
import io.papermc.paper.annotation.GeneratedClass;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.TypedKey;
import io.papermc.paper.world.WorldClock;
import net.kyori.adventure.key.Key;
import org.jspecify.annotations.NullMarked;
/**
* Vanilla keys for {@link RegistryKey#WORLD_CLOCK}.
*
* @apiNote The fields provided here are a direct representation of
* what is available from the vanilla game source. They may be
* changed (including removals) on any Minecraft version
* bump, so cross-version compatibility is not provided on the
* same level as it is on most of the other API.
*/
@SuppressWarnings({
"unused",
"SpellCheckingInspection"
})
@NullMarked
@GeneratedClass
public final class WorldClockKeys {
/**
* {@code minecraft:overworld}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TypedKey<WorldClock> OVERWORLD = create(key("overworld"));
/**
* {@code minecraft:the_end}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TypedKey<WorldClock> THE_END = create(key("the_end"));
private WorldClockKeys() {
}
/**
* Creates a typed key for {@link WorldClock} in the registry {@code minecraft:world_clock}.
*
* @param key the value's key in the registry
* @return a new typed key
*/
public static TypedKey<WorldClock> create(final Key key) {
return TypedKey.create(RegistryKey.WORLD_CLOCK, key);
}
}
@@ -0,0 +1,68 @@
package io.papermc.paper.registry.keys.tags;
import static net.kyori.adventure.key.Key.key;
import io.papermc.paper.annotation.GeneratedClass;
import io.papermc.paper.registry.RegistryKey;
import io.papermc.paper.registry.tag.TagKey;
import io.papermc.paper.world.Timeline;
import net.kyori.adventure.key.Key;
import org.jspecify.annotations.NullMarked;
/**
* Vanilla tag keys for {@link RegistryKey#TIMELINE}.
*
* @apiNote The fields provided here are a direct representation of
* what is available from the vanilla game source. They may be
* changed (including removals) on any Minecraft version
* bump, so cross-version compatibility is not provided on the
* same level as it is on most of the other API.
*/
@SuppressWarnings({
"unused",
"SpellCheckingInspection"
})
@NullMarked
@GeneratedClass
public final class TimelineTagKeys {
/**
* {@code #minecraft:in_end}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TagKey<Timeline> IN_END = create(key("in_end"));
/**
* {@code #minecraft:in_nether}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TagKey<Timeline> IN_NETHER = create(key("in_nether"));
/**
* {@code #minecraft:in_overworld}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TagKey<Timeline> IN_OVERWORLD = create(key("in_overworld"));
/**
* {@code #minecraft:universal}
*
* @apiNote This field is version-dependant and may be removed in future Minecraft versions
*/
public static final TagKey<Timeline> UNIVERSAL = create(key("universal"));
private TimelineTagKeys() {
}
/**
* Creates a tag key for {@link Timeline} in the registry {@code minecraft:timeline}.
*
* @param key the tag key's key
* @return a new tag key
*/
public static TagKey<Timeline> create(final Key key) {
return TagKey.create(RegistryKey.TIMELINE, key);
}
}
@@ -1,8 +1,14 @@
package io.papermc.paper.command.brigadier;
import com.google.common.base.Preconditions;
import com.mojang.brigadier.RedirectModifier;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import com.mojang.brigadier.tree.CommandNode;
import net.kyori.adventure.pointer.Pointered;
import net.kyori.adventure.text.ComponentLike;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.TagResolver;
import org.bukkit.GameRules;
import org.bukkit.Location;
import org.bukkit.command.CommandSender;
import org.bukkit.entity.Entity;
@@ -81,4 +87,123 @@ public interface CommandSourceStack {
* @see com.mojang.brigadier.builder.ArgumentBuilder#fork(CommandNode, RedirectModifier)
*/
CommandSourceStack withExecutor(Entity executor);
/**
* Sends a system message to the {@link #getExecutor()} if it is a {@link Player},
* otherwise sends a system message to the {@link #getSender()}.
*
* @param message the message to send
* @throws IllegalArgumentException if the message is null
*/
void sendReply(ComponentLike message);
/**
* Sends a system message with the MiniMessage format to the {@link #getExecutor()}
* if it is a {@link Player}, otherwise sends it to the {@link #getSender()}.
*
* <p>See <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a> and
* <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements/">MiniMessage Placeholders docs</a>
* for more information on the format.</p>
*
* <p>This method provides the target of the system message as a {@link net.kyori.adventure.pointer.Pointered}
* to the MiniMessage {@link MiniMessage#deserialize(String, Pointered, TagResolver...)} method.</p>
*
* @param message the MiniMessage message to send
* @param resolvers resolvers to use
* @throws IllegalArgumentException if the message is null
*/
default void sendRichReply(final String message, final TagResolver... resolvers) {
Preconditions.checkArgument(message != null, "message cannot be null.");
this.sendReply(MiniMessage.miniMessage().deserialize(
message,
this.getExecutor() instanceof final Player player ? player : this.getSender(),
resolvers
));
}
/**
* Sends a system message to the {@link #getSender()}, admins, and console indicating successful command execution
* according to vanilla semantics.
*
* <p>This currently includes checking for environments with suppressed output,
* {@link GameRules#SEND_COMMAND_FEEDBACK}, and {@link GameRules#LOG_ADMIN_COMMANDS}.</p>
*
* @param message the message to send
* @param allowInformingAdmins whether admins and console may be informed of this success
* @throws IllegalArgumentException if the message is null
*/
void sendSuccess(ComponentLike message, boolean allowInformingAdmins);
/**
* Sends a system message to the {@link #getSender()}, admins, and console indicating successful command execution
* according to vanilla semantics. This method informs admins and the console of this success.
*
* <p>This currently includes checking for environments with suppressed output,
* {@link GameRules#SEND_COMMAND_FEEDBACK}, and {@link GameRules#LOG_ADMIN_COMMANDS}.</p>
*
* @param message the message to send
* @see #sendSuccess(ComponentLike, boolean) to disable if admins and console should be informed
* @throws IllegalArgumentException if the message is null
*/
default void sendSuccess(final ComponentLike message) {
this.sendSuccess(message, true);
}
/**
* Sends a system message with the MiniMessage format to the {@link #getSender()}, admins, and console indicating
* successful command execution according to vanilla semantics. This method informs admins and the console of
* this success.
*
* <p>This currently includes checking for environments with suppressed output,
* {@link GameRules#SEND_COMMAND_FEEDBACK}, and {@link GameRules#LOG_ADMIN_COMMANDS}.</p>
*
* <p>See <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a> and
* <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements/">MiniMessage Placeholders docs</a>
* for more information on the format.</p>
*
* <p>This method provides the {@link CommandSender} of this command as a {@link net.kyori.adventure.pointer.Pointered}
* to the MiniMessage {@link MiniMessage#deserialize(String, Pointered, TagResolver...)} method.</p>
*
* @param message the MiniMessage message to send
* @param resolvers resolvers to use
* @see #sendSuccess(ComponentLike, boolean) to disable if admins and console should be informed
* @throws IllegalArgumentException if the message is null
*/
default void sendRichSuccess(final String message, final TagResolver... resolvers) {
Preconditions.checkArgument(message != null, "message cannot be null.");
this.sendSuccess(MiniMessage.miniMessage().deserialize(message, this.getSender(), resolvers), true);
}
/**
* Sends a system message indicating a failed command execution to the {@link #getSender()}.
* Does not apply red styling to the message as vanilla does to allow for custom failure message styling.
*
* <p>Respects vanilla semantics for accepting failure output and suppressed output environments.</p>
*
* @param message the message to send
* @throws IllegalArgumentException if the message is null
*/
void sendFailure(ComponentLike message);
/**
* Sends a system message with the MiniMessage format indicating a failed command execution to the{@link #getSender()}.
* Does not apply red styling to the message as vanilla does to allow for custom failure message styling.
*
* <p>Respects vanilla semantics for accepting failure output and suppressed output environments.</p>
*
* <p>See <a href="https://docs.papermc.io/adventure/minimessage/">MiniMessage docs</a> and
* <a href="https://docs.papermc.io/adventure/minimessage/dynamic-replacements/">MiniMessage Placeholders docs</a>
* for more information on the format.</p>
*
* <p>This method provides the {@link CommandSender} of this command as a {@link net.kyori.adventure.pointer.Pointered}
* to the MiniMessage {@link MiniMessage#deserialize(String, Pointered, TagResolver...)} method.</p>
*
* @param message the MiniMessage message to send
* @param resolvers resolvers to use
* @throws IllegalArgumentException if the message is null
*/
default void sendRichFailure(final String message, final TagResolver... resolvers) {
Preconditions.checkArgument(message != null, "message cannot be null.");
this.sendFailure(MiniMessage.miniMessage().deserialize(message, this.getSender(), resolvers));
}
}
@@ -0,0 +1,69 @@
package io.papermc.paper.event.entity;
import org.bukkit.Location;
import org.bukkit.block.Block;
import org.bukkit.entity.Entity;
import org.bukkit.event.HandlerList;
import org.bukkit.event.entity.EntityEvent;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* Called when an entity is launched upward by a Potent Sulfur geyser.
*/
@NullMarked
public class EntityPotentSulfurLaunchEvent extends EntityEvent {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final Block sulfurBlock;
private final Location sourceLocation;
private final Vector launchVelocity;
@ApiStatus.Internal
public EntityPotentSulfurLaunchEvent(
final Entity entity,
final Block sulfurBlock,
final Location sourceLocation,
final Vector launchVelocity
) {
super(entity);
this.sulfurBlock = sulfurBlock;
this.sourceLocation = sourceLocation;
this.launchVelocity = launchVelocity;
}
/**
* Gets the Potent Sulfur block.
*/
public Block getSulfurBlock() {
return this.sulfurBlock;
}
/**
* Gets the location where the geyser exits the water column.
*/
public Location getSourceLocation() {
return this.sourceLocation.clone();
}
/**
* Gets the velocity that will be added to the entity.
* <br>
* This value is read-only, changes made to it <b>will not</b> have any
* effect on the final launch velocity.
*/
public Vector getLaunchVelocity() {
return this.launchVelocity.clone();
}
@Override
public HandlerList getHandlers() {
return HANDLER_LIST;
}
public static HandlerList getHandlerList() {
return HANDLER_LIST;
}
}
@@ -5,6 +5,8 @@ import io.papermc.paper.datacomponent.DataComponentType;
import io.papermc.paper.dialog.Dialog;
import io.papermc.paper.entity.poi.PoiType;
import io.papermc.paper.registry.tag.TagKey;
import io.papermc.paper.world.Timeline;
import io.papermc.paper.world.WorldClock;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.KeyPattern;
import net.kyori.adventure.key.Keyed;
@@ -42,6 +44,7 @@ import org.bukkit.inventory.meta.trim.TrimPattern;
import org.bukkit.map.MapCursor;
import org.bukkit.potion.PotionEffectType;
import org.bukkit.potion.PotionType;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
import static io.papermc.paper.registry.RegistryKeyImpl.create;
@@ -269,6 +272,16 @@ public sealed interface RegistryKey<T> extends Keyed permits RegistryKeyImpl {
* @see io.papermc.paper.registry.keys.DialogKeys
*/
RegistryKey<Dialog> DIALOG = create("dialog");
/**
* Data-driven registry for world clocks.
* @see io.papermc.paper.registry.keys.WorldClockKeys
*/
RegistryKey<WorldClock> WORLD_CLOCK = create("world_clock");
/**
* Data-driven registry for timelines.
* @see io.papermc.paper.registry.keys.TimelineKeys
*/
RegistryKey<Timeline> TIMELINE = create("timeline");
/* ******************* *
@@ -0,0 +1,132 @@
package io.papermc.paper.registry.data;
import io.papermc.paper.registry.RegistryBuilder;
import io.papermc.paper.registry.TypedKey;
import io.papermc.paper.world.Timeline;
import io.papermc.paper.world.WorldClock;
import java.util.Map;
import java.util.OptionalInt;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.Range;
import static io.papermc.paper.util.BoundChecker.requireNonNegative;
/**
* A data-centric version-specific registry entry for the {@link Timeline} type.
*/
@ApiStatus.Experimental
@ApiStatus.NonExtendable
public interface TimelineRegistryEntry {
/**
* Gets the clock that drives this timeline.
*
* @return the clock
*/
@Contract(pure = true)
TypedKey<WorldClock> clock();
/**
* Gets the timeline period, if any.
*
* @return the timeline period
*/
@Contract(pure = true)
OptionalInt periodTicks();
/**
* Gets the time markers defined by this timeline.
*
* @return the time markers
*/
@Contract(pure = true)
Map<Key, TimeMarker> timeMarkers();
/**
* A mutable builder for the {@link TimelineRegistryEntry} plugins may change in applicable registry events.
* <p>
* The following values are required for each builder:
* <ul>
* <li>{@link #clock(TypedKey)}</li>
* </ul>
*/
@ApiStatus.NonExtendable
interface Builder extends TimelineRegistryEntry, RegistryBuilder<Timeline> {
/**
* Sets the clock that drives this timeline.
*
* @param clock the clock
* @return this builder
*/
@Contract(value = "_ -> this", mutates = "this")
Builder clock(TypedKey<WorldClock> clock);
/**
* Sets the timeline period.
*
* @param periodTicks the period in ticks
* @return this builder
*/
@Contract(value = "_ -> this", mutates = "this")
Builder periodTicks(@Range(from = 1, to = Integer.MAX_VALUE) int periodTicks);
/**
* Clears the timeline period.
*
* @return this builder
*/
@Contract(value = "-> this", mutates = "this")
Builder clearPeriodTicks();
/**
* Adds or replaces a time marker on this timeline.
*
* @param key the time marker key
* @param ticks the marker tick
* @param showInCommands whether the marker should be suggested in commands
* @return this builder
*/
@Contract(value = "_, _, _ -> this", mutates = "this")
Builder addTimeMarker(Key key, @Range(from = 0, to = Integer.MAX_VALUE) int ticks, boolean showInCommands);
}
/**
* A time marker defined by a timeline.
*/
@ApiStatus.NonExtendable
interface TimeMarker {
/**
* Gets the tick this marker occurs on.
*
* @return the marker tick
*/
@Range(from = 0, to = Integer.MAX_VALUE)
int ticks();
/**
* Gets whether this marker should be suggested in commands.
*
* @return whether the marker should be suggested in commands
*/
boolean showInCommands();
/**
* Creates a new time marker instance based on the passed values.
*
* @param ticks the tick this marker occurs on, as returned by {@link #ticks()}
* @param showInCommands whether the marker should be suggested in commands, as returned by {@link #showInCommands()}
* @return the created instance
*/
@Contract(value = "_, _ -> new", pure = true)
static TimeMarker of(@Range(from = 0, to = Integer.MAX_VALUE) final int ticks, final boolean showInCommands) {
record Impl(int ticks, boolean showInCommands) implements TimeMarker {
}
return new Impl(requireNonNegative(ticks, "ticks"), showInCommands);
}
}
}
@@ -0,0 +1,19 @@
package io.papermc.paper.registry.data;
import io.papermc.paper.registry.RegistryBuilder;
import io.papermc.paper.world.WorldClock;
import org.jetbrains.annotations.ApiStatus;
/**
* A data-centric version-specific registry entry for the {@link WorldClock} type.
*/
@ApiStatus.NonExtendable
public interface WorldClockRegistryEntry {
/**
* A mutable builder for the {@link WorldClockRegistryEntry} plugins may change in applicable registry events.
*/
@ApiStatus.NonExtendable
interface Builder extends WorldClockRegistryEntry, RegistryBuilder<WorldClock> {
}
}
@@ -17,11 +17,15 @@ import io.papermc.paper.registry.data.JukeboxSongRegistryEntry;
import io.papermc.paper.registry.data.PaintingVariantRegistryEntry;
import io.papermc.paper.registry.data.PigVariantRegistryEntry;
import io.papermc.paper.registry.data.SulfurCubeArchetypeRegistryEntry;
import io.papermc.paper.registry.data.TimelineRegistryEntry;
import io.papermc.paper.registry.data.TrimMaterialRegistryEntry;
import io.papermc.paper.registry.data.TrimPatternRegistryEntry;
import io.papermc.paper.registry.data.WolfVariantRegistryEntry;
import io.papermc.paper.registry.data.WorldClockRegistryEntry;
import io.papermc.paper.registry.data.ZombieNautilusVariantRegistryEntry;
import io.papermc.paper.registry.data.dialog.DialogRegistryEntry;
import io.papermc.paper.world.Timeline;
import io.papermc.paper.world.WorldClock;
import org.bukkit.Art;
import org.bukkit.GameEvent;
import org.bukkit.JukeboxSong;
@@ -68,6 +72,8 @@ public final class RegistryEvents {
public static final RegistryEventProvider<ZombieNautilus.Variant, ZombieNautilusVariantRegistryEntry.Builder> ZOMBIE_NAUTILUS_VARIANT = create(RegistryKey.ZOMBIE_NAUTILUS_VARIANT);
public static final RegistryEventProvider<SulfurCube.Archetype, SulfurCubeArchetypeRegistryEntry.Builder> SULFUR_CUBE_ARCHETYPE = create(RegistryKey.SULFUR_CUBE_ARCHETYPE);
public static final RegistryEventProvider<Dialog, DialogRegistryEntry.Builder> DIALOG = create(RegistryKey.DIALOG);
public static final RegistryEventProvider<WorldClock, WorldClockRegistryEntry.Builder> WORLD_CLOCK = create(RegistryKey.WORLD_CLOCK);
public static final RegistryEventProvider<Timeline, TimelineRegistryEntry.Builder> TIMELINE = create(RegistryKey.TIMELINE);
// End generate - RegistryEvents
private RegistryEvents() {
@@ -0,0 +1,31 @@
package io.papermc.paper.world;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.KeyPattern;
import org.bukkit.Keyed;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* Represents a timeline that can apply environment attributes or define time markers for a {@link WorldClock}.
*/
@NullMarked
@ApiStatus.NonExtendable
public interface Timeline extends Keyed {
// Start generate - Timeline
Timeline DAY = getTimeline("day");
Timeline EARLY_GAME = getTimeline("early_game");
Timeline MOON = getTimeline("moon");
Timeline VILLAGER_SCHEDULE = getTimeline("villager_schedule");
// End generate - Timeline
private static Timeline getTimeline(@KeyPattern.Value final String key) {
return RegistryAccess.registryAccess().getRegistry(RegistryKey.TIMELINE).getOrThrow(Key.key(Key.MINECRAFT_NAMESPACE, key));
}
}
@@ -0,0 +1,27 @@
package io.papermc.paper.world;
import io.papermc.paper.registry.RegistryAccess;
import io.papermc.paper.registry.RegistryKey;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.key.KeyPattern;
import org.bukkit.Keyed;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* Represents a world clock that tracks time for worlds and drives timelines.
*/
@NullMarked
@ApiStatus.NonExtendable
public interface WorldClock extends Keyed {
// Start generate - WorldClock
WorldClock OVERWORLD = getWorldClock("overworld");
WorldClock THE_END = getWorldClock("the_end");
// End generate - WorldClock
private static WorldClock getWorldClock(@KeyPattern.Value final String key) {
return RegistryAccess.registryAccess().getRegistry(RegistryKey.WORLD_CLOCK).getOrThrow(Key.key(Key.MINECRAFT_NAMESPACE, key));
}
}
+41 -4
View File
@@ -961,8 +961,7 @@ public final class Bukkit {
* Create a new explorer map targeting the closest nearby structure of a
* given {@link StructureType}.
* <br>
* This method uses implementation default values for radius and
* findUnexplored (usually 100, true).
* This method uses a radius of 100 and only searches for unexplored structures.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
@@ -986,8 +985,7 @@ public final class Bukkit {
* Create a new explorer map targeting the closest nearby structure of a
* given {@link org.bukkit.generator.structure.StructureType}.
* <br>
* This method uses implementation default values for radius and
* findUnexplored (usually 100, true).
* This method uses a radius of 100 and only searches for unexplored structures.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
@@ -1021,6 +1019,45 @@ public final class Bukkit {
public static @Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull org.bukkit.generator.structure.StructureType structureType, @NotNull org.bukkit.map.MapCursor.Type mapIcon, int radius, boolean findUnexplored) {
return server.createExplorerMap(world, location, structureType, mapIcon, radius, findUnexplored);
}
/**
* Create a new explorer map targeting the closest nearby specific
* {@link org.bukkit.generator.structure.Structure}.
* <br>
* This method uses a radius of 100 and only searches for unexplored structures.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
* @param structure the specific structure to find
* @param mapIcon the map icon to use on the map
* @return a newly created item stack or null if it can't find a location
*
* @see World#locateNearestStructure(org.bukkit.Location,
* org.bukkit.generator.structure.Structure, int, boolean)
*/
public static @Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull org.bukkit.generator.structure.Structure structure, @NotNull org.bukkit.map.MapCursor.Type mapIcon) {
return server.createExplorerMap(world, location, structure, mapIcon);
}
/**
* Create a new explorer map targeting the closest nearby specific
* {@link org.bukkit.generator.structure.Structure}.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
* @param structure the specific structure to find
* @param mapIcon the map icon to use on the map
* @param radius radius to search, see World#locateNearestStructure for more
* information
* @param findUnexplored whether to find unexplored structures
* @return the newly created item stack or null if it can't find a location
*
* @see World#locateNearestStructure(org.bukkit.Location,
* org.bukkit.generator.structure.Structure, int, boolean)
*/
public static @Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull org.bukkit.generator.structure.Structure structure, @NotNull org.bukkit.map.MapCursor.Type mapIcon, int radius, boolean findUnexplored) {
return server.createExplorerMap(world, location, structure, mapIcon, radius, findUnexplored);
}
// Paper end
/**
+131 -77
View File
@@ -6,10 +6,12 @@ import java.util.Arrays;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import net.kyori.adventure.util.ARGBLike;
import org.bukkit.configuration.serialization.ConfigurationSerializable;
import org.bukkit.configuration.serialization.SerializableAs;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.Range;
/**
* A container for a color palette. This class is immutable; the set methods
@@ -17,7 +19,7 @@ import org.jetbrains.annotations.Nullable;
* but subject to change.
*/
@SerializableAs("Color")
public final class Color implements ConfigurationSerializable {
public final class Color implements ConfigurationSerializable, ARGBLike {
private static final int BIT_MASK = 0xff;
private static final int DEFAULT_ALPHA = 255;
@@ -122,7 +124,12 @@ public final class Color implements ConfigurationSerializable {
* @throws IllegalArgumentException if any value is strictly {@literal >255 or <0}
*/
@NotNull
public static Color fromARGB(int alpha, int red, int green, int blue) throws IllegalArgumentException {
public static Color fromARGB(
final @Range(from = 0, to = 255) int alpha,
final @Range(from = 0, to = 255) int red,
final @Range(from = 0, to = 255) int green,
final @Range(from = 0, to = 255) int blue
) throws IllegalArgumentException {
return new Color(alpha, red, green, blue);
}
@@ -136,7 +143,11 @@ public final class Color implements ConfigurationSerializable {
* @throws IllegalArgumentException if any value is strictly {@literal >255 or <0}
*/
@NotNull
public static Color fromRGB(int red, int green, int blue) throws IllegalArgumentException {
public static Color fromRGB(
final @Range(from = 0, to = 255) int red,
final @Range(from = 0, to = 255) int green,
final @Range(from = 0, to = 255) int blue
) throws IllegalArgumentException {
return new Color(DEFAULT_ALPHA, red, green, blue);
}
@@ -150,7 +161,11 @@ public final class Color implements ConfigurationSerializable {
* @throws IllegalArgumentException if any value is strictly {@literal >255 or <0}
*/
@NotNull
public static Color fromBGR(int blue, int green, int red) throws IllegalArgumentException {
public static Color fromBGR(
final @Range(from = 0, to = 255) int blue,
final @Range(from = 0, to = 255) int green,
final @Range(from = 0, to = 255) int red
) throws IllegalArgumentException {
return new Color(DEFAULT_ALPHA, red, green, blue);
}
@@ -164,7 +179,7 @@ public final class Color implements ConfigurationSerializable {
* bits
*/
@NotNull
public static Color fromRGB(int rgb) throws IllegalArgumentException {
public static Color fromRGB(final int rgb) throws IllegalArgumentException {
Preconditions.checkArgument((rgb >> 24) == 0, "Extraneous data in: %s", rgb);
return fromRGB(rgb >> 16 & BIT_MASK, rgb >> 8 & BIT_MASK, rgb & BIT_MASK);
}
@@ -177,7 +192,7 @@ public final class Color implements ConfigurationSerializable {
* @return a new color object for specified values
*/
@NotNull
public static Color fromARGB(int argb) {
public static Color fromARGB(final int argb) {
return fromARGB(argb >> 24 & BIT_MASK, argb >> 16 & BIT_MASK, argb >> 8 & BIT_MASK, argb & BIT_MASK);
}
@@ -191,16 +206,16 @@ public final class Color implements ConfigurationSerializable {
* bits
*/
@NotNull
public static Color fromBGR(int bgr) throws IllegalArgumentException {
public static Color fromBGR(final int bgr) throws IllegalArgumentException {
Preconditions.checkArgument((bgr >> 24) == 0, "Extrenuous data in: %s", bgr);
return fromBGR(bgr >> 16 & BIT_MASK, bgr >> 8 & BIT_MASK, bgr & BIT_MASK);
}
private Color(int red, int green, int blue) {
private Color(final int red, final int green, final int blue) {
this(DEFAULT_ALPHA, red, green, blue);
}
private Color(int alpha, int red, int green, int blue) {
private Color(final int alpha, final int red, final int green, final int blue) {
Preconditions.checkArgument(alpha >= 0 && alpha <= 0xff, "Alpha[%s] is not between 0-255", alpha);
Preconditions.checkArgument(red >= 0 && red <= 0xff, "Red[%s] is not between 0-255", red);
Preconditions.checkArgument(green >= 0 && green <= 0xff, "Green[%s] is not between 0-255", green);
@@ -213,83 +228,123 @@ public final class Color implements ConfigurationSerializable {
}
/**
* Gets the alpha component
* Gets the alpha component.
*
* @return alpha component, from 0 to 255
*/
public int getAlpha() {
public @Range(from = 0, to = 255) int getAlpha() {
return BIT_MASK & alpha;
}
/**
* Creates a new Color object with specified component
* Gets the alpha component.
*
* @return alpha component, from 0 to 255
*/
@Override
public @Range(from = 0, to = 255) int alpha() {
return this.getAlpha();
}
/**
* Creates a new Color object with a specified component.
*
* @param alpha the alpha component, from 0 to 255
* @return a new color object with the alpha component
*/
@NotNull
public Color setAlpha(int alpha) {
return fromARGB(alpha, getRed(), getGreen(), getBlue());
public Color setAlpha(final @Range(from = 0, to = 255) int alpha) {
return fromARGB(alpha, this.getRed(), this.getGreen(), this.getBlue());
}
/**
* Gets the red component
* Gets the red component.
*
* @return red component, from 0 to 255
*/
public int getRed() {
return BIT_MASK & red;
public @Range(from = 0, to = 255) int getRed() {
return BIT_MASK & this.red;
}
/**
* Creates a new Color object with specified component
* Gets the red component.
*
* @return red component, from 0 to 255
*/
@Override
public @Range(from = 0, to = 255) int red() {
return this.getRed();
}
/**
* Creates a new Color object with a specified component.
*
* @param red the red component, from 0 to 255
* @return a new color object with the red component
*/
@NotNull
public Color setRed(int red) {
return fromARGB(getAlpha(), red, getGreen(), getBlue());
public Color setRed(final @Range(from = 0, to = 255) int red) {
return fromARGB(this.getAlpha(), red, this.getGreen(), this.getBlue());
}
/**
* Gets the green component
* Gets the green component.
*
* @return green component, from 0 to 255
*/
public int getGreen() {
return BIT_MASK & green;
public @Range(from = 0, to = 255) int getGreen() {
return BIT_MASK & this.green;
}
/**
* Creates a new Color object with specified component
* Gets the green component.
*
* @return green component, from 0 to 255
*/
@Override
public @Range(from = 0, to = 255) int green() {
return this.getGreen();
}
/**
* Creates a new Color object with a specified component.
*
* @param green the green component, from 0 to 255
* @return a new color object with the green component
*/
@NotNull
public Color setGreen(int green) {
return fromARGB(getAlpha(), getRed(), green, getBlue());
public Color setGreen(final @Range(from = 0, to = 255) int green) {
return fromARGB(this.getAlpha(), this.getRed(), green, this.getBlue());
}
/**
* Gets the blue component
* Gets the blue component.
*
* @return blue component, from 0 to 255
*/
public int getBlue() {
return BIT_MASK & blue;
public @Range(from = 0, to = 255) int getBlue() {
return BIT_MASK & this.blue;
}
/**
* Creates a new Color object with specified component
* Gets the blue component.
*
* @return blue component, from 0 to 255
*/
@Override
public @Range(from = 0, to = 255) int blue() {
return this.getBlue();
}
/**
* Creates a new Color object with a specified component.
*
* @param blue the blue component, from 0 to 255
* @return a new color object with the blue component
*/
@NotNull
public Color setBlue(int blue) {
return fromARGB(getAlpha(), getRed(), getGreen(), blue);
public Color setBlue(final @Range(from = 0, to = 255) int blue) {
return fromARGB(this.getAlpha(), this.getRed(), this.getGreen(), blue);
}
/**
@@ -298,7 +353,7 @@ public final class Color implements ConfigurationSerializable {
* @return An integer representation of this color, as 0xRRGGBB
*/
public int asRGB() {
return getRed() << 16 | getGreen() << 8 | getBlue();
return this.getRed() << 16 | this.getGreen() << 8 | this.getBlue();
}
/**
@@ -307,7 +362,7 @@ public final class Color implements ConfigurationSerializable {
* @return An integer representation of this color, as 0xAARRGGBB
*/
public int asARGB() {
return getAlpha() << 24 | getRed() << 16 | getGreen() << 8 | getBlue();
return this.getAlpha() << 24 | this.getRed() << 16 | this.getGreen() << 8 | this.getBlue();
}
/**
@@ -316,33 +371,33 @@ public final class Color implements ConfigurationSerializable {
* @return An integer representation of this color, as 0xBBGGRR
*/
public int asBGR() {
return getBlue() << 16 | getGreen() << 8 | getRed();
}
/**
* Creates a new color with its RGB components changed as if it was dyed
* with the colors passed in, replicating vanilla workbench dyeing
*
* @param colors The DyeColors to dye with
* @return A new color with the changed rgb components
*/
// TODO: Javadoc what this method does, not what it mimics. API != Implementation
@NotNull
public Color mixDyes(@NotNull DyeColor... colors) {
Preconditions.checkArgument(colors != null && Arrays.stream(colors).allMatch(Objects::nonNull), "DyeColor cannot be null or contain null values");
Color[] toPass = new Color[colors.length];
for (int i = 0; i < colors.length; i++) {
toPass[i] = colors[i].getColor();
}
return mixColors(toPass);
return this.getBlue() << 16 | this.getGreen() << 8 | this.getRed();
}
/**
* Creates a new color with its RGB components changed as if it was dyed
* with the colors passed in, replicating vanilla workbench dyeing.
*
* @param colors The DyeColors to dye with
* @return A new color with the changed rgb components
*/
// TODO: Javadoc what this method does, not what it mimics. API != Implementation
@NotNull
public Color mixDyes(final @NotNull DyeColor... colors) {
Preconditions.checkArgument(colors != null && Arrays.stream(colors).allMatch(Objects::nonNull), "DyeColor cannot be null or contain null values");
final Color[] toPass = new Color[colors.length];
for (int i = 0; i < colors.length; i++) {
toPass[i] = colors[i].getColor();
}
return this.mixColors(toPass);
}
/**
* Creates a new color with its RGB components changed as if it was dyed
* with the colors passed in, replicating vanilla workbench dyeing.
* <br>
* <b>Note that this method does not currently take into account alpha
* components.</b>
*
@@ -351,7 +406,7 @@ public final class Color implements ConfigurationSerializable {
*/
// TODO: Javadoc what this method does, not what it mimics. API != Implementation
@NotNull
public Color mixColors(@NotNull Color... colors) {
public Color mixColors(final @NotNull Color... colors) {
Preconditions.checkArgument(colors != null && Arrays.stream(colors).allMatch(Objects::nonNull), "Colors cannot be null");
int totalRed = this.getRed();
@@ -365,44 +420,43 @@ public final class Color implements ConfigurationSerializable {
totalMax += Math.max(Math.max(color.getRed(), color.getGreen()), color.getBlue());
}
float averageRed = totalRed / (colors.length + 1);
float averageGreen = totalGreen / (colors.length + 1);
float averageBlue = totalBlue / (colors.length + 1);
float averageMax = totalMax / (colors.length + 1);
final float averageRed = totalRed / (colors.length + 1);
final float averageGreen = totalGreen / (colors.length + 1);
final float averageBlue = totalBlue / (colors.length + 1);
final float averageMax = totalMax / (colors.length + 1);
float maximumOfAverages = Math.max(Math.max(averageRed, averageGreen), averageBlue);
float gainFactor = averageMax / maximumOfAverages;
final float maximumOfAverages = Math.max(Math.max(averageRed, averageGreen), averageBlue);
final float gainFactor = averageMax / maximumOfAverages;
return Color.fromRGB((int) (averageRed * gainFactor), (int) (averageGreen * gainFactor), (int) (averageBlue * gainFactor));
}
@Override
public boolean equals(Object o) {
if (!(o instanceof Color)) {
public boolean equals(final Object o) {
if (!(o instanceof Color that)) {
return false;
}
final Color that = (Color) o;
return this.alpha == that.alpha && this.blue == that.blue && this.green == that.green && this.red == that.red;
}
@Override
public int hashCode() {
return asARGB() ^ Color.class.hashCode();
return this.asARGB() ^ Color.class.hashCode();
}
@Override
@NotNull
public Map<String, Object> serialize() {
return ImmutableMap.of(
"ALPHA", getAlpha(),
"RED", getRed(),
"BLUE", getBlue(),
"GREEN", getGreen()
"ALPHA", this.getAlpha(),
"RED", this.getRed(),
"BLUE", this.getBlue(),
"GREEN", this.getGreen()
);
}
@NotNull
public static Color deserialize(@NotNull Map<String, Object> map) {
public static Color deserialize(final @NotNull Map<String, Object> map) {
return fromARGB(
asInt("ALPHA", map, DEFAULT_ALPHA),
asInt("RED", map),
@@ -411,23 +465,23 @@ public final class Color implements ConfigurationSerializable {
);
}
private static int asInt(@NotNull String string, @NotNull Map<String, Object> map) {
private static int asInt(final @NotNull String string, final @NotNull Map<String, Object> map) {
return asInt(string, map, null);
}
private static int asInt(@NotNull String string, @NotNull Map<String, Object> map, @Nullable Object defaultValue) {
Object value = map.getOrDefault(string, defaultValue);
private static int asInt(final @NotNull String string, final @NotNull Map<String, Object> map, final @Nullable Object defaultValue) {
final Object value = map.getOrDefault(string, defaultValue);
if (value == null) {
throw new IllegalArgumentException(string + " not in map " + map);
}
if (!(value instanceof Number)) {
if (!(value instanceof Number number)) {
throw new IllegalArgumentException(string + '(' + value + ") is not a number");
}
return ((Number) value).intValue();
return number.intValue();
}
@Override
public String toString() {
return "Color:[argb0x" + Integer.toHexString(asARGB()).toUpperCase(Locale.ROOT) + "]";
return "Color:[argb0x" + Integer.toHexString(this.asARGB()).toUpperCase(Locale.ROOT) + "]";
}
}
+53 -18
View File
@@ -61,6 +61,8 @@ import org.bukkit.profile.PlayerProfile;
import org.bukkit.scheduler.BukkitScheduler;
import org.bukkit.scoreboard.Criteria;
import org.bukkit.scoreboard.ScoreboardManager;
import org.bukkit.generator.structure.Structure;
import org.bukkit.map.MapCursor;
import org.bukkit.structure.StructureManager;
import org.bukkit.util.CachedServerIcon;
import org.jetbrains.annotations.ApiStatus;
@@ -872,8 +874,7 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
* Create a new explorer map targeting the closest nearby structure of a
* given {@link StructureType}.
* <br>
* This method uses implementation default values for radius and
* findUnexplored (usually 100, true).
* This method uses a radius of 100 and only searches for unexplored structures.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
@@ -882,9 +883,9 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
*
* @see World#locateNearestStructure(org.bukkit.Location,
* org.bukkit.StructureType, int, boolean)
* @deprecated use {@link #createExplorerMap(World, Location, org.bukkit.generator.structure.StructureType, org.bukkit.map.MapCursor.Type)}
* @deprecated use {@link #createExplorerMap(World, Location, org.bukkit.generator.structure.StructureType, MapCursor.Type)}
*/
@Deprecated // Paper
@Deprecated
@NotNull
default ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull StructureType structureType) {
return this.createExplorerMap(world, location, structureType, 100, true);
@@ -904,18 +905,16 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
*
* @see World#locateNearestStructure(org.bukkit.Location,
* org.bukkit.StructureType, int, boolean)
* @deprecated use {@link #createExplorerMap(World, Location, org.bukkit.generator.structure.StructureType, org.bukkit.map.MapCursor.Type, int, boolean)}
* @deprecated use {@link #createExplorerMap(World, Location, org.bukkit.generator.structure.StructureType, MapCursor.Type, int, boolean)}
*/
@Deprecated // Paper
@NotNull
public ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull StructureType structureType, int radius, boolean findUnexplored);
// Paper start
@Deprecated
@NotNull ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull StructureType structureType, int radius, boolean findUnexplored);
/**
* Create a new explorer map targeting the closest nearby structure of a
* given {@link org.bukkit.generator.structure.StructureType}.
* given {@link StructureType}.
* <br>
* This method uses implementation default values for radius and
* findUnexplored (usually 100, true).
* This method uses a radius of 100 and only searches for unexplored structures.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
@@ -924,15 +923,15 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
* @return a newly created item stack or null if it can't find a location
*
* @see World#locateNearestStructure(org.bukkit.Location,
* org.bukkit.generator.structure.StructureType, int, boolean)
* StructureType, int, boolean)
*/
default @Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull org.bukkit.generator.structure.StructureType structureType, @NotNull org.bukkit.map.MapCursor.Type mapIcon) {
default @Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull org.bukkit.generator.structure.StructureType structureType, @NotNull MapCursor.Type mapIcon) {
return this.createExplorerMap(world, location, structureType, mapIcon, 100, true);
}
/**
* Create a new explorer map targeting the closest nearby structure of a
* given {@link org.bukkit.generator.structure.StructureType}.
* given {@link StructureType}.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
@@ -944,10 +943,46 @@ public interface Server extends PluginMessageRecipient, net.kyori.adventure.audi
* @return the newly created item stack or null if it can't find a location
*
* @see World#locateNearestStructure(org.bukkit.Location,
* org.bukkit.generator.structure.StructureType, int, boolean)
* StructureType, int, boolean)
*/
@Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull org.bukkit.generator.structure.StructureType structureType, @NotNull org.bukkit.map.MapCursor.Type mapIcon, int radius, boolean findUnexplored);
// Paper end
@Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull org.bukkit.generator.structure.StructureType structureType, @NotNull MapCursor.Type mapIcon, int radius, boolean findUnexplored);
/**
* Create a new explorer map targeting the closest nearby specific
* {@link Structure}.
* <br>
* This method uses a radius of 100 and only searches for unexplored structures.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
* @param structure the specific structure to find
* @param mapIcon the map icon to use on the map
* @return a newly created item stack or null if it can't find a location
*
* @see World#locateNearestStructure(org.bukkit.Location,
* Structure, int, boolean)
*/
default @Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull Structure structure, @NotNull MapCursor.Type mapIcon) {
return this.createExplorerMap(world, location, structure, mapIcon, 100, true);
}
/**
* Create a new explorer map targeting the closest nearby specific
* {@link Structure}.
*
* @param world the world the map will belong to
* @param location the origin location to find the nearest structure
* @param structure the specific structure to find
* @param mapIcon the map icon to use on the map
* @param radius radius to search, see World#locateNearestStructure for more
* information
* @param findUnexplored whether to find unexplored structures
* @return the newly created item stack or null if it can't find a location
*
* @see World#locateNearestStructure(org.bukkit.Location,
* Structure, int, boolean)
*/
@Nullable ItemStack createExplorerMap(@NotNull World world, @NotNull Location location, @NotNull Structure structure, @NotNull MapCursor.Type mapIcon, int radius, boolean findUnexplored);
/**
* Reloads the server, refreshing settings and plugin information.
@@ -110,7 +110,15 @@ public enum TreeType {
*/
PALE_OAK_CREAKING,
/**
* Poplar tree
* Poplar tree with red leaves
*/
POPLAR,
RED_POPLAR,
/**
* Poplar tree with orange leaves
*/
ORANGE_POPLAR,
/**
* Poplar tree with yellow leaves
*/
YELLOW_POPLAR,
}
+67 -4
View File
@@ -4,6 +4,7 @@ import io.papermc.paper.entity.poi.PoiSearchResult;
import io.papermc.paper.entity.poi.PoiType;
import io.papermc.paper.math.Position;
import io.papermc.paper.raytracing.PositionedRayTraceConfigurationBuilder;
import io.papermc.paper.world.WorldClock;
import io.papermc.paper.world.chunk.ChunkGenerationState;
import java.io.File;
import java.nio.file.Path;
@@ -2116,7 +2117,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* @return The current relative time
* @see #getFullTime() Returns an absolute time of this world
*/
public long getTime();
long getTime();
/**
* Sets the relative in-game time on the server.
@@ -2131,7 +2132,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* hours*1000)
* @see #setFullTime(long) Sets the absolute time of this world
*/
public void setTime(long time);
void setTime(long time);
/**
* Gets the full in-game time on this world, or {@code 0} if this world does not have a world clock.
@@ -2139,7 +2140,7 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* @return The current absolute time
* @see #getTime() Returns a relative time of this world
*/
public long getFullTime();
long getFullTime();
/**
* Sets the in-game time on the server
@@ -2151,7 +2152,69 @@ public interface World extends RegionAccessor, WorldInfo, PluginMessageRecipient
* @see #setTime(long) Sets the relative time of this world
* @throws IllegalArgumentException if this world does not have a world clock (e.g. the nether)
*/
public void setFullTime(long time);
void setFullTime(long time);
/**
* Gets the default clock of this world's dimension type.
* <p>
* This is the clock used by {@link #getTime()}, {@link #getFullTime()} and their setters.
*
* @return the default clock, or {@code null} if this world does not have one (e.g. the nether)
*/
@Nullable WorldClock getDefaultClock();
/**
* Gets the total number of ticks the given clock has advanced in this world.
* <p>
* Clocks are per world unless the server is configured to share them between all worlds.
*
* @param clock the clock
* @return the total ticks of the clock
*/
long getFullTime(@NotNull WorldClock clock);
/**
* Sets the total number of ticks of the given clock in this world.
* <p>
* This calls a {@link org.bukkit.event.world.ClockTimeSkipEvent} and does nothing if it is cancelled.
* Clocks are per world unless the server is configured to share them between all worlds.
*
* @param clock the clock
* @param time the new total ticks of the clock, must not be negative
*/
void setFullTime(@NotNull WorldClock clock, long time);
/**
* Gets whether the given clock is paused in this world.
*
* @param clock the clock
* @return whether the clock is paused
*/
boolean isClockPaused(@NotNull WorldClock clock);
/**
* Sets whether the given clock is paused in this world.
*
* @param clock the clock
* @param paused whether the clock should be paused
*/
void setClockPaused(@NotNull WorldClock clock, boolean paused);
/**
* Gets the rate at which the given clock advances in this world, in clock ticks per game tick.
*
* @param clock the clock
* @return the clock rate
*/
float getClockRate(@NotNull WorldClock clock);
/**
* Sets the rate at which the given clock advances in this world, in clock ticks per game tick.
*
* @param clock the clock
* @param rate the new clock rate, must be positive
*/
void setClockRate(@NotNull WorldClock clock, float rate);
// Paper start
@@ -1,5 +1,6 @@
package org.bukkit.entity;
import com.google.common.base.Preconditions;
import java.util.Collection;
import java.util.List;
import java.util.Set;
@@ -1401,25 +1402,39 @@ public interface LivingEntity extends Attributable, Damageable, ProjectileSource
void setHurtDirection(float hurtDirection);
// Paper end - hurt direction API
// Paper start - swing hand API
/**
* Makes this entity swing their hand.
* Makes this entity swing their hand, using the {@link io.papermc.paper.datacomponent.DataComponentTypes#ATTACK_ANIMATION}
* component on the item held in that hand to determine the animation type and duration.
* If the held item does not have the component, the default {@link io.papermc.paper.datacomponent.item.SwingAnimation.Animation#WHACK}
* animation with a duration of {@code 6} ticks is used.
*
* <p>This method does nothing if this entity does not
* have an animation for swinging their hand.
*
* @param hand hand to be swung, either {@link org.bukkit.inventory.EquipmentSlot#HAND} or {@link org.bukkit.inventory.EquipmentSlot#OFF_HAND}
* @throws IllegalArgumentException if invalid hand is passed
* @throws IllegalArgumentException if an invalid hand is passed
*/
default void swingHand(@NotNull org.bukkit.inventory.EquipmentSlot hand) {
com.google.common.base.Preconditions.checkArgument(hand != null && hand.isHand(), String.format("Expected a valid hand, got \"%s\" instead!", hand));
Preconditions.checkArgument(hand != null && hand.isHand(), "Expected a valid hand, got \"%s\" instead!", hand);
if (hand == org.bukkit.inventory.EquipmentSlot.HAND) {
this.swingMainHand();
} else {
this.swingOffHand();
}
}
// Paper end - swing hand API
/**
* Makes this entity swing their hand using the provided {@link io.papermc.paper.datacomponent.item.SwingAnimation},
* instead of the swing animation component the held item may have.
*
* <p>This method does nothing if this entity does not
* have an animation for swinging their hand.
*
* @param hand hand to be swung, either {@link org.bukkit.inventory.EquipmentSlot#HAND} or {@link org.bukkit.inventory.EquipmentSlot#OFF_HAND}
* @param swingAnimation the swing animation to use
* @throws IllegalArgumentException if an invalid hand is passed
*/
void swingHand(@NotNull org.bukkit.inventory.EquipmentSlot hand, @NotNull io.papermc.paper.datacomponent.item.SwingAnimation swingAnimation);
// Paper start - knockback API
/**
@@ -1,5 +1,6 @@
package org.bukkit.event.world;
import io.papermc.paper.world.WorldClock;
import org.bukkit.event.Cancellable;
import org.bukkit.event.Event;
import org.bukkit.event.HandlerList;
@@ -11,24 +12,33 @@ import org.jspecify.annotations.NullMarked;
* <p>
* If the event is cancelled the time will not change.
*/
// TODO - snapshot - 26.1 clock
@ApiStatus.Experimental
@NullMarked
public class ClockTimeSkipEvent extends Event implements Cancellable {
private static final HandlerList HANDLER_LIST = new HandlerList();
private final WorldClock clock;
private final SkipReason skipReason;
private long skipAmount;
private boolean cancelled;
@ApiStatus.Internal
public ClockTimeSkipEvent(final SkipReason skipReason, final long skipAmount) {
public ClockTimeSkipEvent(final WorldClock clock, final SkipReason skipReason, final long skipAmount) {
this.clock = clock;
this.skipReason = skipReason;
this.skipAmount = skipAmount;
}
/**
* Gets the clock whose time is skipping.
*
* @return the clock
*/
public WorldClock getClock() {
return this.clock;
}
/**
* Gets the reason why the time has skipped.
*
@@ -1,5 +1,6 @@
package org.bukkit.event.world;
import io.papermc.paper.world.WorldClock;
import org.bukkit.World;
import org.bukkit.event.HandlerList;
import org.jetbrains.annotations.ApiStatus;
@@ -19,8 +20,8 @@ public class TimeSkipEvent extends ClockTimeSkipEvent {
private final World world;
@ApiStatus.Internal
public TimeSkipEvent(@NotNull World world, @NotNull SkipReason skipReason, long skipAmount) {
super(skipReason, skipAmount);
public TimeSkipEvent(@NotNull World world, @NotNull WorldClock clock, @NotNull SkipReason skipReason, long skipAmount) {
super(clock, skipReason, skipAmount);
this.world = world;
}
@@ -29,7 +29,9 @@ import io.papermc.paper.dialog.Dialog;
import io.papermc.paper.entity.RemovalReason;
import io.papermc.paper.entity.poi.PoiTypes;
import io.papermc.paper.item.MapPostProcessing;
import io.papermc.paper.world.Timeline;
import io.papermc.paper.world.WeatheringCopperState;
import io.papermc.paper.world.WorldClock;
import io.papermc.typewriter.preset.EnumCloneRewriter;
import io.papermc.typewriter.preset.model.EnumValue;
import io.papermc.typewriter.replace.SearchMetadata;
@@ -243,6 +245,8 @@ public final class Rewriters {
.register("ZombieNautilusVariant", ZombieNautilus.Variant.class, new RegistryFieldRewriter<>(Registries.ZOMBIE_NAUTILUS_VARIANT, "getVariant"))
.register("SulfurCubeArchetype", SulfurCube.Archetype.class, new RegistryFieldRewriter<>(Registries.SULFUR_CUBE_ARCHETYPE, "getArchetype"))
.register("Dialog", Dialog.class, new RegistryFieldRewriter<>(Registries.DIALOG, "getDialog"))
.register("WorldClock", WorldClock.class, new RegistryFieldRewriter<>(Registries.WORLD_CLOCK, "getWorldClock"))
.register("Timeline", Timeline.class, new RegistryFieldRewriter<>(Registries.TIMELINE, "getTimeline"))
.register("PoiTypes", PoiTypes.class, new RegistryFieldRewriter<>(Registries.POINT_OF_INTEREST_TYPE, "get"))
.register("MemoryKey", MemoryKey.class, new MemoryKeyRewriter())
// .register("ItemType", org.bukkit.inventory.ItemType.class, new io.papermc.generator.rewriter.types.simple.ItemTypeRewriter()) // - disable for now, lynx want the generic type
@@ -21,11 +21,15 @@ import io.papermc.paper.registry.data.PaintingVariantRegistryEntry;
import io.papermc.paper.registry.data.PigVariantRegistryEntry;
import io.papermc.paper.registry.data.SoundEventRegistryEntry;
import io.papermc.paper.registry.data.SulfurCubeArchetypeRegistryEntry;
import io.papermc.paper.registry.data.TimelineRegistryEntry;
import io.papermc.paper.registry.data.TrimMaterialRegistryEntry;
import io.papermc.paper.registry.data.TrimPatternRegistryEntry;
import io.papermc.paper.registry.data.WolfVariantRegistryEntry;
import io.papermc.paper.registry.data.WorldClockRegistryEntry;
import io.papermc.paper.registry.data.ZombieNautilusVariantRegistryEntry;
import io.papermc.paper.registry.data.dialog.DialogRegistryEntry;
import io.papermc.paper.world.Timeline;
import io.papermc.paper.world.WorldClock;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.lang.reflect.Type;
@@ -45,6 +49,7 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.dialog.Dialogs;
import net.minecraft.sounds.SoundEvents;
import net.minecraft.world.clock.WorldClocks;
import net.minecraft.world.damagesource.DamageTypes;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.SulfurCubeArchetypes;
@@ -80,6 +85,7 @@ import net.minecraft.world.level.gamerules.GameRules;
import net.minecraft.world.level.levelgen.structure.BuiltinStructures;
import net.minecraft.world.level.material.Fluids;
import net.minecraft.world.level.saveddata.maps.MapDecorationTypes;
import net.minecraft.world.timeline.Timelines;
import org.bukkit.Art;
import org.bukkit.Fluid;
import org.bukkit.GameEvent;
@@ -213,7 +219,9 @@ public final class RegistryEntries {
entry(Registries.PIG_SOUND_VARIANT, PigSoundVariants.class, Pig.SoundVariant.class),
entry(Registries.ZOMBIE_NAUTILUS_VARIANT, ZombieNautilusVariants.class, ZombieNautilus.Variant.class).writableApiRegistryBuilder(ZombieNautilusVariantRegistryEntry.Builder.class, "PaperZombieNautilusVariantRegistryEntry.PaperBuilder"),
entry(Registries.SULFUR_CUBE_ARCHETYPE, SulfurCubeArchetypes.class, SulfurCube.Archetype.class).writableApiRegistryBuilder(SulfurCubeArchetypeRegistryEntry.Builder.class, "PaperSulfurCubeArchetypeRegistryEntry.PaperBuilder"),
entry(Registries.DIALOG, Dialogs.class, Dialog.class, "Paper").allowDirect().writableApiRegistryBuilder(DialogRegistryEntry.Builder.class, "PaperDialogRegistryEntry.PaperBuilder")
entry(Registries.DIALOG, Dialogs.class, Dialog.class, "Paper").allowDirect().writableApiRegistryBuilder(DialogRegistryEntry.Builder.class, "PaperDialogRegistryEntry.PaperBuilder"),
entry(Registries.WORLD_CLOCK, WorldClocks.class, WorldClock.class).writableApiRegistryBuilder(WorldClockRegistryEntry.Builder.class, "PaperWorldClockRegistryEntry.PaperBuilder").delayed(),
entry(Registries.TIMELINE, Timelines.class, Timeline.class).writableApiRegistryBuilder(TimelineRegistryEntry.Builder.class, "PaperTimelineRegistryEntry.PaperBuilder").delayed()
);
public static final List<RegistryEntry<?>> API_ONLY = List.of(
+4 -4
View File
@@ -110,7 +110,7 @@ abstract class MockitoAgentProvider : CommandLineArgumentProvider {
dependencies {
implementation(project(":paper-api"))
implementation(platform("ca.spottedleaf.leafpile:bom:1.3.0"))
implementation(platform("ca.spottedleaf.leafpile:bom:1.3.2"))
implementation("ca.spottedleaf.leafpile:common")
implementation("ca.spottedleaf.leafpile:concurrentutil")
implementation("ca.spottedleaf.leafpile:converter")
@@ -285,10 +285,10 @@ fun TaskContainer.registerRunTask(
.asFile
javaLauncher.set(project.javaToolchains.launcherFor {
languageVersion.set(JavaLanguageVersion.of(25))
// TODO - JB runtime 25 has issues with spark rn
// vendor.set(JvmVendorSpec.JETBRAINS)
@Suppress("UnstableApiUsage")
vendor.set(JvmVendorSpec.JETBRAINS)
})
jvmArgs(/*"-XX:+AllowEnhancedClassRedefinition", */"--enable-native-access=ALL-UNNAMED")
jvmArgs("-XX:+AllowEnhancedClassRedefinition", "--enable-native-access=ALL-UNNAMED")
if (rootProject.childProjects["test-plugin"] != null) {
val testPluginJar = rootProject.project(":test-plugin").tasks.jar.flatMap { it.archiveFile }
@@ -34086,10 +34086,10 @@ index 06103ffdf599af91bc1d3baf89772a1acc93a21a..fc1f82d313acb8bf9bd03b1f09c68035
return structureTemplate.save(new CompoundTag());
}
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index d40863262dd7cf0e7c5667fd836e80d4f47e25aa..c4adc7266f41057bc93e8c34929c079c9c468d0b 100644
index a9ee3ae3217ce7182cd8ab11ea455bbedca1c363..7bc5221da2707b53f0d710f80ea4bfa838195ad4 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -293,6 +293,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -294,6 +294,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
private final ServerClockManager clockManager;
public static <S extends MinecraftServer> S spin(final Function<Thread, S> factory) {
File diff suppressed because it is too large. Load diff
@@ -355,10 +355,10 @@ index 0000000000000000000000000000000000000000..829f7685904fd741e00403647ae76d32
+ }
+}
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index 5090c9780ada3c8a7fd070bc6aec3ff1af034d73..a4e6225add32df93b0d6009d98ee322941b69938 100644
index d7b6c1d9058df4615bf2ba807e5f3450c4c06d5a..1170fb4c074c495b1a8afb4c5d50a3904b11e0f7 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -887,6 +887,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -888,6 +888,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
profiler.pop();
}
@@ -366,7 +366,7 @@ index 5090c9780ada3c8a7fd070bc6aec3ff1af034d73..a4e6225add32df93b0d6009d98ee3229
this.entityTickList
.forEach(
entity -> {
@@ -1404,12 +1405,15 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -1405,12 +1406,15 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
profiler.push(entity.typeHolder()::getRegisteredName);
profiler.incrementCounter("tickNonPassenger");
entity.commonTick();
@@ -383,7 +383,7 @@ index 5090c9780ada3c8a7fd070bc6aec3ff1af034d73..a4e6225add32df93b0d6009d98ee3229
}
// Paper start - log detailed entity tick information
} finally {
@@ -1420,7 +1424,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -1421,7 +1425,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
// Paper end - log detailed entity tick information
}
@@ -392,7 +392,7 @@ index 5090c9780ada3c8a7fd070bc6aec3ff1af034d73..a4e6225add32df93b0d6009d98ee3229
if (entity.isRemoved() || entity.getVehicle() != vehicle) {
entity.stopRiding();
} else if (entity instanceof Player || this.entityTickList.contains(entity)) {
@@ -1428,12 +1432,21 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -1429,12 +1433,21 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
profiler.push(entity.typeHolder()::getRegisteredName);
profiler.incrementCounter("tickPassenger");
entity.commonTick();
@@ -463,7 +463,7 @@ index f74837dc0be560a1601132594587bcfc22dcf778..c5a212e73c2efeb1f440778cb3f1fa6e
public void tick() {
super.tick();
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index bb61d4e4ecfe5bab6269fa3d4303dab95719d7bb..3d219969a55896b1495c54c0a12a25b49ba093f0 100644
index 71ce072c2c9a6349d895054184321cb336ae567b..62d534132f6a2d69df28e244fa296f24a007181f 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -388,6 +388,15 @@ public abstract class Entity
@@ -822,7 +822,7 @@ index f46cca0467bb4da5511bc953ce5b43fa606bf978..445c5cafad71028171c1f26193966177
+
}
diff --git a/net/minecraft/world/level/Level.java b/net/minecraft/world/level/Level.java
index d327301693c70affc866d355a08efe935e3656b4..efcf399ced67201525ea6e6b3bd46b93b7e1823d 100644
index a828576457e0bb932750dba507bdef7e8903e144..2f5ec35bf7f104e259f85d15b17c8064daa05a5e 100644
--- a/net/minecraft/world/level/Level.java
+++ b/net/minecraft/world/level/Level.java
@@ -154,6 +154,12 @@ public abstract class Level implements LevelAccessor, AutoCloseable, ca.spottedl
@@ -2321,7 +2321,7 @@ index 0000000000000000000000000000000000000000..505b5e39d0b4cf2a56c1c720f0c9ab01
+ }
+}
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index 7fa69d76bf2c802b538a21af40898b8abb79629e..d9daa3ea8c4dc21bbe0d0ea9f0ece3f6c6777ba4 100644
index 1170fb4c074c495b1a8afb4c5d50a3904b11e0f7..3560df07024e4423835bb724318fcea2adfd4cfe 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -238,6 +238,7 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -2332,7 +2332,7 @@ index 7fa69d76bf2c802b538a21af40898b8abb79629e..d9daa3ea8c4dc21bbe0d0ea9f0ece3f6
@Override
public @Nullable LevelChunk getChunkIfLoaded(int x, int z) {
@@ -3040,6 +3041,13 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -3041,6 +3042,13 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
return this.debugSynchronizers;
}
@@ -5,10 +5,10 @@ Subject: [PATCH] Incremental chunk and player saving
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index 255e01251005a547088aba437ca27ce06fc996e5..487fcb8546ad24125c6573fb84f22a02752b35f1 100644
index 07ba0aeefec186d7befe60c86dc5180e3131de51..482b428cab996c19664b57c341c55db5a66a6387 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -929,7 +929,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -930,7 +930,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
public boolean saveAllChunks(final boolean silent, final boolean flush, final boolean force, final boolean close) {
// Paper end - add close param
@@ -17,7 +17,7 @@ index 255e01251005a547088aba437ca27ce06fc996e5..487fcb8546ad24125c6573fb84f22a02
boolean result = false;
for (ServerLevel level : this.getAllLevels()) {
@@ -941,6 +941,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -942,6 +942,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
result = true;
}
@@ -31,7 +31,7 @@ index 255e01251005a547088aba437ca27ce06fc996e5..487fcb8546ad24125c6573fb84f22a02
GameProfile singleplayerProfile = this.getSingleplayerProfile();
this.storageSource.saveDataTag(this.worldData, singleplayerProfile == null ? null : singleplayerProfile.id());
if (flush) {
@@ -948,14 +955,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -949,14 +956,13 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
} else {
this.savedDataStorage.scheduleSave();
}
@@ -48,7 +48,7 @@ index 255e01251005a547088aba437ca27ce06fc996e5..487fcb8546ad24125c6573fb84f22a02
boolean result = this.saveAllChunks(silent, flush, force);
this.warnOnLowDiskSpace();
return result;
@@ -1600,9 +1606,31 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1602,9 +1608,31 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
}
this.ticksUntilAutosave--;
@@ -83,10 +83,10 @@ index 255e01251005a547088aba437ca27ce06fc996e5..487fcb8546ad24125c6573fb84f22a02
ProfilerFiller profiler = Profiler.get();
this.server.spark.executeMainThreadTasks(); // Paper - spark
diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java
index e4d38cfdac2b926afae3b1b96679b6ce45ea256a..ca793f3f7300148cd55d2e873af5b43d9f4c708f 100644
index 3560df07024e4423835bb724318fcea2adfd4cfe..377c4373785ec5c14a7d7ce9e710c02943546fbe 100644
--- a/net/minecraft/server/level/ServerLevel.java
+++ b/net/minecraft/server/level/ServerLevel.java
@@ -1470,6 +1470,15 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
@@ -1471,6 +1471,15 @@ public class ServerLevel extends Level implements WorldGenLevel, ServerEntityGet
public boolean mayInteract(final Entity entity, final BlockPos pos) {
return !(entity instanceof Player player && (this.server.isUnderSpawnProtection(this, pos, player) || !this.getWorldBorder().isWithinBounds(pos)));
}
@@ -20,10 +20,10 @@ index ba372f75dec3ee8b89c48df2cc646f071a47d25a..706f529a7d1064aad128b1ee8c82f631
@Override
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index 487fcb8546ad24125c6573fb84f22a02752b35f1..b675e9322234761a69e71e9e8b9d41364caf34b6 100644
index 482b428cab996c19664b57c341c55db5a66a6387..bd5efcf1dee77c204bf29ef4693ac54146e8d901 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -1741,32 +1741,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1743,32 +1743,22 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
return new ServerStatus.Players(maxPlayers, players.size(), sample);
}
@@ -66,7 +66,7 @@ index 487fcb8546ad24125c6573fb84f22a02752b35f1..b675e9322234761a69e71e9e8b9d4136
io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.ADVENTURE_CLICK_MANAGER.handleQueue(this.tickCount); // Paper
io.papermc.paper.adventure.providers.ClickCallbackProviderImpl.DIALOG_CLICK_MANAGER.handleQueue(this.tickCount); // Paper
diff --git a/net/minecraft/world/entity/Entity.java b/net/minecraft/world/entity/Entity.java
index 2cec345fdabc8e01e378e66c35093dec1fcb7c2c..0ae23c90594d85e94d17d8114a21fc62b79219d3 100644
index 62d534132f6a2d69df28e244fa296f24a007181f..5cc94652112c67ca4e7ae6e8b324e9c9f8908d7d 100644
--- a/net/minecraft/world/entity/Entity.java
+++ b/net/minecraft/world/entity/Entity.java
@@ -5379,6 +5379,11 @@ public abstract class Entity
@@ -1,195 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Aikar <aikar@aikar.co>
Date: Fri, 15 Feb 2019 01:08:19 -0500
Subject: [PATCH] Allow Saving of Oversized Chunks
The Minecraft World Region File format has a hard cap of 1MB per chunk.
This is due to the fact that the header of the file format only allocates
a single byte for sector count, meaning a maximum of 256 sectors, at 4k per sector.
This limit can be reached fairly easily with books, resulting in the chunk being unable
to save to the world. Worse off, is that nothing printed when this occured, and silently
performed a chunk rollback on next load.
This leads to security risk with duplication and is being actively exploited.
This patch catches the too large scenario, falls back and moves any large Entity
or Tile Entity into a new compound, and this compound is saved into a different file.
On Chunk Load, we check for oversized status, and if so, we load the extra file and
merge the Entities and Tile Entities from the oversized chunk back into the level to
then be loaded as normal.
Once a chunk is returned back to normal size, the oversized flag will clear, and no
extra data file will exist.
This fix maintains compatability with all existing Anvil Region Format tools as it
does not alter the save format. They will just not know about the extra entities.
This fix also maintains compatability if someone switches server jars to one without
this fix, as the data will remain in the oversized file. Once the server returns
to a jar with this fix, the data will be restored.
diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
index 7767e5420e3d2e569008747523d70147ea8d97fb..eddf7c3cef3b3209b47de97d93862937e9d2c82b 100644
--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
+++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
@@ -68,6 +68,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
this.info = info;
this.path = path;
this.version = version;
+ this.initOversizedState(); // Paper
if (!Files.isDirectory(externalFileDir)) {
throw new IllegalArgumentException("Expected directory, got " + externalFileDir.toAbsolutePath());
}
@@ -463,4 +464,75 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
private interface CommitOp {
void run() throws IOException;
}
+
+ // Paper start
+ private final byte[] oversized = new byte[1024];
+ private int oversizedCount;
+
+ private synchronized void initOversizedState() throws IOException {
+ Path metaFile = getOversizedMetaFile();
+ if (Files.exists(metaFile)) {
+ final byte[] read = Files.readAllBytes(metaFile);
+ System.arraycopy(read, 0, oversized, 0, oversized.length);
+ for (byte temp : oversized) {
+ oversizedCount += temp;
+ }
+ }
+ }
+
+ private static int getChunkIndex(int x, int z) {
+ return (x & 31) + (z & 31) * 32;
+ }
+
+ synchronized boolean isOversized(int x, int z) {
+ return this.oversized[getChunkIndex(x, z)] == 1;
+ }
+
+ synchronized void setOversized(int x, int z, boolean oversized) throws IOException {
+ final int offset = getChunkIndex(x, z);
+ boolean previous = this.oversized[offset] == 1;
+ this.oversized[offset] = (byte) (oversized ? 1 : 0);
+ if (!previous && oversized) {
+ oversizedCount++;
+ } else if (!oversized && previous) {
+ oversizedCount--;
+ }
+ if (previous && !oversized) {
+ Path oversizedFile = getOversizedFile(x, z);
+ if (Files.exists(oversizedFile)) {
+ Files.delete(oversizedFile);
+ }
+ }
+ if (oversizedCount > 0) {
+ if (previous != oversized) {
+ writeOversizedMeta();
+ }
+ } else if (previous) {
+ Path oversizedMetaFile = getOversizedMetaFile();
+ if (Files.exists(oversizedMetaFile)) {
+ Files.delete(oversizedMetaFile);
+ }
+ }
+ }
+
+ private void writeOversizedMeta() throws IOException {
+ Files.write(this.getOversizedMetaFile(), this.oversized);
+ }
+
+ private Path getOversizedMetaFile() {
+ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + ".oversized.nbt");
+ }
+
+ private Path getOversizedFile(int x, int z) {
+ return this.path.getParent().resolve(this.path.getFileName().toString().replaceAll("\\.mca$", "") + "_oversized_" + x + "_" + z + ".nbt");
+ }
+
+ synchronized net.minecraft.nbt.CompoundTag getOversizedData(int x, int z) throws IOException {
+ Path file = getOversizedFile(x, z);
+ try (DataInputStream out = new DataInputStream(new java.util.zip.InflaterInputStream(Files.newInputStream(file)))) { // Paper - Optimize NBT reading/writing; FastNBTParser buffers internally
+ return ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead((java.io.DataInput) out); // Paper - Optimize NBT reading/writing
+ }
+
+ }
+ // Paper end
}
diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index a78e27fa5733bdb21e06e89025fd23551b3912e7..939d9a660ca9c878d2c087bad07f64dab3c06fef 100644
--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -19,6 +19,7 @@ import net.minecraft.world.level.ChunkPos;
import org.jspecify.annotations.Nullable;
public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise.patches.chunk_system.io.ChunkSystemRegionFileStorage { // Paper - rewrite chunk system
+ private static final org.slf4j.Logger LOGGER = com.mojang.logging.LogUtils.getLogger(); // Paper
public static final String ANVIL_EXTENSION = ".mca";
private static final int MAX_CACHE_SIZE = 256;
private final Long2ObjectLinkedOpenHashMap<Optional<RegionFile>> regionCache = new Long2ObjectLinkedOpenHashMap<>();
@@ -229,11 +230,54 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
return Objects.requireNonNull(this.getRegionFile(pos, true));
}
+ // Paper start
+ private static void printOversizedLog(String msg, Path file, int x, int z) {
+ LOGGER.error("{} ({} - {},{}) Go clean it up to remove this message. /minecraft:tp {} 128 {} - DO NOT REPORT THIS TO PAPER - You may ask for help on Discord, but do not file an issue. These error messages can not be removed.", msg, file.toString().replaceAll(".+[\\\\/]", ""), x, z, x << 4, z << 4);
+ }
+
+ private static CompoundTag readOversizedChunk(RegionFile regionfile, ChunkPos chunkCoordinate) throws IOException {
+ synchronized (regionfile) {
+ try (DataInputStream datainputstream = regionfile.getChunkDataInputStream(chunkCoordinate)) {
+ CompoundTag oversizedData = regionfile.getOversizedData(chunkCoordinate.x(), chunkCoordinate.z());
+ CompoundTag chunk = ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead(datainputstream); // Paper - Optimize NBT reading/writing
+ if (oversizedData == null) {
+ return chunk;
+ }
+ CompoundTag oversizedLevel = oversizedData.getCompoundOrEmpty("Level");
+
+ mergeChunkList(chunk.getCompoundOrEmpty("Level"), oversizedLevel, "Entities", "Entities");
+ mergeChunkList(chunk.getCompoundOrEmpty("Level"), oversizedLevel, "TileEntities", "TileEntities");
+
+ return chunk;
+ } catch (Throwable t) {
+ t.printStackTrace();
+ throw t;
+ }
+ }
+ }
+
+ private static void mergeChunkList(CompoundTag level, CompoundTag oversizedLevel, String key, String oversizedKey) {
+ net.minecraft.nbt.ListTag levelList = level.getListOrEmpty(key);
+ net.minecraft.nbt.ListTag oversizedList = oversizedLevel.getListOrEmpty(oversizedKey);
+
+ if (!oversizedList.isEmpty()) {
+ levelList.addAll(oversizedList);
+ level.put(key, levelList);
+ }
+ }
+ // Paper end
+
public @Nullable CompoundTag read(final ChunkPos pos) throws IOException {
RegionFile region = this.getRegionFile(pos, false);
if (region == null) {
return null;
}
+ // Paper start
+ if (region.isOversized(pos.x(), pos.z())) {
+ printOversizedLog("Loading Oversized Chunk!", region.getPath(), pos.x(), pos.z());
+ return readOversizedChunk(region, pos);
+ }
+ // Paper end
try (DataInputStream regionChunkInputStream = region.getChunkDataInputStream(pos)) {
return regionChunkInputStream == null ? null : ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead(regionChunkInputStream); // Paper - Optimize NBT reading/writing
@@ -265,6 +309,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
} else {
try (DataOutputStream output = region.getChunkDataOutputStream(pos)) {
NbtIo.write(value, output);
+ region.setOversized(pos.x(), pos.z(), false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone
}
}
}
@@ -8,7 +8,7 @@ This ensures at least a valid version of the chunk exists
on disk, even if outdated
diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
index b6c6bf9b20fee63b7e2a37d949bb1e55cef40de6..3ba442ac6b823e1c6a69398abc4f116da07c32c0 100644
index 1077ff25ca0da7fb6f89dd2e16174f00b4986283..bd4b488cdf7a386bc458fad2ae659b77e60962ad 100644
--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
+++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
@@ -24,6 +24,7 @@ import org.slf4j.Logger;
@@ -19,7 +19,7 @@ index b6c6bf9b20fee63b7e2a37d949bb1e55cef40de6..3ba442ac6b823e1c6a69398abc4f116d
private static final int SECTOR_BYTES = 4096;
@VisibleForTesting
protected static final int SECTOR_INTS = 1024;
@@ -451,6 +452,24 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
@@ -903,6 +904,24 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
this.pos = pos;
}
@@ -45,7 +45,7 @@ index b6c6bf9b20fee63b7e2a37d949bb1e55cef40de6..3ba442ac6b823e1c6a69398abc4f116d
public void close() throws IOException {
ByteBuffer result = ByteBuffer.wrap(this.buf, 0, this.count);
diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index c4f27907774f694c6bf2129ba14e5155c838a19b..a2dd62ced02de213ffe1244b893069b93011713c 100644
index 9f8b1d4235d2f268a523c028006114b61d8d67ef..7e815fee19ab2c283bf1fdc3d6a530d4101de3de 100644
--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -95,11 +95,24 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
@@ -73,7 +73,7 @@ index c4f27907774f694c6bf2129ba14e5155c838a19b..a2dd62ced02de213ffe1244b893069b9
return writeData;
}
@@ -307,9 +320,18 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
@@ -330,8 +343,17 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
if (value == null) {
region.clear(pos);
} else {
@@ -82,7 +82,6 @@ index c4f27907774f694c6bf2129ba14e5155c838a19b..a2dd62ced02de213ffe1244b893069b9
+ DataOutputStream output = region.getChunkDataOutputStream(pos);
+ try { // Paper - Only write if successful
NbtIo.write(value, output);
region.setOversized(pos.x(), pos.z(), false); // Paper - We don't do this anymore, mojang stores differently, but clear old meta flag if it exists to get rid of our own meta file once last oversized is gone
+ // Paper start - don't write garbage data to disk if writing serialization fails
+ output.close();
+ } catch (final RegionFileSizeException ex) {
@@ -93,7 +92,7 @@ index c4f27907774f694c6bf2129ba14e5155c838a19b..a2dd62ced02de213ffe1244b893069b9
}
}
}
@@ -357,4 +379,13 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
@@ -379,4 +401,13 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
public RegionStorageInfo info() {
return this.info;
}
@@ -14,7 +14,7 @@ timestamp so that fs watchers can detect when RegionFiles are
modified.
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
index 6dd6b4fb79870da6d3bf54dc813ae685237d1f0e..09205efff3a1a2042e67600510af270df9e46c67 100644
index 48353259f9a17937f71951706e0172b5b4957d56..e9dc38d934ede23f17e18893ee207d9fbbd26449 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
@@ -1309,6 +1309,14 @@ public final class MoonriseRegionFileIO {
@@ -5,7 +5,7 @@ Subject: [PATCH] Remove debug-like logging
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 951f5ab5a05dcedf323845f57fd061645466e563..19c7dffe5f9c45cd99cb9b14cae5ac908cae0afc 100644
index 2b5377da8fc3d8582c47809a5e4815d7b0293780..d920a4905d044925c4c4ab980fc238c9c489d5ac 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -1761,7 +1761,7 @@ public class ServerGamePacketListenerImpl
@@ -6,7 +6,7 @@ Subject: [PATCH] Optimise collision checking in player move packet handling
Move collision logic to just the hasNewCollision call instead of getCubes + hasNewCollision
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 19c7dffe5f9c45cd99cb9b14cae5ac908cae0afc..1c92e33104cc131269be15333eeee0ca5f0c92b1 100644
index d920a4905d044925c4c4ab980fc238c9c489d5ac..60decc2534c5bc52b1a22e23a416a7660f724cb0 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -647,6 +647,7 @@ public class ServerGamePacketListenerImpl
@@ -1,845 +0,0 @@
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: Spottedleaf <Spottedleaf@users.noreply.github.com>
Date: Sun, 2 Feb 2020 02:25:10 -0800
Subject: [PATCH] Attempt to recalculate regionfile header if it is corrupt
Instead of trying to relocate the chunk, which is seems to never
be the correct choice, so we end up duplicating or swapping chunks,
we instead drop the current regionfile header and recalculate -
hoping that at least then we don't swap chunks, and maybe recover
them all.
diff --git a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
index 09205efff3a1a2042e67600510af270df9e46c67..775d93d35855e394e5eccf0ad80eca43b1ed9562 100644
--- a/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
+++ b/ca/spottedleaf/moonrise/patches/chunk_system/io/MoonriseRegionFileIO.java
@@ -1547,7 +1547,7 @@ public final class MoonriseRegionFileIO {
public abstract void finishWrite(final int chunkX, final int chunkZ, final WriteData writeData) throws IOException;
- public static record ReadData(ReadResult result, DataInputStream input, CompoundTag syncRead) {
+ public static record ReadData(ReadResult result, DataInputStream input, CompoundTag syncRead, int recalculateCount) { // Paper - Attempt to recalculate regionfile header if it is corrupt
public static enum ReadResult {
NO_DATA,
HAS_DATA,
diff --git a/net/minecraft/world/level/chunk/storage/RegionBitmap.java b/net/minecraft/world/level/chunk/storage/RegionBitmap.java
index a6f9435a8e2a6c8cc05bd3873cb6fc0ec98973b8..d44fd9eea0ccb09c894d6431ede8a4c2aae85c5b 100644
--- a/net/minecraft/world/level/chunk/storage/RegionBitmap.java
+++ b/net/minecraft/world/level/chunk/storage/RegionBitmap.java
@@ -9,6 +9,27 @@ import java.util.BitSet;
public class RegionBitmap {
private final BitSet used = new BitSet();
+ // Paper start - Attempt to recalculate regionfile header if it is corrupt
+ public final void copyFrom(RegionBitmap other) {
+ BitSet thisBitset = this.used;
+ BitSet otherBitset = other.used;
+
+ for (int i = 0; i < Math.max(thisBitset.size(), otherBitset.size()); ++i) {
+ thisBitset.set(i, otherBitset.get(i));
+ }
+ }
+
+ public final boolean tryAllocate(int from, int length) {
+ BitSet bitset = this.used;
+ int firstSet = bitset.nextSetBit(from);
+ if (firstSet > 0 && firstSet < (from + length)) {
+ return false;
+ }
+ bitset.set(from, from + length);
+ return true;
+ }
+ // Paper end - Attempt to recalculate regionfile header if it is corrupt
+
public void force(final int position, final int size) {
this.used.set(position, position + size);
}
diff --git a/net/minecraft/world/level/chunk/storage/RegionFile.java b/net/minecraft/world/level/chunk/storage/RegionFile.java
index d1ea7f442e258b06f5c879de7d57fd16caf09178..5e64762059846f14d4e8ddfec1d1640c199cece2 100644
--- a/net/minecraft/world/level/chunk/storage/RegionFile.java
+++ b/net/minecraft/world/level/chunk/storage/RegionFile.java
@@ -46,6 +46,363 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
@VisibleForTesting
protected final RegionBitmap usedSectors = new RegionBitmap();
+ // Paper start - Attempt to recalculate regionfile header if it is corrupt
+ private static long roundToSectors(long bytes) {
+ long sectors = bytes >>> 12; // 4096 = 2^12
+ long remainingBytes = bytes & 4095;
+ long sign = -remainingBytes; // sign is 1 if nonzero
+ return sectors + (sign >>> 63);
+ }
+
+ private static final net.minecraft.nbt.CompoundTag OVERSIZED_COMPOUND = new net.minecraft.nbt.CompoundTag();
+
+ private net.minecraft.nbt.@Nullable CompoundTag attemptRead(long sector, int chunkDataLength, long fileLength) throws IOException {
+ try {
+ if (chunkDataLength < 0) {
+ return null;
+ }
+
+ long offset = sector * 4096L + 4L; // offset for chunk data
+
+ if ((offset + chunkDataLength) > fileLength) {
+ return null;
+ }
+
+ ByteBuffer chunkData = ByteBuffer.allocate(chunkDataLength);
+ if (chunkDataLength != this.file.read(chunkData, offset)) {
+ return null;
+ }
+
+ ((java.nio.Buffer)chunkData).flip();
+
+ byte compressionType = chunkData.get();
+ if (compressionType < 0) { // compressionType & 128 != 0
+ // oversized chunk
+ return OVERSIZED_COMPOUND;
+ }
+
+ RegionFileVersion compression = RegionFileVersion.fromId(compressionType);
+ if (compression == null) {
+ return null;
+ }
+
+ InputStream input = compression.wrap(new ByteArrayInputStream(chunkData.array(), chunkData.position(), chunkDataLength - chunkData.position()));
+
+ return ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead(new DataInputStream(input)); // Paper - Optimize NBT reading/writing
+ } catch (Exception ex) {
+ return null;
+ }
+ }
+
+ private int getLength(long sector) throws IOException {
+ ByteBuffer length = ByteBuffer.allocate(4);
+ if (4 != this.file.read(length, sector * 4096L)) {
+ return -1;
+ }
+
+ return length.getInt(0);
+ }
+
+ private void backupRegionFile() {
+ Path backup = this.path.getParent().resolve(this.path.getFileName() + "." + new java.util.Random().nextLong() + ".backup");
+ this.backupRegionFile(backup);
+ }
+
+ private void backupRegionFile(Path to) {
+ try {
+ this.file.force(true);
+ LOGGER.warn("Backing up regionfile \"{}\" to {}", this.path.toAbsolutePath(), to.toAbsolutePath());
+ Files.copy(this.path, to, java.nio.file.StandardCopyOption.COPY_ATTRIBUTES);
+ LOGGER.warn("Backed up the regionfile to {}", to.toAbsolutePath());
+ } catch (IOException ex) {
+ LOGGER.error("Failed to backup to {}", to.toAbsolutePath(), ex);
+ }
+ }
+
+ private static boolean inSameRegionfile(ChunkPos first, ChunkPos second) {
+ return (first.x() & ~31) == (second.x() & ~31) && (first.z() & ~31) == (second.z() & ~31);
+ }
+
+ // note: only call for CHUNK regionfiles
+ private final java.util.concurrent.atomic.AtomicInteger recalculateCount = new java.util.concurrent.atomic.AtomicInteger();
+
+ public int getRecalculateCount() {
+ return this.recalculateCount.get();
+ }
+
+ boolean recalculateHeader() throws IOException {
+ if (!this.canRecalcHeader) {
+ return false;
+ }
+ ChunkPos ourLowerLeftPosition = RegionFileStorage.getRegionFileCoordinates(this.path);
+ if (ourLowerLeftPosition == null) {
+ LOGGER.error("Unable to get chunk location of regionfile {}, cannot recover header", this.path.toAbsolutePath());
+ return false;
+ }
+ synchronized (this) {
+ this.recalculateCount.getAndIncrement();
+
+ LOGGER.warn("Corrupt regionfile header detected! Attempting to re-calculate header offsets for regionfile {}", this.path.toAbsolutePath(), new Throwable());
+
+ // try to backup file so maybe it could be sent to us for further investigation
+
+ this.backupRegionFile();
+ net.minecraft.nbt.CompoundTag[] compounds = new net.minecraft.nbt.CompoundTag[32 * 32]; // only in the regionfile (i.e exclude mojang/aikar oversized data)
+ int[] rawLengths = new int[32 * 32]; // length of chunk data including 4 byte length field, bytes
+ int[] sectorOffsets = new int[32 * 32]; // in sectors
+ boolean[] hasAikarOversized = new boolean[32 * 32];
+
+ long fileLength = this.file.size();
+ long totalSectors = roundToSectors(fileLength);
+
+ // search the regionfile from start to finish for the most up-to-date chunk data
+
+ for (long i = 2, maxSector = Math.min((long)(Integer.MAX_VALUE >>> 8), totalSectors); i < maxSector; ++i) { // first two sectors are header, skip
+ int chunkDataLength = this.getLength(i);
+ net.minecraft.nbt.CompoundTag compound = this.attemptRead(i, chunkDataLength, fileLength);
+ if (compound == null || compound == OVERSIZED_COMPOUND) {
+ continue;
+ }
+
+ ChunkPos chunkPos = SerializableChunkData.getChunkCoordinate(compound);
+ if (!inSameRegionfile(ourLowerLeftPosition, chunkPos)) {
+ LOGGER.error("Ignoring absolute chunk {} in regionfile as it is not contained in the bounds of the regionfile '{}'. It should be in regionfile ({},{})", chunkPos, this.path.toAbsolutePath(), chunkPos.x() >> 5, chunkPos.z() >> 5);
+ continue;
+ }
+ int location = (chunkPos.x() & 31) | ((chunkPos.z() & 31) << 5);
+
+ net.minecraft.nbt.CompoundTag otherCompound = compounds[location];
+
+ if (otherCompound != null && SerializableChunkData.getLastWorldSaveTime(otherCompound) > SerializableChunkData.getLastWorldSaveTime(compound)) {
+ continue; // don't overwrite newer data.
+ }
+
+ // aikar oversized?
+ Path aikarOversizedFile = this.getOversizedFile(chunkPos.x(), chunkPos.z());
+ boolean isAikarOversized = false;
+ if (Files.exists(aikarOversizedFile)) {
+ try {
+ net.minecraft.nbt.CompoundTag aikarOversizedCompound = this.getOversizedData(chunkPos.x(), chunkPos.z());
+ if (SerializableChunkData.getLastWorldSaveTime(compound) == SerializableChunkData.getLastWorldSaveTime(aikarOversizedCompound)) {
+ // best we got for an id. hope it's good enough
+ isAikarOversized = true;
+ }
+ } catch (Exception ex) {
+ LOGGER.error("Failed to read aikar oversized data for absolute chunk ({},{}) in regionfile {}, oversized data for this chunk will be lost", chunkPos.x(), chunkPos.z(), this.path.toAbsolutePath(), ex);
+ // fall through, if we can't read aikar oversized we can't risk corrupting chunk data
+ }
+ }
+
+ hasAikarOversized[location] = isAikarOversized;
+ compounds[location] = compound;
+ rawLengths[location] = chunkDataLength + 4;
+ sectorOffsets[location] = (int)i;
+
+ int chunkSectorLength = (int)roundToSectors(rawLengths[location]);
+ i += chunkSectorLength;
+ --i; // gets incremented next iteration
+ }
+
+ // forge style oversized data is already handled by the local search, and aikar data we just hope
+ // we get it right as aikar data has no identifiers we could use to try and find its corresponding
+ // local data compound
+
+ java.nio.file.Path containingFolder = this.externalFileDir;
+ Path[] regionFiles = Files.list(containingFolder).toArray(Path[]::new);
+ boolean[] oversized = new boolean[32 * 32];
+ RegionFileVersion[] oversizedCompressionTypes = new RegionFileVersion[32 * 32];
+
+ if (regionFiles != null) {
+ int lowerXBound = ourLowerLeftPosition.x(); // inclusive
+ int lowerZBound = ourLowerLeftPosition.z(); // inclusive
+ int upperXBound = lowerXBound + 32 - 1; // inclusive
+ int upperZBound = lowerZBound + 32 - 1; // inclusive
+
+ // read mojang oversized data
+ for (Path regionFile : regionFiles) {
+ ChunkPos oversizedCoords = getOversizedChunkPair(regionFile);
+ if (oversizedCoords == null) {
+ continue;
+ }
+
+ if ((oversizedCoords.x() < lowerXBound || oversizedCoords.x() > upperXBound) || (oversizedCoords.z() < lowerZBound || oversizedCoords.z() > upperZBound)) {
+ continue; // not in our regionfile
+ }
+
+ // ensure oversized data is valid & is newer than data in the regionfile
+
+ int location = (oversizedCoords.x() & 31) | ((oversizedCoords.z() & 31) << 5);
+
+ byte[] chunkData;
+ try {
+ chunkData = Files.readAllBytes(regionFile);
+ } catch (Exception ex) {
+ LOGGER.error("Failed to read oversized chunk data in file {}, data will be lost", regionFile.toAbsolutePath(), ex);
+ continue;
+ }
+
+ net.minecraft.nbt.CompoundTag compound = null;
+
+ // We do not know the compression type, as it's stored in the regionfile. So we need to try all of them
+ RegionFileVersion compression = null;
+ for (RegionFileVersion compressionType : RegionFileVersion.VERSIONS.values()) {
+ try {
+ DataInputStream in = new DataInputStream(compressionType.wrap(new ByteArrayInputStream(chunkData))); // typical java
+ compound = ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead((java.io.DataInput)in); // Paper - Optimize NBT reading/writing
+ compression = compressionType;
+ break; // reaches here iff readNBT does not throw
+ } catch (Exception ex) {
+ continue;
+ }
+ }
+
+ if (compound == null) {
+ LOGGER.error("Failed to read oversized chunk data in file {}, it's corrupt. Its data will be lost", regionFile.toAbsolutePath());
+ continue;
+ }
+
+ if (!SerializableChunkData.getChunkCoordinate(compound).equals(oversizedCoords)) {
+ LOGGER.error("Can't use oversized chunk stored in {}, got absolute chunkpos: {}, expected {}", regionFile.toAbsolutePath(), net.minecraft.world.level.chunk.storage.SerializableChunkData.getChunkCoordinate(compound), oversizedCoords);
+ continue;
+ }
+
+ if (compounds[location] == null || SerializableChunkData.getLastWorldSaveTime(compound) > SerializableChunkData.getLastWorldSaveTime(compounds[location])) {
+ oversized[location] = true;
+ oversizedCompressionTypes[location] = compression;
+ }
+ }
+ }
+
+ // now we need to calculate a new offset header
+
+ int[] calculatedOffsets = new int[32 * 32];
+ RegionBitmap newSectorAllocations = new RegionBitmap();
+ newSectorAllocations.force(0, 2); // make space for header
+
+ // allocate sectors for normal chunks
+
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+
+ if (oversized[location]) {
+ continue;
+ }
+
+ int rawLength = rawLengths[location]; // bytes
+ int sectorOffset = sectorOffsets[location]; // sectors
+ int sectorLength = (int)roundToSectors(rawLength);
+
+ if (newSectorAllocations.tryAllocate(sectorOffset, sectorLength)) {
+ calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized
+ } else {
+ LOGGER.error("Failed to allocate space for local chunk (overlapping data??) at ({},{}) in regionfile {}, chunk will be regenerated", chunkX, chunkZ, this.path.toAbsolutePath());
+ }
+ }
+ }
+
+ // allocate sectors for oversized chunks
+
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+
+ if (!oversized[location]) {
+ continue;
+ }
+
+ int sectorOffset = newSectorAllocations.allocate(1);
+ int sectorLength = 1;
+
+ try {
+ this.file.write(this.createExternalStub(oversizedCompressionTypes[location]), sectorOffset * 4096);
+ // only allocate in the new offsets if the write succeeds
+ calculatedOffsets[location] = sectorOffset << 8 | (sectorLength > 255 ? 255 : sectorLength); // support forge style oversized
+ } catch (IOException ex) {
+ newSectorAllocations.free(sectorOffset, sectorLength);
+ LOGGER.error("Failed to write new oversized chunk data holder, local chunk at ({},{}) in regionfile {} will be regenerated", chunkX, chunkZ, this.path.toAbsolutePath());
+ }
+ }
+ }
+
+ // rewrite aikar oversized data
+
+ this.oversizedCount = 0;
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+ int isAikarOversized = hasAikarOversized[location] ? 1 : 0;
+
+ this.oversizedCount += isAikarOversized;
+ this.oversized[location] = (byte)isAikarOversized;
+ }
+ }
+
+ if (this.oversizedCount > 0) {
+ try {
+ this.writeOversizedMeta();
+ } catch (Exception ex) {
+ LOGGER.error("Failed to write aikar oversized chunk meta, all aikar style oversized chunk data will be lost for regionfile {}", this.path.toAbsolutePath(), ex);
+ Files.deleteIfExists(this.getOversizedMetaFile());
+ }
+ } else {
+ Files.deleteIfExists(this.getOversizedMetaFile());
+ }
+
+ this.usedSectors.copyFrom(newSectorAllocations);
+
+ // before we overwrite the old sectors, print a summary of the chunks that got changed.
+
+ LOGGER.info("Starting summary of changes for regionfile {}", this.path.toAbsolutePath());
+
+ for (int chunkX = 0; chunkX < 32; ++chunkX) {
+ for (int chunkZ = 0; chunkZ < 32; ++chunkZ) {
+ int location = chunkX | (chunkZ << 5);
+
+ int oldOffset = this.offsets.get(location);
+ int newOffset = calculatedOffsets[location];
+
+ if (oldOffset == newOffset) {
+ continue;
+ }
+
+ this.offsets.put(location, newOffset); // overwrite incorrect offset
+
+ if (oldOffset == 0) {
+ // found lost data
+ LOGGER.info("Found missing data for local chunk ({},{}) in regionfile {}", chunkX, chunkZ, this.path.toAbsolutePath());
+ } else if (newOffset == 0) {
+ LOGGER.warn("Data for local chunk ({},{}) could not be recovered in regionfile {}, it will be regenerated", chunkX, chunkZ, this.path.toAbsolutePath());
+ } else {
+ LOGGER.info("Local chunk ({},{}) changed to point to newer data or correct chunk in regionfile {}", chunkX, chunkZ, this.path.toAbsolutePath());
+ }
+ }
+ }
+
+ LOGGER.info("End of change summary for regionfile {}", this.path.toAbsolutePath());
+
+ // simply destroy the timestamp header, it's not used
+
+ for (int i = 0; i < 32 * 32; ++i) {
+ this.timestamps.put(i, calculatedOffsets[i] != 0 ? RegionFile.getTimestamp() : 0); // write a valid timestamp for valid chunks, I do not want to find out whatever dumb program actually checks this
+ }
+
+ // write new header
+ try {
+ this.flush();
+ this.file.force(true); // try to ensure it goes through...
+ LOGGER.info("Successfully wrote new header to disk for regionfile {}", this.path.toAbsolutePath());
+ } catch (IOException ex) {
+ LOGGER.error("Failed to write new header to disk for regionfile {}", this.path.toAbsolutePath(), ex);
+ }
+ }
+
+ return true;
+ }
+
+ final boolean canRecalcHeader; // final forces compile fail on new constructor
+ // Paper end - Attempt to recalculate regionfile header if it is corrupt
+
// Paper start - rewrite chunk system
@Override
public final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(final net.minecraft.nbt.CompoundTag data, final ChunkPos pos) throws IOException {
@@ -75,6 +432,7 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
}
this.externalFileDir = externalFileDir;
+ this.canRecalcHeader = info.dfuType()[0] == net.minecraft.util.datafix.DataFixTypes.CHUNK; // Paper - add can recalc flag
this.offsets = this.header.asIntBuffer();
this.offsets.limit(1024);
this.header.position(4096);
@@ -95,11 +453,13 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
long size = Files.size(path);
- for (int i = 0; i < 1024; i++) {
+ boolean needsHeaderRecalc = false; // Paper - recalculate header on header corruption
+ boolean hasBackedUp = false; // Paper - recalculate header on header corruption
+ for (int i = 0; i < 1024; i++) { final int headerLocation = i; // Paper - diff on change, we expect this to be the header location
int offset = this.offsets.get(i);
if (offset != 0) {
- int sectorNumber = getSectorNumber(offset);
- int numSectors = getNumSectors(offset);
+ int sectorNumber = getSectorNumber(offset); // Paper - diff on change, we expect this to be offset in file in sectors
+ int numSectors = getNumSectors(offset); // Paper - diff on change, we expect this to be sector length of region - watch out for reassignments
// Spigot start
if (numSectors == 255) {
// We're maxed out, so we need to read the proper length from the section
@@ -110,18 +470,62 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
// Spigot end
if (sectorNumber < 2) {
LOGGER.warn("Region file {} has invalid sector at index: {}; sector {} overlaps with header", path, i, sectorNumber);
- this.offsets.put(i, 0);
+ //this.offsets.put(i, 0); // Paper - we catch this, but need it in the header for the summary change
} else if (numSectors == 0) {
LOGGER.warn("Region file {} has an invalid sector at index: {}; size has to be > 0", path, i);
- this.offsets.put(i, 0);
+ //this.offsets.put(i, 0); // Paper - we catch this, but need it in the header for the summary change
} else if (sectorNumber * 4096L > size) {
LOGGER.warn("Region file {} has an invalid sector at index: {}; sector {} is out of bounds", path, i, sectorNumber);
- this.offsets.put(i, 0);
+ //this.offsets.put(i, 0); // Paper - we catch this, but need it in the header for the summary change
} else {
- this.usedSectors.force(sectorNumber, numSectors);
+ //this.usedSectors.force(sectorNumber, numSectors); // Paper - move this down so we can check if it fails to allocate
+ }
+ // Paper start - recalculate header on header corruption
+ if (sectorNumber < 2 || numSectors <= 0 || ((long)sectorNumber * 4096L) > size) {
+ if (this.canRecalcHeader) {
+ LOGGER.error("Detected invalid header for regionfile {}! Recalculating header...", this.path.toAbsolutePath());
+ needsHeaderRecalc = true;
+ break;
+ } else {
+ // location = chunkX | (chunkZ << 5);
+ LOGGER.error("Detected invalid header for regionfile {}! Cannot recalculate, removing local chunk ({},{}) from header",
+ this.path.toAbsolutePath(), headerLocation & 31, headerLocation >>> 5);
+ if (!hasBackedUp) {
+ hasBackedUp = true;
+ this.backupRegionFile();
+ }
+ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too
+ this.offsets.put(headerLocation, 0); // delete the entry from header
+ continue;
+ }
+ }
+ boolean failedToAllocate = !this.usedSectors.tryAllocate(sectorNumber, numSectors);
+ if (failedToAllocate) {
+ LOGGER.error("Overlapping allocation by local chunk ({},{}) in regionfile {}", headerLocation & 31, headerLocation >>> 5, this.path.toAbsolutePath());
}
+ if (failedToAllocate & !this.canRecalcHeader) {
+ // location = chunkX | (chunkZ << 5);
+ LOGGER.error("Detected invalid header for regionfile {}! Cannot recalculate, removing local chunk ({},{}) from header",
+ this.path.toAbsolutePath(), headerLocation & 31, headerLocation >>> 5);
+ if (!hasBackedUp) {
+ hasBackedUp = true;
+ this.backupRegionFile();
+ }
+ this.timestamps.put(headerLocation, 0); // be consistent, delete the timestamp too
+ this.offsets.put(headerLocation, 0); // delete the entry from header
+ continue;
+ }
+ needsHeaderRecalc |= failedToAllocate;
+ // Paper end - recalculate header on header corruption
}
}
+ // Paper start - recalculate header on header corruption
+ // we move the recalc here so comparison to old header is correct when logging to console
+ if (needsHeaderRecalc) { // true if header gave us overlapping allocations or had other issues
+ LOGGER.error("Recalculating regionfile {}, header gave erroneous offsets & locations", this.path.toAbsolutePath());
+ this.recalculateHeader();
+ }
+ // Paper end
}
}
@@ -130,10 +534,34 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
}
private Path getExternalChunkPath(final ChunkPos pos) {
- String externalFileName = "c." + pos.x() + "." + pos.z() + ".mcc";
+ String externalFileName = "c." + pos.x() + "." + pos.z() + ".mcc"; // Paper - diff on change
return this.externalFileDir.resolve(externalFileName);
}
+ // Paper start
+ private static @Nullable ChunkPos getOversizedChunkPair(Path file) {
+ String fileName = file.getFileName().toString();
+
+ if (!fileName.startsWith("c.") || !fileName.endsWith(".mcc")) {
+ return null;
+ }
+
+ String[] split = fileName.split("\\.");
+
+ if (split.length != 4) {
+ return null;
+ }
+
+ try {
+ int x = Integer.parseInt(split[1]);
+ int z = Integer.parseInt(split[2]);
+
+ return new ChunkPos(x, z);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+
public synchronized @Nullable DataInputStream getChunkDataInputStream(final ChunkPos pos) throws IOException {
int offset = this.getOffset(pos);
if (offset == 0) {
@@ -155,6 +583,11 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
buffer.flip();
if (buffer.remaining() < 5) {
LOGGER.error("Chunk {} header is truncated: expected {} but read {}", pos, sectorsLength, buffer.remaining());
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
}
@@ -162,6 +595,11 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
byte versionId = buffer.get();
if (length == 0) {
LOGGER.warn("Chunk {} is allocated, but stream is missing", pos);
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
}
@@ -169,18 +607,45 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
if (isExternalStreamChunk(versionId)) {
if (streamLength != 0) {
LOGGER.warn("Chunk has both internal and external streams");
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
}
- return this.createExternalChunkInputStream(pos, getExternalChunkVersion(versionId));
+ // Paper start - recalculate header on regionfile corruption
+ final DataInputStream ret = this.createExternalChunkInputStream(pos, getExternalChunkVersion(versionId));
+ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ return ret;
+ // Paper end - recalculate header on regionfile corruption
} else if (streamLength > buffer.remaining()) {
LOGGER.error("Chunk {} stream is truncated: expected {} but read {}", pos, streamLength, buffer.remaining());
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
} else if (streamLength < 0) {
LOGGER.error("Declared size {} of chunk {} is negative", length, pos);
+ // Paper start - recalculate header on regionfile corruption
+ if (this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ // Paper end - recalculate header on regionfile corruption
return null;
} else {
JvmProfiler.INSTANCE.onRegionFileRead(this.info, pos, this.version, streamLength);
- return this.createChunkInputStream(pos, versionId, createStream(buffer, streamLength));
+ // Paper start - recalculate header on regionfile corruption
+ final DataInputStream ret = this.createChunkInputStream(pos, versionId, createStream(buffer, streamLength));
+ if (ret == null && this.canRecalcHeader && this.recalculateHeader()) {
+ return this.getChunkDataInputStream(pos);
+ }
+ return ret;
+ // Paper end - recalculate header on regionfile corruption
}
}
@@ -357,9 +822,14 @@ public class RegionFile implements AutoCloseable, ca.spottedleaf.moonrise.patche
}
private ByteBuffer createExternalStub() {
+ // Paper start - add version param
+ return this.createExternalStub(this.version);
+ }
+ private ByteBuffer createExternalStub(RegionFileVersion version) {
+ // Paper end - add version param
ByteBuffer stub = ByteBuffer.allocate(5);
stub.putInt(1);
- stub.put((byte)(this.version.getId() | EXTERNAL_STREAM_FLAG));
+ stub.put((byte)(version.getId() | EXTERNAL_STREAM_FLAG)); // Paper - add version param
stub.flip();
return stub;
}
diff --git a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
index 4d3abebdb8b5f952837acb3093ff6e04c44b08e4..6db9314f12dd69ab82d689ab77b7cc6bd53af102 100644
--- a/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
+++ b/net/minecraft/world/level/chunk/storage/RegionFileStorage.java
@@ -27,6 +27,32 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
private final Path folder;
private final boolean sync;
+ // Paper start - recalculate region file headers
+ private final boolean isChunkData;
+
+ @Nullable
+ public static ChunkPos getRegionFileCoordinates(Path file) {
+ String fileName = file.getFileName().toString();
+ if (!fileName.startsWith("r.") || !fileName.endsWith(".mca")) {
+ return null;
+ }
+
+ String[] split = fileName.split("\\.");
+
+ if (split.length != 4) {
+ return null;
+ }
+
+ try {
+ int x = Integer.parseInt(split[1]);
+ int z = Integer.parseInt(split[2]);
+
+ return new ChunkPos(x << 5, z << 5);
+ } catch (NumberFormatException ex) {
+ return null;
+ }
+ }
+ // Paper end
// Paper start - rewrite chunk system
private static final int REGION_SHIFT = 5;
private static final int MAX_NON_EXISTING_CACHE = 1024 * 4;
@@ -144,12 +170,12 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
if (input == null) {
return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData(
- ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.NO_DATA, null, null
+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.NO_DATA, null, null, regionFile == null ? 0 : regionFile.getRecalculateCount() // Paper - Attempt to recalculate regionfile header if it is corrupt
);
}
final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData ret = new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData(
- ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.HAS_DATA, input, null
+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.HAS_DATA, input, null, regionFile.getRecalculateCount() // Paper - Attempt to recalculate regionfile header if it is corrupt
);
if (!(input instanceof ca.spottedleaf.moonrise.patches.chunk_system.util.stream.ExternalChunkStreamMarker)) {
@@ -165,7 +191,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
}
return new ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData(
- ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.SYNC_READ, null, syncRead
+ ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData.ReadResult.SYNC_READ, null, syncRead, regionFile.getRecalculateCount() // Paper - Attempt to recalculate regionfile header if it is corrupt
);
}
@@ -175,7 +201,32 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
final int chunkX, final int chunkZ, final ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO.RegionDataController.ReadData readData
) throws IOException {
try {
- return ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead(readData.input()); // Paper - Optimize NBT reading/writing
+ // Paper start - Attempt to recalculate regionfile header if it is corrupt
+ final CompoundTag ret = ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead(readData.input()); // Paper - Optimize NBT reading/writing
+ if (!this.isChunkData) {
+ return ret;
+ }
+
+ final ChunkPos pos = new ChunkPos(chunkX, chunkZ);
+ final ChunkPos headerChunkPos = SerializableChunkData.getChunkCoordinate(ret);
+ final RegionFile regionFile = this.getRegionFile(pos, true);
+
+ if (regionFile.getRecalculateCount() != readData.recalculateCount()) {
+ return null;
+ }
+
+ if (!headerChunkPos.equals(pos)) {
+ LOGGER.error("Attempting to read chunk data at " + pos + " but got chunk data for " + headerChunkPos + " instead! Attempting regionfile recalculation " + regionFile.getPath().toAbsolutePath());
+ if (regionFile.recalculateHeader()) {
+ return null;
+ }
+
+ LOGGER.error(com.mojang.logging.LogUtils.FATAL_MARKER, "Can't recalculate regionfile header?");
+ return ret;
+ }
+
+ return ret;
+ // Paper end - Attempt to recalculate regionfile header if it is corrupt
} finally {
readData.input().close();
}
@@ -186,6 +237,7 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
this.folder = folder;
this.sync = sync;
this.info = info;
+ this.isChunkData = info.dfuType()[0] == net.minecraft.util.datafix.DataFixTypes.CHUNK; // Paper - recalculate region file headers
}
private @Nullable RegionFile getRegionFile(final ChunkPos pos, final boolean create) throws IOException {
@@ -293,7 +345,25 @@ public class RegionFileStorage implements AutoCloseable, ca.spottedleaf.moonrise
// Paper end
try (DataInputStream regionChunkInputStream = region.getChunkDataInputStream(pos)) {
- return regionChunkInputStream == null ? null : ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead(regionChunkInputStream); // Paper - Optimize NBT reading/writing
+ // Paper start - recover from corrupt regionfile header
+ if (regionChunkInputStream == null) {
+ return null;
+ }
+
+ final CompoundTag serialisedChunkData = ca.spottedleaf.moonrise.patches.nbt_rw.FastNBTParser.nbtIoRead(regionChunkInputStream); // Paper - Optimize NBT reading/writing
+ if (this.isChunkData) {
+ ChunkPos headerChunkPos = SerializableChunkData.getChunkCoordinate(serialisedChunkData);
+ if (!headerChunkPos.equals(pos)) {
+ LOGGER.error("Attempting to read chunk data at {} but got chunk data for {} instead! Attempting regionfile recalculation for regionfile {}", pos, headerChunkPos, region.getPath().toAbsolutePath());
+ if (region.recalculateHeader()) {
+ return this.read(pos);
+ }
+ LOGGER.error("Can't recalculate regionfile header, regenerating chunk {} for {}", pos, region.getPath().toAbsolutePath());
+ return null;
+ }
+ }
+ return serialisedChunkData;
+ // Paper end - recover from corrupt regionfile header
}
}
diff --git a/net/minecraft/world/level/chunk/storage/RegionFileVersion.java b/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
index ea229f63f81c5638ded74c28e1e13d8948f2e5e0..d991fccfc3d2cc51611f802a6d40429024e21d9a 100644
--- a/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
+++ b/net/minecraft/world/level/chunk/storage/RegionFileVersion.java
@@ -21,7 +21,7 @@ import org.slf4j.Logger;
public class RegionFileVersion {
private static final Logger LOGGER = LogUtils.getLogger();
- private static final Int2ObjectMap<RegionFileVersion> VERSIONS = new Int2ObjectOpenHashMap<>();
+ public static final Int2ObjectMap<RegionFileVersion> VERSIONS = new Int2ObjectOpenHashMap<>(); // Paper - private -> public
private static final Object2ObjectMap<String, RegionFileVersion> VERSIONS_BY_NAME = new Object2ObjectOpenHashMap<>();
public static final RegionFileVersion VERSION_GZIP = register(
new RegionFileVersion(1, "gzip", in -> new FastBufferedInputStream(new GZIPInputStream(in)), out -> new BufferedOutputStream(new GZIPOutputStream(out))) // Paper - Enable for Configurable region compression format
diff --git a/net/minecraft/world/level/chunk/storage/RegionStorageInfo.java b/net/minecraft/world/level/chunk/storage/RegionStorageInfo.java
index 590bd1ca60956e5c5be58481f97401e55f9e1aeb..4e0c9fa1eecfa9f21285b7911e25830bbcbc8d86 100644
--- a/net/minecraft/world/level/chunk/storage/RegionStorageInfo.java
+++ b/net/minecraft/world/level/chunk/storage/RegionStorageInfo.java
@@ -3,7 +3,12 @@ package net.minecraft.world.level.chunk.storage;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.level.Level;
-public record RegionStorageInfo(String level, ResourceKey<Level> dimension, String type) {
+// Paper start - crazy hack to indicate what type the region file is
+public record RegionStorageInfo(String level, ResourceKey<Level> dimension, String type, net.minecraft.util.datafix.DataFixTypes[] dfuType) {
+ public RegionStorageInfo(String level, ResourceKey<Level> dimension, String type) {
+ this(level, dimension, type, new net.minecraft.util.datafix.DataFixTypes[1]);
+ }
+ // Paper end - crazy hack to indicate what type the region file is
public RegionStorageInfo withTypeSuffix(final String suffix) {
return new RegionStorageInfo(this.level, this.dimension, this.type + suffix);
}
diff --git a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
index 84a92dc868e61969460dddb156af332e6384bcd1..3fef98e8c3c9908c6f05ded33596cbcb18996c3d 100644
--- a/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
+++ b/net/minecraft/world/level/chunk/storage/SerializableChunkData.java
@@ -119,6 +119,18 @@ public record SerializableChunkData(
}
}
// Paper end - guard against serializing mismatching coordinates
+ // Paper start - Attempt to recalculate regionfile header if it is corrupt
+ // TODO: Check on update
+ public static long getLastWorldSaveTime(final CompoundTag chunkData) {
+ final int dataVersion = NbtUtils.getDataVersion(chunkData);
+ if (dataVersion < 2842) { // Level tag is removed after this version
+ final CompoundTag levelData = chunkData.getCompoundOrEmpty("Level");
+ return levelData.getLongOr("LastUpdate", 0L);
+ } else {
+ return chunkData.getLongOr("LastUpdate", 0L);
+ }
+ }
+ // Paper end - Attempt to recalculate regionfile header if it is corrupt
// Paper start - Do not let the server load chunks from newer versions
private static final int CURRENT_DATA_VERSION = net.minecraft.SharedConstants.getCurrentVersion().dataVersion().version();
@@ -528,7 +540,7 @@ public record SerializableChunkData(
tag.putInt("xPos", this.chunkPos.x());
tag.putInt("yPos", this.minSectionY);
tag.putInt("zPos", this.chunkPos.z());
- tag.putLong("LastUpdate", this.lastUpdateTime);
+ tag.putLong("LastUpdate", this.lastUpdateTime); // Paper - Diff on change
tag.putLong("InhabitedTime", this.inhabitedTime);
tag.putString("Status", BuiltInRegistries.CHUNK_STATUS.getKey(this.chunkStatus).toString());
tag.storeNullable("blending_data", BlendingData.Packed.CODEC, this.blendingData);
diff --git a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
index cdbe291d2053239e3b8dd0042dfb475306a6b6e5..12ac13394fc38f089be0ad7731eb4ea9487e1d17 100644
--- a/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
+++ b/net/minecraft/world/level/chunk/storage/SimpleRegionStorage.java
@@ -34,6 +34,7 @@ public class SimpleRegionStorage implements ca.spottedleaf.moonrise.patches.chun
public SimpleRegionStorage(
final RegionStorageInfo info, final Path folder, final DataFixer fixerUpper, final boolean syncWrites, final DataFixTypes dataFixType
) {
+ info.dfuType()[0] = dataFixType; // Paper - crazy hack to indicate what type the region file is
this.fixerUpper = fixerUpper;
this.dataFixType = dataFixType;
this.storage = new IOWorker(info, folder, syncWrites).storage; // Paper - rewrite chunk system
@@ -42,7 +42,7 @@ index 0a60283625536515bfcf6c2a09ca0f4771d6b142..42500899812fe3ef18c09f68a85012b7
protected ChunkGenerator generator() {
return this.worldGenContext.generator();
diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
index 2dc8b94c434e841b319ef5b77bd725fd66f3c10b..a5540612a4052d9e9d8b1b0ac3c1591777f26caa 100644
index 8f2c37a3a534b3b0d4f3ffd2a0d7013a9b454244..f1ca359abcc422e791c81e036b4777643bce53b3 100644
--- a/net/minecraft/server/level/ServerChunkCache.java
+++ b/net/minecraft/server/level/ServerChunkCache.java
@@ -547,7 +547,18 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
@@ -75,7 +75,7 @@ index 2dc8b94c434e841b319ef5b77bd725fd66f3c10b..a5540612a4052d9e9d8b1b0ac3c15917
profiler.popPush("tickSpawningChunks");
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
index 2cf8854636df72c1dccbb73dfe8f642dfdf1ea37..99403edc1eda1e238b66028b7d236e5fceb235a1 100644
index 60db677d06207b488aad0363af55012b2d76fe8b..eeeb4075a6bbc4e310c2ad685f4efeade702ee3f 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -422,6 +422,10 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -37,7 +37,7 @@ index 42500899812fe3ef18c09f68a85012b705c1010d..fdcbf132e2045bc6828028556ca4c354
// Paper end - Optional per player mob spawns
diff --git a/net/minecraft/server/level/ServerChunkCache.java b/net/minecraft/server/level/ServerChunkCache.java
index a5540612a4052d9e9d8b1b0ac3c1591777f26caa..c4cfb53e145d69666ffc27ef4ee0c50b54eb9143 100644
index f1ca359abcc422e791c81e036b4777643bce53b3..ef53d4d17222a882dd3d33575096c864a0a00a06 100644
--- a/net/minecraft/server/level/ServerChunkCache.java
+++ b/net/minecraft/server/level/ServerChunkCache.java
@@ -552,7 +552,17 @@ public class ServerChunkCache extends ChunkSource implements ca.spottedleaf.moon
@@ -60,7 +60,7 @@ index a5540612a4052d9e9d8b1b0ac3c1591777f26caa..c4cfb53e145d69666ffc27ef4ee0c50b
spawnCookie = NaturalSpawner.createState(chunkCount, this.level, this::getFullChunk, null, true);
} else {
diff --git a/net/minecraft/server/level/ServerPlayer.java b/net/minecraft/server/level/ServerPlayer.java
index 99403edc1eda1e238b66028b7d236e5fceb235a1..23b3a82ab3e1c6abc85000d9285470cb12300a6b 100644
index eeeb4075a6bbc4e310c2ad685f4efeade702ee3f..1137be30f125cb79f05f3e5c104cdf8cca979ff1 100644
--- a/net/minecraft/server/level/ServerPlayer.java
+++ b/net/minecraft/server/level/ServerPlayer.java
@@ -426,6 +426,7 @@ public class ServerPlayer extends Player implements ca.spottedleaf.moonrise.patc
@@ -5,7 +5,7 @@ Subject: [PATCH] DataConverter, Moonrise & co fixes
diff --git a/net/minecraft/server/network/ServerGamePacketListenerImpl.java b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
index 1c92e33104cc131269be15333eeee0ca5f0c92b1..6dc9e669a37ddc0a17099bcc4839b42fe237cf99 100644
index 60decc2534c5bc52b1a22e23a416a7660f724cb0..8178922f5ecc7f6709335c7fbeb6dca505b044bb 100644
--- a/net/minecraft/server/network/ServerGamePacketListenerImpl.java
+++ b/net/minecraft/server/network/ServerGamePacketListenerImpl.java
@@ -1857,9 +1857,14 @@ public class ServerGamePacketListenerImpl
@@ -48,10 +48,10 @@ index 0000000000000000000000000000000000000000..24a2090e068ad3c0d08705050944abdf
+ }
+}
diff --git a/net/minecraft/server/MinecraftServer.java b/net/minecraft/server/MinecraftServer.java
index b675e9322234761a69e71e9e8b9d41364caf34b6..c3bf41af0273e902f881cc070fcef6dd64abb063 100644
index bd5efcf1dee77c204bf29ef4693ac54146e8d901..7517b872794f3bbe556f3abbeb294f426c8da899 100644
--- a/net/minecraft/server/MinecraftServer.java
+++ b/net/minecraft/server/MinecraftServer.java
@@ -1797,6 +1797,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
@@ -1799,6 +1799,7 @@ public abstract class MinecraftServer extends ReentrantBlockableEventLoop<TickTa
level.hasPhysicsEvent = org.bukkit.event.block.BlockPhysicsEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - BlockPhysicsEvent
level.hasEntityMoveEvent = io.papermc.paper.event.entity.EntityMoveEvent.getHandlerList().getRegisteredListeners().length > 0; // Paper - Add EntityMoveEvent
level.updateLagCompensationTick(); // Paper - lag compensation
@@ -5,7 +5,7 @@ Subject: [PATCH] Pre-load data off-main during startup
diff --git a/net/minecraft/server/Main.java b/net/minecraft/server/Main.java
index c22cc4387e7cc5da7d38ee14ed7f187b5d9507f4..aa7f2ba4d931f98ce15c52dc2b99f07504149582 100644
index b515d8be0e2f0639f7fc843d6ce074b0a583c673..984cf7143e3809507c557e0e70ab271b753db235 100644
--- a/net/minecraft/server/Main.java
+++ b/net/minecraft/server/Main.java
@@ -102,7 +102,10 @@ public class Main {
@@ -140,7 +140,7 @@
WorldStem worldStem;
try {
@@ -169,17 +_,31 @@
@@ -169,17 +_,40 @@
executor -> WorldLoader.load(
worldLoadConfig,
context -> {
@@ -161,8 +161,16 @@
+ }
+ // Paper end - migrate startup world
LevelDataAndDimensions worldData = LevelStorageSource.getLevelDataAndDimensions(
- access, levelDataTag, context.dataConfiguration(), datapackDimensions, context.datapackWorldRegistries()
+ access, levelDataTag, context.dataConfiguration(), datapackDimensions, context.datapackWorldRegistries(), net.minecraft.world.level.Level.OVERWORLD // Paper
access, levelDataTag, context.dataConfiguration(), datapackDimensions, context.datapackWorldRegistries()
+ // Paper start - pass dimension and regenerate missing world gen settings
+ , net.minecraft.world.level.Level.OVERWORLD
+ , () -> {
+ final DedicatedServerProperties properties = settings.getProperties();
+ return new WorldGenSettings(
+ properties.worldOptions, properties.createDimensions(context.datapackWorldRegistries())
+ );
+ }
+ // Paper end - pass dimension and regenerate missing world gen settings
);
return new WorldLoader.DataLoadOutput<>(
worldData.worldDataAndGenSettings(), worldData.dimensions().dimensionsRegistryAccess()
@@ -33,10 +33,11 @@
private @Nullable ServerStatus status;
private ServerStatus.@Nullable Favicon statusIcon;
private final RandomSource random = RandomSource.create();
@@ -234,13 +_,14 @@
@@ -234,13 +_,15 @@
private Map<ResourceKey<Level>, ServerLevel> levels = Maps.newLinkedHashMap();
private PlayerList playerList;
private volatile boolean running = true;
+ public volatile boolean hasStarted = false; // Paper
+ private volatile boolean isRestarting = false; // Paper - flag to signify we're attempting to restart
private boolean stopped;
private int tickCount;
@@ -758,7 +759,7 @@
protected void runServer() {
try {
if (!this.initServer()) {
@@ -710,30 +_,73 @@
@@ -710,30 +_,74 @@
}
this.nextTickTimeNanos = Util.getNanos();
@@ -778,6 +779,7 @@
+ // tasks are default scheduled at -1 + delay, and first tick will tick at 1
+ final long actualDoneTimeMs = System.currentTimeMillis() - org.bukkit.craftbukkit.Main.BOOT_TIME.toEpochMilli(); // Paper - Improve startup message
+ LOGGER.info("Done ({})! For help, type \"help\"", String.format(java.util.Locale.ROOT, "%.3fs", actualDoneTimeMs / 1000.00D)); // Paper - Improve startup message
+ this.hasStarted = true;
+ org.spigotmc.WatchdogThread.tick();
+ // Paper end
+ org.spigotmc.WatchdogThread.hasStarted = true; // Paper
@@ -0,0 +1,20 @@
--- a/net/minecraft/server/commands/RandomCommand.java
+++ b/net/minecraft/server/commands/RandomCommand.java
@@ -42,7 +_,7 @@
.then(drawRandomValueTree("roll", true))
.then(
Commands.literal("reset")
- .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
+ .requires(stack -> stack.getBukkitSender().hasPermission("minecraft.command.random.reset")) // Paper - specific permissions for sub command
.then(
Commands.literal("*")
.executes(c -> resetAllSequences(c.getSource()))
@@ -122,7 +_,7 @@
.then(
Commands.argument("sequence", IdentifierArgument.id())
.suggests(RandomCommand::suggestRandomSequence)
- .requires(Commands.hasPermission(Commands.LEVEL_GAMEMASTERS))
+ .requires(stack -> stack.getBukkitSender().hasPermission("minecraft.command.random.sequence." + (announce ? "roll" : "value"))) // Paper - specific permissions for sub command
.executes(
c -> randomSample(c.getSource(), RangeArgument.Ints.getRange(c, "range"), IdentifierArgument.getId(c, "sequence"), announce)
)
@@ -45,11 +45,11 @@
+ // Paper start - per-world time
+ ServerClockManager clockManager = source.getLevel().clockManager();
+ long currentTotalTicks = clockManager.getInstance(clock).totalTicks();
+ org.bukkit.event.world.ClockTimeSkipEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.createTimeSkipEvent(source, totalTicks - currentTotalTicks);
+ org.bukkit.event.world.ClockTimeSkipEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.createTimeSkipEvent(source, clock, totalTicks - currentTotalTicks);
+ final long newTotalTicks;
+ if (event.callEvent()) {
+ newTotalTicks = currentTotalTicks + event.getSkipAmount();
+ clockManager.setTotalTicks(clock, newTotalTicks);
+ final boolean cancelled = !event.callEvent();
+ if (!cancelled) {
+ newTotalTicks = Math.max(currentTotalTicks + event.getSkipAmount(), 0L);
+ } else {
+ newTotalTicks = totalTicks; // just pass it through
+ }
@@ -60,7 +60,7 @@
- clockManager.setTotalTicks(clock, totalTicks);
- source.sendSuccess(() -> Component.translatable("commands.time.set.absolute", clock.getRegisteredName(), totalTicks), true);
- return totalTicks;
+ clockManager.setTotalTicks(clock, newTotalTicks);
+ if (!cancelled) clockManager.setTotalTicks(clock, newTotalTicks);
+ source.sendSuccess(() -> Component.translatable("commands.time.set.absolute", clock.getRegisteredName(), newTotalTicks), true);
+ return wrapTime(newTotalTicks);
+ // Paper end - per-world time
@@ -71,7 +71,7 @@
- clockManager.addTicks(clock, time);
+ // Paper start - per-world time
+ ServerClockManager clockManager = source.getLevel().clockManager();
+ org.bukkit.event.world.ClockTimeSkipEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.createTimeSkipEvent(source, time);
+ org.bukkit.event.world.ClockTimeSkipEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.createTimeSkipEvent(source, clock, time);
+ if (event.callEvent()) {
+ clockManager.addTicks(clock, event.getSkipAmount());
+ }
@@ -99,7 +99,7 @@
+ if (timerMove == null) return ServerClockManager.MoveResult.NO_TIME_MARKER_FOUND;
+
+ // We consider it an attempted move even if the marker occurs at this point in time for event calling purposes.
+ final org.bukkit.event.world.ClockTimeSkipEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.createTimeSkipEvent(source, timerMove.targetTime() - timerMove.currentTime());
+ final org.bukkit.event.world.ClockTimeSkipEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.createTimeSkipEvent(source, clock, timerMove.targetTime() - timerMove.currentTime());
+ if (!event.callEvent() || event.getSkipAmount() == 0) return ServerClockManager.MoveResult.NOT_MOVED;
+
+ clockManager.setTotalTicks(clock, timerMove.currentTime() + event.getSkipAmount());
@@ -276,7 +276,7 @@
}
@Override
@@ -373,11 +_,25 @@
@@ -373,11 +_,26 @@
int percentage = this.getGameRules().get(GameRules.PLAYERS_SLEEPING_PERCENTAGE);
if (this.sleepStatus.areEnoughSleeping(percentage) && this.sleepStatus.areEnoughDeepSleeping(percentage, this.players)) {
Optional<Holder<WorldClock>> defaultClock = this.dimensionType().defaultClock();
@@ -286,8 +286,9 @@
+ // Paper start - per-world time
+ ServerClockManager.TimerMove timerMove = this.clockManager().resolveTimerMove(defaultClock.get(), ClockTimeMarkers.WAKE_UP_FROM_SLEEP);
+ if (timerMove != null) {
+ event = new org.bukkit.event.world.TimeSkipEvent(
+ this.getWorld(),
+ event = org.bukkit.craftbukkit.event.CraftEventFactory.createTimeSkipEvent(
+ this,
+ defaultClock.get(),
+ org.bukkit.event.world.ClockTimeSkipEvent.SkipReason.NIGHT_SKIP,
+ timerMove.targetTime() - timerMove.currentTime()
+ );
@@ -60,7 +60,8 @@
+ this.inventory.getItems().stream().filter(item -> !item.isEmpty()).forEach(stack -> this.spawnAtLocation(level, stack)); // Paper - delay removal post event - taken from removeAllItems
+ this.postDeathEventTasks.add(this.inventory::clearContent); // Paper - only clear after death event
ItemStack itemStack = this.getItemBySlot(EquipmentSlot.MAINHAND);
if (!itemStack.isEmpty() && !EnchantmentHelper.has(itemStack, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) {
- if (!itemStack.isEmpty() && !EnchantmentHelper.has(itemStack, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP)) {
+ if (!itemStack.isEmpty() && !EnchantmentHelper.has(itemStack, EnchantmentEffectComponents.PREVENT_EQUIPMENT_DROP) && !this.droppedEquipmentSlots.contains(EquipmentSlot.MAINHAND)) { // Paper
this.spawnAtLocation(level, itemStack);
- this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY);
+ this.postDeathEventTasks.add(() -> this.setItemSlot(EquipmentSlot.MAINHAND, ItemStack.EMPTY)); // Paper
@@ -29,3 +29,20 @@
return this.container.stillValid(player);
}
@@ -107,7 +_,15 @@
if (this.player instanceof ServerPlayer serverPlayer) {
ServerLevel level = serverPlayer.level();
CraftingInput craftInput = this.container.asCraftInput();
- ItemStack result = CrafterBlock.getPotentialResults(level, craftInput).map(recipe -> recipe.value().assemble(craftInput)).orElse(ItemStack.EMPTY);
+ // Paper start - Fire PrepareItemCraftEvent for Crafter preview
+ final java.util.Optional<net.minecraft.world.item.crafting.RecipeHolder<net.minecraft.world.item.crafting.CraftingRecipe>> potentialResults = CrafterBlock.getPotentialResults(level, craftInput);
+ ItemStack result = org.bukkit.craftbukkit.event.CraftEventFactory.callPreCraftEvent(
+ this.container,
+ this.resultContainer,
+ potentialResults.map(recipe -> recipe.value().assemble(craftInput)).orElse(ItemStack.EMPTY),
+ this.getBukkitView(),
+ potentialResults.map(net.minecraft.world.item.crafting.RecipeHolder::value).orElse(null) instanceof net.minecraft.world.item.crafting.RepairItemRecipe);
+ // Paper end - Fire PrepareItemCraftEvent for Crafter preview
this.resultContainer.setItem(0, result);
}
}
@@ -4,7 +4,7 @@
import org.jspecify.annotations.Nullable;
public class BucketItem extends Item implements DispensibleContainerItem {
+ private static @Nullable ItemStack itemLeftInHandAfterPlayerBucketEmptyEvent = null; // Paper - Fix PlayerBucketEmptyEvent result itemstack
+ static @Nullable ItemStack itemLeftInHandAfterPlayerBucketEmptyEvent = null; // Paper - Fix PlayerBucketEmptyEvent result itemstack
protected final Fluid content;
public BucketItem(final Fluid content, final Item.Properties properties) {
@@ -65,7 +65,7 @@
+ return this.emptyContents(user, level, pos, hitResult, null, null, null, InteractionHand.MAIN_HAND);
+ }
+
+ public boolean emptyContents(final @Nullable LivingEntity user, final Level level, final BlockPos pos, final @Nullable BlockHitResult hitResult, final Direction direction, final BlockPos clicked, final ItemStack itemStack, final InteractionHand hand) {
+ public boolean emptyContents(final @Nullable LivingEntity user, final Level level, final BlockPos pos, final @Nullable BlockHitResult hitResult, final @Nullable Direction direction, final @Nullable BlockPos clicked, final @Nullable ItemStack itemStack, final InteractionHand hand) {
+ // CraftBukkit end
if (!(this.content instanceof FlowingFluid flowingFluid)) {
return false;
@@ -1,6 +1,6 @@
--- a/net/minecraft/world/item/MobBucketItem.java
+++ b/net/minecraft/world/item/MobBucketItem.java
@@ -52,18 +_,18 @@
@@ -52,18 +_,28 @@
}
if (mob != null) {
@@ -14,6 +14,16 @@
- public boolean emptyContents(final @Nullable LivingEntity user, final Level level, final BlockPos pos, final @Nullable BlockHitResult hitResult) {
+ public boolean emptyContents(final @Nullable LivingEntity user, final Level level, final BlockPos pos, final @Nullable BlockHitResult hitResult, final net.minecraft.core.Direction direction, final BlockPos clicked, final ItemStack itemStack, final net.minecraft.world.InteractionHand hand) { // Paper
if (this.content == Fluids.EMPTY) {
+ // Paper start - call PlayerBucketEmptyEvent for mobs in buckets without fluids
+ if (user instanceof net.minecraft.world.entity.player.Player player) {
+ org.bukkit.event.player.PlayerBucketEmptyEvent event = org.bukkit.craftbukkit.event.CraftEventFactory.callPlayerBucketEmptyEvent(level, player, pos, clicked, direction, itemStack, hand);
+ if (event.isCancelled()) {
+ player.containerMenu.sendAllDataToRemote(); // SPIGOT-4541
+ return false;
+ }
+ BucketItem.itemLeftInHandAfterPlayerBucketEmptyEvent = event.getItemStack() != null ? event.getItemStack().equals(org.bukkit.inventory.ItemStack.of(org.bukkit.Material.BUCKET)) ? null : org.bukkit.craftbukkit.inventory.CraftItemStack.asNMSCopy(event.getItemStack()) : ItemStack.EMPTY; // Paper - Fix PlayerBucketEmptyEvent result itemstack
+ }
+ // Paper end
this.playEmptySound(user, level, pos);
return true;
} else {
@@ -0,0 +1,23 @@
--- a/net/minecraft/world/level/block/entity/PotentSulfurBlockEntity.java
+++ b/net/minecraft/world/level/block/entity/PotentSulfurBlockEntity.java
@@ -127,7 +_,19 @@
&& !entityToBeLaunched.isPassenger()
&& !entityToBeLaunched.is(EntityTypeTags.NOT_AFFECTED_BY_GEYSERS)
&& entityVelocity.y < 0.3F + waterBlocks * 0.1) {
- entityToBeLaunched.addDeltaMovement(new Vec3(0.0, 0.2F, 0.0));
+ // Paper start - add EntityPotentSulfurLaunchEvent
+ final Vec3 launchVelocity = new Vec3(0.0, GEYSER_LAUNCH_FORCE, 0.0);
+ if (io.papermc.paper.event.entity.EntityPotentSulfurLaunchEvent.getHandlerList().hasListeners()) {
+ io.papermc.paper.event.entity.EntityPotentSulfurLaunchEvent event = new io.papermc.paper.event.entity.EntityPotentSulfurLaunchEvent(
+ entityToBeLaunched.getBukkitEntity(),
+ org.bukkit.craftbukkit.block.CraftBlock.at(level, pos),
+ org.bukkit.craftbukkit.util.CraftLocation.toBukkit(sourceBlock, level.getWorld()),
+ org.bukkit.craftbukkit.util.CraftVector.toBukkit(launchVelocity)
+ );
+ event.callEvent();
+ }
+ entityToBeLaunched.addDeltaMovement(launchVelocity);
+ // Paper end - add EntityPotentSulfurLaunchEvent
}
}
}
@@ -16,7 +16,7 @@
Feature feature = featureHolder.value();
removeSapling(level, pos);
if (feature.place(level, generator, random, pos)) {
@@ -229,4 +_,60 @@
@@ -229,4 +_,64 @@
private record TwoByTwoSaplingPos(int offsetX, int offsetZ, List<Pair<BlockState, BlockPos>> groundLevelSurroundingBlocks) {
}
@@ -69,8 +69,12 @@
+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK;
+ } else if (feature.is(TreeFeatures.PALE_OAK_CREAKING)) {
+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.PALE_OAK_CREAKING;
+ } else if (feature.is(TreeFeatures.RED_POPLAR) || feature.is(TreeFeatures.ORANGE_POPLAR) || feature.is(TreeFeatures.YELLOW_POPLAR)) {
+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.POPLAR;
+ } else if (feature.is(TreeFeatures.RED_POPLAR)) {
+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.RED_POPLAR;
+ } else if (feature.is(TreeFeatures.ORANGE_POPLAR)) {
+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.ORANGE_POPLAR;
+ } else if (feature.is(TreeFeatures.YELLOW_POPLAR)) {
+ net.minecraft.world.level.block.SaplingBlock.treeType = org.bukkit.TreeType.YELLOW_POPLAR;
+ } else {
+ throw new IllegalArgumentException("Unknown tree generator " + feature);
+ }
@@ -8,8 +8,8 @@
+ if (numSectors == 255) {
+ // We're maxed out, so we need to read the proper length from the section
+ ByteBuffer realLen = ByteBuffer.allocate(4);
+ this.file.read(realLen, sectorNumber * 4096);
+ numSectors = (realLen.getInt(0) + 4) / 4096 + 1;
+ this.file.read(realLen, sectorNumber * 4096L);
+ numSectors = sizeToSectors(realLen.getInt(0) + 4);
+ }
+ // Spigot end
if (sectorNumber < 2) {
@@ -22,8 +22,8 @@
+ // Spigot start
+ if (numSectors == 255) {
+ ByteBuffer realLen = ByteBuffer.allocate(4);
+ this.file.read(realLen, sectorNumber * 4096);
+ numSectors = (realLen.getInt(0) + 4) / 4096 + 1;
+ this.file.read(realLen, sectorNumber * 4096L);
+ numSectors = sizeToSectors(realLen.getInt(0) + 4);
+ }
+ // Spigot end
int sectorsLength = numSectors * 4096;
@@ -1,11 +1,32 @@
--- a/net/minecraft/world/level/storage/LevelStorageSource.java
+++ b/net/minecraft/world/level/storage/LevelStorageSource.java
@@ -145,13 +_,14 @@
@@ -145,21 +_,48 @@
final WorldDataConfiguration dataConfiguration,
final Registry<LevelStem> datapackDimensions,
final HolderLookup.Provider registryAccess
- ) {
+ , final ResourceKey<Level> dimension // Paper - pass dimension
) {
+ ) {
+ // Paper start - Regenerate missing overworld world gen settings
+ return getLevelDataAndDimensions(worldAccess, levelDataTag, dataConfiguration, datapackDimensions, registryAccess, dimension, () -> {
+ // Preserve dimension settings if we got them from the datapack
+ WorldDimensions defaultDimensions = datapackDimensions.containsKey(LevelStem.OVERWORLD)
+ ? new WorldDimensions(datapackDimensions)
+ : net.minecraft.world.level.levelgen.presets.WorldPresets.createNormalWorldDimensions(registryAccess);
+ return new WorldGenSettings(WorldOptions.defaultWithRandomSeed(), defaultDimensions);
+ });
+ }
+
+ public static LevelDataAndDimensions getLevelDataAndDimensions(
+ final LevelStorageSource.LevelStorageAccess worldAccess,
+ final Dynamic<?> levelDataTag,
+ final WorldDataConfiguration dataConfiguration,
+ final Registry<LevelStem> datapackDimensions,
+ final HolderLookup.Provider registryAccess,
+ final ResourceKey<Level> dimension,
+ final java.util.function.Supplier<WorldGenSettings> worldGenSettingsFactory
+ ) {
+ // Paper end - Regenerate missing overworld world gen settings
if (DataFixers.getFileFixer().requiresFileFixing(NbtUtils.getDataVersion(levelDataTag))) {
throw new IllegalStateException("Cannot get level data without file fixing first");
}
@@ -16,6 +37,24 @@
.mapOrElse(
Function.identity(),
error -> {
- LOGGER.error(
- "Unable to read or access the world gen settings file! Falling back to the default settings with a random world seed. {}",
- error.message()
+ // Paper start - Regenerate missing overworld world gen settings
+ Path dataLocation = WorldGenSettings.TYPE.id().withSuffix(".dat").resolveAgainst(worldAccess.getDimensionPath(dimension).resolve(LevelResource.DATA.id()));
+ if (!Files.notExists(dataLocation, java.nio.file.LinkOption.NOFOLLOW_LINKS)) {
+ throw new IllegalStateException("Unable to read or access the world gen settings file " + dataLocation + ". " + error.message());
+ }
+ LOGGER.warn(
+ "World gen settings file is missing! Regenerating settings from server.properties: {}",
+ dataLocation
);
- return new WorldGenSettings(WorldOptions.defaultWithRandomSeed(), new WorldDimensions(datapackDimensions));
+ return worldGenSettingsFactory.get();
+ // Paper end - Regenerate missing overworld world gen settings
}
);
LevelSettings settings = LevelSettings.parse(dataTag, dataConfiguration);
@@ -170,9 +_,9 @@
}
@@ -1,5 +1,13 @@
--- a/net/minecraft/world/scores/Scoreboard.java
+++ b/net/minecraft/world/scores/Scoreboard.java
@@ -267,6 +_,7 @@
}
this.onTeamRemoved(team);
+ team.getPlayers().clear(); // Paper - Clear scoreboard team entries on unregister
}
public boolean addPlayerToTeam(final String player, final PlayerTeam team) {
@@ -373,7 +_,7 @@
}
@@ -0,0 +1,40 @@
--- a/net/minecraft/world/timeline/Timeline.java
+++ b/net/minecraft/world/timeline/Timeline.java
@@ -142,6 +_,23 @@
}
}
+ // Paper start - clock API - value equality for registry builder round-trips
+ @Override
+ public boolean equals(final Object o) {
+ if (this == o) return true;
+ if (!(o instanceof final Timeline other)) return false;
+ return this.clock.equals(other.clock)
+ && this.periodTicks.equals(other.periodTicks)
+ && this.tracks.equals(other.tracks)
+ && this.timeMarkers.equals(other.timeMarkers);
+ }
+
+ @Override
+ public int hashCode() {
+ return java.util.Objects.hash(this.clock, this.periodTicks, this.tracks, this.timeMarkers);
+ }
+ // Paper end - clock API - value equality for registry builder round-trips
+
public Set<EnvironmentAttribute<?>> attributes() {
return this.tracks.keySet();
}
@@ -181,6 +_,13 @@
this.tracks.put(attribute, new AttributeTrack<>(modifier, argumentTrack.build()));
return this;
}
+
+ // Paper start - clock API
+ public Timeline.Builder addTracks(final Map<EnvironmentAttribute<?>, AttributeTrack<?, ?>> tracks) {
+ this.tracks.putAll(tracks);
+ return this;
+ }
+ // Paper end - clock API
public <Value> Timeline.Builder addTrack(final EnvironmentAttribute<Value> attribute, final Consumer<KeyframeTrack.Builder<Value>> builder) {
return this.addModifierTrack(attribute, AttributeModifier.override(), builder);
@@ -2,6 +2,8 @@ package io.papermc.paper.command.brigadier;
import com.destroystokyo.paper.brigadier.BukkitBrigadierCommandSource;
import com.google.common.base.Preconditions;
import io.papermc.paper.adventure.PaperAdventure;
import net.kyori.adventure.text.ComponentLike;
import com.mojang.brigadier.exceptions.CommandSyntaxException;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec2;
@@ -49,6 +51,24 @@ public interface PaperCommandSourceStack extends CommandSourceStack, BukkitBriga
return this.getHandle().withEntity(((CraftEntity) executor).getHandle());
}
@Override
default void sendReply(final ComponentLike message) {
Preconditions.checkArgument(message != null, "message cannot be null.");
this.getHandle().sendSystemMessage(PaperAdventure.asVanilla(message.asComponent()));
}
@Override
default void sendSuccess(final ComponentLike message, final boolean allowInformingAdmins) {
Preconditions.checkArgument(message != null, "message cannot be null.");
this.getHandle().sendSuccess(() -> PaperAdventure.asVanilla(message.asComponent()), allowInformingAdmins);
}
@Override
default void sendFailure(final ComponentLike message) {
Preconditions.checkArgument(message != null, "message cannot be null.");
this.getHandle().sendFailure(PaperAdventure.asVanilla(message.asComponent()), false);
}
@Override
default Player getPlayerOrThrow() throws CommandSyntaxException {
return this.getHandle().getPlayerOrException().getBukkitEntity();
@@ -12,10 +12,11 @@ import org.jspecify.annotations.NullMarked;
@NullMarked
public interface PluginMessageBridgeImpl {
boolean DISABLE_CHANNEL_LIMIT = System.getProperty("paper.disableChannelLimit") != null; // Paper - add a flag to disable the channel limit
boolean DISABLE_CHANNEL_LIMIT = System.getProperty("paper.disableChannelLimit") != null;
int CHANNEL_LIMIT = Integer.getInteger("paper.pluginMessageChannelLimit", 128);
default boolean addChannel(String channel) {
Preconditions.checkState(DISABLE_CHANNEL_LIMIT || this.channels().size() < 128, "Cannot register channel. Too many channels registered!"); // Paper - flag to disable channel limit
Preconditions.checkState(DISABLE_CHANNEL_LIMIT || this.channels().size() < CHANNEL_LIMIT, "Cannot register channel. Too many channels registered!");
channel = StandardMessenger.validateAndCorrectChannel(channel);
if (channels().add(channel)) {
if (this instanceof CraftPlayer player) {
@@ -37,7 +38,7 @@ public interface PluginMessageBridgeImpl {
}
return false;
};
}
Set<String> channels();
}
@@ -25,14 +25,18 @@ import io.papermc.paper.registry.data.PaperPaintingVariantRegistryEntry;
import io.papermc.paper.registry.data.PaperPigVariantRegistryEntry;
import io.papermc.paper.registry.data.PaperSoundEventRegistryEntry;
import io.papermc.paper.registry.data.PaperSulfurCubeArchetypeRegistryEntry;
import io.papermc.paper.registry.data.PaperTimelineRegistryEntry;
import io.papermc.paper.registry.data.PaperTrimMaterialRegistryEntry;
import io.papermc.paper.registry.data.PaperTrimPatternRegistryEntry;
import io.papermc.paper.registry.data.PaperWolfVariantRegistryEntry;
import io.papermc.paper.registry.data.PaperWorldClockRegistryEntry;
import io.papermc.paper.registry.data.PaperZombieNautilusVariantRegistryEntry;
import io.papermc.paper.registry.data.dialog.PaperDialogRegistryEntry;
import io.papermc.paper.registry.entry.RegistryEntry;
import io.papermc.paper.registry.entry.RegistryEntryMeta;
import io.papermc.paper.registry.tag.TagKey;
import io.papermc.paper.world.Timeline;
import io.papermc.paper.world.WorldClock;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
@@ -60,6 +64,8 @@ import org.bukkit.craftbukkit.CraftGameRule;
import org.bukkit.craftbukkit.CraftJukeboxSong;
import org.bukkit.craftbukkit.CraftMusicInstrument;
import org.bukkit.craftbukkit.CraftSound;
import org.bukkit.craftbukkit.CraftTimeline;
import org.bukkit.craftbukkit.CraftWorldClock;
import org.bukkit.craftbukkit.attribute.CraftAttribute;
import org.bukkit.craftbukkit.block.CraftBiome;
import org.bukkit.craftbukkit.block.CraftBlockType;
@@ -159,6 +165,8 @@ public final class PaperRegistries {
start(Registries.ZOMBIE_NAUTILUS_VARIANT, RegistryKey.ZOMBIE_NAUTILUS_VARIANT).craft(ZombieNautilus.Variant.class, CraftZombieNautilus.CraftVariant::new).writable(PaperZombieNautilusVariantRegistryEntry.PaperBuilder::new),
start(Registries.SULFUR_CUBE_ARCHETYPE, RegistryKey.SULFUR_CUBE_ARCHETYPE).craft(SulfurCube.Archetype.class, CraftSulfurCube.CraftArchetype::new).writable(PaperSulfurCubeArchetypeRegistryEntry.PaperBuilder::new),
start(Registries.DIALOG, RegistryKey.DIALOG).craft(Dialog.class, PaperDialog::new, true).writable(PaperDialogRegistryEntry.PaperBuilder::new),
start(Registries.WORLD_CLOCK, RegistryKey.WORLD_CLOCK).craft(WorldClock.class, CraftWorldClock::new).writable(PaperWorldClockRegistryEntry.PaperBuilder::new).delayed(),
start(Registries.TIMELINE, RegistryKey.TIMELINE).craft(Timeline.class, CraftTimeline::new).writable(PaperTimelineRegistryEntry.PaperBuilder::new).delayed(),
// api-only
start(Registries.ENTITY_TYPE, RegistryKey.ENTITY_TYPE).apiOnly(PaperSimpleRegistry::entityType),
@@ -0,0 +1,103 @@
package io.papermc.paper.registry.data;
import com.google.common.base.Preconditions;
import io.papermc.paper.adventure.PaperAdventure;
import io.papermc.paper.registry.PaperRegistries;
import io.papermc.paper.registry.PaperRegistryBuilder;
import io.papermc.paper.registry.TypedKey;
import io.papermc.paper.registry.data.util.Conversions;
import io.papermc.paper.world.WorldClock;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.OptionalInt;
import net.kyori.adventure.key.Key;
import net.minecraft.core.Holder;
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.timeline.Timeline;
import org.jetbrains.annotations.Range;
import org.jspecify.annotations.Nullable;
import static io.papermc.paper.registry.data.util.Checks.asArgument;
import static io.papermc.paper.registry.data.util.Checks.asConfigured;
public class PaperTimelineRegistryEntry implements TimelineRegistryEntry {
protected final Conversions conversions;
protected @Nullable Timeline internal;
protected @Nullable Holder<net.minecraft.world.clock.WorldClock> clock;
protected @Nullable Integer periodTicks;
protected final Map<Key, TimeMarker> timeMarkers = new LinkedHashMap<>();
public PaperTimelineRegistryEntry(final Conversions conversions, final @Nullable Timeline internal) {
this.conversions = conversions;
this.internal = internal;
if (internal == null) {
return;
}
this.clock = internal.clock();
this.periodTicks = internal.periodTicks().orElse(null);
internal.registerTimeMarkers((key, marker) -> this.timeMarkers.put(PaperAdventure.asAdventure(key.identifier()), TimeMarker.of(marker.ticks(), marker.showInCommands())));
}
@Override
public TypedKey<WorldClock> clock() {
return PaperRegistries.fromNms(asConfigured(this.clock, "clock").unwrapKey().orElseThrow());
}
@Override
public OptionalInt periodTicks() {
return this.periodTicks != null ? OptionalInt.of(this.periodTicks) : OptionalInt.empty();
}
@Override
public Map<Key, TimeMarker> timeMarkers() {
return Map.copyOf(this.timeMarkers);
}
public static final class PaperBuilder extends PaperTimelineRegistryEntry implements TimelineRegistryEntry.Builder, PaperRegistryBuilder<Timeline, io.papermc.paper.world.Timeline> {
public PaperBuilder(final Conversions conversions, final @Nullable Timeline internal) {
super(conversions, internal);
}
@Override
public Builder clock(final TypedKey<WorldClock> clock) {
this.clock = this.conversions.getReferenceHolder(PaperRegistries.toNms(asArgument(clock, "clock")));
return this;
}
@Override
public Builder periodTicks(final @Range(from = 1, to = Integer.MAX_VALUE) int periodTicks) {
Preconditions.checkArgument(periodTicks >= 1, "periodTicks must be at least 1, was %s", periodTicks);
this.periodTicks = periodTicks;
return this;
}
@Override
public Builder clearPeriodTicks() {
this.periodTicks = null;
return this;
}
@Override
public Builder addTimeMarker(final Key key, final @Range(from = 0, to = Integer.MAX_VALUE) int ticks, final boolean showInCommands) {
Preconditions.checkArgument(ticks >= 0, "ticks must be at least 0, was %s", ticks);
this.timeMarkers.put(asArgument(key, "key"), TimeMarker.of(ticks, showInCommands));
return this;
}
@Override
public Timeline build() {
final Timeline.Builder builder = Timeline.builder(asConfigured(this.clock, "clock"));
if (this.periodTicks != null) {
builder.setPeriodTicks(this.periodTicks);
}
if (this.internal != null) {
builder.addTracks(this.internal.tracks);
}
this.timeMarkers.forEach((key, marker) -> builder.addTimeMarker(ResourceKey.create(net.minecraft.world.clock.ClockTimeMarkers.ROOT_ID, PaperAdventure.asVanilla(key)), marker.ticks(), marker.showInCommands()));
return builder.build();
}
}
}
@@ -0,0 +1,24 @@
package io.papermc.paper.registry.data;
import io.papermc.paper.registry.PaperRegistryBuilder;
import io.papermc.paper.registry.data.util.Conversions;
import net.minecraft.world.clock.WorldClock;
import org.jspecify.annotations.Nullable;
public class PaperWorldClockRegistryEntry implements WorldClockRegistryEntry {
public PaperWorldClockRegistryEntry(final Conversions ignoredConversions, final @Nullable WorldClock ignoredInternal) {
}
public static final class PaperBuilder extends PaperWorldClockRegistryEntry implements WorldClockRegistryEntry.Builder, PaperRegistryBuilder<WorldClock, io.papermc.paper.world.WorldClock> {
public PaperBuilder(final Conversions conversions, final @Nullable WorldClock internal) {
super(conversions, internal);
}
@Override
public WorldClock build() {
return new WorldClock();
}
}
}
@@ -233,6 +233,7 @@ public final class FoliaAsyncScheduler implements AsyncScheduler {
this.delay = FoliaAsyncScheduler.this.timerThread.schedule(this, delay, TimeUnit.NANOSECONDS);
} else {
// cancelled repeating task
this.state = STATE_CANCELLED;
removeFromTasks = true;
}
}
@@ -174,13 +174,18 @@ public final class FoliaEntityScheduler implements EntityScheduler {
this.plugin.getLogger().log(Level.WARNING, "Entity task for " + this.plugin.getDescription().getFullName() + " generated an exception", throwable);
} finally {
boolean reschedule = false;
if (!repeating && !retired) {
if (!repeating && !retired) {
this.setStateVolatile(STATE_FINISHED);
} else if (retired || !this.plugin.isEnabled()) {
this.setStateVolatile(STATE_CANCELLED);
} else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) {
reschedule = true;
} // else: cancelled repeating task
this.setStateVolatile(STATE_CANCELLED);
} else {
final int previousState = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE);
if (previousState == STATE_EXECUTING) {
reschedule = true;
} else if (previousState == STATE_EXECUTING_CANCELLED) {
this.setStateVolatile(STATE_CANCELLED);
}
}
if (!reschedule) {
this.run = null;
@@ -182,9 +182,14 @@ public class FoliaGlobalRegionScheduler implements GlobalRegionScheduler {
boolean reschedule = false;
if (!repeating) {
this.setStateVolatile(STATE_FINISHED);
} else if (STATE_EXECUTING == this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE)) {
reschedule = true;
} // else: cancelled repeating task
} else {
final int previousState = this.compareAndExchangeStateVolatile(STATE_EXECUTING, STATE_IDLE);
if (previousState == STATE_EXECUTING) {
reschedule = true;
} else if (previousState == STATE_EXECUTING_CANCELLED) {
this.setStateVolatile(STATE_CANCELLED);
}
}
if (!reschedule) {
this.run = null;
@@ -144,7 +144,9 @@ public record PaperWorldLoader(MinecraftServer server, String levelId) {
return;
}
final WorldGenSettings worldGenSettings = !hasWorldData
final boolean isOverworldLevel = loading.info().dimensionKey() == Level.OVERWORLD;
final WorldGenSettings worldGenSettings = isOverworldLevel || !hasWorldData
? this.server.getWorldGenSettings()
: loadWorldGenSettings(
this.server.storageSource,
@@ -153,7 +155,7 @@ public record PaperWorldLoader(MinecraftServer server, String levelId) {
);
final var worldDataAndGenSettings = new LevelDataAndDimensions.WorldDataAndGenSettings(this.server.getWorldData(), worldGenSettings);
if (loading.info().dimensionKey() == Level.OVERWORLD) {
if (isOverworldLevel) {
final var primaryLevelData = ((PrimaryLevelData) this.server.getWorldData());
primaryLevelData.checkName(loading.data().bukkitName());
primaryLevelData.setModdedInfo(this.server.getServerModName(), this.server.getModdedStatus().shouldReportAsModified());
@@ -239,9 +239,15 @@ public abstract class CraftRegionAccessor implements RegionAccessor {
case PALE_OAK_CREAKING:
gen = TreeFeatures.PALE_OAK_CREAKING;
break;
case POPLAR:
case RED_POPLAR:
gen = TreeFeatures.RED_POPLAR;
break;
case ORANGE_POPLAR:
gen = TreeFeatures.ORANGE_POPLAR;
break;
case YELLOW_POPLAR:
gen = TreeFeatures.YELLOW_POPLAR;
break;
case TREE:
default:
gen = TreeFeatures.OAK;
@@ -216,6 +216,7 @@ import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.Merchant;
import org.bukkit.inventory.Recipe;
import org.bukkit.loot.LootTable;
import org.bukkit.map.MapCursor;
import org.bukkit.map.MapPalette;
import org.bukkit.map.MapView;
import org.bukkit.packs.ResourcePack;
@@ -233,6 +234,7 @@ import org.bukkit.profile.PlayerProfile;
import org.bukkit.scheduler.BukkitWorker;
import org.bukkit.scoreboard.Criteria;
import org.bukkit.structure.StructureManager;
import org.bukkit.util.StructureSearchResult;
import org.bukkit.util.permissions.DefaultPermissions;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
@@ -1847,7 +1849,6 @@ public final class CraftServer implements Server {
return CraftItemStack.asBukkitCopy(stack);
}
// Paper start - copied from above (uses un-deprecated StructureType type)
@Override
public ItemStack createExplorerMap(World world, Location location, org.bukkit.generator.structure.StructureType structureType, org.bukkit.map.MapCursor.Type mapIcon, int radius, boolean findUnexplored) {
Preconditions.checkArgument(world != null, "World cannot be null");
@@ -1855,23 +1856,43 @@ public final class CraftServer implements Server {
Preconditions.checkArgument(structureType != null, "StructureType cannot be null");
Preconditions.checkArgument(mapIcon != null, "mapIcon cannot be null");
ServerLevel level = ((CraftWorld) world).getHandle();
final org.bukkit.util.StructureSearchResult structureSearchResult = world.locateNearestStructure(location, structureType, radius, findUnexplored);
final ServerLevel level = ((CraftWorld) world).getHandle();
final StructureSearchResult structureSearchResult = world.locateNearestStructure(location, structureType, radius, findUnexplored);
if (structureSearchResult == null) {
return null;
}
Location structureLocation = structureSearchResult.getLocation();
BlockPos structurePos = new BlockPos(structureLocation.getBlockX(), structureLocation.getBlockY(), structureLocation.getBlockZ());
return this.createExplorerMap(level, structureSearchResult, mapIcon);
}
@Override
public ItemStack createExplorerMap(World world, Location location, org.bukkit.generator.structure.Structure structure, org.bukkit.map.MapCursor.Type mapIcon, int radius, boolean findUnexplored) {
Preconditions.checkArgument(world != null, "World cannot be null");
Preconditions.checkArgument(location != null, "Location cannot be null");
Preconditions.checkArgument(structure != null, "Structure cannot be null");
Preconditions.checkArgument(mapIcon != null, "mapIcon cannot be null");
final ServerLevel level = ((CraftWorld) world).getHandle();
final StructureSearchResult structureSearchResult = world.locateNearestStructure(location, structure, radius, findUnexplored);
if (structureSearchResult == null) {
return null;
}
return this.createExplorerMap(level, structureSearchResult, mapIcon);
}
private ItemStack createExplorerMap(final ServerLevel level, final StructureSearchResult structureSearchResult, final MapCursor.Type mapIcon) {
final Location structureLocation = structureSearchResult.getLocation();
final BlockPos structurePos = CraftLocation.toBlockPos(structureLocation);
// Create map with trackingPosition = true, unlimitedTracking = true
net.minecraft.world.item.ItemStack stack = MapItem.create(level, structurePos.getX(), structurePos.getZ(), MapView.Scale.NORMAL.getValue(), true, true);
final net.minecraft.world.item.ItemStack stack = MapItem.create(level, structurePos.getX(), structurePos.getZ(), MapView.Scale.NORMAL.getValue(), true, true);
MapItem.renderBiomePreviewMap(level, stack);
// "+" map ID taken from VillagerTrades$TreasureMapForEmeralds
MapItemSavedData.addTargetDecoration(stack, structurePos, "+", CraftMapCursor.CraftType.bukkitToMinecraftHolder(mapIcon));
return CraftItemStack.asBukkitCopy(stack);
}
// Paper end
@Override
public void shutdown() {
@@ -0,0 +1,21 @@
package org.bukkit.craftbukkit;
import io.papermc.paper.registry.HolderableBase;
import io.papermc.paper.world.Timeline;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
public class CraftTimeline extends HolderableBase<net.minecraft.world.timeline.Timeline> implements Timeline {
public static Timeline minecraftHolderToBukkit(final Holder<net.minecraft.world.timeline.Timeline> minecraft) {
return CraftRegistry.minecraftHolderToBukkit(minecraft, Registries.TIMELINE);
}
public static Holder<net.minecraft.world.timeline.Timeline> bukkitToMinecraftHolder(final Timeline bukkit) {
return CraftRegistry.bukkitToMinecraftHolder(bukkit);
}
public CraftTimeline(final Holder<net.minecraft.world.timeline.Timeline> holder) {
super(holder);
}
}
@@ -143,7 +143,6 @@ import org.bukkit.event.weather.ThunderChangeEvent;
import org.bukkit.event.weather.WeatherChangeEvent;
import org.bukkit.event.world.ClockTimeSkipEvent;
import org.bukkit.event.world.SpawnChangeEvent;
import org.bukkit.event.world.TimeSkipEvent;
import org.bukkit.generator.BiomeProvider;
import org.bukkit.generator.BlockPopulator;
import org.bukkit.generator.ChunkGenerator;
@@ -823,19 +822,63 @@ public class CraftWorld extends CraftRegionAccessor implements World {
@Override
public void setFullTime(long time) {
if (this.world.dimensionType().defaultClock().isEmpty()) {
throw new IllegalArgumentException("Cannot set time in world without world clock");
}
final Holder<net.minecraft.world.clock.WorldClock> clock = this.world.dimensionType().defaultClock()
.orElseThrow(() -> new IllegalArgumentException("Cannot set time in world without world clock"));
this.setFullTime(clock, time);
}
final long currentClockTime = this.world.getDefaultClockTime();
final ClockTimeSkipEvent event = new TimeSkipEvent(this, ClockTimeSkipEvent.SkipReason.CUSTOM, time - currentClockTime);
this.server.getPluginManager().callEvent(event);
if (event.isCancelled()) {
@Override
public io.papermc.paper.world.@Nullable WorldClock getDefaultClock() {
return this.world.dimensionType().defaultClock().map(CraftWorldClock::minecraftHolderToBukkit).orElse(null);
}
@Override
public long getFullTime(final io.papermc.paper.world.WorldClock clock) {
Preconditions.checkArgument(clock != null, "clock cannot be null");
return this.world.clockManager().getInstance(CraftWorldClock.bukkitToMinecraftHolder(clock)).totalTicks();
}
@Override
public void setFullTime(final io.papermc.paper.world.WorldClock clock, final long time) {
Preconditions.checkArgument(clock != null, "clock cannot be null");
this.setFullTime(CraftWorldClock.bukkitToMinecraftHolder(clock), time);
}
private void setFullTime(final Holder<net.minecraft.world.clock.WorldClock> clock, final long time) {
Preconditions.checkArgument(time >= 0, "time cannot be negative, was %s", time);
final long currentClockTime = this.world.clockManager().getInstance(clock).totalTicks();
final ClockTimeSkipEvent event = CraftEventFactory.createTimeSkipEvent(this.world, clock, ClockTimeSkipEvent.SkipReason.CUSTOM, time - currentClockTime);
if (!event.callEvent()) {
return;
}
// Updates the clock for all players
this.world.clockManager().setTotalTicks(this.world.dimensionType().defaultClock().get(), currentClockTime + event.getSkipAmount());
this.world.clockManager().setTotalTicks(clock, Math.max(currentClockTime + event.getSkipAmount(), 0L));
}
@Override
public boolean isClockPaused(final io.papermc.paper.world.WorldClock clock) {
Preconditions.checkArgument(clock != null, "clock cannot be null");
return this.world.clockManager().getInstance(CraftWorldClock.bukkitToMinecraftHolder(clock)).isPaused();
}
@Override
public void setClockPaused(final io.papermc.paper.world.WorldClock clock, final boolean paused) {
Preconditions.checkArgument(clock != null, "clock cannot be null");
this.world.clockManager().setPaused(CraftWorldClock.bukkitToMinecraftHolder(clock), paused);
}
@Override
public float getClockRate(final io.papermc.paper.world.WorldClock clock) {
Preconditions.checkArgument(clock != null, "clock cannot be null");
return this.world.clockManager().getInstance(CraftWorldClock.bukkitToMinecraftHolder(clock)).rate();
}
@Override
public void setClockRate(final io.papermc.paper.world.WorldClock clock, final float rate) {
Preconditions.checkArgument(clock != null, "clock cannot be null");
Preconditions.checkArgument(Float.isFinite(rate) && rate > 0.0F, "rate must be positive and finite, was %s", rate);
this.world.clockManager().setRate(CraftWorldClock.bukkitToMinecraftHolder(clock), rate);
}
// Paper start
@@ -0,0 +1,21 @@
package org.bukkit.craftbukkit;
import io.papermc.paper.registry.HolderableBase;
import io.papermc.paper.world.WorldClock;
import net.minecraft.core.Holder;
import net.minecraft.core.registries.Registries;
public class CraftWorldClock extends HolderableBase<net.minecraft.world.clock.WorldClock> implements WorldClock {
public static WorldClock minecraftHolderToBukkit(final Holder<net.minecraft.world.clock.WorldClock> minecraft) {
return CraftRegistry.minecraftHolderToBukkit(minecraft, Registries.WORLD_CLOCK);
}
public static Holder<net.minecraft.world.clock.WorldClock> bukkitToMinecraftHolder(final WorldClock bukkit) {
return CraftRegistry.bukkitToMinecraftHolder(bukkit);
}
public CraftWorldClock(final Holder<net.minecraft.world.clock.WorldClock> holder) {
super(holder);
}
}
@@ -820,15 +820,26 @@ public class CraftLivingEntity extends CraftEntity implements LivingEntity {
@Override
public void swingMainHand() {
Preconditions.checkState(!this.getHandle().generation, "Cannot swing hand during world generation");
this.getHandle().swing(InteractionHand.MAIN_HAND, SwingAnimation.DEFAULT, true);
this.getHandle().swing(InteractionHand.MAIN_HAND, this.getSwingAnimation(InteractionHand.MAIN_HAND), true);
}
@Override
public void swingOffHand() {
Preconditions.checkState(!this.getHandle().generation, "Cannot swing hand during world generation");
this.getHandle().swing(InteractionHand.OFF_HAND, this.getSwingAnimation(InteractionHand.OFF_HAND), true);
}
this.getHandle().swing(InteractionHand.OFF_HAND, SwingAnimation.DEFAULT, true);
@Override
public void swingHand(org.bukkit.inventory.EquipmentSlot hand, io.papermc.paper.datacomponent.item.SwingAnimation swingAnimation) {
Preconditions.checkState(!this.getHandle().generation, "Cannot swing hand during world generation");
this.getHandle().swing(CraftEquipmentSlot.getHand(hand), ((io.papermc.paper.datacomponent.item.PaperSwingAnimation) swingAnimation).getHandle(), true);
}
private net.minecraft.world.item.component.SwingAnimation getSwingAnimation(InteractionHand hand) {
final net.minecraft.world.item.component.SwingAnimation animation = this.getHandle()
.getItemInHand(hand)
.get(net.minecraft.core.component.DataComponents.ATTACK_ANIMATION);
return animation != null ? animation : SwingAnimation.DEFAULT;
}
@Override
@@ -30,6 +30,7 @@ import net.minecraft.Optionull;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.core.Holder;
import net.minecraft.network.Connection;
import net.minecraft.network.chat.Component;
import net.minecraft.network.protocol.game.ServerboundContainerClosePacket;
@@ -2469,11 +2470,16 @@ public class CraftEventFactory {
return new ItemTransportingEntitySortEvent(mob.getBukkitEntity(), CraftItemStack.asBukkitCopy(mob.getMainHandItem()), new CraftInventory(container));
}
public static ClockTimeSkipEvent createTimeSkipEvent(final CommandSourceStack source, final long skipAmount) {
public static ClockTimeSkipEvent createTimeSkipEvent(final CommandSourceStack source, final Holder<net.minecraft.world.clock.WorldClock> clock, final long skipAmount) {
return createTimeSkipEvent(source.getLevel(), clock, ClockTimeSkipEvent.SkipReason.COMMAND, skipAmount);
}
public static ClockTimeSkipEvent createTimeSkipEvent(final ServerLevel level, final Holder<net.minecraft.world.clock.WorldClock> clock, final ClockTimeSkipEvent.SkipReason reason, final long skipAmount) {
final io.papermc.paper.world.WorldClock bukkitClock = org.bukkit.craftbukkit.CraftWorldClock.minecraftHolderToBukkit(clock);
if (io.papermc.paper.configuration.GlobalConfiguration.get().time.affectsAllWorlds) {
return new ClockTimeSkipEvent(ClockTimeSkipEvent.SkipReason.COMMAND, skipAmount);
return new ClockTimeSkipEvent(bukkitClock, reason, skipAmount);
}
return new TimeSkipEvent(source.getLevel().getWorld(), ClockTimeSkipEvent.SkipReason.COMMAND, skipAmount);
return new TimeSkipEvent(level.getWorld(), bukkitClock, reason, skipAmount);
}
public static int callEntityIgniteEvent(Entity entity, int fuseTime) {
@@ -53,7 +53,7 @@ public class CraftMetaArmorStand extends CraftMetaItem implements com.destroysto
java.io.ByteArrayInputStream buf = new java.io.ByteArrayInputStream(java.util.Base64.getDecoder().decode(entityTag));
try {
this.entityTag = net.minecraft.nbt.NbtIo.readCompressed(buf, net.minecraft.nbt.NbtAccounter.unlimitedHeap());
} catch (java.io.IOException ex) {
} catch (java.io.IOException | net.minecraft.nbt.ReportedNbtException | net.minecraft.nbt.NbtException ex) {
java.util.logging.Logger.getLogger(CraftMetaItem.class.getName()).log(java.util.logging.Level.SEVERE, null, ex);
}
return;
@@ -57,8 +57,10 @@ import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.nbt.ListTag;
import net.minecraft.nbt.NbtAccounter;
import net.minecraft.nbt.NbtException;
import net.minecraft.nbt.NbtIo;
import net.minecraft.nbt.NbtOps;
import net.minecraft.nbt.ReportedNbtException;
import net.minecraft.nbt.SnbtPrinterTagVisitor;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.Identifier;
@@ -754,7 +756,7 @@ public class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDat
try {
CompoundTag internalTag = NbtIo.readCompressed(buf, NbtAccounter.unlimitedHeap());
this.deserializeInternal(internalTag, map);
} catch (IOException ex) {
} catch (IOException | ReportedNbtException | NbtException ex) {
LOGGER.error("Failed to read internal tag from object", ex);
}
}
@@ -783,7 +785,7 @@ public class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDat
this.removedTags.add(key);
}
}
} catch (IOException ex) {
} catch (IOException | ReportedNbtException | NbtException ex) {
LOGGER.error("Failed to read unhandled tag for item", ex);
}
}
@@ -813,7 +815,7 @@ public class CraftMetaItem implements ItemMeta, Damageable, Repairable, BlockDat
ByteArrayInputStream buf = new ByteArrayInputStream(Base64.getDecoder().decode(custom));
try {
this.customTag = NbtIo.readCompressed(buf, NbtAccounter.unlimitedHeap());
} catch (IOException ex) {
} catch (IOException | ReportedNbtException | NbtException ex) {
LOGGER.error("Failed to read custom tag for item", ex);
}
}
@@ -154,6 +154,8 @@ public class CraftPersistentDataContainer extends io.papermc.paper.persistence.P
try (final java.io.DataInputStream dataInput = new java.io.DataInputStream(new java.io.ByteArrayInputStream(bytes))) {
final net.minecraft.nbt.CompoundTag compound = net.minecraft.nbt.NbtIo.read(dataInput);
this.putAll(compound);
} catch (final net.minecraft.nbt.ReportedNbtException | net.minecraft.nbt.NbtException ex) {
throw new java.io.IOException("Invalid NBT data", ex);
}
}
@@ -1,12 +1,14 @@
package org.bukkit.craftbukkit.util;
import ca.spottedleaf.moonrise.common.util.TickThread;
import io.papermc.paper.log.LoggerShutdown;
import net.minecraft.server.MinecraftServer;
public class ServerShutdownThread extends Thread {
public class ServerShutdownThread extends TickThread {
private final MinecraftServer server;
public ServerShutdownThread(MinecraftServer server) {
super("Server Shutdown Thread");
this.server = server;
}
@@ -15,6 +17,14 @@ public class ServerShutdownThread extends Thread {
try {
// Paper start - try to shutdown on main
server.halt(false, false);
// If the server hasn't finished starting up yet, give it as much time as it needs to start up and close on its own. This is
// assuming that as long as the server thread is running, it isn't deadlocked and will eventually either successfully init or fail and stop running.
while (!server.hasStopped() && !server.hasStarted && server.getRunningThread().isAlive()) {
Thread.sleep(100L);
}
// Give the server at least a second to realize that it needs to stop, if it takes longer that means it could be stuck.
for (int i = 1000; i > 0 && !server.hasStopped(); i -= 100) {
Thread.sleep(100);
}
@@ -170,6 +170,8 @@ public class RegistriesArgumentProvider implements ArgumentsProvider {
register(Registries.GAME_RULE, GameRule.class, GameRules.class, CraftGameRule.class, net.minecraft.world.level.gamerules.GameRule.class);
register(Registries.POINT_OF_INTEREST_TYPE, PoiType.class, PoiTypes.class, PaperPoiType.class, net.minecraft.world.entity.ai.village.poi.PoiType.class);
register(Registries.SULFUR_CUBE_ARCHETYPE, SulfurCube.Archetype.class, CraftSulfurCube.CraftArchetype.class, SulfurCubeArchetype.class);
register(Registries.WORLD_CLOCK, io.papermc.paper.world.WorldClock.class, org.bukkit.craftbukkit.CraftWorldClock.class, net.minecraft.world.clock.WorldClock.class);
register(Registries.TIMELINE, io.papermc.paper.world.Timeline.class, org.bukkit.craftbukkit.CraftTimeline.class, net.minecraft.world.timeline.Timeline.class);
}
private static <M, B extends Keyed> void register(ResourceKey<? extends Registry<? extends M>> registryKey, Class<B> api, Class<? extends B> impl, Class<M> internal) {
@@ -1,6 +1,8 @@
package io.papermc.testplugin.brigtests;
import com.mojang.brigadier.Command;
import com.mojang.brigadier.arguments.BoolArgumentType;
import com.mojang.brigadier.arguments.StringArgumentType;
import io.papermc.paper.command.brigadier.BasicCommand;
import io.papermc.paper.command.brigadier.CommandSourceStack;
import io.papermc.paper.command.brigadier.Commands;
@@ -19,6 +21,8 @@ import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import org.bukkit.Material;
import org.bukkit.command.CommandSender;
import org.bukkit.command.defaults.BukkitCommand;
@@ -45,7 +49,7 @@ public final class Registration {
.then(
Commands.argument("name", ArgumentTypes.resource(RegistryKey.ENCHANTMENT))
.executes(ctx -> {
ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("name", Enchantment.class).toString());
ctx.getSource().sendSuccess(Component.translatable(ctx.getArgument("name", Enchantment.class)), false);
return Command.SINGLE_SUCCESS;
})
).build()
@@ -55,7 +59,7 @@ public final class Registration {
Commands.argument("key", ArgumentTypes.resourceKey(RegistryKey.ENCHANTMENT))
.executes(ctx -> {
final TypedKey<Enchantment> key = RegistryArgumentExtractor.getTypedKey(ctx, RegistryKey.ENCHANTMENT, "key");
ctx.getSource().getSender().sendPlainMessage(key.toString());
ctx.getSource().sendSuccess(Component.text(key.toString()), false);
return Command.SINGLE_SUCCESS;
})
).build()
@@ -65,7 +69,7 @@ public final class Registration {
Commands.argument("pos", ArgumentTypes.finePosition(false))
.executes(ctx -> {
final FinePositionResolver position = ctx.getArgument("pos", FinePositionResolver.class);
ctx.getSource().getSender().sendPlainMessage("Position: " + position.resolve(ctx.getSource()));
ctx.getSource().sendRichSuccess("Position: <red>" + position.resolve(ctx.getSource()));
return Command.SINGLE_SUCCESS;
})
).build()
@@ -73,7 +77,7 @@ public final class Registration {
// ensure plugin commands override
commands.register(Commands.literal("tag")
.executes(ctx -> {
ctx.getSource().getSender().sendPlainMessage("overriden command");
ctx.getSource().sendRichSuccess("overriden command");
return Command.SINGLE_SUCCESS;
})
.build(),
@@ -88,7 +92,7 @@ public final class Registration {
.then(Commands.literal("sub_command")
.requires(source -> source.getSender().hasPermission("testplugin.test"))
.executes(ctx -> {
ctx.getSource().getSender().sendPlainMessage("root_command sub_command");
ctx.getSource().sendRichSuccess("<red>root_command <gold>sub_command");
return Command.SINGLE_SUCCESS;
})).build(),
null,
@@ -126,7 +130,7 @@ public final class Registration {
return true;
})
.executes(ctx -> {
ctx.getSource().getExecutor().sendMessage("You are in lava!");
ctx.getSource().sendRichReply("You are in lava!");
return Command.SINGLE_SUCCESS;
})).build(),
null,
@@ -165,14 +169,14 @@ public final class Registration {
.then(Commands.literal("item")
.then(Commands.argument("mat", MaterialArgumentType.item())
.executes(ctx -> {
ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name());
ctx.getSource().sendSuccess(Component.translatable(ctx.getArgument("mat", Material.class)), false);
return Command.SINGLE_SUCCESS;
})
)
).then(Commands.literal("block")
.then(Commands.argument("mat", MaterialArgumentType.block())
.executes(ctx -> {
ctx.getSource().getSender().sendPlainMessage(ctx.getArgument("mat", Material.class).name());
ctx.getSource().sendSuccess(Component.translatable(ctx.getArgument("mat", Material.class)), false);
return Command.SINGLE_SUCCESS;
})
)
@@ -181,6 +185,21 @@ public final class Registration {
null,
Collections.emptyList()
);
commands.register(Commands.literal("send_success")
.then(Commands.argument("allow_inform_admins", BoolArgumentType.bool())
.then(Commands.argument("msg", StringArgumentType.greedyString())
.executes(ctx -> {
ctx.getSource().sendSuccess(
MiniMessage.miniMessage().deserialize(StringArgumentType.getString(ctx, "msg")),
BoolArgumentType.getBool(ctx, "allow_inform_admins")
);
return Command.SINGLE_SUCCESS;
})))
.build(),
null,
Collections.emptyList()
);
});
lifecycleManager.registerEventHandler(LifecycleEvents.COMMANDS.newHandler(event -> {
@@ -188,8 +207,8 @@ public final class Registration {
commands.register(Commands.literal("heya")
.then(Commands.argument("range", ArgumentTypes.doubleRange())
.executes((ct) -> {
ct.getSource().getSender().sendPlainMessage(ct.getArgument("range", DoubleRangeProvider.class).range().toString());
return 1;
ct.getSource().sendSuccess(Component.text(ct.getArgument("range", DoubleRangeProvider.class).range().toString()), false);
return Command.SINGLE_SUCCESS;
})
).build(),
null,