Merge branch 'main' of https://github.com/gamma-delta/HexMod
This commit is contained in:
commit
6632c5597b
24 changed files with 163 additions and 18 deletions
|
@ -219,6 +219,7 @@ public abstract class BlockEntityAbstractImpetus extends PaucalBlockEntity imple
|
|||
}
|
||||
|
||||
if (this.foundAll) {
|
||||
this.clearEnergized();
|
||||
this.castSpell();
|
||||
this.stopCasting();
|
||||
return;
|
||||
|
@ -424,7 +425,7 @@ public abstract class BlockEntityAbstractImpetus extends PaucalBlockEntity imple
|
|||
level.playSound(null, vpos.x, vpos.y, vpos.z, sound, SoundSource.BLOCKS, 1f, pitch);
|
||||
}
|
||||
|
||||
protected void stopCasting() {
|
||||
protected void clearEnergized() {
|
||||
if (this.trackedBlocks != null) {
|
||||
for (var tracked : this.trackedBlocks) {
|
||||
var bs = this.level.getBlockState(tracked);
|
||||
|
@ -433,6 +434,10 @@ public abstract class BlockEntityAbstractImpetus extends PaucalBlockEntity imple
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void stopCasting() {
|
||||
clearEnergized();
|
||||
|
||||
this.activator = null;
|
||||
this.nextBlock = null;
|
||||
|
|
|
@ -44,6 +44,7 @@ public class BlockStoredPlayerImpetus extends BlockAbstractImpetus {
|
|||
if (entity instanceof Player) {
|
||||
// phew, we got something
|
||||
tile.setPlayer(entity.getUUID());
|
||||
tile.setChanged();
|
||||
|
||||
pLevel.playSound(pPlayer, pPos, HexSounds.IMPETUS_STOREDPLAYER_DING.get(), SoundSource.BLOCKS,
|
||||
1f, 1f);
|
||||
|
|
|
@ -26,7 +26,7 @@ object OpEval : Operator {
|
|||
val pattern = if (pat.payload is HexPattern) {
|
||||
pat.payload
|
||||
} else {
|
||||
throw MishapInvalidIota(pat, 0, TranslatableComponent("hexcasting.mishap.invalid_value.list.pattern"))
|
||||
throw MishapInvalidIota(SpellDatum.make(instrs), 0, TranslatableComponent("hexcasting.mishap.invalid_value.list.pattern"))
|
||||
}
|
||||
val res = harness.getUpdate(pattern, ctx.world)
|
||||
sideEffects.addAll(res.sideEffects)
|
||||
|
|
|
@ -43,7 +43,7 @@ object OpForEach : Operator {
|
|||
val res = harness.getUpdate(pattern, ctx.world)
|
||||
sideEffects.addAll(res.sideEffects)
|
||||
if (res.sideEffects.any { it is OperatorSideEffect.DoMishap }) {
|
||||
break
|
||||
return OperationResult(harness.stack, sideEffects)
|
||||
}
|
||||
harness.applyFunctionalData(res.newData)
|
||||
}
|
||||
|
|
|
@ -34,6 +34,9 @@ object OpBreakBlock : SpellOperator {
|
|||
override fun cast(ctx: CastingContext) {
|
||||
val pos = BlockPos(v)
|
||||
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
|
||||
val blockstate = ctx.world.getBlockState(pos)
|
||||
val tier =
|
||||
HexConfig.Server.getOpBreakHarvestLevelBecauseForgeThoughtItWasAGoodIdeaToImplementHarvestTiersUsingAnHonestToGodTopoSort()
|
||||
|
|
|
@ -33,7 +33,7 @@ object OpColorize : SpellOperator {
|
|||
|
||||
private object Spell : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
val (handStack) = ctx.getHeldItemToOperateOn { FrozenColorizer.isColorizer(it) }
|
||||
val (handStack) = ctx.getHeldItemToOperateOn { FrozenColorizer.isColorizer(it) }.copy()
|
||||
if (FrozenColorizer.isColorizer(handStack)) {
|
||||
if (ctx.withdrawItem(handStack.item, 1, true)) {
|
||||
HexPlayerDataHelper.setColorizer(ctx.caster,
|
||||
|
|
|
@ -34,6 +34,10 @@ class OpConjure(val light: Boolean) : SpellOperator {
|
|||
private data class Spell(val target: Vec3, val light: Boolean) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
val pos = BlockPos(target)
|
||||
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
|
||||
val placeContext = DirectionalPlaceContext(ctx.world, pos, Direction.DOWN, ItemStack.EMPTY, Direction.UP)
|
||||
|
||||
val worldState = ctx.world.getBlockState(pos)
|
||||
|
|
|
@ -30,11 +30,15 @@ object OpCreateWater : SpellOperator {
|
|||
|
||||
private data class Spell(val target: Vec3) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
val pos = BlockPos(target)
|
||||
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
// Just steal bucket code lmao
|
||||
val charlie = Items.WATER_BUCKET
|
||||
if (charlie is BucketItem) {
|
||||
// make the player null so we don't give them a usage statistic for example
|
||||
charlie.emptyContents(null, ctx.world, BlockPos(target), null)
|
||||
charlie.emptyContents(null, ctx.world, pos, null)
|
||||
} else {
|
||||
HexMod.getLogger().warn("Items.WATER_BUCKET wasn't a BucketItem?")
|
||||
}
|
||||
|
|
|
@ -51,7 +51,7 @@ object OpDestroyWater : SpellOperator {
|
|||
val here = todo.removeFirst()
|
||||
val distFromFocus =
|
||||
ctx.caster.position().distanceToSqr(Vec3(here.x.toDouble(), here.y.toDouble(), here.z.toDouble()))
|
||||
if (distFromFocus < Operator.MAX_DISTANCE * Operator.MAX_DISTANCE && seen.add(here)) {
|
||||
if (distFromFocus < Operator.MAX_DISTANCE * Operator.MAX_DISTANCE && seen.add(here) && ctx.world.mayInteract(ctx.caster, here)) {
|
||||
// never seen this pos in my life
|
||||
val fluid = ctx.world.getFluidState(here)
|
||||
if (fluid != Fluids.EMPTY.defaultFluidState()) {
|
||||
|
|
|
@ -36,6 +36,9 @@ object OpEdifySapling : SpellOperator {
|
|||
|
||||
private data class Spell(val pos: BlockPos) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
|
||||
val bs = ctx.world.getBlockState(pos)
|
||||
for (i in 0 until 8) {
|
||||
val success = AkashicTreeGrower.INSTANCE.growTree(
|
||||
|
|
|
@ -6,6 +6,7 @@ import at.petrak.hexcasting.api.spell.RenderedSpell
|
|||
import at.petrak.hexcasting.api.spell.SpellDatum
|
||||
import at.petrak.hexcasting.api.spell.SpellOperator
|
||||
import at.petrak.hexcasting.api.spell.casting.CastingContext
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.util.Mth
|
||||
import net.minecraft.world.level.Explosion
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
@ -30,6 +31,9 @@ class OpExplode(val fire: Boolean) : SpellOperator {
|
|||
|
||||
private data class Spell(val pos: Vec3, val strength: Double, val fire: Boolean) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
if (!ctx.world.mayInteract(ctx.caster, BlockPos(pos)))
|
||||
return
|
||||
|
||||
ctx.world.explode(
|
||||
ctx.caster,
|
||||
pos.x,
|
||||
|
|
|
@ -55,7 +55,7 @@ object OpExtinguish : SpellOperator {
|
|||
here.z.toDouble()
|
||||
)
|
||||
) // max distance to prevent runaway shenanigans
|
||||
if (distFromFocus < Operator.MAX_DISTANCE * Operator.MAX_DISTANCE && seen.add(here) && distFromTarget < 10) {
|
||||
if (distFromFocus < Operator.MAX_DISTANCE * Operator.MAX_DISTANCE && seen.add(here) && distFromTarget < 10 && ctx.world.mayInteract(ctx.caster, here)) {
|
||||
// never seen this pos in my life
|
||||
val blockstate = ctx.world.getBlockState(here)
|
||||
val success =
|
||||
|
|
|
@ -35,6 +35,10 @@ object OpIgnite : SpellOperator {
|
|||
|
||||
private data class Spell(val target: Vec3) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
val pos = BlockPos(target)
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
|
||||
// steal petra code that steals bucket code
|
||||
val maxwell = Items.FIRE_CHARGE
|
||||
if (maxwell is FireChargeItem) {
|
||||
|
@ -45,7 +49,7 @@ object OpIgnite : SpellOperator {
|
|||
null,
|
||||
InteractionHand.MAIN_HAND,
|
||||
ItemStack(maxwell.asItem()),
|
||||
BlockHitResult(target, Direction.UP, BlockPos(target), false)
|
||||
BlockHitResult(target, Direction.UP, pos, false)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
|
|
|
@ -56,7 +56,7 @@ class OpMakePackagedSpell<T : ItemPackagedSpell>(val itemType: T, val cost: Int)
|
|||
val (handStack) = ctx.getHeldItemToOperateOn { it.`is`(itemType) }
|
||||
val spellHolder = handStack.getCapability(HexCapabilities.SPELL).resolve()
|
||||
if (spellHolder.isPresent
|
||||
&& spellHolder.get().patterns != null
|
||||
&& spellHolder.get().patterns == null
|
||||
&& itemEntity.isAlive
|
||||
) {
|
||||
val entityStack = itemEntity.item.copy()
|
||||
|
|
|
@ -39,11 +39,15 @@ object OpPlaceBlock : SpellOperator {
|
|||
private data class Spell(val vec: Vec3) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
val pos = BlockPos(vec)
|
||||
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
|
||||
val bstate = ctx.world.getBlockState(pos)
|
||||
if (bstate.isAir || bstate.material.isReplaceable) {
|
||||
val placeeSlot = ctx.getOperativeSlot { it.item is BlockItem }
|
||||
if (placeeSlot != null) {
|
||||
val placeeStack = ctx.caster.inventory.getItem(placeeSlot)
|
||||
val placeeStack = ctx.caster.inventory.getItem(placeeSlot).copy()
|
||||
val placee = placeeStack.item as BlockItem
|
||||
if (ctx.withdrawItem(placee, 1, false)) {
|
||||
// https://github.com/VazkiiMods/Psi/blob/master/src/main/java/vazkii/psi/common/spell/trick/block/PieceTrickPlaceBlock.java#L143
|
||||
|
|
|
@ -36,6 +36,10 @@ object OpTheOnlyReasonAnyoneDownloadedPsi : SpellOperator {
|
|||
override fun cast(ctx: CastingContext) {
|
||||
// https://github.com/VazkiiMods/Psi/blob/master/src/main/java/vazkii/psi/common/spell/trick/PieceTrickOvergrow.java
|
||||
val pos = BlockPos(target)
|
||||
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
|
||||
val hit = BlockHitResult(Vec3.ZERO, Direction.UP, pos, false)
|
||||
val save: ItemStack = ctx.caster.getItemInHand(InteractionHand.MAIN_HAND)
|
||||
ctx.caster.setItemInHand(InteractionHand.MAIN_HAND, ItemStack(Items.BONE_MEAL))
|
||||
|
|
|
@ -31,11 +31,16 @@ object OpCreateLava : SpellOperator {
|
|||
|
||||
private data class Spell(val target: Vec3) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
val pos = BlockPos(target)
|
||||
|
||||
if (!ctx.world.mayInteract(ctx.caster, pos))
|
||||
return
|
||||
|
||||
// Just steal bucket code lmao
|
||||
val charlie = Items.LAVA_BUCKET
|
||||
if (charlie is BucketItem) {
|
||||
// make the player null so we don't give them a usage statistic for example
|
||||
charlie.emptyContents(null, ctx.world, BlockPos(target), null)
|
||||
charlie.emptyContents(null, ctx.world, pos, null)
|
||||
} else {
|
||||
HexMod.getLogger().warn("Items.LAVA_BUCKET wasn't a BucketItem?")
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import at.petrak.hexcasting.api.spell.RenderedSpell
|
|||
import at.petrak.hexcasting.api.spell.SpellDatum
|
||||
import at.petrak.hexcasting.api.spell.SpellOperator
|
||||
import at.petrak.hexcasting.api.spell.casting.CastingContext
|
||||
import net.minecraft.core.BlockPos
|
||||
import net.minecraft.world.entity.EntityType
|
||||
import net.minecraft.world.entity.LightningBolt
|
||||
import net.minecraft.world.phys.Vec3
|
||||
|
@ -29,6 +30,9 @@ object OpLightning : SpellOperator {
|
|||
|
||||
private data class Spell(val target: Vec3) : RenderedSpell {
|
||||
override fun cast(ctx: CastingContext) {
|
||||
if (!ctx.world.mayInteract(ctx.caster, BlockPos(target)))
|
||||
return
|
||||
|
||||
val lightning = LightningBolt(EntityType.LIGHTNING_BOLT, ctx.world)
|
||||
lightning.setPosRaw(target.x, target.y, target.z)
|
||||
ctx.world.addWithUUID(lightning) // why the hell is it called this it doesnt even involve a uuid
|
||||
|
|
|
@ -11,8 +11,11 @@ import net.minecraft.nbt.Tag;
|
|||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.network.chat.TranslatableComponent;
|
||||
import net.minecraft.resources.ResourceLocation;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.item.ItemEntity;
|
||||
import net.minecraft.world.item.BlockItem;
|
||||
import net.minecraft.world.item.ItemStack;
|
||||
import net.minecraft.world.level.Level;
|
||||
import net.minecraft.world.level.block.Block;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
|
@ -39,6 +42,21 @@ public class ItemSlate extends BlockItem implements DataHolderItem {
|
|||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onEntityItemUpdate(ItemStack stack, ItemEntity entity) {
|
||||
var tag = stack.getTagElement("BlockEntityTag");
|
||||
if (tag != null && tag.isEmpty())
|
||||
stack.removeTagKey("BlockEntityTag");
|
||||
return super.onEntityItemUpdate(stack, entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void inventoryTick(ItemStack pStack, Level pLevel, Entity pEntity, int pSlotId, boolean pIsSelected) {
|
||||
var tag = pStack.getTagElement("BlockEntityTag");
|
||||
if (tag != null && tag.isEmpty())
|
||||
pStack.removeTagKey("BlockEntityTag");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable CompoundTag readDatumTag(ItemStack stack) {
|
||||
var stackTag = stack.getTag();
|
||||
|
@ -61,14 +79,20 @@ public class ItemSlate extends BlockItem implements DataHolderItem {
|
|||
|
||||
@Override
|
||||
public boolean canWrite(ItemStack stack, SpellDatum<?> datum) {
|
||||
return !(datum == null || datum.getType() != DatumType.PATTERN);
|
||||
return datum == null || datum.getType() == DatumType.PATTERN;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeDatum(ItemStack stack, SpellDatum<?> datum) {
|
||||
if (this.canWrite(stack, datum) && datum.getPayload() instanceof HexPattern pat) {
|
||||
var beTag = stack.getOrCreateTagElement("BlockEntityTag");
|
||||
beTag.put(BlockEntitySlate.TAG_PATTERN, pat.serializeToNBT());
|
||||
}
|
||||
if (this.canWrite(stack, datum))
|
||||
if (datum == null) {
|
||||
var beTag = stack.getOrCreateTagElement("BlockEntityTag");
|
||||
beTag.remove(BlockEntitySlate.TAG_PATTERN);
|
||||
if (beTag.isEmpty())
|
||||
stack.removeTagKey("BlockEntityTag");
|
||||
} else if (datum.getPayload() instanceof HexPattern pat) {
|
||||
var beTag = stack.getOrCreateTagElement("BlockEntityTag");
|
||||
beTag.put(BlockEntitySlate.TAG_PATTERN, pat.serializeToNBT());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
package at.petrak.hexcasting.common.misc;
|
||||
|
||||
import at.petrak.hexcasting.common.network.HexMessages;
|
||||
import at.petrak.hexcasting.common.network.MsgBrainsweepAck;
|
||||
import at.petrak.hexcasting.mixin.AccessorLivingEntity;
|
||||
import net.minecraft.server.level.ServerLevel;
|
||||
import net.minecraft.server.level.ServerPlayer;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraft.world.entity.ai.Brain;
|
||||
import net.minecraft.world.entity.npc.Villager;
|
||||
import net.minecraft.world.entity.npc.VillagerDataHolder;
|
||||
import net.minecraftforge.event.entity.living.LivingConversionEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerEvent;
|
||||
import net.minecraftforge.event.entity.player.PlayerInteractEvent;
|
||||
import net.minecraftforge.eventbus.api.SubscribeEvent;
|
||||
import net.minecraftforge.network.PacketDistributor;
|
||||
|
||||
public class Brainsweeping {
|
||||
|
||||
|
@ -29,6 +35,19 @@ public class Brainsweeping {
|
|||
}
|
||||
((AccessorLivingEntity) entity).hex$SetBrain(brain.copyWithoutBehaviors());
|
||||
}
|
||||
|
||||
if (entity.level instanceof ServerLevel) {
|
||||
HexMessages.getNetwork().send(PacketDistributor.TRACKING_ENTITY.with(() -> entity), MsgBrainsweepAck.of(entity));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SubscribeEvent
|
||||
public static void startTracking(PlayerEvent.StartTracking evt) {
|
||||
Entity target = evt.getTarget();
|
||||
if (evt.getPlayer() instanceof ServerPlayer serverPlayer &&
|
||||
target instanceof VillagerDataHolder && target instanceof LivingEntity living && isBrainswept(living)) {
|
||||
HexMessages.getNetwork().send(PacketDistributor.PLAYER.with(() -> serverPlayer), MsgBrainsweepAck.of(living));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -40,6 +40,8 @@ public class HexMessages {
|
|||
MsgOpenSpellGuiAck::deserialize, MsgOpenSpellGuiAck::handle);
|
||||
NETWORK.registerMessage(messageIdx++, MsgBeepAck.class, MsgBeepAck::serialize,
|
||||
MsgBeepAck::deserialize, MsgBeepAck::handle);
|
||||
NETWORK.registerMessage(messageIdx++, MsgBrainsweepAck.class, MsgBrainsweepAck::serialize,
|
||||
MsgBrainsweepAck::deserialize, MsgBrainsweepAck::handle);
|
||||
|
||||
HexApiMessages.setSyncChannel(NETWORK, MsgSentinelStatusUpdateAck::new, MsgColorizerUpdateAck::new, MsgCastParticleAck::new);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
package at.petrak.hexcasting.common.network;
|
||||
|
||||
import at.petrak.hexcasting.common.misc.Brainsweeping;
|
||||
import io.netty.buffer.ByteBuf;
|
||||
import net.minecraft.client.Minecraft;
|
||||
import net.minecraft.network.FriendlyByteBuf;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.LivingEntity;
|
||||
import net.minecraftforge.api.distmarker.Dist;
|
||||
import net.minecraftforge.fml.DistExecutor;
|
||||
import net.minecraftforge.network.NetworkEvent;
|
||||
|
||||
import java.util.function.Supplier;
|
||||
|
||||
/**
|
||||
* Sent server->client to synchronize the status of a brainswept mob.
|
||||
*/
|
||||
public record MsgBrainsweepAck(int target) {
|
||||
public static MsgBrainsweepAck deserialize(ByteBuf buffer) {
|
||||
var buf = new FriendlyByteBuf(buffer);
|
||||
|
||||
var target = buf.readInt();
|
||||
return new MsgBrainsweepAck(target);
|
||||
}
|
||||
|
||||
public void serialize(ByteBuf buffer) {
|
||||
var buf = new FriendlyByteBuf(buffer);
|
||||
buf.writeInt(target);
|
||||
}
|
||||
|
||||
public static MsgBrainsweepAck of(Entity target) {
|
||||
return new MsgBrainsweepAck(target.getId());
|
||||
}
|
||||
|
||||
public void handle(Supplier<NetworkEvent.Context> ctx) {
|
||||
ctx.get().enqueueWork(() ->
|
||||
DistExecutor.unsafeRunWhenOn(Dist.CLIENT, () -> () -> {
|
||||
IHateJava.handle(target);
|
||||
})
|
||||
);
|
||||
ctx.get().setPacketHandled(true);
|
||||
}
|
||||
|
||||
private static class IHateJava {
|
||||
public static void handle(int target) {
|
||||
var level = Minecraft.getInstance().level;
|
||||
if (level != null) {
|
||||
Entity entity = level.getEntity(target);
|
||||
if (entity instanceof LivingEntity living) {
|
||||
Brainsweeping.brainsweep(living);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -690,7 +690,7 @@
|
|||
"hexcasting.page.math.cos": "Take the cosine of an angle in radians, yielding the horizontal component of that angle drawn on a unit circle.",
|
||||
"hexcasting.page.math.tan": "Take the tangent of an angle in radians, yielding the slope of that angle drawn on a circle.",
|
||||
"hexcasting.page.math.arcsin": "Take the inverse sine of a value with absolute value 1 or less, yielding the angle whose sine is that value.",
|
||||
"hexcasting.page.math.arccos": "Take the inverse cosine of a value with absolute value 1 or less, yielding the angle whose sine is that value.",
|
||||
"hexcasting.page.math.arccos": "Take the inverse cosine of a value with absolute value 1 or less, yielding the angle whose cosine is that value.",
|
||||
"hexcasting.page.math.arctan": "Take the inverse tangent of a value, yielding the angle whose tangent is that value.",
|
||||
"hexcasting.page.math.random": "Creates a random number between 0 and 1.",
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
{
|
||||
"name": "hexcasting.entry.101",
|
||||
"category": "hexcasting:casting",
|
||||
"icon": "hexcasting:edified_wand",
|
||||
"icon": "hexcasting:wand_akashic",
|
||||
"advancement": "hexcasting:root",
|
||||
"priority": true,
|
||||
"pages": [
|
||||
|
|
Loading…
Reference in a new issue