mirror of
https://github.com/Sollace/Unicopia.git
synced 2025-02-08 06:26:43 +01:00
Implement method to obtain the spectral clock using the altar
This commit is contained in:
parent
28e64eebe1
commit
5a5ec8b24c
3 changed files with 238 additions and 31 deletions
|
@ -0,0 +1,42 @@
|
||||||
|
package com.minelittlepony.unicopia.ability.magic.spell.crafting;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
|
import com.minelittlepony.unicopia.item.UItems;
|
||||||
|
|
||||||
|
import net.minecraft.entity.Entity;
|
||||||
|
import net.minecraft.entity.ItemEntity;
|
||||||
|
import net.minecraft.item.ItemStack;
|
||||||
|
import net.minecraft.item.Items;
|
||||||
|
|
||||||
|
public record AltarRecipeMatch(
|
||||||
|
ItemEntity target,
|
||||||
|
List<ItemEntity> ingredients,
|
||||||
|
ItemStack result
|
||||||
|
) {
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static AltarRecipeMatch of(List<ItemEntity> inputs) {
|
||||||
|
ItemEntity clock = inputs.stream().filter(item -> item.getStack().isOf(Items.CLOCK)).findFirst().orElse(null);
|
||||||
|
|
||||||
|
if (clock != null) {
|
||||||
|
return new AltarRecipeMatch(clock, List.of(), UItems.SPECTRAL_CLOCK.getDefaultStack());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isRemoved() {
|
||||||
|
return target.isRemoved() || ingredients.stream().anyMatch(ItemEntity::isRemoved);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void craft() {
|
||||||
|
ItemStack clockStack = result.copyWithCount(target.getStack().getCount());
|
||||||
|
clockStack.setNbt(target.getStack().getNbt());
|
||||||
|
target.setStack(clockStack);
|
||||||
|
target.setInvulnerable(true);
|
||||||
|
ingredients.forEach(Entity::discard);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,19 +1,31 @@
|
||||||
package com.minelittlepony.unicopia.client.render.entity;
|
package com.minelittlepony.unicopia.client.render.entity;
|
||||||
|
|
||||||
|
import org.joml.Matrix3f;
|
||||||
|
import org.joml.Matrix4f;
|
||||||
|
|
||||||
import com.minelittlepony.unicopia.Unicopia;
|
import com.minelittlepony.unicopia.Unicopia;
|
||||||
import com.minelittlepony.unicopia.entity.mob.SpellbookEntity;
|
import com.minelittlepony.unicopia.entity.mob.SpellbookEntity;
|
||||||
|
import com.minelittlepony.unicopia.server.world.Altar;
|
||||||
|
|
||||||
|
import net.minecraft.client.render.OverlayTexture;
|
||||||
|
import net.minecraft.client.render.RenderLayer;
|
||||||
|
import net.minecraft.client.render.VertexConsumer;
|
||||||
|
import net.minecraft.client.render.VertexConsumerProvider;
|
||||||
import net.minecraft.client.render.entity.EntityRendererFactory;
|
import net.minecraft.client.render.entity.EntityRendererFactory;
|
||||||
import net.minecraft.client.render.entity.LivingEntityRenderer;
|
import net.minecraft.client.render.entity.LivingEntityRenderer;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRenderer;
|
||||||
|
import net.minecraft.client.render.entity.feature.FeatureRendererContext;
|
||||||
import net.minecraft.client.util.math.MatrixStack;
|
import net.minecraft.client.util.math.MatrixStack;
|
||||||
import net.minecraft.util.Identifier;
|
import net.minecraft.util.Identifier;
|
||||||
import net.minecraft.util.math.*;
|
import net.minecraft.util.math.*;
|
||||||
|
|
||||||
public class SpellbookEntityRenderer extends LivingEntityRenderer<SpellbookEntity, SpellbookModel> {
|
public class SpellbookEntityRenderer extends LivingEntityRenderer<SpellbookEntity, SpellbookModel> {
|
||||||
private static final Identifier TEXTURE = Unicopia.id("textures/entity/spellbook/normal.png");
|
private static final Identifier TEXTURE = Unicopia.id("textures/entity/spellbook/normal.png");
|
||||||
|
private static final Identifier ALTAR_BEAM_TEXTURE = new Identifier("textures/entity/end_crystal/end_crystal_beam.png");
|
||||||
|
|
||||||
public SpellbookEntityRenderer(EntityRendererFactory.Context context) {
|
public SpellbookEntityRenderer(EntityRendererFactory.Context context) {
|
||||||
super(context, new SpellbookModel(SpellbookModel.getTexturedModelData().createModel()), 0);
|
super(context, new SpellbookModel(SpellbookModel.getTexturedModelData().createModel()), 0);
|
||||||
|
addFeature(new AltarBeamFeature(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -51,4 +63,79 @@ public class SpellbookEntityRenderer extends LivingEntityRenderer<SpellbookEntit
|
||||||
|| targetEntity.hasCustomName()
|
|| targetEntity.hasCustomName()
|
||||||
&& targetEntity == dispatcher.targetedEntity);
|
&& targetEntity == dispatcher.targetedEntity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static class AltarBeamFeature extends FeatureRenderer<SpellbookEntity, SpellbookModel> {
|
||||||
|
public AltarBeamFeature(FeatureRendererContext<SpellbookEntity, SpellbookModel> context) {
|
||||||
|
super(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void render(MatrixStack matrices, VertexConsumerProvider vertices, int light, SpellbookEntity entity, float limbPos, float limbSpeed, float tickDelta, float animationProgress, float yaw, float pitch) {
|
||||||
|
if (!entity.hasBeams()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
matrices.peek();
|
||||||
|
matrices.pop();
|
||||||
|
matrices.push();
|
||||||
|
|
||||||
|
|
||||||
|
Altar altar = entity.getAltar().get();
|
||||||
|
Vec3d center = altar.origin().toCenterPos();
|
||||||
|
|
||||||
|
float x = (float)MathHelper.lerp(tickDelta, entity.prevX, entity.getX());
|
||||||
|
float y = (float)MathHelper.lerp(tickDelta, entity.prevY, entity.getY());
|
||||||
|
float z = (float)MathHelper.lerp(tickDelta, entity.prevZ, entity.getZ());
|
||||||
|
Vec3d bookPos = new Vec3d(x, y, z);
|
||||||
|
Vec3d shift = bookPos.subtract(center);
|
||||||
|
|
||||||
|
matrices.push();
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_Z.rotationDegrees(180));
|
||||||
|
matrices.translate(shift.x, shift.y - 1, shift.z);
|
||||||
|
|
||||||
|
for (BlockPos pillar : altar.pillars()) {
|
||||||
|
renderBeam(center.subtract(pillar.toCenterPos()), -tickDelta, -entity.age, matrices, vertices, light, 1, 0, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
matrices.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static float getYOffset(float animationProgress) {
|
||||||
|
animationProgress = MathHelper.sin(animationProgress * 0.2F) * 0.5F + 0.5F;
|
||||||
|
return ((animationProgress * animationProgress + animationProgress) * 0.4F) - 1.4F;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void renderBeam(Vec3d offset, float tickDelta, int age, MatrixStack matrices, VertexConsumerProvider buffers, int light, float r, float g, float b) {
|
||||||
|
final float horizontalDistance = (float)offset.horizontalLength();
|
||||||
|
final float distance = (float)offset.length();
|
||||||
|
matrices.push();
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_Y.rotation((float)(-Math.atan2(offset.z, offset.x)) - 1.5707964f));
|
||||||
|
matrices.multiply(RotationAxis.POSITIVE_X.rotation((float)(-Math.atan2(horizontalDistance, offset.y)) - 1.5707964f));
|
||||||
|
VertexConsumer buffer = buffers.getBuffer(RenderLayer.getEntityTranslucent(ALTAR_BEAM_TEXTURE));
|
||||||
|
final float minV = -(age + tickDelta) * 0.01f;
|
||||||
|
final float maxV = minV + (distance / 32F);
|
||||||
|
final int sides = 8;
|
||||||
|
final float diameter = 0.35F;
|
||||||
|
float segmentX = 0;
|
||||||
|
float segmentY = diameter;
|
||||||
|
float minU = 0;
|
||||||
|
MatrixStack.Entry entry = matrices.peek();
|
||||||
|
Matrix4f positionMat = entry.getPositionMatrix();
|
||||||
|
Matrix3f normalMat = entry.getNormalMatrix();
|
||||||
|
|
||||||
|
for (int i = 1; i <= sides; i++) {
|
||||||
|
float o = MathHelper.sin(i * MathHelper.TAU / sides) * diameter;
|
||||||
|
float p = MathHelper.cos(i * MathHelper.TAU / sides) * diameter;
|
||||||
|
float maxU = i / (float)sides;
|
||||||
|
buffer.vertex(positionMat, segmentX * 0.2F, segmentY * 0.2F, 0).color(0, 0, 0, 255).texture(minU, minV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next();
|
||||||
|
buffer.vertex(positionMat, segmentX, segmentY, distance).color(r, g, b, 1).texture(minU, maxV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next();
|
||||||
|
buffer.vertex(positionMat, o, p, distance).color(r, g, b, 1).texture(maxU, maxV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next();
|
||||||
|
buffer.vertex(positionMat, o * 0.2F, p * 0.2F, 0).color(0, 0, 0, 255).texture(maxU, minV).overlay(OverlayTexture.DEFAULT_UV).light(light).normal(normalMat, 0, -1, 0).next();
|
||||||
|
segmentX = o;
|
||||||
|
segmentY = p;
|
||||||
|
minU = maxU;
|
||||||
|
}
|
||||||
|
matrices.pop();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -2,9 +2,12 @@ package com.minelittlepony.unicopia.entity.mob;
|
||||||
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import org.jetbrains.annotations.Nullable;
|
||||||
|
|
||||||
import com.minelittlepony.unicopia.EquinePredicates;
|
import com.minelittlepony.unicopia.EquinePredicates;
|
||||||
import com.minelittlepony.unicopia.USounds;
|
import com.minelittlepony.unicopia.USounds;
|
||||||
import com.minelittlepony.unicopia.UTags;
|
import com.minelittlepony.unicopia.UTags;
|
||||||
|
import com.minelittlepony.unicopia.ability.magic.spell.crafting.AltarRecipeMatch;
|
||||||
import com.minelittlepony.unicopia.container.SpellbookScreenHandler;
|
import com.minelittlepony.unicopia.container.SpellbookScreenHandler;
|
||||||
import com.minelittlepony.unicopia.container.SpellbookState;
|
import com.minelittlepony.unicopia.container.SpellbookState;
|
||||||
import com.minelittlepony.unicopia.entity.MagicImmune;
|
import com.minelittlepony.unicopia.entity.MagicImmune;
|
||||||
|
@ -19,6 +22,7 @@ import net.fabricmc.fabric.api.util.TriState;
|
||||||
import net.minecraft.block.Blocks;
|
import net.minecraft.block.Blocks;
|
||||||
import net.minecraft.entity.Entity;
|
import net.minecraft.entity.Entity;
|
||||||
import net.minecraft.entity.EntityType;
|
import net.minecraft.entity.EntityType;
|
||||||
|
import net.minecraft.entity.ItemEntity;
|
||||||
import net.minecraft.entity.damage.DamageSource;
|
import net.minecraft.entity.damage.DamageSource;
|
||||||
import net.minecraft.entity.data.DataTracker;
|
import net.minecraft.entity.data.DataTracker;
|
||||||
import net.minecraft.entity.data.TrackedData;
|
import net.minecraft.entity.data.TrackedData;
|
||||||
|
@ -30,6 +34,7 @@ import net.minecraft.item.ItemStack;
|
||||||
import net.minecraft.nbt.NbtCompound;
|
import net.minecraft.nbt.NbtCompound;
|
||||||
import net.minecraft.network.PacketByteBuf;
|
import net.minecraft.network.PacketByteBuf;
|
||||||
import net.minecraft.particle.ParticleTypes;
|
import net.minecraft.particle.ParticleTypes;
|
||||||
|
import net.minecraft.predicate.entity.EntityPredicates;
|
||||||
import net.minecraft.screen.*;
|
import net.minecraft.screen.*;
|
||||||
import net.minecraft.server.network.ServerPlayerEntity;
|
import net.minecraft.server.network.ServerPlayerEntity;
|
||||||
import net.minecraft.server.world.ServerWorld;
|
import net.minecraft.server.world.ServerWorld;
|
||||||
|
@ -38,13 +43,18 @@ import net.minecraft.sound.SoundCategory;
|
||||||
import net.minecraft.text.Text;
|
import net.minecraft.text.Text;
|
||||||
import net.minecraft.util.ActionResult;
|
import net.minecraft.util.ActionResult;
|
||||||
import net.minecraft.util.Hand;
|
import net.minecraft.util.Hand;
|
||||||
|
import net.minecraft.util.math.BlockPos;
|
||||||
|
import net.minecraft.util.math.Box;
|
||||||
import net.minecraft.util.math.Vec3d;
|
import net.minecraft.util.math.Vec3d;
|
||||||
import net.minecraft.world.GameRules;
|
import net.minecraft.world.GameRules;
|
||||||
import net.minecraft.world.World;
|
import net.minecraft.world.World;
|
||||||
|
import net.minecraft.world.World.ExplosionSourceType;
|
||||||
|
|
||||||
public class SpellbookEntity extends MobEntity implements MagicImmune {
|
public class SpellbookEntity extends MobEntity implements MagicImmune {
|
||||||
private static final TrackedData<Byte> LOCKED = DataTracker.registerData(SpellbookEntity.class, TrackedDataHandlerRegistry.BYTE);
|
private static final TrackedData<Byte> LOCKED = DataTracker.registerData(SpellbookEntity.class, TrackedDataHandlerRegistry.BYTE);
|
||||||
private static final TrackedData<Boolean> ALTERED = DataTracker.registerData(SpellbookEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
|
private static final TrackedData<Boolean> ALTERED = DataTracker.registerData(SpellbookEntity.class, TrackedDataHandlerRegistry.BOOLEAN);
|
||||||
|
private static final byte ALTAR_BEAMS_START = 61;
|
||||||
|
private static final byte ALTAR_BEAMS_END = 62;
|
||||||
|
|
||||||
private static final int TICKS_TO_SLEEP = 600;
|
private static final int TICKS_TO_SLEEP = 600;
|
||||||
|
|
||||||
|
@ -55,6 +65,12 @@ public class SpellbookEntity extends MobEntity implements MagicImmune {
|
||||||
|
|
||||||
private Optional<Altar> altar = Optional.empty();
|
private Optional<Altar> altar = Optional.empty();
|
||||||
|
|
||||||
|
private boolean hasBeams;
|
||||||
|
private int beamsActive;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private AltarRecipeMatch activeRecipe;
|
||||||
|
|
||||||
public SpellbookEntity(EntityType<SpellbookEntity> type, World world) {
|
public SpellbookEntity(EntityType<SpellbookEntity> type, World world) {
|
||||||
super(type, world);
|
super(type, world);
|
||||||
setPersistent();
|
setPersistent();
|
||||||
|
@ -104,6 +120,14 @@ public class SpellbookEntity extends MobEntity implements MagicImmune {
|
||||||
this.altar = Optional.of(altar);
|
this.altar = Optional.of(altar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Optional<Altar> getAltar() {
|
||||||
|
return altar;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasBeams() {
|
||||||
|
return hasBeams && altar.isPresent();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isAltered() {
|
public boolean isAltered() {
|
||||||
return dataTracker.get(ALTERED);
|
return dataTracker.get(ALTERED);
|
||||||
}
|
}
|
||||||
|
@ -205,44 +229,83 @@ public class SpellbookEntity extends MobEntity implements MagicImmune {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
altar.pillars().forEach(pillar -> {
|
tickAltarCrafting(altar);
|
||||||
Vec3d center = pillar.toCenterPos().add(
|
|
||||||
random.nextTriangular(0.5, 0.2),
|
|
||||||
random.nextTriangular(0.5, 0.2),
|
|
||||||
random.nextTriangular(0.5, 0.2)
|
|
||||||
);
|
|
||||||
|
|
||||||
((ServerWorld)getWorld()).spawnParticles(
|
Vec3d origin = altar.origin().toCenterPos();
|
||||||
ParticleTypes.SOUL_FIRE_FLAME,
|
altar.pillars().forEach(pillar -> tickAltarPillar(origin, pillar));
|
||||||
center.x - 0.5, center.y + 0.5, center.z - 0.5,
|
|
||||||
0,
|
|
||||||
0.5, 0.5, 0.5, 0);
|
|
||||||
|
|
||||||
if (random.nextInt(12) != 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Vec3d vel = center.subtract(this.altar.get().origin().toCenterPos()).normalize();
|
|
||||||
|
|
||||||
((ServerWorld)getWorld()).spawnParticles(
|
|
||||||
ParticleTypes.SOUL_FIRE_FLAME,
|
|
||||||
center.x - 0.5, center.y + 0.5, center.z - 0.5,
|
|
||||||
0,
|
|
||||||
vel.x, vel.y, vel.z, -0.2);
|
|
||||||
|
|
||||||
if (random.nextInt(2000) == 0) {
|
|
||||||
if (getWorld().getBlockState(pillar).isOf(Blocks.CRYING_OBSIDIAN)) {
|
|
||||||
pillar = pillar.down();
|
|
||||||
}
|
|
||||||
getWorld().setBlockState(pillar, Blocks.CRYING_OBSIDIAN.getDefaultState());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setBeamTicks(int ticks) {
|
||||||
|
getWorld().sendEntityStatus(this, ticks > 0 ? ALTAR_BEAMS_START : ALTAR_BEAMS_END);
|
||||||
|
beamsActive = ticks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickAltarCrafting(Altar altar) {
|
||||||
|
if (activeRecipe == null || activeRecipe.isRemoved()) {
|
||||||
|
activeRecipe = AltarRecipeMatch.of(getWorld().getEntitiesByClass(ItemEntity.class, Box.of(altar.origin().toCenterPos(), 2, 2, 2), EntityPredicates.VALID_ENTITY));
|
||||||
|
|
||||||
|
if (activeRecipe != null) {
|
||||||
|
setBeamTicks(5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (beamsActive <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (--beamsActive > 0) {
|
||||||
|
playSound(USounds.Vanilla.ENTITY_GUARDIAN_ATTACK, 1.5F, 0.5F);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//setBeamTicks(0);
|
||||||
|
|
||||||
|
if (activeRecipe == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
activeRecipe.craft();
|
||||||
|
activeRecipe = null;
|
||||||
|
getWorld().createExplosion(this, altar.origin().getX(), altar.origin().getY(), altar.origin().getZ(), 0, ExplosionSourceType.NONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void tickAltarPillar(Vec3d origin, BlockPos pillar) {
|
||||||
|
Vec3d center = pillar.toCenterPos().add(
|
||||||
|
random.nextTriangular(0.5, 0.2),
|
||||||
|
random.nextTriangular(0.5, 0.2),
|
||||||
|
random.nextTriangular(0.5, 0.2)
|
||||||
|
);
|
||||||
|
|
||||||
|
((ServerWorld)getWorld()).spawnParticles(
|
||||||
|
ParticleTypes.SOUL_FIRE_FLAME,
|
||||||
|
center.x - 0.5, center.y + 0.5, center.z - 0.5,
|
||||||
|
0,
|
||||||
|
0.5, 0.5, 0.5, 0);
|
||||||
|
|
||||||
|
if (random.nextInt(12) != 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Vec3d vel = center.subtract(origin).normalize();
|
||||||
|
|
||||||
|
((ServerWorld)getWorld()).spawnParticles(
|
||||||
|
ParticleTypes.SOUL_FIRE_FLAME,
|
||||||
|
center.x - 0.5, center.y + 0.5, center.z - 0.5,
|
||||||
|
0,
|
||||||
|
vel.x, vel.y, vel.z, -0.2);
|
||||||
|
|
||||||
|
if (random.nextInt(2000) == 0) {
|
||||||
|
if (getWorld().getBlockState(pillar).isOf(Blocks.CRYING_OBSIDIAN)) {
|
||||||
|
pillar = pillar.down();
|
||||||
|
}
|
||||||
|
getWorld().setBlockState(pillar, Blocks.CRYING_OBSIDIAN.getDefaultState());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private boolean shouldBeSleeping() {
|
private boolean shouldBeSleeping() {
|
||||||
return MeteorlogicalUtil.getSkyAngle(getWorld()) > 1 && activeTicks <= 0;
|
return MeteorlogicalUtil.getSkyAngle(getWorld()) > 1 && activeTicks <= 0;
|
||||||
}
|
}
|
||||||
|
@ -337,4 +400,19 @@ public class SpellbookEntity extends MobEntity implements MagicImmune {
|
||||||
});
|
});
|
||||||
Altar.SERIALIZER.writeOptional("altar", compound, altar);
|
Altar.SERIALIZER.writeOptional("altar", compound, altar);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void handleStatus(byte status) {
|
||||||
|
switch (status) {
|
||||||
|
case ALTAR_BEAMS_START:
|
||||||
|
altar = Altar.locateAltar(getWorld(), getBlockPos());
|
||||||
|
hasBeams = altar.isPresent();
|
||||||
|
break;
|
||||||
|
case ALTAR_BEAMS_END:
|
||||||
|
altar = Optional.empty();
|
||||||
|
hasBeams = false;
|
||||||
|
default:
|
||||||
|
super.handleStatus(status);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue