Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4ae40bf
Use modern plain message API
pxlarified Aug 23, 2026
8c402fe
Use component item display names
pxlarified Aug 23, 2026
7ffa0db
Use component item lore
pxlarified Aug 23, 2026
5c8e913
Use component broadcasts for debug logs
pxlarified Aug 23, 2026
4bd847a
Use component player locale
pxlarified Aug 23, 2026
8c3cf56
Use component inventory titles
pxlarified Aug 23, 2026
13286fe
Use damageable armor metadata
pxlarified Aug 23, 2026
b272c74
Use inventory views for cursor updates
pxlarified Aug 23, 2026
a7de1be
Use typed falling block spawning
pxlarified Aug 23, 2026
778dfbf
Use precise target block lookup
pxlarified Aug 23, 2026
00c05e8
Use fireball acceleration API
pxlarified Aug 23, 2026
568f7da
Use recipe choice APIs
pxlarified Aug 23, 2026
7ba53d1
Use event-owned dispatch
pxlarified Aug 23, 2026
e70ff52
Use cross-version custom item names
pxlarified Aug 23, 2026
f9fc117
Use cross-version custom model data
pxlarified Aug 23, 2026
d37a54f
Use damage source API for energy blasts
pxlarified Aug 23, 2026
639cd02
Use Paper item damage APIs
pxlarified Aug 23, 2026
4aaa0ad
Use registry sound lookup
pxlarified Aug 23, 2026
b6f778e
Add variable rule to AGENTS.md
pxlarified Aug 24, 2026
67263c7
Add backpack cosmetic armor stand support
pxlarified Aug 26, 2026
26ccc43
Make armor stand backpack settings configurable
pxlarified Aug 26, 2026
b64bc27
Remove Orbs setup
pxlarified Aug 26, 2026
0d225c4
Bump version to 1.220.0
pxlarified Aug 27, 2026
7c25f24
Handle null recipes in crafting permission checks
pxlarified Aug 27, 2026
050b375
Use Paper events for furniture item picking
pxlarified Aug 29, 2026
a9126f7
Refactor armor stand backpack display handling
pxlarified Aug 29, 2026
31994fe
Add smithing, anvil and grindstone recipe builders
pxlarified Aug 30, 2026
8f4ed6b
Fix workstation recipe builder inventory handling
pxlarified Aug 31, 2026
f4bd76f
Update recipe builder titles and messages
pxlarified Sep 1, 2026
d2e9103
Add support for picking custom blocks
pxlarified Aug 30, 2026
9c29efe
Add support for picking shaped blocks
pxlarified Aug 30, 2026
49619a5
Fix custom item picking behavior
pxlarified Aug 31, 2026
22e1893
Register furniture block location serialization
pxlarified Sep 1, 2026
e3a9223
Merge 1.219.0 into 1.220.0
pxlarified Sep 1, 2026
b256a04
Remove unnecessary NMS abstractions
pxlarified Sep 1, 2026
b497bd1
Add death protection item component support
pxlarified Sep 2, 2026
1a2e9bf
Fix death protection component handling
pxlarified Sep 2, 2026
40e0913
Fix death protection consume effects
pxlarified Sep 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1 +1 @@
pluginVersion=1.219.0
pluginVersion=1.220.0
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
import net.minecraft.resources.ResourceKey;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.PositionMoveRotation;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.sounds.SoundEvent;
Expand All @@ -49,6 +48,7 @@
import net.minecraft.world.item.JukeboxSong;
import net.minecraft.world.item.component.Consumable;
import net.minecraft.world.item.component.CustomData;
import net.minecraft.world.item.component.DeathProtection;
import net.minecraft.world.item.consume_effects.*;
import net.minecraft.world.item.context.BlockPlaceContext;
import net.minecraft.world.item.context.DirectionalPlaceContext;
Expand All @@ -71,10 +71,9 @@
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.craftbukkit.inventory.CraftItemStack;
import org.bukkit.entity.Player;
import org.bukkit.event.Listener;
import org.bukkit.inventory.EquipmentSlot;
import org.bukkit.inventory.ItemStack;
import org.bukkit.inventory.meta.components.FoodComponent;
import org.bukkit.event.Listener;
import org.jetbrains.annotations.NotNull;

import javax.annotation.Nullable;
Expand All @@ -83,7 +82,6 @@
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;

public class NMSHandler implements io.th0rgal.oraxen.nms.NMSHandler {

Expand Down Expand Up @@ -249,11 +247,6 @@ public BlockHitResult getPlayerPOVHitResult(Level world, net.minecraft.world.ent
return world.clip(new ClipContext(vec3, vec32, ClipContext.Block.OUTLINE, fluidHandling, player));
}

@Override
public void customBlockDefaultTools(Player player) {

}

private TagNetworkSerialization.NetworkPayload createPayload() {
Constructor<?> constructor = Arrays
.stream(TagNetworkSerialization.NetworkPayload.class.getDeclaredConstructors()).findFirst()
Expand Down Expand Up @@ -288,11 +281,6 @@ private Map<Object, IntList> createTagRegistryMap() {
}).collect(HashMap::new, Map::putAll, Map::putAll);
}

