464 lines
20 KiB
Java
464 lines
20 KiB
Java
package net.minecraft.world.level.block.entity;
|
|
|
|
import com.mojang.serialization.Codec;
|
|
import com.mojang.serialization.DataResult;
|
|
import com.mojang.serialization.codecs.RecordCodecBuilder;
|
|
import com.mojang.serialization.codecs.RecordCodecBuilder.Instance;
|
|
import io.netty.buffer.ByteBuf;
|
|
import java.io.IOException;
|
|
import java.nio.file.Path;
|
|
import java.util.List;
|
|
import java.util.Optional;
|
|
import java.util.function.Consumer;
|
|
import java.util.function.IntFunction;
|
|
import net.minecraft.ChatFormatting;
|
|
import net.minecraft.FileUtil;
|
|
import net.minecraft.core.BlockPos;
|
|
import net.minecraft.core.Holder;
|
|
import net.minecraft.core.HolderLookup;
|
|
import net.minecraft.core.Vec3i;
|
|
import net.minecraft.core.registries.Registries;
|
|
import net.minecraft.data.CachedOutput;
|
|
import net.minecraft.data.structures.NbtToSnbt;
|
|
import net.minecraft.gametest.framework.FailedTestTracker;
|
|
import net.minecraft.gametest.framework.GameTestInfo;
|
|
import net.minecraft.gametest.framework.GameTestInstance;
|
|
import net.minecraft.gametest.framework.GameTestRunner;
|
|
import net.minecraft.gametest.framework.GameTestTicker;
|
|
import net.minecraft.gametest.framework.RetryOptions;
|
|
import net.minecraft.gametest.framework.StructureUtils;
|
|
import net.minecraft.gametest.framework.TestCommand;
|
|
import net.minecraft.nbt.CompoundTag;
|
|
import net.minecraft.nbt.NbtOps;
|
|
import net.minecraft.nbt.Tag;
|
|
import net.minecraft.network.RegistryFriendlyByteBuf;
|
|
import net.minecraft.network.chat.Component;
|
|
import net.minecraft.network.chat.ComponentSerialization;
|
|
import net.minecraft.network.codec.ByteBufCodecs;
|
|
import net.minecraft.network.codec.StreamCodec;
|
|
import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket;
|
|
import net.minecraft.resources.ResourceKey;
|
|
import net.minecraft.resources.ResourceLocation;
|
|
import net.minecraft.server.level.ServerLevel;
|
|
import net.minecraft.util.ARGB;
|
|
import net.minecraft.util.ByIdMap;
|
|
import net.minecraft.util.StringRepresentable;
|
|
import net.minecraft.world.entity.Entity;
|
|
import net.minecraft.world.entity.player.Player;
|
|
import net.minecraft.world.level.ChunkPos;
|
|
import net.minecraft.world.level.block.Blocks;
|
|
import net.minecraft.world.level.block.Rotation;
|
|
import net.minecraft.world.level.block.state.BlockState;
|
|
import net.minecraft.world.level.levelgen.structure.BoundingBox;
|
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructurePlaceSettings;
|
|
import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate;
|
|
import net.minecraft.world.phys.AABB;
|
|
|
|
public class TestInstanceBlockEntity extends BlockEntity implements BeaconBeamOwner, BoundingBoxRenderable {
|
|
private static final Component INVALID_TEST_NAME = Component.translatable("test_instance_block.invalid_test");
|
|
private static final List<BeaconBeamOwner.Section> BEAM_CLEARED = List.of();
|
|
private static final List<BeaconBeamOwner.Section> BEAM_RUNNING = List.of(new BeaconBeamOwner.Section(ARGB.color(128, 128, 128)));
|
|
private static final List<BeaconBeamOwner.Section> BEAM_SUCCESS = List.of(new BeaconBeamOwner.Section(ARGB.color(0, 255, 0)));
|
|
private static final List<BeaconBeamOwner.Section> BEAM_REQUIRED_FAILED = List.of(new BeaconBeamOwner.Section(ARGB.color(255, 0, 0)));
|
|
private static final List<BeaconBeamOwner.Section> BEAM_OPTIONAL_FAILED = List.of(new BeaconBeamOwner.Section(ARGB.color(255, 128, 0)));
|
|
private static final Vec3i STRUCTURE_OFFSET = new Vec3i(0, 1, 1);
|
|
private TestInstanceBlockEntity.Data data = new TestInstanceBlockEntity.Data(
|
|
Optional.empty(), Vec3i.ZERO, Rotation.NONE, false, TestInstanceBlockEntity.Status.CLEARED, Optional.empty()
|
|
);
|
|
|
|
public TestInstanceBlockEntity(BlockPos p_391764_, BlockState p_397638_) {
|
|
super(BlockEntityType.TEST_INSTANCE_BLOCK, p_391764_, p_397638_);
|
|
}
|
|
|
|
public void set(TestInstanceBlockEntity.Data p_393871_) {
|
|
this.data = p_393871_;
|
|
this.setChanged();
|
|
}
|
|
|
|
public static Optional<Vec3i> getStructureSize(ServerLevel p_398000_, ResourceKey<GameTestInstance> p_395860_) {
|
|
return getStructureTemplate(p_398000_, p_395860_).map(StructureTemplate::getSize);
|
|
}
|
|
|
|
public BoundingBox getStructureBoundingBox() {
|
|
BlockPos blockpos = this.getStructurePos();
|
|
BlockPos blockpos1 = blockpos.offset(this.getTransformedSize()).offset(-1, -1, -1);
|
|
return BoundingBox.fromCorners(blockpos, blockpos1);
|
|
}
|
|
|
|
public AABB getStructureBounds() {
|
|
return AABB.of(this.getStructureBoundingBox());
|
|
}
|
|
|
|
private static Optional<StructureTemplate> getStructureTemplate(ServerLevel p_392001_, ResourceKey<GameTestInstance> p_396410_) {
|
|
return p_392001_.registryAccess()
|
|
.get(p_396410_)
|
|
.map(p_394388_ -> p_394388_.value().structure())
|
|
.flatMap(p_396007_ -> p_392001_.getStructureManager().get(p_396007_));
|
|
}
|
|
|
|
public Optional<ResourceKey<GameTestInstance>> test() {
|
|
return this.data.test();
|
|
}
|
|
|
|
public Component getTestName() {
|
|
return this.test().<Component>map(p_392404_ -> Component.literal(p_392404_.location().toString())).orElse(INVALID_TEST_NAME);
|
|
}
|
|
|
|
private Optional<Holder.Reference<GameTestInstance>> getTestHolder() {
|
|
return this.test().flatMap(this.level.registryAccess()::get);
|
|
}
|
|
|
|
public boolean ignoreEntities() {
|
|
return this.data.ignoreEntities();
|
|
}
|
|
|
|
public Vec3i getSize() {
|
|
return this.data.size();
|
|
}
|
|
|
|
public Rotation getRotation() {
|
|
return this.getTestHolder().map(Holder::value).map(GameTestInstance::rotation).orElse(Rotation.NONE).getRotated(this.data.rotation());
|
|
}
|
|
|
|
public Optional<Component> errorMessage() {
|
|
return this.data.errorMessage();
|
|
}
|
|
|
|
public void setErrorMessage(Component p_397441_) {
|
|
this.set(this.data.withError(p_397441_));
|
|
}
|
|
|
|
public void setSuccess() {
|
|
this.set(this.data.withStatus(TestInstanceBlockEntity.Status.FINISHED));
|
|
this.removeBarriers();
|
|
}
|
|
|
|
public void setRunning() {
|
|
this.set(this.data.withStatus(TestInstanceBlockEntity.Status.RUNNING));
|
|
}
|
|
|
|
@Override
|
|
public void setChanged() {
|
|
super.setChanged();
|
|
if (this.level instanceof ServerLevel) {
|
|
this.level.sendBlockUpdated(this.getBlockPos(), Blocks.AIR.defaultBlockState(), this.getBlockState(), 3);
|
|
}
|
|
}
|
|
|
|
public ClientboundBlockEntityDataPacket getUpdatePacket() {
|
|
return ClientboundBlockEntityDataPacket.create(this);
|
|
}
|
|
|
|
@Override
|
|
public CompoundTag getUpdateTag(HolderLookup.Provider p_395983_) {
|
|
CompoundTag compoundtag = new CompoundTag();
|
|
this.saveAdditional(compoundtag, p_395983_);
|
|
return compoundtag;
|
|
}
|
|
|
|
@Override
|
|
protected void loadAdditional(CompoundTag p_395055_, HolderLookup.Provider p_397378_) {
|
|
Tag tag = p_395055_.get("data");
|
|
if (tag != null) {
|
|
TestInstanceBlockEntity.Data.CODEC.parse(NbtOps.INSTANCE, tag).ifSuccess(this::set);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
protected void saveAdditional(CompoundTag p_394253_, HolderLookup.Provider p_394177_) {
|
|
DataResult<Tag> dataresult = TestInstanceBlockEntity.Data.CODEC.encode(this.data, NbtOps.INSTANCE, new CompoundTag());
|
|
dataresult.ifSuccess(p_391242_ -> p_394253_.put("data", p_391242_));
|
|
}
|
|
|
|
@Override
|
|
public BoundingBoxRenderable.Mode renderMode() {
|
|
return BoundingBoxRenderable.Mode.BOX;
|
|
}
|
|
|
|
public BlockPos getStructurePos() {
|
|
return getStructurePos(this.getBlockPos());
|
|
}
|
|
|
|
public static BlockPos getStructurePos(BlockPos p_397922_) {
|
|
return p_397922_.offset(STRUCTURE_OFFSET);
|
|
}
|
|
|
|
@Override
|
|
public BoundingBoxRenderable.RenderableBox getRenderableBox() {
|
|
return new BoundingBoxRenderable.RenderableBox(new BlockPos(STRUCTURE_OFFSET), this.getTransformedSize());
|
|
}
|
|
|
|
@Override
|
|
public List<BeaconBeamOwner.Section> getBeamSections() {
|
|
return switch (this.data.status()) {
|
|
case CLEARED -> BEAM_CLEARED;
|
|
case RUNNING -> BEAM_RUNNING;
|
|
case FINISHED -> this.errorMessage().isEmpty()
|
|
? BEAM_SUCCESS
|
|
: (this.getTestHolder().map(Holder::value).map(GameTestInstance::required).orElse(true) ? BEAM_REQUIRED_FAILED : BEAM_OPTIONAL_FAILED);
|
|
};
|
|
}
|
|
|
|
private Vec3i getTransformedSize() {
|
|
Vec3i vec3i = this.getSize();
|
|
Rotation rotation = this.getRotation();
|
|
boolean flag = rotation == Rotation.CLOCKWISE_90 || rotation == Rotation.COUNTERCLOCKWISE_90;
|
|
int i = flag ? vec3i.getZ() : vec3i.getX();
|
|
int j = flag ? vec3i.getX() : vec3i.getZ();
|
|
return new Vec3i(i, vec3i.getY(), j);
|
|
}
|
|
|
|
public void resetTest(Consumer<Component> p_393061_) {
|
|
this.removeBarriers();
|
|
boolean flag = this.placeStructure();
|
|
if (flag) {
|
|
p_393061_.accept(Component.translatable("test_instance_block.reset_success", this.getTestName()).withStyle(ChatFormatting.GREEN));
|
|
}
|
|
|
|
this.set(this.data.withStatus(TestInstanceBlockEntity.Status.CLEARED));
|
|
}
|
|
|
|
public Optional<ResourceLocation> saveTest(Consumer<Component> p_394558_) {
|
|
Optional<Holder.Reference<GameTestInstance>> optional = this.getTestHolder();
|
|
Optional<ResourceLocation> optional1;
|
|
if (optional.isPresent()) {
|
|
optional1 = Optional.of(optional.get().value().structure());
|
|
} else {
|
|
optional1 = this.test().map(ResourceKey::location);
|
|
}
|
|
|
|
if (optional1.isEmpty()) {
|
|
BlockPos blockpos = this.getBlockPos();
|
|
p_394558_.accept(
|
|
Component.translatable("test_instance_block.error.unable_to_save", blockpos.getX(), blockpos.getY(), blockpos.getZ())
|
|
.withStyle(ChatFormatting.RED)
|
|
);
|
|
return optional1;
|
|
} else {
|
|
if (this.level instanceof ServerLevel serverlevel) {
|
|
StructureBlockEntity.saveStructure(serverlevel, optional1.get(), this.getStructurePos(), this.getSize(), this.ignoreEntities(), "", true);
|
|
}
|
|
|
|
return optional1;
|
|
}
|
|
}
|
|
|
|
public boolean exportTest(Consumer<Component> p_394235_) {
|
|
Optional<ResourceLocation> optional = this.saveTest(p_394235_);
|
|
return !optional.isEmpty() && this.level instanceof ServerLevel serverlevel ? export(serverlevel, optional.get(), p_394235_) : false;
|
|
}
|
|
|
|
public static boolean export(ServerLevel p_397675_, ResourceLocation p_396122_, Consumer<Component> p_397687_) {
|
|
Path path = StructureUtils.testStructuresDir;
|
|
Path path1 = p_397675_.getStructureManager().createAndValidatePathToGeneratedStructure(p_396122_, ".nbt");
|
|
Path path2 = NbtToSnbt.convertStructure(CachedOutput.NO_CACHE, path1, p_396122_.getPath(), path.resolve(p_396122_.getNamespace()).resolve("structure"));
|
|
if (path2 == null) {
|
|
p_397687_.accept(Component.literal("Failed to export " + path1).withStyle(ChatFormatting.RED));
|
|
return true;
|
|
} else {
|
|
try {
|
|
FileUtil.createDirectoriesSafe(path2.getParent());
|
|
} catch (IOException ioexception) {
|
|
p_397687_.accept(Component.literal("Could not create folder " + path2.getParent()).withStyle(ChatFormatting.RED));
|
|
return true;
|
|
}
|
|
|
|
p_397687_.accept(Component.literal("Exported " + p_396122_ + " to " + path2.toAbsolutePath()));
|
|
return false;
|
|
}
|
|
}
|
|
|
|
public void runTest(Consumer<Component> p_394749_) {
|
|
if (this.level instanceof ServerLevel serverlevel) {
|
|
Optional optional = this.getTestHolder();
|
|
BlockPos $$4 = this.getBlockPos();
|
|
if (optional.isEmpty()) {
|
|
p_394749_.accept(
|
|
Component.translatable("test_instance_block.error.no_test", $$4.getX(), $$4.getY(), $$4.getZ()).withStyle(ChatFormatting.RED)
|
|
);
|
|
} else if (!this.placeStructure()) {
|
|
p_394749_.accept(
|
|
Component.translatable("test_instance_block.error.no_test_structure", $$4.getX(), $$4.getY(), $$4.getZ())
|
|
.withStyle(ChatFormatting.RED)
|
|
);
|
|
} else {
|
|
GameTestRunner.clearMarkers(serverlevel);
|
|
GameTestTicker.SINGLETON.clear();
|
|
FailedTestTracker.forgetFailedTests();
|
|
p_394749_.accept(Component.translatable("test_instance_block.starting", ((Holder.Reference)optional.get()).getRegisteredName()));
|
|
GameTestInfo gametestinfo = new GameTestInfo(
|
|
(Holder.Reference<GameTestInstance>)optional.get(), this.data.rotation(), serverlevel, RetryOptions.noRetries()
|
|
);
|
|
gametestinfo.setTestBlockPos($$4);
|
|
GameTestRunner gametestrunner = GameTestRunner.Builder.fromInfo(List.of(gametestinfo), serverlevel).build();
|
|
TestCommand.trackAndStartRunner(serverlevel.getServer().createCommandSourceStack(), gametestrunner);
|
|
}
|
|
}
|
|
}
|
|
|
|
public boolean placeStructure() {
|
|
if (this.level instanceof ServerLevel serverlevel) {
|
|
Optional<StructureTemplate> optional = this.data
|
|
.test()
|
|
.flatMap(p_391773_ -> getStructureTemplate(serverlevel, (ResourceKey<GameTestInstance>)p_391773_));
|
|
if (optional.isPresent()) {
|
|
this.placeStructure(serverlevel, optional.get());
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private void placeStructure(ServerLevel p_397389_, StructureTemplate p_391354_) {
|
|
StructurePlaceSettings structureplacesettings = new StructurePlaceSettings()
|
|
.setRotation(this.getRotation())
|
|
.setIgnoreEntities(this.data.ignoreEntities())
|
|
.setKnownShape(true);
|
|
BlockPos blockpos = this.getStartCorner();
|
|
this.forceLoadChunks();
|
|
this.removeEntities();
|
|
p_391354_.placeInWorld(p_397389_, blockpos, blockpos, structureplacesettings, p_397389_.getRandom(), 818);
|
|
}
|
|
|
|
private void removeEntities() {
|
|
this.level.getEntities(null, this.getStructureBounds()).stream().filter(p_395081_ -> !(p_395081_ instanceof Player)).forEach(Entity::discard);
|
|
}
|
|
|
|
private void forceLoadChunks() {
|
|
if (this.level instanceof ServerLevel serverlevel) {
|
|
this.getStructureBoundingBox().intersectingChunks().forEach(p_393129_ -> serverlevel.setChunkForced(p_393129_.x, p_393129_.z, true));
|
|
}
|
|
}
|
|
|
|
public BlockPos getStartCorner() {
|
|
Vec3i vec3i = this.getSize();
|
|
Rotation rotation = this.getRotation();
|
|
BlockPos blockpos = this.getStructurePos();
|
|
|
|
return switch (rotation) {
|
|
case NONE -> blockpos;
|
|
case CLOCKWISE_90 -> blockpos.offset(vec3i.getZ() - 1, 0, 0);
|
|
case CLOCKWISE_180 -> blockpos.offset(vec3i.getX() - 1, 0, vec3i.getZ() - 1);
|
|
case COUNTERCLOCKWISE_90 -> blockpos.offset(0, 0, vec3i.getX() - 1);
|
|
};
|
|
}
|
|
|
|
public void encaseStructure() {
|
|
this.processStructureBoundary(p_392761_ -> {
|
|
if (!this.level.getBlockState(p_392761_).is(Blocks.TEST_INSTANCE_BLOCK)) {
|
|
this.level.setBlockAndUpdate(p_392761_, Blocks.BARRIER.defaultBlockState());
|
|
}
|
|
});
|
|
}
|
|
|
|
public void removeBarriers() {
|
|
this.processStructureBoundary(p_392752_ -> {
|
|
if (this.level.getBlockState(p_392752_).is(Blocks.BARRIER)) {
|
|
this.level.setBlockAndUpdate(p_392752_, Blocks.AIR.defaultBlockState());
|
|
}
|
|
});
|
|
}
|
|
|
|
public void processStructureBoundary(Consumer<BlockPos> p_392766_) {
|
|
AABB aabb = this.getStructureBounds();
|
|
boolean flag = !this.getTestHolder().map(p_392664_ -> p_392664_.value().skyAccess()).orElse(false);
|
|
BlockPos blockpos = BlockPos.containing(aabb.minX, aabb.minY, aabb.minZ).offset(-1, -1, -1);
|
|
BlockPos blockpos1 = BlockPos.containing(aabb.maxX, aabb.maxY, aabb.maxZ);
|
|
BlockPos.betweenClosedStream(blockpos, blockpos1)
|
|
.forEach(
|
|
p_396842_ -> {
|
|
boolean flag1 = p_396842_.getX() == blockpos.getX()
|
|
|| p_396842_.getX() == blockpos1.getX()
|
|
|| p_396842_.getZ() == blockpos.getZ()
|
|
|| p_396842_.getZ() == blockpos1.getZ()
|
|
|| p_396842_.getY() == blockpos.getY();
|
|
boolean flag2 = p_396842_.getY() == blockpos1.getY();
|
|
if (flag1 || flag2 && flag) {
|
|
p_392766_.accept(p_396842_);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
public record Data(
|
|
Optional<ResourceKey<GameTestInstance>> test,
|
|
Vec3i size,
|
|
Rotation rotation,
|
|
boolean ignoreEntities,
|
|
TestInstanceBlockEntity.Status status,
|
|
Optional<Component> errorMessage
|
|
) {
|
|
public static final Codec<TestInstanceBlockEntity.Data> CODEC = RecordCodecBuilder.create(
|
|
p_396530_ -> p_396530_.group(
|
|
ResourceKey.codec(Registries.TEST_INSTANCE).optionalFieldOf("test").forGetter(TestInstanceBlockEntity.Data::test),
|
|
Vec3i.CODEC.fieldOf("size").forGetter(TestInstanceBlockEntity.Data::size),
|
|
Rotation.CODEC.fieldOf("rotation").forGetter(TestInstanceBlockEntity.Data::rotation),
|
|
Codec.BOOL.fieldOf("ignore_entities").forGetter(TestInstanceBlockEntity.Data::ignoreEntities),
|
|
TestInstanceBlockEntity.Status.CODEC.fieldOf("status").forGetter(TestInstanceBlockEntity.Data::status),
|
|
ComponentSerialization.CODEC.optionalFieldOf("error_message").forGetter(TestInstanceBlockEntity.Data::errorMessage)
|
|
)
|
|
.apply(p_396530_, TestInstanceBlockEntity.Data::new)
|
|
);
|
|
public static final StreamCodec<RegistryFriendlyByteBuf, TestInstanceBlockEntity.Data> STREAM_CODEC = StreamCodec.composite(
|
|
ByteBufCodecs.optional(ResourceKey.streamCodec(Registries.TEST_INSTANCE)),
|
|
TestInstanceBlockEntity.Data::test,
|
|
Vec3i.STREAM_CODEC,
|
|
TestInstanceBlockEntity.Data::size,
|
|
Rotation.STREAM_CODEC,
|
|
TestInstanceBlockEntity.Data::rotation,
|
|
ByteBufCodecs.BOOL,
|
|
TestInstanceBlockEntity.Data::ignoreEntities,
|
|
TestInstanceBlockEntity.Status.STREAM_CODEC,
|
|
TestInstanceBlockEntity.Data::status,
|
|
ByteBufCodecs.optional(ComponentSerialization.STREAM_CODEC),
|
|
TestInstanceBlockEntity.Data::errorMessage,
|
|
TestInstanceBlockEntity.Data::new
|
|
);
|
|
|
|
public TestInstanceBlockEntity.Data withSize(Vec3i p_396807_) {
|
|
return new TestInstanceBlockEntity.Data(this.test, p_396807_, this.rotation, this.ignoreEntities, this.status, this.errorMessage);
|
|
}
|
|
|
|
public TestInstanceBlockEntity.Data withStatus(TestInstanceBlockEntity.Status p_395515_) {
|
|
return new TestInstanceBlockEntity.Data(this.test, this.size, this.rotation, this.ignoreEntities, p_395515_, Optional.empty());
|
|
}
|
|
|
|
public TestInstanceBlockEntity.Data withError(Component p_392455_) {
|
|
return new TestInstanceBlockEntity.Data(
|
|
this.test, this.size, this.rotation, this.ignoreEntities, TestInstanceBlockEntity.Status.FINISHED, Optional.of(p_392455_)
|
|
);
|
|
}
|
|
}
|
|
|
|
public static enum Status implements StringRepresentable {
|
|
CLEARED("cleared", 0),
|
|
RUNNING("running", 1),
|
|
FINISHED("finished", 2);
|
|
|
|
private static final IntFunction<TestInstanceBlockEntity.Status> ID_MAP = ByIdMap.continuous(
|
|
p_392218_ -> p_392218_.index, values(), ByIdMap.OutOfBoundsStrategy.ZERO
|
|
);
|
|
public static final Codec<TestInstanceBlockEntity.Status> CODEC = StringRepresentable.fromEnum(TestInstanceBlockEntity.Status::values);
|
|
public static final StreamCodec<ByteBuf, TestInstanceBlockEntity.Status> STREAM_CODEC = ByteBufCodecs.idMapper(
|
|
TestInstanceBlockEntity.Status::byIndex, p_394997_ -> p_394997_.index
|
|
);
|
|
private final String id;
|
|
private final int index;
|
|
|
|
private Status(final String p_392199_, final int p_395871_) {
|
|
this.id = p_392199_;
|
|
this.index = p_395871_;
|
|
}
|
|
|
|
@Override
|
|
public String getSerializedName() {
|
|
return this.id;
|
|
}
|
|
|
|
public static TestInstanceBlockEntity.Status byIndex(int p_397766_) {
|
|
return ID_MAP.apply(p_397766_);
|
|
}
|
|
}
|
|
}
|