mirror of
https://github.com/NoCheatPlus/NoCheatPlus.git
synced 2024-09-27 14:13:11 +02:00
[BLEEDING] Elytra boost: track and allow ascend.
Lots of issues remain with elytra, with and without boost. Selection: * maxheight will trigger with the rocket feature, naturally. Mendable by increasing it via configuration (checks.moving.creativelfly.model.elytra.vertical.maxheight). Not sure we'll just increase the limit or alter how it's dealt with (e.g. also for sf: lock to a max / high slope value, independently of the set-back and world height, alter as necessary). * All sorts of transitions, e.g. onto ground, into water. * Loss of boost right after adding (not sure if already fixed). * What with hover, actually? * Is the flight duration infinite with power 127? * Issues with ascending after descending, even without boost?
This commit is contained in:
parent
8237fbca57
commit
c2449ac08a
@ -30,10 +30,14 @@ import fr.neatmonster.nocheatplus.NCPAPIProvider;
|
||||
import fr.neatmonster.nocheatplus.checks.CheckListener;
|
||||
import fr.neatmonster.nocheatplus.checks.CheckType;
|
||||
import fr.neatmonster.nocheatplus.checks.combined.CombinedConfig;
|
||||
import fr.neatmonster.nocheatplus.checks.moving.MovingData;
|
||||
import fr.neatmonster.nocheatplus.checks.moving.magic.Magic;
|
||||
import fr.neatmonster.nocheatplus.compat.Bridge1_9;
|
||||
import fr.neatmonster.nocheatplus.compat.BridgeHealth;
|
||||
import fr.neatmonster.nocheatplus.compat.BridgeMisc;
|
||||
import fr.neatmonster.nocheatplus.stats.Counters;
|
||||
import fr.neatmonster.nocheatplus.utilities.InventoryUtil;
|
||||
import fr.neatmonster.nocheatplus.utilities.TickTask;
|
||||
import fr.neatmonster.nocheatplus.utilities.location.LocUtil;
|
||||
import fr.neatmonster.nocheatplus.utilities.map.BlockProperties;
|
||||
|
||||
@ -72,8 +76,7 @@ public class BlockInteractListener extends CheckListener {
|
||||
* @param event
|
||||
* the event
|
||||
*/
|
||||
@EventHandler(
|
||||
ignoreCancelled = false, priority = EventPriority.LOWEST)
|
||||
@EventHandler(ignoreCancelled = false, priority = EventPriority.LOWEST)
|
||||
protected void onPlayerInteract(final PlayerInteractEvent event) {
|
||||
final Player player = event.getPlayer();
|
||||
// Cancel interact events for dead players.
|
||||
@ -86,51 +89,51 @@ public class BlockInteractListener extends CheckListener {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: Re-arrange for interact spam, possibly move ender pearl stuff to a method.
|
||||
// TODO: Re-arrange for interact spamming.
|
||||
final Action action = event.getAction();
|
||||
final Block block = event.getClickedBlock();
|
||||
|
||||
if (block == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final BlockInteractData data = BlockInteractData.getData(player);
|
||||
final int previousLastTick = data.lastTick;
|
||||
data.setLastBlock(block, action);
|
||||
// TODO: Last block setting: better on monitor !?.
|
||||
if (block == null) {
|
||||
data.resetLastBlock();
|
||||
}
|
||||
else {
|
||||
data.setLastBlock(block, action);
|
||||
}
|
||||
final BlockFace face = event.getBlockFace();
|
||||
final ItemStack stack;
|
||||
switch(action) {
|
||||
case RIGHT_CLICK_AIR:
|
||||
// TODO: What else to adapt?
|
||||
case LEFT_CLICK_AIR:
|
||||
// TODO: What else to adapt?
|
||||
case LEFT_CLICK_BLOCK:
|
||||
stack = null;
|
||||
break;
|
||||
case RIGHT_CLICK_BLOCK:
|
||||
final ItemStack stack = Bridge1_9.getUsedItem(player, event);
|
||||
stack = Bridge1_9.getUsedItem(player, event);
|
||||
if (stack != null && stack.getType() == Material.ENDER_PEARL) {
|
||||
if (!BlockProperties.isPassable(block.getType())) {
|
||||
final CombinedConfig ccc = CombinedConfig.getConfig(player);
|
||||
if (ccc.enderPearlCheck && ccc.enderPearlPreventClickBlock) {
|
||||
event.setUseItemInHand(Result.DENY);
|
||||
if (data.debug) {
|
||||
final BlockInteractConfig cc = BlockInteractConfig.getConfig(player);
|
||||
genericDebug(player, block, face, event, "click block: deny use ender pearl", previousLastTick, data, cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
checkEnderPearlRightClickBlock(player, block, face, event, previousLastTick, data);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
|
||||
boolean cancelled = false;
|
||||
if (event.isCancelled() && event.useInteractedBlock() != Result.ALLOW) {
|
||||
data.subsequentCancel ++;
|
||||
return;
|
||||
}
|
||||
|
||||
final BlockInteractConfig cc = BlockInteractConfig.getConfig(player);
|
||||
boolean cancelled = false;
|
||||
boolean preventUseItem = false;
|
||||
|
||||
final Location loc = player.getLocation(useLoc);
|
||||
|
||||
// TODO: Always run all checks, also for !isBlock ?
|
||||
|
||||
// Interaction speed.
|
||||
if (!cancelled && speed.isEnabled(player) && speed.check(player, data, cc)) {
|
||||
cancelled = true;
|
||||
@ -154,36 +157,7 @@ public class BlockInteractListener extends CheckListener {
|
||||
|
||||
// If one of the checks requested to cancel the event, do so.
|
||||
if (cancelled) {
|
||||
if (event.isCancelled()) {
|
||||
// Just prevent using the block.
|
||||
event.setUseInteractedBlock(Result.DENY);
|
||||
if (data.debug) {
|
||||
genericDebug(player, block, face, event, "already cancelled: deny use block", previousLastTick, data, cc);
|
||||
}
|
||||
} else {
|
||||
final Result previousUseItem = event.useItemInHand();
|
||||
event.setCancelled(true);
|
||||
event.setUseInteractedBlock(Result.DENY);
|
||||
if (
|
||||
previousUseItem == Result.DENY || preventUseItem
|
||||
// Allow consumption still.
|
||||
|| !InventoryUtil.isConsumable(Bridge1_9.getUsedItem(player, event))
|
||||
) {
|
||||
event.setUseItemInHand(Result.DENY);
|
||||
if (data.debug) {
|
||||
genericDebug(player, block, face, event, "deny item use", previousLastTick, data, cc);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Consumable and not prevented otherwise.
|
||||
// TODO: Ender pearl?
|
||||
event.setUseItemInHand(Result.ALLOW);
|
||||
if (data.debug) {
|
||||
genericDebug(player, block, face, event, "allow edible item use", previousLastTick, data, cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
data.subsequentCancel ++;
|
||||
onCancelInteract(player, block, face, event, previousLastTick, preventUseItem, data, cc);
|
||||
}
|
||||
else {
|
||||
data.subsequentCancel = 0;
|
||||
@ -191,6 +165,87 @@ public class BlockInteractListener extends CheckListener {
|
||||
useLoc.setWorld(null);
|
||||
}
|
||||
|
||||
private void onCancelInteract(final Player player, final Block block, final BlockFace face,
|
||||
final PlayerInteractEvent event, final int previousLastTick, final boolean preventUseItem,
|
||||
final BlockInteractData data, final BlockInteractConfig cc) {
|
||||
if (event.isCancelled()) {
|
||||
// Just prevent using the block.
|
||||
event.setUseInteractedBlock(Result.DENY);
|
||||
if (data.debug) {
|
||||
genericDebug(player, block, face, event, "already cancelled: deny use block", previousLastTick, data, cc);
|
||||
}
|
||||
} else {
|
||||
final Result previousUseItem = event.useItemInHand();
|
||||
event.setCancelled(true);
|
||||
event.setUseInteractedBlock(Result.DENY);
|
||||
if (
|
||||
previousUseItem == Result.DENY || preventUseItem
|
||||
// Allow consumption still.
|
||||
|| !InventoryUtil.isConsumable(Bridge1_9.getUsedItem(player, event))
|
||||
) {
|
||||
event.setUseItemInHand(Result.DENY);
|
||||
if (data.debug) {
|
||||
genericDebug(player, block, face, event, "deny item use", previousLastTick, data, cc);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// Consumable and not prevented otherwise.
|
||||
// TODO: Ender pearl?
|
||||
event.setUseItemInHand(Result.ALLOW);
|
||||
if (data.debug) {
|
||||
genericDebug(player, block, face, event, "allow edible item use", previousLastTick, data, cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
data.subsequentCancel ++;
|
||||
}
|
||||
|
||||
@EventHandler(ignoreCancelled = false, priority = EventPriority.MONITOR)
|
||||
public void onPlayerInteractMonitor(final PlayerInteractEvent event) {
|
||||
// Elytra boost.
|
||||
if (event.getAction() == Action.RIGHT_CLICK_AIR
|
||||
&& event.isCancelled() && event.useItemInHand() != Result.DENY) {
|
||||
final Player player = event.getPlayer();
|
||||
final ItemStack stack = Bridge1_9.getUsedItem(player, event);
|
||||
if (stack != null && BridgeMisc.maybeElytraBoost(player, stack.getType())) {
|
||||
final BlockInteractData data = BlockInteractData.getData(player);
|
||||
final int power = BridgeMisc.getFireworksPower(stack);
|
||||
/*
|
||||
* The default Material.FIREWORK has power 0 and roughly does 10
|
||||
* blocks in one seconds. With power 127, the flight duration
|
||||
* roughly is 7 seconds, so we assume the effect duration to be
|
||||
* something like 20 + power. What does the wiki tell? However
|
||||
* the actual flight duration with elytra + fireworks seems to
|
||||
* be higher, or the friction behavior has changed.
|
||||
*/
|
||||
final MovingData mData = MovingData.getData(player);
|
||||
mData.fireworksBoostDuration = power + Magic.FIREWORKS_BOOST_EXTRA_TICKS;
|
||||
// Expiration tick: not general latency, rather a minimum margin for sudden congestion.
|
||||
mData.fireworksBoostTickExpire = TickTask.getTick() + power + Magic.FIREWORKS_BOOST_EXTRA_TICKS;
|
||||
// TODO: Invalidation mechanics: by tick/time well ?
|
||||
// TODO: Implement using it in CreativeFly.
|
||||
if (data.debug) {
|
||||
debug(player, "Elytra boost (power " + power + "): " + stack);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void checkEnderPearlRightClickBlock(final Player player, final Block block,
|
||||
final BlockFace face, final PlayerInteractEvent event,
|
||||
final int previousLastTick, final BlockInteractData data) {
|
||||
if (!BlockProperties.isPassable(block.getType())) {
|
||||
final CombinedConfig ccc = CombinedConfig.getConfig(player);
|
||||
if (ccc.enderPearlCheck && ccc.enderPearlPreventClickBlock) {
|
||||
event.setUseItemInHand(Result.DENY);
|
||||
if (data.debug) {
|
||||
final BlockInteractConfig cc = BlockInteractConfig.getConfig(player);
|
||||
genericDebug(player, block, face, event, "click block: deny use ender pearl", previousLastTick, data, cc);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void genericDebug(final Player player, final Block block, final BlockFace face,
|
||||
final PlayerInteractEvent event, final String tag, final int previousLastTick,
|
||||
final BlockInteractData data, final BlockInteractConfig cc) {
|
||||
|
@ -220,6 +220,13 @@ public class MovingData extends ACheckData {
|
||||
/** Horizontal velocity modeled as an axis (always positive) */
|
||||
private final FrictionAxisVelocity horVel = new FrictionAxisVelocity();
|
||||
|
||||
/** Duration of the boost effect in ticks. */
|
||||
public int fireworksBoostDuration = 0;
|
||||
/**
|
||||
* Expire at this tick.
|
||||
*/
|
||||
public int fireworksBoostTickExpire = 0;
|
||||
|
||||
// Coordinates.
|
||||
/** Moving trace (to-positions, use tick as time). This is initialized on "playerJoins, i.e. MONITOR, and set to null on playerLeaves." */
|
||||
private final LocationTrace trace;
|
||||
@ -291,6 +298,7 @@ public class MovingData extends ACheckData {
|
||||
public double sfCobwebVL = 0;
|
||||
public long sfVLTime = 0;
|
||||
|
||||
|
||||
// Accounting info.
|
||||
public final ActionAccumulator vDistAcc = new ActionAccumulator(3, 3);
|
||||
/**
|
||||
@ -389,8 +397,7 @@ public class MovingData extends ACheckData {
|
||||
sfZeroVdistRepeat = 0;
|
||||
clearAccounting();
|
||||
clearNoFallData();
|
||||
removeAllVelocity();
|
||||
sfHorizontalBuffer = 0.0;
|
||||
removeAllPlayerSpeedModifiers();
|
||||
lostSprintCount = 0;
|
||||
sfHoverTicks = sfHoverLoginTicks = -1;
|
||||
sfDirty = false;
|
||||
@ -421,14 +428,13 @@ public class MovingData extends ACheckData {
|
||||
// Keep jump amplifier
|
||||
// Keep bunny-hop delay (?)
|
||||
// keep jump phase.
|
||||
sfHorizontalBuffer = 0.0;
|
||||
lostSprintCount = 0;
|
||||
sfHoverTicks = -1; // 0 ?
|
||||
sfDirty = false;
|
||||
sfLowJump = false;
|
||||
liftOffEnvelope = defaultLiftOffEnvelope;
|
||||
insideMediumCount = 0;
|
||||
removeAllVelocity();
|
||||
removeAllPlayerSpeedModifiers();
|
||||
vehicleConsistency = MoveConsistency.INCONSISTENT; // Not entirely sure here.
|
||||
lastFrictionHorizontal = lastFrictionVertical = 0.0;
|
||||
verticalBounce = null;
|
||||
@ -496,7 +502,7 @@ public class MovingData extends ACheckData {
|
||||
* Called when a player leaves the server.
|
||||
*/
|
||||
public void onPlayerLeave() {
|
||||
removeAllVelocity();
|
||||
removeAllPlayerSpeedModifiers();
|
||||
trace.reset();
|
||||
playerMoves.invalidate();
|
||||
vehicleMoves.invalidate();
|
||||
@ -864,7 +870,21 @@ public class MovingData extends ACheckData {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all vertical and horizontal velocity.
|
||||
* Remove/reset all speed modifier tracking, like vertical and horizontal
|
||||
* velocity, elytra boost, buffer.
|
||||
*/
|
||||
private void removeAllPlayerSpeedModifiers() {
|
||||
// Velocity
|
||||
removeAllVelocity();
|
||||
// Elytra boost best fits velocity / effects.
|
||||
fireworksBoostDuration = 0;
|
||||
fireworksBoostTickExpire = 0;
|
||||
// Horizontal buffer.
|
||||
sfHorizontalBuffer = 0.0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset velocity tracking (h+v).
|
||||
*/
|
||||
public void removeAllVelocity() {
|
||||
horVel.clear();
|
||||
@ -1210,7 +1230,7 @@ public class MovingData extends ACheckData {
|
||||
sfCobwebTime = Math.min(sfCobwebTime, time);
|
||||
sfVLTime = Math.min(sfVLTime, time);
|
||||
clearAccounting(); // Not sure: adding up might not be nice.
|
||||
removeAllVelocity(); // TODO: This likely leads to problems.
|
||||
removeAllPlayerSpeedModifiers(); // TODO: This likely leads to problems.
|
||||
// (ActionFrequency can handle this.)
|
||||
}
|
||||
|
||||
|
@ -822,7 +822,7 @@ public class MovingListener extends CheckListener implements TickListener, IRemo
|
||||
else if (checkCf) {
|
||||
// CreativeFly
|
||||
if (newTo == null) {
|
||||
newTo = creativeFly.check(player, pFrom, pTo, data, cc, time, useBlockChangeTracker);
|
||||
newTo = creativeFly.check(player, pFrom, pTo, data, cc, time, tick, useBlockChangeTracker);
|
||||
}
|
||||
data.sfHoverTicks = -1;
|
||||
data.sfLowJump = false;
|
||||
|
@ -82,6 +82,7 @@ public class Magic {
|
||||
* moving direction).
|
||||
*/
|
||||
public static final double GLIDE_DESCEND_GAIN_MAX_POS = GRAVITY_ODD / 1.95;
|
||||
public static final int FIREWORKS_BOOST_EXTRA_TICKS = 50;
|
||||
|
||||
// On-ground.
|
||||
public static final double Y_ON_GROUND_MIN = 0.00001;
|
||||
|
@ -71,7 +71,8 @@ public class CreativeFly extends Check {
|
||||
* @return
|
||||
*/
|
||||
public Location check(final Player player, final PlayerLocation from, final PlayerLocation to,
|
||||
final MovingData data, final MovingConfig cc, final long time, final boolean useBlockChangeTracker) {
|
||||
final MovingData data, final MovingConfig cc, final long time, final int tick,
|
||||
final boolean useBlockChangeTracker) {
|
||||
|
||||
// Reset tags, just in case.
|
||||
tags.clear();
|
||||
@ -87,6 +88,18 @@ public class CreativeFly extends Check {
|
||||
thisMove.modelFlying = model;
|
||||
final PlayerMoveData lastMove = data.playerMoves.getFirstPastMove();
|
||||
|
||||
// Proactive reset of elytraBoost (MC 1.11.2).
|
||||
if (data.fireworksBoostDuration > 0) {
|
||||
if (!lastMove.valid || lastMove.flyCheck != CheckType.MOVING_CREATIVEFLY
|
||||
|| lastMove.modelFlying != model
|
||||
|| data.fireworksBoostTickExpire < tick ) {
|
||||
data.fireworksBoostDuration = 0;
|
||||
}
|
||||
else {
|
||||
data.fireworksBoostDuration --;
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate some distances.
|
||||
final double yDistance = thisMove.yDistance;
|
||||
final double hDistance = thisMove.hDistance;
|
||||
@ -361,9 +374,9 @@ public class CreativeFly extends Check {
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Hack, move / config / something.
|
||||
// TODO: Confine more. hdist change relates to ydist change
|
||||
// Related to elytra.
|
||||
if (limitV == 0.0 && Bridge1_9.isGlidingWithElytra(from.getPlayer())) {
|
||||
// TODO: Better detection of an elytra model (extra flags?).
|
||||
limitV = hackLytra(yDistance, limitV, thisMove, lastMove, data);
|
||||
}
|
||||
|
||||
@ -423,6 +436,8 @@ public class CreativeFly extends Check {
|
||||
}
|
||||
|
||||
private double hackLytra(final double yDistance, final double limitV, final PlayerMoveData thisMove, final PlayerMoveData lastMove, final MovingData data) {
|
||||
// TODO: Hack, move / config / something.
|
||||
// TODO: Confine more. hdist change relates to ydist change
|
||||
// TODO: Further: jumpphase vs. y-distance to set-back. Problem: velocity
|
||||
// TODO: Further: record max h and descend speeds and relate to those.
|
||||
// TODO: Demand total speed to decrease.
|
||||
@ -461,8 +476,33 @@ public class CreativeFly extends Check {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Elytra boost with fireworks rockets.
|
||||
if (yDistance > limitV && data.fireworksBoostDuration > 0 && lastMove.toIsValid
|
||||
&& (
|
||||
yDistance >= lastMove.yDistance
|
||||
|| yDistance - lastMove.yDistance < Magic.GRAVITY_MAX
|
||||
// TODO: Head blocked -> friction does it?
|
||||
)
|
||||
&& (
|
||||
yDistance - lastMove.yDistance < 0.77 // TODO
|
||||
|| lastMove.yDistance < 0.0 && yDistance < 1.54
|
||||
)
|
||||
&& yDistance < 1.67 // Last resort, check / TODO
|
||||
) {
|
||||
/*
|
||||
* TODO: Do cross check item consumption (do other events fire?).
|
||||
* [?on tick: expectations framework, check before tick and before
|
||||
* other inventory events, once set]
|
||||
*/
|
||||
// TODO: Remove fumbling with magic constants.
|
||||
// TODO: Relate horizontal to vertical + relate to looking direction.
|
||||
// TODO: More invalidation conditions, like total age (checked elsewhere?).
|
||||
tags.add("fw_boost_asc");
|
||||
return yDistance;
|
||||
}
|
||||
|
||||
return limitV;
|
||||
}
|
||||
|
||||
@ -527,6 +567,9 @@ public class CreativeFly extends Check {
|
||||
if (thisMove.verVelUsed != null) {
|
||||
builder.append(" , vVelUsed: " + thisMove.verVelUsed);
|
||||
}
|
||||
if (data.fireworksBoostDuration > 0 && model.id.equals("jetpack.elytra")) {
|
||||
builder.append(" , boost: " + data.fireworksBoostDuration);
|
||||
}
|
||||
builder.append(" , model: " + model.id);
|
||||
if (!tags.isEmpty()) {
|
||||
builder.append(" , tags: ");
|
||||
|
@ -20,9 +20,13 @@ import java.util.Collection;
|
||||
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.GameMode;
|
||||
import org.bukkit.Material;
|
||||
import org.bukkit.entity.Entity;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.bukkit.entity.Projectile;
|
||||
import org.bukkit.inventory.ItemStack;
|
||||
import org.bukkit.inventory.meta.FireworkMeta;
|
||||
import org.bukkit.inventory.meta.ItemMeta;
|
||||
|
||||
import fr.neatmonster.nocheatplus.utilities.ReflectionUtil;
|
||||
|
||||
@ -42,9 +46,11 @@ public class BridgeMisc {
|
||||
}
|
||||
|
||||
public static final GameMode GAME_MODE_SPECTATOR = getSpectatorGameMode();
|
||||
|
||||
|
||||
private static final Method Bukkit_getOnlinePlayers = ReflectionUtil.getMethodNoArgs(Bukkit.class, "getOnlinePlayers");
|
||||
|
||||
public static final Material FIREWORK = Material.getMaterial("FIREWORK");
|
||||
|
||||
/**
|
||||
* Return a shooter of a projectile if we get an entity, null otherwise.
|
||||
*/
|
||||
@ -104,4 +110,39 @@ public class BridgeMisc {
|
||||
return new Player[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Test side conditions for fireworks boost with elytra on, for interaction
|
||||
* with the item in hand. Added with Minecraft 1.11.2.
|
||||
*
|
||||
* @param player
|
||||
* @param materialInHand
|
||||
* The type of the item used with interaction.
|
||||
* @return
|
||||
*/
|
||||
public static boolean maybeElytraBoost(final Player player, final Material materialInHand) {
|
||||
// TODO: Account for MC version (needs configuration override or auto adapt to protocol support).
|
||||
// TODO: Non-static due to version checks (...).
|
||||
return FIREWORK != null && materialInHand == FIREWORK && Bridge1_9.isGlidingWithElytra(player);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the power for a firework(s) item (stack).
|
||||
*
|
||||
* @param item
|
||||
* @return The power. Should be between 0 and 127 including edges, in case
|
||||
* of valid items, according to javadocs (Spigot MC 1.11.2). In case
|
||||
* the item is null or can't be judged, -1 is returned.
|
||||
*/
|
||||
public static int getFireworksPower(final ItemStack item) {
|
||||
if (item == null || item.getType() != FIREWORK) {
|
||||
return 0;
|
||||
}
|
||||
final ItemMeta meta = item.getItemMeta();
|
||||
if (!(meta instanceof FireworkMeta)) { // INDIRECT: With elytra, this already exists.
|
||||
return 0;
|
||||
}
|
||||
final FireworkMeta fwMeta = (FireworkMeta) meta;
|
||||
return fwMeta.getPower();
|
||||
}
|
||||
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user