@Override
public boolean getSupported() {
return true;
}

/**
* Sets a component on an item using the DataComponents registry
*
Expand Down Expand Up @@ -449,24 +437,6 @@ private void handleListValue(net.minecraft.nbt.CompoundTag nbt, String key, List
}
}

@SuppressWarnings("UnstableApiUsage")
@Override
public void foodComponent(ItemBuilder item, ConfigurationSection foodSection) {
FoodComponent foodComponent = new ItemStack(item.getType()).getItemMeta().getFood();

// Ensure nutrition is non-negative
int nutrition = Math.max(foodSection.getInt("nutrition"), 0);
foodComponent.setNutrition(nutrition);

// Ensure saturation is non-negative
float saturation = Math.max((float) foodSection.getDouble("saturation", 0.0), 0f);
foodComponent.setSaturation(saturation);

foodComponent.setCanAlwaysEat(foodSection.getBoolean("can_always_eat", false));

item.setFoodComponent(foodComponent);
}

@SuppressWarnings("UnstableApiUsage")
@Override
public void consumableComponent(ItemBuilder item, ConfigurationSection section) {
Expand Down Expand Up @@ -522,6 +492,105 @@ public void consumableComponent(ItemBuilder item, ConfigurationSection section)
item.setConsumableComponent(consumable.build());
}

@Override
public void deathProtectionComponent(ItemBuilder item, ConfigurationSection section) {
List<ConsumeEffect> effects = parseDeathProtectionEffects(section.getMapList("death_effects"));
item.setDeathProtectionComponent(new DeathProtection(effects));
}

private List<ConsumeEffect> parseDeathProtectionEffects(List<Map<?, ?>> effectSections) {
List<ConsumeEffect> effects = new ArrayList<>();

for (Map<?, ?> effectSection : effectSections) {
String type = Optional.ofNullable(effectSection.get("type"))
.map(Object::toString)
.orElse("");

switch (type.toLowerCase(Locale.ROOT)) {
case "apply_effects" -> addDeathProtectionStatusEffects(effects, effectSection);
case "remove_effects" -> addDeathProtectionRemoveEffects(effects, effectSection);
case "clear_all_effects" -> effects.add(new ClearAllStatusEffectsConsumeEffect());
case "teleport_randomly" -> {
float diameter = parseFloatValue(effectSection.get("diameter"), 16f,
"death_protection.teleport_randomly.diameter");
effects.add(new TeleportRandomlyConsumeEffect(diameter));
}
case "play_sound" -> {
String soundId = Optional.ofNullable(effectSection.get("sound"))
.map(Object::toString)
.orElse(null);
if (soundId != null) {
SoundEvent soundEvent = getSoundEventFromId(soundId);
if (soundEvent != null)
effects.add(new PlaySoundConsumeEffect(Holder.direct(soundEvent)));
}
}
default -> Logs.logWarning("Invalid death_protection ConsumeEffect-Type " + type);
}
}

return effects;
}

private void addDeathProtectionStatusEffects(List<ConsumeEffect> effects, Map<?, ?> effectSection) {
if (!(effectSection.get("effects") instanceof Map<?, ?> configuredEffects))
return;

float probability = Math.max(0f, Math.min(1f,
parseFloatValue(effectSection.get("probability"), 1f, "death_protection.probability")));
List<MobEffectInstance> statusEffects = new ArrayList<>();

for (Map.Entry<?, ?> entry : configuredEffects.entrySet()) {
String effectId = entry.getKey().toString();
if (!(entry.getValue() instanceof Map<?, ?> rawMap)) {
Logs.logWarning("Invalid death_protection effect data for " + effectId + ": expected map");
continue;
}

Map<String, Object> effectData = new HashMap<>();
for (Map.Entry<?, ?> effectEntry : rawMap.entrySet())
effectData.put(String.valueOf(effectEntry.getKey()), effectEntry.getValue());

getMobEffectOptional(effectId)
.map(BuiltInRegistries.MOB_EFFECT::wrapAsHolder)
.ifPresentOrElse(effect -> {
int duration = Math.max(parseIntegerValue(effectData.get("duration"), 1, "duration", effectId), 0) * 20;
int amplifier = Math.max(parseIntegerValue(effectData.get("amplifier"), 0, "amplifier", effectId), 0);
boolean ambient = Optional.ofNullable(effectData.get("ambient"))
.map(value -> Boolean.parseBoolean(value.toString()))
.orElse(false);
boolean particles = Optional.ofNullable(effectData.get("show_particles"))
.map(value -> Boolean.parseBoolean(value.toString()))
.orElse(true);
boolean icon = Optional.ofNullable(effectData.get("show_icon"))
.map(value -> Boolean.parseBoolean(value.toString()))
.orElse(true);

statusEffects.add(new MobEffectInstance(
effect, duration, amplifier, ambient, particles, icon));
}, () -> Logs.logWarning("Invalid potion effect in death_protection: " + effectId));
}

if (!statusEffects.isEmpty())
effects.add(new ApplyStatusEffectsConsumeEffect(statusEffects, probability));
}

private void addDeathProtectionRemoveEffects(List<ConsumeEffect> effects, Map<?, ?> effectSection) {
if (!(effectSection.get("effects") instanceof List<?> effectIds))
return;

List<Holder<MobEffect>> mobEffects = effectIds.stream()
.map(Object::toString)
.map(this::getMobEffectOptional)
.filter(Optional::isPresent)
.map(Optional::get)
.map(BuiltInRegistries.MOB_EFFECT::wrapAsHolder)
.toList();

if (!mobEffects.isEmpty())
effects.add(new RemoveStatusEffectsConsumeEffect(HolderSet.direct(mobEffects)));
}

private void handleApplyEffects(Consumable.Builder consumable, Map<?, ?> effectSection) {
if (!(effectSection.get("effects") instanceof Map<?, ?> effects))
return;
Expand Down Expand Up @@ -711,6 +780,20 @@ public Object consumableComponent(final ItemStack itemStack) {
return null;
}

@Override
@Nullable
public Object deathProtectionComponent(final ItemStack itemStack) {
if (itemStack == null)
return null;
try {
net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(itemStack);
return nmsItem.get(DataComponents.DEATH_PROTECTION);
} catch (Exception e) {
Logs.debug(e);
}
return null;
}

@Override
public ItemStack consumableComponent(final ItemStack itemStack, @Nullable Object consumable) {
if (consumable == null)
Expand All @@ -725,6 +808,20 @@ public ItemStack consumableComponent(final ItemStack itemStack, @Nullable Object
return itemStack;
}

@Override
public ItemStack deathProtectionComponent(final ItemStack itemStack, @Nullable Object deathProtection) {
if (!(deathProtection instanceof DeathProtection component))
return itemStack;
try {
net.minecraft.world.item.ItemStack nmsItem = CraftItemStack.asNMSCopy(itemStack);
nmsItem.set(DataComponents.DEATH_PROTECTION, component);
return asBukkitCopy(nmsItem);
} catch (Exception e) {
Logs.debug(e);
}
return itemStack;
}

@Override
public boolean supportsJukeboxPlaying() {
return true;
Expand All @@ -743,23 +840,8 @@ public void playJukeBoxSong(Location location, ItemStack itemStack) {
new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), id);
}

@Override
public void stopJukeBox(Location location) {
if (location == null || location.getWorld() == null) return;
ServerLevel level = ((CraftWorld) location.getWorld()).getHandle().getLevel();
level.levelEvent(null, LevelEvent.SOUND_STOP_JUKEBOX_SONG,
new BlockPos(location.getBlockX(), location.getBlockY(), location.getBlockZ()), 0);
}

// ============ Backpack Cosmetic Packet Methods ============

private static final AtomicInteger ENTITY_ID_COUNTER = new AtomicInteger(Integer.MAX_VALUE / 2);

@Override
public int getNextEntityId() {
return ENTITY_ID_COUNTER.decrementAndGet();
}

private static EntityType<?> getEntityType(String entityId) {
try {
Object location = ResourceLocationHelper.parse(entityId);
Expand Down Expand Up @@ -837,27 +919,6 @@ public void spawnBackpackArmorStand(Player viewer, int entityId, Location locati
}
}

@Override
public void sendEntityTeleport(Player viewer, int entityId, Location location) {
ServerPlayer serverPlayer = ((CraftPlayer) viewer).getHandle();
Connection connection = serverPlayer.connection.connection;

// Create position/rotation data
PositionMoveRotation positionData = new PositionMoveRotation(
new Vec3(location.getX(), location.getY(), location.getZ()),
Vec3.ZERO, // delta movement
location.getYaw(),
location.getPitch()
);

ClientboundEntityPositionSyncPacket teleportPacket = new ClientboundEntityPositionSyncPacket(
entityId,
positionData,
false // on ground
);
connection.send(teleportPacket);
}

@Override
public void sendEntityHeadRotation(Player viewer, int entityId, float yaw) {
ServerPlayer serverPlayer = ((CraftPlayer) viewer).getHandle();
Expand Down
Loading