minecraft-src/net/minecraft/world/level/block/entity/TestInstanceBlockEntity.java
2025-07-04 03:45:38 +03:00

455 lines
18 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 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.Holder.Reference;
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.util.ByIdMap.OutOfBoundsStrategy;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
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 pos, BlockState state) {
super(BlockEntityType.TEST_INSTANCE_BLOCK, pos, state);
}
public void set(TestInstanceBlockEntity.Data data) {
this.data = data;
this.setChanged();
}
public static Optional<Vec3i> getStructureSize(ServerLevel level, ResourceKey<GameTestInstance> testKey) {
return getStructureTemplate(level, testKey).map(StructureTemplate::getSize);
}
public BoundingBox getStructureBoundingBox() {
BlockPos blockPos = this.getStructurePos();
BlockPos blockPos2 = blockPos.offset(this.getTransformedSize()).offset(-1, -1, -1);
return BoundingBox.fromCorners(blockPos, blockPos2);
}
public AABB getStructureBounds() {
return AABB.of(this.getStructureBoundingBox());
}
private static Optional<StructureTemplate> getStructureTemplate(ServerLevel level, ResourceKey<GameTestInstance> testKey) {
return level.registryAccess()
.get(testKey)
.map(reference -> ((GameTestInstance)reference.value()).structure())
.flatMap(resourceLocation -> level.getStructureManager().get(resourceLocation));
}
public Optional<ResourceKey<GameTestInstance>> test() {
return this.data.test();
}
public Component getTestName() {
return (Component)this.test().map(resourceKey -> Component.literal(resourceKey.location().toString())).orElse(INVALID_TEST_NAME);
}
private Optional<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 ((Rotation)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 errorMessage) {
this.set(this.data.withError(errorMessage));
}
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 registries) {
CompoundTag compoundTag = new CompoundTag();
this.saveAdditional(compoundTag, registries);
return compoundTag;
}
@Override
protected void loadAdditional(CompoundTag tag, HolderLookup.Provider registries) {
Tag tag2 = tag.get("data");
if (tag2 != null) {
TestInstanceBlockEntity.Data.CODEC.parse(NbtOps.INSTANCE, tag2).ifSuccess(this::set);
}
}
@Override
protected void saveAdditional(CompoundTag tag, HolderLookup.Provider registries) {
DataResult<Tag> dataResult = TestInstanceBlockEntity.Data.CODEC.encode(this.data, NbtOps.INSTANCE, new CompoundTag());
dataResult.ifSuccess(tagx -> tag.put("data", tagx));
}
@Override
public BoundingBoxRenderable.Mode renderMode() {
return BoundingBoxRenderable.Mode.BOX;
}
public BlockPos getStructurePos() {
return getStructurePos(this.getBlockPos());
}
public static BlockPos getStructurePos(BlockPos pos) {
return pos.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 bl = rotation == Rotation.CLOCKWISE_90 || rotation == Rotation.COUNTERCLOCKWISE_90;
int i = bl ? vec3i.getZ() : vec3i.getX();
int j = bl ? vec3i.getX() : vec3i.getZ();
return new Vec3i(i, vec3i.getY(), j);
}
public void resetTest(Consumer<Component> messageSender) {
this.removeBarriers();
boolean bl = this.placeStructure();
if (bl) {
messageSender.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> messageSender) {
Optional<Reference<GameTestInstance>> optional = this.getTestHolder();
Optional<ResourceLocation> optional2;
if (optional.isPresent()) {
optional2 = Optional.of(((GameTestInstance)((Reference)optional.get()).value()).structure());
} else {
optional2 = this.test().map(ResourceKey::location);
}
if (optional2.isEmpty()) {
BlockPos blockPos = this.getBlockPos();
messageSender.accept(
Component.translatable("test_instance_block.error.unable_to_save", blockPos.getX(), blockPos.getY(), blockPos.getZ()).withStyle(ChatFormatting.RED)
);
return optional2;
} else {
if (this.level instanceof ServerLevel serverLevel) {
StructureBlockEntity.saveStructure(serverLevel, (ResourceLocation)optional2.get(), this.getStructurePos(), this.getSize(), this.ignoreEntities(), "", true);
}
return optional2;
}
}
public boolean exportTest(Consumer<Component> messageSender) {
Optional<ResourceLocation> optional = this.saveTest(messageSender);
return !optional.isEmpty() && this.level instanceof ServerLevel serverLevel ? export(serverLevel, (ResourceLocation)optional.get(), messageSender) : false;
}
public static boolean export(ServerLevel level, ResourceLocation test, Consumer<Component> messageSender) {
Path path = StructureUtils.testStructuresDir;
Path path2 = level.getStructureManager().createAndValidatePathToGeneratedStructure(test, ".nbt");
Path path3 = NbtToSnbt.convertStructure(CachedOutput.NO_CACHE, path2, test.getPath(), path.resolve(test.getNamespace()).resolve("structure"));
if (path3 == null) {
messageSender.accept(Component.literal("Failed to export " + path2).withStyle(ChatFormatting.RED));
return true;
} else {
try {
FileUtil.createDirectoriesSafe(path3.getParent());
} catch (IOException var7) {
messageSender.accept(Component.literal("Could not create folder " + path3.getParent()).withStyle(ChatFormatting.RED));
return true;
}
messageSender.accept(Component.literal("Exported " + test + " to " + path3.toAbsolutePath()));
return false;
}
}
public void runTest(Consumer<Component> messageSender) {
if (this.level instanceof ServerLevel serverLevel) {
Optional var7 = this.getTestHolder();
BlockPos blockPos = this.getBlockPos();
if (var7.isEmpty()) {
messageSender.accept(
Component.translatable("test_instance_block.error.no_test", blockPos.getX(), blockPos.getY(), blockPos.getZ()).withStyle(ChatFormatting.RED)
);
} else if (!this.placeStructure()) {
messageSender.accept(
Component.translatable("test_instance_block.error.no_test_structure", blockPos.getX(), blockPos.getY(), blockPos.getZ()).withStyle(ChatFormatting.RED)
);
} else {
GameTestRunner.clearMarkers(serverLevel);
GameTestTicker.SINGLETON.clear();
FailedTestTracker.forgetFailedTests();
messageSender.accept(Component.translatable("test_instance_block.starting", ((Reference)var7.get()).getRegisteredName()));
GameTestInfo gameTestInfo = new GameTestInfo((Reference<GameTestInstance>)var7.get(), this.data.rotation(), serverLevel, RetryOptions.noRetries());
gameTestInfo.setTestBlockPos(blockPos);
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(resourceKey -> getStructureTemplate(serverLevel, resourceKey));
if (optional.isPresent()) {
this.placeStructure(serverLevel, (StructureTemplate)optional.get());
return true;
}
}
return false;
}
private void placeStructure(ServerLevel level, StructureTemplate structureTemplate) {
StructurePlaceSettings structurePlaceSettings = new StructurePlaceSettings()
.setRotation(this.getRotation())
.setIgnoreEntities(this.data.ignoreEntities())
.setKnownShape(true);
BlockPos blockPos = this.getStartCorner();
this.forceLoadChunks();
this.removeEntities();
structureTemplate.placeInWorld(level, blockPos, blockPos, structurePlaceSettings, level.getRandom(), 818);
}
private void removeEntities() {
this.level.getEntities(null, this.getStructureBounds()).stream().filter(entity -> !(entity instanceof Player)).forEach(Entity::discard);
}
private void forceLoadChunks() {
if (this.level instanceof ServerLevel serverLevel) {
this.getStructureBoundingBox().intersectingChunks().forEach(chunkPos -> serverLevel.setChunkForced(chunkPos.x, chunkPos.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(blockPos -> {
if (!this.level.getBlockState(blockPos).is(Blocks.TEST_INSTANCE_BLOCK)) {
this.level.setBlockAndUpdate(blockPos, Blocks.BARRIER.defaultBlockState());
}
});
}
public void removeBarriers() {
this.processStructureBoundary(blockPos -> {
if (this.level.getBlockState(blockPos).is(Blocks.BARRIER)) {
this.level.setBlockAndUpdate(blockPos, Blocks.AIR.defaultBlockState());
}
});
}
public void processStructureBoundary(Consumer<BlockPos> processor) {
AABB aABB = this.getStructureBounds();
boolean bl = !(Boolean)this.getTestHolder().map(reference -> ((GameTestInstance)reference.value()).skyAccess()).orElse(false);
BlockPos blockPos = BlockPos.containing(aABB.minX, aABB.minY, aABB.minZ).offset(-1, -1, -1);
BlockPos blockPos2 = BlockPos.containing(aABB.maxX, aABB.maxY, aABB.maxZ);
BlockPos.betweenClosedStream(blockPos, blockPos2)
.forEach(
blockPos3 -> {
boolean bl2 = blockPos3.getX() == blockPos.getX()
|| blockPos3.getX() == blockPos2.getX()
|| blockPos3.getZ() == blockPos.getZ()
|| blockPos3.getZ() == blockPos2.getZ()
|| blockPos3.getY() == blockPos.getY();
boolean bl3 = blockPos3.getY() == blockPos2.getY();
if (bl2 || bl3 && bl) {
processor.accept(blockPos3);
}
}
);
}
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(
instance -> instance.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(instance, 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 size) {
return new TestInstanceBlockEntity.Data(this.test, size, this.rotation, this.ignoreEntities, this.status, this.errorMessage);
}
public TestInstanceBlockEntity.Data withStatus(TestInstanceBlockEntity.Status status) {
return new TestInstanceBlockEntity.Data(this.test, this.size, this.rotation, this.ignoreEntities, status, Optional.empty());
}
public TestInstanceBlockEntity.Data withError(Component error) {
return new TestInstanceBlockEntity.Data(
this.test, this.size, this.rotation, this.ignoreEntities, TestInstanceBlockEntity.Status.FINISHED, Optional.of(error)
);
}
}
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(status -> status.index, values(), 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, status -> status.index
);
private final String id;
private final int index;
private Status(final String id, final int index) {
this.id = id;
this.index = index;
}
@Override
public String getSerializedName() {
return this.id;
}
public static TestInstanceBlockEntity.Status byIndex(int index) {
return (TestInstanceBlockEntity.Status)ID_MAP.apply(index);
}
}
}