diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 01ddb0ce..1c91525f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -68,12 +68,6 @@ jobs: with: name: ${{ env.jar }} path: ${{ env.jar }} - - name: Delete Draft Releases - if: "!contains(github.event.commits[0].message, '[release-skip]')" - continue-on-error: true - uses: hugo19941994/delete-draft-releases@v1.0.1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Create Release if: "!contains(github.event.commits[0].message, '[release-skip]')" continue-on-error: true diff --git a/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch b/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch index 301a67b5..ca535cda 100644 --- a/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch +++ b/leaves-server/minecraft-patches/features/0090-Servux-Protocol.patch @@ -5,14 +5,14 @@ Subject: [PATCH] Servux Protocol diff --git a/net/minecraft/server/level/ServerLevel.java b/net/minecraft/server/level/ServerLevel.java -index 1deedd3fc28792fc368580973038c1824b15691d..8dc71b01b9e7970729a82c68402eec9375dbae04 100644 +index 1deedd3fc28792fc368580973038c1824b15691d..f25eca541f377ee7b908a900c6a1b50f02a41eef 100644 --- a/net/minecraft/server/level/ServerLevel.java +++ b/net/minecraft/server/level/ServerLevel.java @@ -2189,6 +2189,7 @@ public class ServerLevel extends Level implements ServerEntityGetter, WorldGenLe } this.lastSpawnChunkRadius = i; -+ org.leavesmc.leaves.protocol.servux.ServuxStructuresProtocol.refreshSpawnMetadata = true; // Leaves - servux ++ org.leavesmc.leaves.protocol.servux.ServuxHudDataProtocol.refreshSpawnMetadata = true; // Leaves - servux } public LongSet getForcedChunks() { diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java index 855e0d8d..4880e1ef 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/LeavesConfig.java @@ -747,6 +747,12 @@ public final class LeavesConfig { @GlobalConfig("entity-protocol") public boolean entityProtocol = false; + + @GlobalConfig("hud-metadata-protocol") + public boolean hudMetadataProtocol = false; + + @GlobalConfig("hud-metadata-protocol-share-seed") + public boolean hudMetadataShareSeed = true; } @GlobalConfig("bbor-protocol") diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java index eceb1b83..e9e8ad9c 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/LitematicaEasyPlaceProtocol.java @@ -49,10 +49,16 @@ public class LitematicaEasyPlaceProtocol { BlockStateProperties.ROTATION_16 ); + public static final ImmutableSet> BLACKLISTED_PROPERTIES = ImmutableSet.of( + BlockStateProperties.WATERLOGGED, + BlockStateProperties.POWERED + ); + public static BlockState applyPlacementProtocol(BlockState state, BlockPlaceContext context) { return applyPlacementProtocolV3(state, UseContext.from(context, context.getHand())); } + @SuppressWarnings("unchecked") @Nullable private static > BlockState applyPlacementProtocolV3(BlockState state, @NotNull UseContext context) { int protocolValue = (int) (context.getHitVec().x - (double) context.getPos().getX()) - 2; @@ -86,37 +92,49 @@ public class LitematicaEasyPlaceProtocol { try { for (Property p : propList) { - if (((p instanceof EnumProperty ep) && !ep.getValueClass().equals(Direction.class)) && WHITELISTED_PROPERTIES.contains(p)) { - @SuppressWarnings("unchecked") - Property prop = (Property) p; - List list = new ArrayList<>(prop.getPossibleValues()); - list.sort(Comparable::compareTo); + if (property != null && property.equals(p)) { + continue; + } + if (!WHITELISTED_PROPERTIES.contains(p) || BLACKLISTED_PROPERTIES.contains(p)) { + continue; + } - int requiredBits = Mth.log2(Mth.smallestEncompassingPowerOfTwo(list.size())); - int bitMask = ~(0xFFFFFFFF << requiredBits); - int valueIndex = protocolValue & bitMask; + Property prop = (Property) p; + List list = new ArrayList<>(prop.getPossibleValues()); + list.sort(Comparable::compareTo); - if (valueIndex < list.size()) { - T value = list.get(valueIndex); + int requiredBits = Mth.log2(Mth.smallestEncompassingPowerOfTwo(list.size())); + int bitMask = ~(0xFFFFFFFF << requiredBits); + int valueIndex = protocolValue & bitMask; - if (!state.getValue(prop).equals(value) && value != SlabType.DOUBLE) { - state = state.setValue(prop, value); + if (valueIndex < list.size()) { + T value = list.get(valueIndex); - if (state.canSurvive(context.getWorld(), context.getPos())) { - oldState = state; - } else { - state = oldState; - } + if (!state.getValue(prop).equals(value) && value != SlabType.DOUBLE) { + state = state.setValue(prop, value); + + if (state.canSurvive(context.getWorld(), context.getPos())) { + oldState = state; + } else { + state = oldState; } - - protocolValue >>>= requiredBits; } + + protocolValue >>>= requiredBits; } } } catch (Exception e) { LeavesLogger.LOGGER.warning("Exception trying to apply placement protocol value", e); } + for (Property p : BLACKLISTED_PROPERTIES) { + if (state.hasProperty(p)) { + Property prop = (Property) p; + BlockState def = state.getBlock().defaultBlockState(); + state = state.setValue(prop, def.getValue(prop)); + } + } + if (state.canSurvive(context.getWorld(), context.getPos())) { return state; } else { diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java new file mode 100644 index 00000000..78b45989 --- /dev/null +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxHudDataProtocol.java @@ -0,0 +1,265 @@ +package org.leavesmc.leaves.protocol.servux; + +import com.mojang.serialization.DataResult; +import io.netty.buffer.Unpooled; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.nbt.ListTag; +import net.minecraft.nbt.NbtOps; +import net.minecraft.nbt.Tag; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.resources.ResourceLocation; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; +import net.minecraft.world.item.crafting.Recipe; +import net.minecraft.world.item.crafting.RecipeHolder; +import net.minecraft.world.level.GameRules; +import org.jetbrains.annotations.NotNull; +import org.leavesmc.leaves.LeavesConfig; +import org.leavesmc.leaves.protocol.core.LeavesCustomPayload; +import org.leavesmc.leaves.protocol.core.LeavesProtocol; +import org.leavesmc.leaves.protocol.core.ProtocolHandler; +import org.leavesmc.leaves.protocol.core.ProtocolUtils; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +@LeavesProtocol(namespace = "servux") +public class ServuxHudDataProtocol { + + public static final ResourceLocation CHANNEL = ServuxProtocol.id("hud_metadata"); + public static final int PROTOCOL_VERSION = 1; + + private static final List players = new ArrayList<>(); + private static final int updateInterval = 80; + + public static boolean refreshSpawnMetadata = false; + + @ProtocolHandler.PayloadReceiver(payload = HudDataPayload.class, payloadId = "hud_metadata") + public static void onPacketReceive(ServerPlayer player, HudDataPayload payload) { + if (!LeavesConfig.protocol.servux.hudMetadataProtocol) { + return; + } + + switch (payload.packetType) { + case PACKET_C2S_METADATA_REQUEST -> { + players.add(player); + + CompoundTag metadata = new CompoundTag(); + metadata.putString("name", "hud_metadata"); + metadata.putString("id", CHANNEL.toString()); + metadata.putInt("version", PROTOCOL_VERSION); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + putWorldData(metadata); + + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_METADATA, metadata)); + } + + case PACKET_C2S_SPAWN_DATA_REQUEST -> refreshSpawnMetadata(player); + case PACKET_C2S_RECIPE_MANAGER_REQUEST -> refreshRecipeManager(player); + } + } + + @ProtocolHandler.Ticker + public void onTick() { + if (!LeavesConfig.protocol.servux.hudMetadataProtocol) { + return; + } + + MinecraftServer server = MinecraftServer.getServer(); + int tickCounter = server.getTickCount(); + if ((tickCounter % updateInterval) == 0) { + for (ServerPlayer player : players) { + if (refreshSpawnMetadata) { + refreshSpawnMetadata(player); + } + refreshWeatherData(player); + } + refreshSpawnMetadata = false; + } + } + + public static void refreshSpawnMetadata(ServerPlayer player) { + if (!LeavesConfig.protocol.servux.hudMetadataProtocol) { + return; + } + + CompoundTag metadata = new CompoundTag(); + metadata.putString("id", CHANNEL.toString()); + metadata.putString("servux", ServuxProtocol.SERVUX_STRING); + putWorldData(metadata); + + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_SPAWN_DATA, metadata)); + } + + public static void refreshRecipeManager(ServerPlayer player) { + if (!LeavesConfig.protocol.servux.hudMetadataProtocol) { + return; + } + + Collection> recipes = player.server.getRecipeManager().getRecipes(); + CompoundTag nbt = new CompoundTag(); + ListTag list = new ListTag(); + + recipes.forEach((recipeEntry -> { + DataResult dr = Recipe.CODEC.encodeStart(NbtOps.INSTANCE, recipeEntry.value()); + + if (dr.result().isPresent()) { + CompoundTag entry = new CompoundTag(); + entry.putString("id_reg", recipeEntry.id().registry().toString()); + entry.putString("id_value", recipeEntry.id().location().toString()); + entry.put("recipe", dr.result().get()); + list.add(entry); + } + })); + + nbt.put("RecipeManager", list); + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_START, nbt)); + } + + public static void refreshWeatherData(ServerPlayer player) { + if (!LeavesConfig.protocol.servux.hudMetadataProtocol) { + return; + } + + ServerLevel level = MinecraftServer.getServer().overworld(); + if (level.getGameRules().getBoolean(GameRules.RULE_WEATHER_CYCLE)) { + return; + } + + CompoundTag nbt = new CompoundTag(); + nbt.putString("id", CHANNEL.toString()); + nbt.putString("servux", ServuxProtocol.SERVUX_STRING); + + if (level.serverLevelData.isRaining() && level.serverLevelData.getRainTime() > -1) { + nbt.putInt("SetRaining", level.serverLevelData.getRainTime()); + nbt.putBoolean("isRaining", true); + } else { + nbt.putBoolean("isRaining", false); + } + + if (level.serverLevelData.isThundering() && level.serverLevelData.getThunderTime() > -1) { + nbt.putInt("SetThundering", level.serverLevelData.getThunderTime()); + nbt.putBoolean("isThundering", true); + } else { + nbt.putBoolean("isThundering", false); + } + + if (level.serverLevelData.getClearWeatherTime() > -1) { + nbt.putInt("SetClear", level.serverLevelData.getClearWeatherTime()); + } + + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_WEATHER_TICK, nbt)); + } + + private static void putWorldData(@NotNull CompoundTag metadata) { + ServerLevel level = MinecraftServer.getServer().overworld(); + BlockPos spawnPos = level.levelData.getSpawnPos(); + metadata.putInt("spawnPosX", spawnPos.getX()); + metadata.putInt("spawnPosY", spawnPos.getY()); + metadata.putInt("spawnPosZ", spawnPos.getZ()); + metadata.putInt("spawnChunkRadius", level.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); + + if (LeavesConfig.protocol.servux.hudMetadataShareSeed) { + metadata.putLong("worldSeed", level.getSeed()); + } + } + + public static void sendPacket(ServerPlayer player, HudDataPayload payload) { + if (!LeavesConfig.protocol.servux.hudMetadataProtocol) { + return; + } + + if (payload.packetType == HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_START) { + FriendlyByteBuf buffer = new FriendlyByteBuf(Unpooled.buffer()); + buffer.writeNbt(payload.nbt); + PacketSplitter.send(ServuxHudDataProtocol::sendWithSplitter, buffer, player); + } else { + ProtocolUtils.sendPayloadPacket(player, payload); + } + } + + private static void sendWithSplitter(ServerPlayer player, FriendlyByteBuf buf) { + sendPacket(player, new HudDataPayload(HudDataPayloadType.PACKET_S2C_NBT_RESPONSE_DATA, buf)); + } + + public enum HudDataPayloadType { + PACKET_S2C_METADATA(1), + PACKET_C2S_METADATA_REQUEST(2), + PACKET_S2C_SPAWN_DATA(3), + PACKET_C2S_SPAWN_DATA_REQUEST(4), + PACKET_S2C_WEATHER_TICK(5), + PACKET_C2S_RECIPE_MANAGER_REQUEST(6), + // For Packet Splitter (Oversize Packets, S2C) + PACKET_S2C_NBT_RESPONSE_START(10), + PACKET_S2C_NBT_RESPONSE_DATA(11); + + private static final class Helper { + static Map ID_TO_TYPE = new HashMap<>(); + } + + public final int type; + + HudDataPayloadType(int type) { + this.type = type; + HudDataPayloadType.Helper.ID_TO_TYPE.put(type, this); + } + + public static HudDataPayloadType fromId(int id) { + return HudDataPayloadType.Helper.ID_TO_TYPE.get(id); + } + } + + public record HudDataPayload(HudDataPayloadType packetType, CompoundTag nbt, FriendlyByteBuf buffer) implements LeavesCustomPayload { + + public HudDataPayload(HudDataPayloadType type, CompoundTag nbt) { + this(type, nbt, null); + } + + public HudDataPayload(HudDataPayloadType type, FriendlyByteBuf buffer) { + this(type, new CompoundTag(), buffer); + } + + @New + @NotNull + public static HudDataPayload decode(ResourceLocation location, @NotNull FriendlyByteBuf buf) { + HudDataPayloadType type = HudDataPayloadType.fromId(buf.readVarInt()); + if (type == null) { + throw new IllegalStateException("invalid packet type received"); + } + + switch (type) { + case PACKET_S2C_NBT_RESPONSE_DATA -> { + return new HudDataPayload(type, new FriendlyByteBuf(buf.readBytes(buf.readableBytes()))); + } + + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA, PACKET_C2S_SPAWN_DATA_REQUEST, PACKET_S2C_SPAWN_DATA, PACKET_S2C_WEATHER_TICK, + PACKET_C2S_RECIPE_MANAGER_REQUEST -> { + return new HudDataPayload(type, buf.readNbt()); + } + } + + throw new IllegalStateException("invalid packet type received"); + } + + @Override + public void write(@NotNull FriendlyByteBuf buf) { + buf.writeVarInt(this.packetType.type); + + switch (this.packetType) { + case PACKET_S2C_NBT_RESPONSE_DATA -> buf.writeBytes(this.buffer.copy()); + case PACKET_C2S_METADATA_REQUEST, PACKET_S2C_METADATA, PACKET_C2S_SPAWN_DATA_REQUEST, + PACKET_S2C_SPAWN_DATA, PACKET_S2C_WEATHER_TICK, PACKET_C2S_RECIPE_MANAGER_REQUEST -> buf.writeNbt(this.nbt); + } + } + + @Override + public ResourceLocation id() { + return CHANNEL; + } + } +} diff --git a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java index fb5779d4..9902dc66 100644 --- a/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java +++ b/leaves-server/src/main/java/org/leavesmc/leaves/protocol/servux/ServuxStructuresProtocol.java @@ -4,7 +4,6 @@ import io.netty.buffer.Unpooled; import it.unimi.dsi.fastutil.longs.LongIterator; import it.unimi.dsi.fastutil.longs.LongOpenHashSet; import it.unimi.dsi.fastutil.longs.LongSet; -import net.minecraft.core.BlockPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.ListTag; import net.minecraft.network.FriendlyByteBuf; @@ -13,7 +12,6 @@ import net.minecraft.server.MinecraftServer; import net.minecraft.server.level.ServerLevel; import net.minecraft.server.level.ServerPlayer; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.GameRules; import net.minecraft.world.level.Level; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.LevelChunk; @@ -46,7 +44,6 @@ public class ServuxStructuresProtocol { private static final int updateInterval = 40; private static final int timeout = 30 * 20; - public static boolean refreshSpawnMetadata = false; private static int retainDistance; public static final ResourceLocation CHANNEL = ServuxProtocol.id("structures"); @@ -62,23 +59,11 @@ public class ServuxStructuresProtocol { switch (payload.packetType()) { case PACKET_C2S_STRUCTURES_REGISTER -> onPlayerSubscribed(player); - case PACKET_C2S_REQUEST_SPAWN_METADATA -> refreshSpawnMetadata(player); - case PACKET_C2S_STRUCTURES_UNREGISTER -> { - onPlayerLoggedOut(player); - refreshSpawnMetadata(player); - } + case PACKET_C2S_REQUEST_SPAWN_METADATA -> ServuxHudDataProtocol.refreshSpawnMetadata(player); // move to + case PACKET_C2S_STRUCTURES_UNREGISTER -> onPlayerLoggedOut(player); } } - @ProtocolHandler.PlayerJoin - public static void onPlayerLoggedIn(ServerPlayer player) { - if (!LeavesConfig.protocol.servux.structureProtocol) { - return; - } - - onPlayerSubscribed(player); - } - @ProtocolHandler.PlayerLeave public static void onPlayerLoggedOut(@NotNull ServerPlayer player) { if (!LeavesConfig.protocol.servux.structureProtocol) { @@ -99,17 +84,9 @@ public class ServuxStructuresProtocol { if ((tickCounter % updateInterval) == 0) { retainDistance = server.getPlayerList().getViewDistance() + 2; for (ServerPlayer player : players.values()) { - if (refreshSpawnMetadata) { - refreshSpawnMetadata(player); - } - // TODO DimensionChange refreshTrackedChunks(player, tickCounter); } - - if (refreshSpawnMetadata) { - refreshSpawnMetadata = false; - } } } @@ -134,23 +111,24 @@ public class ServuxStructuresProtocol { } } - private static boolean chunkHasStructureReferences(int chunkX, int chunkZ, Level world) { + private static boolean chunkHasStructureReferences(int chunkX, int chunkZ, @NotNull Level world) { if (!world.hasChunk(chunkX, chunkZ)) { return false; } ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); - for (Map.Entry entry : chunk.getAllReferences().entrySet()) { - if (!entry.getValue().isEmpty()) { - return true; + if (chunk != null) { + for (Map.Entry entry : chunk.getAllReferences().entrySet()) { + if (!entry.getValue().isEmpty()) { + return true; + } } } return false; } - public static void onPlayerSubscribed(@NotNull ServerPlayer player) { if (!players.containsKey(player.getId())) { players.put(player.getId(), player); @@ -158,6 +136,7 @@ public class ServuxStructuresProtocol { LeavesLogger.LOGGER.warning(player.getScoreboardName() + " re-register servux:structures"); } + MinecraftServer server = MinecraftServer.getServer(); CompoundTag tag = new CompoundTag(); tag.putString("name", "structure_bounding_boxes"); tag.putString("id", CHANNEL.toString()); @@ -165,32 +144,10 @@ public class ServuxStructuresProtocol { tag.putString("servux", ServuxProtocol.SERVUX_STRING); tag.putInt("timeout", timeout); - MinecraftServer server = MinecraftServer.getServer(); - BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); - tag.putInt("spawnPosX", spawnPos.getX()); - tag.putInt("spawnPosY", spawnPos.getY()); - tag.putInt("spawnPosZ", spawnPos.getZ()); - tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); - sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_METADATA, tag)); initialSyncStructures(player, player.moonrise$getViewDistanceHolder().getViewDistances().sendViewDistance() + 2, server.getTickCount()); } - public static void refreshSpawnMetadata(ServerPlayer player) { - CompoundTag tag = new CompoundTag(); - tag.putString("id", CHANNEL.toString()); - tag.putString("servux", ServuxProtocol.SERVUX_STRING); - - MinecraftServer server = MinecraftServer.getServer(); - BlockPos spawnPos = server.overworld().levelData.getSpawnPos(); - tag.putInt("spawnPosX", spawnPos.getX()); - tag.putInt("spawnPosY", spawnPos.getY()); - tag.putInt("spawnPosZ", spawnPos.getZ()); - tag.putInt("spawnChunkRadius", server.getGameRules().getInt(GameRules.RULE_SPAWN_CHUNK_RADIUS)); - - sendPacket(player, new StructuresPayload(StructuresPayloadType.PACKET_S2C_SPAWN_METADATA, tag)); - } - public static void initialSyncStructures(ServerPlayer player, int chunkRadius, int tickCounter) { UUID uuid = player.getUUID(); ChunkPos center = player.getLastSectionPos().chunk(); @@ -220,17 +177,19 @@ public class ServuxStructuresProtocol { ChunkAccess chunk = world.getChunk(chunkX, chunkZ, ChunkStatus.STRUCTURE_STARTS, false); - for (Map.Entry entry : chunk.getAllReferences().entrySet()) { - Structure feature = entry.getKey(); - LongSet startChunks = entry.getValue(); + if (chunk != null) { + for (Map.Entry entry : chunk.getAllReferences().entrySet()) { + Structure feature = entry.getKey(); + LongSet startChunks = entry.getValue(); - // TODO add an option && feature != StructureFeature.MINESHAFT (?) - if (!startChunks.isEmpty()) { - references.merge(feature, startChunks, (oldSet, entrySet) -> { - LongOpenHashSet newSet = new LongOpenHashSet(oldSet); - newSet.addAll(entrySet); - return newSet; - }); + // TODO add an option && feature != StructureFeature.MINESHAFT (?) + if (!startChunks.isEmpty()) { + references.merge(feature, startChunks, (oldSet, entrySet) -> { + LongOpenHashSet newSet = new LongOpenHashSet(oldSet); + newSet.addAll(entrySet); + return newSet; + }); + } } } } @@ -280,7 +239,10 @@ public class ServuxStructuresProtocol { } ChunkAccess chunk = world.getChunk(pos.x, pos.z, ChunkStatus.STRUCTURE_STARTS, false); - StructureStart start = chunk.getStartForStructure(structure); + StructureStart start = null; + if (chunk != null) { + start = chunk.getStartForStructure(structure); + } if (start != null) { starts.put(pos, start);