Add AllowElytraFlight event ()

Use explicit casts instead of .class.cast in mixins

Reorganize API class, and make it work for any living entity

add LivingEntityFeatureRenderEvents to disable cape rendering

Reorganize/rename hook, and add ALLOW event

Fix missing mixin return & cosmetic adjustements
This commit is contained in:
Technici4n 2021-11-23 18:04:34 +01:00 committed by modmuss50
parent f7c1d59979
commit 6b21378a26
14 changed files with 527 additions and 0 deletions
fabric-entity-events-v1
fabric-rendering-v1/src/main
java/net/fabricmc/fabric
api/client/rendering/v1
mixin/client/rendering
resources

View file

@ -9,4 +9,5 @@ dependencies {
testmodImplementation project(path: ':fabric-command-api-v1', configuration: 'namedElements')
testmodImplementation project(path: ':fabric-networking-api-v1', configuration: 'namedElements')
testmodImplementation project(path: ':fabric-registry-sync-v0', configuration: 'namedElements')
testmodImplementation project(path: ':fabric-rendering-v1', configuration: 'namedElements')
}

View file

@ -0,0 +1,110 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.entity.event.v1;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.ItemStack;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
/**
* Events related to elytra flight for living entities. Elytra flight is also known as "fall flying".
*/
public final class EntityElytraEvents {
/**
* An event to check if elytra flight (both through normal and custom elytras) is allowed.
* All listeners need to return true to allow the entity to fly, otherwise elytra flight will be blocked/stopped.
*/
public static final Event<Allow> ALLOW = EventFactory.createArrayBacked(Allow.class, listeners -> entity -> {
for (Allow listener : listeners) {
if (!listener.allowElytraFlight(entity)) {
return false;
}
}
return true;
});
/**
* An event to grant elytra flight to living entities when some condition is met.
* Will be called when players try to start elytra flight by pressing space in mid-air, and every tick for all flying living entities to check if elytra flight is still allowed.
*
* <p>Items that wish to enable custom elytra flight when worn in the chest equipment slot can simply implement {@link FabricElytraItem} instead of registering a listener.
*/
public static final Event<Custom> CUSTOM = EventFactory.createArrayBacked(Custom.class, listeners -> (entity, tickElytra) -> {
for (Custom listener : listeners) {
if (listener.useCustomElytra(entity, tickElytra)) {
return true;
}
}
return false;
});
static {
CUSTOM.register((entity, tickElytra) -> {
ItemStack chestStack = entity.getEquippedStack(EquipmentSlot.CHEST);
if (chestStack.getItem() instanceof FabricElytraItem fabricElytraItem) {
return fabricElytraItem.useCustomElytra(entity, chestStack, tickElytra);
}
return false;
});
}
@FunctionalInterface
public interface Allow {
/**
* @return false to block elytra flight, true to allow it (unless another listener returns false)
*/
boolean allowElytraFlight(LivingEntity entity);
}
@FunctionalInterface
public interface Custom {
/**
* Try to use a custom elytra for an entity.
* A custom elytra is anything that allows an entity to enter and continue elytra flight when some condition is met.
* Listeners should follow the following pattern:
* <pre>{@code
* EntityElytraEvents.CUSTOM.register((entity, tickElytra) -> {
* if (check if condition for custom elytra is met) {
* if (tickElytra) {
* // Optionally consume some resources that are being used up in order to fly, for example damaging an item.
* // Optionally perform other side effects of elytra flight, for example playing a sound.
* }
* // Allow entering/continuing elytra flight with this custom elytra
* return true;
* }
* // Condition for the custom elytra is not met: don't let players enter or continue elytra flight (unless another elytra is available).
* return false;
* });
* }</pre>
*
* @param entity the entity
* @param tickElytra false if this is just to check if the custom elytra can be used, true if the custom elytra should also be ticked, i.e. perform side-effects of flying such as using resources.
* @return true to use a custom elytra, enabling elytra flight for the entity and cancelling subsequent handlers
*/
boolean useCustomElytra(LivingEntity entity, boolean tickElytra);
}
private EntityElytraEvents() {
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.entity.event.v1;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.entity.LivingEntity;
import net.minecraft.item.ElytraItem;
import net.minecraft.item.ItemStack;
import net.minecraft.world.event.GameEvent;
/**
* An interface that can be implemented on an item to provide custom elytra flight when it is worn in the {@link EquipmentSlot#CHEST} slot.
*
* <p>To disable cape rendering when this item is worn (like the vanilla elytra item), have a look at {@code LivingEntityFeatureRenderEvents}.
*/
public interface FabricElytraItem {
/**
* Try to use this custom elytra.
*
* @param entity the entity
* @param chestStack the stack currently worn in the chest slot, will always be of this item
* @param tickElytra true to tick the elytra, false to only perform the check; vanilla-like elytras can use {@link #doVanillaElytraTick} to handle ticking
* @return true to enable elytra flight for the entity
*/
default boolean useCustomElytra(LivingEntity entity, ItemStack chestStack, boolean tickElytra) {
if (ElytraItem.isUsable(chestStack)) {
if (tickElytra) {
doVanillaElytraTick(entity, chestStack);
}
return true;
}
return false;
}
/**
* A helper to perform the default vanilla elytra tick logic: damage the elytra every 20 ticks, and send a game event every 10 ticks.
*/
default void doVanillaElytraTick(LivingEntity entity, ItemStack chestStack) {
int nextRoll = entity.getRoll() + 1;
if (!entity.world.isClient && nextRoll % 10 == 0) {
if ((nextRoll / 10) % 2 == 0) {
chestStack.damage(1, entity, p -> p.sendEquipmentBreakStatus(EquipmentSlot.CHEST));
}
entity.emitGameEvent(GameEvent.ELYTRA_FREE_FALL);
}
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.entity.event.elytra;
import com.mojang.authlib.GameProfile;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.Slice;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.network.ClientPlayerEntity;
import net.minecraft.client.network.ClientPlayNetworkHandler;
import net.minecraft.client.world.ClientWorld;
import net.minecraft.item.Items;
import net.minecraft.network.packet.c2s.play.ClientCommandC2SPacket;
@SuppressWarnings("unused")
@Mixin(ClientPlayerEntity.class)
abstract class ClientPlayerEntityMixin extends AbstractClientPlayerEntity {
ClientPlayerEntityMixin(ClientWorld world, GameProfile profile) {
super(world, profile);
throw new AssertionError();
}
@Shadow
@Final
private ClientPlayNetworkHandler networkHandler;
/**
* Call {@link #checkFallFlying()} even if the player is not wearing {@link Items#ELYTRA} to allow custom elytra flight.
*/
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/entity/EquipmentSlot;CHEST:Lnet/minecraft/entity/EquipmentSlot;"), method = "tickMovement", slice = @Slice(from = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;isClimbing()Z"), to = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;checkFallFlying()Z")), allow = 1)
void injectElytraStart(CallbackInfo info) {
// Note that if fall flying is not ALLOWed, checkFallFlying will return false and nothing will happen.
if (this.checkFallFlying()) {
networkHandler.sendPacket(new ClientCommandC2SPacket(this, ClientCommandC2SPacket.Mode.START_FALL_FLYING));
}
}
}

View file

@ -0,0 +1,61 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.entity.event.elytra;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.world.World;
import net.fabricmc.fabric.api.entity.event.v1.EntityElytraEvents;
@SuppressWarnings("unused")
@Mixin(LivingEntity.class)
abstract class LivingEntityMixin extends Entity {
LivingEntityMixin(EntityType<?> type, World world) {
super(type, world);
throw new AssertionError();
}
/**
* Handle ALLOW and CUSTOM {@link EntityElytraEvents} when an entity is fall flying.
*/
@SuppressWarnings("ConstantConditions")
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/entity/EquipmentSlot;CHEST:Lnet/minecraft/entity/EquipmentSlot;"), method = "tickFallFlying()V", allow = 1, cancellable = true)
void injectElytraTick(CallbackInfo info) {
LivingEntity self = (LivingEntity) (Object) this;
if (!EntityElytraEvents.ALLOW.invoker().allowElytraFlight(self)) {
// The entity is already fall flying by now, we just need to stop it.
if (!world.isClient) {
setFlag(Entity.FALL_FLYING_FLAG_INDEX, false);
}
info.cancel();
}
if (EntityElytraEvents.CUSTOM.invoker().useCustomElytra(self, true)) {
// The entity is already fall flying by now, so all we need to do is an early return to bypass vanilla's own elytra check.
info.cancel();
}
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.entity.event.elytra;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import net.minecraft.entity.EntityType;
import net.minecraft.entity.LivingEntity;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.world.World;
import net.fabricmc.fabric.api.entity.event.v1.EntityElytraEvents;
@SuppressWarnings("unused")
@Mixin(PlayerEntity.class)
abstract class PlayerEntityMixin extends LivingEntity {
PlayerEntityMixin(EntityType<? extends LivingEntity> entityType, World world) {
super(entityType, world);
throw new AssertionError();
}
@Shadow
public abstract void startFallFlying();
/**
* Allow the server-side and client-side elytra checks to fail when {@link EntityElytraEvents#ALLOW} blocks flight,
* and otherwise to succeed for elytra flight through {@link EntityElytraEvents#CUSTOM}.
*/
@SuppressWarnings("ConstantConditions")
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/entity/EquipmentSlot;CHEST:Lnet/minecraft/entity/EquipmentSlot;"), method = "checkFallFlying()Z", allow = 1, cancellable = true)
void injectElytraCheck(CallbackInfoReturnable<Boolean> cir) {
PlayerEntity self = (PlayerEntity) (Object) this;
if (!EntityElytraEvents.ALLOW.invoker().allowElytraFlight(self)) {
cir.setReturnValue(false);
return; // Return to prevent the rest of this injector from running.
}
if (EntityElytraEvents.CUSTOM.invoker().useCustomElytra(self, false)) {
startFallFlying();
cir.setReturnValue(true);
}
}
}

View file

@ -3,6 +3,8 @@
"package": "net.fabricmc.fabric.mixin.entity.event",
"compatibilityLevel": "JAVA_16",
"mixins": [
"elytra/LivingEntityMixin",
"elytra/PlayerEntityMixin",
"BedBlockMixin",
"EntityMixin",
"LivingEntityMixin",
@ -11,6 +13,9 @@
"ServerPlayerEntityMixin",
"TeleportCommandMixin"
],
"client": [
"elytra/ClientPlayerEntityMixin"
],
"injectors": {
"defaultRequire": 1,
"maxShiftBy": 3

View file

@ -0,0 +1,30 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.entity.event;
import net.minecraft.entity.EquipmentSlot;
import net.minecraft.item.ArmorItem;
import net.minecraft.item.ArmorMaterials;
import net.minecraft.item.ItemGroup;
import net.fabricmc.fabric.api.entity.event.v1.FabricElytraItem;
public class DiamondElytraItem extends ArmorItem implements FabricElytraItem {
public DiamondElytraItem() {
super(ArmorMaterials.DIAMOND, EquipmentSlot.CHEST, new Settings().maxCount(1).group(ItemGroup.COMBAT));
}
}

View file

@ -41,6 +41,7 @@ import net.minecraft.util.registry.Registry;
import net.fabricmc.api.ModInitializer;
import net.fabricmc.fabric.api.command.v1.CommandRegistrationCallback;
import net.fabricmc.fabric.api.entity.event.v1.EntityElytraEvents;
import net.fabricmc.fabric.api.entity.event.v1.EntitySleepEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityCombatEvents;
import net.fabricmc.fabric.api.entity.event.v1.ServerEntityWorldChangeEvents;
@ -49,11 +50,13 @@ import net.fabricmc.fabric.api.entity.event.v1.ServerPlayerEvents;
public final class EntityEventTests implements ModInitializer {
private static final Logger LOGGER = LogManager.getLogger(EntityEventTests.class);
public static final Block TEST_BED = new TestBedBlock(AbstractBlock.Settings.of(Material.WOOL).strength(1, 1));
public static final Item DIAMOND_ELYTRA = new DiamondElytraItem();
@Override
public void onInitialize() {
Registry.register(Registry.BLOCK, new Identifier("fabric-entity-events-v1-testmod", "test_bed"), TEST_BED);
Registry.register(Registry.ITEM, new Identifier("fabric-entity-events-v1-testmod", "test_bed"), new BlockItem(TEST_BED, new Item.Settings().group(ItemGroup.DECORATIONS)));
Registry.register(Registry.ITEM, new Identifier("fabric-entity-events-v1-testmod", "diamond_elytra"), DIAMOND_ELYTRA);
ServerEntityCombatEvents.AFTER_KILLED_OTHER_ENTITY.register((world, entity, killed) -> {
LOGGER.info("Entity Killed: {}", killed);
@ -172,6 +175,11 @@ public final class EntityEventTests implements ModInitializer {
return 0;
}));
});
// Block elytra flight when holding a torch in the off-hand.
EntityElytraEvents.ALLOW.register(entity -> {
return !entity.getOffHandStack().isOf(Items.TORCH);
});
}
private static void addSleepWools(PlayerEntity player) {

View file

@ -0,0 +1,32 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.test.entity.event.client;
import net.minecraft.entity.EquipmentSlot;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRenderEvents;
import net.fabricmc.fabric.test.entity.event.EntityEventTests;
public class EntityEventTestsClient implements ClientModInitializer {
@Override
public void onInitializeClient() {
LivingEntityFeatureRenderEvents.ALLOW_CAPE_RENDER.register(player -> {
return !player.getEquippedStack(EquipmentSlot.CHEST).isOf(EntityEventTests.DIAMOND_ELYTRA);
});
}
}

View file

@ -13,6 +13,9 @@
"entrypoints": {
"main": [
"net.fabricmc.fabric.test.entity.event.EntityEventTests"
],
"client": [
"net.fabricmc.fabric.test.entity.event.client.EntityEventTestsClient"
]
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.api.client.rendering.v1;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.render.entity.feature.FeatureRenderer;
import net.fabricmc.fabric.api.event.Event;
import net.fabricmc.fabric.api.event.EventFactory;
/**
* Events related to living entity {@link FeatureRenderer}s.
* To register a renderer, see {@link LivingEntityFeatureRendererRegistrationCallback} instead.
*/
public final class LivingEntityFeatureRenderEvents {
/**
* An event that can prevent capes from rendering.
*/
public static final Event<AllowCapeRender> ALLOW_CAPE_RENDER = EventFactory.createArrayBacked(AllowCapeRender.class, listeners -> player -> {
for (AllowCapeRender listener : listeners) {
if (!listener.allowCapeRender(player)) {
return false;
}
}
return true;
});
@FunctionalInterface
public interface AllowCapeRender {
/**
* @return false to prevent rendering the cape
*/
boolean allowCapeRender(AbstractClientPlayerEntity player);
}
private LivingEntityFeatureRenderEvents() {
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright (c) 2016, 2017, 2018, 2019 FabricMC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.fabricmc.fabric.mixin.client.rendering;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import net.minecraft.client.network.AbstractClientPlayerEntity;
import net.minecraft.client.render.VertexConsumerProvider;
import net.minecraft.client.render.entity.feature.CapeFeatureRenderer;
import net.minecraft.client.util.math.MatrixStack;
import net.fabricmc.fabric.api.client.rendering.v1.LivingEntityFeatureRenderEvents;
@Mixin(CapeFeatureRenderer.class)
public class CapeFeatureRendererMixin {
@Inject(at = @At(value = "FIELD", target = "Lnet/minecraft/entity/EquipmentSlot;CHEST:Lnet/minecraft/entity/EquipmentSlot;"), method = "render", require = 1, allow = 1, cancellable = true)
public void injectCapeRenderCheck(MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, AbstractClientPlayerEntity abstractClientPlayerEntity, float f, float g, float h, float j, float k, float l, CallbackInfo ci) {
if (!LivingEntityFeatureRenderEvents.ALLOW_CAPE_RENDER.invoker().allowCapeRender(abstractClientPlayerEntity)) {
ci.cancel();
}
}
}

View file

@ -3,6 +3,7 @@
"package": "net.fabricmc.fabric.mixin.client.rendering",
"compatibilityLevel": "JAVA_16",
"client": [
"CapeFeatureRendererMixin",
"MixinArmorFeatureRenderer",
"MixinBlockColorMap",
"MixinBuiltinModelItemRenderer",