dev
This commit is contained in:
@@ -55,7 +55,7 @@ val hytaleServer: Configuration by configurations.creating
|
||||
val patchedJar = rootDir.resolve("repo/applications/HytaleServerPatched.jar")
|
||||
|
||||
dependencies {
|
||||
hytaleServer("com.hypixel.hytale:Server:2026.01.28-87d03be09")
|
||||
hytaleServer("com.hypixel.hytale:Server:2026.02.17-255364b8e")
|
||||
}
|
||||
|
||||
configurations {
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
package com.hypixel.hytale.builtin.adventure.memories.interactions;
|
||||
|
||||
import com.hypixel.hytale.builtin.adventure.memories.component.PlayerMemories;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.KeyedCodec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.protocol.InteractionState;
|
||||
import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.protocol.WaitForDataFrom;
|
||||
import com.hypixel.hytale.protocol.packets.player.UpdateMemoriesFeatureStatus;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.entity.InteractionContext;
|
||||
import com.hypixel.hytale.server.core.io.PacketHandler;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.SimpleInstantInteraction;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.util.NotificationUtil;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
public class SetMemoriesCapacityInteraction extends SimpleInstantInteraction {
|
||||
public static final BuilderCodec<SetMemoriesCapacityInteraction> CODEC = BuilderCodec.builder(
|
||||
SetMemoriesCapacityInteraction.class, SetMemoriesCapacityInteraction::new, SimpleInstantInteraction.CODEC
|
||||
)
|
||||
.documentation("Sets how many memories a player can store.")
|
||||
.<Integer>appendInherited(
|
||||
new KeyedCodec<>("Capacity", Codec.INTEGER), (i, s) -> i.capacity = s, i -> i.capacity, (i, parent) -> i.capacity = parent.capacity
|
||||
)
|
||||
.documentation("Defines the amount of memories that a player can store.")
|
||||
.add()
|
||||
.build();
|
||||
private int capacity;
|
||||
|
||||
public SetMemoriesCapacityInteraction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void firstRun(@Nonnull InteractionType type, @Nonnull InteractionContext context, @Nonnull CooldownHandler cooldownHandler) {
|
||||
// HyFix #52: Validate ComponentType before use to prevent crash when memories module isn't loaded
|
||||
if (!PlayerMemories.getComponentType().isValid()) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
Ref<EntityStore> ref = context.getEntity();
|
||||
CommandBuffer<EntityStore> commandBuffer = context.getCommandBuffer();
|
||||
|
||||
assert commandBuffer != null;
|
||||
|
||||
PlayerMemories memoriesComponent = commandBuffer.ensureAndGetComponent(ref, PlayerMemories.getComponentType());
|
||||
if (this.capacity <= memoriesComponent.getMemoriesCapacity()) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
} else {
|
||||
int previousCapacity = memoriesComponent.getMemoriesCapacity();
|
||||
memoriesComponent.setMemoriesCapacity(this.capacity);
|
||||
if (previousCapacity <= 0) {
|
||||
PlayerRef playerRefComponent = commandBuffer.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
PacketHandler playerConnection = playerRefComponent.getPacketHandler();
|
||||
playerConnection.writeNoCache(new UpdateMemoriesFeatureStatus(true));
|
||||
NotificationUtil.sendNotification(
|
||||
playerConnection, Message.translation("server.memories.general.featureUnlockedNotification"), null, "NotificationIcons/MemoriesIcon.png"
|
||||
);
|
||||
playerRefComponent.sendMessage(Message.translation("server.memories.general.featureUnlockedMessage"));
|
||||
}
|
||||
|
||||
context.getState().state = InteractionState.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public WaitForDataFrom getWaitForDataFrom() {
|
||||
return WaitForDataFrom.Server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
package com.hypixel.hytale.builtin.adventure.objectives.task;
|
||||
|
||||
import com.hypixel.hytale.builtin.adventure.objectives.Objective;
|
||||
import com.hypixel.hytale.builtin.adventure.objectives.config.task.BlockTagOrItemIdField;
|
||||
import com.hypixel.hytale.builtin.adventure.objectives.config.task.GatherObjectiveTaskAsset;
|
||||
import com.hypixel.hytale.builtin.adventure.objectives.transaction.RegistrationTransactionRecord;
|
||||
import com.hypixel.hytale.builtin.adventure.objectives.transaction.TransactionRecord;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.math.util.MathUtil;
|
||||
import com.hypixel.hytale.server.core.entity.LivingEntity;
|
||||
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.event.events.entity.LivingEntityInventoryChangeEvent;
|
||||
import com.hypixel.hytale.server.core.inventory.container.CombinedItemContainer;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
|
||||
public class GatherObjectiveTask extends CountObjectiveTask {
|
||||
public static final BuilderCodec<GatherObjectiveTask> CODEC = BuilderCodec.builder(
|
||||
GatherObjectiveTask.class, GatherObjectiveTask::new, CountObjectiveTask.CODEC
|
||||
)
|
||||
.build();
|
||||
|
||||
public GatherObjectiveTask(@Nonnull GatherObjectiveTaskAsset asset, int taskSetIndex, int taskIndex) {
|
||||
super(asset, taskSetIndex, taskIndex);
|
||||
}
|
||||
|
||||
protected GatherObjectiveTask() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public GatherObjectiveTaskAsset getAsset() {
|
||||
return (GatherObjectiveTaskAsset) super.getAsset();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected TransactionRecord[] setup0(@Nonnull Objective objective, @Nonnull World world, @Nonnull Store<EntityStore> store) {
|
||||
Set<UUID> participatingPlayers = objective.getPlayerUUIDs();
|
||||
int countItem = this.countObjectiveItemInInventories(participatingPlayers, store);
|
||||
if (this.areTaskConditionsFulfilled(null, null, participatingPlayers)) {
|
||||
this.count = MathUtil.clamp(countItem, 0, this.getAsset().getCount());
|
||||
if (this.checkCompletion()) {
|
||||
this.consumeTaskConditions(null, null, participatingPlayers);
|
||||
this.complete = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
this.eventRegistry.register(LivingEntityInventoryChangeEvent.class, world.getName(), event -> {
|
||||
LivingEntity livingEntity = event.getEntity();
|
||||
if (livingEntity instanceof Player) {
|
||||
Ref<EntityStore> ref = livingEntity.getReference();
|
||||
World refWorld = store.getExternalData().getWorld();
|
||||
refWorld.execute(() -> {
|
||||
// HyFix: Validate ref before use - prevents NPE crash when player disconnects
|
||||
// between event dispatch and lambda execution
|
||||
if (ref == null || !ref.isValid()) {
|
||||
return;
|
||||
}
|
||||
UUIDComponent uuidComponent = store.getComponent(ref, UUIDComponent.getComponentType());
|
||||
if (uuidComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<UUID> activePlayerUUIDs = objective.getActivePlayerUUIDs();
|
||||
if (activePlayerUUIDs.contains(uuidComponent.getUuid())) {
|
||||
int count = this.countObjectiveItemInInventories(activePlayerUUIDs, store);
|
||||
this.setTaskCompletion(store, ref, count, objective);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return RegistrationTransactionRecord.wrap(this.eventRegistry);
|
||||
}
|
||||
|
||||
private int countObjectiveItemInInventories(@Nonnull Set<UUID> participatingPlayers, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
int count = 0;
|
||||
BlockTagOrItemIdField blockTypeOrSet = this.getAsset().getBlockTagOrItemIdField();
|
||||
|
||||
for (UUID playerUUID : participatingPlayers) {
|
||||
PlayerRef playerRefComponent = Universe.get().getPlayer(playerUUID);
|
||||
if (playerRefComponent != null) {
|
||||
Ref<EntityStore> playerRef = playerRefComponent.getReference();
|
||||
if (playerRef != null && playerRef.isValid()) {
|
||||
Player playerComponent = componentAccessor.getComponent(playerRef, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
CombinedItemContainer inventory = playerComponent.getInventory().getCombinedHotbarFirst();
|
||||
count += inventory.countItemStacks(itemStack -> blockTypeOrSet.isBlockTypeIncluded(itemStack.getItemId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GatherObjectiveTask{} " + super.toString();
|
||||
}
|
||||
}
|
||||
@@ -1,143 +0,0 @@
|
||||
package com.hypixel.hytale.builtin.blocktick.system;
|
||||
|
||||
import com.hypixel.hytale.builtin.blocktick.BlockTickPlugin;
|
||||
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.dependency.Dependency;
|
||||
import com.hypixel.hytale.component.dependency.Order;
|
||||
import com.hypixel.hytale.component.dependency.SystemDependency;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktick.BlockTickManager;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktick.BlockTickStrategy;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktick.config.TickProcedure;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
|
||||
import com.hypixel.hytale.server.core.modules.time.WorldTimeResource;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ChunkBlockTickSystem {
|
||||
protected static final HytaleLogger LOGGER = BlockTickPlugin.get().getLogger();
|
||||
|
||||
public ChunkBlockTickSystem() {
|
||||
}
|
||||
|
||||
public static class PreTick extends EntityTickingSystem<ChunkStore> {
|
||||
private static final ComponentType<ChunkStore, BlockChunk> COMPONENT_TYPE = BlockChunk.getComponentType();
|
||||
|
||||
public PreTick() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return COMPONENT_TYPE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.useParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
Instant time = commandBuffer.getExternalData().getWorld().getEntityStore().getStore().getResource(WorldTimeResource.getResourceType()).getGameTime();
|
||||
BlockChunk chunk = archetypeChunk.getComponent(index, COMPONENT_TYPE);
|
||||
|
||||
assert chunk != null;
|
||||
|
||||
try {
|
||||
chunk.preTick(time);
|
||||
} catch (Throwable var9) {
|
||||
ChunkBlockTickSystem.LOGGER.at(Level.SEVERE).withCause(var9).log("Failed to pre-tick chunk: %s", chunk);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class Ticking extends EntityTickingSystem<ChunkStore> {
|
||||
private static final ComponentType<ChunkStore, WorldChunk> COMPONENT_TYPE = WorldChunk.getComponentType();
|
||||
private static final Set<Dependency<ChunkStore>> DEPENDENCIES = Set.of(new SystemDependency<>(Order.AFTER, ChunkBlockTickSystem.PreTick.class));
|
||||
|
||||
public Ticking() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return COMPONENT_TYPE;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<ChunkStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
Ref<ChunkStore> reference = archetypeChunk.getReferenceTo(index);
|
||||
WorldChunk worldChunk = archetypeChunk.getComponent(index, COMPONENT_TYPE);
|
||||
|
||||
try {
|
||||
tick(reference, worldChunk);
|
||||
} catch (Throwable var9) {
|
||||
ChunkBlockTickSystem.LOGGER.at(Level.SEVERE).withCause(var9).log("Failed to tick chunk: %s", worldChunk);
|
||||
}
|
||||
}
|
||||
|
||||
protected static void tick(Ref<ChunkStore> ref, @Nonnull WorldChunk worldChunk) {
|
||||
int ticked = worldChunk.getBlockChunk().forEachTicking(ref, worldChunk, (r, c, localX, localY, localZ, blockId) -> {
|
||||
World world = c.getWorld();
|
||||
int blockX = c.getX() << 5 | localX;
|
||||
int blockZ = c.getZ() << 5 | localZ;
|
||||
return tickProcedure(world, c, blockX, localY, blockZ, blockId);
|
||||
});
|
||||
if (ticked > 0) {
|
||||
ChunkBlockTickSystem.LOGGER.at(Level.FINER).log("Ticked %d blocks in chunk (%d, %d)", ticked, worldChunk.getX(), worldChunk.getZ());
|
||||
}
|
||||
}
|
||||
|
||||
protected static BlockTickStrategy tickProcedure(@Nonnull World world, @Nonnull WorldChunk chunk, int blockX, int blockY, int blockZ, int blockId) {
|
||||
if (world.getWorldConfig().isBlockTicking() && BlockTickManager.hasBlockTickProvider()) {
|
||||
TickProcedure procedure = BlockTickPlugin.get().getTickProcedure(blockId);
|
||||
if (procedure == null) {
|
||||
return BlockTickStrategy.IGNORED;
|
||||
} else {
|
||||
try {
|
||||
return procedure.onTick(world, chunk, blockX, blockY, blockZ, blockId);
|
||||
} catch (Throwable var9) {
|
||||
BlockType blockType = BlockType.getAssetMap().getAsset(blockId);
|
||||
ChunkBlockTickSystem.LOGGER
|
||||
.at(Level.WARNING)
|
||||
.withCause(var9)
|
||||
.log("Failed to tick block at (%d, %d, %d) ID %s in world %s:", blockX, blockY, blockZ, blockType.getId(), world.getName());
|
||||
return BlockTickStrategy.SLEEP;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return BlockTickStrategy.IGNORED;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,982 +0,0 @@
|
||||
package com.hypixel.hytale.builtin.crafting.component;
|
||||
|
||||
import com.google.gson.JsonArray;
|
||||
import com.hypixel.hytale.builtin.adventure.memories.MemoriesPlugin;
|
||||
import com.hypixel.hytale.builtin.crafting.CraftingPlugin;
|
||||
import com.hypixel.hytale.builtin.crafting.state.BenchState;
|
||||
import com.hypixel.hytale.builtin.crafting.window.BenchWindow;
|
||||
import com.hypixel.hytale.builtin.crafting.window.CraftingWindow;
|
||||
import com.hypixel.hytale.component.Component;
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.spatial.SpatialResource;
|
||||
import com.hypixel.hytale.event.IEventDispatcher;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.shape.Box;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.hypixel.hytale.protocol.BenchRequirement;
|
||||
import com.hypixel.hytale.protocol.BenchType;
|
||||
import com.hypixel.hytale.protocol.GameMode;
|
||||
import com.hypixel.hytale.protocol.ItemQuantity;
|
||||
import com.hypixel.hytale.protocol.ItemResourceType;
|
||||
import com.hypixel.hytale.protocol.SoundCategory;
|
||||
import com.hypixel.hytale.protocol.packets.interface_.NotificationStyle;
|
||||
import com.hypixel.hytale.server.core.HytaleServer;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.Bench;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.BenchTierLevel;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.BenchUpgradeRequirement;
|
||||
import com.hypixel.hytale.server.core.asset.type.item.config.CraftingRecipe;
|
||||
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerConfigData;
|
||||
import com.hypixel.hytale.server.core.entity.entities.player.windows.MaterialExtraResourcesSection;
|
||||
import com.hypixel.hytale.server.core.event.events.ecs.CraftRecipeEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerCraftEvent;
|
||||
import com.hypixel.hytale.server.core.inventory.Inventory;
|
||||
import com.hypixel.hytale.server.core.inventory.ItemStack;
|
||||
import com.hypixel.hytale.server.core.inventory.MaterialQuantity;
|
||||
import com.hypixel.hytale.server.core.inventory.container.CombinedItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.DelegateItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.EmptyItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.filter.FilterType;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.ListTransaction;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.MaterialSlotTransaction;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.MaterialTransaction;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.BlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.BlockStateModule;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.state.ItemContainerState;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.util.NotificationUtil;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
import org.bson.BsonDocument;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class CraftingManager implements Component<EntityStore> {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
@Nonnull
|
||||
private final BlockingQueue<CraftingManager.CraftingJob> queuedCraftingJobs = new LinkedBlockingQueue<>();
|
||||
@Nullable
|
||||
private CraftingManager.BenchUpgradingJob upgradingJob;
|
||||
private int x;
|
||||
private int y;
|
||||
private int z;
|
||||
@Nullable
|
||||
private BlockType blockType;
|
||||
|
||||
@Nonnull
|
||||
public static ComponentType<EntityStore, CraftingManager> getComponentType() {
|
||||
return CraftingPlugin.get().getCraftingManagerComponentType();
|
||||
}
|
||||
|
||||
public CraftingManager() {
|
||||
}
|
||||
|
||||
private CraftingManager(@Nonnull CraftingManager other) {
|
||||
this.x = other.x;
|
||||
this.y = other.y;
|
||||
this.z = other.z;
|
||||
this.blockType = other.blockType;
|
||||
this.queuedCraftingJobs.addAll(other.queuedCraftingJobs);
|
||||
this.upgradingJob = other.upgradingJob;
|
||||
}
|
||||
|
||||
public boolean hasBenchSet() {
|
||||
return this.blockType != null;
|
||||
}
|
||||
|
||||
public void setBench(int x, int y, int z, @Nonnull BlockType blockType) {
|
||||
Bench bench = blockType.getBench();
|
||||
Objects.requireNonNull(bench, "blockType isn't a bench!");
|
||||
if (bench.getType() != BenchType.Crafting
|
||||
&& bench.getType() != BenchType.DiagramCrafting
|
||||
&& bench.getType() != BenchType.StructuralCrafting
|
||||
&& bench.getType() != BenchType.Processing) {
|
||||
throw new IllegalArgumentException("blockType isn't a crafting bench!");
|
||||
}
|
||||
// HyFix: Auto-clear stale bench reference instead of throwing exception
|
||||
// This prevents "Bench blockType is already set!" crashes when player rapidly
|
||||
// opens multiple benches or previous interaction didn't properly clean up
|
||||
else if (this.blockType != null) {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.z = 0;
|
||||
this.blockType = null;
|
||||
this.queuedCraftingJobs.clear();
|
||||
this.upgradingJob = null;
|
||||
}
|
||||
if (!this.queuedCraftingJobs.isEmpty()) {
|
||||
throw new IllegalArgumentException("Queue already has jobs!");
|
||||
} else if (this.upgradingJob != null) {
|
||||
throw new IllegalArgumentException("Upgrading job is already set!");
|
||||
} else {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.blockType = blockType;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean clearBench(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
boolean result = this.cancelAllCrafting(ref, componentAccessor);
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.z = 0;
|
||||
this.blockType = null;
|
||||
this.upgradingJob = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean craftItem(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull CraftingRecipe recipe,
|
||||
int quantity,
|
||||
@Nonnull ItemContainer itemContainer
|
||||
) {
|
||||
if (this.upgradingJob != null) {
|
||||
return false;
|
||||
} else {
|
||||
Objects.requireNonNull(recipe, "Recipe can't be null");
|
||||
CraftRecipeEvent.Pre preEvent = new CraftRecipeEvent.Pre(recipe, quantity);
|
||||
componentAccessor.invoke(ref, preEvent);
|
||||
if (preEvent.isCancelled()) {
|
||||
return false;
|
||||
} else if (!this.isValidBenchForRecipe(ref, componentAccessor, recipe)) {
|
||||
return false;
|
||||
} else {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
if (playerComponent.getGameMode() != GameMode.Creative && !removeInputFromInventory(itemContainer, recipe, quantity)) {
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
String translationKey = getRecipeOutputTranslationKey(recipe);
|
||||
if (translationKey != null) {
|
||||
NotificationUtil.sendNotification(
|
||||
playerRefComponent.getPacketHandler(),
|
||||
Message.translation("server.general.crafting.missingIngredient").param("item", Message.translation(translationKey)),
|
||||
NotificationStyle.Danger
|
||||
);
|
||||
}
|
||||
|
||||
LOGGER.at(Level.FINE).log("Missing items required to craft the item: %s", recipe);
|
||||
return false;
|
||||
} else {
|
||||
CraftRecipeEvent.Post postEvent = new CraftRecipeEvent.Post(recipe, quantity);
|
||||
componentAccessor.invoke(ref, postEvent);
|
||||
if (postEvent.isCancelled()) {
|
||||
return true;
|
||||
} else {
|
||||
giveOutput(ref, componentAccessor, recipe, quantity);
|
||||
IEventDispatcher<PlayerCraftEvent, PlayerCraftEvent> dispatcher = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(PlayerCraftEvent.class, world.getName());
|
||||
if (dispatcher.hasListener()) {
|
||||
dispatcher.dispatch(new PlayerCraftEvent(ref, playerComponent, recipe, quantity));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getRecipeOutputTranslationKey(@Nonnull CraftingRecipe recipe) {
|
||||
String itemId = recipe.getPrimaryOutput().getItemId();
|
||||
if (itemId == null) {
|
||||
return null;
|
||||
} else {
|
||||
Item itemAsset = Item.getAssetMap().getAsset(itemId);
|
||||
return itemAsset != null ? itemAsset.getTranslationKey() : null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean queueCraft(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull CraftingWindow window,
|
||||
int transactionId,
|
||||
@Nonnull CraftingRecipe recipe,
|
||||
int quantity,
|
||||
@Nonnull ItemContainer inputItemContainer,
|
||||
@Nonnull CraftingManager.InputRemovalType inputRemovalType
|
||||
) {
|
||||
if (this.upgradingJob != null) {
|
||||
return false;
|
||||
} else {
|
||||
Objects.requireNonNull(recipe, "Recipe can't be null");
|
||||
if (!this.isValidBenchForRecipe(ref, componentAccessor, recipe)) {
|
||||
return false;
|
||||
} else {
|
||||
float recipeTime = recipe.getTimeSeconds();
|
||||
if (recipeTime > 0.0F) {
|
||||
int level = this.getBenchTierLevel(componentAccessor);
|
||||
if (level > 1) {
|
||||
BenchTierLevel tierLevelData = this.getBenchTierLevelData(level);
|
||||
if (tierLevelData != null) {
|
||||
recipeTime -= recipeTime * tierLevelData.getCraftingTimeReductionModifier();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.queuedCraftingJobs
|
||||
.offer(new CraftingManager.CraftingJob(window, transactionId, recipe, quantity, recipeTime, inputItemContainer, inputRemovalType));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tick(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, float dt) {
|
||||
if (this.upgradingJob != null) {
|
||||
if (dt > 0.0F) {
|
||||
this.upgradingJob.timeSecondsCompleted += dt;
|
||||
}
|
||||
|
||||
this.upgradingJob.window.updateBenchUpgradeJob(this.upgradingJob.computeLoadingPercent());
|
||||
if (this.upgradingJob.timeSecondsCompleted >= this.upgradingJob.timeSeconds) {
|
||||
this.upgradingJob.window.updateBenchTierLevel(this.finishTierUpgrade(ref, componentAccessor));
|
||||
this.upgradingJob = null;
|
||||
}
|
||||
} else {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
while (dt > 0.0F && !this.queuedCraftingJobs.isEmpty()) {
|
||||
CraftingManager.CraftingJob currentJob = this.queuedCraftingJobs.peek();
|
||||
boolean isCreativeMode = playerComponent.getGameMode() == GameMode.Creative;
|
||||
if (currentJob != null && currentJob.quantityStarted < currentJob.quantity && currentJob.quantityStarted <= currentJob.quantityCompleted) {
|
||||
LOGGER.at(Level.FINE).log("Removing Items for next quantity: %s", currentJob);
|
||||
int currentItemId = currentJob.quantityStarted++;
|
||||
if (!isCreativeMode && !removeInputFromInventory(currentJob, currentItemId)) {
|
||||
String translationKey = getRecipeOutputTranslationKey(currentJob.recipe);
|
||||
if (translationKey != null) {
|
||||
NotificationUtil.sendNotification(
|
||||
playerRefComponent.getPacketHandler(),
|
||||
Message.translation("server.general.crafting.missingIngredient").param("item", Message.translation(translationKey)),
|
||||
NotificationStyle.Danger
|
||||
);
|
||||
}
|
||||
|
||||
LOGGER.at(Level.FINE).log("Missing items required to craft the item: %s", currentJob);
|
||||
currentJob = null;
|
||||
this.queuedCraftingJobs.poll();
|
||||
}
|
||||
|
||||
if (!isCreativeMode
|
||||
&& currentJob != null
|
||||
&& currentJob.quantityStarted < currentJob.quantity
|
||||
&& currentJob.quantityStarted <= currentJob.quantityCompleted) {
|
||||
NotificationUtil.sendNotification(
|
||||
playerRefComponent.getPacketHandler(),
|
||||
Message.translation("server.general.crafting.failedTakingCorrectQuantity"),
|
||||
NotificationStyle.Danger
|
||||
);
|
||||
LOGGER.at(Level.SEVERE).log("Failed to remove the correct quantity of input, removing crafting job %s", currentJob);
|
||||
currentJob = null;
|
||||
this.queuedCraftingJobs.poll();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentJob != null) {
|
||||
currentJob.timeSecondsCompleted += dt;
|
||||
float percent = currentJob.timeSeconds <= 0.0F ? 1.0F : currentJob.timeSecondsCompleted / currentJob.timeSeconds;
|
||||
if (percent > 1.0F) {
|
||||
percent = 1.0F;
|
||||
}
|
||||
|
||||
currentJob.window.updateCraftingJob(percent);
|
||||
LOGGER.at(Level.FINEST).log("Update time: %s", currentJob);
|
||||
dt = 0.0F;
|
||||
if (currentJob.timeSecondsCompleted >= currentJob.timeSeconds) {
|
||||
dt = currentJob.timeSecondsCompleted - currentJob.timeSeconds;
|
||||
int currentCompletedItemId = currentJob.quantityCompleted++;
|
||||
currentJob.timeSecondsCompleted = 0.0F;
|
||||
LOGGER.at(Level.FINE).log("Crafted 1 Quantity: %s", currentJob);
|
||||
if (currentJob.quantityCompleted == currentJob.quantity) {
|
||||
giveOutput(ref, componentAccessor, currentJob, currentCompletedItemId);
|
||||
LOGGER.at(Level.FINE).log("Crafting Finished: %s", currentJob);
|
||||
this.queuedCraftingJobs.poll();
|
||||
} else {
|
||||
if (currentJob.quantityCompleted > currentJob.quantity) {
|
||||
this.queuedCraftingJobs.poll();
|
||||
throw new RuntimeException("QuantityCompleted is greater than the Quality! " + currentJob);
|
||||
}
|
||||
|
||||
giveOutput(ref, componentAccessor, currentJob, currentCompletedItemId);
|
||||
}
|
||||
|
||||
if (this.queuedCraftingJobs.isEmpty()) {
|
||||
currentJob.window.setBlockInteractionState("default", componentAccessor.getExternalData().getWorld());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean cancelAllCrafting(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
LOGGER.at(Level.FINE).log("Cancel Crafting!");
|
||||
ObjectList<CraftingManager.CraftingJob> oldJobs = new ObjectArrayList<>(this.queuedCraftingJobs.size());
|
||||
this.queuedCraftingJobs.drainTo(oldJobs);
|
||||
if (!oldJobs.isEmpty()) {
|
||||
CraftingManager.CraftingJob currentJob = oldJobs.getFirst();
|
||||
LOGGER.at(Level.FINE).log("Refunding Items for: %s", currentJob);
|
||||
refundInputToInventory(ref, componentAccessor, currentJob, currentJob.quantityStarted - 1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidBenchForRecipe(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingRecipe recipe
|
||||
) {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerConfigData playerConfigData = playerComponent.getPlayerConfigData();
|
||||
String primaryOutputItemId = recipe.getPrimaryOutput() != null ? recipe.getPrimaryOutput().getItemId() : null;
|
||||
if (!recipe.isKnowledgeRequired() || primaryOutputItemId != null && playerConfigData.getKnownRecipes().contains(primaryOutputItemId)) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
if (recipe.getRequiredMemoriesLevel() > 1 && MemoriesPlugin.get().getMemoriesLevel(world.getGameplayConfig()) < recipe.getRequiredMemoriesLevel()) {
|
||||
LOGGER.at(Level.WARNING).log("Attempted to craft %s but doesn't have the required world memories level!", recipe.getId());
|
||||
return false;
|
||||
} else {
|
||||
BenchType benchType = this.blockType != null ? this.blockType.getBench().getType() : BenchType.Crafting;
|
||||
String benchName = this.blockType != null ? this.blockType.getBench().getId() : "Fieldcraft";
|
||||
boolean meetsRequirements = false;
|
||||
BlockState state = world.getState(this.x, this.y, this.z, true);
|
||||
int benchTierLevel = state instanceof BenchState ? ((BenchState) state).getTierLevel() : 0;
|
||||
BenchRequirement[] requirements = recipe.getBenchRequirement();
|
||||
if (requirements != null) {
|
||||
for (BenchRequirement benchRequirement : requirements) {
|
||||
if (benchRequirement.type == benchType && benchName.equals(benchRequirement.id) && benchRequirement.requiredTierLevel <= benchTierLevel) {
|
||||
meetsRequirements = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!meetsRequirements) {
|
||||
LOGGER.at(Level.WARNING)
|
||||
.log("Attempted to craft %s using %s, %s but requires bench %s but a bench is NOT set!", recipe.getId(), benchType, benchName, requirements);
|
||||
return false;
|
||||
} else if (benchType == BenchType.Crafting && !"Fieldcraft".equals(benchName)) {
|
||||
CraftingManager.CraftingJob craftingJob = this.queuedCraftingJobs.peek();
|
||||
return craftingJob == null || craftingJob.recipe.getId().equals(recipe.getId());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOGGER.at(Level.WARNING).log("%s - Attempted to craft %s but doesn't know the recipe!", recipe.getId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void giveOutput(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingManager.CraftingJob job, int currentItemId
|
||||
) {
|
||||
job.removedItems.remove(currentItemId);
|
||||
String recipeId = job.recipe.getId();
|
||||
CraftingRecipe recipeAsset = CraftingRecipe.getAssetMap().getAsset(recipeId);
|
||||
if (recipeAsset == null) {
|
||||
throw new RuntimeException("A non-existent item ID was provided! " + recipeId);
|
||||
} else {
|
||||
giveOutput(ref, componentAccessor, recipeAsset, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void giveOutput(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingRecipe craftingRecipe, int quantity
|
||||
) {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
if (playerComponent == null) {
|
||||
LOGGER.at(Level.WARNING).log("Attempted to give output to a non-player entity: %s", ref);
|
||||
} else {
|
||||
List<ItemStack> itemStacks = getOutputItemStacks(craftingRecipe, quantity);
|
||||
Inventory inventory = playerComponent.getInventory();
|
||||
SimpleItemContainer.addOrDropItemStacks(componentAccessor, ref, inventory.getCombinedArmorHotbarStorage(), itemStacks);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean removeInputFromInventory(@Nonnull CraftingManager.CraftingJob job, int currentItemId) {
|
||||
Objects.requireNonNull(job, "Job can't be null!");
|
||||
CraftingRecipe craftingRecipe = job.recipe;
|
||||
Objects.requireNonNull(craftingRecipe, "CraftingRecipe can't be null!");
|
||||
List<MaterialQuantity> materialsToRemove = getInputMaterials(craftingRecipe);
|
||||
if (materialsToRemove.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", job, materialsToRemove);
|
||||
ObjectList<ItemStack> itemStackList = new ObjectArrayList<>();
|
||||
|
||||
boolean succeeded = switch (job.inputRemovalType) {
|
||||
case NORMAL -> {
|
||||
ListTransaction<MaterialTransaction> materialTransactions = job.inputItemContainer.removeMaterials(materialsToRemove, true, true, true);
|
||||
|
||||
for (MaterialTransaction transaction : materialTransactions.getList()) {
|
||||
for (MaterialSlotTransaction slotTransaction : transaction.getList()) {
|
||||
if (!ItemStack.isEmpty(slotTransaction.getOutput())) {
|
||||
itemStackList.add(slotTransaction.getOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield materialTransactions.succeeded();
|
||||
}
|
||||
case ORDERED -> {
|
||||
ListTransaction<MaterialSlotTransaction> materialTransactions = job.inputItemContainer
|
||||
.removeMaterialsOrdered(materialsToRemove, true, true, true);
|
||||
|
||||
for (MaterialSlotTransaction transaction : materialTransactions.getList()) {
|
||||
if (!ItemStack.isEmpty(transaction.getOutput())) {
|
||||
itemStackList.add(transaction.getOutput());
|
||||
}
|
||||
}
|
||||
|
||||
yield materialTransactions.succeeded();
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unknown enum: " + job.inputRemovalType);
|
||||
};
|
||||
job.removedItems.put(currentItemId, itemStackList);
|
||||
job.window.invalidateExtraResources();
|
||||
return succeeded;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean removeInputFromInventory(@Nonnull ItemContainer itemContainer, @Nonnull CraftingRecipe craftingRecipe, int quantity) {
|
||||
List<MaterialQuantity> materialsToRemove = getInputMaterials(craftingRecipe, quantity);
|
||||
if (materialsToRemove.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", craftingRecipe, materialsToRemove);
|
||||
ListTransaction<MaterialTransaction> materialTransactions = itemContainer.removeMaterials(materialsToRemove, true, true, true);
|
||||
return materialTransactions.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
private static void refundInputToInventory(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingManager.CraftingJob job, int currentItemId
|
||||
) {
|
||||
Objects.requireNonNull(job, "Job can't be null!");
|
||||
List<ItemStack> itemStacks = job.removedItems.get(currentItemId);
|
||||
if (itemStacks != null) {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
SimpleItemContainer.addOrDropItemStacks(componentAccessor, ref, playerComponent.getInventory().getCombinedHotbarFirst(), itemStacks);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<ItemStack> getOutputItemStacks(@Nonnull CraftingRecipe recipe) {
|
||||
return getOutputItemStacks(recipe, 1);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<ItemStack> getOutputItemStacks(@Nonnull CraftingRecipe recipe, int quantity) {
|
||||
Objects.requireNonNull(recipe);
|
||||
MaterialQuantity[] output = recipe.getOutputs();
|
||||
if (output == null) {
|
||||
return List.of();
|
||||
} else {
|
||||
ObjectList<ItemStack> outputItemStacks = new ObjectArrayList<>();
|
||||
|
||||
for (MaterialQuantity outputMaterial : output) {
|
||||
ItemStack outputItemStack = getOutputItemStack(outputMaterial, quantity);
|
||||
if (outputItemStack != null) {
|
||||
outputItemStacks.add(outputItemStack);
|
||||
}
|
||||
}
|
||||
|
||||
return outputItemStacks;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ItemStack getOutputItemStack(@Nonnull MaterialQuantity outputMaterial, @Nonnull String id) {
|
||||
return getOutputItemStack(outputMaterial, 1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ItemStack getOutputItemStack(@Nonnull MaterialQuantity outputMaterial, int quantity) {
|
||||
String itemId = outputMaterial.getItemId();
|
||||
if (itemId == null) {
|
||||
return null;
|
||||
} else {
|
||||
int materialQuantity = outputMaterial.getQuantity() <= 0 ? 1 : outputMaterial.getQuantity();
|
||||
return new ItemStack(itemId, materialQuantity * quantity, outputMaterial.getMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<MaterialQuantity> getInputMaterials(@Nonnull CraftingRecipe recipe) {
|
||||
return getInputMaterials(recipe, 1);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static List<MaterialQuantity> getInputMaterials(@Nonnull MaterialQuantity[] input) {
|
||||
return getInputMaterials(input, 1);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<MaterialQuantity> getInputMaterials(@Nonnull CraftingRecipe recipe, int quantity) {
|
||||
Objects.requireNonNull(recipe);
|
||||
return recipe.getInput() == null ? Collections.emptyList() : getInputMaterials(recipe.getInput(), quantity);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static List<MaterialQuantity> getInputMaterials(@Nonnull MaterialQuantity[] input, int quantity) {
|
||||
ObjectList<MaterialQuantity> materials = new ObjectArrayList<>();
|
||||
|
||||
for (MaterialQuantity craftingMaterial : input) {
|
||||
String itemId = craftingMaterial.getItemId();
|
||||
String resourceTypeId = craftingMaterial.getResourceTypeId();
|
||||
int materialQuantity = craftingMaterial.getQuantity();
|
||||
BsonDocument metadata = craftingMaterial.getMetadata();
|
||||
materials.add(new MaterialQuantity(itemId, resourceTypeId, null, materialQuantity * quantity, metadata));
|
||||
}
|
||||
|
||||
return materials;
|
||||
}
|
||||
|
||||
public static boolean matches(@Nonnull MaterialQuantity craftingMaterial, @Nonnull ItemStack itemStack) {
|
||||
String itemId = craftingMaterial.getItemId();
|
||||
if (itemId != null) {
|
||||
return itemId.equals(itemStack.getItemId());
|
||||
} else {
|
||||
String resourceTypeId = craftingMaterial.getResourceTypeId();
|
||||
if (resourceTypeId != null && itemStack.getItem().getResourceTypes() != null) {
|
||||
for (ItemResourceType itemResourceType : itemStack.getItem().getResourceTypes()) {
|
||||
if (resourceTypeId.equals(itemResourceType.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static JsonArray generateInventoryHints(@Nonnull List<CraftingRecipe> recipes, int inputSlotIndex, @Nonnull ItemContainer container) {
|
||||
JsonArray inventoryHints = new JsonArray();
|
||||
short storageSlotIndex = 0;
|
||||
|
||||
for (short bound = container.getCapacity(); storageSlotIndex < bound; storageSlotIndex++) {
|
||||
ItemStack itemStack = container.getItemStack(storageSlotIndex);
|
||||
if (itemStack != null && !itemStack.isEmpty() && matchesAnyRecipe(recipes, inputSlotIndex, itemStack)) {
|
||||
inventoryHints.add(storageSlotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return inventoryHints;
|
||||
}
|
||||
|
||||
public static boolean matchesAnyRecipe(@Nonnull List<CraftingRecipe> recipes, int inputSlotIndex, @Nonnull ItemStack slotItemStack) {
|
||||
for (CraftingRecipe recipe : recipes) {
|
||||
MaterialQuantity[] input = recipe.getInput();
|
||||
if (inputSlotIndex < input.length) {
|
||||
MaterialQuantity slotCraftingMaterial = input[inputSlotIndex];
|
||||
if (slotCraftingMaterial.getItemId() != null && slotCraftingMaterial.getItemId().equals(slotItemStack.getItemId())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (slotCraftingMaterial.getResourceTypeId() != null && slotItemStack.getItem().getResourceTypes() != null) {
|
||||
for (ItemResourceType itemResourceType : slotItemStack.getItem().getResourceTypes()) {
|
||||
if (slotCraftingMaterial.getResourceTypeId().equals(itemResourceType.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean startTierUpgrade(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull BenchWindow window) {
|
||||
if (this.upgradingJob != null) {
|
||||
return false;
|
||||
} else {
|
||||
BenchUpgradeRequirement requirements = this.getBenchUpgradeRequirement(this.getBenchTierLevel(componentAccessor));
|
||||
if (requirements == null) {
|
||||
return false;
|
||||
} else {
|
||||
List<MaterialQuantity> input = getInputMaterials(requirements.getInput());
|
||||
if (input.isEmpty()) {
|
||||
return false;
|
||||
} else {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
if (playerComponent.getGameMode() != GameMode.Creative) {
|
||||
CombinedItemContainer combined = new CombinedItemContainer(
|
||||
playerComponent.getInventory().getCombinedBackpackStorageHotbar(), window.getExtraResourcesSection().getItemContainer()
|
||||
);
|
||||
if (!combined.canRemoveMaterials(input)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.upgradingJob = new CraftingManager.BenchUpgradingJob(window, requirements.getTimeSeconds());
|
||||
this.cancelAllCrafting(ref, componentAccessor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int finishTierUpgrade(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (this.upgradingJob == null) {
|
||||
return 0;
|
||||
} else {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
BlockState state = world.getState(this.x, this.y, this.z, true);
|
||||
BenchState benchState = state instanceof BenchState ? (BenchState) state : null;
|
||||
if (benchState != null && benchState.getTierLevel() != 0) {
|
||||
BenchUpgradeRequirement requirements = this.getBenchUpgradeRequirement(benchState.getTierLevel());
|
||||
if (requirements == null) {
|
||||
return benchState.getTierLevel();
|
||||
} else {
|
||||
List<MaterialQuantity> input = getInputMaterials(requirements.getInput());
|
||||
if (input.isEmpty()) {
|
||||
return benchState.getTierLevel();
|
||||
} else {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
boolean canUpgrade = playerComponent.getGameMode() == GameMode.Creative;
|
||||
if (!canUpgrade) {
|
||||
CombinedItemContainer combined = new CombinedItemContainer(
|
||||
playerComponent.getInventory().getCombinedBackpackStorageHotbar(),
|
||||
this.upgradingJob.window.getExtraResourcesSection().getItemContainer()
|
||||
);
|
||||
combined = new CombinedItemContainer(combined, this.upgradingJob.window.getExtraResourcesSection().getItemContainer());
|
||||
ListTransaction<MaterialTransaction> materialTransactions = combined.removeMaterials(input);
|
||||
if (materialTransactions.succeeded()) {
|
||||
List<ItemStack> consumed = new ObjectArrayList<>();
|
||||
|
||||
for (MaterialTransaction transaction : materialTransactions.getList()) {
|
||||
for (MaterialSlotTransaction matSlot : transaction.getList()) {
|
||||
consumed.add(matSlot.getOutput());
|
||||
}
|
||||
}
|
||||
|
||||
benchState.addUpgradeItems(consumed);
|
||||
canUpgrade = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (canUpgrade) {
|
||||
benchState.setTierLevel(benchState.getTierLevel() + 1);
|
||||
if (benchState.getBench().getBenchUpgradeCompletedSoundEventIndex() != 0) {
|
||||
SoundUtil.playSoundEvent3d(
|
||||
benchState.getBench().getBenchUpgradeCompletedSoundEventIndex(),
|
||||
SoundCategory.SFX,
|
||||
this.x + 0.5,
|
||||
this.y + 0.5,
|
||||
this.z + 0.5,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return benchState.getTierLevel();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BenchTierLevel getBenchTierLevelData(int level) {
|
||||
if (this.blockType == null) {
|
||||
return null;
|
||||
} else {
|
||||
Bench bench = this.blockType.getBench();
|
||||
return bench == null ? null : bench.getTierLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BenchUpgradeRequirement getBenchUpgradeRequirement(int tierLevel) {
|
||||
BenchTierLevel tierData = this.getBenchTierLevelData(tierLevel);
|
||||
return tierData == null ? null : tierData.getUpgradeRequirement();
|
||||
}
|
||||
|
||||
private int getBenchTierLevel(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
BlockState state = world.getState(this.x, this.y, this.z, true);
|
||||
return state instanceof BenchState ? ((BenchState) state).getTierLevel() : 0;
|
||||
}
|
||||
|
||||
public static int feedExtraResourcesSection(@Nonnull BenchState benchState, @Nonnull MaterialExtraResourcesSection extraResourcesSection) {
|
||||
try {
|
||||
CraftingManager.ChestLookupResult result = getContainersAroundBench(benchState);
|
||||
List<ItemContainer> chests = result.containers;
|
||||
List<ItemContainerState> chestStates = result.states;
|
||||
ItemContainer itemContainer = EmptyItemContainer.INSTANCE;
|
||||
if (!chests.isEmpty()) {
|
||||
itemContainer = new CombinedItemContainer(chests.stream().map(container -> {
|
||||
DelegateItemContainer<ItemContainer> delegate = new DelegateItemContainer<>(container);
|
||||
delegate.setGlobalFilter(FilterType.ALLOW_OUTPUT_ONLY);
|
||||
return delegate;
|
||||
}).toArray(ItemContainer[]::new));
|
||||
}
|
||||
|
||||
Map<String, ItemQuantity> materials = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
for (ItemContainer chest : chests) {
|
||||
chest.forEach((i, itemStack) -> {
|
||||
if (CraftingPlugin.isValidUpgradeMaterialForBench(benchState, itemStack) || CraftingPlugin.isValidCraftingMaterialForBench(benchState, itemStack)) {
|
||||
ItemQuantity var10000 = materials.computeIfAbsent(itemStack.getItemId(), k -> new ItemQuantity(itemStack.getItemId(), 0));
|
||||
var10000.quantity = var10000.quantity + itemStack.getQuantity();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
extraResourcesSection.setItemContainer(itemContainer);
|
||||
extraResourcesSection.setExtraMaterials(materials.values().toArray(new ItemQuantity[0]));
|
||||
extraResourcesSection.setValid(true);
|
||||
return chestStates.size();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected static CraftingManager.ChestLookupResult getContainersAroundBench(@Nonnull BenchState benchState) {
|
||||
List<ItemContainer> containers = new ObjectArrayList<>();
|
||||
List<ItemContainerState> states = new ObjectArrayList<>();
|
||||
List<ItemContainerState> spatialResults = new ObjectArrayList<>();
|
||||
List<ItemContainerState> filteredOut = new ObjectArrayList<>();
|
||||
World world = benchState.getChunk().getWorld();
|
||||
Store<ChunkStore> store = world.getChunkStore().getStore();
|
||||
int limit = world.getGameplayConfig().getCraftingConfig().getBenchMaterialChestLimit();
|
||||
double horizontalRadius = world.getGameplayConfig().getCraftingConfig().getBenchMaterialHorizontalChestSearchRadius();
|
||||
double verticalRadius = world.getGameplayConfig().getCraftingConfig().getBenchMaterialVerticalChestSearchRadius();
|
||||
Vector3d blockPos = benchState.getBlockPosition().toVector3d();
|
||||
BlockBoundingBoxes hitboxAsset = BlockBoundingBoxes.getAssetMap().getAsset(benchState.getBlockType().getHitboxTypeIndex());
|
||||
BlockBoundingBoxes.RotatedVariantBoxes rotatedHitbox = hitboxAsset.get(benchState.getRotationIndex());
|
||||
Box boundingBox = rotatedHitbox.getBoundingBox();
|
||||
double benchWidth = boundingBox.width();
|
||||
double benchHeight = boundingBox.height();
|
||||
double benchDepth = boundingBox.depth();
|
||||
double extraSearchRadius = Math.max(benchWidth, Math.max(benchDepth, benchHeight)) - 1.0;
|
||||
SpatialResource<Ref<ChunkStore>, ChunkStore> blockStateSpatialStructure = store.getResource(BlockStateModule.get().getItemContainerSpatialResourceType());
|
||||
ObjectList<Ref<ChunkStore>> results = SpatialResource.getThreadLocalReferenceList();
|
||||
blockStateSpatialStructure.getSpatialStructure()
|
||||
.ordered3DAxis(blockPos, horizontalRadius + extraSearchRadius, verticalRadius + extraSearchRadius, horizontalRadius + extraSearchRadius, results);
|
||||
if (!results.isEmpty()) {
|
||||
int benchMinBlockX = (int) Math.floor(boundingBox.min.x);
|
||||
int benchMinBlockY = (int) Math.floor(boundingBox.min.y);
|
||||
int benchMinBlockZ = (int) Math.floor(boundingBox.min.z);
|
||||
int benchMaxBlockX = (int) Math.ceil(boundingBox.max.x) - 1;
|
||||
int benchMaxBlockY = (int) Math.ceil(boundingBox.max.y) - 1;
|
||||
int benchMaxBlockZ = (int) Math.ceil(boundingBox.max.z) - 1;
|
||||
double minX = blockPos.x + benchMinBlockX - horizontalRadius;
|
||||
double minY = blockPos.y + benchMinBlockY - verticalRadius;
|
||||
double minZ = blockPos.z + benchMinBlockZ - horizontalRadius;
|
||||
double maxX = blockPos.x + benchMaxBlockX + horizontalRadius;
|
||||
double maxY = blockPos.y + benchMaxBlockY + verticalRadius;
|
||||
double maxZ = blockPos.z + benchMaxBlockZ + horizontalRadius;
|
||||
|
||||
for (Ref<ChunkStore> ref : results) {
|
||||
if (BlockState.getBlockState(ref, ref.getStore()) instanceof ItemContainerState chest) {
|
||||
spatialResults.add(chest);
|
||||
}
|
||||
}
|
||||
|
||||
for (ItemContainerState chest : spatialResults) {
|
||||
Vector3d chestBlockPos = chest.getBlockPosition().toVector3d();
|
||||
if (chestBlockPos.x >= minX
|
||||
&& chestBlockPos.x <= maxX
|
||||
&& chestBlockPos.y >= minY
|
||||
&& chestBlockPos.y <= maxY
|
||||
&& chestBlockPos.z >= minZ
|
||||
&& chestBlockPos.z <= maxZ) {
|
||||
containers.add(chest.getItemContainer());
|
||||
states.add(chest);
|
||||
if (containers.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
filteredOut.add(chest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CraftingManager.ChestLookupResult(containers, states, spatialResults, filteredOut, blockPos);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CraftingManager{queuedCraftingJobs="
|
||||
+ this.queuedCraftingJobs
|
||||
+ ", x="
|
||||
+ this.x
|
||||
+ ", y="
|
||||
+ this.y
|
||||
+ ", z="
|
||||
+ this.z
|
||||
+ ", blockType="
|
||||
+ this.blockType
|
||||
+ "}";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<EntityStore> clone() {
|
||||
return new CraftingManager(this);
|
||||
}
|
||||
|
||||
private static class BenchUpgradingJob {
|
||||
@Nonnull
|
||||
private final BenchWindow window;
|
||||
private final float timeSeconds;
|
||||
private float timeSecondsCompleted;
|
||||
private float lastSentPercent;
|
||||
|
||||
private BenchUpgradingJob(@Nonnull BenchWindow window, float timeSeconds) {
|
||||
this.window = window;
|
||||
this.timeSeconds = timeSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BenchUpgradingJob{window=" + this.window + ", timeSeconds=" + this.timeSeconds + "}";
|
||||
}
|
||||
|
||||
public float computeLoadingPercent() {
|
||||
return this.timeSeconds <= 0.0F ? 1.0F : Math.min(this.timeSecondsCompleted / this.timeSeconds, 1.0F);
|
||||
}
|
||||
}
|
||||
|
||||
protected record ChestLookupResult(
|
||||
List<ItemContainer> containers,
|
||||
List<ItemContainerState> states,
|
||||
List<ItemContainerState> spatialResults,
|
||||
List<ItemContainerState> filteredOut,
|
||||
Vector3d benchCenteredPos
|
||||
) {
|
||||
}
|
||||
|
||||
private static class CraftingJob {
|
||||
@Nonnull
|
||||
private final CraftingWindow window;
|
||||
private final int transactionId;
|
||||
@Nonnull
|
||||
private final CraftingRecipe recipe;
|
||||
private final int quantity;
|
||||
private final float timeSeconds;
|
||||
@Nonnull
|
||||
private final ItemContainer inputItemContainer;
|
||||
@Nonnull
|
||||
private final CraftingManager.InputRemovalType inputRemovalType;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<List<ItemStack>> removedItems = new Int2ObjectOpenHashMap<>();
|
||||
private int quantityStarted;
|
||||
private int quantityCompleted;
|
||||
private float timeSecondsCompleted;
|
||||
|
||||
public CraftingJob(
|
||||
@Nonnull CraftingWindow window,
|
||||
int transactionId,
|
||||
@Nonnull CraftingRecipe recipe,
|
||||
int quantity,
|
||||
float timeSeconds,
|
||||
@Nonnull ItemContainer inputItemContainer,
|
||||
@Nonnull CraftingManager.InputRemovalType inputRemovalType
|
||||
) {
|
||||
this.window = window;
|
||||
this.transactionId = transactionId;
|
||||
this.recipe = recipe;
|
||||
this.quantity = quantity;
|
||||
this.timeSeconds = timeSeconds;
|
||||
this.inputItemContainer = inputItemContainer;
|
||||
this.inputRemovalType = inputRemovalType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CraftingJob{window="
|
||||
+ this.window
|
||||
+ ", transactionId="
|
||||
+ this.transactionId
|
||||
+ ", recipe="
|
||||
+ this.recipe
|
||||
+ ", quantity="
|
||||
+ this.quantity
|
||||
+ ", timeSeconds="
|
||||
+ this.timeSeconds
|
||||
+ ", inputItemContainer="
|
||||
+ this.inputItemContainer
|
||||
+ ", inputRemovalType="
|
||||
+ this.inputRemovalType
|
||||
+ ", removedItems="
|
||||
+ this.removedItems
|
||||
+ ", quantityStarted="
|
||||
+ this.quantityStarted
|
||||
+ ", quantityCompleted="
|
||||
+ this.quantityCompleted
|
||||
+ ", timeSecondsCompleted="
|
||||
+ this.timeSecondsCompleted
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
||||
public static enum InputRemovalType {
|
||||
NORMAL,
|
||||
ORDERED;
|
||||
|
||||
private InputRemovalType() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,840 +0,0 @@
|
||||
package com.hypixel.hytale.builtin.crafting.state;
|
||||
|
||||
import com.google.common.flogger.LazyArgs;
|
||||
import com.hypixel.hytale.builtin.crafting.CraftingPlugin;
|
||||
import com.hypixel.hytale.builtin.crafting.component.CraftingManager;
|
||||
import com.hypixel.hytale.builtin.crafting.window.ProcessingBenchWindow;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.KeyedCodec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.event.EventPriority;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.util.MathUtil;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.hypixel.hytale.math.vector.Vector3f;
|
||||
import com.hypixel.hytale.math.vector.Vector3i;
|
||||
import com.hypixel.hytale.protocol.SoundCategory;
|
||||
import com.hypixel.hytale.protocol.Transform;
|
||||
import com.hypixel.hytale.protocol.packets.worldmap.MapMarker;
|
||||
import com.hypixel.hytale.server.core.asset.type.blockhitbox.BlockBoundingBoxes;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.BenchTierLevel;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.bench.ProcessingBench;
|
||||
import com.hypixel.hytale.server.core.asset.type.item.config.CraftingRecipe;
|
||||
import com.hypixel.hytale.server.core.asset.type.item.config.Item;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.entity.entities.player.windows.WindowManager;
|
||||
import com.hypixel.hytale.server.core.inventory.ItemStack;
|
||||
import com.hypixel.hytale.server.core.inventory.MaterialQuantity;
|
||||
import com.hypixel.hytale.server.core.inventory.ResourceQuantity;
|
||||
import com.hypixel.hytale.server.core.inventory.container.CombinedItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.InternalContainerUtilMaterial;
|
||||
import com.hypixel.hytale.server.core.inventory.container.ItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.SimpleItemContainer;
|
||||
import com.hypixel.hytale.server.core.inventory.container.TestRemoveItemSlotResult;
|
||||
import com.hypixel.hytale.server.core.inventory.container.filter.FilterActionType;
|
||||
import com.hypixel.hytale.server.core.inventory.container.filter.FilterType;
|
||||
import com.hypixel.hytale.server.core.inventory.container.filter.ResourceFilter;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.ItemStackTransaction;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.ListTransaction;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.MaterialSlotTransaction;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.MaterialTransaction;
|
||||
import com.hypixel.hytale.server.core.inventory.transaction.ResourceTransaction;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.item.ItemComponent;
|
||||
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.accessor.BlockAccessor;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.state.TickableBlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.BlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.state.DestroyableBlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.state.ItemContainerBlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.state.MarkerBlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.state.PlacedByBlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapManager;
|
||||
import com.hypixel.hytale.server.core.util.PositionUtil;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import org.bson.BsonDocument;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ProcessingBenchState
|
||||
extends BenchState
|
||||
implements TickableBlockState,
|
||||
ItemContainerBlockState,
|
||||
DestroyableBlockState,
|
||||
MarkerBlockState,
|
||||
PlacedByBlockState {
|
||||
public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final boolean EXACT_RESOURCE_AMOUNTS = true;
|
||||
public static final Codec<ProcessingBenchState> CODEC = BuilderCodec.builder(ProcessingBenchState.class, ProcessingBenchState::new, BenchState.CODEC)
|
||||
.append(new KeyedCodec<>("InputContainer", ItemContainer.CODEC), (state, o) -> state.inputContainer = o, state -> state.inputContainer)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("FuelContainer", ItemContainer.CODEC), (state, o) -> state.fuelContainer = o, state -> state.fuelContainer)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("OutputContainer", ItemContainer.CODEC), (state, o) -> state.outputContainer = o, state -> state.outputContainer)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("Progress", Codec.DOUBLE), (state, d) -> state.inputProgress = d.floatValue(), state -> (double) state.inputProgress)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("FuelTime", Codec.DOUBLE), (state, d) -> state.fuelTime = d.floatValue(), state -> (double) state.fuelTime)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("Active", Codec.BOOLEAN), (state, b) -> state.active = b, state -> state.active)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("NextExtra", Codec.INTEGER), (state, b) -> state.nextExtra = b, state -> state.nextExtra)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("Marker", WorldMapManager.MarkerReference.CODEC), (state, o) -> state.marker = o, state -> state.marker)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("RecipeId", Codec.STRING), (state, o) -> state.recipeId = o, state -> state.recipeId)
|
||||
.add()
|
||||
.build();
|
||||
private static final float EJECT_VELOCITY = 2.0F;
|
||||
private static final float EJECT_SPREAD_VELOCITY = 1.0F;
|
||||
private static final float EJECT_VERTICAL_VELOCITY = 3.25F;
|
||||
public static final String PROCESSING = "Processing";
|
||||
public static final String PROCESS_COMPLETED = "ProcessCompleted";
|
||||
protected WorldMapManager.MarkerReference marker;
|
||||
private ProcessingBench processingBench;
|
||||
private ItemContainer inputContainer;
|
||||
private ItemContainer fuelContainer;
|
||||
private ItemContainer outputContainer;
|
||||
private CombinedItemContainer combinedItemContainer;
|
||||
private float inputProgress;
|
||||
private float fuelTime;
|
||||
private int lastConsumedFuelTotal;
|
||||
private int nextExtra = -1;
|
||||
private final Set<Short> processingSlots = new HashSet<>();
|
||||
private final Set<Short> processingFuelSlots = new HashSet<>();
|
||||
@Nullable
|
||||
private String recipeId;
|
||||
@Nullable
|
||||
private CraftingRecipe recipe;
|
||||
private boolean active = false;
|
||||
|
||||
public ProcessingBenchState() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initialize(@Nonnull BlockType blockType) {
|
||||
if (!super.initialize(blockType)) {
|
||||
if (this.bench == null) {
|
||||
List<ItemStack> itemStacks = new ObjectArrayList<>();
|
||||
if (this.inputContainer != null) {
|
||||
itemStacks.addAll(this.inputContainer.dropAllItemStacks());
|
||||
}
|
||||
|
||||
if (this.fuelContainer != null) {
|
||||
itemStacks.addAll(this.fuelContainer.dropAllItemStacks());
|
||||
}
|
||||
|
||||
if (this.outputContainer != null) {
|
||||
itemStacks.addAll(this.outputContainer.dropAllItemStacks());
|
||||
}
|
||||
|
||||
World world = this.getChunk().getWorld();
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(store, itemStacks);
|
||||
if (itemEntityHolders.length > 0) {
|
||||
world.execute(() -> store.addEntities(itemEntityHolders, AddReason.SPAWN));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (!(this.bench instanceof ProcessingBench)) {
|
||||
LOGGER.at(Level.SEVERE).log("Wrong bench type for processing. Got %s", this.bench.getClass().getName());
|
||||
return false;
|
||||
} else {
|
||||
this.processingBench = (ProcessingBench) this.bench;
|
||||
if (this.nextExtra == -1) {
|
||||
this.nextExtra = this.processingBench.getExtraOutput() != null ? this.processingBench.getExtraOutput().getPerFuelItemsConsumed() : 0;
|
||||
}
|
||||
|
||||
this.setupSlots();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSlots() {
|
||||
List<ItemStack> remainder = new ObjectArrayList<>();
|
||||
int tierLevel = this.getTierLevel();
|
||||
ProcessingBench.ProcessingSlot[] input = this.processingBench.getInput(tierLevel);
|
||||
short inputSlotsCount = (short) input.length;
|
||||
this.inputContainer = ItemContainer.ensureContainerCapacity(this.inputContainer, inputSlotsCount, SimpleItemContainer::getNewContainer, remainder);
|
||||
this.inputContainer.registerChangeEvent(EventPriority.LAST, this::onItemChange);
|
||||
|
||||
for (short slot = 0; slot < inputSlotsCount; slot++) {
|
||||
ProcessingBench.ProcessingSlot inputSlot = input[slot];
|
||||
String resourceTypeId = inputSlot.getResourceTypeId();
|
||||
boolean shouldFilterValidIngredients = inputSlot.shouldFilterValidIngredients();
|
||||
if (resourceTypeId != null) {
|
||||
this.inputContainer.setSlotFilter(FilterActionType.ADD, slot, new ResourceFilter(new ResourceQuantity(resourceTypeId, 1)));
|
||||
} else if (shouldFilterValidIngredients) {
|
||||
ObjectArrayList<MaterialQuantity> validIngredients = new ObjectArrayList<>();
|
||||
|
||||
for (CraftingRecipe recipe : CraftingPlugin.getBenchRecipes(this.bench.getType(), this.bench.getId())) {
|
||||
if (!recipe.isRestrictedByBenchTierLevel(this.bench.getId(), tierLevel)) {
|
||||
List<MaterialQuantity> inputMaterials = CraftingManager.getInputMaterials(recipe);
|
||||
validIngredients.addAll(inputMaterials);
|
||||
}
|
||||
}
|
||||
|
||||
this.inputContainer.setSlotFilter(FilterActionType.ADD, slot, (actionType, container, slotIndex, itemStack) -> {
|
||||
if (itemStack == null) {
|
||||
return true;
|
||||
} else {
|
||||
for (MaterialQuantity ingredient : validIngredients) {
|
||||
if (CraftingManager.matches(ingredient, itemStack)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
input = this.processingBench.getFuel();
|
||||
inputSlotsCount = (short) (input != null ? input.length : 0);
|
||||
this.fuelContainer = ItemContainer.ensureContainerCapacity(this.fuelContainer, inputSlotsCount, SimpleItemContainer::getNewContainer, remainder);
|
||||
this.fuelContainer.registerChangeEvent(EventPriority.LAST, this::onItemChange);
|
||||
if (inputSlotsCount > 0) {
|
||||
for (int i = 0; i < input.length; i++) {
|
||||
ProcessingBench.ProcessingSlot fuel = input[i];
|
||||
String resourceTypeId = fuel.getResourceTypeId();
|
||||
if (resourceTypeId != null) {
|
||||
this.fuelContainer.setSlotFilter(FilterActionType.ADD, (short) i, new ResourceFilter(new ResourceQuantity(resourceTypeId, 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
short outputSlotsCount = (short) this.processingBench.getOutputSlotsCount(tierLevel);
|
||||
this.outputContainer = ItemContainer.ensureContainerCapacity(this.outputContainer, outputSlotsCount, SimpleItemContainer::getNewContainer, remainder);
|
||||
this.outputContainer.registerChangeEvent(EventPriority.LAST, this::onItemChange);
|
||||
if (outputSlotsCount > 0) {
|
||||
this.outputContainer.setGlobalFilter(FilterType.ALLOW_OUTPUT_ONLY);
|
||||
}
|
||||
|
||||
this.combinedItemContainer = new CombinedItemContainer(this.fuelContainer, this.inputContainer, this.outputContainer);
|
||||
World world = this.getChunk().getWorld();
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(store, remainder);
|
||||
if (itemEntityHolders.length > 0) {
|
||||
world.execute(() -> store.addEntities(itemEntityHolders, AddReason.SPAWN));
|
||||
}
|
||||
|
||||
this.inputContainer.registerChangeEvent(EventPriority.LAST, event -> this.updateRecipe());
|
||||
if (this.processingBench.getFuel() == null) {
|
||||
this.setActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(float dt, int index, ArchetypeChunk<ChunkStore> archetypeChunk, @Nonnull Store<ChunkStore> store, CommandBuffer<ChunkStore> commandBuffer) {
|
||||
World world = store.getExternalData().getWorld();
|
||||
Store<EntityStore> entityStore = world.getEntityStore().getStore();
|
||||
BlockType blockType = this.getBlockType();
|
||||
String currentState = BlockAccessor.getCurrentInteractionState(blockType);
|
||||
List<ItemStack> outputItemStacks = null;
|
||||
List<MaterialQuantity> inputMaterials = null;
|
||||
this.processingSlots.clear();
|
||||
this.checkForRecipeUpdate();
|
||||
if (this.recipe != null) {
|
||||
outputItemStacks = CraftingManager.getOutputItemStacks(this.recipe);
|
||||
if (!this.outputContainer.canAddItemStacks(outputItemStacks, false, false)) {
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
}
|
||||
|
||||
this.setActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
inputMaterials = CraftingManager.getInputMaterials(this.recipe);
|
||||
List<TestRemoveItemSlotResult> result = this.inputContainer.getSlotMaterialsToRemove(inputMaterials, true, true);
|
||||
if (result.isEmpty()) {
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
}
|
||||
|
||||
this.inputProgress = 0.0F;
|
||||
this.setActive(false);
|
||||
this.recipeId = null;
|
||||
this.recipe = null;
|
||||
return;
|
||||
}
|
||||
|
||||
for (TestRemoveItemSlotResult item : result) {
|
||||
this.processingSlots.addAll(item.getPickedSlots());
|
||||
}
|
||||
|
||||
this.sendProcessingSlots();
|
||||
} else {
|
||||
if (this.processingBench.getFuel() == null) {
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
boolean allowNoInputProcessing = this.processingBench.shouldAllowNoInputProcessing();
|
||||
if (!allowNoInputProcessing && "Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
this.setActive(false);
|
||||
this.sendProgress(0.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendProgress(0.0F);
|
||||
if (!allowNoInputProcessing) {
|
||||
this.setActive(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
boolean needsUpdate = false;
|
||||
if (this.fuelTime > 0.0F && this.active) {
|
||||
this.fuelTime -= dt;
|
||||
if (this.fuelTime < 0.0F) {
|
||||
this.fuelTime = 0.0F;
|
||||
}
|
||||
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
ProcessingBench.ProcessingSlot[] fuelSlots = this.processingBench.getFuel();
|
||||
boolean hasFuelSlots = fuelSlots != null && fuelSlots.length > 0;
|
||||
if ((this.processingBench.getMaxFuel() <= 0 || this.fuelTime < this.processingBench.getMaxFuel()) && !this.fuelContainer.isEmpty()) {
|
||||
if (!hasFuelSlots) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.active) {
|
||||
if (this.fuelTime > 0.0F) {
|
||||
for (int i = 0; i < fuelSlots.length; i++) {
|
||||
ItemStack itemInSlot = this.fuelContainer.getItemStack((short) i);
|
||||
if (itemInSlot != null) {
|
||||
this.processingFuelSlots.add((short) i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.fuelTime < 0.0F) {
|
||||
this.fuelTime = 0.0F;
|
||||
}
|
||||
|
||||
this.processingFuelSlots.clear();
|
||||
|
||||
for (int ix = 0; ix < fuelSlots.length; ix++) {
|
||||
ProcessingBench.ProcessingSlot fuelSlot = fuelSlots[ix];
|
||||
String resourceTypeId = fuelSlot.getResourceTypeId() != null ? fuelSlot.getResourceTypeId() : "Fuel";
|
||||
ResourceQuantity resourceQuantity = new ResourceQuantity(resourceTypeId, 1);
|
||||
ItemStack slot = this.fuelContainer.getItemStack((short) ix);
|
||||
if (slot != null) {
|
||||
double fuelQuality = slot.getItem().getFuelQuality();
|
||||
ResourceTransaction transaction = this.fuelContainer.removeResource(resourceQuantity, true, true, true);
|
||||
this.processingFuelSlots.add((short) ix);
|
||||
if (transaction.getRemainder() <= 0) {
|
||||
ProcessingBench.ExtraOutput extra = this.processingBench.getExtraOutput();
|
||||
if (extra != null && !extra.isIgnoredFuelSource(slot.getItem())) {
|
||||
this.nextExtra--;
|
||||
if (this.nextExtra <= 0) {
|
||||
this.nextExtra = extra.getPerFuelItemsConsumed();
|
||||
ObjectArrayList<ItemStack> extraItemStacks = new ObjectArrayList<>(extra.getOutputs().length);
|
||||
|
||||
for (MaterialQuantity e : extra.getOutputs()) {
|
||||
extraItemStacks.add(e.toItemStack());
|
||||
}
|
||||
|
||||
ListTransaction<ItemStackTransaction> addTransaction = this.outputContainer.addItemStacks(extraItemStacks, false, false, false);
|
||||
List<ItemStack> remainderItems = new ObjectArrayList<>();
|
||||
|
||||
for (ItemStackTransaction itemStackTransaction : addTransaction.getList()) {
|
||||
ItemStack remainder = itemStackTransaction.getRemainder();
|
||||
if (remainder != null && !remainder.isEmpty()) {
|
||||
remainderItems.add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
if (!remainderItems.isEmpty()) {
|
||||
LOGGER.at(Level.WARNING).log("Dropping excess items at %s", this.getBlockPosition());
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(entityStore, remainderItems);
|
||||
entityStore.addEntities(itemEntityHolders, AddReason.SPAWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.fuelTime = (float) (this.fuelTime + transaction.getConsumed() * fuelQuality);
|
||||
needsUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
this.updateFuelValues();
|
||||
}
|
||||
|
||||
if (!hasFuelSlots || this.active && !(this.fuelTime <= 0.0F)) {
|
||||
if (!"Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("Processing", blockType);
|
||||
}
|
||||
|
||||
if (this.recipe != null && (this.fuelTime > 0.0F || this.processingBench.getFuel() == null)) {
|
||||
this.inputProgress += dt;
|
||||
}
|
||||
|
||||
if (this.recipe != null) {
|
||||
float recipeTime = this.recipe.getTimeSeconds();
|
||||
float craftingTimeReductionModifier = this.getCraftingTimeReductionModifier();
|
||||
if (craftingTimeReductionModifier > 0.0F) {
|
||||
recipeTime -= recipeTime * craftingTimeReductionModifier;
|
||||
}
|
||||
|
||||
if (this.inputProgress > recipeTime) {
|
||||
if (recipeTime > 0.0F) {
|
||||
this.inputProgress -= recipeTime;
|
||||
float progressPercent = this.inputProgress / recipeTime;
|
||||
this.sendProgress(progressPercent);
|
||||
} else {
|
||||
this.inputProgress = 0.0F;
|
||||
this.sendProgress(0.0F);
|
||||
}
|
||||
|
||||
LOGGER.at(Level.FINE).log("Do Process for %s %s", this.recipeId, this.recipe);
|
||||
if (inputMaterials != null) {
|
||||
List<ItemStack> remainderItems = new ObjectArrayList<>();
|
||||
int success = 0;
|
||||
IntArrayList slots = new IntArrayList();
|
||||
|
||||
for (int j = 0; j < this.inputContainer.getCapacity(); j++) {
|
||||
slots.add(j);
|
||||
}
|
||||
|
||||
for (MaterialQuantity material : inputMaterials) {
|
||||
for (int ixx = 0; ixx < slots.size(); ixx++) {
|
||||
int slot = slots.getInt(ixx);
|
||||
MaterialSlotTransaction transaction = this.inputContainer.removeMaterialFromSlot((short) slot, material, true, true, true);
|
||||
if (transaction.succeeded()) {
|
||||
success++;
|
||||
slots.removeInt(ixx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListTransaction<ItemStackTransaction> addTransaction = this.outputContainer.addItemStacks(outputItemStacks, false, false, false);
|
||||
if (!addTransaction.succeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ItemStackTransaction itemStackTransactionx : addTransaction.getList()) {
|
||||
ItemStack remainder = itemStackTransactionx.getRemainder();
|
||||
if (remainder != null && !remainder.isEmpty()) {
|
||||
remainderItems.add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
if (success == inputMaterials.size()) {
|
||||
this.setBlockInteractionState("ProcessCompleted", blockType);
|
||||
this.playSound(world, this.bench.getCompletedSoundEventIndex(), entityStore);
|
||||
if (!remainderItems.isEmpty()) {
|
||||
LOGGER.at(Level.WARNING).log("Dropping excess items at %s", this.getBlockPosition());
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(entityStore, remainderItems);
|
||||
entityStore.addEntities(itemEntityHolders, AddReason.SPAWN);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<ItemStack> remainderItems = new ObjectArrayList<>();
|
||||
ListTransaction<MaterialTransaction> transaction = this.inputContainer.removeMaterials(inputMaterials, true, true, true);
|
||||
if (!transaction.succeeded()) {
|
||||
LOGGER.at(Level.WARNING).log("Failed to remove input materials at %s", this.getBlockPosition());
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setBlockInteractionState("ProcessCompleted", blockType);
|
||||
this.playSound(world, this.bench.getCompletedSoundEventIndex(), entityStore);
|
||||
ListTransaction<ItemStackTransaction> addTransactionx = this.outputContainer.addItemStacks(outputItemStacks, false, false, false);
|
||||
if (addTransactionx.succeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.at(Level.WARNING).log("Dropping excess items at %s", this.getBlockPosition());
|
||||
|
||||
for (ItemStackTransaction itemStackTransactionxx : addTransactionx.getList()) {
|
||||
ItemStack remainder = itemStackTransactionxx.getRemainder();
|
||||
if (remainder != null && !remainder.isEmpty()) {
|
||||
remainderItems.add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(entityStore, remainderItems);
|
||||
entityStore.addEntities(itemEntityHolders, AddReason.SPAWN);
|
||||
} else if (this.recipe != null && recipeTime > 0.0F) {
|
||||
float progressPercent = this.inputProgress / recipeTime;
|
||||
this.sendProgress(progressPercent);
|
||||
} else {
|
||||
this.sendProgress(0.0F);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.lastConsumedFuelTotal = 0;
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
if (this.processingBench.getFuel() != null) {
|
||||
this.setActive(false);
|
||||
}
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
if (this.processingBench.getFuel() != null) {
|
||||
this.setActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float getCraftingTimeReductionModifier() {
|
||||
BenchTierLevel levelData = this.bench.getTierLevel(this.getTierLevel());
|
||||
return levelData != null ? levelData.getCraftingTimeReductionModifier() : 0.0F;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Holder<EntityStore>[] ejectItems(@Nonnull ComponentAccessor<EntityStore> accessor, @Nonnull List<ItemStack> itemStacks) {
|
||||
if (itemStacks.isEmpty()) {
|
||||
return Holder.emptyArray();
|
||||
} else {
|
||||
RotationTuple rotation = RotationTuple.get(this.getRotationIndex());
|
||||
Vector3d frontDir = new Vector3d(0.0, 0.0, 1.0);
|
||||
rotation.yaw().rotateY(frontDir, frontDir);
|
||||
BlockType blockType = this.getBlockType();
|
||||
Vector3d dropPosition;
|
||||
if (blockType == null) {
|
||||
dropPosition = this.getBlockPosition().toVector3d().add(0.5, 0.0, 0.5);
|
||||
} else {
|
||||
BlockBoundingBoxes hitboxAsset = BlockBoundingBoxes.getAssetMap().getAsset(blockType.getHitboxTypeIndex());
|
||||
if (hitboxAsset == null) {
|
||||
dropPosition = this.getBlockPosition().toVector3d().add(0.5, 0.0, 0.5);
|
||||
} else {
|
||||
double depth = hitboxAsset.get(0).getBoundingBox().depth();
|
||||
double frontOffset = depth / 2.0 + 0.1F;
|
||||
dropPosition = this.getCenteredBlockPosition();
|
||||
dropPosition.add(frontDir.x * frontOffset, 0.0, frontDir.z * frontOffset);
|
||||
}
|
||||
}
|
||||
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
ObjectArrayList<Holder<EntityStore>> result = new ObjectArrayList<>(itemStacks.size());
|
||||
|
||||
for (ItemStack item : itemStacks) {
|
||||
float velocityX = (float) (frontDir.x * 2.0 + 2.0 * (random.nextDouble() - 0.5));
|
||||
float velocityZ = (float) (frontDir.z * 2.0 + 2.0 * (random.nextDouble() - 0.5));
|
||||
Holder<EntityStore> holder = ItemComponent.generateItemDrop(accessor, item, dropPosition, Vector3f.ZERO, velocityX, 3.25F, velocityZ);
|
||||
if (holder != null) {
|
||||
result.add(holder);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toArray(Holder[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendProgress(float progress) {
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setProgress(progress));
|
||||
}
|
||||
|
||||
private void sendProcessingSlots() {
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setProcessingSlots(this.processingSlots));
|
||||
}
|
||||
|
||||
private void sendProcessingFuelSlots() {
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setProcessingFuelSlots(this.processingFuelSlots));
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
public boolean setActive(boolean active) {
|
||||
if (this.active != active) {
|
||||
if (active && this.processingBench.getFuel() != null && this.fuelContainer.isEmpty()) {
|
||||
return false;
|
||||
} else {
|
||||
this.active = active;
|
||||
if (!active) {
|
||||
this.processingSlots.clear();
|
||||
this.processingFuelSlots.clear();
|
||||
this.sendProcessingSlots();
|
||||
this.sendProcessingFuelSlots();
|
||||
}
|
||||
|
||||
this.updateRecipe();
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setActive(active));
|
||||
this.markNeedsSave();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFuelValues() {
|
||||
if (this.fuelTime > this.lastConsumedFuelTotal) {
|
||||
this.lastConsumedFuelTotal = MathUtil.ceil(this.fuelTime);
|
||||
}
|
||||
|
||||
float fuelPercent = this.lastConsumedFuelTotal > 0 ? this.fuelTime / this.lastConsumedFuelTotal : 0.0F;
|
||||
this.windows.forEach((uuid, window) -> {
|
||||
ProcessingBenchWindow processingBenchWindow = (ProcessingBenchWindow) window;
|
||||
processingBenchWindow.setFuelTime(fuelPercent);
|
||||
processingBenchWindow.setMaxFuel(this.lastConsumedFuelTotal);
|
||||
processingBenchWindow.setProcessingFuelSlots(this.processingFuelSlots);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// HyFix: Clear windows map before closeAndRemoveAll to prevent NPE cascade
|
||||
// When a player breaks a bench while another player has it open, the close handlers
|
||||
// try to access block data that is already being destroyed, causing NPE in
|
||||
// BlockType.getDefaultStateKey() and BenchWindow.onClose0()
|
||||
if (this.windows != null) {
|
||||
this.windows.clear();
|
||||
}
|
||||
WindowManager.closeAndRemoveAll(this.windows);
|
||||
if (this.combinedItemContainer != null) {
|
||||
List<ItemStack> itemStacks = this.combinedItemContainer.dropAllItemStacks();
|
||||
this.dropFuelItems(itemStacks);
|
||||
World world = this.getChunk().getWorld();
|
||||
Store<EntityStore> entityStore = world.getEntityStore().getStore();
|
||||
Vector3d dropPosition = this.getBlockPosition().toVector3d().add(0.5, 0.0, 0.5);
|
||||
Holder<EntityStore>[] itemEntityHolders = ItemComponent.generateItemDrops(entityStore, itemStacks, dropPosition, Vector3f.ZERO);
|
||||
if (itemEntityHolders.length > 0) {
|
||||
world.execute(() -> entityStore.addEntities(itemEntityHolders, AddReason.SPAWN));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.marker != null) {
|
||||
this.marker.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public CombinedItemContainer getItemContainer() {
|
||||
return this.combinedItemContainer;
|
||||
}
|
||||
|
||||
private void checkForRecipeUpdate() {
|
||||
if (this.recipe == null && this.recipeId != null) {
|
||||
this.updateRecipe();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRecipe() {
|
||||
List<CraftingRecipe> recipes = CraftingPlugin.getBenchRecipes(this.bench.getType(), this.bench.getId());
|
||||
if (recipes.isEmpty()) {
|
||||
this.clearRecipe();
|
||||
} else {
|
||||
List<CraftingRecipe> matching = new ObjectArrayList<>();
|
||||
|
||||
for (CraftingRecipe recipe : recipes) {
|
||||
if (!recipe.isRestrictedByBenchTierLevel(this.bench.getId(), this.getTierLevel())) {
|
||||
MaterialQuantity[] input = recipe.getInput();
|
||||
int matches = 0;
|
||||
IntArrayList slots = new IntArrayList();
|
||||
|
||||
for (int j = 0; j < this.inputContainer.getCapacity(); j++) {
|
||||
slots.add(j);
|
||||
}
|
||||
|
||||
for (MaterialQuantity craftingMaterial : input) {
|
||||
String itemId = craftingMaterial.getItemId();
|
||||
String resourceTypeId = craftingMaterial.getResourceTypeId();
|
||||
int materialQuantity = craftingMaterial.getQuantity();
|
||||
BsonDocument metadata = craftingMaterial.getMetadata();
|
||||
MaterialQuantity material = new MaterialQuantity(itemId, resourceTypeId, null, materialQuantity, metadata);
|
||||
|
||||
for (int k = 0; k < slots.size(); k++) {
|
||||
int j = slots.getInt(k);
|
||||
int out = InternalContainerUtilMaterial.testRemoveMaterialFromSlot(this.inputContainer, (short) j, material, material.getQuantity(), true);
|
||||
if (out == 0) {
|
||||
matches++;
|
||||
slots.removeInt(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches == input.length) {
|
||||
matching.add(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matching.isEmpty()) {
|
||||
this.clearRecipe();
|
||||
} else {
|
||||
matching.sort(Comparator.comparingInt(o -> CraftingManager.getInputMaterials(o).size()));
|
||||
Collections.reverse(matching);
|
||||
if (this.recipeId != null) {
|
||||
for (CraftingRecipe rec : matching) {
|
||||
if (Objects.equals(this.recipeId, rec.getId())) {
|
||||
LOGGER.at(Level.FINE).log("%s - Keeping existing Recipe %s %s", LazyArgs.lazy(this::getBlockPosition), this.recipeId, rec);
|
||||
this.recipe = rec;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CraftingRecipe recipex = matching.getFirst();
|
||||
if (this.recipeId == null || !Objects.equals(this.recipeId, recipex.getId())) {
|
||||
this.inputProgress = 0.0F;
|
||||
this.sendProgress(0.0F);
|
||||
}
|
||||
|
||||
this.recipeId = recipex.getId();
|
||||
this.recipe = recipex;
|
||||
LOGGER.at(Level.FINE).log("%s - Found Recipe %s %s", LazyArgs.lazy(this::getBlockPosition), this.recipeId, this.recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearRecipe() {
|
||||
this.recipeId = null;
|
||||
this.recipe = null;
|
||||
this.lastConsumedFuelTotal = 0;
|
||||
this.inputProgress = 0.0F;
|
||||
this.sendProgress(0.0F);
|
||||
LOGGER.at(Level.FINE).log("%s - Cleared Recipe", LazyArgs.lazy(this::getBlockPosition));
|
||||
}
|
||||
|
||||
public void dropFuelItems(@Nonnull List<ItemStack> itemStacks) {
|
||||
String fuelDropItemId = this.processingBench.getFuelDropItemId();
|
||||
if (fuelDropItemId != null) {
|
||||
Item item = Item.getAssetMap().getAsset(fuelDropItemId);
|
||||
int dropAmount = (int) this.fuelTime;
|
||||
this.fuelTime = 0.0F;
|
||||
|
||||
while (dropAmount > 0) {
|
||||
int quantity = Math.min(dropAmount, item.getMaxStack());
|
||||
itemStacks.add(new ItemStack(fuelDropItemId, quantity));
|
||||
dropAmount -= quantity;
|
||||
}
|
||||
} else {
|
||||
LOGGER.at(Level.WARNING).log("No FuelDropItemId defined for %s fuel value of %s will be lost!", this.bench.getId(), this.fuelTime);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CraftingRecipe getRecipe() {
|
||||
return this.recipe;
|
||||
}
|
||||
|
||||
public float getInputProgress() {
|
||||
return this.inputProgress;
|
||||
}
|
||||
|
||||
public void onItemChange(ItemContainer.ItemContainerChangeEvent event) {
|
||||
this.markNeedsSave();
|
||||
}
|
||||
|
||||
public void setBlockInteractionState(@Nonnull String state, @Nonnull BlockType blockType) {
|
||||
this.getChunk().setBlockInteractionState(this.getBlockPosition(), blockType, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarker(WorldMapManager.MarkerReference marker) {
|
||||
this.marker = marker;
|
||||
this.markNeedsSave();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void placedBy(
|
||||
@Nonnull Ref<EntityStore> playerRef,
|
||||
@Nonnull String blockTypeKey,
|
||||
@Nonnull BlockState blockState,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor
|
||||
) {
|
||||
if (blockTypeKey.equals(this.processingBench.getIconItem()) && this.processingBench.getIcon() != null) {
|
||||
Player playerComponent = componentAccessor.getComponent(playerRef, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
TransformComponent transformComponent = componentAccessor.getComponent(playerRef, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
Transform transformPacket = PositionUtil.toTransformPacket(transformComponent.getTransform());
|
||||
transformPacket.orientation.yaw = 0.0F;
|
||||
transformPacket.orientation.pitch = 0.0F;
|
||||
transformPacket.orientation.roll = 0.0F;
|
||||
MapMarker marker = new MapMarker(
|
||||
this.processingBench.getIconId() + "-" + UUID.randomUUID(),
|
||||
this.processingBench.getIconName(),
|
||||
this.processingBench.getIcon(),
|
||||
transformPacket,
|
||||
null
|
||||
);
|
||||
((MarkerBlockState) blockState).setMarker(WorldMapManager.createPlayerMarker(playerRef, marker, componentAccessor));
|
||||
}
|
||||
}
|
||||
|
||||
private void playSound(@Nonnull World world, int soundEventIndex, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (soundEventIndex != 0) {
|
||||
Vector3i pos = this.getBlockPosition();
|
||||
SoundUtil.playSoundEvent3d(soundEventIndex, SoundCategory.SFX, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, componentAccessor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTierLevelChange() {
|
||||
super.onTierLevelChange();
|
||||
this.setupSlots();
|
||||
}
|
||||
}
|
||||
@@ -1,414 +0,0 @@
|
||||
package com.hypixel.hytale.builtin.fluid;
|
||||
|
||||
import com.hypixel.hytale.builtin.blocktick.system.ChunkBlockTickSystem;
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.RemoveReason;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.dependency.Dependency;
|
||||
import com.hypixel.hytale.component.dependency.Order;
|
||||
import com.hypixel.hytale.component.dependency.RootDependency;
|
||||
import com.hypixel.hytale.component.dependency.SystemDependency;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.HolderSystem;
|
||||
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
|
||||
import com.hypixel.hytale.component.system.tick.RunWhenPausedSystem;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.util.ChunkUtil;
|
||||
import com.hypixel.hytale.protocol.Packet;
|
||||
import com.hypixel.hytale.protocol.packets.world.ServerSetFluid;
|
||||
import com.hypixel.hytale.protocol.packets.world.ServerSetFluids;
|
||||
import com.hypixel.hytale.protocol.packets.world.SetFluidCmd;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktick.BlockTickStrategy;
|
||||
import com.hypixel.hytale.server.core.asset.type.fluid.Fluid;
|
||||
import com.hypixel.hytale.server.core.asset.type.fluid.FluidTicker;
|
||||
import com.hypixel.hytale.server.core.modules.LegacyModule;
|
||||
import com.hypixel.hytale.server.core.modules.entity.player.ChunkTracker;
|
||||
import com.hypixel.hytale.server.core.modules.migrations.ChunkColumnMigrationSystem;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.ChunkColumn;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.section.ChunkSection;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.section.FluidSection;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import it.unimi.dsi.fastutil.ints.IntIterator;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.function.Function;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class FluidSystems {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
private static final int MAX_CHANGES_PER_PACKET = 1024;
|
||||
|
||||
public FluidSystems() {
|
||||
}
|
||||
|
||||
public static class EnsureFluidSection extends HolderSystem<ChunkStore> {
|
||||
@Nonnull
|
||||
private static final Query<ChunkStore> QUERY = Query.and(ChunkSection.getComponentType(), Query.not(FluidSection.getComponentType()));
|
||||
|
||||
public EnsureFluidSection() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdd(@Nonnull Holder<ChunkStore> holder, @Nonnull AddReason reason, @Nonnull Store<ChunkStore> store) {
|
||||
holder.addComponent(FluidSection.getComponentType(), new FluidSection());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemoved(@Nonnull Holder<ChunkStore> holder, @Nonnull RemoveReason reason, @Nonnull Store<ChunkStore> store) {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return QUERY;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<ChunkStore>> getDependencies() {
|
||||
return RootDependency.firstSet();
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoadPacketGenerator extends ChunkStore.LoadFuturePacketDataQuerySystem {
|
||||
public LoadPacketGenerator() {
|
||||
}
|
||||
|
||||
public void fetch(
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer,
|
||||
PlayerRef query,
|
||||
@Nonnull List<CompletableFuture<Packet>> results
|
||||
) {
|
||||
ChunkColumn chunkColumnComponent = archetypeChunk.getComponent(index, ChunkColumn.getComponentType());
|
||||
|
||||
assert chunkColumnComponent != null;
|
||||
|
||||
for (Ref<ChunkStore> sectionRef : chunkColumnComponent.getSections()) {
|
||||
FluidSection fluidSectionComponent = commandBuffer.getComponent(sectionRef, FluidSection.getComponentType());
|
||||
if (fluidSectionComponent != null) {
|
||||
results.add(fluidSectionComponent.getCachedPacket().exceptionally(throwable -> {
|
||||
if (throwable != null) {
|
||||
FluidSystems.LOGGER.at(Level.SEVERE).withCause(throwable).log("Exception when compressing chunk fluids:");
|
||||
}
|
||||
|
||||
return null;
|
||||
}).thenApply(Function.identity()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return ChunkColumn.getComponentType();
|
||||
}
|
||||
}
|
||||
|
||||
public static class MigrateFromColumn extends ChunkColumnMigrationSystem {
|
||||
@Nonnull
|
||||
private final Query<ChunkStore> QUERY = Query.and(ChunkColumn.getComponentType(), BlockChunk.getComponentType());
|
||||
@Nonnull
|
||||
private final Set<Dependency<ChunkStore>> DEPENDENCIES = Set.of(new SystemDependency<>(Order.BEFORE, LegacyModule.MigrateLegacySections.class));
|
||||
|
||||
public MigrateFromColumn() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdd(@Nonnull Holder<ChunkStore> holder, @Nonnull AddReason reason, @Nonnull Store<ChunkStore> store) {
|
||||
ChunkColumn chunkColumnComponent = holder.getComponent(ChunkColumn.getComponentType());
|
||||
|
||||
assert chunkColumnComponent != null;
|
||||
|
||||
BlockChunk blockChunkComponent = holder.getComponent(BlockChunk.getComponentType());
|
||||
|
||||
assert blockChunkComponent != null;
|
||||
|
||||
Holder<ChunkStore>[] sections = chunkColumnComponent.getSectionHolders();
|
||||
BlockSection[] legacySections = blockChunkComponent.getMigratedSections();
|
||||
if (legacySections != null) {
|
||||
for (int i = 0; i < sections.length; i++) {
|
||||
Holder<ChunkStore> section = sections[i];
|
||||
BlockSection paletteSection = legacySections[i];
|
||||
if (section != null && paletteSection != null) {
|
||||
FluidSection fluid = paletteSection.takeMigratedFluid();
|
||||
if (fluid != null) {
|
||||
section.putComponent(FluidSection.getComponentType(), fluid);
|
||||
blockChunkComponent.markNeedsSaving();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemoved(@Nonnull Holder<ChunkStore> holder, @Nonnull RemoveReason reason, @Nonnull Store<ChunkStore> store) {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return this.QUERY;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<ChunkStore>> getDependencies() {
|
||||
return this.DEPENDENCIES;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ReplicateChanges extends EntityTickingSystem<ChunkStore> implements RunWhenPausedSystem<ChunkStore> {
|
||||
@Nonnull
|
||||
private static final Query<ChunkStore> QUERY = Query.and(ChunkSection.getComponentType(), FluidSection.getComponentType());
|
||||
|
||||
public ReplicateChanges() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.useParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
FluidSection fluidSectionComponent = archetypeChunk.getComponent(index, FluidSection.getComponentType());
|
||||
|
||||
assert fluidSectionComponent != null;
|
||||
|
||||
IntOpenHashSet changes = fluidSectionComponent.getAndClearChangedPositions();
|
||||
if (!changes.isEmpty()) {
|
||||
ChunkSection chunkSectionComponent = archetypeChunk.getComponent(index, ChunkSection.getComponentType());
|
||||
|
||||
assert chunkSectionComponent != null;
|
||||
|
||||
World world = commandBuffer.getExternalData().getWorld();
|
||||
WorldChunk worldChunkComponent = commandBuffer.getComponent(chunkSectionComponent.getChunkColumnReference(), WorldChunk.getComponentType());
|
||||
int sectionY = chunkSectionComponent.getY();
|
||||
world.execute(() -> {
|
||||
if (worldChunkComponent != null && worldChunkComponent.getWorld() != null) {
|
||||
worldChunkComponent.getWorld().getChunkLighting().invalidateLightInChunkSection(worldChunkComponent, sectionY);
|
||||
}
|
||||
});
|
||||
Collection<PlayerRef> playerRefs = store.getExternalData().getWorld().getPlayerRefs();
|
||||
if (playerRefs.isEmpty()) {
|
||||
changes.clear();
|
||||
} else {
|
||||
long chunkIndex = ChunkUtil.indexChunk(fluidSectionComponent.getX(), fluidSectionComponent.getZ());
|
||||
if (changes.size() >= 1024) {
|
||||
ObjectArrayList<PlayerRef> playersCopy = new ObjectArrayList<>(playerRefs);
|
||||
fluidSectionComponent.getCachedPacket().whenComplete((packetx, throwable) -> {
|
||||
if (throwable != null) {
|
||||
FluidSystems.LOGGER.at(Level.SEVERE).withCause(throwable).log("Exception when compressing chunk fluids:");
|
||||
} else {
|
||||
for (PlayerRef playerRefx : playersCopy) {
|
||||
Ref<EntityStore> refx = playerRefx.getReference();
|
||||
if (refx != null && refx.isValid()) {
|
||||
ChunkTracker trackerx = playerRefx.getChunkTracker();
|
||||
if (trackerx.isLoaded(chunkIndex)) {
|
||||
playerRefx.getPacketHandler().writeNoCache(packetx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
changes.clear();
|
||||
} else {
|
||||
if (changes.size() == 1) {
|
||||
int change = changes.iterator().nextInt();
|
||||
int x = ChunkUtil.minBlock(fluidSectionComponent.getX()) + ChunkUtil.xFromIndex(change);
|
||||
int y = ChunkUtil.minBlock(fluidSectionComponent.getY()) + ChunkUtil.yFromIndex(change);
|
||||
int z = ChunkUtil.minBlock(fluidSectionComponent.getZ()) + ChunkUtil.zFromIndex(change);
|
||||
int fluid = fluidSectionComponent.getFluidId(change);
|
||||
byte level = fluidSectionComponent.getFluidLevel(change);
|
||||
ServerSetFluid packet = new ServerSetFluid(x, y, z, fluid, level);
|
||||
|
||||
for (PlayerRef playerRef : playerRefs) {
|
||||
Ref<EntityStore> ref = playerRef.getReference();
|
||||
if (ref != null && ref.isValid()) {
|
||||
ChunkTracker tracker = playerRef.getChunkTracker();
|
||||
if (tracker.isLoaded(chunkIndex)) {
|
||||
playerRef.getPacketHandler().writeNoCache(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SetFluidCmd[] cmds = new SetFluidCmd[changes.size()];
|
||||
IntIterator iter = changes.intIterator();
|
||||
int i = 0;
|
||||
|
||||
while (iter.hasNext()) {
|
||||
int change = iter.nextInt();
|
||||
int fluid = fluidSectionComponent.getFluidId(change);
|
||||
byte level = fluidSectionComponent.getFluidLevel(change);
|
||||
cmds[i++] = new SetFluidCmd((short) change, fluid, level);
|
||||
}
|
||||
|
||||
ServerSetFluids packet = new ServerSetFluids(
|
||||
fluidSectionComponent.getX(), fluidSectionComponent.getY(), fluidSectionComponent.getZ(), cmds
|
||||
);
|
||||
|
||||
for (PlayerRef playerRefx : playerRefs) {
|
||||
Ref<EntityStore> ref = playerRefx.getReference();
|
||||
if (ref != null && ref.isValid()) {
|
||||
ChunkTracker tracker = playerRefx.getChunkTracker();
|
||||
if (tracker.isLoaded(chunkIndex)) {
|
||||
playerRefx.getPacketHandler().writeNoCache(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changes.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return QUERY;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<ChunkStore>> getDependencies() {
|
||||
return RootDependency.lastSet();
|
||||
}
|
||||
}
|
||||
|
||||
public static class SetupSection extends HolderSystem<ChunkStore> {
|
||||
@Nonnull
|
||||
private static final Query<ChunkStore> QUERY = Query.and(ChunkSection.getComponentType(), FluidSection.getComponentType());
|
||||
@Nonnull
|
||||
private static final Set<Dependency<ChunkStore>> DEPENDENCIES = Set.of(new SystemDependency<>(Order.AFTER, FluidSystems.MigrateFromColumn.class));
|
||||
|
||||
public SetupSection() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdd(@Nonnull Holder<ChunkStore> holder, @Nonnull AddReason reason, @Nonnull Store<ChunkStore> store) {
|
||||
ChunkSection chunkSectionComponent = holder.getComponent(ChunkSection.getComponentType());
|
||||
|
||||
assert chunkSectionComponent != null;
|
||||
|
||||
FluidSection fluidSectionComponent = holder.getComponent(FluidSection.getComponentType());
|
||||
|
||||
assert fluidSectionComponent != null;
|
||||
|
||||
fluidSectionComponent.load(chunkSectionComponent.getX(), chunkSectionComponent.getY(), chunkSectionComponent.getZ());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemoved(@Nonnull Holder<ChunkStore> holder, @Nonnull RemoveReason reason, @Nonnull Store<ChunkStore> store) {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return QUERY;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<ChunkStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Ticking extends EntityTickingSystem<ChunkStore> {
|
||||
@Nonnull
|
||||
private static final Query<ChunkStore> QUERY = Query.and(FluidSection.getComponentType(), ChunkSection.getComponentType());
|
||||
@Nonnull
|
||||
private static final Set<Dependency<ChunkStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, ChunkBlockTickSystem.PreTick.class), new SystemDependency<>(Order.BEFORE, ChunkBlockTickSystem.Ticking.class)
|
||||
);
|
||||
|
||||
public Ticking() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.useParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
ChunkSection chunkSectionComponent = archetypeChunk.getComponent(index, ChunkSection.getComponentType());
|
||||
|
||||
assert chunkSectionComponent != null;
|
||||
|
||||
FluidSection fluidSectionComponent = archetypeChunk.getComponent(index, FluidSection.getComponentType());
|
||||
|
||||
assert fluidSectionComponent != null;
|
||||
|
||||
Ref<ChunkStore> chunkRef = chunkSectionComponent.getChunkColumnReference();
|
||||
BlockChunk blockChunkComponent = commandBuffer.getComponent(chunkRef, BlockChunk.getComponentType());
|
||||
|
||||
assert blockChunkComponent != null;
|
||||
|
||||
BlockSection blockSection = blockChunkComponent.getSectionAtIndex(fluidSectionComponent.getY());
|
||||
if (blockSection != null) {
|
||||
if (blockSection.getTickingBlocksCountCopy() != 0) {
|
||||
FluidTicker.CachedAccessor accessor = FluidTicker.CachedAccessor.of(commandBuffer, fluidSectionComponent, blockSection, 5);
|
||||
blockSection.forEachTicking(accessor, commandBuffer, fluidSectionComponent.getY(), (accessor1, commandBuffer1, x, y, z, block) -> {
|
||||
FluidSection fluidSection1 = accessor1.selfFluidSection;
|
||||
BlockSection blockSection1 = accessor1.selfBlockSection;
|
||||
int fluidId = fluidSection1.getFluidId(x, y, z);
|
||||
if (fluidId == 0) {
|
||||
return BlockTickStrategy.IGNORED;
|
||||
} else {
|
||||
Fluid fluid = Fluid.getAssetMap().getAsset(fluidId);
|
||||
int blockX = fluidSection1.getX() << 5 | x;
|
||||
int blockZ = fluidSection1.getZ() << 5 | z;
|
||||
return fluid.getTicker().tick(commandBuffer1, accessor1, fluidSection1, blockSection1, fluid, fluidId, blockX, y, blockZ);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return QUERY;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<ChunkStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,644 +0,0 @@
|
||||
package com.hypixel.hytale.builtin.instances;
|
||||
|
||||
import com.hypixel.hytale.assetstore.AssetPack;
|
||||
import com.hypixel.hytale.builtin.blockphysics.WorldValidationUtil;
|
||||
import com.hypixel.hytale.builtin.instances.blocks.ConfigurableInstanceBlock;
|
||||
import com.hypixel.hytale.builtin.instances.blocks.InstanceBlock;
|
||||
import com.hypixel.hytale.builtin.instances.command.InstancesCommand;
|
||||
import com.hypixel.hytale.builtin.instances.config.ExitInstance;
|
||||
import com.hypixel.hytale.builtin.instances.config.InstanceDiscoveryConfig;
|
||||
import com.hypixel.hytale.builtin.instances.config.InstanceEntityConfig;
|
||||
import com.hypixel.hytale.builtin.instances.config.InstanceWorldConfig;
|
||||
import com.hypixel.hytale.builtin.instances.config.WorldReturnPoint;
|
||||
import com.hypixel.hytale.builtin.instances.event.DiscoverInstanceEvent;
|
||||
import com.hypixel.hytale.builtin.instances.interactions.ExitInstanceInteraction;
|
||||
import com.hypixel.hytale.builtin.instances.interactions.TeleportConfigInstanceInteraction;
|
||||
import com.hypixel.hytale.builtin.instances.interactions.TeleportInstanceInteraction;
|
||||
import com.hypixel.hytale.builtin.instances.page.ConfigureInstanceBlockPage;
|
||||
import com.hypixel.hytale.builtin.instances.removal.IdleTimeoutCondition;
|
||||
import com.hypixel.hytale.builtin.instances.removal.InstanceDataResource;
|
||||
import com.hypixel.hytale.builtin.instances.removal.RemovalCondition;
|
||||
import com.hypixel.hytale.builtin.instances.removal.RemovalSystem;
|
||||
import com.hypixel.hytale.builtin.instances.removal.TimeoutCondition;
|
||||
import com.hypixel.hytale.builtin.instances.removal.WorldEmptyCondition;
|
||||
import com.hypixel.hytale.codec.schema.config.ObjectSchema;
|
||||
import com.hypixel.hytale.codec.schema.config.Schema;
|
||||
import com.hypixel.hytale.codec.schema.config.StringSchema;
|
||||
import com.hypixel.hytale.common.util.FormatUtil;
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.ComponentRegistryProxy;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.ResourceType;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.event.EventRegistry;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.vector.Transform;
|
||||
import com.hypixel.hytale.math.vector.Vector3f;
|
||||
import com.hypixel.hytale.protocol.GameMode;
|
||||
import com.hypixel.hytale.protocol.SoundCategory;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.Options;
|
||||
import com.hypixel.hytale.server.core.asset.AssetModule;
|
||||
import com.hypixel.hytale.server.core.asset.GenerateSchemaEvent;
|
||||
import com.hypixel.hytale.server.core.asset.LoadAssetEvent;
|
||||
import com.hypixel.hytale.server.core.asset.type.gameplay.respawn.RespawnController;
|
||||
import com.hypixel.hytale.server.core.asset.type.soundevent.config.SoundEvent;
|
||||
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerConfigData;
|
||||
import com.hypixel.hytale.server.core.event.events.player.AddPlayerToWorldEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.DrainPlayerFromWorldEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerReadyEvent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.teleport.Teleport;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.server.OpenCustomUIInteraction;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import com.hypixel.hytale.server.core.universe.world.SoundUtil;
|
||||
import com.hypixel.hytale.server.core.universe.world.ValidationOption;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
|
||||
import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.EmptyChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.IChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.MigrationChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.resources.EmptyResourceStorageProvider;
|
||||
import com.hypixel.hytale.server.core.util.EventTitleUtil;
|
||||
import com.hypixel.hytale.server.core.util.io.FileUtil;
|
||||
import com.hypixel.hytale.sneakythrow.SneakyThrow;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.EnumSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class InstancesPlugin extends JavaPlugin {
|
||||
private static InstancesPlugin instance;
|
||||
@Nonnull
|
||||
public static final String INSTANCE_PREFIX = "instance-";
|
||||
@Nonnull
|
||||
public static final String CONFIG_FILENAME = "instance.bson";
|
||||
private ResourceType<ChunkStore, InstanceDataResource> instanceDataResourceType;
|
||||
private ComponentType<EntityStore, InstanceEntityConfig> instanceEntityConfigComponentType;
|
||||
private ComponentType<ChunkStore, InstanceBlock> instanceBlockComponentType;
|
||||
private ComponentType<ChunkStore, ConfigurableInstanceBlock> configurableInstanceBlockComponentType;
|
||||
|
||||
public static InstancesPlugin get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public InstancesPlugin(@Nonnull JavaPluginInit init) {
|
||||
super(init);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setup() {
|
||||
EventRegistry eventRegistry = this.getEventRegistry();
|
||||
ComponentRegistryProxy<ChunkStore> chunkStoreRegistry = this.getChunkStoreRegistry();
|
||||
this.getCommandRegistry().registerCommand(new InstancesCommand());
|
||||
eventRegistry.register((short) 64, LoadAssetEvent.class, this::validateInstanceAssets);
|
||||
eventRegistry.register(GenerateSchemaEvent.class, InstancesPlugin::generateSchema);
|
||||
eventRegistry.registerGlobal(AddPlayerToWorldEvent.class, InstancesPlugin::onPlayerAddToWorld);
|
||||
eventRegistry.registerGlobal(DrainPlayerFromWorldEvent.class, InstancesPlugin::onPlayerDrainFromWorld);
|
||||
eventRegistry.register(PlayerConnectEvent.class, InstancesPlugin::onPlayerConnect);
|
||||
eventRegistry.registerGlobal(PlayerReadyEvent.class, InstancesPlugin::onPlayerReady);
|
||||
this.instanceBlockComponentType = chunkStoreRegistry.registerComponent(InstanceBlock.class, "Instance", InstanceBlock.CODEC);
|
||||
chunkStoreRegistry.registerSystem(new InstanceBlock.OnRemove());
|
||||
this.configurableInstanceBlockComponentType = chunkStoreRegistry.registerComponent(
|
||||
ConfigurableInstanceBlock.class, "InstanceConfig", ConfigurableInstanceBlock.CODEC
|
||||
);
|
||||
chunkStoreRegistry.registerSystem(new ConfigurableInstanceBlock.OnRemove());
|
||||
this.instanceDataResourceType = chunkStoreRegistry.registerResource(InstanceDataResource.class, "InstanceData", InstanceDataResource.CODEC);
|
||||
chunkStoreRegistry.registerSystem(new RemovalSystem());
|
||||
this.instanceEntityConfigComponentType = this.getEntityStoreRegistry()
|
||||
.registerComponent(InstanceEntityConfig.class, "Instance", InstanceEntityConfig.CODEC);
|
||||
this.getCodecRegistry(RemovalCondition.CODEC)
|
||||
.register("WorldEmpty", WorldEmptyCondition.class, WorldEmptyCondition.CODEC)
|
||||
.register("IdleTimeout", IdleTimeoutCondition.class, IdleTimeoutCondition.CODEC)
|
||||
.register("Timeout", TimeoutCondition.class, TimeoutCondition.CODEC);
|
||||
this.getCodecRegistry(Interaction.CODEC)
|
||||
.register("TeleportInstance", TeleportInstanceInteraction.class, TeleportInstanceInteraction.CODEC)
|
||||
.register("TeleportConfigInstance", TeleportConfigInstanceInteraction.class, TeleportConfigInstanceInteraction.CODEC)
|
||||
.register("ExitInstance", ExitInstanceInteraction.class, ExitInstanceInteraction.CODEC);
|
||||
this.getCodecRegistry(RespawnController.CODEC).register("ExitInstance", ExitInstance.class, ExitInstance.CODEC);
|
||||
OpenCustomUIInteraction.registerBlockEntityCustomPage(
|
||||
this, ConfigureInstanceBlockPage.class, "ConfigInstanceBlock", ConfigureInstanceBlockPage::new, () -> {
|
||||
Holder<ChunkStore> holder = ChunkStore.REGISTRY.newHolder();
|
||||
holder.ensureComponent(ConfigurableInstanceBlock.getComponentType());
|
||||
return holder;
|
||||
}
|
||||
);
|
||||
this.getCodecRegistry(WorldConfig.PLUGIN_CODEC).register(InstanceWorldConfig.class, "Instance", InstanceWorldConfig.CODEC);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<World> spawnInstance(@Nonnull String name, @Nonnull World forWorld, @Nonnull Transform returnPoint) {
|
||||
return this.spawnInstance(name, null, forWorld, returnPoint);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<World> spawnInstance(@Nonnull String name, @Nullable String worldName, @Nonnull World forWorld, @Nonnull Transform returnPoint) {
|
||||
Universe universe = Universe.get();
|
||||
Path path = universe.getPath();
|
||||
Path assetPath = getInstanceAssetPath(name);
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String worldKey = worldName;
|
||||
if (worldName == null) {
|
||||
worldKey = "instance-" + safeName(name) + "-" + uuid;
|
||||
}
|
||||
|
||||
Path worldPath = path.resolve("worlds").resolve(worldKey);
|
||||
String finalWorldKey = worldKey;
|
||||
return WorldConfig.load(assetPath.resolve("instance.bson"))
|
||||
.thenApplyAsync(
|
||||
SneakyThrow.sneakyFunction(
|
||||
config -> {
|
||||
config.setUuid(uuid);
|
||||
config.setDisplayName(WorldConfig.formatDisplayName(name));
|
||||
InstanceWorldConfig instanceConfig = InstanceWorldConfig.ensureAndGet(config);
|
||||
instanceConfig.setReturnPoint(
|
||||
new WorldReturnPoint(forWorld.getWorldConfig().getUuid(), returnPoint, instanceConfig.shouldPreventReconnection())
|
||||
);
|
||||
config.markChanged();
|
||||
long start = System.nanoTime();
|
||||
this.getLogger().at(Level.INFO).log("Copying instance files for %s to world %s", name, finalWorldKey);
|
||||
|
||||
try (Stream<Path> files = Files.walk(assetPath, FileUtil.DEFAULT_WALK_TREE_OPTIONS_ARRAY)) {
|
||||
files.forEach(SneakyThrow.sneakyConsumer(filePath -> {
|
||||
Path rel = assetPath.relativize(filePath);
|
||||
Path toPath = worldPath.resolve(rel.toString());
|
||||
if (Files.isDirectory(filePath)) {
|
||||
Files.createDirectories(toPath);
|
||||
} else {
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
Files.copy(filePath, toPath);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this.getLogger()
|
||||
.at(Level.INFO)
|
||||
.log("Completed instance files for %s to world %s in %s", name, finalWorldKey, FormatUtil.nanosToString(System.nanoTime() - start));
|
||||
return config;
|
||||
}
|
||||
)
|
||||
)
|
||||
.thenCompose(config -> universe.makeWorld(finalWorldKey, worldPath, config));
|
||||
}
|
||||
|
||||
public static void teleportPlayerToLoadingInstance(
|
||||
@Nonnull Ref<EntityStore> entityRef,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull CompletableFuture<World> worldFuture,
|
||||
@Nullable Transform overrideReturn
|
||||
) {
|
||||
World originalWorld = componentAccessor.getExternalData().getWorld();
|
||||
TransformComponent transformComponent = componentAccessor.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
Transform originalPosition = transformComponent.getTransform().clone();
|
||||
InstanceEntityConfig instanceEntityConfigComponent = componentAccessor.getComponent(entityRef, InstanceEntityConfig.getComponentType());
|
||||
if (instanceEntityConfigComponent == null) {
|
||||
instanceEntityConfigComponent = componentAccessor.addComponent(entityRef, InstanceEntityConfig.getComponentType());
|
||||
}
|
||||
|
||||
if (overrideReturn != null) {
|
||||
instanceEntityConfigComponent.setReturnPointOverride(new WorldReturnPoint(originalWorld.getWorldConfig().getUuid(), overrideReturn, false));
|
||||
} else {
|
||||
instanceEntityConfigComponent.setReturnPointOverride(null);
|
||||
}
|
||||
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(entityRef, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
UUIDComponent uuidComponent = componentAccessor.getComponent(entityRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID playerUUID = uuidComponent.getUuid();
|
||||
InstanceEntityConfig finalPlayerConfig = instanceEntityConfigComponent;
|
||||
CompletableFuture.runAsync(playerRefComponent::removeFromStore, originalWorld)
|
||||
.thenCombine(worldFuture.orTimeout(1L, TimeUnit.MINUTES), (ignored, world) -> (World) world)
|
||||
.thenCompose(world -> {
|
||||
ISpawnProvider spawnProvider = world.getWorldConfig().getSpawnProvider();
|
||||
Transform spawnPoint = spawnProvider != null ? spawnProvider.getSpawnPoint(world, playerUUID) : null;
|
||||
return world.addPlayer(playerRefComponent, spawnPoint, Boolean.TRUE, Boolean.FALSE);
|
||||
})
|
||||
.whenComplete((ret, ex) -> {
|
||||
if (ex != null) {
|
||||
get().getLogger().at(Level.SEVERE).withCause(ex).log("Failed to send %s to instance world", playerRefComponent.getUsername());
|
||||
finalPlayerConfig.setReturnPointOverride(null);
|
||||
}
|
||||
|
||||
if (ret == null) {
|
||||
if (originalWorld.isAlive()) {
|
||||
originalWorld.addPlayer(playerRefComponent, originalPosition, Boolean.TRUE, Boolean.FALSE);
|
||||
} else {
|
||||
World defaultWorld = Universe.get().getDefaultWorld();
|
||||
if (defaultWorld != null) {
|
||||
defaultWorld.addPlayer(playerRefComponent, null, Boolean.TRUE, Boolean.FALSE);
|
||||
} else {
|
||||
get().getLogger().at(Level.SEVERE).log("No fallback world for %s, disconnecting", playerRefComponent.getUsername());
|
||||
playerRefComponent.getPacketHandler().disconnect("Failed to teleport - no world available");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void teleportPlayerToInstance(
|
||||
@Nonnull Ref<EntityStore> playerRef,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull World targetWorld,
|
||||
@Nullable Transform overrideReturn
|
||||
) {
|
||||
World originalWorld = componentAccessor.getExternalData().getWorld();
|
||||
WorldConfig originalWorldConfig = originalWorld.getWorldConfig();
|
||||
if (overrideReturn != null) {
|
||||
InstanceEntityConfig instanceConfig = componentAccessor.ensureAndGetComponent(playerRef, InstanceEntityConfig.getComponentType());
|
||||
instanceConfig.setReturnPointOverride(new WorldReturnPoint(originalWorldConfig.getUuid(), overrideReturn, false));
|
||||
}
|
||||
|
||||
UUIDComponent uuidComponent = componentAccessor.getComponent(playerRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID playerUUID = uuidComponent.getUuid();
|
||||
WorldConfig targetWorldConfig = targetWorld.getWorldConfig();
|
||||
ISpawnProvider spawnProvider = targetWorldConfig.getSpawnProvider();
|
||||
if (spawnProvider == null) {
|
||||
throw new IllegalStateException("Spawn provider cannot be null when teleporting player to instance!");
|
||||
} else {
|
||||
Transform spawnTransform = spawnProvider.getSpawnPoint(targetWorld, playerUUID);
|
||||
Teleport teleportComponent = Teleport.createForPlayer(targetWorld, spawnTransform);
|
||||
componentAccessor.addComponent(playerRef, Teleport.getComponentType(), teleportComponent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void exitInstance(@Nonnull Ref<EntityStore> targetRef, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
InstanceEntityConfig entityConfig = componentAccessor.getComponent(targetRef, InstanceEntityConfig.getComponentType());
|
||||
WorldReturnPoint returnPoint = entityConfig != null ? entityConfig.getReturnPoint() : null;
|
||||
if (returnPoint == null) {
|
||||
WorldConfig config = world.getWorldConfig();
|
||||
InstanceWorldConfig instanceConfig = InstanceWorldConfig.get(config);
|
||||
returnPoint = instanceConfig != null ? instanceConfig.getReturnPoint() : null;
|
||||
if (returnPoint == null) {
|
||||
throw new IllegalArgumentException("Player is not in an instance");
|
||||
}
|
||||
}
|
||||
|
||||
Universe universe = Universe.get();
|
||||
World targetWorld = universe.getWorld(returnPoint.getWorld());
|
||||
if (targetWorld == null) {
|
||||
throw new IllegalArgumentException("Missing return world");
|
||||
} else {
|
||||
Teleport teleportComponent = Teleport.createForPlayer(targetWorld, returnPoint.getReturnPoint());
|
||||
componentAccessor.addComponent(targetRef, Teleport.getComponentType(), teleportComponent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void safeRemoveInstance(@Nonnull String worldName) {
|
||||
safeRemoveInstance(Universe.get().getWorld(worldName));
|
||||
}
|
||||
|
||||
public static void safeRemoveInstance(@Nonnull UUID worldUUID) {
|
||||
safeRemoveInstance(Universe.get().getWorld(worldUUID));
|
||||
}
|
||||
|
||||
public static void safeRemoveInstance(@Nullable World instanceWorld) {
|
||||
if (instanceWorld != null) {
|
||||
Store<ChunkStore> chunkStore = instanceWorld.getChunkStore().getStore();
|
||||
chunkStore.getResource(InstanceDataResource.getResourceType()).setHadPlayer(true);
|
||||
WorldConfig config = instanceWorld.getWorldConfig();
|
||||
InstanceWorldConfig instanceConfig = InstanceWorldConfig.get(config);
|
||||
if (instanceConfig != null) {
|
||||
instanceConfig.setRemovalConditions(WorldEmptyCondition.REMOVE_WHEN_EMPTY);
|
||||
}
|
||||
|
||||
config.markChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Path getInstanceAssetPath(@Nonnull String name) {
|
||||
for (AssetPack pack : AssetModule.get().getAssetPacks()) {
|
||||
Path path = pack.getRoot().resolve("Server").resolve("Instances").resolve(name);
|
||||
if (Files.exists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return AssetModule.get().getBaseAssetPack().getRoot().resolve("Server").resolve("Instances").resolve(name);
|
||||
}
|
||||
|
||||
public static boolean doesInstanceAssetExist(@Nonnull String name) {
|
||||
return Files.exists(getInstanceAssetPath(name).resolve("instance.bson"));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static CompletableFuture<World> loadInstanceAssetForEdit(@Nonnull String name) {
|
||||
Path path = getInstanceAssetPath(name);
|
||||
Universe universe = Universe.get();
|
||||
return WorldConfig.load(path.resolve("instance.bson")).thenCompose(config -> {
|
||||
config.setUuid(UUID.randomUUID());
|
||||
config.setSavingPlayers(false);
|
||||
config.setIsAllNPCFrozen(true);
|
||||
config.setTicking(false);
|
||||
config.setGameMode(GameMode.Creative);
|
||||
config.setDeleteOnRemove(false);
|
||||
InstanceWorldConfig.ensureAndGet(config).setRemovalConditions(RemovalCondition.EMPTY);
|
||||
config.markChanged();
|
||||
String worldName = "instance-edit-" + safeName(name);
|
||||
return universe.makeWorld(worldName, path, config);
|
||||
});
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<String> getInstanceAssets() {
|
||||
final List<String> instances = new ObjectArrayList<>();
|
||||
|
||||
for (AssetPack pack : AssetModule.get().getAssetPacks()) {
|
||||
final Path path = pack.getRoot().resolve("Server").resolve("Instances");
|
||||
if (Files.isDirectory(path)) {
|
||||
try {
|
||||
Files.walkFileTree(path, FileUtil.DEFAULT_WALK_TREE_OPTIONS_SET, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
|
||||
@Nonnull
|
||||
public FileVisitResult preVisitDirectory(@Nonnull Path dir, @Nonnull BasicFileAttributes attrs) {
|
||||
if (Files.exists(dir.resolve("instance.bson"))) {
|
||||
Path relative = path.relativize(dir);
|
||||
String name = relative.toString();
|
||||
instances.add(name);
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
} else {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException var6) {
|
||||
throw SneakyThrow.sneakyThrow(var6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
private static void onPlayerConnect(@Nonnull PlayerConnectEvent event) {
|
||||
Holder<EntityStore> holder = event.getHolder();
|
||||
Player playerComponent = holder.getComponent(Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerConfigData playerConfig = playerComponent.getPlayerConfigData();
|
||||
InstanceEntityConfig config = InstanceEntityConfig.ensureAndGet(holder);
|
||||
String lastWorldName = playerConfig.getWorld();
|
||||
World lastWorld = Universe.get().getWorld(lastWorldName);
|
||||
WorldReturnPoint fallbackWorld = config.getReturnPoint();
|
||||
if (fallbackWorld != null && (lastWorld == null || fallbackWorld.isReturnOnReconnect())) {
|
||||
lastWorld = Universe.get().getWorld(fallbackWorld.getWorld());
|
||||
if (lastWorld != null) {
|
||||
Transform transform = fallbackWorld.getReturnPoint();
|
||||
TransformComponent transformComponent = holder.ensureAndGetComponent(TransformComponent.getComponentType());
|
||||
transformComponent.setPosition(transform.getPosition());
|
||||
Vector3f rotationClone = transformComponent.getRotation().clone();
|
||||
rotationClone.setYaw(transform.getRotation().getYaw());
|
||||
transformComponent.setRotation(rotationClone);
|
||||
HeadRotation headRotationComponent = holder.ensureAndGetComponent(HeadRotation.getComponentType());
|
||||
headRotationComponent.teleportRotation(transform.getRotation());
|
||||
}
|
||||
} else if (lastWorld != null) {
|
||||
config.setReturnPointOverride(config.getReturnPoint());
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPlayerAddToWorld(@Nonnull AddPlayerToWorldEvent event) {
|
||||
Holder<EntityStore> holder = event.getHolder();
|
||||
InstanceWorldConfig worldConfig = InstanceWorldConfig.get(event.getWorld().getWorldConfig());
|
||||
if (worldConfig == null) {
|
||||
InstanceEntityConfig entityConfig = holder.getComponent(InstanceEntityConfig.getComponentType());
|
||||
if (entityConfig != null && entityConfig.getReturnPoint() != null) {
|
||||
entityConfig.setReturnPoint(null);
|
||||
}
|
||||
} else {
|
||||
InstanceEntityConfig entityConfig = InstanceEntityConfig.ensureAndGet(holder);
|
||||
if (entityConfig.getReturnPointOverride() == null) {
|
||||
entityConfig.setReturnPoint(worldConfig.getReturnPoint());
|
||||
} else {
|
||||
WorldReturnPoint override = entityConfig.getReturnPointOverride();
|
||||
override.setReturnOnReconnect(worldConfig.shouldPreventReconnection());
|
||||
entityConfig.setReturnPoint(override);
|
||||
entityConfig.setReturnPointOverride(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPlayerReady(@Nonnull PlayerReadyEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
World world = player.getWorld();
|
||||
if (world != null) {
|
||||
WorldConfig worldConfig = world.getWorldConfig();
|
||||
InstanceWorldConfig instanceWorldConfig = InstanceWorldConfig.get(worldConfig);
|
||||
if (instanceWorldConfig != null) {
|
||||
InstanceDiscoveryConfig discoveryConfig = instanceWorldConfig.getDiscovery();
|
||||
if (discoveryConfig != null) {
|
||||
PlayerConfigData playerConfigData = player.getPlayerConfigData();
|
||||
UUID instanceUuid = worldConfig.getUuid();
|
||||
if (discoveryConfig.alwaysDisplay() || !playerConfigData.getDiscoveredInstances().contains(instanceUuid)) {
|
||||
Set<UUID> discoveredInstances = new HashSet<>(playerConfigData.getDiscoveredInstances());
|
||||
discoveredInstances.add(instanceUuid);
|
||||
playerConfigData.setDiscoveredInstances(discoveredInstances);
|
||||
Ref<EntityStore> playerRef = event.getPlayerRef();
|
||||
if (playerRef.isValid()) {
|
||||
world.execute(() -> {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
showInstanceDiscovery(playerRef, store, instanceUuid, discoveryConfig);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void showInstanceDiscovery(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store, @Nonnull UUID instanceUuid, @Nonnull InstanceDiscoveryConfig discoveryConfig
|
||||
) {
|
||||
DiscoverInstanceEvent.Display discoverInstanceEvent = new DiscoverInstanceEvent.Display(instanceUuid, discoveryConfig.clone());
|
||||
store.invoke(ref, discoverInstanceEvent);
|
||||
discoveryConfig = discoverInstanceEvent.getDiscoveryConfig();
|
||||
if (!discoverInstanceEvent.isCancelled() && discoverInstanceEvent.shouldDisplay()) {
|
||||
PlayerRef playerRefComponent = store.getComponent(ref, PlayerRef.getComponentType());
|
||||
if (playerRefComponent != null) {
|
||||
String subtitleKey = discoveryConfig.getSubtitleKey();
|
||||
Message subtitle = subtitleKey != null ? Message.translation(subtitleKey) : Message.empty();
|
||||
EventTitleUtil.showEventTitleToPlayer(
|
||||
playerRefComponent,
|
||||
Message.translation(discoveryConfig.getTitleKey()),
|
||||
subtitle,
|
||||
discoveryConfig.isMajor(),
|
||||
discoveryConfig.getIcon(),
|
||||
discoveryConfig.getDuration(),
|
||||
discoveryConfig.getFadeInDuration(),
|
||||
discoveryConfig.getFadeOutDuration()
|
||||
);
|
||||
String discoverySoundEventId = discoveryConfig.getDiscoverySoundEventId();
|
||||
if (discoverySoundEventId != null) {
|
||||
int assetIndex = SoundEvent.getAssetMap().getIndex(discoverySoundEventId);
|
||||
if (assetIndex != Integer.MIN_VALUE) {
|
||||
SoundUtil.playSoundEvent2d(ref, assetIndex, SoundCategory.UI, store);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPlayerDrainFromWorld(@Nonnull DrainPlayerFromWorldEvent event) {
|
||||
InstanceEntityConfig config = InstanceEntityConfig.removeAndGet(event.getHolder());
|
||||
if (config != null) {
|
||||
WorldReturnPoint returnPoint = config.getReturnPoint();
|
||||
if (returnPoint != null) {
|
||||
World returnWorld = Universe.get().getWorld(returnPoint.getWorld());
|
||||
if (returnWorld != null) {
|
||||
event.setWorld(returnWorld);
|
||||
event.setTransform(returnPoint.getReturnPoint());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// HyFix: Fallback to default world when return world is null or unloaded
|
||||
// Prevents "Missing return world" crash that kicks players from the server
|
||||
World defaultWorld = Universe.get().getDefaultWorld();
|
||||
if (defaultWorld != null) {
|
||||
event.setWorld(defaultWorld);
|
||||
// Use default spawn point if available
|
||||
if (defaultWorld.getWorldConfig().getSpawnProvider() != null) {
|
||||
event.setTransform(defaultWorld.getWorldConfig().getSpawnProvider().getSpawnPoint(defaultWorld, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateSchema(@Nonnull GenerateSchemaEvent event) {
|
||||
ObjectSchema worldConfig = WorldConfig.CODEC.toSchema(event.getContext());
|
||||
Map<String, Schema> props = worldConfig.getProperties();
|
||||
props.put("UUID", Schema.anyOf(new StringSchema(), new ObjectSchema()));
|
||||
worldConfig.setTitle("Instance Configuration");
|
||||
worldConfig.setId("InstanceConfig.json");
|
||||
Schema.HytaleMetadata hytaleMetadata = worldConfig.getHytale();
|
||||
if (hytaleMetadata != null) {
|
||||
hytaleMetadata.setPath("Instances");
|
||||
hytaleMetadata.setExtension("instance.bson");
|
||||
hytaleMetadata.setUiEditorIgnore(Boolean.TRUE);
|
||||
}
|
||||
|
||||
event.addSchema("InstanceConfig.json", worldConfig);
|
||||
event.addSchemaLink("InstanceConfig", List.of("Instances/**/instance.bson"), ".bson");
|
||||
}
|
||||
|
||||
private void validateInstanceAssets(@Nonnull LoadAssetEvent event) {
|
||||
Path path = AssetModule.get().getBaseAssetPack().getRoot().resolve("Server").resolve("Instances");
|
||||
if (Options.getOptionSet().has(Options.VALIDATE_ASSETS) && Files.isDirectory(path) && !event.isShouldShutdown()) {
|
||||
StringBuilder errors = new StringBuilder();
|
||||
|
||||
for (String name : this.getInstanceAssets()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Path instancePath = getInstanceAssetPath(name);
|
||||
Universe universe = Universe.get();
|
||||
WorldConfig config = WorldConfig.load(instancePath.resolve("instance.bson")).join();
|
||||
IChunkStorageProvider storage = config.getChunkStorageProvider();
|
||||
config.setChunkStorageProvider(new MigrationChunkStorageProvider(new IChunkStorageProvider[]{storage}, EmptyChunkStorageProvider.INSTANCE));
|
||||
config.setResourceStorageProvider(EmptyResourceStorageProvider.INSTANCE);
|
||||
config.setUuid(UUID.randomUUID());
|
||||
config.setSavingPlayers(false);
|
||||
config.setIsAllNPCFrozen(true);
|
||||
config.setSavingConfig(false);
|
||||
config.setTicking(false);
|
||||
config.setGameMode(GameMode.Creative);
|
||||
config.setDeleteOnRemove(false);
|
||||
config.setCompassUpdating(false);
|
||||
InstanceWorldConfig.ensureAndGet(config).setRemovalConditions(RemovalCondition.EMPTY);
|
||||
config.markChanged();
|
||||
String worldName = "instance-validate-" + safeName(name);
|
||||
|
||||
try {
|
||||
World world = universe.makeWorld(worldName, instancePath, config, false).join();
|
||||
EnumSet<ValidationOption> options = EnumSet.of(ValidationOption.BLOCK_STATES, ValidationOption.BLOCKS);
|
||||
world.validate(sb, WorldValidationUtil.blockValidator(errors, options), options);
|
||||
} catch (Exception var18) {
|
||||
sb.append("\t").append(var18.getMessage());
|
||||
this.getLogger().at(Level.SEVERE).withCause(var18).log("Failed to validate: " + name);
|
||||
} finally {
|
||||
if (!sb.isEmpty()) {
|
||||
errors.append("Instance: ").append(name).append('\n').append((CharSequence) sb).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (universe.getWorld(worldName) != null) {
|
||||
universe.removeWorld(worldName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
this.getLogger().at(Level.SEVERE).log("Failed to validate instances:\n" + errors);
|
||||
event.failed(true, "failed to validate instances");
|
||||
}
|
||||
|
||||
HytaleLogger.getLogger()
|
||||
.at(Level.INFO)
|
||||
.log("Loading Instance assets phase completed! Boot time %s", FormatUtil.nanosToString(System.nanoTime() - event.getBootStart()));
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String safeName(@Nonnull String name) {
|
||||
return name.replace('/', '-');
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ResourceType<ChunkStore, InstanceDataResource> getInstanceDataResourceType() {
|
||||
return this.instanceDataResourceType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<EntityStore, InstanceEntityConfig> getInstanceEntityConfigComponentType() {
|
||||
return this.instanceEntityConfigComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<ChunkStore, InstanceBlock> getInstanceBlockComponentType() {
|
||||
return this.instanceBlockComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<ChunkStore, ConfigurableInstanceBlock> getConfigurableInstanceBlockComponentType() {
|
||||
return this.configurableInstanceBlockComponentType;
|
||||
}
|
||||
}
|
||||
@@ -69,17 +69,11 @@ public class ArchetypeChunk<ECS_TYPE> {
|
||||
|
||||
@Nullable
|
||||
public <T extends Component<ECS_TYPE>> T getComponent(int index, @Nonnull ComponentType<ECS_TYPE, T> componentType) {
|
||||
// HyFix #20: Handle stale entity references gracefully
|
||||
try {
|
||||
componentType.validateRegistry(this.store.getRegistry());
|
||||
if (index < 0 || index >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(index);
|
||||
} else {
|
||||
return (T) (!this.archetype.contains(componentType) ? null : this.components[componentType.getIndex()][index]);
|
||||
}
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
System.out.println("[HyFix] WARNING: getComponent() IndexOutOfBounds - returning null (stale entity ref)");
|
||||
return null;
|
||||
componentType.validateRegistry(this.store.getRegistry());
|
||||
if (index < 0 || index >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(index);
|
||||
} else {
|
||||
return (T) (!this.archetype.contains(componentType) ? null : this.components[componentType.getIndex()][index]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,30 +135,24 @@ public class ArchetypeChunk<ECS_TYPE> {
|
||||
|
||||
@Nonnull
|
||||
public Holder<ECS_TYPE> copySerializableEntity(@Nonnull ComponentRegistry.Data<ECS_TYPE> data, int entityIndex, @Nonnull Holder<ECS_TYPE> target) {
|
||||
// HyFix #29: Handle IndexOutOfBoundsException during chunk saving gracefully
|
||||
try {
|
||||
if (entityIndex >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(entityIndex);
|
||||
} else {
|
||||
Archetype<ECS_TYPE> serializableArchetype = this.archetype.getSerializableArchetype(data);
|
||||
Component<ECS_TYPE>[] entityComponents = target.ensureComponentsSize(serializableArchetype.length());
|
||||
if (entityIndex >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(entityIndex);
|
||||
} else {
|
||||
Archetype<ECS_TYPE> serializableArchetype = this.archetype.getSerializableArchetype(data);
|
||||
Component<ECS_TYPE>[] entityComponents = target.ensureComponentsSize(serializableArchetype.length());
|
||||
|
||||
for (int i = serializableArchetype.getMinIndex(); i < serializableArchetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) serializableArchetype.get(
|
||||
i
|
||||
);
|
||||
if (componentType != null) {
|
||||
int componentTypeIndex = componentType.getIndex();
|
||||
Component<ECS_TYPE> component = this.components[componentTypeIndex][entityIndex];
|
||||
entityComponents[componentTypeIndex] = component.cloneSerializable();
|
||||
}
|
||||
for (int i = serializableArchetype.getMinIndex(); i < serializableArchetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) serializableArchetype.get(
|
||||
i
|
||||
);
|
||||
if (componentType != null) {
|
||||
int componentTypeIndex = componentType.getIndex();
|
||||
Component<ECS_TYPE> component = this.components[componentTypeIndex][entityIndex];
|
||||
entityComponents[componentTypeIndex] = component.cloneSerializable();
|
||||
}
|
||||
|
||||
target.init(serializableArchetype, entityComponents);
|
||||
return target;
|
||||
}
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
System.out.println("[HyFix] WARNING: copySerializableEntity() IndexOutOfBounds - skipping (stale entity ref)");
|
||||
|
||||
target.init(serializableArchetype, entityComponents);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,6 +34,7 @@ public class Box implements Shape {
|
||||
})
|
||||
.build();
|
||||
public static final Box UNIT = new Box(Vector3d.ZERO, Vector3d.ALL_ONES);
|
||||
public static final Box ZERO = new Box(Vector3d.ZERO, Vector3d.ZERO);
|
||||
@Nonnull
|
||||
public final Vector3d min = new Vector3d();
|
||||
@Nonnull
|
||||
|
||||
@@ -1,544 +0,0 @@
|
||||
package com.hypixel.hytale.server.core;
|
||||
|
||||
import com.hypixel.hytale.assetstore.AssetPack;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.common.plugin.PluginManifest;
|
||||
import com.hypixel.hytale.common.thread.HytaleForkJoinThreadFactory;
|
||||
import com.hypixel.hytale.common.util.FormatUtil;
|
||||
import com.hypixel.hytale.common.util.GCUtil;
|
||||
import com.hypixel.hytale.common.util.HardwareUtil;
|
||||
import com.hypixel.hytale.common.util.NetworkUtil;
|
||||
import com.hypixel.hytale.common.util.java.ManifestUtil;
|
||||
import com.hypixel.hytale.event.EventBus;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.logger.backend.HytaleLogManager;
|
||||
import com.hypixel.hytale.logger.backend.HytaleLoggerBackend;
|
||||
import com.hypixel.hytale.math.util.MathUtil;
|
||||
import com.hypixel.hytale.math.util.TrigMathUtil;
|
||||
import com.hypixel.hytale.metrics.JVMMetrics;
|
||||
import com.hypixel.hytale.metrics.MetricsRegistry;
|
||||
import com.hypixel.hytale.plugin.early.EarlyPluginLoader;
|
||||
import com.hypixel.hytale.server.core.asset.AssetModule;
|
||||
import com.hypixel.hytale.server.core.asset.AssetRegistryLoader;
|
||||
import com.hypixel.hytale.server.core.asset.LoadAssetEvent;
|
||||
import com.hypixel.hytale.server.core.auth.ServerAuthManager;
|
||||
import com.hypixel.hytale.server.core.auth.SessionServiceClient;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandManager;
|
||||
import com.hypixel.hytale.server.core.console.ConsoleSender;
|
||||
import com.hypixel.hytale.server.core.event.events.BootEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.ShutdownEvent;
|
||||
import com.hypixel.hytale.server.core.io.ServerManager;
|
||||
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
|
||||
import com.hypixel.hytale.server.core.modules.singleplayer.SingleplayerModule;
|
||||
import com.hypixel.hytale.server.core.plugin.PluginBase;
|
||||
import com.hypixel.hytale.server.core.plugin.PluginClassLoader;
|
||||
import com.hypixel.hytale.server.core.plugin.PluginManager;
|
||||
import com.hypixel.hytale.server.core.plugin.PluginState;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import com.hypixel.hytale.server.core.universe.datastore.DataStoreProvider;
|
||||
import com.hypixel.hytale.server.core.universe.datastore.DiskDataStoreProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.update.UpdateModule;
|
||||
import com.hypixel.hytale.server.core.util.concurrent.ThreadUtil;
|
||||
import io.netty.handler.codec.quic.Quic;
|
||||
import io.sentry.Sentry;
|
||||
import io.sentry.SentryOptions;
|
||||
import io.sentry.protocol.Contexts;
|
||||
import io.sentry.protocol.OperatingSystem;
|
||||
import io.sentry.protocol.User;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import joptsimple.OptionSet;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class HytaleServer {
|
||||
public static final int DEFAULT_PORT = 5520;
|
||||
public static final ScheduledExecutorService SCHEDULED_EXECUTOR = Executors.newSingleThreadScheduledExecutor(ThreadUtil.daemon("Scheduler"));
|
||||
@Nonnull
|
||||
public static final MetricsRegistry<HytaleServer> METRICS_REGISTRY = new MetricsRegistry<HytaleServer>()
|
||||
.register("Time", server -> Instant.now(), Codec.INSTANT)
|
||||
.register("Boot", server -> server.boot, Codec.INSTANT)
|
||||
.register("BootStart", server -> server.bootStart, Codec.LONG)
|
||||
.register("Booting", server -> server.booting.get(), Codec.BOOLEAN)
|
||||
.register("ShutdownReason", server -> {
|
||||
ShutdownReason reason = server.shutdown.get();
|
||||
return reason == null ? null : reason.toString();
|
||||
}, Codec.STRING)
|
||||
.register("PluginManager", HytaleServer::getPluginManager, PluginManager.METRICS_REGISTRY)
|
||||
.register("Config", HytaleServer::getConfig, HytaleServerConfig.CODEC)
|
||||
.register("JVM", JVMMetrics.METRICS_REGISTRY);
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
private static HytaleServer instance;
|
||||
private final Semaphore aliveLock = new Semaphore(0);
|
||||
private final AtomicBoolean booting = new AtomicBoolean(false);
|
||||
private final AtomicBoolean booted = new AtomicBoolean(false);
|
||||
private final AtomicReference<ShutdownReason> shutdown = new AtomicReference<>();
|
||||
private final EventBus eventBus = new EventBus(Options.getOptionSet().has(Options.EVENT_DEBUG));
|
||||
private final PluginManager pluginManager = new PluginManager();
|
||||
private final CommandManager commandManager = new CommandManager();
|
||||
@Nonnull
|
||||
private final HytaleServerConfig hytaleServerConfig;
|
||||
private final Instant boot;
|
||||
private final long bootStart;
|
||||
private int pluginsProgress;
|
||||
|
||||
public HytaleServer() throws IOException {
|
||||
instance = this;
|
||||
Quic.ensureAvailability();
|
||||
HytaleLoggerBackend.setIndent(25);
|
||||
ThreadUtil.forceTimeHighResolution();
|
||||
ThreadUtil.createKeepAliveThread(this.aliveLock);
|
||||
this.boot = Instant.now();
|
||||
this.bootStart = System.nanoTime();
|
||||
LOGGER.at(Level.INFO).log("Starting HytaleServer");
|
||||
Constants.init();
|
||||
DataStoreProvider.CODEC.register("Disk", DiskDataStoreProvider.class, DiskDataStoreProvider.CODEC);
|
||||
LOGGER.at(Level.INFO).log("Loading config...");
|
||||
this.hytaleServerConfig = HytaleServerConfig.load();
|
||||
HytaleLoggerBackend.reloadLogLevels();
|
||||
System.setProperty("java.util.concurrent.ForkJoinPool.common.threadFactory", HytaleForkJoinThreadFactory.class.getName());
|
||||
OptionSet optionSet = Options.getOptionSet();
|
||||
LOGGER.at(Level.INFO).log("Authentication mode: %s", optionSet.valueOf(Options.AUTH_MODE));
|
||||
ServerAuthManager.getInstance().initialize();
|
||||
if (EarlyPluginLoader.hasTransformers()) {
|
||||
HytaleLogger.getLogger().at(Level.INFO).log("Early plugins loaded!! Disabling Sentry!!");
|
||||
} else if (!optionSet.has(Options.DISABLE_SENTRY)) {
|
||||
LOGGER.at(Level.INFO).log("Enabling Sentry");
|
||||
SentryOptions options = new SentryOptions();
|
||||
options.setDsn("https://6043a13c7b5c45b5c834b6d896fb378e@sentry.hytale.com/4");
|
||||
options.setRelease(ManifestUtil.getImplementationVersion());
|
||||
options.setDist(ManifestUtil.getImplementationRevisionId());
|
||||
options.setEnvironment("release");
|
||||
options.setTag("patchline", ManifestUtil.getPatchline());
|
||||
options.setServerName(NetworkUtil.getHostName());
|
||||
options.setBeforeSend((event, hint) -> {
|
||||
Throwable throwable = event.getThrowable();
|
||||
if (PluginClassLoader.isFromThirdPartyPlugin(throwable)) {
|
||||
return null;
|
||||
} else {
|
||||
Contexts contexts = event.getContexts();
|
||||
HashMap<String, Object> serverContext = new HashMap<>();
|
||||
serverContext.put("name", this.getServerName());
|
||||
serverContext.put("max-players", this.getConfig().getMaxPlayers());
|
||||
ServerManager serverManager = ServerManager.get();
|
||||
if (serverManager != null) {
|
||||
serverContext.put("listeners", serverManager.getListeners().stream().map(Object::toString).toList());
|
||||
}
|
||||
|
||||
contexts.put("server", serverContext);
|
||||
Universe universe = Universe.get();
|
||||
if (universe != null) {
|
||||
HashMap<String, Object> universeContext = new HashMap<>();
|
||||
universeContext.put("path", universe.getPath().toString());
|
||||
universeContext.put("player-count", universe.getPlayerCount());
|
||||
universeContext.put("worlds", universe.getWorlds().keySet().stream().toList());
|
||||
contexts.put("universe", universeContext);
|
||||
}
|
||||
|
||||
HashMap<String, Object> pluginsContext = new HashMap<>();
|
||||
|
||||
for (PluginBase plugin : this.pluginManager.getPlugins()) {
|
||||
PluginManifest manifestxx = plugin.getManifest();
|
||||
HashMap<String, Object> pluginInfo = new HashMap<>();
|
||||
pluginInfo.put("version", manifestxx.getVersion().toString());
|
||||
pluginInfo.put("state", plugin.getState().name());
|
||||
pluginsContext.put(plugin.getIdentifier().toString(), pluginInfo);
|
||||
}
|
||||
|
||||
contexts.put("plugins", pluginsContext);
|
||||
AssetModule assetModule = AssetModule.get();
|
||||
if (assetModule != null) {
|
||||
HashMap<String, Object> packsContext = new HashMap<>();
|
||||
|
||||
for (AssetPack pack : assetModule.getAssetPacks()) {
|
||||
HashMap<String, Object> packInfo = new HashMap<>();
|
||||
PluginManifest manifestx = pack.getManifest();
|
||||
if (manifestx != null && manifestx.getVersion() != null) {
|
||||
packInfo.put("version", manifestx.getVersion().toString());
|
||||
}
|
||||
|
||||
packInfo.put("immutable", pack.isImmutable());
|
||||
packsContext.put(pack.getName(), packInfo);
|
||||
}
|
||||
|
||||
contexts.put("packs", packsContext);
|
||||
}
|
||||
|
||||
User user = new User();
|
||||
HashMap<String, Object> unknown = new HashMap<>();
|
||||
user.setUnknown(unknown);
|
||||
UUID hardwareUUID = HardwareUtil.getUUID();
|
||||
if (hardwareUUID != null) {
|
||||
unknown.put("hardware-uuid", hardwareUUID.toString());
|
||||
}
|
||||
|
||||
ServerAuthManager authManager = ServerAuthManager.getInstance();
|
||||
unknown.put("auth-mode", authManager.getAuthMode().toString());
|
||||
SessionServiceClient.GameProfile profile = authManager.getSelectedProfile();
|
||||
if (profile != null) {
|
||||
user.setUsername(profile.username);
|
||||
user.setId(profile.uuid.toString());
|
||||
}
|
||||
|
||||
user.setIpAddress("{{auto}}");
|
||||
event.setUser(user);
|
||||
return event;
|
||||
}
|
||||
});
|
||||
Sentry.init(options);
|
||||
Sentry.configureScope(
|
||||
scope -> {
|
||||
UUID hardwareUUID = HardwareUtil.getUUID();
|
||||
if (hardwareUUID != null) {
|
||||
scope.setContexts("hardware", Map.of("uuid", hardwareUUID.toString()));
|
||||
}
|
||||
|
||||
OperatingSystem os = new OperatingSystem();
|
||||
os.setName(System.getProperty("os.name"));
|
||||
os.setVersion(System.getProperty("os.version"));
|
||||
scope.getContexts().setOperatingSystem(os);
|
||||
scope.setContexts(
|
||||
"build",
|
||||
Map.of(
|
||||
"version",
|
||||
String.valueOf(ManifestUtil.getImplementationVersion()),
|
||||
"revision-id",
|
||||
String.valueOf(ManifestUtil.getImplementationRevisionId()),
|
||||
"patchline",
|
||||
String.valueOf(ManifestUtil.getPatchline()),
|
||||
"environment",
|
||||
"release"
|
||||
)
|
||||
);
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
scope.setContexts(
|
||||
"singleplayer", Map.of("owner-uuid", String.valueOf(SingleplayerModule.getUuid()), "owner-name", SingleplayerModule.getUsername())
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
HytaleLogger.getLogger().setSentryClient(Sentry.getCurrentScopes());
|
||||
}
|
||||
|
||||
ServerAuthManager.getInstance().checkPendingFatalError();
|
||||
NettyUtil.init();
|
||||
float sin = TrigMathUtil.sin(0.0F);
|
||||
float atan2 = TrigMathUtil.atan2(0.0F, 0.0F);
|
||||
Thread shutdownHook = new Thread(() -> {
|
||||
if (this.shutdown.getAndSet(ShutdownReason.SIGINT) == null) {
|
||||
this.shutdown0(ShutdownReason.SIGINT);
|
||||
}
|
||||
}, "ShutdownHook");
|
||||
shutdownHook.setDaemon(false);
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||
AssetRegistryLoader.init();
|
||||
|
||||
for (PluginManifest manifest : Constants.CORE_PLUGINS) {
|
||||
this.pluginManager.registerCorePlugin(manifest);
|
||||
}
|
||||
|
||||
GCUtil.register(info -> {
|
||||
Universe universe = Universe.get();
|
||||
if (universe != null) {
|
||||
for (World world : universe.getWorlds().values()) {
|
||||
world.markGCHasRun();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.boot();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public EventBus getEventBus() {
|
||||
return this.eventBus;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PluginManager getPluginManager() {
|
||||
return this.pluginManager;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CommandManager getCommandManager() {
|
||||
return this.commandManager;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public HytaleServerConfig getConfig() {
|
||||
return this.hytaleServerConfig;
|
||||
}
|
||||
|
||||
private void boot() {
|
||||
if (!this.booting.getAndSet(true)) {
|
||||
LOGGER.at(Level.INFO)
|
||||
.log("Booting up HytaleServer - Version: " + ManifestUtil.getImplementationVersion() + ", Revision: " + ManifestUtil.getImplementationRevisionId());
|
||||
|
||||
try {
|
||||
this.pluginsProgress = 0;
|
||||
this.sendSingleplayerProgress();
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.at(Level.INFO).log("Setup phase...");
|
||||
this.commandManager.registerCommands();
|
||||
this.pluginManager.setup();
|
||||
ServerAuthManager.getInstance().initializeCredentialStore();
|
||||
LOGGER.at(Level.INFO).log("Setup phase completed! Boot time %s", FormatUtil.nanosToString(System.nanoTime() - this.bootStart));
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoadAssetEvent loadAssetEvent = get()
|
||||
.getEventBus()
|
||||
.<Void, LoadAssetEvent>dispatchFor(LoadAssetEvent.class)
|
||||
.dispatch(new LoadAssetEvent(this.bootStart));
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadAssetEvent.isShouldShutdown()) {
|
||||
List<String> reasons = loadAssetEvent.getReasons();
|
||||
String join = String.join(", ", reasons);
|
||||
LOGGER.at(Level.SEVERE).log("Asset validation FAILED with %d reason(s): %s", reasons.size(), join);
|
||||
this.shutdownServer(ShutdownReason.VALIDATE_ERROR.withMessage(join));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Options.getOptionSet().has(Options.SHUTDOWN_AFTER_VALIDATE)) {
|
||||
LOGGER.at(Level.INFO).log("Asset validation passed");
|
||||
this.shutdownServer(ShutdownReason.SHUTDOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pluginsProgress = 0;
|
||||
this.sendSingleplayerProgress();
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.at(Level.INFO).log("Starting plugin manager...");
|
||||
this.pluginManager.start();
|
||||
LOGGER.at(Level.INFO).log("Plugin manager started! Startup time so far: %s", FormatUtil.nanosToString(System.nanoTime() - this.bootStart));
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendSingleplayerSignal("-=|Enabled|0");
|
||||
} catch (Throwable var6) {
|
||||
LOGGER.at(Level.SEVERE).withCause(var6).log("Failed to boot HytaleServer!");
|
||||
Throwable t = var6;
|
||||
|
||||
while (t.getCause() != null) {
|
||||
t = t.getCause();
|
||||
}
|
||||
|
||||
this.shutdownServer(ShutdownReason.CRASH.withMessage("Failed to start server! " + t.getMessage()));
|
||||
}
|
||||
|
||||
if (this.hytaleServerConfig.consumeHasChanged()) {
|
||||
HytaleServerConfig.save(this.hytaleServerConfig).join();
|
||||
}
|
||||
|
||||
SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (this.hytaleServerConfig.consumeHasChanged()) {
|
||||
HytaleServerConfig.save(this.hytaleServerConfig).join();
|
||||
}
|
||||
} catch (Exception var2x) {
|
||||
LOGGER.at(Level.SEVERE).withCause(var2x).log("Failed to save server config!");
|
||||
}
|
||||
}, 1L, 1L, TimeUnit.MINUTES);
|
||||
LOGGER.at(Level.INFO).log("Getting Hytale Universe ready...");
|
||||
Universe.get().getUniverseReady().join();
|
||||
LOGGER.at(Level.INFO).log("Universe ready!");
|
||||
List<String> tags = new ObjectArrayList<>();
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
tags.add("Singleplayer");
|
||||
} else {
|
||||
tags.add("Multiplayer");
|
||||
}
|
||||
|
||||
if (Constants.FRESH_UNIVERSE) {
|
||||
tags.add("Fresh Universe");
|
||||
}
|
||||
|
||||
this.booted.set(true);
|
||||
ServerManager.get().waitForBindComplete();
|
||||
this.eventBus.dispatch(BootEvent.class);
|
||||
List<String> bootCommands = Options.getOptionSet().valuesOf(Options.BOOT_COMMAND);
|
||||
if (!bootCommands.isEmpty()) {
|
||||
CommandManager.get().handleCommands(ConsoleSender.INSTANCE, new ArrayDeque<>(bootCommands)).join();
|
||||
}
|
||||
|
||||
String border = "\u001b[0;32m===============================================================================================";
|
||||
LOGGER.at(Level.INFO).log("\u001b[0;32m===============================================================================================");
|
||||
LOGGER.at(Level.INFO)
|
||||
.log(
|
||||
"%s Hytale Server Booted! [%s] took %s",
|
||||
"\u001b[0;32m",
|
||||
String.join(", ", tags),
|
||||
FormatUtil.nanosToString(System.nanoTime() - this.bootStart)
|
||||
);
|
||||
LOGGER.at(Level.INFO).log("\u001b[0;32m===============================================================================================");
|
||||
UpdateModule updateModule = UpdateModule.get();
|
||||
if (updateModule != null) {
|
||||
updateModule.onServerReady();
|
||||
}
|
||||
|
||||
ServerAuthManager authManager = ServerAuthManager.getInstance();
|
||||
if (!authManager.isSingleplayer() && authManager.getAuthMode() == ServerAuthManager.AuthMode.NONE) {
|
||||
LOGGER.at(Level.WARNING).log("%sNo server tokens configured. Use /auth login to authenticate.", "\u001b[0;31m");
|
||||
}
|
||||
|
||||
this.sendSingleplayerSignal(">> Singleplayer Ready <<");
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdownServer() {
|
||||
this.shutdownServer(ShutdownReason.SHUTDOWN);
|
||||
}
|
||||
|
||||
public void shutdownServer(@Nonnull ShutdownReason reason) {
|
||||
Objects.requireNonNull(reason, "Server shutdown reason can't be null!");
|
||||
if (this.shutdown.getAndSet(reason) == null) {
|
||||
if (reason.getMessage() != null) {
|
||||
this.sendSingleplayerSignal("-=|Shutdown|" + reason.getMessage());
|
||||
}
|
||||
|
||||
Thread shutdownThread = new Thread(() -> this.shutdown0(reason), "ShutdownThread");
|
||||
shutdownThread.setDaemon(false);
|
||||
shutdownThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown0(@Nonnull ShutdownReason reason) {
|
||||
LOGGER.at(Level.INFO).log("Shutdown triggered!!!");
|
||||
|
||||
try {
|
||||
LOGGER.at(Level.INFO).log("Shutting down... %d '%s'", reason.getExitCode(), reason.getMessage());
|
||||
this.eventBus.dispatch(ShutdownEvent.class);
|
||||
this.pluginManager.shutdown();
|
||||
this.commandManager.shutdown();
|
||||
this.eventBus.shutdown();
|
||||
ServerAuthManager.getInstance().shutdown();
|
||||
LOGGER.at(Level.INFO).log("Saving config...");
|
||||
if (this.hytaleServerConfig.consumeHasChanged()) {
|
||||
HytaleServerConfig.save(this.hytaleServerConfig).join();
|
||||
}
|
||||
|
||||
LOGGER.at(Level.INFO).log("Shutdown completed!");
|
||||
} catch (Throwable var3) {
|
||||
LOGGER.at(Level.SEVERE).withCause(var3).log("Exception while shutting down:");
|
||||
}
|
||||
|
||||
this.aliveLock.release();
|
||||
HytaleLogManager.resetFinally();
|
||||
SCHEDULED_EXECUTOR.schedule(() -> {
|
||||
LOGGER.at(Level.SEVERE).log("Forcing shutdown!");
|
||||
Runtime.getRuntime().halt(reason.getExitCode());
|
||||
}, 3L, TimeUnit.SECONDS);
|
||||
if (reason != ShutdownReason.SIGINT) {
|
||||
System.exit(reason.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
public void doneSetup(PluginBase plugin) {
|
||||
this.pluginsProgress++;
|
||||
this.sendSingleplayerProgress();
|
||||
}
|
||||
|
||||
public void doneStart(PluginBase plugin) {
|
||||
this.pluginsProgress++;
|
||||
this.sendSingleplayerProgress();
|
||||
}
|
||||
|
||||
public void doneStop(PluginBase plugin) {
|
||||
this.pluginsProgress--;
|
||||
this.sendSingleplayerProgress();
|
||||
}
|
||||
|
||||
public void sendSingleplayerProgress() {
|
||||
List<PluginBase> plugins = this.pluginManager.getPlugins();
|
||||
if (this.shutdown.get() != null) {
|
||||
this.sendSingleplayerSignal("-=|Shutdown Modules|" + MathUtil.round((double) (plugins.size() - this.pluginsProgress) / plugins.size(), 2) * 100.0);
|
||||
} else if (this.pluginManager.getState() == PluginState.SETUP) {
|
||||
this.sendSingleplayerSignal("-=|Setup|" + MathUtil.round((double) this.pluginsProgress / plugins.size(), 2) * 100.0);
|
||||
} else if (this.pluginManager.getState() == PluginState.START) {
|
||||
this.sendSingleplayerSignal("-=|Starting|" + MathUtil.round((double) this.pluginsProgress / plugins.size(), 2) * 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
public String getServerName() {
|
||||
return this.getConfig().getServerName();
|
||||
}
|
||||
|
||||
public boolean isBooting() {
|
||||
return this.booting.get();
|
||||
}
|
||||
|
||||
public boolean isBooted() {
|
||||
return this.booted.get();
|
||||
}
|
||||
|
||||
public boolean isShuttingDown() {
|
||||
return this.shutdown.get() != null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Instant getBoot() {
|
||||
return this.boot;
|
||||
}
|
||||
|
||||
public long getBootStart() {
|
||||
return this.bootStart;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ShutdownReason getShutdownReason() {
|
||||
return this.shutdown.get();
|
||||
}
|
||||
|
||||
private void sendSingleplayerSignal(String message) {
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
HytaleLoggerBackend.rawLog(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void reportSingleplayerStatus(String message) {
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
HytaleLoggerBackend.rawLog("-=|" + message + "|0");
|
||||
}
|
||||
}
|
||||
|
||||
public void reportSaveProgress(@Nonnull World world, int saved, int total) {
|
||||
if (this.isShuttingDown()) {
|
||||
double progress = MathUtil.round((double) saved / total, 2) * 100.0;
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
this.sendSingleplayerSignal("-=|Saving world " + world.getName() + " chunks|" + progress);
|
||||
} else if (total < 10 || saved % (total / 10) == 0) {
|
||||
world.getLogger().at(Level.INFO).log("Saving chunks: %.0f%%", progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static HytaleServer get() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
@@ -1,851 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.entity;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.protocol.ForkedChainId;
|
||||
import com.hypixel.hytale.protocol.InteractionChainData;
|
||||
import com.hypixel.hytale.protocol.InteractionCooldown;
|
||||
import com.hypixel.hytale.protocol.InteractionState;
|
||||
import com.hypixel.hytale.protocol.InteractionSyncData;
|
||||
import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChain;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.CooldownHandler;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.Interaction;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.interaction.config.RootInteraction;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2LongOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectIterator;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class InteractionChain implements ChainSyncStorage {
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
private static final long NULL_FORK_ID = forkedIdToIndex(new ForkedChainId(-1, Integer.MAX_VALUE, null));
|
||||
private final InteractionType type;
|
||||
private InteractionType baseType;
|
||||
private final InteractionChainData chainData;
|
||||
private int chainId;
|
||||
private final ForkedChainId forkedChainId;
|
||||
private final ForkedChainId baseForkedChainId;
|
||||
private boolean predicted;
|
||||
private final InteractionContext context;
|
||||
@Nonnull
|
||||
private final Long2ObjectMap<InteractionChain> forkedChains = new Long2ObjectOpenHashMap<>();
|
||||
@Nonnull
|
||||
private final Long2ObjectMap<InteractionChain.TempChain> tempForkedChainData = new Long2ObjectOpenHashMap<>();
|
||||
@Nonnull
|
||||
private final Long2LongMap forkedChainsMap = new Long2LongOpenHashMap();
|
||||
@Nonnull
|
||||
private final List<InteractionChain> newForks = new ObjectArrayList<>();
|
||||
@Nonnull
|
||||
private final RootInteraction initialRootInteraction;
|
||||
private RootInteraction rootInteraction;
|
||||
private int operationCounter = 0;
|
||||
@Nonnull
|
||||
private final List<InteractionChain.CallState> callStack = new ObjectArrayList<>();
|
||||
private int simulatedCallStack = 0;
|
||||
private final boolean requiresClient;
|
||||
private int simulatedOperationCounter = 0;
|
||||
private RootInteraction simulatedRootInteraction;
|
||||
private int operationIndex = 0;
|
||||
private int operationIndexOffset = 0;
|
||||
private int clientOperationIndex = 0;
|
||||
@Nonnull
|
||||
private final List<InteractionEntry> interactions = new ObjectArrayList<>();
|
||||
@Nonnull
|
||||
private final List<InteractionSyncData> tempSyncData = new ObjectArrayList<>();
|
||||
private int tempSyncDataOffset = 0;
|
||||
private long timestamp = System.nanoTime();
|
||||
private long waitingForServerFinished;
|
||||
private long waitingForClientFinished;
|
||||
private InteractionState clientState = InteractionState.NotFinished;
|
||||
private InteractionState serverState = InteractionState.NotFinished;
|
||||
private InteractionState finalState = InteractionState.Finished;
|
||||
@Nullable
|
||||
private Runnable onCompletion;
|
||||
private boolean sentInitial;
|
||||
private boolean desynced;
|
||||
private float timeShift;
|
||||
private boolean firstRun = true;
|
||||
private boolean isFirstRun = true;
|
||||
private boolean completed = false;
|
||||
private boolean preTicked;
|
||||
boolean skipChainOnClick;
|
||||
|
||||
public InteractionChain(
|
||||
InteractionType type,
|
||||
InteractionContext context,
|
||||
InteractionChainData chainData,
|
||||
@Nonnull RootInteraction rootInteraction,
|
||||
@Nullable Runnable onCompletion,
|
||||
boolean requiresClient
|
||||
) {
|
||||
this(null, null, type, context, chainData, rootInteraction, onCompletion, requiresClient);
|
||||
}
|
||||
|
||||
public InteractionChain(
|
||||
ForkedChainId forkedChainId,
|
||||
ForkedChainId baseForkedChainId,
|
||||
InteractionType type,
|
||||
InteractionContext context,
|
||||
InteractionChainData chainData,
|
||||
@Nonnull RootInteraction rootInteraction,
|
||||
@Nullable Runnable onCompletion,
|
||||
boolean requiresClient
|
||||
) {
|
||||
this.type = this.baseType = type;
|
||||
this.context = context;
|
||||
this.chainData = chainData;
|
||||
this.forkedChainId = forkedChainId;
|
||||
this.baseForkedChainId = baseForkedChainId;
|
||||
this.onCompletion = onCompletion;
|
||||
this.initialRootInteraction = this.rootInteraction = this.simulatedRootInteraction = rootInteraction;
|
||||
this.requiresClient = requiresClient || rootInteraction.needsRemoteSync();
|
||||
this.forkedChainsMap.defaultReturnValue(NULL_FORK_ID);
|
||||
}
|
||||
|
||||
public InteractionType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public int getChainId() {
|
||||
return this.chainId;
|
||||
}
|
||||
|
||||
public ForkedChainId getForkedChainId() {
|
||||
return this.forkedChainId;
|
||||
}
|
||||
|
||||
public ForkedChainId getBaseForkedChainId() {
|
||||
return this.baseForkedChainId;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public RootInteraction getInitialRootInteraction() {
|
||||
return this.initialRootInteraction;
|
||||
}
|
||||
|
||||
public boolean isPredicted() {
|
||||
return this.predicted;
|
||||
}
|
||||
|
||||
public InteractionContext getContext() {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
public InteractionChainData getChainData() {
|
||||
return this.chainData;
|
||||
}
|
||||
|
||||
public InteractionState getServerState() {
|
||||
return this.serverState;
|
||||
}
|
||||
|
||||
public boolean requiresClient() {
|
||||
return this.requiresClient;
|
||||
}
|
||||
|
||||
public RootInteraction getRootInteraction() {
|
||||
return this.rootInteraction;
|
||||
}
|
||||
|
||||
public RootInteraction getSimulatedRootInteraction() {
|
||||
return this.simulatedRootInteraction;
|
||||
}
|
||||
|
||||
public int getOperationCounter() {
|
||||
return this.operationCounter;
|
||||
}
|
||||
|
||||
public void setOperationCounter(int operationCounter) {
|
||||
this.operationCounter = operationCounter;
|
||||
}
|
||||
|
||||
public int getSimulatedOperationCounter() {
|
||||
return this.simulatedOperationCounter;
|
||||
}
|
||||
|
||||
public void setSimulatedOperationCounter(int simulatedOperationCounter) {
|
||||
this.simulatedOperationCounter = simulatedOperationCounter;
|
||||
}
|
||||
|
||||
public boolean wasPreTicked() {
|
||||
return this.preTicked;
|
||||
}
|
||||
|
||||
public void setPreTicked(boolean preTicked) {
|
||||
this.preTicked = preTicked;
|
||||
}
|
||||
|
||||
public int getOperationIndex() {
|
||||
return this.operationIndex;
|
||||
}
|
||||
|
||||
public void nextOperationIndex() {
|
||||
this.operationIndex++;
|
||||
this.clientOperationIndex++;
|
||||
}
|
||||
|
||||
public int getClientOperationIndex() {
|
||||
return this.clientOperationIndex;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InteractionChain findForkedChain(@Nonnull ForkedChainId chainId, @Nullable InteractionChainData data) {
|
||||
long id = forkedIdToIndex(chainId);
|
||||
long altId = this.forkedChainsMap.get(id);
|
||||
if (altId != NULL_FORK_ID) {
|
||||
id = altId;
|
||||
}
|
||||
|
||||
InteractionChain chain = this.forkedChains.get(id);
|
||||
if (chain == null && chainId.subIndex < 0 && data != null) {
|
||||
InteractionEntry entry = this.getInteraction(chainId.entryIndex);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
} else {
|
||||
int rootId = entry.getServerState().rootInteraction;
|
||||
int opCounter = entry.getServerState().operationCounter;
|
||||
RootInteraction root = RootInteraction.getAssetMap().getAsset(rootId);
|
||||
if (root.getOperation(opCounter).getInnerOperation() instanceof Interaction interaction) {
|
||||
this.context.initEntry(this, entry, null);
|
||||
chain = interaction.mapForkChain(this.context, data);
|
||||
this.context.deinitEntry(this, entry, null);
|
||||
if (chain != null) {
|
||||
this.forkedChainsMap.put(id, forkedIdToIndex(chain.getBaseForkedChainId()));
|
||||
}
|
||||
|
||||
return chain;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
|
||||
public InteractionChain getForkedChain(@Nonnull ForkedChainId chainId) {
|
||||
long id = forkedIdToIndex(chainId);
|
||||
if (chainId.subIndex < 0) {
|
||||
long altId = this.forkedChainsMap.get(id);
|
||||
if (altId != NULL_FORK_ID) {
|
||||
id = altId;
|
||||
}
|
||||
}
|
||||
|
||||
return this.forkedChains.get(id);
|
||||
}
|
||||
|
||||
public void putForkedChain(@Nonnull ForkedChainId chainId, @Nonnull InteractionChain chain) {
|
||||
this.newForks.add(chain);
|
||||
this.forkedChains.put(forkedIdToIndex(chainId), chain);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InteractionChain.TempChain getTempForkedChain(@Nonnull ForkedChainId chainId) {
|
||||
InteractionEntry entry = this.getInteraction(chainId.entryIndex);
|
||||
if (entry != null) {
|
||||
if (chainId.subIndex < entry.getNextForkId()) {
|
||||
return null;
|
||||
}
|
||||
} else if (chainId.entryIndex < this.operationIndexOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.tempForkedChainData.computeIfAbsent(forkedIdToIndex(chainId), i -> new InteractionChain.TempChain());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
InteractionChain.TempChain removeTempForkedChain(@Nonnull ForkedChainId chainId, InteractionChain forkChain) {
|
||||
long id = forkedIdToIndex(chainId);
|
||||
long altId = this.forkedChainsMap.get(id);
|
||||
if (altId != NULL_FORK_ID) {
|
||||
id = altId;
|
||||
}
|
||||
|
||||
InteractionChain.TempChain found = this.tempForkedChainData.remove(id);
|
||||
if (found != null) {
|
||||
return found;
|
||||
} else {
|
||||
InteractionEntry iEntry = this.context.getEntry();
|
||||
RootInteraction root = RootInteraction.getAssetMap().getAsset(iEntry.getState().rootInteraction);
|
||||
if (root.getOperation(iEntry.getState().operationCounter).getInnerOperation() instanceof Interaction interaction) {
|
||||
ObjectIterator<Entry<InteractionChain.TempChain>> it = Long2ObjectMaps.fastIterator(this.getTempForkedChainData());
|
||||
|
||||
while (it.hasNext()) {
|
||||
Entry<InteractionChain.TempChain> entry = it.next();
|
||||
InteractionChain.TempChain tempChain = entry.getValue();
|
||||
if (tempChain.baseForkedChainId != null) {
|
||||
int entryId = tempChain.baseForkedChainId.entryIndex;
|
||||
if (entryId == iEntry.getIndex()) {
|
||||
InteractionChain chain = interaction.mapForkChain(this.getContext(), tempChain.chainData);
|
||||
if (chain != null) {
|
||||
this.forkedChainsMap.put(forkedIdToIndex(tempChain.baseForkedChainId), forkedIdToIndex(chain.getBaseForkedChainId()));
|
||||
}
|
||||
|
||||
if (chain == forkChain) {
|
||||
it.remove();
|
||||
return tempChain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSentInitial() {
|
||||
return this.sentInitial;
|
||||
}
|
||||
|
||||
public void setSentInitial(boolean sentInitial) {
|
||||
this.sentInitial = sentInitial;
|
||||
}
|
||||
|
||||
public float getTimeShift() {
|
||||
return this.timeShift;
|
||||
}
|
||||
|
||||
public void setTimeShift(float timeShift) {
|
||||
this.timeShift = timeShift;
|
||||
}
|
||||
|
||||
public boolean consumeFirstRun() {
|
||||
this.isFirstRun = this.firstRun;
|
||||
this.firstRun = false;
|
||||
return this.isFirstRun;
|
||||
}
|
||||
|
||||
public boolean isFirstRun() {
|
||||
return this.isFirstRun;
|
||||
}
|
||||
|
||||
public void setFirstRun(boolean firstRun) {
|
||||
this.isFirstRun = firstRun;
|
||||
}
|
||||
|
||||
public int getCallDepth() {
|
||||
return this.callStack.size();
|
||||
}
|
||||
|
||||
public int getSimulatedCallDepth() {
|
||||
return this.simulatedCallStack;
|
||||
}
|
||||
|
||||
public void pushRoot(RootInteraction nextInteraction, boolean simulate) {
|
||||
if (simulate) {
|
||||
this.simulatedRootInteraction = nextInteraction;
|
||||
this.simulatedOperationCounter = 0;
|
||||
this.simulatedCallStack++;
|
||||
} else {
|
||||
this.callStack.add(new InteractionChain.CallState(this.rootInteraction, this.operationCounter));
|
||||
this.operationCounter = 0;
|
||||
this.rootInteraction = nextInteraction;
|
||||
}
|
||||
}
|
||||
|
||||
public void popRoot() {
|
||||
InteractionChain.CallState state = this.callStack.removeLast();
|
||||
this.rootInteraction = state.rootInteraction;
|
||||
this.operationCounter = state.operationCounter + 1;
|
||||
this.simulatedRootInteraction = this.rootInteraction;
|
||||
this.simulatedOperationCounter = this.operationCounter;
|
||||
this.simulatedCallStack--;
|
||||
}
|
||||
|
||||
public float getTimeInSeconds() {
|
||||
if (this.timestamp == 0L) {
|
||||
return 0.0F;
|
||||
} else {
|
||||
long diff = System.nanoTime() - this.timestamp;
|
||||
return (float) diff / 1.0E9F;
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnCompletion(Runnable onCompletion) {
|
||||
this.onCompletion = onCompletion;
|
||||
}
|
||||
|
||||
void onCompletion(CooldownHandler cooldownHandler, boolean isRemote) {
|
||||
if (!this.completed) {
|
||||
this.completed = true;
|
||||
if (this.onCompletion != null) {
|
||||
this.onCompletion.run();
|
||||
this.onCompletion = null;
|
||||
}
|
||||
|
||||
if (isRemote) {
|
||||
InteractionCooldown cooldown = this.initialRootInteraction.getCooldown();
|
||||
String cooldownId = this.initialRootInteraction.getId();
|
||||
if (cooldown != null && cooldown.cooldownId != null) {
|
||||
cooldownId = cooldown.cooldownId;
|
||||
}
|
||||
|
||||
CooldownHandler.Cooldown cooldownTracker = cooldownHandler.getCooldown(cooldownId);
|
||||
if (cooldownTracker != null) {
|
||||
cooldownTracker.tick(0.016666668F);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateServerState() {
|
||||
if (this.serverState == InteractionState.NotFinished) {
|
||||
if (this.operationCounter >= this.rootInteraction.getOperationMax()) {
|
||||
this.serverState = this.finalState;
|
||||
} else {
|
||||
InteractionEntry entry = this.getOrCreateInteractionEntry(this.operationIndex);
|
||||
|
||||
this.serverState = switch (entry.getServerState().state) {
|
||||
case NotFinished, Finished -> InteractionState.NotFinished;
|
||||
default -> InteractionState.Failed;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateSimulatedState() {
|
||||
if (this.clientState == InteractionState.NotFinished) {
|
||||
if (this.simulatedOperationCounter >= this.rootInteraction.getOperationMax()) {
|
||||
this.clientState = this.finalState;
|
||||
} else {
|
||||
InteractionEntry entry = this.getOrCreateInteractionEntry(this.clientOperationIndex);
|
||||
|
||||
this.clientState = switch (entry.getSimulationState().state) {
|
||||
case NotFinished, Finished -> InteractionState.NotFinished;
|
||||
default -> InteractionState.Failed;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionState getClientState() {
|
||||
return this.clientState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientState(InteractionState state) {
|
||||
this.clientState = state;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public InteractionEntry getOrCreateInteractionEntry(int index) {
|
||||
int oIndex = index - this.operationIndexOffset;
|
||||
if (oIndex < 0) {
|
||||
throw new IllegalArgumentException("Trying to access removed interaction entry");
|
||||
} else {
|
||||
InteractionEntry entry = oIndex < this.interactions.size() ? this.interactions.get(oIndex) : null;
|
||||
if (entry == null) {
|
||||
if (oIndex != this.interactions.size()) {
|
||||
throw new IllegalArgumentException("Trying to add interaction entry at a weird location: " + oIndex + " " + this.interactions.size());
|
||||
}
|
||||
|
||||
entry = new InteractionEntry(index, this.operationCounter, RootInteraction.getRootInteractionIdOrUnknown(this.rootInteraction.getId()));
|
||||
this.interactions.add(entry);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InteractionEntry getInteraction(int index) {
|
||||
index -= this.operationIndexOffset;
|
||||
return index >= 0 && index < this.interactions.size() ? this.interactions.get(index) : null;
|
||||
}
|
||||
|
||||
// HyFix #40: Wrap in try-catch to handle out-of-order removal gracefully
|
||||
public void removeInteractionEntry(@Nonnull InteractionManager interactionManager, int index) {
|
||||
try {
|
||||
int oIndex = index - this.operationIndexOffset;
|
||||
if (oIndex != 0) {
|
||||
throw new IllegalArgumentException("Trying to remove out of order");
|
||||
} else {
|
||||
InteractionEntry entry = this.interactions.remove(oIndex);
|
||||
this.operationIndexOffset++;
|
||||
this.tempForkedChainData.values().removeIf(fork -> {
|
||||
if (fork.baseForkedChainId.entryIndex != entry.getIndex()) {
|
||||
return false;
|
||||
} else {
|
||||
interactionManager.sendCancelPacket(this.getChainId(), fork.forkedChainId);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.out.println("[HyFix] WARNING: Suppressed out-of-order removal in InteractionChain (Issue #40)");
|
||||
}
|
||||
}
|
||||
|
||||
// HyFix: Expand buffer backwards instead of dropping data when index < offset
|
||||
@Override
|
||||
public void putInteractionSyncData(int index, InteractionSyncData data) {
|
||||
int adjustedIndex = index - this.tempSyncDataOffset;
|
||||
|
||||
// HyFix: Handle negative indices by expanding buffer backwards
|
||||
if (adjustedIndex < 0) {
|
||||
int expansion = -adjustedIndex;
|
||||
for (int i = 0; i < expansion; i++) {
|
||||
this.tempSyncData.add(0, null); // prepend nulls
|
||||
}
|
||||
this.tempSyncDataOffset = this.tempSyncDataOffset + adjustedIndex; // shift offset down
|
||||
adjustedIndex = 0;
|
||||
}
|
||||
|
||||
if (adjustedIndex < this.tempSyncData.size()) {
|
||||
this.tempSyncData.set(adjustedIndex, data);
|
||||
} else if (adjustedIndex == this.tempSyncData.size()) {
|
||||
this.tempSyncData.add(data);
|
||||
} else {
|
||||
LOGGER.at(Level.WARNING).log("Temp sync data sent out of order: " + adjustedIndex + " " + this.tempSyncData.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearInteractionSyncData(int operationIndex) {
|
||||
int tempIdx = operationIndex - this.tempSyncDataOffset;
|
||||
if (!this.tempSyncData.isEmpty()) {
|
||||
for (int end = this.tempSyncData.size() - 1; end >= tempIdx && end >= 0; end--) {
|
||||
this.tempSyncData.remove(end);
|
||||
}
|
||||
}
|
||||
|
||||
int idx = operationIndex - this.operationIndexOffset;
|
||||
|
||||
for (int i = Math.max(idx, 0); i < this.interactions.size(); i++) {
|
||||
this.interactions.get(i).setClientState(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InteractionSyncData removeInteractionSyncData(int index) {
|
||||
index -= this.tempSyncDataOffset;
|
||||
if (index != 0) {
|
||||
return null;
|
||||
} else if (this.tempSyncData.isEmpty()) {
|
||||
return null;
|
||||
} else if (this.tempSyncData.get(index) == null) {
|
||||
return null;
|
||||
} else {
|
||||
this.tempSyncDataOffset++;
|
||||
return this.tempSyncData.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
// HyFix: Handle sync gaps gracefully instead of throwing
|
||||
@Override
|
||||
public void updateSyncPosition(int index) {
|
||||
// HyFix: Accept any index >= offset, not just == offset
|
||||
if (index >= this.tempSyncDataOffset) {
|
||||
this.tempSyncDataOffset = index + 1;
|
||||
}
|
||||
// index < offset is silently ignored (already processed)
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSyncDataOutOfOrder(int index) {
|
||||
return index > this.tempSyncDataOffset + this.tempSyncData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncFork(@Nonnull Ref<EntityStore> ref, @Nonnull InteractionManager manager, @Nonnull SyncInteractionChain packet) {
|
||||
ForkedChainId baseId = packet.forkedId;
|
||||
|
||||
while (baseId.forkedId != null) {
|
||||
baseId = baseId.forkedId;
|
||||
}
|
||||
|
||||
InteractionChain fork = this.findForkedChain(baseId, packet.data);
|
||||
if (fork != null) {
|
||||
manager.sync(ref, fork, packet);
|
||||
} else {
|
||||
InteractionChain.TempChain temp = this.getTempForkedChain(baseId);
|
||||
if (temp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
temp.setForkedChainId(packet.forkedId);
|
||||
temp.setBaseForkedChainId(baseId);
|
||||
temp.setChainData(packet.data);
|
||||
manager.sync(ref, temp, packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void copyTempFrom(@Nonnull InteractionChain.TempChain temp) {
|
||||
this.setClientState(temp.clientState);
|
||||
this.tempSyncData.addAll(temp.tempSyncData);
|
||||
this.getTempForkedChainData().putAll(temp.tempForkedChainData);
|
||||
}
|
||||
|
||||
private static long forkedIdToIndex(@Nonnull ForkedChainId chainId) {
|
||||
return (long) chainId.entryIndex << 32 | chainId.subIndex & 4294967295L;
|
||||
}
|
||||
|
||||
public void setChainId(int chainId) {
|
||||
this.chainId = chainId;
|
||||
}
|
||||
|
||||
public InteractionType getBaseType() {
|
||||
return this.baseType;
|
||||
}
|
||||
|
||||
public void setBaseType(InteractionType baseType) {
|
||||
this.baseType = baseType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Long2ObjectMap<InteractionChain> getForkedChains() {
|
||||
return this.forkedChains;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Long2ObjectMap<InteractionChain.TempChain> getTempForkedChainData() {
|
||||
return this.tempForkedChainData;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public long getWaitingForServerFinished() {
|
||||
return this.waitingForServerFinished;
|
||||
}
|
||||
|
||||
public void setWaitingForServerFinished(long waitingForServerFinished) {
|
||||
this.waitingForServerFinished = waitingForServerFinished;
|
||||
}
|
||||
|
||||
public long getWaitingForClientFinished() {
|
||||
return this.waitingForClientFinished;
|
||||
}
|
||||
|
||||
public void setWaitingForClientFinished(long waitingForClientFinished) {
|
||||
this.waitingForClientFinished = waitingForClientFinished;
|
||||
}
|
||||
|
||||
public void setServerState(InteractionState serverState) {
|
||||
this.serverState = serverState;
|
||||
}
|
||||
|
||||
public InteractionState getFinalState() {
|
||||
return this.finalState;
|
||||
}
|
||||
|
||||
public void setFinalState(InteractionState finalState) {
|
||||
this.finalState = finalState;
|
||||
}
|
||||
|
||||
void setPredicted(boolean predicted) {
|
||||
this.predicted = predicted;
|
||||
}
|
||||
|
||||
public void flagDesync() {
|
||||
this.desynced = true;
|
||||
this.forkedChains.forEach((k, c) -> c.flagDesync());
|
||||
}
|
||||
|
||||
public boolean isDesynced() {
|
||||
return this.desynced;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<InteractionChain> getNewForks() {
|
||||
return this.newForks;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InteractionChain{type="
|
||||
+ this.type
|
||||
+ ", chainData="
|
||||
+ this.chainData
|
||||
+ ", chainId="
|
||||
+ this.chainId
|
||||
+ ", forkedChainId="
|
||||
+ this.forkedChainId
|
||||
+ ", predicted="
|
||||
+ this.predicted
|
||||
+ ", context="
|
||||
+ this.context
|
||||
+ ", forkedChains="
|
||||
+ this.forkedChains
|
||||
+ ", tempForkedChainData="
|
||||
+ this.tempForkedChainData
|
||||
+ ", initialRootInteraction="
|
||||
+ this.initialRootInteraction
|
||||
+ ", rootInteraction="
|
||||
+ this.rootInteraction
|
||||
+ ", operationCounter="
|
||||
+ this.operationCounter
|
||||
+ ", callStack="
|
||||
+ this.callStack
|
||||
+ ", simulatedCallStack="
|
||||
+ this.simulatedCallStack
|
||||
+ ", requiresClient="
|
||||
+ this.requiresClient
|
||||
+ ", simulatedOperationCounter="
|
||||
+ this.simulatedOperationCounter
|
||||
+ ", simulatedRootInteraction="
|
||||
+ this.simulatedRootInteraction
|
||||
+ ", operationIndex="
|
||||
+ this.operationIndex
|
||||
+ ", operationIndexOffset="
|
||||
+ this.operationIndexOffset
|
||||
+ ", clientOperationIndex="
|
||||
+ this.clientOperationIndex
|
||||
+ ", interactions="
|
||||
+ this.interactions
|
||||
+ ", tempSyncData="
|
||||
+ this.tempSyncData
|
||||
+ ", tempSyncDataOffset="
|
||||
+ this.tempSyncDataOffset
|
||||
+ ", timestamp="
|
||||
+ this.timestamp
|
||||
+ ", waitingForServerFinished="
|
||||
+ this.waitingForServerFinished
|
||||
+ ", waitingForClientFinished="
|
||||
+ this.waitingForClientFinished
|
||||
+ ", clientState="
|
||||
+ this.clientState
|
||||
+ ", serverState="
|
||||
+ this.serverState
|
||||
+ ", onCompletion="
|
||||
+ this.onCompletion
|
||||
+ ", sentInitial="
|
||||
+ this.sentInitial
|
||||
+ ", desynced="
|
||||
+ this.desynced
|
||||
+ ", timeShift="
|
||||
+ this.timeShift
|
||||
+ ", firstRun="
|
||||
+ this.firstRun
|
||||
+ ", skipChainOnClick="
|
||||
+ this.skipChainOnClick
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private record CallState(RootInteraction rootInteraction, int operationCounter) {
|
||||
}
|
||||
|
||||
static class TempChain implements ChainSyncStorage {
|
||||
final Long2ObjectMap<InteractionChain.TempChain> tempForkedChainData = new Long2ObjectOpenHashMap<>();
|
||||
final List<InteractionSyncData> tempSyncData = new ObjectArrayList<>();
|
||||
ForkedChainId forkedChainId;
|
||||
InteractionState clientState = InteractionState.NotFinished;
|
||||
ForkedChainId baseForkedChainId;
|
||||
InteractionChainData chainData;
|
||||
|
||||
TempChain() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public InteractionChain.TempChain getOrCreateTempForkedChain(@Nonnull ForkedChainId chainId) {
|
||||
return this.tempForkedChainData.computeIfAbsent(InteractionChain.forkedIdToIndex(chainId), i -> new InteractionChain.TempChain());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionState getClientState() {
|
||||
return this.clientState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientState(InteractionState state) {
|
||||
this.clientState = state;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InteractionEntry getInteraction(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putInteractionSyncData(int index, InteractionSyncData data) {
|
||||
if (index < this.tempSyncData.size()) {
|
||||
this.tempSyncData.set(index, data);
|
||||
} else {
|
||||
if (index != this.tempSyncData.size()) {
|
||||
throw new IllegalArgumentException("Temp sync data sent out of order: " + index + " " + this.tempSyncData.size());
|
||||
}
|
||||
|
||||
this.tempSyncData.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSyncPosition(int index) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSyncDataOutOfOrder(int index) {
|
||||
return index > this.tempSyncData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncFork(@Nonnull Ref<EntityStore> ref, @Nonnull InteractionManager manager, @Nonnull SyncInteractionChain packet) {
|
||||
ForkedChainId baseId = packet.forkedId;
|
||||
|
||||
while (baseId.forkedId != null) {
|
||||
baseId = baseId.forkedId;
|
||||
}
|
||||
|
||||
InteractionChain.TempChain temp = this.getOrCreateTempForkedChain(baseId);
|
||||
temp.setForkedChainId(packet.forkedId);
|
||||
temp.setBaseForkedChainId(baseId);
|
||||
temp.setChainData(packet.data);
|
||||
manager.sync(ref, temp, packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearInteractionSyncData(int index) {
|
||||
for (int end = this.tempSyncData.size() - 1; end >= index; end--) {
|
||||
this.tempSyncData.remove(end);
|
||||
}
|
||||
}
|
||||
|
||||
public InteractionChainData getChainData() {
|
||||
return this.chainData;
|
||||
}
|
||||
|
||||
public void setChainData(InteractionChainData chainData) {
|
||||
this.chainData = chainData;
|
||||
}
|
||||
|
||||
public ForkedChainId getBaseForkedChainId() {
|
||||
return this.baseForkedChainId;
|
||||
}
|
||||
|
||||
public void setBaseForkedChainId(ForkedChainId baseForkedChainId) {
|
||||
this.baseForkedChainId = baseForkedChainId;
|
||||
}
|
||||
|
||||
public void setForkedChainId(ForkedChainId forkedChainId) {
|
||||
this.forkedChainId = forkedChainId;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TempChain{tempForkedChainData=" + this.tempForkedChainData + ", tempSyncData=" + this.tempSyncData + ", clientState=" + this.clientState + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,570 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.io;
|
||||
|
||||
import com.google.common.flogger.LazyArgs;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.codecs.EnumCodec;
|
||||
import com.hypixel.hytale.common.util.FormatUtil;
|
||||
import com.hypixel.hytale.common.util.NetworkUtil;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.metrics.MetricsRegistry;
|
||||
import com.hypixel.hytale.metrics.metric.HistoricMetric;
|
||||
import com.hypixel.hytale.metrics.metric.Metric;
|
||||
import com.hypixel.hytale.protocol.CachedPacket;
|
||||
import com.hypixel.hytale.protocol.Packet;
|
||||
import com.hypixel.hytale.protocol.io.PacketStatsRecorder;
|
||||
import com.hypixel.hytale.protocol.io.netty.ProtocolUtil;
|
||||
import com.hypixel.hytale.protocol.packets.connection.Disconnect;
|
||||
import com.hypixel.hytale.protocol.packets.connection.DisconnectType;
|
||||
import com.hypixel.hytale.protocol.packets.connection.Ping;
|
||||
import com.hypixel.hytale.protocol.packets.connection.Pong;
|
||||
import com.hypixel.hytale.protocol.packets.connection.PongType;
|
||||
import com.hypixel.hytale.server.core.auth.PlayerAuthentication;
|
||||
import com.hypixel.hytale.server.core.io.adapter.PacketAdapters;
|
||||
import com.hypixel.hytale.server.core.io.handlers.login.AuthenticationPacketHandler;
|
||||
import com.hypixel.hytale.server.core.io.handlers.login.PasswordPacketHandler;
|
||||
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
|
||||
import com.hypixel.hytale.server.core.modules.time.WorldTimeResource;
|
||||
import com.hypixel.hytale.server.core.receiver.IPacketReceiver;
|
||||
import io.netty.channel.Channel;
|
||||
import io.netty.channel.ChannelHandlerContext;
|
||||
import io.netty.channel.local.LocalAddress;
|
||||
import io.netty.channel.unix.DomainSocketAddress;
|
||||
import io.netty.handler.codec.quic.QuicStreamChannel;
|
||||
import io.netty.util.Attribute;
|
||||
import io.netty.util.AttributeKey;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.ints.IntPriorityQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongArrayFIFOQueue;
|
||||
import it.unimi.dsi.fastutil.longs.LongPriorityQueue;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.net.InetAddress;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.net.SocketAddress;
|
||||
import java.security.SecureRandom;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ScheduledFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
import java.util.function.BooleanSupplier;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public abstract class PacketHandler implements IPacketReceiver {
|
||||
public static final int MAX_PACKET_ID = 512;
|
||||
private static final HytaleLogger LOGIN_TIMING_LOGGER = HytaleLogger.get("LoginTiming");
|
||||
private static final AttributeKey<Long> LOGIN_START_ATTRIBUTE_KEY = AttributeKey.newInstance("LOGIN_START");
|
||||
@Nonnull
|
||||
protected final Channel channel;
|
||||
@Nonnull
|
||||
protected final ProtocolVersion protocolVersion;
|
||||
@Nullable
|
||||
protected PlayerAuthentication auth;
|
||||
protected boolean queuePackets;
|
||||
protected final AtomicInteger queuedPackets = new AtomicInteger();
|
||||
protected final SecureRandom pingIdRandom = new SecureRandom();
|
||||
@Nonnull
|
||||
protected final PacketHandler.PingInfo[] pingInfo;
|
||||
private float pingTimer;
|
||||
protected boolean registered;
|
||||
private ScheduledFuture<?> timeoutTask;
|
||||
@Nullable
|
||||
protected Throwable clientReadyForChunksFutureStack;
|
||||
@Nullable
|
||||
protected CompletableFuture<Void> clientReadyForChunksFuture;
|
||||
@Nonnull
|
||||
protected final PacketHandler.DisconnectReason disconnectReason = new PacketHandler.DisconnectReason();
|
||||
|
||||
public PacketHandler(@Nonnull Channel channel, @Nonnull ProtocolVersion protocolVersion) {
|
||||
this.channel = channel;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.pingInfo = new PacketHandler.PingInfo[PongType.VALUES.length];
|
||||
|
||||
for (PongType pingType : PongType.VALUES) {
|
||||
this.pingInfo[pingType.ordinal()] = new PacketHandler.PingInfo(pingType);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Channel getChannel() {
|
||||
return this.channel;
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
public void setCompressionEnabled(boolean compressionEnabled) {
|
||||
HytaleLogger.getLogger().at(Level.INFO).log(this.getIdentifier() + " compression now handled by encoder");
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
public boolean isCompressionEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public abstract String getIdentifier();
|
||||
|
||||
@Nonnull
|
||||
public ProtocolVersion getProtocolVersion() {
|
||||
return this.protocolVersion;
|
||||
}
|
||||
|
||||
public final void registered(@Nullable PacketHandler oldHandler) {
|
||||
this.registered = true;
|
||||
this.registered0(oldHandler);
|
||||
}
|
||||
|
||||
protected void registered0(@Nullable PacketHandler oldHandler) {
|
||||
}
|
||||
|
||||
public final void unregistered(@Nullable PacketHandler newHandler) {
|
||||
this.registered = false;
|
||||
this.clearTimeout();
|
||||
this.unregistered0(newHandler);
|
||||
}
|
||||
|
||||
protected void unregistered0(@Nullable PacketHandler newHandler) {
|
||||
}
|
||||
|
||||
public void handle(@Nonnull Packet packet) {
|
||||
this.accept(packet);
|
||||
}
|
||||
|
||||
public abstract void accept(@Nonnull Packet var1);
|
||||
|
||||
public void logCloseMessage() {
|
||||
HytaleLogger.getLogger().at(Level.INFO).log("%s was closed.", this.getIdentifier());
|
||||
}
|
||||
|
||||
public void closed(ChannelHandlerContext ctx) {
|
||||
this.clearTimeout();
|
||||
}
|
||||
|
||||
public void setQueuePackets(boolean queuePackets) {
|
||||
this.queuePackets = queuePackets;
|
||||
}
|
||||
|
||||
public void tryFlush() {
|
||||
if (this.queuedPackets.getAndSet(0) > 0) {
|
||||
this.channel.flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void write(@Nonnull Packet... packets) {
|
||||
Packet[] cachedPackets = new Packet[packets.length];
|
||||
this.handleOutboundAndCachePackets(packets, cachedPackets);
|
||||
if (this.queuePackets) {
|
||||
this.channel.write(cachedPackets, this.channel.voidPromise());
|
||||
this.queuedPackets.getAndIncrement();
|
||||
} else {
|
||||
this.channel.writeAndFlush(cachedPackets, this.channel.voidPromise());
|
||||
}
|
||||
}
|
||||
|
||||
public void write(@Nonnull Packet[] packets, @Nonnull Packet finalPacket) {
|
||||
Packet[] cachedPackets = new Packet[packets.length + 1];
|
||||
this.handleOutboundAndCachePackets(packets, cachedPackets);
|
||||
cachedPackets[cachedPackets.length - 1] = this.handleOutboundAndCachePacket(finalPacket);
|
||||
if (this.queuePackets) {
|
||||
this.channel.write(cachedPackets, this.channel.voidPromise());
|
||||
this.queuedPackets.getAndIncrement();
|
||||
} else {
|
||||
this.channel.writeAndFlush(cachedPackets, this.channel.voidPromise());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(@Nonnull Packet packet) {
|
||||
this.writePacket(packet, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeNoCache(@Nonnull Packet packet) {
|
||||
this.writePacket(packet, false);
|
||||
}
|
||||
|
||||
public void writePacket(@Nonnull Packet packet, boolean cache) {
|
||||
if (!PacketAdapters.__handleOutbound(this, packet)) {
|
||||
Packet toSend;
|
||||
if (cache) {
|
||||
toSend = this.handleOutboundAndCachePacket(packet);
|
||||
} else {
|
||||
toSend = packet;
|
||||
}
|
||||
|
||||
if (this.queuePackets) {
|
||||
this.channel.write(toSend, this.channel.voidPromise());
|
||||
this.queuedPackets.getAndIncrement();
|
||||
} else {
|
||||
this.channel.writeAndFlush(toSend, this.channel.voidPromise());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleOutboundAndCachePackets(@Nonnull Packet[] packets, @Nonnull Packet[] cachedPackets) {
|
||||
for (int i = 0; i < packets.length; i++) {
|
||||
Packet packet = packets[i];
|
||||
if (!PacketAdapters.__handleOutbound(this, packet)) {
|
||||
cachedPackets[i] = this.handleOutboundAndCachePacket(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Packet handleOutboundAndCachePacket(@Nonnull Packet packet) {
|
||||
return (Packet) (packet instanceof CachedPacket ? packet : CachedPacket.cache(packet));
|
||||
}
|
||||
|
||||
public void disconnect(@Nonnull String message) {
|
||||
this.disconnectReason.setServerDisconnectReason(message);
|
||||
HytaleLogger.getLogger().at(Level.INFO).log("Disconnecting %s with the message: %s", NettyUtil.formatRemoteAddress(this.channel), message);
|
||||
this.disconnect0(message);
|
||||
}
|
||||
|
||||
protected void disconnect0(@Nonnull String message) {
|
||||
this.channel.writeAndFlush(new Disconnect(message, DisconnectType.Disconnect)).addListener(ProtocolUtil.CLOSE_ON_COMPLETE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PacketStatsRecorder getPacketStatsRecorder() {
|
||||
return this.channel.attr(PacketStatsRecorder.CHANNEL_KEY).get();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PacketHandler.PingInfo getPingInfo(@Nonnull PongType pongType) {
|
||||
return this.pingInfo[pongType.ordinal()];
|
||||
}
|
||||
|
||||
public long getOperationTimeoutThreshold() {
|
||||
double average = this.getPingInfo(PongType.Tick).getPingMetricSet().getAverage(0);
|
||||
return PacketHandler.PingInfo.TIME_UNIT.toMillis(Math.round(average * 2.0)) + 3000L;
|
||||
}
|
||||
|
||||
public void tickPing(float dt) {
|
||||
this.pingTimer -= dt;
|
||||
if (this.pingTimer <= 0.0F) {
|
||||
this.pingTimer = 1.0F;
|
||||
this.sendPing();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendPing() {
|
||||
int id = this.pingIdRandom.nextInt();
|
||||
Instant nowInstant = Instant.now();
|
||||
long nowTimestamp = System.nanoTime();
|
||||
|
||||
for (PacketHandler.PingInfo info : this.pingInfo) {
|
||||
info.recordSent(id, nowTimestamp);
|
||||
}
|
||||
|
||||
this.writeNoCache(
|
||||
new Ping(
|
||||
id,
|
||||
WorldTimeResource.instantToInstantData(nowInstant),
|
||||
(int) this.getPingInfo(PongType.Raw).getPingMetricSet().getLastValue(),
|
||||
(int) this.getPingInfo(PongType.Direct).getPingMetricSet().getLastValue(),
|
||||
(int) this.getPingInfo(PongType.Tick).getPingMetricSet().getLastValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void handlePong(@Nonnull Pong packet) {
|
||||
this.pingInfo[packet.type.ordinal()].handlePacket(packet);
|
||||
}
|
||||
|
||||
protected void initStage(@Nonnull String stage, @Nonnull Duration timeout, @Nonnull BooleanSupplier condition) {
|
||||
NettyUtil.TimeoutContext.init(this.channel, stage, this.getIdentifier());
|
||||
this.setStageTimeout(stage, timeout, condition);
|
||||
}
|
||||
|
||||
protected void enterStage(@Nonnull String stage, @Nonnull Duration timeout, @Nonnull BooleanSupplier condition) {
|
||||
NettyUtil.TimeoutContext.update(this.channel, stage, this.getIdentifier());
|
||||
this.updatePacketTimeout(timeout);
|
||||
this.setStageTimeout(stage, timeout, condition);
|
||||
}
|
||||
|
||||
protected void enterStage(@Nonnull String stage, @Nonnull Duration timeout) {
|
||||
NettyUtil.TimeoutContext.update(this.channel, stage, this.getIdentifier());
|
||||
this.updatePacketTimeout(timeout);
|
||||
}
|
||||
|
||||
protected void continueStage(@Nonnull String stage, @Nonnull Duration timeout, @Nonnull BooleanSupplier condition) {
|
||||
NettyUtil.TimeoutContext.update(this.channel, stage);
|
||||
this.updatePacketTimeout(timeout);
|
||||
this.setStageTimeout(stage, timeout, condition);
|
||||
}
|
||||
|
||||
private void setStageTimeout(@Nonnull String stageId, @Nonnull Duration timeout, @Nonnull BooleanSupplier meets) {
|
||||
if (this.timeoutTask != null) {
|
||||
this.timeoutTask.cancel(false);
|
||||
}
|
||||
|
||||
if (this instanceof AuthenticationPacketHandler || !(this instanceof PasswordPacketHandler) || this.auth != null) {
|
||||
logConnectionTimings(this.channel, "Entering stage '" + stageId + "'", Level.FINEST);
|
||||
long timeoutMillis = timeout.toMillis();
|
||||
this.timeoutTask = this.channel
|
||||
.eventLoop()
|
||||
.schedule(
|
||||
() -> {
|
||||
if (this.channel.isOpen()) {
|
||||
if (!meets.getAsBoolean()) {
|
||||
NettyUtil.TimeoutContext context = this.channel.attr(NettyUtil.TimeoutContext.KEY).get();
|
||||
String duration = context != null ? FormatUtil.nanosToString(System.nanoTime() - context.connectionStartNs()) : "unknown";
|
||||
HytaleLogger.getLogger()
|
||||
.at(Level.WARNING)
|
||||
.log("Stage timeout for %s at stage '%s' after %s connected", this.getIdentifier(), stageId, duration);
|
||||
this.disconnect("Either you took too long to login or we took too long to process your request! Retry again in a moment.");
|
||||
}
|
||||
}
|
||||
},
|
||||
timeoutMillis,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePacketTimeout(@Nonnull Duration timeout) {
|
||||
this.channel.attr(ProtocolUtil.PACKET_TIMEOUT_KEY).set(timeout);
|
||||
}
|
||||
|
||||
protected void clearTimeout() {
|
||||
if (this.timeoutTask != null) {
|
||||
this.timeoutTask.cancel(false);
|
||||
}
|
||||
|
||||
if (this.clientReadyForChunksFuture != null) {
|
||||
this.clientReadyForChunksFuture.cancel(true);
|
||||
this.clientReadyForChunksFuture = null;
|
||||
this.clientReadyForChunksFutureStack = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerAuthentication getAuth() {
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
public boolean stillActive() {
|
||||
return this.channel.isActive();
|
||||
}
|
||||
|
||||
public int getQueuedPacketsCount() {
|
||||
return this.queuedPackets.get();
|
||||
}
|
||||
|
||||
public boolean isLocalConnection() {
|
||||
SocketAddress socketAddress;
|
||||
if (this.channel instanceof QuicStreamChannel quicStreamChannel) {
|
||||
socketAddress = quicStreamChannel.parent().remoteSocketAddress();
|
||||
} else {
|
||||
socketAddress = this.channel.remoteAddress();
|
||||
}
|
||||
|
||||
if (socketAddress instanceof InetSocketAddress) {
|
||||
InetAddress address = ((InetSocketAddress) socketAddress).getAddress();
|
||||
return NetworkUtil.addressMatchesAny(address, NetworkUtil.AddressType.ANY_LOCAL, NetworkUtil.AddressType.LOOPBACK);
|
||||
} else {
|
||||
return socketAddress instanceof DomainSocketAddress || socketAddress instanceof LocalAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isLANConnection() {
|
||||
SocketAddress socketAddress;
|
||||
if (this.channel instanceof QuicStreamChannel quicStreamChannel) {
|
||||
socketAddress = quicStreamChannel.parent().remoteSocketAddress();
|
||||
} else {
|
||||
socketAddress = this.channel.remoteAddress();
|
||||
}
|
||||
|
||||
if (socketAddress instanceof InetSocketAddress) {
|
||||
InetAddress address = ((InetSocketAddress) socketAddress).getAddress();
|
||||
return NetworkUtil.addressMatchesAny(address);
|
||||
} else {
|
||||
return socketAddress instanceof DomainSocketAddress || socketAddress instanceof LocalAddress;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PacketHandler.DisconnectReason getDisconnectReason() {
|
||||
return this.disconnectReason;
|
||||
}
|
||||
|
||||
public void setClientReadyForChunksFuture(@Nonnull CompletableFuture<Void> clientReadyFuture) {
|
||||
if (this.clientReadyForChunksFuture != null) {
|
||||
throw new IllegalStateException("Tried to hook client ready but something is already waiting for it!", this.clientReadyForChunksFutureStack);
|
||||
} else {
|
||||
HytaleLogger.getLogger().at(Level.WARNING).log("%s Added future for ClientReady packet?", this.getIdentifier());
|
||||
this.clientReadyForChunksFutureStack = new Throwable();
|
||||
this.clientReadyForChunksFuture = clientReadyFuture;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CompletableFuture<Void> getClientReadyForChunksFuture() {
|
||||
return this.clientReadyForChunksFuture;
|
||||
}
|
||||
|
||||
public static void logConnectionTimings(@Nonnull Channel channel, @Nonnull String message, @Nonnull Level level) {
|
||||
Attribute<Long> loginStartAttribute = channel.attr(LOGIN_START_ATTRIBUTE_KEY);
|
||||
long now = System.nanoTime();
|
||||
Long before = loginStartAttribute.getAndSet(now);
|
||||
NettyUtil.TimeoutContext context = channel.attr(NettyUtil.TimeoutContext.KEY).get();
|
||||
String identifier = context != null ? context.playerIdentifier() : NettyUtil.formatRemoteAddress(channel);
|
||||
if (before == null) {
|
||||
LOGIN_TIMING_LOGGER.at(level).log("[%s] %s", identifier, message);
|
||||
} else {
|
||||
LOGIN_TIMING_LOGGER.at(level).log("[%s] %s took %s", identifier, message, LazyArgs.lazy(() -> FormatUtil.nanosToString(now - before)));
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
LOGIN_TIMING_LOGGER.setLevel(Level.ALL);
|
||||
}
|
||||
|
||||
public static class DisconnectReason {
|
||||
@Nullable
|
||||
private String serverDisconnectReason;
|
||||
@Nullable
|
||||
private DisconnectType clientDisconnectType;
|
||||
|
||||
protected DisconnectReason() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getServerDisconnectReason() {
|
||||
return this.serverDisconnectReason;
|
||||
}
|
||||
|
||||
public void setServerDisconnectReason(String serverDisconnectReason) {
|
||||
this.serverDisconnectReason = serverDisconnectReason;
|
||||
this.clientDisconnectType = null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public DisconnectType getClientDisconnectType() {
|
||||
return this.clientDisconnectType;
|
||||
}
|
||||
|
||||
public void setClientDisconnectType(DisconnectType clientDisconnectType) {
|
||||
this.clientDisconnectType = clientDisconnectType;
|
||||
this.serverDisconnectReason = null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "DisconnectReason{serverDisconnectReason='" + this.serverDisconnectReason + "', clientDisconnectType=" + this.clientDisconnectType + "}";
|
||||
}
|
||||
}
|
||||
|
||||
public static class PingInfo {
|
||||
public static final MetricsRegistry<PacketHandler.PingInfo> METRICS_REGISTRY = new MetricsRegistry<PacketHandler.PingInfo>()
|
||||
.register("PingType", pingInfo -> pingInfo.pingType, new EnumCodec<>(PongType.class))
|
||||
.register("PingMetrics", PacketHandler.PingInfo::getPingMetricSet, HistoricMetric.METRICS_CODEC)
|
||||
.register("PacketQueueMin", pingInfo -> pingInfo.packetQueueMetric.getMin(), Codec.LONG)
|
||||
.register("PacketQueueAvg", pingInfo -> pingInfo.packetQueueMetric.getAverage(), Codec.DOUBLE)
|
||||
.register("PacketQueueMax", pingInfo -> pingInfo.packetQueueMetric.getMax(), Codec.LONG);
|
||||
public static final TimeUnit TIME_UNIT = TimeUnit.MICROSECONDS;
|
||||
public static final int ONE_SECOND_INDEX = 0;
|
||||
public static final int ONE_MINUTE_INDEX = 1;
|
||||
public static final int FIVE_MINUTE_INDEX = 2;
|
||||
public static final double PERCENTILE = 0.99F;
|
||||
public static final int PING_FREQUENCY = 1;
|
||||
public static final TimeUnit PING_FREQUENCY_UNIT = TimeUnit.SECONDS;
|
||||
public static final int PING_FREQUENCY_MILLIS = 1000;
|
||||
public static final int PING_HISTORY_MILLIS = 15000;
|
||||
public static final int PING_HISTORY_LENGTH = 15;
|
||||
protected final PongType pingType;
|
||||
protected final Lock queueLock = new ReentrantLock();
|
||||
protected final IntPriorityQueue pingIdQueue = new IntArrayFIFOQueue(15);
|
||||
protected final LongPriorityQueue pingTimestampQueue = new LongArrayFIFOQueue(15);
|
||||
protected final Lock pingLock = new ReentrantLock();
|
||||
@Nonnull
|
||||
protected final HistoricMetric pingMetricSet;
|
||||
protected final Metric packetQueueMetric = new Metric();
|
||||
|
||||
public PingInfo(PongType pingType) {
|
||||
this.pingType = pingType;
|
||||
this.pingMetricSet = HistoricMetric.builder(1000L, TimeUnit.MILLISECONDS)
|
||||
.addPeriod(1L, TimeUnit.SECONDS)
|
||||
.addPeriod(1L, TimeUnit.MINUTES)
|
||||
.addPeriod(5L, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected void recordSent(int id, long timestamp) {
|
||||
this.queueLock.lock();
|
||||
|
||||
try {
|
||||
this.pingIdQueue.enqueue(id);
|
||||
this.pingTimestampQueue.enqueue(timestamp);
|
||||
} finally {
|
||||
this.queueLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
protected void handlePacket(@Nonnull Pong packet) {
|
||||
if (packet.type != this.pingType) {
|
||||
throw new IllegalArgumentException("Got packet for " + packet.type + " but expected " + this.pingType);
|
||||
} else {
|
||||
this.queueLock.lock();
|
||||
|
||||
int nextIdToHandle;
|
||||
long sentTimestamp;
|
||||
try {
|
||||
nextIdToHandle = this.pingIdQueue.dequeueInt();
|
||||
sentTimestamp = this.pingTimestampQueue.dequeueLong();
|
||||
} finally {
|
||||
this.queueLock.unlock();
|
||||
}
|
||||
|
||||
if (packet.id != nextIdToHandle) {
|
||||
throw new IllegalArgumentException(String.valueOf(packet.id));
|
||||
} else {
|
||||
long nanoTime = System.nanoTime();
|
||||
long pingValue = nanoTime - sentTimestamp;
|
||||
if (pingValue <= 0L) {
|
||||
throw new IllegalArgumentException(String.format("Ping must be received after its sent! %s", pingValue));
|
||||
} else {
|
||||
this.pingLock.lock();
|
||||
|
||||
try {
|
||||
this.pingMetricSet.add(nanoTime, TIME_UNIT.convert(pingValue, TimeUnit.NANOSECONDS));
|
||||
this.packetQueueMetric.add(packet.packetQueueSize);
|
||||
} finally {
|
||||
this.pingLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PongType getPingType() {
|
||||
return this.pingType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Metric getPacketQueueMetric() {
|
||||
return this.packetQueueMetric;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public HistoricMetric getPingMetricSet() {
|
||||
return this.pingMetricSet;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.pingLock.lock();
|
||||
|
||||
try {
|
||||
this.packetQueueMetric.clear();
|
||||
this.pingMetricSet.clear();
|
||||
} finally {
|
||||
this.pingLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,21 +20,25 @@ import io.netty.channel.EventLoopGroup;
|
||||
import io.netty.channel.socket.DatagramChannel;
|
||||
import io.netty.channel.socket.SocketProtocolFamily;
|
||||
import io.netty.channel.socket.nio.NioChannelOption;
|
||||
import io.netty.handler.codec.quic.InsecureQuicTokenHandler;
|
||||
import io.netty.handler.codec.quic.QLogConfiguration;
|
||||
import io.netty.handler.codec.quic.QuicChannel;
|
||||
import io.netty.handler.codec.quic.QuicChannelOption;
|
||||
import io.netty.handler.codec.quic.QuicCongestionControlAlgorithm;
|
||||
import io.netty.handler.codec.quic.QuicServerCodecBuilder;
|
||||
import io.netty.handler.codec.quic.QuicSslContext;
|
||||
import io.netty.handler.codec.quic.QuicSslContextBuilder;
|
||||
import io.netty.handler.ssl.ClientAuth;
|
||||
import io.netty.handler.ssl.SniCompletionEvent;
|
||||
import io.netty.handler.ssl.util.InsecureTrustManagerFactory;
|
||||
import io.netty.handler.ssl.util.SelfSignedCertificate;
|
||||
import io.netty.util.AttributeKey;
|
||||
import io.netty.util.Mapping;
|
||||
import jdk.net.ExtendedSocketOptions;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import javax.net.ssl.SSLEngine;
|
||||
import java.lang.reflect.Field;
|
||||
import java.net.Inet4Address;
|
||||
import java.net.Inet6Address;
|
||||
import java.net.InetSocketAddress;
|
||||
@@ -49,6 +53,7 @@ public class QUICTransport implements Transport {
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final AttributeKey<X509Certificate> CLIENT_CERTIFICATE_ATTR = AttributeKey.valueOf("CLIENT_CERTIFICATE");
|
||||
public static final AttributeKey<Integer> ALPN_REJECT_ERROR_CODE_ATTR = AttributeKey.valueOf("ALPN_REJECT_ERROR_CODE");
|
||||
public static final AttributeKey<String> SNI_HOSTNAME_ATTR = AttributeKey.valueOf("SNI_HOSTNAME");
|
||||
@Nonnull
|
||||
private final EventLoopGroup workerGroup = NettyUtil.getEventLoopGroup("ServerWorkerGroup");
|
||||
private final Bootstrap bootstrapIpv4;
|
||||
@@ -59,18 +64,31 @@ public class QUICTransport implements Transport {
|
||||
|
||||
try {
|
||||
ssc = new SelfSignedCertificate("localhost");
|
||||
} catch (CertificateException var5) {
|
||||
throw new RuntimeException(var5);
|
||||
} catch (CertificateException var7) {
|
||||
throw new RuntimeException(var7);
|
||||
}
|
||||
|
||||
ServerAuthManager.getInstance().setServerCertificate(ssc.cert());
|
||||
LOGGER.at(Level.INFO).log("Server certificate registered for mutual auth, fingerprint: %s", CertificateUtil.computeCertificateFingerprint(ssc.cert()));
|
||||
QuicSslContext sslContext = QuicSslContextBuilder.forServer(ssc.key(), null, ssc.cert())
|
||||
QuicSslContext baseSslContext = QuicSslContextBuilder.forServer(ssc.key(), null, ssc.cert())
|
||||
.applicationProtocols("hytale/2", "hytale/1")
|
||||
.earlyData(false)
|
||||
.clientAuth(ClientAuth.REQUIRE)
|
||||
.trustManager(InsecureTrustManagerFactory.INSTANCE)
|
||||
.build();
|
||||
|
||||
QuicSslContext sslContext;
|
||||
try {
|
||||
QuicSslContextBuilder builder = QuicSslContextBuilder.forServer(ssc.key(), null, ssc.cert()).earlyData(false).clientAuth(ClientAuth.REQUIRE);
|
||||
Field field = builder.getClass().getDeclaredField("mapping");
|
||||
field.setAccessible(true);
|
||||
field.set(builder, (Mapping<String, QuicSslContext>) sni -> baseSslContext);
|
||||
sslContext = builder.build();
|
||||
} catch (IllegalAccessException | NoSuchFieldException var6) {
|
||||
LOGGER.at(Level.WARNING).withCause(var6).log("Failed to set SNI mapping via reflection, SNI support disabled");
|
||||
sslContext = baseSslContext;
|
||||
}
|
||||
|
||||
NettyUtil.ReflectiveChannelFactory<? extends DatagramChannel> channelFactoryIpv4 = NettyUtil.getDatagramChannelFactory(SocketProtocolFamily.INET);
|
||||
LOGGER.at(Level.INFO).log("Using IPv4 Datagram Channel: %s...", channelFactoryIpv4.getSimpleName());
|
||||
this.bootstrapIpv4 = new Bootstrap()
|
||||
@@ -139,7 +157,8 @@ public class QUICTransport implements Transport {
|
||||
Duration playTimeout = HytaleServer.get().getConfig().getConnectionTimeouts().getPlay();
|
||||
ChannelHandler quicHandler = new QuicServerCodecBuilder()
|
||||
.sslContext(this.sslContext)
|
||||
.tokenHandler(InsecureQuicTokenHandler.INSTANCE)
|
||||
.tokenHandler(null)
|
||||
.activeMigration(false)
|
||||
.maxIdleTimeout(playTimeout.toMillis(), TimeUnit.MILLISECONDS)
|
||||
.ackDelayExponent(3L)
|
||||
.initialMaxData(524288L)
|
||||
@@ -150,6 +169,7 @@ public class QUICTransport implements Transport {
|
||||
.initialMaxStreamsBidirectional(1L)
|
||||
.discoverPmtu(true)
|
||||
.congestionControlAlgorithm(QuicCongestionControlAlgorithm.BBR)
|
||||
.option(QuicChannelOption.QLOG, System.getProperty("hytale.qlog") != null ? new QLogConfiguration(".", "hytale-server-quic-qlogs", "") : null)
|
||||
.handler(
|
||||
new ChannelInboundHandlerAdapter() {
|
||||
@Override
|
||||
@@ -157,18 +177,34 @@ public class QUICTransport implements Transport {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void userEventTriggered(ChannelHandlerContext ctx, Object evt) throws Exception {
|
||||
if (evt instanceof SniCompletionEvent sniEvent) {
|
||||
ctx.channel().attr(QUICTransport.SNI_HOSTNAME_ATTR).set(sniEvent.hostname());
|
||||
}
|
||||
|
||||
super.userEventTriggered(ctx, evt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void channelActive(@Nonnull ChannelHandlerContext ctx) throws Exception {
|
||||
QuicChannel channel = (QuicChannel) ctx.channel();
|
||||
String sni = channel.attr(QUICTransport.SNI_HOSTNAME_ATTR).get();
|
||||
QUICTransport.LOGGER
|
||||
.at(Level.INFO)
|
||||
.log("Received connection from %s to %s", NettyUtil.formatRemoteAddress(channel), NettyUtil.formatLocalAddress(channel));
|
||||
.log("Received connection from %s to %s (SNI: %s)", NettyUtil.formatRemoteAddress(channel), NettyUtil.formatLocalAddress(channel), sni);
|
||||
String negotiatedAlpn = channel.sslEngine().getApplicationProtocol();
|
||||
int negotiatedVersion = this.parseProtocolVersion(negotiatedAlpn);
|
||||
if (negotiatedVersion < 2) {
|
||||
QUICTransport.LOGGER
|
||||
.at(Level.INFO)
|
||||
.log("Marking connection from %s for rejection: ALPN %s < required %d", NettyUtil.formatRemoteAddress(channel), negotiatedAlpn, 2);
|
||||
.log(
|
||||
"Marking connection from %s (SNI: %s) for rejection: ALPN %s < required %d",
|
||||
NettyUtil.formatRemoteAddress(channel),
|
||||
sni,
|
||||
negotiatedAlpn,
|
||||
2
|
||||
);
|
||||
channel.attr(QUICTransport.ALPN_REJECT_ERROR_CODE_ATTR).set(5);
|
||||
}
|
||||
|
||||
@@ -176,7 +212,7 @@ public class QUICTransport implements Transport {
|
||||
if (clientCert == null) {
|
||||
QUICTransport.LOGGER
|
||||
.at(Level.WARNING)
|
||||
.log("Connection rejected: no client certificate from %s", NettyUtil.formatRemoteAddress(channel));
|
||||
.log("Connection rejected: no client certificate from %s (SNI: %s)", NettyUtil.formatRemoteAddress(channel), sni);
|
||||
ProtocolUtil.closeConnection(channel);
|
||||
} else {
|
||||
channel.attr(QUICTransport.CLIENT_CERTIFICATE_ATTR).set(clientCert);
|
||||
|
||||
@@ -24,6 +24,7 @@ import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.hypixel.hytale.protocol.ComponentUpdate;
|
||||
import com.hypixel.hytale.protocol.ComponentUpdateType;
|
||||
import com.hypixel.hytale.protocol.EntityEffectsUpdate;
|
||||
import com.hypixel.hytale.protocol.packets.entities.EntityUpdates;
|
||||
import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.EntityModule;
|
||||
@@ -55,54 +56,69 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
|
||||
public class EntityTrackerSystems {
|
||||
@Nonnull
|
||||
public static final SystemGroup<EntityStore> FIND_VISIBLE_ENTITIES_GROUP = EntityStore.REGISTRY.registerSystemGroup();
|
||||
@Nonnull
|
||||
public static final SystemGroup<EntityStore> QUEUE_UPDATE_GROUP = EntityStore.REGISTRY.registerSystemGroup();
|
||||
|
||||
public EntityTrackerSystems() {
|
||||
}
|
||||
|
||||
public static boolean despawnAll(@Nonnull Ref<EntityStore> viewerRef, @Nonnull Store<EntityStore> store) {
|
||||
EntityTrackerSystems.EntityViewer viewer = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (viewer == null) {
|
||||
if (!viewerRef.isValid()) {
|
||||
return false;
|
||||
} else {
|
||||
int networkId = viewer.sent.removeInt(viewerRef);
|
||||
EntityUpdates packet = new EntityUpdates();
|
||||
packet.removed = viewer.sent.values().toIntArray();
|
||||
viewer.packetReceiver.writeNoCache(packet);
|
||||
clear(viewerRef, store);
|
||||
viewer.sent.put(viewerRef, networkId);
|
||||
return true;
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (entityViewerComponent == null) {
|
||||
return false;
|
||||
} else {
|
||||
int networkId = entityViewerComponent.sent.removeInt(viewerRef);
|
||||
EntityUpdates packet = new EntityUpdates();
|
||||
packet.removed = entityViewerComponent.sent.values().toIntArray();
|
||||
entityViewerComponent.packetReceiver.writeNoCache(packet);
|
||||
clear(viewerRef, store);
|
||||
entityViewerComponent.sent.put(viewerRef, networkId);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean clear(@Nonnull Ref<EntityStore> viewerRef, @Nonnull Store<EntityStore> store) {
|
||||
EntityTrackerSystems.EntityViewer viewer = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (viewer == null) {
|
||||
if (!viewerRef.isValid()) {
|
||||
return false;
|
||||
} else {
|
||||
for (Ref<EntityStore> ref : viewer.sent.keySet()) {
|
||||
EntityTrackerSystems.Visible visible = store.getComponent(ref, EntityTrackerSystems.Visible.getComponentType());
|
||||
if (visible != null) {
|
||||
visible.visibleTo.remove(viewerRef);
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (entityViewerComponent == null) {
|
||||
return false;
|
||||
} else {
|
||||
for (Ref<EntityStore> ref : entityViewerComponent.sent.keySet()) {
|
||||
if (ref != null && ref.isValid()) {
|
||||
EntityTrackerSystems.Visible visibleComponent = store.getComponent(ref, EntityTrackerSystems.Visible.getComponentType());
|
||||
if (visibleComponent != null) {
|
||||
visibleComponent.visibleTo.remove(viewerRef);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
viewer.sent.clear();
|
||||
return true;
|
||||
entityViewerComponent.sent.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddToVisible extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Collections.singleton(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.EnsureVisibleComponent.class)
|
||||
);
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
|
||||
public AddToVisible(
|
||||
ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType,
|
||||
ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType
|
||||
@Nonnull ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType,
|
||||
@Nonnull ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType
|
||||
) {
|
||||
this.entityViewerComponentType = entityViewerComponentType;
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
@@ -132,23 +148,32 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
Ref<EntityStore> viewerRef = archetypeChunk.getReferenceTo(index);
|
||||
EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.entityViewerComponentType);
|
||||
Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = archetypeChunk.getComponent(index, this.entityViewerComponentType);
|
||||
|
||||
for (Ref<EntityStore> ref : viewer.visible) {
|
||||
commandBuffer.getComponent(ref, this.visibleComponentType).addViewerParallel(viewerRef, viewer);
|
||||
assert entityViewerComponent != null;
|
||||
|
||||
for (Ref<EntityStore> vislbleRef : entityViewerComponent.visible) {
|
||||
if (vislbleRef != null && vislbleRef.isValid()) {
|
||||
EntityTrackerSystems.Visible visibleComponent = commandBuffer.getComponent(vislbleRef, this.visibleComponentType);
|
||||
if (visibleComponent != null) {
|
||||
visibleComponent.addViewerParallel(ref, entityViewerComponent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ClearEntityViewers extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Collections.singleton(
|
||||
new SystemGroupDependency<>(Order.BEFORE, EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
|
||||
public ClearEntityViewers(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType) {
|
||||
this.componentType = componentType;
|
||||
public ClearEntityViewers(@Nonnull ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType) {
|
||||
this.entityViewerComponentType = entityViewerComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@@ -159,7 +184,7 @@ public class EntityTrackerSystems {
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.componentType;
|
||||
return this.entityViewerComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -175,22 +200,27 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.componentType);
|
||||
viewer.visible.clear();
|
||||
viewer.lodExcludedCount = 0;
|
||||
viewer.hiddenCount = 0;
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = archetypeChunk.getComponent(index, this.entityViewerComponentType);
|
||||
|
||||
assert entityViewerComponent != null;
|
||||
|
||||
entityViewerComponent.visible.clear();
|
||||
entityViewerComponent.lodExcludedCount = 0;
|
||||
entityViewerComponent.hiddenCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ClearPreviouslyVisible extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.ClearEntityViewers.class),
|
||||
new SystemGroupDependency<EntityStore>(Order.AFTER, EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
|
||||
public ClearPreviouslyVisible(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
public ClearPreviouslyVisible(@Nonnull ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType) {
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@@ -201,7 +231,7 @@ public class EntityTrackerSystems {
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.componentType;
|
||||
return this.visibleComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -217,24 +247,29 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityTrackerSystems.Visible visible = archetypeChunk.getComponent(index, this.componentType);
|
||||
Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> oldVisibleTo = visible.previousVisibleTo;
|
||||
visible.previousVisibleTo = visible.visibleTo;
|
||||
visible.visibleTo = oldVisibleTo;
|
||||
visible.visibleTo.clear();
|
||||
visible.newlyVisibleTo.clear();
|
||||
EntityTrackerSystems.Visible visibleComponent = archetypeChunk.getComponent(index, this.visibleComponentType);
|
||||
|
||||
assert visibleComponent != null;
|
||||
|
||||
Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> oldVisibleTo = visibleComponent.previousVisibleTo;
|
||||
visibleComponent.previousVisibleTo = visibleComponent.visibleTo;
|
||||
visibleComponent.visibleTo = oldVisibleTo;
|
||||
visibleComponent.visibleTo.clear();
|
||||
visibleComponent.newlyVisibleTo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static class CollectVisible extends EntityTickingSystem<EntityStore> {
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
|
||||
public CollectVisible(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType) {
|
||||
this.componentType = componentType;
|
||||
this.query = Archetype.of(componentType, TransformComponent.getComponentType());
|
||||
public CollectVisible(@Nonnull ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType) {
|
||||
this.entityViewerComponentType = entityViewerComponentType;
|
||||
this.query = Archetype.of(entityViewerComponentType, TransformComponent.getComponentType());
|
||||
this.dependencies = Collections.singleton(new SystemDependency<>(Order.AFTER, NetworkSendableSpatialSystem.class));
|
||||
}
|
||||
|
||||
@@ -268,14 +303,20 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
TransformComponent transform = archetypeChunk.getComponent(index, TransformComponent.getComponentType());
|
||||
Vector3d position = transform.getPosition();
|
||||
EntityTrackerSystems.EntityViewer entityViewer = archetypeChunk.getComponent(index, this.componentType);
|
||||
TransformComponent transformComponent = archetypeChunk.getComponent(index, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
Vector3d position = transformComponent.getPosition();
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = archetypeChunk.getComponent(index, this.entityViewerComponentType);
|
||||
|
||||
assert entityViewerComponent != null;
|
||||
|
||||
SpatialStructure<Ref<EntityStore>> spatialStructure = store.getResource(EntityModule.get().getNetworkSendableSpatialResourceType())
|
||||
.getSpatialStructure();
|
||||
ObjectList<Ref<EntityStore>> results = SpatialResource.getThreadLocalReferenceList();
|
||||
spatialStructure.collect(position, entityViewer.viewRadiusBlocks, results);
|
||||
entityViewer.visible.addAll(results);
|
||||
spatialStructure.collect(position, entityViewerComponent.viewRadiusBlocks, results);
|
||||
entityViewerComponent.visible.addAll(results);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -325,17 +366,17 @@ public class EntityTrackerSystems {
|
||||
|
||||
assert visibleComponent != null;
|
||||
|
||||
Ref<EntityStore> entityRef = archetypeChunk.getReferenceTo(index);
|
||||
EffectControllerComponent effectControllerComponent = archetypeChunk.getComponent(index, this.effectControllerComponentType);
|
||||
|
||||
assert effectControllerComponent != null;
|
||||
|
||||
Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);
|
||||
if (!visibleComponent.newlyVisibleTo.isEmpty()) {
|
||||
queueFullUpdate(entityRef, effectControllerComponent, visibleComponent.newlyVisibleTo);
|
||||
queueFullUpdate(ref, effectControllerComponent, visibleComponent.newlyVisibleTo);
|
||||
}
|
||||
|
||||
if (effectControllerComponent.consumeNetworkOutdated()) {
|
||||
queueUpdatesFor(entityRef, effectControllerComponent, visibleComponent.visibleTo, visibleComponent.newlyVisibleTo);
|
||||
queueUpdatesFor(ref, effectControllerComponent, visibleComponent.visibleTo, visibleComponent.newlyVisibleTo);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,8 +385,7 @@ public class EntityTrackerSystems {
|
||||
@Nonnull EffectControllerComponent effectControllerComponent,
|
||||
@Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo
|
||||
) {
|
||||
ComponentUpdate update = new ComponentUpdate();
|
||||
update.type = ComponentUpdateType.EntityEffects;
|
||||
EntityEffectsUpdate update = new EntityEffectsUpdate();
|
||||
update.entityEffectUpdates = effectControllerComponent.createInitUpdates();
|
||||
|
||||
for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) {
|
||||
@@ -359,8 +399,7 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo,
|
||||
@Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> exclude
|
||||
) {
|
||||
ComponentUpdate update = new ComponentUpdate();
|
||||
update.type = ComponentUpdateType.EntityEffects;
|
||||
EntityEffectsUpdate update = new EntityEffectsUpdate();
|
||||
update.entityEffectUpdates = effectControllerComponent.consumeChanges();
|
||||
if (!exclude.isEmpty()) {
|
||||
for (Entry<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> entry : visibleTo.entrySet()) {
|
||||
@@ -377,15 +416,18 @@ public class EntityTrackerSystems {
|
||||
}
|
||||
|
||||
public static class EnsureVisibleComponent extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Collections.singleton(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.ClearPreviouslyVisible.class)
|
||||
);
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
|
||||
public EnsureVisibleComponent(
|
||||
ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType,
|
||||
ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType
|
||||
@Nonnull ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType,
|
||||
@Nonnull ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType
|
||||
) {
|
||||
this.entityViewerComponentType = entityViewerComponentType;
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
@@ -415,9 +457,13 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
for (Ref<EntityStore> ref : archetypeChunk.getComponent(index, this.entityViewerComponentType).visible) {
|
||||
if (!commandBuffer.getArchetype(ref).contains(this.visibleComponentType)) {
|
||||
commandBuffer.ensureComponent(ref, this.visibleComponentType);
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = archetypeChunk.getComponent(index, this.entityViewerComponentType);
|
||||
|
||||
assert entityViewerComponent != null;
|
||||
|
||||
for (Ref<EntityStore> visibleRef : entityViewerComponent.visible) {
|
||||
if (visibleRef != null && visibleRef.isValid() && !commandBuffer.getArchetype(visibleRef).contains(this.visibleComponentType)) {
|
||||
commandBuffer.ensureComponent(visibleRef, this.visibleComponentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -483,9 +529,13 @@ public class EntityTrackerSystems {
|
||||
private static final float VISIBILITY_UPDATE_INTERVAL = 0.2f;
|
||||
|
||||
public int viewRadiusBlocks;
|
||||
@Nonnull
|
||||
public IPacketReceiver packetReceiver;
|
||||
@Nonnull
|
||||
public Set<Ref<EntityStore>> visible;
|
||||
@Nonnull
|
||||
public Map<Ref<EntityStore>, EntityTrackerSystems.EntityUpdate> updates;
|
||||
@Nonnull
|
||||
public Object2IntMap<Ref<EntityStore>> sent;
|
||||
public int lodExcludedCount;
|
||||
public int hiddenCount;
|
||||
@@ -495,7 +545,7 @@ public class EntityTrackerSystems {
|
||||
return EntityModule.get().getEntityViewerComponentType();
|
||||
}
|
||||
|
||||
public EntityViewer(int viewRadiusBlocks, IPacketReceiver packetReceiver) {
|
||||
public EntityViewer(int viewRadiusBlocks, @Nonnull IPacketReceiver packetReceiver) {
|
||||
this.viewRadiusBlocks = viewRadiusBlocks;
|
||||
this.packetReceiver = packetReceiver;
|
||||
this.visible = new ObjectOpenHashSet<>();
|
||||
@@ -524,7 +574,7 @@ public class EntityTrackerSystems {
|
||||
return new EntityTrackerSystems.EntityViewer(this);
|
||||
}
|
||||
|
||||
public void queueRemove(Ref<EntityStore> ref, ComponentUpdateType type) {
|
||||
public void queueRemove(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentUpdateType type) {
|
||||
if (!this.visible.contains(ref)) {
|
||||
throw new IllegalArgumentException("Entity is not visible!");
|
||||
} else {
|
||||
@@ -532,7 +582,7 @@ public class EntityTrackerSystems {
|
||||
}
|
||||
}
|
||||
|
||||
public void queueUpdate(Ref<EntityStore> ref, ComponentUpdate update) {
|
||||
public void queueUpdate(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentUpdate update) {
|
||||
if (!this.visible.contains(ref)) {
|
||||
throw new IllegalArgumentException("Entity is not visible!");
|
||||
} else {
|
||||
@@ -567,14 +617,16 @@ public class EntityTrackerSystems {
|
||||
}
|
||||
|
||||
public static class RemoveEmptyVisibleComponent extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.AddToVisible.class),
|
||||
new SystemGroupDependency<EntityStore>(Order.BEFORE, EntityTrackerSystems.QUEUE_UPDATE_GROUP)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
|
||||
public RemoveEmptyVisibleComponent(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
public RemoveEmptyVisibleComponent(@Nonnull ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType) {
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@@ -585,7 +637,7 @@ public class EntityTrackerSystems {
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.componentType;
|
||||
return this.visibleComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -601,22 +653,27 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
if (archetypeChunk.getComponent(index, this.componentType).visibleTo.isEmpty()) {
|
||||
commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), this.componentType);
|
||||
EntityTrackerSystems.Visible visibleComponent = archetypeChunk.getComponent(index, this.visibleComponentType);
|
||||
|
||||
assert visibleComponent != null;
|
||||
|
||||
if (visibleComponent.visibleTo.isEmpty()) {
|
||||
commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), this.visibleComponentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoveVisibleComponent extends HolderSystem<EntityStore> {
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
|
||||
public RemoveVisibleComponent(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
public RemoveVisibleComponent(@Nonnull ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType) {
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.componentType;
|
||||
return this.visibleComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -625,18 +682,22 @@ public class EntityTrackerSystems {
|
||||
|
||||
@Override
|
||||
public void onEntityRemoved(@Nonnull Holder<EntityStore> holder, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store) {
|
||||
holder.removeComponent(this.componentType);
|
||||
holder.removeComponent(this.visibleComponentType);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SendPackets extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
@Nonnull
|
||||
public static final ThreadLocal<IntList> INT_LIST_THREAD_LOCAL = ThreadLocal.withInitial(IntArrayList::new);
|
||||
@Nonnull
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(new SystemGroupDependency<>(Order.AFTER, EntityTrackerSystems.QUEUE_UPDATE_GROUP));
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
|
||||
public SendPackets(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType) {
|
||||
this.componentType = componentType;
|
||||
public SendPackets(@Nonnull ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType) {
|
||||
this.entityViewerComponentType = entityViewerComponentType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@@ -653,7 +714,7 @@ public class EntityTrackerSystems {
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.componentType;
|
||||
return this.entityViewerComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -669,61 +730,70 @@ public class EntityTrackerSystems {
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.componentType);
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = archetypeChunk.getComponent(index, this.entityViewerComponentType);
|
||||
|
||||
assert entityViewerComponent != null;
|
||||
|
||||
IntList removedEntities = INT_LIST_THREAD_LOCAL.get();
|
||||
removedEntities.clear();
|
||||
int before = viewer.updates.size();
|
||||
viewer.updates.entrySet().removeIf(v -> !v.getKey().isValid());
|
||||
if (before != viewer.updates.size()) {
|
||||
LOGGER.atWarning().log("Removed %d invalid updates for removed entities.", before - viewer.updates.size());
|
||||
int before = entityViewerComponent.updates.size();
|
||||
entityViewerComponent.updates.entrySet().removeIf(v -> !v.getKey().isValid());
|
||||
if (before != entityViewerComponent.updates.size()) {
|
||||
LOGGER.atWarning().log("Removed %d invalid updates for removed entities.", before - entityViewerComponent.updates.size());
|
||||
}
|
||||
|
||||
ObjectIterator<it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<Ref<EntityStore>>> iterator = viewer.sent.object2IntEntrySet().iterator();
|
||||
ObjectIterator<it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<Ref<EntityStore>>> iterator = entityViewerComponent.sent
|
||||
.object2IntEntrySet()
|
||||
.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<Ref<EntityStore>> entry = iterator.next();
|
||||
Ref<EntityStore> ref = entry.getKey();
|
||||
if (!ref.isValid() || !viewer.visible.contains(ref)) {
|
||||
if (ref == null || !ref.isValid() || !entityViewerComponent.visible.contains(ref)) {
|
||||
removedEntities.add(entry.getIntValue());
|
||||
iterator.remove();
|
||||
if (viewer.updates.remove(ref) != null) {
|
||||
if (entityViewerComponent.updates.remove(ref) != null) {
|
||||
LOGGER.atSevere().log("Entity can't be removed and also receive an update! " + ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!removedEntities.isEmpty() || !viewer.updates.isEmpty()) {
|
||||
Iterator<Ref<EntityStore>> iteratorx = viewer.updates.keySet().iterator();
|
||||
if (!removedEntities.isEmpty() || !entityViewerComponent.updates.isEmpty()) {
|
||||
Iterator<Ref<EntityStore>> iteratorx = entityViewerComponent.updates.keySet().iterator();
|
||||
|
||||
while (iteratorx.hasNext()) {
|
||||
Ref<EntityStore> ref = iteratorx.next();
|
||||
if (!ref.isValid() || ref.getStore() != store) {
|
||||
if (ref == null || !ref.isValid() || ref.getStore() != store) {
|
||||
iteratorx.remove();
|
||||
} else if (!viewer.sent.containsKey(ref)) {
|
||||
int networkId = commandBuffer.getComponent(ref, NetworkId.getComponentType()).getId();
|
||||
} else if (!entityViewerComponent.sent.containsKey(ref)) {
|
||||
NetworkId networkIdComponent = commandBuffer.getComponent(ref, NetworkId.getComponentType());
|
||||
|
||||
assert networkIdComponent != null;
|
||||
|
||||
int networkId = networkIdComponent.getId();
|
||||
if (networkId == -1) {
|
||||
throw new IllegalArgumentException("Invalid entity network id: " + ref);
|
||||
}
|
||||
|
||||
viewer.sent.put(ref, networkId);
|
||||
entityViewerComponent.sent.put(ref, networkId);
|
||||
}
|
||||
}
|
||||
|
||||
EntityUpdates packet = new EntityUpdates();
|
||||
packet.removed = !removedEntities.isEmpty() ? removedEntities.toIntArray() : null;
|
||||
packet.updates = new com.hypixel.hytale.protocol.EntityUpdate[viewer.updates.size()];
|
||||
packet.updates = new com.hypixel.hytale.protocol.EntityUpdate[entityViewerComponent.updates.size()];
|
||||
int i = 0;
|
||||
|
||||
for (Entry<Ref<EntityStore>, EntityTrackerSystems.EntityUpdate> entry : viewer.updates.entrySet()) {
|
||||
for (Entry<Ref<EntityStore>, EntityTrackerSystems.EntityUpdate> entry : entityViewerComponent.updates.entrySet()) {
|
||||
com.hypixel.hytale.protocol.EntityUpdate entityUpdate = packet.updates[i++] = new com.hypixel.hytale.protocol.EntityUpdate();
|
||||
entityUpdate.networkId = viewer.sent.getInt(entry.getKey());
|
||||
entityUpdate.networkId = entityViewerComponent.sent.getInt(entry.getKey());
|
||||
EntityTrackerSystems.EntityUpdate update = entry.getValue();
|
||||
entityUpdate.removed = update.toRemovedArray();
|
||||
entityUpdate.updates = update.toUpdatesArray();
|
||||
}
|
||||
|
||||
viewer.updates.clear();
|
||||
viewer.packetReceiver.writeNoCache(packet);
|
||||
entityViewerComponent.updates.clear();
|
||||
entityViewerComponent.packetReceiver.writeNoCache(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -752,13 +822,13 @@ public class EntityTrackerSystems {
|
||||
return new EntityTrackerSystems.Visible();
|
||||
}
|
||||
|
||||
public void addViewerParallel(Ref<EntityStore> ref, EntityTrackerSystems.EntityViewer entityViewer) {
|
||||
public void addViewerParallel(@Nonnull Ref<EntityStore> ref, @Nonnull EntityTrackerSystems.EntityViewer entityViewerComponent) {
|
||||
long stamp = this.lock.writeLock();
|
||||
|
||||
try {
|
||||
this.visibleTo.put(ref, entityViewer);
|
||||
this.visibleTo.put(ref, entityViewerComponent);
|
||||
if (!this.previousVisibleTo.containsKey(ref)) {
|
||||
this.newlyVisibleTo.put(ref, entityViewer);
|
||||
this.newlyVisibleTo.put(ref, entityViewerComponent);
|
||||
}
|
||||
} finally {
|
||||
this.lock.unlockWrite(stamp);
|
||||
|
||||
@@ -13,10 +13,11 @@ import com.hypixel.hytale.component.dependency.SystemDependency;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.hypixel.hytale.protocol.ComponentUpdate;
|
||||
import com.hypixel.hytale.protocol.ComponentUpdateType;
|
||||
import com.hypixel.hytale.protocol.Equipment;
|
||||
import com.hypixel.hytale.protocol.EquipmentUpdate;
|
||||
import com.hypixel.hytale.protocol.ItemArmorSlot;
|
||||
import com.hypixel.hytale.protocol.ModelUpdate;
|
||||
import com.hypixel.hytale.protocol.PlayerSkinUpdate;
|
||||
import com.hypixel.hytale.protocol.PropUpdate;
|
||||
import com.hypixel.hytale.server.core.asset.type.gameplay.PlayerConfig;
|
||||
import com.hypixel.hytale.server.core.entity.Entity;
|
||||
import com.hypixel.hytale.server.core.entity.EntityUtils;
|
||||
@@ -30,6 +31,7 @@ import com.hypixel.hytale.server.core.modules.entity.EntityModule;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.BoundingBox;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.EntityScaleComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.PropComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.player.PlayerSettings;
|
||||
import com.hypixel.hytale.server.core.modules.entity.player.PlayerSkinComponent;
|
||||
@@ -119,24 +121,35 @@ public class LegacyEntityTrackerSystems {
|
||||
}
|
||||
|
||||
boolean modelOutdated = modelComponent.consumeNetworkOutdated();
|
||||
Ref<EntityStore> ref = archetypeChunk.getReferenceTo(index);
|
||||
boolean isProp = store.getComponent(ref, PropComponent.getComponentType()) != null;
|
||||
if (modelOutdated || scaleOutdated) {
|
||||
queueUpdatesFor(archetypeChunk.getReferenceTo(index), modelComponent, entityScale, visibleComponent.visibleTo);
|
||||
queueUpdatesFor(ref, modelComponent, entityScale, isProp, visibleComponent.visibleTo);
|
||||
} else if (!visibleComponent.newlyVisibleTo.isEmpty()) {
|
||||
queueUpdatesFor(archetypeChunk.getReferenceTo(index), modelComponent, entityScale, visibleComponent.newlyVisibleTo);
|
||||
queueUpdatesFor(ref, modelComponent, entityScale, isProp, visibleComponent.newlyVisibleTo);
|
||||
}
|
||||
}
|
||||
|
||||
private static void queueUpdatesFor(
|
||||
Ref<EntityStore> ref, @Nullable ModelComponent model, float entityScale, @Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo
|
||||
Ref<EntityStore> ref,
|
||||
@Nullable ModelComponent model,
|
||||
float entityScale,
|
||||
boolean isProp,
|
||||
@Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo
|
||||
) {
|
||||
ComponentUpdate update = new ComponentUpdate();
|
||||
update.type = ComponentUpdateType.Model;
|
||||
update.model = model != null ? model.getModel().toPacket() : null;
|
||||
update.entityScale = entityScale;
|
||||
ModelUpdate update = new ModelUpdate(model != null ? model.getModel().toPacket() : null, entityScale);
|
||||
|
||||
for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) {
|
||||
viewer.queueUpdate(ref, update);
|
||||
}
|
||||
|
||||
if (isProp) {
|
||||
PropUpdate propUpdate = new PropUpdate();
|
||||
|
||||
for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) {
|
||||
viewer.queueUpdate(ref, propUpdate);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,8 +211,7 @@ public class LegacyEntityTrackerSystems {
|
||||
private static void queueUpdatesFor(
|
||||
Ref<EntityStore> ref, @Nonnull PlayerSkinComponent component, @Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo
|
||||
) {
|
||||
ComponentUpdate update = new ComponentUpdate();
|
||||
update.type = ComponentUpdateType.PlayerSkin;
|
||||
PlayerSkinUpdate update = new PlayerSkinUpdate();
|
||||
update.skin = component.getPlayerSkin();
|
||||
|
||||
for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) {
|
||||
@@ -261,14 +273,12 @@ public class LegacyEntityTrackerSystems {
|
||||
private static void queueUpdatesFor(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull LivingEntity entity, @Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo
|
||||
) {
|
||||
ComponentUpdate update = new ComponentUpdate();
|
||||
update.type = ComponentUpdateType.Equipment;
|
||||
update.equipment = new Equipment();
|
||||
EquipmentUpdate update = new EquipmentUpdate();
|
||||
Inventory inventory = entity.getInventory();
|
||||
ItemContainer armor = inventory.getArmor();
|
||||
update.equipment.armorIds = new String[armor.getCapacity()];
|
||||
Arrays.fill(update.equipment.armorIds, "");
|
||||
armor.forEachWithMeta((slot, itemStack, armorIds) -> armorIds[slot] = itemStack.getItemId(), update.equipment.armorIds);
|
||||
update.armorIds = new String[armor.getCapacity()];
|
||||
Arrays.fill(update.armorIds, "");
|
||||
armor.forEachWithMeta((slot, itemStack, armorIds) -> armorIds[slot] = itemStack.getItemId(), update.armorIds);
|
||||
Store<EntityStore> store = ref.getStore();
|
||||
PlayerSettings playerSettings = store.getComponent(ref, PlayerSettings.getComponentType());
|
||||
if (playerSettings != null) {
|
||||
@@ -278,26 +288,26 @@ public class LegacyEntityTrackerSystems {
|
||||
.getPlayerConfig()
|
||||
.getArmorVisibilityOption();
|
||||
if (armorVisibilityOption.canHideHelmet() && playerSettings.hideHelmet()) {
|
||||
update.equipment.armorIds[ItemArmorSlot.Head.ordinal()] = "";
|
||||
update.armorIds[ItemArmorSlot.Head.ordinal()] = "";
|
||||
}
|
||||
|
||||
if (armorVisibilityOption.canHideCuirass() && playerSettings.hideCuirass()) {
|
||||
update.equipment.armorIds[ItemArmorSlot.Chest.ordinal()] = "";
|
||||
update.armorIds[ItemArmorSlot.Chest.ordinal()] = "";
|
||||
}
|
||||
|
||||
if (armorVisibilityOption.canHideGauntlets() && playerSettings.hideGauntlets()) {
|
||||
update.equipment.armorIds[ItemArmorSlot.Hands.ordinal()] = "";
|
||||
update.armorIds[ItemArmorSlot.Hands.ordinal()] = "";
|
||||
}
|
||||
|
||||
if (armorVisibilityOption.canHidePants() && playerSettings.hidePants()) {
|
||||
update.equipment.armorIds[ItemArmorSlot.Legs.ordinal()] = "";
|
||||
update.armorIds[ItemArmorSlot.Legs.ordinal()] = "";
|
||||
}
|
||||
}
|
||||
|
||||
ItemStack itemInHand = inventory.getItemInHand();
|
||||
update.equipment.rightHandItemId = itemInHand != null ? itemInHand.getItemId() : "Empty";
|
||||
update.rightHandItemId = itemInHand != null ? itemInHand.getItemId() : "Empty";
|
||||
ItemStack utilityItem = inventory.getUtilityItem();
|
||||
update.equipment.leftHandItemId = utilityItem != null ? utilityItem.getItemId() : "Empty";
|
||||
update.leftHandItemId = utilityItem != null ? utilityItem.getItemId() : "Empty";
|
||||
|
||||
for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) {
|
||||
viewer.queueUpdate(ref, update);
|
||||
|
||||
@@ -1,124 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.modules.interaction.blocktrack;
|
||||
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.KeyedCodec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.Component;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.RemoveReason;
|
||||
import com.hypixel.hytale.component.ResourceType;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.RefSystem;
|
||||
import com.hypixel.hytale.math.util.ChunkUtil;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
|
||||
import com.hypixel.hytale.server.core.modules.block.BlockModule;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.InteractionModule;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
public class TrackedPlacement implements Component<ChunkStore> {
|
||||
public static final BuilderCodec<TrackedPlacement> CODEC = BuilderCodec.builder(TrackedPlacement.class, TrackedPlacement::new)
|
||||
.append(new KeyedCodec<>("BlockName", Codec.STRING), (o, v) -> o.blockName = v, o -> o.blockName)
|
||||
.add()
|
||||
.build();
|
||||
private String blockName;
|
||||
|
||||
public static ComponentType<ChunkStore, TrackedPlacement> getComponentType() {
|
||||
return InteractionModule.get().getTrackedPlacementComponentType();
|
||||
}
|
||||
|
||||
public TrackedPlacement() {
|
||||
}
|
||||
|
||||
public TrackedPlacement(String blockName) {
|
||||
this.blockName = blockName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Component<ChunkStore> clone() {
|
||||
return new TrackedPlacement(this.blockName);
|
||||
}
|
||||
|
||||
public static class OnAddRemove extends RefSystem<ChunkStore> {
|
||||
private static final ComponentType<ChunkStore, TrackedPlacement> COMPONENT_TYPE = TrackedPlacement.getComponentType();
|
||||
private static final ResourceType<ChunkStore, BlockCounter> BLOCK_COUNTER_RESOURCE_TYPE = BlockCounter.getResourceType();
|
||||
|
||||
public OnAddRemove() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<ChunkStore> ref, @Nonnull AddReason reason, @Nonnull Store<ChunkStore> store, @Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
if (reason == AddReason.SPAWN) {
|
||||
BlockModule.BlockStateInfo blockInfo = commandBuffer.getComponent(ref, BlockModule.BlockStateInfo.getComponentType());
|
||||
|
||||
assert blockInfo != null;
|
||||
|
||||
Ref<ChunkStore> chunkRef = blockInfo.getChunkRef();
|
||||
if (chunkRef != null && chunkRef.isValid()) {
|
||||
BlockChunk blockChunk = commandBuffer.getComponent(chunkRef, BlockChunk.getComponentType());
|
||||
|
||||
assert blockChunk != null;
|
||||
|
||||
int blockId = blockChunk.getBlock(
|
||||
ChunkUtil.xFromBlockInColumn(blockInfo.getIndex()),
|
||||
ChunkUtil.yFromBlockInColumn(blockInfo.getIndex()),
|
||||
ChunkUtil.zFromBlockInColumn(blockInfo.getIndex())
|
||||
);
|
||||
BlockType blockType = BlockType.getAssetMap().getAsset(blockId);
|
||||
if (blockType != null) {
|
||||
BlockCounter counter = commandBuffer.getResource(BLOCK_COUNTER_RESOURCE_TYPE);
|
||||
String blockName = blockType.getId();
|
||||
counter.trackBlock(blockName);
|
||||
TrackedPlacement tracked = commandBuffer.getComponent(ref, COMPONENT_TYPE);
|
||||
|
||||
assert tracked != null;
|
||||
|
||||
tracked.blockName = blockName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<ChunkStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<ChunkStore> store, @Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
if (reason == RemoveReason.REMOVE) {
|
||||
TrackedPlacement tracked = commandBuffer.getComponent(ref, COMPONENT_TYPE);
|
||||
|
||||
// HyFix #11: Add null check for tracked component
|
||||
if (tracked == null) {
|
||||
System.out.println("[HyFix] WARNING: TrackedPlacement component was null on entity remove - BlockCounter not decremented");
|
||||
return;
|
||||
}
|
||||
|
||||
String blockName = tracked.blockName;
|
||||
|
||||
// HyFix #11: Add null/empty check for blockName
|
||||
if (blockName == null || blockName.isEmpty()) {
|
||||
System.out.println("[HyFix] WARNING: TrackedPlacement.blockName was null/empty on entity remove - BlockCounter not decremented");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockCounter counter = commandBuffer.getResource(BLOCK_COUNTER_RESOURCE_TYPE);
|
||||
counter.untrackBlock(blockName);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return COMPONENT_TYPE;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -84,7 +84,8 @@ public class PlaceFluidInteraction extends SimpleBlockInteraction {
|
||||
Fluid fluid = Fluid.getAssetMap().getAsset(fluidIndex);
|
||||
Vector3i target = targetBlock;
|
||||
BlockType targetBlockType = world.getBlockType(targetBlock);
|
||||
if (FluidTicker.isSolid(targetBlockType)) {
|
||||
FluidTicker ticker = fluid.getTicker();
|
||||
if (!ticker.canOccupySolidBlocks() && FluidTicker.isSolid(targetBlockType)) {
|
||||
target = targetBlock.clone();
|
||||
BlockFace face = BlockFace.fromProtocolFace(context.getClientState().blockFace);
|
||||
target.add(face.getDirection());
|
||||
|
||||
@@ -1,980 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe;
|
||||
|
||||
import com.hypixel.hytale.assetstore.AssetRegistry;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
|
||||
import com.hypixel.hytale.codec.lookup.Priority;
|
||||
import com.hypixel.hytale.common.plugin.PluginIdentifier;
|
||||
import com.hypixel.hytale.common.plugin.PluginManifest;
|
||||
import com.hypixel.hytale.common.semver.SemverRange;
|
||||
import com.hypixel.hytale.common.util.CompletableFutureUtil;
|
||||
import com.hypixel.hytale.component.ComponentRegistryProxy;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.ResourceType;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.event.EventRegistry;
|
||||
import com.hypixel.hytale.event.IEventDispatcher;
|
||||
import com.hypixel.hytale.math.vector.Transform;
|
||||
import com.hypixel.hytale.metrics.MetricProvider;
|
||||
import com.hypixel.hytale.metrics.MetricResults;
|
||||
import com.hypixel.hytale.metrics.MetricsRegistry;
|
||||
import com.hypixel.hytale.protocol.Packet;
|
||||
import com.hypixel.hytale.protocol.PlayerSkin;
|
||||
import com.hypixel.hytale.protocol.packets.setup.ServerTags;
|
||||
import com.hypixel.hytale.server.core.Constants;
|
||||
import com.hypixel.hytale.server.core.HytaleServer;
|
||||
import com.hypixel.hytale.server.core.HytaleServerConfig;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.NameMatching;
|
||||
import com.hypixel.hytale.server.core.Options;
|
||||
import com.hypixel.hytale.server.core.auth.PlayerAuthentication;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandRegistry;
|
||||
import com.hypixel.hytale.server.core.cosmetics.CosmeticsModule;
|
||||
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerConfigData;
|
||||
import com.hypixel.hytale.server.core.event.events.PrepareUniverseEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.ShutdownEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerConnectEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
|
||||
import com.hypixel.hytale.server.core.io.PacketHandler;
|
||||
import com.hypixel.hytale.server.core.io.ProtocolVersion;
|
||||
import com.hypixel.hytale.server.core.io.handlers.game.GamePacketHandler;
|
||||
import com.hypixel.hytale.server.core.io.netty.NettyUtil;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.MovementAudioComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.PositionDataComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.player.ChunkTracker;
|
||||
import com.hypixel.hytale.server.core.modules.entity.player.PlayerConnectionFlushSystem;
|
||||
import com.hypixel.hytale.server.core.modules.entity.player.PlayerPingSystem;
|
||||
import com.hypixel.hytale.server.core.modules.entity.player.PlayerSkinComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.tracker.EntityTrackerSystems;
|
||||
import com.hypixel.hytale.server.core.modules.singleplayer.SingleplayerModule;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPlugin;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||
import com.hypixel.hytale.server.core.plugin.PluginManager;
|
||||
import com.hypixel.hytale.server.core.receiver.IMessageReceiver;
|
||||
import com.hypixel.hytale.server.core.universe.playerdata.PlayerStorage;
|
||||
import com.hypixel.hytale.server.core.universe.system.PlayerRefAddedSystem;
|
||||
import com.hypixel.hytale.server.core.universe.system.WorldConfigSaveSystem;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.WorldConfig;
|
||||
import com.hypixel.hytale.server.core.universe.world.WorldConfigProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.commands.SetTickingCommand;
|
||||
import com.hypixel.hytale.server.core.universe.world.commands.block.BlockCommand;
|
||||
import com.hypixel.hytale.server.core.universe.world.commands.block.BlockSelectCommand;
|
||||
import com.hypixel.hytale.server.core.universe.world.commands.world.WorldCommand;
|
||||
import com.hypixel.hytale.server.core.universe.world.events.AddWorldEvent;
|
||||
import com.hypixel.hytale.server.core.universe.world.events.AllWorldsLoadedEvent;
|
||||
import com.hypixel.hytale.server.core.universe.world.events.RemoveWorldEvent;
|
||||
import com.hypixel.hytale.server.core.universe.world.spawn.FitToHeightMapSpawnProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.spawn.GlobalSpawnProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.spawn.ISpawnProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.spawn.IndividualSpawnProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.component.ChunkSavingSystems;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.DefaultChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.EmptyChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.IChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.IndexedStorageChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.MigrationChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.resources.DefaultResourceStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.resources.DiskResourceStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.resources.EmptyResourceStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.resources.IResourceStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.system.WorldPregenerateSystem;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.DummyWorldGenProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.FlatWorldGenProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.IWorldGenProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldgen.provider.VoidWorldGenProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldmap.provider.DisabledWorldMapProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldmap.provider.IWorldMapProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldmap.provider.chunk.WorldGenWorldMapProvider;
|
||||
import com.hypixel.hytale.server.core.util.AssetUtil;
|
||||
import com.hypixel.hytale.server.core.util.BsonUtil;
|
||||
import com.hypixel.hytale.server.core.util.backup.BackupTask;
|
||||
import com.hypixel.hytale.server.core.util.io.FileUtil;
|
||||
import com.hypixel.hytale.sneakythrow.SneakyThrow;
|
||||
import io.netty.channel.Channel;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import joptsimple.OptionSet;
|
||||
import org.bson.BsonDocument;
|
||||
import org.bson.BsonValue;
|
||||
|
||||
import javax.annotation.CheckReturnValue;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.DirectoryStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.ConcurrentModificationException;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.BiPredicate;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class Universe extends JavaPlugin implements IMessageReceiver, MetricProvider {
|
||||
@Nonnull
|
||||
public static final PluginManifest MANIFEST = PluginManifest.corePlugin(Universe.class).build();
|
||||
@Nonnull
|
||||
private static Map<Integer, String> LEGACY_BLOCK_ID_MAP = Collections.emptyMap();
|
||||
@Nonnull
|
||||
public static final MetricsRegistry<Universe> METRICS_REGISTRY = new MetricsRegistry<Universe>()
|
||||
.register("Worlds", universe -> universe.getWorlds().values().toArray(World[]::new), new ArrayCodec<>(World.METRICS_REGISTRY, World[]::new))
|
||||
.register("PlayerCount", Universe::getPlayerCount, Codec.INTEGER);
|
||||
private static Universe instance;
|
||||
private ComponentType<EntityStore, PlayerRef> playerRefComponentType;
|
||||
@Nonnull
|
||||
private final Path path = Constants.UNIVERSE_PATH;
|
||||
@Nonnull
|
||||
private final Map<UUID, PlayerRef> players = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Map<String, World> worlds = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Map<UUID, World> worldsByUuid = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Map<String, World> unmodifiableWorlds = Collections.unmodifiableMap(this.worlds);
|
||||
private PlayerStorage playerStorage;
|
||||
private WorldConfigProvider worldConfigProvider;
|
||||
private ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> indexedStorageCacheResourceType;
|
||||
private CompletableFuture<Void> universeReady;
|
||||
|
||||
public static Universe get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Universe(@Nonnull JavaPluginInit init) {
|
||||
super(init);
|
||||
instance = this;
|
||||
if (!Files.isDirectory(this.path) && !Options.getOptionSet().has(Options.BARE)) {
|
||||
try {
|
||||
Files.createDirectories(this.path);
|
||||
} catch (IOException var3) {
|
||||
throw new RuntimeException("Failed to create universe directory", var3);
|
||||
}
|
||||
}
|
||||
|
||||
if (Options.getOptionSet().has(Options.BACKUP)) {
|
||||
int frequencyMinutes = Math.max(Options.getOptionSet().valueOf(Options.BACKUP_FREQUENCY_MINUTES), 1);
|
||||
this.getLogger().at(Level.INFO).log("Scheduled backup to run every %d minute(s)", frequencyMinutes);
|
||||
HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
this.getLogger().at(Level.INFO).log("Backing up universe...");
|
||||
this.runBackup().thenAccept(aVoid -> this.getLogger().at(Level.INFO).log("Completed scheduled backup."));
|
||||
} catch (Exception var2x) {
|
||||
this.getLogger().at(Level.SEVERE).withCause(var2x).log("Error backing up universe");
|
||||
}
|
||||
}, frequencyMinutes, frequencyMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Void> runBackup() {
|
||||
return CompletableFuture.allOf(this.worlds.values().stream().map(world -> CompletableFuture.<ChunkSavingSystems.Data>supplyAsync(() -> {
|
||||
Store<ChunkStore> componentStore = world.getChunkStore().getStore();
|
||||
ChunkSavingSystems.Data data = componentStore.getResource(ChunkStore.SAVE_RESOURCE);
|
||||
data.isSaving = false;
|
||||
return data;
|
||||
}, world).thenCompose(ChunkSavingSystems.Data::waitForSavingChunks)).toArray(CompletableFuture[]::new))
|
||||
.thenCompose(aVoid -> BackupTask.start(this.path, Options.getOptionSet().valueOf(Options.BACKUP_DIRECTORY)))
|
||||
.thenCompose(success -> CompletableFuture.allOf(this.worlds.values().stream().map(world -> CompletableFuture.runAsync(() -> {
|
||||
Store<ChunkStore> componentStore = world.getChunkStore().getStore();
|
||||
ChunkSavingSystems.Data data = componentStore.getResource(ChunkStore.SAVE_RESOURCE);
|
||||
data.isSaving = true;
|
||||
}, world)).toArray(CompletableFuture[]::new)).thenApply(aVoid -> success));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setup() {
|
||||
EventRegistry eventRegistry = this.getEventRegistry();
|
||||
ComponentRegistryProxy<ChunkStore> chunkStoreRegistry = this.getChunkStoreRegistry();
|
||||
ComponentRegistryProxy<EntityStore> entityStoreRegistry = this.getEntityStoreRegistry();
|
||||
CommandRegistry commandRegistry = this.getCommandRegistry();
|
||||
eventRegistry.register((short) -48, ShutdownEvent.class, event -> this.disconnectAllPLayers());
|
||||
eventRegistry.register((short) -32, ShutdownEvent.class, event -> this.shutdownAllWorlds());
|
||||
ISpawnProvider.CODEC.register("Global", GlobalSpawnProvider.class, GlobalSpawnProvider.CODEC);
|
||||
ISpawnProvider.CODEC.register("Individual", IndividualSpawnProvider.class, IndividualSpawnProvider.CODEC);
|
||||
ISpawnProvider.CODEC.register("FitToHeightMap", FitToHeightMapSpawnProvider.class, FitToHeightMapSpawnProvider.CODEC);
|
||||
IWorldGenProvider.CODEC.register("Flat", FlatWorldGenProvider.class, FlatWorldGenProvider.CODEC);
|
||||
IWorldGenProvider.CODEC.register("Dummy", DummyWorldGenProvider.class, DummyWorldGenProvider.CODEC);
|
||||
IWorldGenProvider.CODEC.register(Priority.DEFAULT, "Void", VoidWorldGenProvider.class, VoidWorldGenProvider.CODEC);
|
||||
IWorldMapProvider.CODEC.register("Disabled", DisabledWorldMapProvider.class, DisabledWorldMapProvider.CODEC);
|
||||
IWorldMapProvider.CODEC.register(Priority.DEFAULT, "WorldGen", WorldGenWorldMapProvider.class, WorldGenWorldMapProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register(Priority.DEFAULT, "Hytale", DefaultChunkStorageProvider.class, DefaultChunkStorageProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register("Migration", MigrationChunkStorageProvider.class, MigrationChunkStorageProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register("IndexedStorage", IndexedStorageChunkStorageProvider.class, IndexedStorageChunkStorageProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register("Empty", EmptyChunkStorageProvider.class, EmptyChunkStorageProvider.CODEC);
|
||||
IResourceStorageProvider.CODEC.register(Priority.DEFAULT, "Hytale", DefaultResourceStorageProvider.class, DefaultResourceStorageProvider.CODEC);
|
||||
IResourceStorageProvider.CODEC.register("Disk", DiskResourceStorageProvider.class, DiskResourceStorageProvider.CODEC);
|
||||
IResourceStorageProvider.CODEC.register("Empty", EmptyResourceStorageProvider.class, EmptyResourceStorageProvider.CODEC);
|
||||
this.indexedStorageCacheResourceType = chunkStoreRegistry.registerResource(
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache.class, IndexedStorageChunkStorageProvider.IndexedStorageCache::new
|
||||
);
|
||||
chunkStoreRegistry.registerSystem(new IndexedStorageChunkStorageProvider.IndexedStorageCacheSetupSystem());
|
||||
chunkStoreRegistry.registerSystem(new WorldPregenerateSystem());
|
||||
entityStoreRegistry.registerSystem(new WorldConfigSaveSystem());
|
||||
this.playerRefComponentType = entityStoreRegistry.registerComponent(PlayerRef.class, () -> {
|
||||
throw new UnsupportedOperationException();
|
||||
});
|
||||
entityStoreRegistry.registerSystem(new PlayerPingSystem());
|
||||
entityStoreRegistry.registerSystem(new PlayerConnectionFlushSystem(this.playerRefComponentType));
|
||||
entityStoreRegistry.registerSystem(new PlayerRefAddedSystem(this.playerRefComponentType));
|
||||
commandRegistry.registerCommand(new SetTickingCommand());
|
||||
commandRegistry.registerCommand(new BlockCommand());
|
||||
commandRegistry.registerCommand(new BlockSelectCommand());
|
||||
commandRegistry.registerCommand(new WorldCommand());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void start() {
|
||||
HytaleServerConfig config = HytaleServer.get().getConfig();
|
||||
if (config == null) {
|
||||
throw new IllegalStateException("Server config is not loaded!");
|
||||
} else {
|
||||
this.playerStorage = config.getPlayerStorageProvider().getPlayerStorage();
|
||||
WorldConfigProvider.Default defaultConfigProvider = new WorldConfigProvider.Default();
|
||||
PrepareUniverseEvent event = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.<Void, PrepareUniverseEvent>dispatchFor(PrepareUniverseEvent.class)
|
||||
.dispatch(new PrepareUniverseEvent(defaultConfigProvider));
|
||||
WorldConfigProvider worldConfigProvider = event.getWorldConfigProvider();
|
||||
if (worldConfigProvider == null) {
|
||||
worldConfigProvider = defaultConfigProvider;
|
||||
}
|
||||
|
||||
this.worldConfigProvider = worldConfigProvider;
|
||||
|
||||
try {
|
||||
Path blockIdMapPath = this.path.resolve("blockIdMap.json");
|
||||
Path path = this.path.resolve("blockIdMap.legacy.json");
|
||||
if (Files.isRegularFile(blockIdMapPath)) {
|
||||
Files.move(blockIdMapPath, path, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
Files.deleteIfExists(this.path.resolve("blockIdMap.json.bak"));
|
||||
if (Files.isRegularFile(path)) {
|
||||
Map<Integer, String> map = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
for (BsonValue bsonValue : BsonUtil.readDocument(path).thenApply(document -> document.getArray("Blocks")).join()) {
|
||||
BsonDocument bsonDocument = bsonValue.asDocument();
|
||||
map.put(bsonDocument.getNumber("Id").intValue(), bsonDocument.getString("BlockType").getValue());
|
||||
}
|
||||
|
||||
LEGACY_BLOCK_ID_MAP = Collections.unmodifiableMap(map);
|
||||
}
|
||||
} catch (IOException var14) {
|
||||
this.getLogger().at(Level.SEVERE).withCause(var14).log("Failed to delete blockIdMap.json");
|
||||
}
|
||||
|
||||
if (Options.getOptionSet().has(Options.BARE)) {
|
||||
this.universeReady = CompletableFuture.completedFuture(null);
|
||||
HytaleServer.get().getEventBus().dispatch(AllWorldsLoadedEvent.class);
|
||||
} else {
|
||||
ObjectArrayList<CompletableFuture<?>> loadingWorlds = new ObjectArrayList<>();
|
||||
|
||||
try {
|
||||
Path worldsPath = this.path.resolve("worlds");
|
||||
Files.createDirectories(worldsPath);
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(worldsPath)) {
|
||||
for (Path file : stream) {
|
||||
if (HytaleServer.get().isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.equals(worldsPath) && Files.isDirectory(file)) {
|
||||
String name = file.getFileName().toString();
|
||||
if (this.getWorld(name) == null) {
|
||||
loadingWorlds.add(this.loadWorldFromStart(file, name).exceptionally(throwable -> {
|
||||
this.getLogger().at(Level.SEVERE).withCause(throwable).log("Failed to load world: %s", name);
|
||||
return null;
|
||||
}));
|
||||
} else {
|
||||
this.getLogger().at(Level.SEVERE).log("Skipping loading world '%s' because it already exists!", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.universeReady = CompletableFutureUtil._catch(
|
||||
CompletableFuture.allOf(loadingWorlds.toArray(CompletableFuture[]::new))
|
||||
.thenCompose(
|
||||
v -> {
|
||||
String worldName = config.getDefaults().getWorld();
|
||||
return worldName != null && !this.worlds.containsKey(worldName.toLowerCase())
|
||||
? CompletableFutureUtil._catch(this.addWorld(worldName))
|
||||
: CompletableFuture.completedFuture(null);
|
||||
}
|
||||
)
|
||||
.thenRun(() -> HytaleServer.get().getEventBus().dispatch(AllWorldsLoadedEvent.class))
|
||||
);
|
||||
} catch (IOException var13) {
|
||||
throw new RuntimeException("Failed to load Worlds", var13);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutdown() {
|
||||
this.disconnectAllPLayers();
|
||||
this.shutdownAllWorlds();
|
||||
}
|
||||
|
||||
public void disconnectAllPLayers() {
|
||||
this.players.values().forEach(player -> player.getPacketHandler().disconnect("Stopping server!"));
|
||||
}
|
||||
|
||||
public void shutdownAllWorlds() {
|
||||
Iterator<World> iterator = this.worlds.values().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
World world = iterator.next();
|
||||
world.stop();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MetricResults toMetricResults() {
|
||||
return METRICS_REGISTRY.toMetricResults(this);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> getUniverseReady() {
|
||||
return this.universeReady;
|
||||
}
|
||||
|
||||
public ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> getIndexedStorageCacheResourceType() {
|
||||
return this.indexedStorageCacheResourceType;
|
||||
}
|
||||
|
||||
public boolean isWorldLoadable(@Nonnull String name) {
|
||||
Path savePath = this.path.resolve("worlds").resolve(name);
|
||||
return Files.isDirectory(savePath) && (Files.exists(savePath.resolve("config.bson")) || Files.exists(savePath.resolve("config.json")));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> addWorld(@Nonnull String name) {
|
||||
return this.addWorld(name, null, null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Deprecated
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> addWorld(@Nonnull String name, @Nullable String generatorType, @Nullable String chunkStorageType) {
|
||||
if (this.worlds.containsKey(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already exists!");
|
||||
} else if (this.isWorldLoadable(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already exists on disk!");
|
||||
} else {
|
||||
Path savePath = this.path.resolve("worlds").resolve(name);
|
||||
return this.worldConfigProvider.load(savePath, name).thenCompose(worldConfig -> {
|
||||
if (generatorType != null && !"default".equals(generatorType)) {
|
||||
BuilderCodec<? extends IWorldGenProvider> providerCodec = IWorldGenProvider.CODEC.getCodecFor(generatorType);
|
||||
if (providerCodec == null) {
|
||||
throw new IllegalArgumentException("Unknown generatorType '" + generatorType + "'");
|
||||
}
|
||||
|
||||
IWorldGenProvider provider = providerCodec.getDefaultValue();
|
||||
worldConfig.setWorldGenProvider(provider);
|
||||
worldConfig.markChanged();
|
||||
}
|
||||
|
||||
if (chunkStorageType != null && !"default".equals(chunkStorageType)) {
|
||||
BuilderCodec<? extends IChunkStorageProvider> providerCodec = IChunkStorageProvider.CODEC.getCodecFor(chunkStorageType);
|
||||
if (providerCodec == null) {
|
||||
throw new IllegalArgumentException("Unknown chunkStorageType '" + chunkStorageType + "'");
|
||||
}
|
||||
|
||||
IChunkStorageProvider provider = providerCodec.getDefaultValue();
|
||||
worldConfig.setChunkStorageProvider(provider);
|
||||
worldConfig.markChanged();
|
||||
}
|
||||
|
||||
return this.makeWorld(name, savePath, worldConfig);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> makeWorld(@Nonnull String name, @Nonnull Path savePath, @Nonnull WorldConfig worldConfig) {
|
||||
return this.makeWorld(name, savePath, worldConfig, true);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> makeWorld(@Nonnull String name, @Nonnull Path savePath, @Nonnull WorldConfig worldConfig, boolean start) {
|
||||
Map<PluginIdentifier, SemverRange> map = worldConfig.getRequiredPlugins();
|
||||
if (map != null) {
|
||||
PluginManager pluginManager = PluginManager.get();
|
||||
|
||||
for (Entry<PluginIdentifier, SemverRange> entry : map.entrySet()) {
|
||||
if (!pluginManager.hasPlugin(entry.getKey(), entry.getValue())) {
|
||||
this.getLogger().at(Level.SEVERE).log("Failed to load world! Missing plugin: %s, Version: %s", entry.getKey(), entry.getValue());
|
||||
throw new IllegalStateException("Missing plugin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.worlds.containsKey(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already exists!");
|
||||
} else {
|
||||
return CompletableFuture.supplyAsync(
|
||||
SneakyThrow.sneakySupplier(
|
||||
() -> {
|
||||
World world = new World(name, savePath, worldConfig);
|
||||
AddWorldEvent event = HytaleServer.get().getEventBus().dispatchFor(AddWorldEvent.class, name).dispatch(new AddWorldEvent(world));
|
||||
if (!event.isCancelled() && !HytaleServer.get().isShuttingDown()) {
|
||||
World oldWorldByName = this.worlds.putIfAbsent(name.toLowerCase(), world);
|
||||
if (oldWorldByName != null) {
|
||||
throw new ConcurrentModificationException(
|
||||
"World with name " + name + " already exists but didn't before! Looks like you have a race condition."
|
||||
);
|
||||
} else {
|
||||
World oldWorldByUuid = this.worldsByUuid.putIfAbsent(worldConfig.getUuid(), world);
|
||||
if (oldWorldByUuid != null) {
|
||||
throw new ConcurrentModificationException(
|
||||
"World with UUID " + worldConfig.getUuid() + " already exists but didn't before! Looks like you have a race condition."
|
||||
);
|
||||
} else {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new WorldLoadCancelledException();
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
.thenCompose(World::init)
|
||||
.thenCompose(
|
||||
world -> !Options.getOptionSet().has(Options.MIGRATIONS) && start
|
||||
? world.start().thenApply(v -> world)
|
||||
: CompletableFuture.completedFuture(world)
|
||||
)
|
||||
.whenComplete((world, throwable) -> {
|
||||
if (throwable != null) {
|
||||
String nameLower = name.toLowerCase();
|
||||
if (this.worlds.containsKey(nameLower)) {
|
||||
try {
|
||||
this.removeWorldExceptionally(name);
|
||||
} catch (Exception var6x) {
|
||||
this.getLogger().at(Level.WARNING).withCause(var6x).log("Failed to clean up world '%s' after init failure", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadWorldFromStart(@Nonnull Path savePath, @Nonnull String name) {
|
||||
return this.worldConfigProvider
|
||||
.load(savePath, name)
|
||||
.thenCompose(worldConfig -> worldConfig.isDeleteOnUniverseStart() ? CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
FileUtil.deleteDirectory(savePath);
|
||||
this.getLogger().at(Level.INFO).log("Deleted world " + name + " from DeleteOnUniverseStart flag on universe start at " + savePath);
|
||||
} catch (Throwable var4) {
|
||||
throw new RuntimeException("Error deleting world directory on universe start", var4);
|
||||
}
|
||||
}) : this.makeWorld(name, savePath, worldConfig).thenApply(x -> null));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> loadWorld(@Nonnull String name) {
|
||||
if (this.worlds.containsKey(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already loaded!");
|
||||
} else {
|
||||
Path savePath = this.path.resolve("worlds").resolve(name);
|
||||
if (!Files.isDirectory(savePath)) {
|
||||
throw new IllegalArgumentException("World " + name + " does not exist!");
|
||||
} else {
|
||||
return this.worldConfigProvider.load(savePath, name).thenCompose(worldConfig -> this.makeWorld(name, savePath, worldConfig));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public World getWorld(@Nullable String worldName) {
|
||||
return worldName == null ? null : this.worlds.get(worldName.toLowerCase());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public World getWorld(@Nonnull UUID uuid) {
|
||||
return this.worldsByUuid.get(uuid);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public World getDefaultWorld() {
|
||||
HytaleServerConfig config = HytaleServer.get().getConfig();
|
||||
if (config == null) {
|
||||
return null;
|
||||
} else {
|
||||
String worldName = config.getDefaults().getWorld();
|
||||
return worldName != null ? this.getWorld(worldName) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeWorld(@Nonnull String name) {
|
||||
Objects.requireNonNull(name, "Name can't be null!");
|
||||
String nameLower = name.toLowerCase();
|
||||
World world = this.worlds.get(nameLower);
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World " + name + " doesn't exist!");
|
||||
} else {
|
||||
RemoveWorldEvent event = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(RemoveWorldEvent.class, name)
|
||||
.dispatch(new RemoveWorldEvent(world, RemoveWorldEvent.RemovalReason.GENERAL));
|
||||
if (event.isCancelled()) {
|
||||
return false;
|
||||
} else {
|
||||
this.worlds.remove(nameLower);
|
||||
this.worldsByUuid.remove(world.getWorldConfig().getUuid());
|
||||
if (world.isAlive()) {
|
||||
world.stopIndividualWorld();
|
||||
}
|
||||
|
||||
world.validateDeleteOnRemove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeWorldExceptionally(@Nonnull String name) {
|
||||
Objects.requireNonNull(name, "Name can't be null!");
|
||||
this.getLogger().at(Level.INFO).log("Removing world exceptionally: %s", name);
|
||||
String nameLower = name.toLowerCase();
|
||||
World world = this.worlds.get(nameLower);
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World " + name + " doesn't exist!");
|
||||
} else {
|
||||
HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(RemoveWorldEvent.class, name)
|
||||
.dispatch(new RemoveWorldEvent(world, RemoveWorldEvent.RemovalReason.EXCEPTIONAL));
|
||||
this.worlds.remove(nameLower);
|
||||
this.worldsByUuid.remove(world.getWorldConfig().getUuid());
|
||||
if (world.isAlive()) {
|
||||
world.stopIndividualWorld();
|
||||
}
|
||||
|
||||
world.validateDeleteOnRemove();
|
||||
|
||||
// HyFix: Shut down server if default world crashes exceptionally
|
||||
// This prevents players from being stuck in a broken state
|
||||
String defaultWorldName = HytaleServer.get().getConfig().getDefaults().getWorld();
|
||||
if (defaultWorldName != null && defaultWorldName.equalsIgnoreCase(name)) {
|
||||
this.getLogger().at(Level.SEVERE).log("Default world '%s' crashed! Shutting down server.", name);
|
||||
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> {
|
||||
HytaleServer.get().shutdownServer();
|
||||
}, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Path getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Map<String, World> getWorlds() {
|
||||
return this.unmodifiableWorlds;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<PlayerRef> getPlayers() {
|
||||
return new ObjectArrayList<>(this.players.values());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayer(@Nonnull UUID uuid) {
|
||||
return this.players.get(uuid);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayer(@Nonnull String value, @Nonnull NameMatching matching) {
|
||||
return matching.find(this.players.values(), value, v -> v.getComponent(PlayerRef.getComponentType()).getUsername());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayer(@Nonnull String value, @Nonnull Comparator<String> comparator, @Nonnull BiPredicate<String, String> equality) {
|
||||
return NameMatching.find(this.players.values(), value, v -> v.getComponent(PlayerRef.getComponentType()).getUsername(), comparator, equality);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayerByUsername(@Nonnull String value, @Nonnull NameMatching matching) {
|
||||
return matching.find(this.players.values(), value, PlayerRef::getUsername);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayerByUsername(@Nonnull String value, @Nonnull Comparator<String> comparator, @Nonnull BiPredicate<String, String> equality) {
|
||||
return NameMatching.find(this.players.values(), value, PlayerRef::getUsername, comparator, equality);
|
||||
}
|
||||
|
||||
public int getPlayerCount() {
|
||||
return this.players.size();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> addPlayer(
|
||||
@Nonnull Channel channel,
|
||||
@Nonnull String language,
|
||||
@Nonnull ProtocolVersion protocolVersion,
|
||||
@Nonnull UUID uuid,
|
||||
@Nonnull String username,
|
||||
@Nonnull PlayerAuthentication auth,
|
||||
int clientViewRadiusChunks,
|
||||
@Nullable PlayerSkin skin
|
||||
) {
|
||||
GamePacketHandler playerConnection = new GamePacketHandler(channel, protocolVersion, auth);
|
||||
playerConnection.setQueuePackets(false);
|
||||
this.getLogger().at(Level.INFO).log("Adding player '%s (%s)", username, uuid);
|
||||
return this.playerStorage
|
||||
.load(uuid)
|
||||
.exceptionally(throwable -> {
|
||||
throw new RuntimeException("Exception when adding player to universe:", throwable);
|
||||
})
|
||||
.thenCompose(
|
||||
holder -> {
|
||||
ChunkTracker chunkTrackerComponent = new ChunkTracker();
|
||||
PlayerRef playerRefComponent = new PlayerRef((Holder<EntityStore>) holder, uuid, username, language, playerConnection, chunkTrackerComponent);
|
||||
chunkTrackerComponent.setDefaultMaxChunksPerSecond(playerRefComponent);
|
||||
holder.putComponent(PlayerRef.getComponentType(), playerRefComponent);
|
||||
holder.putComponent(ChunkTracker.getComponentType(), chunkTrackerComponent);
|
||||
holder.putComponent(UUIDComponent.getComponentType(), new UUIDComponent(uuid));
|
||||
holder.ensureComponent(PositionDataComponent.getComponentType());
|
||||
holder.ensureComponent(MovementAudioComponent.getComponentType());
|
||||
Player playerComponent = holder.ensureAndGetComponent(Player.getComponentType());
|
||||
playerComponent.init(uuid, playerRefComponent);
|
||||
PlayerConfigData playerConfig = playerComponent.getPlayerConfigData();
|
||||
playerConfig.cleanup(this);
|
||||
PacketHandler.logConnectionTimings(channel, "Load Player Config", Level.FINEST);
|
||||
if (skin != null) {
|
||||
holder.putComponent(PlayerSkinComponent.getComponentType(), new PlayerSkinComponent(skin));
|
||||
holder.putComponent(ModelComponent.getComponentType(), new ModelComponent(CosmeticsModule.get().createModel(skin)));
|
||||
}
|
||||
|
||||
playerConnection.setPlayerRef(playerRefComponent, playerComponent);
|
||||
NettyUtil.setChannelHandler(channel, playerConnection);
|
||||
playerComponent.setClientViewRadius(clientViewRadiusChunks);
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = holder.getComponent(EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (entityViewerComponent != null) {
|
||||
entityViewerComponent.viewRadiusBlocks = playerComponent.getViewRadius() * 32;
|
||||
} else {
|
||||
entityViewerComponent = new EntityTrackerSystems.EntityViewer(playerComponent.getViewRadius() * 32, playerConnection);
|
||||
holder.addComponent(EntityTrackerSystems.EntityViewer.getComponentType(), entityViewerComponent);
|
||||
}
|
||||
|
||||
PlayerRef existingPlayer = this.players.putIfAbsent(uuid, playerRefComponent);
|
||||
if (existingPlayer != null) {
|
||||
this.getLogger().at(Level.WARNING).log("Player '%s' (%s) already joining from another connection, rejecting duplicate", username, uuid);
|
||||
playerConnection.disconnect("A connection with this account is already in progress");
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
String lastWorldName = playerConfig.getWorld();
|
||||
World lastWorld = this.getWorld(lastWorldName);
|
||||
PlayerConnectEvent event = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.<Void, PlayerConnectEvent>dispatchFor(PlayerConnectEvent.class)
|
||||
.dispatch(new PlayerConnectEvent((Holder<EntityStore>) holder, playerRefComponent, lastWorld != null ? lastWorld : this.getDefaultWorld()));
|
||||
if (!channel.isActive()) {
|
||||
this.players.remove(uuid, playerRefComponent);
|
||||
this.getLogger().at(Level.INFO).log("Player '%s' (%s) disconnected during PlayerConnectEvent, cleaned up", username, uuid);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
World world = event.getWorld() != null ? event.getWorld() : this.getDefaultWorld();
|
||||
if (world == null) {
|
||||
this.players.remove(uuid, playerRefComponent);
|
||||
playerConnection.disconnect("No world available to join");
|
||||
this.getLogger().at(Level.SEVERE).log("Player '%s' (%s) could not join - no default world configured", username, uuid);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
if (lastWorldName != null && lastWorld == null) {
|
||||
playerComponent.sendMessage(
|
||||
Message.translation("server.universe.failedToFindWorld").param("lastWorldName", lastWorldName).param("name", world.getName())
|
||||
);
|
||||
}
|
||||
|
||||
PacketHandler.logConnectionTimings(channel, "Processed Referral", Level.FINEST);
|
||||
playerRefComponent.getPacketHandler().write(new ServerTags(AssetRegistry.getClientTags()));
|
||||
CompletableFuture<PlayerRef> addPlayerFuture = world.addPlayer(playerRefComponent, null, false, false);
|
||||
if (addPlayerFuture == null) {
|
||||
this.players.remove(uuid, playerRefComponent);
|
||||
this.getLogger().at(Level.INFO).log("Player '%s' (%s) disconnected before world addition, cleaned up", username, uuid);
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
return addPlayerFuture.<PlayerRef>thenApply(
|
||||
p -> {
|
||||
PacketHandler.logConnectionTimings(channel, "Add to World", Level.FINEST);
|
||||
if (!channel.isActive()) {
|
||||
if (p != null) {
|
||||
playerComponent.remove();
|
||||
}
|
||||
|
||||
this.players.remove(uuid, playerRefComponent);
|
||||
this.getLogger()
|
||||
.at(Level.WARNING)
|
||||
.log("Player '%s' (%s) disconnected during world join, cleaned up from universe", username, uuid);
|
||||
return null;
|
||||
} else if (playerComponent.wasRemoved()) {
|
||||
this.players.remove(uuid, playerRefComponent);
|
||||
return null;
|
||||
} else {
|
||||
return (PlayerRef) p;
|
||||
}
|
||||
}
|
||||
)
|
||||
.exceptionally(throwable -> {
|
||||
this.players.remove(uuid, playerRefComponent);
|
||||
playerComponent.remove();
|
||||
throw new RuntimeException("Exception when adding player to universe:", throwable);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public void removePlayer(@Nonnull PlayerRef playerRef) {
|
||||
this.getLogger().at(Level.INFO).log("Removing player '" + playerRef.getUsername() + "' (" + playerRef.getUuid() + ")");
|
||||
IEventDispatcher<PlayerDisconnectEvent, PlayerDisconnectEvent> eventDispatcher = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(PlayerDisconnectEvent.class);
|
||||
if (eventDispatcher.hasListener()) {
|
||||
eventDispatcher.dispatch(new PlayerDisconnectEvent(playerRef));
|
||||
}
|
||||
|
||||
Ref<EntityStore> ref = playerRef.getReference();
|
||||
if (ref == null) {
|
||||
this.finalizePlayerRemoval(playerRef);
|
||||
} else {
|
||||
World world = ref.getStore().getExternalData().getWorld();
|
||||
if (world.isInThread()) {
|
||||
// HyFix: Wrap in try-catch for IllegalStateException with fallback cleanup
|
||||
try {
|
||||
Player playerComponent = ref.getStore().getComponent(ref, Player.getComponentType());
|
||||
if (playerComponent != null) {
|
||||
playerComponent.remove();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
System.err.println("[HyFix] Player ref invalid during removal - performing fallback cleanup");
|
||||
try {
|
||||
playerRef.getChunkTracker().clear();
|
||||
System.err.println("[HyFix] ChunkTracker cleared - memory leak prevented");
|
||||
} catch (Exception cleanupEx) {
|
||||
System.err.println("[HyFix] Fallback cleanup failed - memory may leak");
|
||||
}
|
||||
}
|
||||
|
||||
this.finalizePlayerRemoval(playerRef);
|
||||
} else {
|
||||
CompletableFuture<Void> removedFuture = new CompletableFuture<>();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// HyFix: Wrap in try-catch for IllegalStateException with fallback cleanup
|
||||
try {
|
||||
Player playerComponent = ref.getStore().getComponent(ref, Player.getComponentType());
|
||||
if (playerComponent != null) {
|
||||
playerComponent.remove();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
System.err.println("[HyFix] Player ref invalid during async removal - performing fallback cleanup");
|
||||
try {
|
||||
playerRef.getChunkTracker().clear();
|
||||
System.err.println("[HyFix] ChunkTracker cleared - memory leak prevented");
|
||||
} catch (Exception cleanupEx) {
|
||||
System.err.println("[HyFix] Fallback cleanup failed - memory may leak");
|
||||
}
|
||||
}
|
||||
}, world).whenComplete((unused, throwable) -> {
|
||||
if (throwable != null) {
|
||||
removedFuture.completeExceptionally(throwable);
|
||||
} else {
|
||||
removedFuture.complete(unused);
|
||||
}
|
||||
});
|
||||
removedFuture.orTimeout(5L, TimeUnit.SECONDS)
|
||||
.whenComplete(
|
||||
(result, error) -> {
|
||||
if (error != null) {
|
||||
this.getLogger()
|
||||
.at(Level.WARNING)
|
||||
.withCause(error)
|
||||
.log("Timeout or error waiting for player '%s' removal from world store", playerRef.getUsername());
|
||||
// HyFix: Perform fallback cleanup on timeout
|
||||
try {
|
||||
playerRef.getChunkTracker().clear();
|
||||
System.err.println("[HyFix] ChunkTracker cleared on timeout - memory leak prevented");
|
||||
} catch (Exception cleanupEx) {
|
||||
System.err.println("[HyFix] Fallback cleanup on timeout failed - memory may leak");
|
||||
}
|
||||
}
|
||||
|
||||
this.finalizePlayerRemoval(playerRef);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizePlayerRemoval(@Nonnull PlayerRef playerRef) {
|
||||
this.players.remove(playerRef.getUuid());
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
if (this.players.isEmpty()) {
|
||||
this.getLogger().at(Level.INFO).log("No players left on singleplayer server shutting down!");
|
||||
HytaleServer.get().shutdownServer();
|
||||
} else if (SingleplayerModule.isOwner(playerRef)) {
|
||||
this.getLogger().at(Level.INFO).log("Owner left the singleplayer server shutting down!");
|
||||
this.getPlayers().forEach(p -> p.getPacketHandler().disconnect(playerRef.getUsername() + " left! Shutting down singleplayer world!"));
|
||||
HytaleServer.get().shutdownServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> resetPlayer(@Nonnull PlayerRef oldPlayer) {
|
||||
return this.playerStorage.load(oldPlayer.getUuid()).exceptionally(throwable -> {
|
||||
throw new RuntimeException("Exception when adding player to universe:", throwable);
|
||||
}).thenCompose(holder -> this.resetPlayer(oldPlayer, (Holder<EntityStore>) holder));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> resetPlayer(@Nonnull PlayerRef oldPlayer, @Nonnull Holder<EntityStore> holder) {
|
||||
return this.resetPlayer(oldPlayer, holder, null, null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> resetPlayer(
|
||||
@Nonnull PlayerRef playerRef, @Nonnull Holder<EntityStore> holder, @Nullable World world, @Nullable Transform transform
|
||||
) {
|
||||
UUID uuid = playerRef.getUuid();
|
||||
Player oldPlayer = playerRef.getComponent(Player.getComponentType());
|
||||
World targetWorld;
|
||||
if (world == null) {
|
||||
targetWorld = oldPlayer.getWorld();
|
||||
} else {
|
||||
targetWorld = world;
|
||||
}
|
||||
|
||||
this.getLogger()
|
||||
.at(Level.INFO)
|
||||
.log(
|
||||
"Resetting player '%s', moving to world '%s' at location %s (%s)",
|
||||
playerRef.getUsername(),
|
||||
world != null ? world.getName() : null,
|
||||
transform,
|
||||
playerRef.getUuid()
|
||||
);
|
||||
GamePacketHandler playerConnection = (GamePacketHandler) playerRef.getPacketHandler();
|
||||
Player newPlayer = holder.ensureAndGetComponent(Player.getComponentType());
|
||||
newPlayer.init(uuid, playerRef);
|
||||
CompletableFuture<Void> leaveWorld = new CompletableFuture<>();
|
||||
if (oldPlayer.getWorld() != null) {
|
||||
oldPlayer.getWorld().execute(() -> {
|
||||
playerRef.removeFromStore();
|
||||
leaveWorld.complete(null);
|
||||
});
|
||||
} else {
|
||||
leaveWorld.complete(null);
|
||||
}
|
||||
|
||||
return leaveWorld.thenAccept(v -> {
|
||||
oldPlayer.resetManagers(holder);
|
||||
newPlayer.copyFrom(oldPlayer);
|
||||
EntityTrackerSystems.EntityViewer viewer = holder.getComponent(EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (viewer != null) {
|
||||
viewer.viewRadiusBlocks = newPlayer.getViewRadius() * 32;
|
||||
} else {
|
||||
viewer = new EntityTrackerSystems.EntityViewer(newPlayer.getViewRadius() * 32, playerConnection);
|
||||
holder.addComponent(EntityTrackerSystems.EntityViewer.getComponentType(), viewer);
|
||||
}
|
||||
|
||||
playerConnection.setPlayerRef(playerRef, newPlayer);
|
||||
playerRef.replaceHolder(holder);
|
||||
holder.putComponent(PlayerRef.getComponentType(), playerRef);
|
||||
}).thenCompose(v -> targetWorld.addPlayer(playerRef, transform));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@Nonnull Message message) {
|
||||
for (PlayerRef ref : this.players.values()) {
|
||||
ref.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPacket(@Nonnull Packet packet) {
|
||||
for (PlayerRef player : this.players.values()) {
|
||||
player.getPacketHandler().write(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPacketNoCache(@Nonnull Packet packet) {
|
||||
for (PlayerRef player : this.players.values()) {
|
||||
player.getPacketHandler().writeNoCache(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPacket(@Nonnull Packet... packets) {
|
||||
for (PlayerRef player : this.players.values()) {
|
||||
player.getPacketHandler().write(packets);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerStorage getPlayerStorage() {
|
||||
return this.playerStorage;
|
||||
}
|
||||
|
||||
public void setPlayerStorage(@Nonnull PlayerStorage playerStorage) {
|
||||
this.playerStorage = playerStorage;
|
||||
}
|
||||
|
||||
public WorldConfigProvider getWorldConfigProvider() {
|
||||
return this.worldConfigProvider;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<EntityStore, PlayerRef> getPlayerRefComponentType() {
|
||||
return this.playerRefComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Deprecated
|
||||
public static Map<Integer, String> getLegacyBlockIdMap() {
|
||||
return LEGACY_BLOCK_ID_MAP;
|
||||
}
|
||||
|
||||
public static Path getWorldGenPath() {
|
||||
OptionSet optionSet = Options.getOptionSet();
|
||||
Path worldGenPath;
|
||||
if (optionSet.has(Options.WORLD_GEN_DIRECTORY)) {
|
||||
worldGenPath = optionSet.valueOf(Options.WORLD_GEN_DIRECTORY);
|
||||
} else {
|
||||
worldGenPath = AssetUtil.getHytaleAssetsPath().resolve("Server").resolve("World");
|
||||
}
|
||||
|
||||
return worldGenPath;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,660 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe.world;
|
||||
|
||||
import com.hypixel.hytale.common.fastutil.HLongOpenHashSet;
|
||||
import com.hypixel.hytale.common.fastutil.HLongSet;
|
||||
import com.hypixel.hytale.common.thread.ticking.Tickable;
|
||||
import com.hypixel.hytale.common.util.CompletableFutureUtil;
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.iterator.CircleSpiralIterator;
|
||||
import com.hypixel.hytale.math.shape.Box2D;
|
||||
import com.hypixel.hytale.math.util.ChunkUtil;
|
||||
import com.hypixel.hytale.math.util.MathUtil;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.hypixel.hytale.protocol.SoundCategory;
|
||||
import com.hypixel.hytale.protocol.packets.worldmap.ClearWorldMap;
|
||||
import com.hypixel.hytale.protocol.packets.worldmap.MapChunk;
|
||||
import com.hypixel.hytale.protocol.packets.worldmap.MapImage;
|
||||
import com.hypixel.hytale.protocol.packets.worldmap.MapMarker;
|
||||
import com.hypixel.hytale.protocol.packets.worldmap.UpdateWorldMap;
|
||||
import com.hypixel.hytale.protocol.packets.worldmap.UpdateWorldMapSettings;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.asset.type.soundevent.config.SoundEvent;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.event.events.ecs.DiscoverZoneEvent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapManager;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapSettings;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldmap.markers.MapMarkerTracker;
|
||||
import com.hypixel.hytale.server.core.util.EventTitleUtil;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.longs.LongIterator;
|
||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.locks.ReentrantReadWriteLock;
|
||||
import java.util.function.Predicate;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class WorldMapTracker implements Tickable {
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final float UPDATE_SPEED = 1.0F;
|
||||
public static final int RADIUS_MAX = 512;
|
||||
public static final int EMPTY_UPDATE_WORLD_MAP_SIZE = 13;
|
||||
private static final int EMPTY_MAP_CHUNK_SIZE = 10;
|
||||
private static final int FULL_MAP_CHUNK_SIZE = 23;
|
||||
public static final int MAX_IMAGE_GENERATION = 20;
|
||||
public static final int MAX_FRAME = 2621440;
|
||||
private final Player player;
|
||||
private final CircleSpiralIterator spiralIterator = new CircleSpiralIterator();
|
||||
private final ReentrantReadWriteLock loadedLock = new ReentrantReadWriteLock();
|
||||
private final HLongSet loaded = new HLongOpenHashSet();
|
||||
private final HLongSet pendingReloadChunks = new HLongOpenHashSet();
|
||||
private final Long2ObjectOpenHashMap<CompletableFuture<MapImage>> pendingReloadFutures = new Long2ObjectOpenHashMap<>();
|
||||
private final MapMarkerTracker markerTracker;
|
||||
private float updateTimer;
|
||||
private Integer viewRadiusOverride;
|
||||
private boolean started;
|
||||
private int sentViewRadius;
|
||||
private int lastChunkX;
|
||||
private int lastChunkZ;
|
||||
@Nullable
|
||||
private String currentBiomeName;
|
||||
@Nullable
|
||||
private WorldMapTracker.ZoneDiscoveryInfo currentZone;
|
||||
private boolean clientHasWorldMapVisible;
|
||||
@Nullable
|
||||
private TransformComponent transformComponent;
|
||||
|
||||
public WorldMapTracker(@Nonnull Player player) {
|
||||
this.player = player;
|
||||
this.markerTracker = new MapMarkerTracker(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(float dt) {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
LOGGER.at(Level.INFO).log("Started Generating Map!");
|
||||
}
|
||||
|
||||
World world = this.player.getWorld();
|
||||
if (world != null) {
|
||||
if (this.transformComponent == null) {
|
||||
this.transformComponent = this.player.getTransformComponent();
|
||||
if (this.transformComponent == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WorldMapManager worldMapManager = world.getWorldMapManager();
|
||||
WorldMapSettings worldMapSettings = worldMapManager.getWorldMapSettings();
|
||||
int viewRadius;
|
||||
if (this.viewRadiusOverride != null) {
|
||||
viewRadius = this.viewRadiusOverride;
|
||||
} else {
|
||||
viewRadius = worldMapSettings.getViewRadius(this.player.getViewRadius());
|
||||
}
|
||||
|
||||
Vector3d position = this.transformComponent.getPosition();
|
||||
int playerX = MathUtil.floor(position.getX());
|
||||
int playerZ = MathUtil.floor(position.getZ());
|
||||
int playerChunkX = playerX >> 5;
|
||||
int playerChunkZ = playerZ >> 5;
|
||||
if (world.isCompassUpdating()) {
|
||||
this.markerTracker.updatePointsOfInterest(dt, world, viewRadius, playerChunkX, playerChunkZ);
|
||||
}
|
||||
|
||||
if (worldMapManager.isWorldMapEnabled()) {
|
||||
this.updateWorldMap(world, dt, worldMapSettings, viewRadius, playerChunkX, playerChunkZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateCurrentZoneAndBiome(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nullable WorldMapTracker.ZoneDiscoveryInfo zoneDiscoveryInfo,
|
||||
@Nullable String biomeName,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor
|
||||
) {
|
||||
this.currentBiomeName = biomeName;
|
||||
this.currentZone = zoneDiscoveryInfo;
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
if (!playerComponent.isWaitingForClientReady()) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
if (zoneDiscoveryInfo != null && this.discoverZone(world, zoneDiscoveryInfo.regionName())) {
|
||||
this.onZoneDiscovered(ref, zoneDiscoveryInfo, componentAccessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onZoneDiscovered(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull WorldMapTracker.ZoneDiscoveryInfo zoneDiscoveryInfo, @Nonnull ComponentAccessor<EntityStore> componentAccessor
|
||||
) {
|
||||
WorldMapTracker.ZoneDiscoveryInfo discoverZoneEventInfo = zoneDiscoveryInfo.clone();
|
||||
DiscoverZoneEvent.Display discoverZoneEvent = new DiscoverZoneEvent.Display(discoverZoneEventInfo);
|
||||
componentAccessor.invoke(ref, discoverZoneEvent);
|
||||
if (!discoverZoneEvent.isCancelled() && discoverZoneEventInfo.display()) {
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
EventTitleUtil.showEventTitleToPlayer(
|
||||
playerRefComponent,
|
||||
Message.translation(String.format("server.map.region.%s", discoverZoneEventInfo.regionName())),
|
||||
Message.translation(String.format("server.map.zone.%s", discoverZoneEventInfo.zoneName())),
|
||||
discoverZoneEventInfo.major(),
|
||||
discoverZoneEventInfo.icon(),
|
||||
discoverZoneEventInfo.duration(),
|
||||
discoverZoneEventInfo.fadeInDuration(),
|
||||
discoverZoneEventInfo.fadeOutDuration()
|
||||
);
|
||||
String discoverySoundEventId = discoverZoneEventInfo.discoverySoundEventId();
|
||||
if (discoverySoundEventId != null) {
|
||||
int assetIndex = SoundEvent.getAssetMap().getIndex(discoverySoundEventId);
|
||||
if (assetIndex != Integer.MIN_VALUE) {
|
||||
SoundUtil.playSoundEvent2d(ref, assetIndex, SoundCategory.UI, componentAccessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWorldMap(
|
||||
@Nonnull World world, float dt, @Nonnull WorldMapSettings worldMapSettings, int chunkViewRadius, int playerChunkX, int playerChunkZ
|
||||
) {
|
||||
this.processPendingReloadChunks(world);
|
||||
Box2D worldMapArea = worldMapSettings.getWorldMapArea();
|
||||
if (worldMapArea == null) {
|
||||
int xDiff = Math.abs(this.lastChunkX - playerChunkX);
|
||||
int zDiff = Math.abs(this.lastChunkZ - playerChunkZ);
|
||||
int chunkMoveDistance = xDiff <= 0 && zDiff <= 0 ? 0 : (int) Math.ceil(Math.sqrt(xDiff * xDiff + zDiff * zDiff));
|
||||
this.sentViewRadius = Math.max(0, this.sentViewRadius - chunkMoveDistance);
|
||||
this.lastChunkX = playerChunkX;
|
||||
this.lastChunkZ = playerChunkZ;
|
||||
this.updateTimer -= dt;
|
||||
if (this.updateTimer > 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sentViewRadius != chunkViewRadius) {
|
||||
if (this.sentViewRadius > chunkViewRadius) {
|
||||
this.sentViewRadius = chunkViewRadius;
|
||||
}
|
||||
|
||||
this.unloadImages(chunkViewRadius, playerChunkX, playerChunkZ);
|
||||
if (this.sentViewRadius < chunkViewRadius) {
|
||||
this.loadImages(world, chunkViewRadius, playerChunkX, playerChunkZ, 20);
|
||||
}
|
||||
} else {
|
||||
this.updateTimer = 1.0F;
|
||||
}
|
||||
} else {
|
||||
this.updateTimer -= dt;
|
||||
if (this.updateTimer > 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadWorldMap(world, worldMapArea, 20);
|
||||
}
|
||||
}
|
||||
|
||||
private void unloadImages(int chunkViewRadius, int playerChunkX, int playerChunkZ) {
|
||||
// HyFix #16: Wrap in try-catch for iterator corruption NPE under high load
|
||||
try {
|
||||
List<MapChunk> currentUnloadList = null;
|
||||
List<List<MapChunk>> allUnloadLists = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
int packetSize = 2621427;
|
||||
LongIterator iterator = this.loaded.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
long chunkCoordinates = iterator.nextLong();
|
||||
int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
if (!shouldBeVisible(chunkViewRadius, playerChunkX, playerChunkZ, mapChunkX, mapChunkZ)) {
|
||||
if (currentUnloadList == null) {
|
||||
currentUnloadList = new ObjectArrayList<>(packetSize / 10);
|
||||
}
|
||||
|
||||
currentUnloadList.add(new MapChunk(mapChunkX, mapChunkZ, null));
|
||||
packetSize -= 10;
|
||||
iterator.remove();
|
||||
if (packetSize < 10) {
|
||||
packetSize = 2621427;
|
||||
if (allUnloadLists == null) {
|
||||
allUnloadLists = new ObjectArrayList<>(this.loaded.size() / (packetSize / 10));
|
||||
}
|
||||
|
||||
allUnloadLists.add(currentUnloadList);
|
||||
currentUnloadList = new ObjectArrayList<>(packetSize / 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allUnloadLists != null) {
|
||||
for (List<MapChunk> unloadList : allUnloadLists) {
|
||||
this.writeUpdatePacket(unloadList);
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(currentUnloadList);
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
System.out.println("[HyFix] WARNING: Iterator corruption in WorldMapTracker.unloadImages() - recovered gracefully (Issue #16)");
|
||||
}
|
||||
}
|
||||
|
||||
private void processPendingReloadChunks(@Nonnull World world) {
|
||||
List<MapChunk> chunksToSend = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
if (!this.pendingReloadChunks.isEmpty()) {
|
||||
int imageSize = MathUtil.fastFloor(32.0F * world.getWorldMapManager().getWorldMapSettings().getImageScale());
|
||||
int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
|
||||
int packetSize = 2621427;
|
||||
LongIterator iterator = this.pendingReloadChunks.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
long chunkCoordinates = iterator.nextLong();
|
||||
CompletableFuture<MapImage> future = this.pendingReloadFutures.get(chunkCoordinates);
|
||||
if (future == null) {
|
||||
future = world.getWorldMapManager().getImageAsync(chunkCoordinates);
|
||||
this.pendingReloadFutures.put(chunkCoordinates, future);
|
||||
}
|
||||
|
||||
if (future.isDone()) {
|
||||
iterator.remove();
|
||||
this.pendingReloadFutures.remove(chunkCoordinates);
|
||||
if (chunksToSend == null) {
|
||||
chunksToSend = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
|
||||
int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
chunksToSend.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
|
||||
this.loaded.add(chunkCoordinates);
|
||||
packetSize -= fullMapChunkSize;
|
||||
if (packetSize < fullMapChunkSize) {
|
||||
this.writeUpdatePacket(chunksToSend);
|
||||
chunksToSend = new ObjectArrayList<>(2621440 - 13 / fullMapChunkSize);
|
||||
packetSize = 2621427;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(chunksToSend);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private int loadImages(@Nonnull World world, int chunkViewRadius, int playerChunkX, int playerChunkZ, int maxGeneration) {
|
||||
List<MapChunk> currentLoadList = null;
|
||||
List<List<MapChunk>> allLoadLists = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
int packetSize = 2621427;
|
||||
int imageSize = MathUtil.fastFloor(32.0F * world.getWorldMapManager().getWorldMapSettings().getImageScale());
|
||||
int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
|
||||
boolean areAllLoaded = true;
|
||||
this.spiralIterator.init(playerChunkX, playerChunkZ, this.sentViewRadius, chunkViewRadius);
|
||||
|
||||
while (maxGeneration > 0 && this.spiralIterator.hasNext()) {
|
||||
long chunkCoordinates = this.spiralIterator.next();
|
||||
if (!this.loaded.contains(chunkCoordinates)) {
|
||||
areAllLoaded = false;
|
||||
CompletableFuture<MapImage> future = world.getWorldMapManager().getImageAsync(chunkCoordinates);
|
||||
if (!future.isDone()) {
|
||||
maxGeneration--;
|
||||
} else if (this.loaded.add(chunkCoordinates)) {
|
||||
if (currentLoadList == null) {
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
|
||||
int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
currentLoadList.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
|
||||
packetSize -= fullMapChunkSize;
|
||||
if (packetSize < fullMapChunkSize) {
|
||||
packetSize = 2621427;
|
||||
if (allLoadLists == null) {
|
||||
allLoadLists = new ObjectArrayList<>();
|
||||
}
|
||||
|
||||
allLoadLists.add(currentLoadList);
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
}
|
||||
} else if (areAllLoaded) {
|
||||
this.sentViewRadius = this.spiralIterator.getCompletedRadius();
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllLoaded) {
|
||||
this.sentViewRadius = this.spiralIterator.getCompletedRadius();
|
||||
}
|
||||
|
||||
if (allLoadLists != null) {
|
||||
for (List<MapChunk> unloadList : allLoadLists) {
|
||||
this.writeUpdatePacket(unloadList);
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(currentLoadList);
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
return maxGeneration;
|
||||
}
|
||||
|
||||
private int loadWorldMap(@Nonnull World world, @Nonnull Box2D worldMapArea, int maxGeneration) {
|
||||
List<MapChunk> currentLoadList = null;
|
||||
List<List<MapChunk>> allLoadLists = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
int packetSize = 2621427;
|
||||
int imageSize = MathUtil.fastFloor(32.0F * world.getWorldMapManager().getWorldMapSettings().getImageScale());
|
||||
int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
|
||||
|
||||
for (int mapChunkX = MathUtil.floor(worldMapArea.min.x); mapChunkX < MathUtil.ceil(worldMapArea.max.x) && maxGeneration > 0; mapChunkX++) {
|
||||
for (int mapChunkZ = MathUtil.floor(worldMapArea.min.y); mapChunkZ < MathUtil.ceil(worldMapArea.max.y) && maxGeneration > 0; mapChunkZ++) {
|
||||
long chunkCoordinates = ChunkUtil.indexChunk(mapChunkX, mapChunkZ);
|
||||
if (!this.loaded.contains(chunkCoordinates)) {
|
||||
CompletableFuture<MapImage> future = CompletableFutureUtil._catch(world.getWorldMapManager().getImageAsync(chunkCoordinates));
|
||||
if (!future.isDone()) {
|
||||
maxGeneration--;
|
||||
} else {
|
||||
if (currentLoadList == null) {
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
|
||||
currentLoadList.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
|
||||
this.loaded.add(chunkCoordinates);
|
||||
packetSize -= fullMapChunkSize;
|
||||
if (packetSize < fullMapChunkSize) {
|
||||
packetSize = 2621427;
|
||||
if (allLoadLists == null) {
|
||||
allLoadLists = new ObjectArrayList<>(Math.max(packetSize / fullMapChunkSize, 1));
|
||||
}
|
||||
|
||||
allLoadLists.add(currentLoadList);
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
if (allLoadLists != null) {
|
||||
for (List<MapChunk> unloadList : allLoadLists) {
|
||||
this.writeUpdatePacket(unloadList);
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(currentLoadList);
|
||||
return maxGeneration;
|
||||
}
|
||||
|
||||
private void writeUpdatePacket(@Nullable List<MapChunk> list) {
|
||||
if (list != null) {
|
||||
UpdateWorldMap packet = new UpdateWorldMap(list.toArray(MapChunk[]::new), null, null);
|
||||
LOGGER.at(Level.FINE).log("Sending world map update to %s - %d chunks", this.player.getUuid(), list.size());
|
||||
this.player.getPlayerConnection().write(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Map<String, MapMarker> getSentMarkers() {
|
||||
return this.markerTracker.getSentMarkers();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Player getPlayer() {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TransformComponent getTransformComponent() {
|
||||
return this.transformComponent;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
this.loaded.clear();
|
||||
this.sentViewRadius = 0;
|
||||
this.markerTracker.getSentMarkers().clear();
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
this.player.getPlayerConnection().write(new ClearWorldMap());
|
||||
}
|
||||
|
||||
public void clearChunks(@Nonnull LongSet chunkIndices) {
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
chunkIndices.forEach(index -> {
|
||||
this.loaded.remove(index);
|
||||
this.pendingReloadChunks.add(index);
|
||||
this.pendingReloadFutures.remove(index);
|
||||
});
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
this.updateTimer = 0.0F;
|
||||
}
|
||||
|
||||
public void sendSettings(@Nonnull World world) {
|
||||
UpdateWorldMapSettings worldMapSettingsPacket = new UpdateWorldMapSettings(world.getWorldMapManager().getWorldMapSettings().getSettingsPacket());
|
||||
world.execute(() -> {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Ref<EntityStore> ref = this.player.getReference();
|
||||
if (ref != null) {
|
||||
Player playerComponent = store.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerRef playerRefComponent = store.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
worldMapSettingsPacket.allowTeleportToCoordinates = this.isAllowTeleportToCoordinates();
|
||||
worldMapSettingsPacket.allowTeleportToMarkers = this.isAllowTeleportToMarkers();
|
||||
playerRefComponent.getPacketHandler().write(worldMapSettingsPacket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean hasDiscoveredZone(@Nonnull String zoneName) {
|
||||
return this.player.getPlayerConfigData().getDiscoveredZones().contains(zoneName);
|
||||
}
|
||||
|
||||
public boolean discoverZone(@Nonnull World world, @Nonnull String zoneName) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (!discoveredZones.contains(zoneName)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.add(zoneName);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean undiscoverZone(@Nonnull World world, @Nonnull String zoneName) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (discoveredZones.contains(zoneName)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.remove(zoneName);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean discoverZones(@Nonnull World world, @Nonnull Set<String> zoneNames) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (!discoveredZones.containsAll(zoneNames)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.addAll(zoneNames);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean undiscoverZones(@Nonnull World world, @Nonnull Set<String> zoneNames) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (discoveredZones.containsAll(zoneNames)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.removeAll(zoneNames);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAllowTeleportToCoordinates() {
|
||||
return this.player.hasPermission("hytale.world_map.teleport.coordinate");
|
||||
}
|
||||
|
||||
public boolean isAllowTeleportToMarkers() {
|
||||
return this.player.hasPermission("hytale.world_map.teleport.marker");
|
||||
}
|
||||
|
||||
public void setPlayerMapFilter(Predicate<PlayerRef> playerMapFilter) {
|
||||
this.markerTracker.setPlayerMapFilter(playerMapFilter);
|
||||
}
|
||||
|
||||
public void setClientHasWorldMapVisible(boolean visible) {
|
||||
this.clientHasWorldMapVisible = visible;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getViewRadiusOverride() {
|
||||
return this.viewRadiusOverride;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getCurrentBiomeName() {
|
||||
return this.currentBiomeName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WorldMapTracker.ZoneDiscoveryInfo getCurrentZone() {
|
||||
return this.currentZone;
|
||||
}
|
||||
|
||||
public void setViewRadiusOverride(@Nullable Integer viewRadiusOverride) {
|
||||
this.viewRadiusOverride = viewRadiusOverride;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public int getEffectiveViewRadius(@Nonnull World world) {
|
||||
return this.viewRadiusOverride != null
|
||||
? this.viewRadiusOverride
|
||||
: world.getWorldMapManager().getWorldMapSettings().getViewRadius(this.player.getViewRadius());
|
||||
}
|
||||
|
||||
public boolean shouldBeVisible(int chunkViewRadius, long chunkCoordinates) {
|
||||
if (this.player != null && this.transformComponent != null) {
|
||||
Vector3d position = this.transformComponent.getPosition();
|
||||
int chunkX = MathUtil.floor(position.getX()) >> 5;
|
||||
int chunkZ = MathUtil.floor(position.getZ()) >> 5;
|
||||
int x = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int z = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
return shouldBeVisible(chunkViewRadius, chunkX, chunkZ, x, z);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void copyFrom(@Nonnull WorldMapTracker worldMapTracker) {
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
worldMapTracker.loadedLock.readLock().lock();
|
||||
|
||||
try {
|
||||
this.loaded.addAll(worldMapTracker.loaded);
|
||||
this.markerTracker.copyFrom(worldMapTracker.markerTracker);
|
||||
} finally {
|
||||
worldMapTracker.loadedLock.readLock().unlock();
|
||||
}
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldBeVisible(int chunkViewRadius, int chunkX, int chunkZ, int x, int z) {
|
||||
int xDiff = Math.abs(x - chunkX);
|
||||
int zDiff = Math.abs(z - chunkZ);
|
||||
int distanceSq = xDiff * xDiff + zDiff * zDiff;
|
||||
return distanceSq <= chunkViewRadius * chunkViewRadius;
|
||||
}
|
||||
|
||||
public record ZoneDiscoveryInfo(
|
||||
@Nonnull String zoneName,
|
||||
@Nonnull String regionName,
|
||||
boolean display,
|
||||
@Nullable String discoverySoundEventId,
|
||||
@Nullable String icon,
|
||||
boolean major,
|
||||
float duration,
|
||||
float fadeInDuration,
|
||||
float fadeOutDuration
|
||||
) {
|
||||
@Nonnull
|
||||
public WorldMapTracker.ZoneDiscoveryInfo clone() {
|
||||
return new WorldMapTracker.ZoneDiscoveryInfo(
|
||||
this.zoneName,
|
||||
this.regionName,
|
||||
this.display,
|
||||
this.discoverySoundEventId,
|
||||
this.icon,
|
||||
this.major,
|
||||
this.duration,
|
||||
this.fadeInDuration,
|
||||
this.fadeOutDuration
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,429 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe.world.chunk;
|
||||
|
||||
import com.hypixel.hytale.codec.KeyedCodec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.codec.codecs.map.Int2ObjectMapCodec;
|
||||
import com.hypixel.hytale.codec.store.StoredCodec;
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
import com.hypixel.hytale.component.Archetype;
|
||||
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.Component;
|
||||
import com.hypixel.hytale.component.ComponentRegistry;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import com.hypixel.hytale.component.NonTicking;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.RemoveReason;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.RefChangeSystem;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.util.ChunkUtil;
|
||||
import com.hypixel.hytale.math.vector.Vector3i;
|
||||
import com.hypixel.hytale.protocol.Packet;
|
||||
import com.hypixel.hytale.server.core.modules.LegacyModule;
|
||||
import com.hypixel.hytale.server.core.modules.block.BlockModule;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.meta.BlockState;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap.Entry;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMaps;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectCollection;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BlockComponentChunk implements Component<ChunkStore> {
|
||||
public static final BuilderCodec<BlockComponentChunk> CODEC = BuilderCodec.builder(BlockComponentChunk.class, BlockComponentChunk::new)
|
||||
.addField(
|
||||
new KeyedCodec<>("BlockComponents", new Int2ObjectMapCodec<>(new StoredCodec<>(ChunkStore.HOLDER_CODEC_KEY), Int2ObjectOpenHashMap::new)),
|
||||
(entityChunk, map) -> {
|
||||
entityChunk.entityHolders.clear();
|
||||
entityChunk.entityHolders.putAll(map);
|
||||
},
|
||||
entityChunk -> {
|
||||
if (entityChunk.entityReferences.isEmpty()) {
|
||||
return entityChunk.entityHolders;
|
||||
} else {
|
||||
Int2ObjectMap<Holder<ChunkStore>> map = new Int2ObjectOpenHashMap<>(entityChunk.entityHolders.size() + entityChunk.entityReferences.size());
|
||||
map.putAll(entityChunk.entityHolders);
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entry : entityChunk.entityReferences.int2ObjectEntrySet()) {
|
||||
Ref<ChunkStore> reference = entry.getValue();
|
||||
Store<ChunkStore> store = reference.getStore();
|
||||
if (store.getArchetype(reference).hasSerializableComponents(store.getRegistry().getData())) {
|
||||
map.put(entry.getIntKey(), store.copySerializableEntity(reference));
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
)
|
||||
.build();
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Holder<ChunkStore>> entityHolders;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Ref<ChunkStore>> entityReferences;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Holder<ChunkStore>> entityHoldersUnmodifiable;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Ref<ChunkStore>> entityReferencesUnmodifiable;
|
||||
private boolean needsSaving;
|
||||
|
||||
public static ComponentType<ChunkStore, BlockComponentChunk> getComponentType() {
|
||||
return LegacyModule.get().getBlockComponentChunkComponentType();
|
||||
}
|
||||
|
||||
public BlockComponentChunk() {
|
||||
this.entityHolders = new Int2ObjectOpenHashMap<>();
|
||||
this.entityReferences = new Int2ObjectOpenHashMap<>();
|
||||
this.entityHoldersUnmodifiable = Int2ObjectMaps.unmodifiable(this.entityHolders);
|
||||
this.entityReferencesUnmodifiable = Int2ObjectMaps.unmodifiable(this.entityReferences);
|
||||
}
|
||||
|
||||
public BlockComponentChunk(@Nonnull Int2ObjectMap<Holder<ChunkStore>> entityHolders, @Nonnull Int2ObjectMap<Ref<ChunkStore>> entityReferences) {
|
||||
this.entityHolders = entityHolders;
|
||||
this.entityReferences = entityReferences;
|
||||
this.entityHoldersUnmodifiable = Int2ObjectMaps.unmodifiable(entityHolders);
|
||||
this.entityReferencesUnmodifiable = Int2ObjectMaps.unmodifiable(entityReferences);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<ChunkStore> clone() {
|
||||
Int2ObjectOpenHashMap<Holder<ChunkStore>> entityHoldersClone = new Int2ObjectOpenHashMap<>(this.entityHolders.size() + this.entityReferences.size());
|
||||
|
||||
for (Entry<Holder<ChunkStore>> entry : this.entityHolders.int2ObjectEntrySet()) {
|
||||
entityHoldersClone.put(entry.getIntKey(), entry.getValue().clone());
|
||||
}
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entry : this.entityReferences.int2ObjectEntrySet()) {
|
||||
Ref<ChunkStore> reference = entry.getValue();
|
||||
entityHoldersClone.put(entry.getIntKey(), reference.getStore().copyEntity(reference));
|
||||
}
|
||||
|
||||
return new BlockComponentChunk(entityHoldersClone, new Int2ObjectOpenHashMap<>());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<ChunkStore> cloneSerializable() {
|
||||
ComponentRegistry.Data<ChunkStore> data = ChunkStore.REGISTRY.getData();
|
||||
Int2ObjectOpenHashMap<Holder<ChunkStore>> entityHoldersClone = new Int2ObjectOpenHashMap<>(this.entityHolders.size() + this.entityReferences.size());
|
||||
|
||||
for (Entry<Holder<ChunkStore>> entry : this.entityHolders.int2ObjectEntrySet()) {
|
||||
Holder<ChunkStore> holder = entry.getValue();
|
||||
if (holder.getArchetype().hasSerializableComponents(data)) {
|
||||
entityHoldersClone.put(entry.getIntKey(), holder.cloneSerializable(data));
|
||||
}
|
||||
}
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entryx : this.entityReferences.int2ObjectEntrySet()) {
|
||||
Ref<ChunkStore> reference = entryx.getValue();
|
||||
Store<ChunkStore> store = reference.getStore();
|
||||
if (store.getArchetype(reference).hasSerializableComponents(data)) {
|
||||
entityHoldersClone.put(entryx.getIntKey(), store.copySerializableEntity(reference));
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockComponentChunk(entityHoldersClone, new Int2ObjectOpenHashMap<>());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Int2ObjectMap<Holder<ChunkStore>> getEntityHolders() {
|
||||
return this.entityHoldersUnmodifiable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Holder<ChunkStore> getEntityHolder(int index) {
|
||||
return this.entityHolders.get(index);
|
||||
}
|
||||
|
||||
public void addEntityHolder(int index, @Nonnull Holder<ChunkStore> holder) {
|
||||
if (this.entityReferences.containsKey(index)) {
|
||||
throw new IllegalArgumentException("Duplicate block components at: " + index);
|
||||
} else if (this.entityHolders.putIfAbsent(index, Objects.requireNonNull(holder)) != null) {
|
||||
throw new IllegalArgumentException("Duplicate block components (entity holder) at: " + index);
|
||||
} else {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
}
|
||||
|
||||
public void storeEntityHolder(int index, @Nonnull Holder<ChunkStore> holder) {
|
||||
if (this.entityHolders.putIfAbsent(index, Objects.requireNonNull(holder)) != null) {
|
||||
throw new IllegalArgumentException("Duplicate block components (entity holder) at: " + index);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Holder<ChunkStore> removeEntityHolder(int index) {
|
||||
Holder<ChunkStore> reference = this.entityHolders.remove(index);
|
||||
if (reference != null) {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Int2ObjectMap<Ref<ChunkStore>> getEntityReferences() {
|
||||
return this.entityReferencesUnmodifiable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getEntityReference(int index) {
|
||||
return this.entityReferences.get(index);
|
||||
}
|
||||
|
||||
public void addEntityReference(int index, @Nonnull Ref<ChunkStore> reference) {
|
||||
reference.validate();
|
||||
// HyFix #8: Handle duplicate block components gracefully instead of throwing
|
||||
if (this.entityHolders.containsKey(index)) {
|
||||
System.out.println("[HyFix] WARNING: Duplicate block component detected at index " + index + " - ignoring (teleporter fix)");
|
||||
return;
|
||||
} else if (this.entityReferences.putIfAbsent(index, Objects.requireNonNull(reference)) != null) {
|
||||
System.out.println("[HyFix] WARNING: Duplicate block component (entity reference) detected at index " + index + " - ignoring (teleporter fix)");
|
||||
return;
|
||||
} else {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadEntityReference(int index, @Nonnull Ref<ChunkStore> reference) {
|
||||
reference.validate();
|
||||
if (this.entityHolders.containsKey(index)) {
|
||||
throw new IllegalArgumentException("Duplicate block components at: " + index);
|
||||
} else if (this.entityReferences.putIfAbsent(index, Objects.requireNonNull(reference)) != null) {
|
||||
throw new IllegalArgumentException("Duplicate block components (entity reference) at: " + index);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeEntityReference(int index, Ref<ChunkStore> reference) {
|
||||
if (this.entityReferences.remove(index, reference)) {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
}
|
||||
|
||||
public void unloadEntityReference(int index, Ref<ChunkStore> reference) {
|
||||
this.entityReferences.remove(index, reference);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Int2ObjectMap<Holder<ChunkStore>> takeEntityHolders() {
|
||||
if (this.entityHolders.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
Int2ObjectOpenHashMap<Holder<ChunkStore>> holders = new Int2ObjectOpenHashMap<>(this.entityHolders);
|
||||
this.entityHolders.clear();
|
||||
return holders;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Int2ObjectMap<Ref<ChunkStore>> takeEntityReferences() {
|
||||
if (this.entityReferences.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
Int2ObjectOpenHashMap<Ref<ChunkStore>> holders = new Int2ObjectOpenHashMap<>(this.entityReferences);
|
||||
this.entityReferences.clear();
|
||||
return holders;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T extends Component<ChunkStore>> T getComponent(int index, @Nonnull ComponentType<ChunkStore, T> componentType) {
|
||||
Ref<ChunkStore> reference = this.entityReferences.get(index);
|
||||
if (reference != null) {
|
||||
return reference.getStore().getComponent(reference, componentType);
|
||||
} else {
|
||||
Holder<ChunkStore> holder = this.entityHolders.get(index);
|
||||
return holder != null ? holder.getComponent(componentType) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasComponents(int index) {
|
||||
return this.entityReferences.containsKey(index) || this.entityHolders.containsKey(index);
|
||||
}
|
||||
|
||||
public boolean getNeedsSaving() {
|
||||
return this.needsSaving;
|
||||
}
|
||||
|
||||
public void markNeedsSaving() {
|
||||
this.needsSaving = true;
|
||||
}
|
||||
|
||||
public boolean consumeNeedsSaving() {
|
||||
boolean out = this.needsSaving;
|
||||
this.needsSaving = false;
|
||||
return out;
|
||||
}
|
||||
|
||||
public static class BlockComponentChunkLoadingSystem extends RefChangeSystem<ChunkStore, NonTicking<ChunkStore>> {
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
private final Archetype<ChunkStore> archetype = Archetype.of(WorldChunk.getComponentType(), BlockComponentChunk.getComponentType());
|
||||
|
||||
public BlockComponentChunkLoadingSystem() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return this.archetype;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ComponentType<ChunkStore, NonTicking<ChunkStore>> componentType() {
|
||||
return ChunkStore.REGISTRY.getNonTickingComponentType();
|
||||
}
|
||||
|
||||
public void onComponentAdded(
|
||||
@Nonnull Ref<ChunkStore> ref,
|
||||
@Nonnull NonTicking<ChunkStore> component,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
BlockComponentChunk blockComponentChunk = store.getComponent(ref, BlockComponentChunk.getComponentType());
|
||||
Int2ObjectMap<Ref<ChunkStore>> entityReferences = blockComponentChunk.takeEntityReferences();
|
||||
if (entityReferences != null) {
|
||||
int size = entityReferences.size();
|
||||
int[] indexes = new int[size];
|
||||
Ref<ChunkStore>[] references = new Ref[size];
|
||||
int j = 0;
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entry : entityReferences.int2ObjectEntrySet()) {
|
||||
indexes[j] = entry.getIntKey();
|
||||
references[j] = entry.getValue();
|
||||
j++;
|
||||
}
|
||||
|
||||
ComponentRegistry.Data<ChunkStore> data = ChunkStore.REGISTRY.getData();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (store.getArchetype(references[i]).hasSerializableComponents(data)) {
|
||||
Holder<ChunkStore> holder = ChunkStore.REGISTRY.newHolder();
|
||||
commandBuffer.removeEntity(references[i], holder, RemoveReason.UNLOAD);
|
||||
blockComponentChunk.storeEntityHolder(indexes[i], holder);
|
||||
} else {
|
||||
commandBuffer.removeEntity(references[i], RemoveReason.UNLOAD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onComponentSet(
|
||||
@Nonnull Ref<ChunkStore> ref,
|
||||
NonTicking<ChunkStore> oldComponent,
|
||||
@Nonnull NonTicking<ChunkStore> newComponent,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
}
|
||||
|
||||
public void onComponentRemoved(
|
||||
@Nonnull Ref<ChunkStore> ref,
|
||||
@Nonnull NonTicking<ChunkStore> component,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
WorldChunk chunk = store.getComponent(ref, WorldChunk.getComponentType());
|
||||
BlockComponentChunk blockComponentChunk = store.getComponent(ref, BlockComponentChunk.getComponentType());
|
||||
Int2ObjectMap<Holder<ChunkStore>> entityHolders = blockComponentChunk.takeEntityHolders();
|
||||
if (entityHolders != null) {
|
||||
int holderCount = entityHolders.size();
|
||||
int[] indexes = new int[holderCount];
|
||||
Holder<ChunkStore>[] holders = new Holder[holderCount];
|
||||
int j = 0;
|
||||
|
||||
for (Entry<Holder<ChunkStore>> entry : entityHolders.int2ObjectEntrySet()) {
|
||||
indexes[j] = entry.getIntKey();
|
||||
holders[j] = entry.getValue();
|
||||
j++;
|
||||
}
|
||||
|
||||
for (int i = holderCount - 1; i >= 0; i--) {
|
||||
Holder<ChunkStore> holder = holders[i];
|
||||
if (holder.getArchetype().isEmpty()) {
|
||||
LOGGER.at(Level.SEVERE).log("Empty archetype entity holder: %s (#%d)", holder, i);
|
||||
holders[i] = holders[--holderCount];
|
||||
holders[holderCount] = holder;
|
||||
chunk.markNeedsSaving();
|
||||
} else {
|
||||
int index = indexes[i];
|
||||
int x = ChunkUtil.xFromBlockInColumn(index);
|
||||
int y = ChunkUtil.yFromBlockInColumn(index);
|
||||
int z = ChunkUtil.zFromBlockInColumn(index);
|
||||
holder.putComponent(BlockModule.BlockStateInfo.getComponentType(), new BlockModule.BlockStateInfo(index, ref));
|
||||
BlockState state = BlockState.getBlockState(holder);
|
||||
if (state != null) {
|
||||
state.setPosition(chunk, new Vector3i(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandBuffer.addEntities(holders, AddReason.LOAD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoadBlockComponentPacketSystem extends ChunkStore.LoadPacketDataQuerySystem {
|
||||
private final ComponentType<ChunkStore, BlockComponentChunk> componentType;
|
||||
|
||||
public LoadBlockComponentPacketSystem(ComponentType<ChunkStore, BlockComponentChunk> blockComponentChunkComponentType) {
|
||||
this.componentType = blockComponentChunkComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return this.componentType;
|
||||
}
|
||||
|
||||
public void fetch(
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
CommandBuffer<ChunkStore> commandBuffer,
|
||||
PlayerRef player,
|
||||
@Nonnull List<Packet> results
|
||||
) {
|
||||
BlockComponentChunk component = archetypeChunk.getComponent(index, this.componentType);
|
||||
ObjectCollection<Ref<ChunkStore>> references = component.entityReferences.values();
|
||||
Store<ChunkStore> componentStore = store.getExternalData().getWorld().getChunkStore().getStore();
|
||||
componentStore.fetch(references, ChunkStore.LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE, player, results);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnloadBlockComponentPacketSystem extends ChunkStore.UnloadPacketDataQuerySystem {
|
||||
private final ComponentType<ChunkStore, BlockComponentChunk> componentType;
|
||||
|
||||
public UnloadBlockComponentPacketSystem(ComponentType<ChunkStore, BlockComponentChunk> blockComponentChunkComponentType) {
|
||||
this.componentType = blockComponentChunkComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return this.componentType;
|
||||
}
|
||||
|
||||
public void fetch(
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
CommandBuffer<ChunkStore> commandBuffer,
|
||||
PlayerRef player,
|
||||
@Nonnull List<Packet> results
|
||||
) {
|
||||
BlockComponentChunk component = archetypeChunk.getComponent(index, this.componentType);
|
||||
ObjectCollection<Ref<ChunkStore>> references = component.entityReferences.values();
|
||||
Store<ChunkStore> componentStore = store.getExternalData().getWorld().getChunkStore().getStore();
|
||||
componentStore.fetch(references, ChunkStore.UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE, player, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,241 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe.world.lighting;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.util.ChunkUtil;
|
||||
import com.hypixel.hytale.math.vector.Vector3i;
|
||||
import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.section.BlockSection;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayFIFOQueue;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Semaphore;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ChunkLightingManager implements Runnable {
|
||||
@Nonnull
|
||||
private final HytaleLogger logger;
|
||||
@Nonnull
|
||||
private final Thread thread;
|
||||
@Nonnull
|
||||
private final World world;
|
||||
private final Semaphore semaphore = new Semaphore(1);
|
||||
private final Set<Vector3i> set = ConcurrentHashMap.newKeySet();
|
||||
private final ObjectArrayFIFOQueue<Vector3i> queue = new ObjectArrayFIFOQueue<>();
|
||||
private LightCalculation lightCalculation;
|
||||
|
||||
// HyFix: Periodic chunk lighting invalidation to prevent memory leaks
|
||||
private static final long PERIODIC_INVALIDATION_INTERVAL_MS = 30000; // 30 seconds
|
||||
private long lastPeriodicInvalidation = 0;
|
||||
|
||||
public ChunkLightingManager(@Nonnull World world) {
|
||||
this.logger = HytaleLogger.get("World|" + world.getName() + "|L");
|
||||
this.thread = new Thread(this, "ChunkLighting - " + world.getName());
|
||||
this.thread.setDaemon(true);
|
||||
this.world = world;
|
||||
this.lightCalculation = new FloodLightCalculation(this);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected HytaleLogger getLogger() {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public World getWorld() {
|
||||
return this.world;
|
||||
}
|
||||
|
||||
public void setLightCalculation(LightCalculation lightCalculation) {
|
||||
this.lightCalculation = lightCalculation;
|
||||
}
|
||||
|
||||
public LightCalculation getLightCalculation() {
|
||||
return this.lightCalculation;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
this.thread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
int lastSize = 0;
|
||||
int count = 0;
|
||||
this.lastPeriodicInvalidation = System.currentTimeMillis();
|
||||
|
||||
while (!this.thread.isInterrupted()) {
|
||||
this.semaphore.drainPermits();
|
||||
Vector3i pos;
|
||||
synchronized (this.queue) {
|
||||
pos = this.queue.isEmpty() ? null : this.queue.dequeue();
|
||||
}
|
||||
|
||||
if (pos != null) {
|
||||
this.process(pos);
|
||||
}
|
||||
|
||||
// HyFix: Periodic chunk lighting invalidation to prevent memory leaks and stale lighting
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - this.lastPeriodicInvalidation >= PERIODIC_INVALIDATION_INTERVAL_MS) {
|
||||
this.lastPeriodicInvalidation = now;
|
||||
try {
|
||||
this.invalidateLoadedChunks();
|
||||
} catch (Exception e) {
|
||||
// Silently handle errors - may occur during world transitions
|
||||
}
|
||||
}
|
||||
|
||||
Thread.yield();
|
||||
int currentSize;
|
||||
synchronized (this.queue) {
|
||||
currentSize = this.queue.size();
|
||||
}
|
||||
|
||||
if (currentSize != lastSize) {
|
||||
count = 0;
|
||||
lastSize = currentSize;
|
||||
} else if (count <= currentSize) {
|
||||
count++;
|
||||
} else {
|
||||
this.semaphore.acquire();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException var9) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private void process(Vector3i chunkPosition) {
|
||||
try {
|
||||
switch (this.lightCalculation.calculateLight(chunkPosition)) {
|
||||
case NOT_LOADED:
|
||||
case WAITING_FOR_NEIGHBOUR:
|
||||
case DONE:
|
||||
this.set.remove(chunkPosition);
|
||||
break;
|
||||
case INVALIDATED:
|
||||
synchronized (this.queue) {
|
||||
this.queue.enqueue(chunkPosition);
|
||||
}
|
||||
}
|
||||
} catch (Exception var5) {
|
||||
this.logger.at(Level.WARNING).withCause(var5).log("Failed to calculate lighting for: %s", chunkPosition);
|
||||
this.set.remove(chunkPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean interrupt() {
|
||||
if (this.thread.isAlive()) {
|
||||
this.thread.interrupt();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
int i = 0;
|
||||
|
||||
while (this.thread.isAlive()) {
|
||||
this.thread.interrupt();
|
||||
this.thread.join(this.world.getTickStepNanos() / 1000000);
|
||||
i += this.world.getTickStepNanos() / 1000000;
|
||||
if (i > 5000) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (StackTraceElement traceElement : this.thread.getStackTrace()) {
|
||||
sb.append("\tat ").append(traceElement).append('\n');
|
||||
}
|
||||
|
||||
HytaleLogger.getLogger().at(Level.SEVERE).log("Forcing ChunkLighting Thread %s to stop:\n%s", this.thread, sb.toString());
|
||||
this.thread.stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException var7) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void init(WorldChunk worldChunk) {
|
||||
this.lightCalculation.init(worldChunk);
|
||||
}
|
||||
|
||||
public void addToQueue(Vector3i chunkPosition) {
|
||||
if (this.set.add(chunkPosition)) {
|
||||
synchronized (this.queue) {
|
||||
this.queue.enqueue(chunkPosition);
|
||||
}
|
||||
|
||||
this.semaphore.release(1);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isQueued(int chunkX, int chunkZ) {
|
||||
Vector3i chunkPos = new Vector3i(chunkX, 0, chunkZ);
|
||||
|
||||
for (int chunkY = 0; chunkY < 10; chunkY++) {
|
||||
chunkPos.setY(chunkY);
|
||||
if (this.isQueued(chunkPos)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isQueued(Vector3i chunkPosition) {
|
||||
return this.set.contains(chunkPosition);
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
synchronized (this.queue) {
|
||||
return this.queue.size();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean invalidateLightAtBlock(WorldChunk worldChunk, int blockX, int blockY, int blockZ, BlockType blockType, int oldHeight, int newHeight) {
|
||||
return this.lightCalculation.invalidateLightAtBlock(worldChunk, blockX, blockY, blockZ, blockType, oldHeight, newHeight);
|
||||
}
|
||||
|
||||
public boolean invalidateLightInChunk(WorldChunk worldChunk) {
|
||||
return this.lightCalculation.invalidateLightInChunkSections(worldChunk, 0, 10);
|
||||
}
|
||||
|
||||
public boolean invalidateLightInChunkSection(WorldChunk worldChunk, int sectionIndex) {
|
||||
return this.lightCalculation.invalidateLightInChunkSections(worldChunk, sectionIndex, sectionIndex + 1);
|
||||
}
|
||||
|
||||
public boolean invalidateLightInChunkSections(WorldChunk worldChunk, int sectionIndexFrom, int sectionIndexTo) {
|
||||
return this.lightCalculation.invalidateLightInChunkSections(worldChunk, sectionIndexFrom, sectionIndexTo);
|
||||
}
|
||||
|
||||
public void invalidateLoadedChunks() {
|
||||
this.world.getChunkStore().getStore().forEachEntityParallel(WorldChunk.getComponentType(), (index, archetypeChunk, storeCommandBuffer) -> {
|
||||
WorldChunk chunk = archetypeChunk.getComponent(index, WorldChunk.getComponentType());
|
||||
|
||||
for (int y = 0; y < 10; y++) {
|
||||
BlockSection section = chunk.getBlockChunk().getSectionAtIndex(y);
|
||||
section.invalidateLocalLight();
|
||||
if (BlockChunk.SEND_LOCAL_LIGHTING_DATA || BlockChunk.SEND_GLOBAL_LIGHTING_DATA) {
|
||||
chunk.getBlockChunk().invalidateChunkSection(y);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.world.getChunkStore().getChunkIndexes().forEach(index -> {
|
||||
int x = ChunkUtil.xOfChunkIndex(index);
|
||||
int z = ChunkUtil.zOfChunkIndex(index);
|
||||
|
||||
for (int y = 0; y < 10; y++) {
|
||||
this.addToQueue(new Vector3i(x, y, z));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,923 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe.world.storage;
|
||||
|
||||
import com.hypixel.fastutil.longs.Long2ObjectConcurrentHashMap;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.store.CodecKey;
|
||||
import com.hypixel.hytale.codec.store.CodecStore;
|
||||
import com.hypixel.hytale.common.util.FormatUtil;
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
import com.hypixel.hytale.component.Component;
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.ComponentRegistry;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import com.hypixel.hytale.component.IResourceStorage;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.RemoveReason;
|
||||
import com.hypixel.hytale.component.ResourceType;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.SystemGroup;
|
||||
import com.hypixel.hytale.component.SystemType;
|
||||
import com.hypixel.hytale.component.system.StoreSystem;
|
||||
import com.hypixel.hytale.component.system.data.EntityDataSystem;
|
||||
import com.hypixel.hytale.event.IEventDispatcher;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.util.ChunkUtil;
|
||||
import com.hypixel.hytale.metrics.MetricProvider;
|
||||
import com.hypixel.hytale.metrics.MetricsRegistry;
|
||||
import com.hypixel.hytale.protocol.Packet;
|
||||
import com.hypixel.hytale.server.core.HytaleServer;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.WorldProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.BlockChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.ChunkColumn;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.ChunkFlag;
|
||||
import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.events.ChunkPreLoadProcessEvent;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.component.ChunkSavingSystems;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.component.ChunkUnloadingSystem;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.provider.IChunkStorageProvider;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldgen.GeneratedChunk;
|
||||
import com.hypixel.hytale.server.core.universe.world.worldgen.IWorldGen;
|
||||
import com.hypixel.hytale.sneakythrow.SneakyThrow;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
|
||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||
import it.unimi.dsi.fastutil.longs.LongSets;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.time.Instant;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class ChunkStore implements WorldProvider {
|
||||
@Nonnull
|
||||
public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final MetricsRegistry<ChunkStore> METRICS_REGISTRY = new MetricsRegistry<ChunkStore>()
|
||||
.register("Store", ChunkStore::getStore, Store.METRICS_REGISTRY)
|
||||
.register("ChunkLoader", MetricProvider.maybe(ChunkStore::getLoader))
|
||||
.register("ChunkSaver", MetricProvider.maybe(ChunkStore::getSaver))
|
||||
.register("WorldGen", MetricProvider.maybe(ChunkStore::getGenerator))
|
||||
.register("TotalGeneratedChunkCount", chunkComponentStore -> (long) chunkComponentStore.totalGeneratedChunksCount.get(), Codec.LONG)
|
||||
.register("TotalLoadedChunkCount", chunkComponentStore -> (long) chunkComponentStore.totalLoadedChunksCount.get(), Codec.LONG);
|
||||
public static final long MAX_FAILURE_BACKOFF_NANOS = TimeUnit.SECONDS.toNanos(10L);
|
||||
public static final long FAILURE_BACKOFF_NANOS = TimeUnit.MILLISECONDS.toNanos(1L);
|
||||
public static final ComponentRegistry<ChunkStore> REGISTRY = new ComponentRegistry<>();
|
||||
public static final CodecKey<Holder<ChunkStore>> HOLDER_CODEC_KEY = new CodecKey<>("ChunkHolder");
|
||||
@Nonnull
|
||||
public static final SystemType<ChunkStore, ChunkStore.LoadPacketDataQuerySystem> LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType(
|
||||
ChunkStore.LoadPacketDataQuerySystem.class
|
||||
);
|
||||
@Nonnull
|
||||
public static final SystemType<ChunkStore, ChunkStore.LoadFuturePacketDataQuerySystem> LOAD_FUTURE_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType(
|
||||
ChunkStore.LoadFuturePacketDataQuerySystem.class
|
||||
);
|
||||
@Nonnull
|
||||
public static final SystemType<ChunkStore, ChunkStore.UnloadPacketDataQuerySystem> UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType(
|
||||
ChunkStore.UnloadPacketDataQuerySystem.class
|
||||
);
|
||||
@Nonnull
|
||||
public static final ResourceType<ChunkStore, ChunkUnloadingSystem.Data> UNLOAD_RESOURCE = REGISTRY.registerResource(
|
||||
ChunkUnloadingSystem.Data.class, ChunkUnloadingSystem.Data::new
|
||||
);
|
||||
@Nonnull
|
||||
public static final ResourceType<ChunkStore, ChunkSavingSystems.Data> SAVE_RESOURCE = REGISTRY.registerResource(
|
||||
ChunkSavingSystems.Data.class, ChunkSavingSystems.Data::new
|
||||
);
|
||||
public static final SystemGroup<ChunkStore> INIT_GROUP = REGISTRY.registerSystemGroup();
|
||||
@Nonnull
|
||||
private final World world;
|
||||
@Nonnull
|
||||
private final Long2ObjectConcurrentHashMap<ChunkStore.ChunkLoadState> chunks = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND);
|
||||
private Store<ChunkStore> store;
|
||||
@Nullable
|
||||
private IChunkLoader loader;
|
||||
@Nullable
|
||||
private IChunkSaver saver;
|
||||
@Nullable
|
||||
private IWorldGen generator;
|
||||
@Nonnull
|
||||
private CompletableFuture<Void> generatorLoaded = new CompletableFuture<>();
|
||||
private final StampedLock generatorLock = new StampedLock();
|
||||
private final AtomicInteger totalGeneratedChunksCount = new AtomicInteger();
|
||||
private final AtomicInteger totalLoadedChunksCount = new AtomicInteger();
|
||||
|
||||
public ChunkStore(@Nonnull World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public World getWorld() {
|
||||
return this.world;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Store<ChunkStore> getStore() {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IChunkLoader getLoader() {
|
||||
return this.loader;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IChunkSaver getSaver() {
|
||||
return this.saver;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IWorldGen getGenerator() {
|
||||
long readStamp = this.generatorLock.readLock();
|
||||
|
||||
IWorldGen var3;
|
||||
try {
|
||||
var3 = this.generator;
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStamp);
|
||||
}
|
||||
|
||||
return var3;
|
||||
}
|
||||
|
||||
public void shutdownGenerator() {
|
||||
this.setGenerator(null);
|
||||
}
|
||||
|
||||
public void setGenerator(@Nullable IWorldGen generator) {
|
||||
long writeStamp = this.generatorLock.writeLock();
|
||||
|
||||
try {
|
||||
if (this.generator != null) {
|
||||
this.generator.shutdown();
|
||||
}
|
||||
|
||||
this.totalGeneratedChunksCount.set(0);
|
||||
this.generator = generator;
|
||||
if (generator != null) {
|
||||
this.generatorLoaded.complete(null);
|
||||
this.generatorLoaded = new CompletableFuture<>();
|
||||
}
|
||||
} finally {
|
||||
this.generatorLock.unlockWrite(writeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public LongSet getChunkIndexes() {
|
||||
return LongSets.unmodifiable(this.chunks.keySet());
|
||||
}
|
||||
|
||||
public int getLoadedChunksCount() {
|
||||
return this.chunks.size();
|
||||
}
|
||||
|
||||
public int getTotalGeneratedChunksCount() {
|
||||
return this.totalGeneratedChunksCount.get();
|
||||
}
|
||||
|
||||
public int getTotalLoadedChunksCount() {
|
||||
return this.totalLoadedChunksCount.get();
|
||||
}
|
||||
|
||||
public void start(@Nonnull IResourceStorage resourceStorage) {
|
||||
this.store = REGISTRY.addStore(this, resourceStorage, store -> this.store = store);
|
||||
}
|
||||
|
||||
public void waitForLoadingChunks() {
|
||||
long start = System.nanoTime();
|
||||
|
||||
boolean hasLoadingChunks;
|
||||
do {
|
||||
this.world.consumeTaskQueue();
|
||||
Thread.yield();
|
||||
hasLoadingChunks = false;
|
||||
|
||||
for (Entry<ChunkStore.ChunkLoadState> entry : this.chunks.long2ObjectEntrySet()) {
|
||||
ChunkStore.ChunkLoadState chunkState = entry.getValue();
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
try {
|
||||
CompletableFuture<Ref<ChunkStore>> future = chunkState.future;
|
||||
if (future != null && !future.isDone()) {
|
||||
hasLoadingChunks = true;
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
}
|
||||
} while (hasLoadingChunks && System.nanoTime() - start <= 5000000000L);
|
||||
|
||||
this.world.consumeTaskQueue();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.store.shutdown();
|
||||
this.chunks.clear();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Ref<ChunkStore> add(@Nonnull Holder<ChunkStore> holder) {
|
||||
this.world.debugAssertInTickingThread();
|
||||
WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(worldChunkComponent.getIndex());
|
||||
if (chunkState == null) {
|
||||
throw new IllegalStateException("Expected the ChunkLoadState to exist!");
|
||||
} else {
|
||||
Ref<ChunkStore> oldReference = null;
|
||||
long stamp = chunkState.lock.writeLock();
|
||||
|
||||
try {
|
||||
if (chunkState.future == null) {
|
||||
throw new IllegalStateException("Expected the ChunkLoadState to have a future!");
|
||||
}
|
||||
|
||||
if (chunkState.reference != null) {
|
||||
oldReference = chunkState.reference;
|
||||
chunkState.reference = null;
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
}
|
||||
|
||||
if (oldReference != null) {
|
||||
WorldChunk oldWorldChunkComponent = this.store.getComponent(oldReference, WorldChunk.getComponentType());
|
||||
|
||||
assert oldWorldChunkComponent != null;
|
||||
|
||||
oldWorldChunkComponent.setFlag(ChunkFlag.TICKING, false);
|
||||
this.store.removeEntity(oldReference, RemoveReason.REMOVE);
|
||||
this.world.getNotificationHandler().updateChunk(worldChunkComponent.getIndex());
|
||||
}
|
||||
|
||||
oldReference = this.store.addEntity(holder, AddReason.SPAWN);
|
||||
if (oldReference == null) {
|
||||
throw new UnsupportedOperationException("Unable to add the chunk to the world!");
|
||||
} else {
|
||||
worldChunkComponent.setReference(oldReference);
|
||||
stamp = chunkState.lock.writeLock();
|
||||
|
||||
Ref var17;
|
||||
try {
|
||||
chunkState.reference = oldReference;
|
||||
chunkState.flags = 0;
|
||||
chunkState.future = null;
|
||||
chunkState.throwable = null;
|
||||
chunkState.failedWhen = 0L;
|
||||
chunkState.failedCounter = 0;
|
||||
var17 = oldReference;
|
||||
} finally {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
}
|
||||
|
||||
return var17;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(@Nonnull Ref<ChunkStore> reference, @Nonnull RemoveReason reason) {
|
||||
this.world.debugAssertInTickingThread();
|
||||
WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
long index = worldChunkComponent.getIndex();
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(index);
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
try {
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, false);
|
||||
this.store.removeEntity(reference, reason);
|
||||
if (chunkState.future != null) {
|
||||
chunkState.reference = null;
|
||||
} else {
|
||||
this.chunks.remove(index, chunkState);
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getChunkReference(long index) {
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(index);
|
||||
if (chunkState == null) {
|
||||
return null;
|
||||
} else {
|
||||
long stamp = chunkState.lock.tryOptimisticRead();
|
||||
Ref<ChunkStore> reference = chunkState.reference;
|
||||
if (chunkState.lock.validate(stamp)) {
|
||||
return reference;
|
||||
} else {
|
||||
stamp = chunkState.lock.readLock();
|
||||
|
||||
Ref var7;
|
||||
try {
|
||||
var7 = chunkState.reference;
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
|
||||
return var7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getChunkSectionReference(int x, int y, int z) {
|
||||
Ref<ChunkStore> ref = this.getChunkReference(ChunkUtil.indexChunk(x, z));
|
||||
if (ref == null) {
|
||||
return null;
|
||||
} else {
|
||||
ChunkColumn chunkColumnComponent = this.store.getComponent(ref, ChunkColumn.getComponentType());
|
||||
return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getChunkSectionReference(@Nonnull ComponentAccessor<ChunkStore> commandBuffer, int x, int y, int z) {
|
||||
Ref<ChunkStore> ref = this.getChunkReference(ChunkUtil.indexChunk(x, z));
|
||||
if (ref == null) {
|
||||
return null;
|
||||
} else {
|
||||
ChunkColumn chunkColumnComponent = commandBuffer.getComponent(ref, ChunkColumn.getComponentType());
|
||||
return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Ref<ChunkStore>> getChunkSectionReferenceAsync(int x, int y, int z) {
|
||||
return y >= 0 && y < 10 ? this.getChunkReferenceAsync(ChunkUtil.indexChunk(x, z)).thenApplyAsync(ref -> {
|
||||
if (ref != null && ref.isValid()) {
|
||||
Store<ChunkStore> store = ref.getStore();
|
||||
ChunkColumn chunkColumnComponent = store.getComponent((Ref<ChunkStore>) ref, ChunkColumn.getComponentType());
|
||||
return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, this.store.getExternalData().getWorld()) : CompletableFuture.failedFuture(new IndexOutOfBoundsException("Invalid y: " + y));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T extends Component<ChunkStore>> T getChunkComponent(long index, @Nonnull ComponentType<ChunkStore, T> componentType) {
|
||||
Ref<ChunkStore> reference = this.getChunkReference(index);
|
||||
return reference != null && reference.isValid() ? this.store.getComponent(reference, componentType) : null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Ref<ChunkStore>> getChunkReferenceAsync(long index) {
|
||||
return this.getChunkReferenceAsync(index, 0);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Ref<ChunkStore>> getChunkReferenceAsync(long index, int flags) {
|
||||
if (this.store.isShutdown()) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
ChunkStore.ChunkLoadState chunkState;
|
||||
if ((flags & 3) == 3) {
|
||||
chunkState = this.chunks.get(index);
|
||||
if (chunkState == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
try {
|
||||
if ((flags & 4) == 0 || (chunkState.flags & 4) != 0) {
|
||||
if (chunkState.reference != null) {
|
||||
return CompletableFuture.completedFuture(chunkState.reference);
|
||||
}
|
||||
|
||||
if (chunkState.future != null) {
|
||||
return chunkState.future;
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
} else {
|
||||
chunkState = this.chunks.computeIfAbsent(index, l -> new ChunkStore.ChunkLoadState());
|
||||
}
|
||||
|
||||
long stamp = chunkState.lock.writeLock();
|
||||
if (chunkState.future == null && chunkState.reference != null && (flags & 8) == 0) {
|
||||
Ref<ChunkStore> reference = chunkState.reference;
|
||||
if ((flags & 4) == 0) {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
return CompletableFuture.completedFuture(reference);
|
||||
} else if (this.world.isInThread() && (flags & -2147483648) == 0) {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, true);
|
||||
return CompletableFuture.completedFuture(reference);
|
||||
} else {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, true);
|
||||
return reference;
|
||||
}, this.world);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (chunkState.throwable != null) {
|
||||
long nanosSince = System.nanoTime() - chunkState.failedWhen;
|
||||
int count = chunkState.failedCounter;
|
||||
if (nanosSince < Math.min(MAX_FAILURE_BACKOFF_NANOS, count * count * FAILURE_BACKOFF_NANOS)) {
|
||||
return CompletableFuture.failedFuture(new RuntimeException("Chunk failure backoff", chunkState.throwable));
|
||||
}
|
||||
|
||||
chunkState.throwable = null;
|
||||
chunkState.failedWhen = 0L;
|
||||
}
|
||||
|
||||
boolean isNew = chunkState.future == null;
|
||||
if (isNew) {
|
||||
chunkState.flags = flags;
|
||||
}
|
||||
|
||||
int x = ChunkUtil.xOfChunkIndex(index);
|
||||
int z = ChunkUtil.zOfChunkIndex(index);
|
||||
int seed = (int) this.world.getWorldConfig().getSeed();
|
||||
if ((isNew || (chunkState.flags & 1) != 0) && (flags & 1) == 0) {
|
||||
if (chunkState.future == null) {
|
||||
chunkState.future = this.loader.loadHolder(x, z).thenApplyAsync(holder -> {
|
||||
if (holder != null && !this.store.isShutdown()) {
|
||||
this.totalLoadedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, (Holder<ChunkStore>) holder, false);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).exceptionallyCompose(throwable -> {
|
||||
// Corruption detected during load - recover by regenerating
|
||||
return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable);
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world);
|
||||
} else {
|
||||
chunkState.flags &= -2;
|
||||
chunkState.future = chunkState.future
|
||||
.thenCompose(
|
||||
reference -> reference != null
|
||||
? CompletableFuture.completedFuture((Ref<ChunkStore>) reference)
|
||||
: this.loader.loadHolder(x, z).thenApplyAsync(holder -> {
|
||||
if (holder != null && !this.store.isShutdown()) {
|
||||
this.totalLoadedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, (Holder<ChunkStore>) holder, false);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).exceptionallyCompose(throwable -> {
|
||||
// Corruption detected during load - recover by regenerating
|
||||
return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable);
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ((isNew || (chunkState.flags & 2) != 0) && (flags & 2) == 0) {
|
||||
if (chunkState.future == null) {
|
||||
long readStamp = this.generatorLock.readLock();
|
||||
|
||||
CompletableFuture<GeneratedChunk> future;
|
||||
try {
|
||||
if (this.generator == null) {
|
||||
future = this.generatorLoaded
|
||||
.thenCompose(aVoid -> this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null));
|
||||
} else {
|
||||
future = this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null);
|
||||
}
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStamp);
|
||||
}
|
||||
|
||||
chunkState.future = future.<Holder<ChunkStore>>thenApplyAsync(generatedChunk -> {
|
||||
if (generatedChunk != null && !this.store.isShutdown()) {
|
||||
this.totalGeneratedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> {
|
||||
LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z);
|
||||
chunkState.fail(throwable);
|
||||
throw SneakyThrow.sneakyThrow(throwable);
|
||||
});
|
||||
} else {
|
||||
chunkState.flags &= -3;
|
||||
chunkState.future = chunkState.future.thenCompose(reference -> {
|
||||
if (reference != null) {
|
||||
return CompletableFuture.completedFuture((Ref<ChunkStore>) reference);
|
||||
} else {
|
||||
long readStampx = this.generatorLock.readLock();
|
||||
|
||||
CompletableFuture<GeneratedChunk> future;
|
||||
try {
|
||||
if (this.generator == null) {
|
||||
future = this.generatorLoaded.thenCompose(aVoid -> this.generator.generate(seed, index, x, z, null));
|
||||
} else {
|
||||
future = this.generator.generate(seed, index, x, z, null);
|
||||
}
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStampx);
|
||||
}
|
||||
|
||||
return future.<Holder<ChunkStore>>thenApplyAsync(generatedChunk -> {
|
||||
if (generatedChunk != null && !this.store.isShutdown()) {
|
||||
this.totalGeneratedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> {
|
||||
LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z);
|
||||
chunkState.fail(throwable);
|
||||
throw SneakyThrow.sneakyThrow(throwable);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ((isNew || (chunkState.flags & 4) == 0) && (flags & 4) != 0) {
|
||||
chunkState.flags |= 4;
|
||||
if (chunkState.future != null) {
|
||||
chunkState.future = chunkState.future.<Ref<ChunkStore>>thenApplyAsync(reference -> {
|
||||
if (reference != null) {
|
||||
WorldChunk worldChunkComponent = this.store.getComponent((Ref<ChunkStore>) reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, true);
|
||||
}
|
||||
|
||||
return reference;
|
||||
}, this.world).exceptionally(throwable -> {
|
||||
LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to set chunk ticking! %s, %s", x, z);
|
||||
chunkState.fail(throwable);
|
||||
throw SneakyThrow.sneakyThrow(throwable);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return chunkState.future != null ? chunkState.future : CompletableFuture.completedFuture(null);
|
||||
} finally {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChunkStillNeeded(long index) {
|
||||
for (PlayerRef playerRef : this.world.getPlayerRefs()) {
|
||||
if (playerRef.getChunkTracker().shouldBeVisible(index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles corruption recovery by logging the corruption, marking the region file as corrupted,
|
||||
* and triggering regeneration.
|
||||
*/
|
||||
@Nonnull
|
||||
private CompletableFuture<Holder<ChunkStore>> handleCorruptionAndRegenerate(
|
||||
long index, int x, int z, int seed, Throwable corruptionCause) {
|
||||
|
||||
// Log corruption to file
|
||||
CorruptedChunkLogger.logCorruption(this.world.getName(), x, z, corruptionCause);
|
||||
|
||||
LOGGER.at(Level.WARNING).log(
|
||||
"Corrupted chunk detected at (%d, %d) in world '%s' - marking region as corrupted and regenerating. See logs/corrupted_chunks.log for details.",
|
||||
x, z, this.world.getName()
|
||||
);
|
||||
|
||||
// Mark the entire region file as corrupted (renames to .corrupted suffix)
|
||||
CompletableFuture<Void> removeFuture;
|
||||
if (this.saver != null) {
|
||||
removeFuture = this.saver.deleteRegionFile(x, z).exceptionally(removeError -> {
|
||||
LOGGER.at(Level.WARNING).withCause(removeError).log(
|
||||
"Failed to mark region as corrupted for chunk (%d, %d)", x, z
|
||||
);
|
||||
return null;
|
||||
});
|
||||
} else {
|
||||
removeFuture = CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
// After removal, regenerate the chunk
|
||||
return removeFuture.thenCompose(ignored -> {
|
||||
long readStamp = this.generatorLock.readLock();
|
||||
try {
|
||||
CompletableFuture<GeneratedChunk> genFuture;
|
||||
if (this.generator == null) {
|
||||
genFuture = this.generatorLoaded.thenCompose(
|
||||
aVoid -> this.generator.generate(seed, index, x, z, null)
|
||||
);
|
||||
} else {
|
||||
genFuture = this.generator.generate(seed, index, x, z, null);
|
||||
}
|
||||
return genFuture.thenApplyAsync(generatedChunk -> {
|
||||
if (generatedChunk != null && !this.store.isShutdown()) {
|
||||
this.totalGeneratedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStamp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isChunkOnBackoff(long index, long maxFailureBackoffNanos) {
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(index);
|
||||
if (chunkState == null) {
|
||||
return false;
|
||||
} else {
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
boolean nanosSince;
|
||||
try {
|
||||
if (chunkState.throwable != null) {
|
||||
long nanosSincex = System.nanoTime() - chunkState.failedWhen;
|
||||
int count = chunkState.failedCounter;
|
||||
return nanosSincex < Math.min(maxFailureBackoffNanos, count * count * FAILURE_BACKOFF_NANOS);
|
||||
}
|
||||
|
||||
nanosSince = false;
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
|
||||
return nanosSince;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Holder<ChunkStore> preLoadChunkAsync(long index, @Nonnull Holder<ChunkStore> holder, boolean newlyGenerated) {
|
||||
WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType());
|
||||
if (worldChunkComponent == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Holder missing WorldChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index))
|
||||
);
|
||||
} else if (worldChunkComponent.getIndex() != index) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Incorrect chunk index! Got (%d, %d) expected (%d, %d)",
|
||||
worldChunkComponent.getX(),
|
||||
worldChunkComponent.getZ(),
|
||||
ChunkUtil.xOfChunkIndex(index),
|
||||
ChunkUtil.zOfChunkIndex(index)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
BlockChunk blockChunk = holder.getComponent(BlockChunk.getComponentType());
|
||||
if (blockChunk == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Holder missing BlockChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index))
|
||||
);
|
||||
} else {
|
||||
blockChunk.loadFromHolder(holder);
|
||||
worldChunkComponent.setFlag(ChunkFlag.NEWLY_GENERATED, newlyGenerated);
|
||||
worldChunkComponent.setLightingUpdatesEnabled(false);
|
||||
if (newlyGenerated && this.world.getWorldConfig().shouldSaveNewChunks()) {
|
||||
worldChunkComponent.markNeedsSaving();
|
||||
}
|
||||
|
||||
try {
|
||||
long start = System.nanoTime();
|
||||
IEventDispatcher<ChunkPreLoadProcessEvent, ChunkPreLoadProcessEvent> dispatcher = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(ChunkPreLoadProcessEvent.class, this.world.getName());
|
||||
if (dispatcher.hasListener()) {
|
||||
ChunkPreLoadProcessEvent event = dispatcher.dispatch(new ChunkPreLoadProcessEvent(holder, worldChunkComponent, newlyGenerated, start));
|
||||
if (!event.didLog()) {
|
||||
long end = System.nanoTime();
|
||||
long diff = end - start;
|
||||
if (diff > this.world.getTickStepNanos()) {
|
||||
LOGGER.at(Level.SEVERE)
|
||||
.log(
|
||||
"Took too long to pre-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s",
|
||||
FormatUtil.nanosToString(diff),
|
||||
this.world.consumeGCHasRun(),
|
||||
worldChunkComponent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
worldChunkComponent.setLightingUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Ref<ChunkStore> postLoadChunk(@Nullable Holder<ChunkStore> holder) {
|
||||
this.world.debugAssertInTickingThread();
|
||||
if (holder != null && !this.store.isShutdown()) {
|
||||
long start = System.nanoTime();
|
||||
WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.START_INIT, true);
|
||||
if (worldChunkComponent.is(ChunkFlag.TICKING)) {
|
||||
holder.tryRemoveComponent(REGISTRY.getNonTickingComponentType());
|
||||
} else {
|
||||
holder.ensureComponent(REGISTRY.getNonTickingComponentType());
|
||||
}
|
||||
|
||||
Ref<ChunkStore> reference = this.add(holder);
|
||||
worldChunkComponent.initFlags();
|
||||
this.world.getChunkLighting().init(worldChunkComponent);
|
||||
long end = System.nanoTime();
|
||||
long diff = end - start;
|
||||
if (diff > this.world.getTickStepNanos()) {
|
||||
LOGGER.at(Level.SEVERE)
|
||||
.log(
|
||||
"Took too long to post-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s",
|
||||
FormatUtil.nanosToString(diff),
|
||||
this.world.consumeGCHasRun(),
|
||||
worldChunkComponent
|
||||
);
|
||||
}
|
||||
|
||||
return reference;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
CodecStore.STATIC.putCodecSupplier(HOLDER_CODEC_KEY, REGISTRY::getEntityCodec);
|
||||
REGISTRY.registerSystem(new ChunkStore.ChunkLoaderSaverSetupSystem());
|
||||
REGISTRY.registerSystem(new ChunkUnloadingSystem());
|
||||
REGISTRY.registerSystem(new ChunkSavingSystems.WorldRemoved());
|
||||
REGISTRY.registerSystem(new ChunkSavingSystems.Ticking());
|
||||
}
|
||||
|
||||
private static class ChunkLoadState {
|
||||
private final StampedLock lock = new StampedLock();
|
||||
private int flags = 0;
|
||||
@Nullable
|
||||
private CompletableFuture<Ref<ChunkStore>> future;
|
||||
@Nullable
|
||||
private Ref<ChunkStore> reference;
|
||||
@Nullable
|
||||
private Throwable throwable;
|
||||
private long failedWhen;
|
||||
private int failedCounter;
|
||||
|
||||
private ChunkLoadState() {
|
||||
}
|
||||
|
||||
private void fail(Throwable throwable) {
|
||||
long stamp = this.lock.writeLock();
|
||||
|
||||
try {
|
||||
this.flags = 0;
|
||||
this.future = null;
|
||||
this.throwable = throwable;
|
||||
this.failedWhen = System.nanoTime();
|
||||
this.failedCounter++;
|
||||
} finally {
|
||||
this.lock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for logging corrupted chunk information to logs/corrupted_chunks.log
|
||||
*/
|
||||
private static class CorruptedChunkLogger {
|
||||
private static final Path LOG_PATH = Paths.get("logs", "corrupted_chunks.log");
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
private static final Object WRITE_LOCK = new Object();
|
||||
|
||||
static void logCorruption(String worldName, int chunkX, int chunkZ, Throwable cause) {
|
||||
try {
|
||||
synchronized (WRITE_LOCK) {
|
||||
Files.createDirectories(LOG_PATH.getParent());
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(LOG_PATH,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
|
||||
String timestamp = FORMATTER.format(Instant.now());
|
||||
writer.write(String.format("[%s] CORRUPTED CHUNK DETECTED - Regenerating%n", timestamp));
|
||||
writer.write(String.format(" World: %s%n", worldName));
|
||||
writer.write(String.format(" Chunk: (%d, %d)%n", chunkX, chunkZ));
|
||||
writer.write(String.format(" Block coords: (%d, %d) to (%d, %d)%n",
|
||||
chunkX * 16, chunkZ * 16, chunkX * 16 + 15, chunkZ * 16 + 15));
|
||||
writer.write(String.format(" Cause: %s%n", cause.getClass().getSimpleName()));
|
||||
writer.write(String.format(" Message: %s%n", cause.getMessage()));
|
||||
|
||||
// Write stack trace
|
||||
StringWriter sw = new StringWriter();
|
||||
cause.printStackTrace(new PrintWriter(sw));
|
||||
String stackTrace = sw.toString();
|
||||
// Indent stack trace
|
||||
for (String line : stackTrace.split("\n")) {
|
||||
writer.write(String.format(" %s%n", line));
|
||||
}
|
||||
writer.write(String.format("---%n%n"));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.at(Level.WARNING).withCause(e).log("Failed to write to corrupted_chunks.log");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChunkLoaderSaverSetupSystem extends StoreSystem<ChunkStore> {
|
||||
public ChunkLoaderSaverSetupSystem() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<ChunkStore> getGroup() {
|
||||
return ChunkStore.INIT_GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemAddedToStore(@Nonnull Store<ChunkStore> store) {
|
||||
ChunkStore data = store.getExternalData();
|
||||
World world = data.getWorld();
|
||||
IChunkStorageProvider chunkStorageProvider = world.getWorldConfig().getChunkStorageProvider();
|
||||
|
||||
try {
|
||||
data.loader = chunkStorageProvider.getLoader(store);
|
||||
data.saver = chunkStorageProvider.getSaver(store);
|
||||
} catch (IOException var6) {
|
||||
throw SneakyThrow.sneakyThrow(var6);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemRemovedFromStore(@Nonnull Store<ChunkStore> store) {
|
||||
ChunkStore data = store.getExternalData();
|
||||
|
||||
try {
|
||||
if (data.loader != null) {
|
||||
IChunkLoader oldLoader = data.loader;
|
||||
data.loader = null;
|
||||
oldLoader.close();
|
||||
}
|
||||
|
||||
if (data.saver != null) {
|
||||
IChunkSaver oldSaver = data.saver;
|
||||
data.saver = null;
|
||||
oldSaver.close();
|
||||
}
|
||||
} catch (IOException var4) {
|
||||
ChunkStore.LOGGER.at(Level.SEVERE).withCause(var4).log("Failed to close storage!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class LoadFuturePacketDataQuerySystem extends EntityDataSystem<ChunkStore, PlayerRef, CompletableFuture<Packet>> {
|
||||
public LoadFuturePacketDataQuerySystem() {
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class LoadPacketDataQuerySystem extends EntityDataSystem<ChunkStore, PlayerRef, Packet> {
|
||||
public LoadPacketDataQuerySystem() {
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class UnloadPacketDataQuerySystem extends EntityDataSystem<ChunkStore, PlayerRef, Packet> {
|
||||
public UnloadPacketDataQuerySystem() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,183 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe.world.storage;
|
||||
|
||||
import com.hypixel.hytale.codec.store.CodecKey;
|
||||
import com.hypixel.hytale.codec.store.CodecStore;
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.ComponentRegistry;
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import com.hypixel.hytale.component.IResourceStorage;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.RemoveReason;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.SystemGroup;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.RefSystem;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.metrics.MetricsRegistry;
|
||||
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.tracker.NetworkId;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.WorldProvider;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectMap;
|
||||
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class EntityStore implements WorldProvider {
|
||||
@Nonnull
|
||||
public static final MetricsRegistry<EntityStore> METRICS_REGISTRY = new MetricsRegistry<EntityStore>()
|
||||
.register("Store", EntityStore::getStore, Store.METRICS_REGISTRY);
|
||||
@Nonnull
|
||||
public static final ComponentRegistry<EntityStore> REGISTRY = new ComponentRegistry<>();
|
||||
@Nonnull
|
||||
public static final CodecKey<Holder<EntityStore>> HOLDER_CODEC_KEY = new CodecKey<>("EntityHolder");
|
||||
@Nonnull
|
||||
public static final SystemGroup<EntityStore> SEND_PACKET_GROUP = REGISTRY.registerSystemGroup();
|
||||
@Nonnull
|
||||
private final AtomicInteger networkIdCounter = new AtomicInteger(1);
|
||||
@Nonnull
|
||||
private final World world;
|
||||
private Store<EntityStore> store;
|
||||
@Nonnull
|
||||
private final Map<UUID, Ref<EntityStore>> entitiesByUuid = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Ref<EntityStore>> networkIdToRef = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
public EntityStore(@Nonnull World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public void start(@Nonnull IResourceStorage resourceStorage) {
|
||||
this.store = REGISTRY.addStore(this, resourceStorage, store -> this.store = store);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.store.shutdown();
|
||||
this.entitiesByUuid.clear();
|
||||
}
|
||||
|
||||
public Store<EntityStore> getStore() {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<EntityStore> getRefFromUUID(@Nonnull UUID uuid) {
|
||||
return this.entitiesByUuid.get(uuid);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<EntityStore> getRefFromNetworkId(int networkId) {
|
||||
return this.networkIdToRef.get(networkId);
|
||||
}
|
||||
|
||||
public int takeNextNetworkId() {
|
||||
return this.networkIdCounter.getAndIncrement();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public World getWorld() {
|
||||
return this.world;
|
||||
}
|
||||
|
||||
static {
|
||||
CodecStore.STATIC.putCodecSupplier(HOLDER_CODEC_KEY, REGISTRY::getEntityCodec);
|
||||
}
|
||||
|
||||
public static class NetworkIdSystem extends RefSystem<EntityStore> {
|
||||
public NetworkIdSystem() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return NetworkId.getComponentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityStore entityStore = store.getExternalData();
|
||||
NetworkId networkIdComponent = commandBuffer.getComponent(ref, NetworkId.getComponentType());
|
||||
|
||||
assert networkIdComponent != null;
|
||||
|
||||
int networkId = networkIdComponent.getId();
|
||||
if (entityStore.networkIdToRef.putIfAbsent(networkId, ref) != null) {
|
||||
networkId = entityStore.takeNextNetworkId();
|
||||
commandBuffer.putComponent(ref, NetworkId.getComponentType(), new NetworkId(networkId));
|
||||
entityStore.networkIdToRef.put(networkId, ref);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityStore entityStore = store.getExternalData();
|
||||
NetworkId networkIdComponent = commandBuffer.getComponent(ref, NetworkId.getComponentType());
|
||||
|
||||
assert networkIdComponent != null;
|
||||
|
||||
entityStore.networkIdToRef.remove(networkIdComponent.getId(), ref);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UUIDSystem extends RefSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
public UUIDSystem() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return UUIDComponent.getComponentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
UUIDComponent uuidComponent = commandBuffer.getComponent(ref, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
Ref<EntityStore> currentRef = store.getExternalData().entitiesByUuid.putIfAbsent(uuidComponent.getUuid(), ref);
|
||||
if (currentRef != null) {
|
||||
LOGGER.at(Level.WARNING).log("Removing duplicate entity with UUID: %s", uuidComponent.getUuid());
|
||||
commandBuffer.removeEntity(ref, RemoveReason.REMOVE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
UUIDComponent uuidComponent = commandBuffer.getComponent(ref, UUIDComponent.getComponentType());
|
||||
|
||||
// HyFix: Null check for uuidComponent and uuid to prevent NPE on entity remove
|
||||
// Entities can be removed before UUID component is fully initialized
|
||||
if (uuidComponent == null) {
|
||||
LOGGER.at(Level.WARNING).log("Null UUIDComponent during entity remove - skipping UUID cleanup");
|
||||
return;
|
||||
}
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
if (uuid == null) {
|
||||
LOGGER.at(Level.WARNING).log("Null UUID in UUIDComponent during entity remove - skipping UUID cleanup");
|
||||
return;
|
||||
}
|
||||
|
||||
store.getExternalData().entitiesByUuid.remove(uuid, ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe.world.storage;
|
||||
|
||||
import com.hypixel.hytale.component.Holder;
|
||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public interface IChunkSaver extends Closeable {
|
||||
@Nonnull
|
||||
CompletableFuture<Void> saveHolder(int var1, int var2, @Nonnull Holder<ChunkStore> var3);
|
||||
|
||||
@Nonnull
|
||||
CompletableFuture<Void> removeHolder(int var1, int var2);
|
||||
|
||||
/**
|
||||
* Deletes the entire region file containing the specified chunk coordinates.
|
||||
* Used for corruption recovery when the region file is unrecoverable.
|
||||
*/
|
||||
@Nonnull
|
||||
default CompletableFuture<Void> deleteRegionFile(int x, int z) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
LongSet getIndexes() throws IOException;
|
||||
|
||||
void flush() throws IOException;
|
||||
}
|
||||
@@ -1,682 +0,0 @@
|
||||
package com.hypixel.hytale.server.core.universe.world.storage.provider;
|
||||
|
||||
import com.hypixel.fastutil.longs.Long2ObjectConcurrentHashMap;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.KeyedCodec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
|
||||
import com.hypixel.hytale.component.Resource;
|
||||
import com.hypixel.hytale.component.ResourceType;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.SystemGroup;
|
||||
import com.hypixel.hytale.component.system.StoreSystem;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.util.ChunkUtil;
|
||||
import com.hypixel.hytale.metrics.MetricProvider;
|
||||
import com.hypixel.hytale.metrics.MetricResults;
|
||||
import com.hypixel.hytale.metrics.MetricsRegistry;
|
||||
import com.hypixel.hytale.server.core.universe.Universe;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.BufferChunkLoader;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.BufferChunkSaver;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.IChunkLoader;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.IChunkSaver;
|
||||
import com.hypixel.hytale.sneakythrow.SneakyThrow;
|
||||
import com.hypixel.hytale.storage.IndexedStorageFile;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
import it.unimi.dsi.fastutil.ints.IntListIterator;
|
||||
import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry;
|
||||
import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.longs.LongSet;
|
||||
import it.unimi.dsi.fastutil.longs.LongSets;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.FileAlreadyExistsException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.ScheduledExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.logging.Level;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public class IndexedStorageChunkStorageProvider implements IChunkStorageProvider {
|
||||
public static final String ID = "IndexedStorage";
|
||||
@Nonnull
|
||||
public static final BuilderCodec<IndexedStorageChunkStorageProvider> CODEC = BuilderCodec.builder(
|
||||
IndexedStorageChunkStorageProvider.class, IndexedStorageChunkStorageProvider::new
|
||||
)
|
||||
.documentation("Uses the indexed storage file format to store chunks.")
|
||||
.<Boolean>appendInherited(
|
||||
new KeyedCodec<>("FlushOnWrite", Codec.BOOLEAN), (o, i) -> o.flushOnWrite = i, o -> o.flushOnWrite, (o, p) -> o.flushOnWrite = p.flushOnWrite
|
||||
)
|
||||
.documentation(
|
||||
"Controls whether the indexed storage flushes during writes.\nRecommended to be enabled to prevent corruption of chunks during unclean shutdowns."
|
||||
)
|
||||
.add()
|
||||
.build();
|
||||
private boolean flushOnWrite = false;
|
||||
|
||||
public IndexedStorageChunkStorageProvider() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public IChunkLoader getLoader(@Nonnull Store<ChunkStore> store) {
|
||||
return new IndexedStorageChunkStorageProvider.IndexedStorageChunkLoader(store, this.flushOnWrite);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public IChunkSaver getSaver(@Nonnull Store<ChunkStore> store) {
|
||||
return new IndexedStorageChunkStorageProvider.IndexedStorageChunkSaver(store, this.flushOnWrite);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IndexedStorageChunkStorageProvider{}";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String toFileName(int regionX, int regionZ) {
|
||||
return regionX + "." + regionZ + ".region.bin";
|
||||
}
|
||||
|
||||
private static long fromFileName(@Nonnull String fileName) {
|
||||
String[] split = fileName.split("\\.");
|
||||
if (split.length != 4) {
|
||||
throw new IllegalArgumentException("Unexpected file name format!");
|
||||
} else if (!"region".equals(split[2])) {
|
||||
throw new IllegalArgumentException("Unexpected file name format!");
|
||||
} else if (!"bin".equals(split[3])) {
|
||||
throw new IllegalArgumentException("Unexpected file extension!");
|
||||
} else {
|
||||
int regionX = Integer.parseInt(split[0]);
|
||||
int regionZ = Integer.parseInt(split[1]);
|
||||
return ChunkUtil.indexChunk(regionX, regionZ);
|
||||
}
|
||||
}
|
||||
|
||||
public static class IndexedStorageCache implements Closeable, MetricProvider, Resource<ChunkStore> {
|
||||
@Nonnull
|
||||
public static final MetricsRegistry<IndexedStorageChunkStorageProvider.IndexedStorageCache> METRICS_REGISTRY = new MetricsRegistry<IndexedStorageChunkStorageProvider.IndexedStorageCache>()
|
||||
.register(
|
||||
"Files",
|
||||
cache -> cache.cache
|
||||
.long2ObjectEntrySet()
|
||||
.stream()
|
||||
.map(IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData::new)
|
||||
.toArray(IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData[]::new),
|
||||
new ArrayCodec<>(
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData.CODEC,
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData[]::new
|
||||
)
|
||||
);
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
/**
|
||||
* How long a region file can be idle before being closed (in milliseconds)
|
||||
*/
|
||||
private static final long IDLE_TIMEOUT_MS = TimeUnit.MINUTES.toMillis(5);
|
||||
/**
|
||||
* How often to check for idle region files (in milliseconds)
|
||||
*/
|
||||
private static final long CLEANUP_INTERVAL_MS = TimeUnit.MINUTES.toMillis(1);
|
||||
|
||||
/**
|
||||
* Wrapper for IndexedStorageFile that tracks usage for async-safe cleanup.
|
||||
*/
|
||||
private static class CachedFile {
|
||||
final IndexedStorageFile file;
|
||||
final AtomicInteger activeOps = new AtomicInteger(0);
|
||||
volatile long lastAccessTime;
|
||||
volatile boolean markedForClose = false;
|
||||
|
||||
CachedFile(IndexedStorageFile file) {
|
||||
this.file = file;
|
||||
this.lastAccessTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
void recordAccess() {
|
||||
this.lastAccessTime = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a reference for an operation. Returns false if file is marked for close.
|
||||
*/
|
||||
boolean acquire() {
|
||||
if (markedForClose) {
|
||||
return false;
|
||||
}
|
||||
activeOps.incrementAndGet();
|
||||
// Double-check after increment
|
||||
if (markedForClose) {
|
||||
activeOps.decrementAndGet();
|
||||
return false;
|
||||
}
|
||||
recordAccess();
|
||||
return true;
|
||||
}
|
||||
|
||||
void release() {
|
||||
activeOps.decrementAndGet();
|
||||
}
|
||||
|
||||
boolean isIdle(long threshold) {
|
||||
return lastAccessTime < threshold && activeOps.get() == 0;
|
||||
}
|
||||
}
|
||||
|
||||
private final Long2ObjectConcurrentHashMap<IndexedStorageFile> cache = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND);
|
||||
private final Map<Long, CachedFile> trackedFiles = new ConcurrentHashMap<>();
|
||||
private Path path;
|
||||
private ScheduledExecutorService cleanupExecutor;
|
||||
|
||||
public IndexedStorageCache() {
|
||||
}
|
||||
|
||||
private void startCleanupTask() {
|
||||
if (cleanupExecutor != null) {
|
||||
return;
|
||||
}
|
||||
LOGGER.at(Level.INFO).log("Starting region file cleanup task (idle timeout: %ds, interval: %ds)",
|
||||
IDLE_TIMEOUT_MS / 1000, CLEANUP_INTERVAL_MS / 1000);
|
||||
cleanupExecutor = Executors.newSingleThreadScheduledExecutor(r -> {
|
||||
Thread t = new Thread(r, "IndexedStorageCache-Cleanup");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
cleanupExecutor.scheduleAtFixedRate(() -> {
|
||||
try {
|
||||
cleanupIdleFiles();
|
||||
} catch (Exception e) {
|
||||
LOGGER.at(Level.SEVERE).withCause(e).log("Region cleanup task failed");
|
||||
}
|
||||
}, CLEANUP_INTERVAL_MS, CLEANUP_INTERVAL_MS, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void cleanupIdleFiles() {
|
||||
long now = System.currentTimeMillis();
|
||||
long threshold = now - IDLE_TIMEOUT_MS;
|
||||
int totalFiles = trackedFiles.size();
|
||||
int idleCount = 0;
|
||||
|
||||
LOGGER.at(Level.INFO).log("Running region cleanup check: %d tracked files", totalFiles);
|
||||
|
||||
for (Map.Entry<Long, CachedFile> entry : trackedFiles.entrySet()) {
|
||||
long regionKey = entry.getKey();
|
||||
CachedFile cached = entry.getValue();
|
||||
|
||||
long idleTimeMs = now - cached.lastAccessTime;
|
||||
int activeOps = cached.activeOps.get();
|
||||
|
||||
if (cached.isIdle(threshold)) {
|
||||
idleCount++;
|
||||
// Mark for close first - prevents new acquisitions
|
||||
cached.markedForClose = true;
|
||||
|
||||
// Double-check no operations started between isIdle check and marking
|
||||
if (cached.activeOps.get() > 0) {
|
||||
cached.markedForClose = false;
|
||||
LOGGER.at(Level.INFO).log("Region cleanup skipped - ops started during close");
|
||||
continue;
|
||||
}
|
||||
|
||||
// Safe to close now
|
||||
cache.remove(regionKey);
|
||||
trackedFiles.remove(regionKey);
|
||||
|
||||
int regionX = ChunkUtil.xOfChunkIndex(regionKey);
|
||||
int regionZ = ChunkUtil.zOfChunkIndex(regionKey);
|
||||
try {
|
||||
cached.file.close();
|
||||
LOGGER.at(Level.INFO).log("%d,%d region unloaded", regionX, regionZ);
|
||||
} catch (IOException e) {
|
||||
LOGGER.at(Level.WARNING).withCause(e).log("Failed to close idle region file %d.%d", regionX, regionZ);
|
||||
}
|
||||
} else {
|
||||
LOGGER.at(Level.FINE).log("Region %d not idle: lastAccess=%dms ago, activeOps=%d, threshold=%dms",
|
||||
regionKey, idleTimeMs, activeOps, IDLE_TIMEOUT_MS);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalFiles > 0) {
|
||||
LOGGER.at(Level.INFO).log("Region cleanup complete: %d/%d files were idle", idleCount, totalFiles);
|
||||
}
|
||||
}
|
||||
|
||||
private void trackFile(long regionKey, IndexedStorageFile file) {
|
||||
trackedFiles.computeIfAbsent(regionKey, k -> new CachedFile(file));
|
||||
}
|
||||
|
||||
private void recordAccess(long regionKey) {
|
||||
CachedFile cached = trackedFiles.get(regionKey);
|
||||
if (cached != null) {
|
||||
cached.recordAccess();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquire a file for use. Must call releaseFile() when done.
|
||||
* Returns null if file doesn't exist or is being closed.
|
||||
*/
|
||||
@Nullable
|
||||
public IndexedStorageFile acquireFile(int regionX, int regionZ, boolean flushOnWrite) {
|
||||
long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
|
||||
IndexedStorageFile file = getOrTryOpen(regionX, regionZ, flushOnWrite);
|
||||
if (file == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
CachedFile cached = trackedFiles.get(regionKey);
|
||||
if (cached != null && cached.acquire()) {
|
||||
return file;
|
||||
}
|
||||
|
||||
// File is being closed, retry to get a fresh one
|
||||
return getOrTryOpen(regionX, regionZ, flushOnWrite);
|
||||
}
|
||||
|
||||
/**
|
||||
* Release a file after use.
|
||||
*/
|
||||
public void releaseFile(int regionX, int regionZ) {
|
||||
long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
|
||||
CachedFile cached = trackedFiles.get(regionKey);
|
||||
if (cached != null) {
|
||||
cached.release();
|
||||
}
|
||||
}
|
||||
|
||||
public static ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> getResourceType() {
|
||||
return Universe.get().getIndexedStorageCacheResourceType();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Long2ObjectConcurrentHashMap<IndexedStorageFile> getCache() {
|
||||
return this.cache;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
// Shutdown cleanup task first
|
||||
if (cleanupExecutor != null) {
|
||||
cleanupExecutor.shutdown();
|
||||
try {
|
||||
cleanupExecutor.awaitTermination(5, TimeUnit.SECONDS);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
cleanupExecutor = null;
|
||||
}
|
||||
|
||||
IOException exception = null;
|
||||
Iterator<IndexedStorageFile> iterator = this.cache.values().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
try {
|
||||
iterator.next().close();
|
||||
iterator.remove();
|
||||
} catch (Exception var4) {
|
||||
if (exception == null) {
|
||||
exception = new IOException("Failed to close one or more loaders!");
|
||||
}
|
||||
|
||||
exception.addSuppressed(var4);
|
||||
}
|
||||
}
|
||||
|
||||
trackedFiles.clear();
|
||||
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IndexedStorageFile getOrTryOpen(int regionX, int regionZ, boolean flushOnWrite) {
|
||||
long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
|
||||
IndexedStorageFile file = this.cache.computeIfAbsent(regionKey, k -> {
|
||||
startCleanupTask();
|
||||
Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ));
|
||||
if (!Files.exists(regionFile)) {
|
||||
return null;
|
||||
} else {
|
||||
try {
|
||||
IndexedStorageFile open = IndexedStorageFile.open(regionFile, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
||||
open.setFlushOnWrite(flushOnWrite);
|
||||
return open;
|
||||
} catch (FileNotFoundException var8) {
|
||||
return null;
|
||||
} catch (Exception var9) {
|
||||
// Corruption detected - rename file and return null to trigger regeneration
|
||||
LOGGER.at(Level.SEVERE).withCause(var9).log("Corrupted region file detected: %s", regionFile);
|
||||
try {
|
||||
Path corruptedPath = regionFile.resolveSibling(regionFile.getFileName() + ".corrupted");
|
||||
Files.move(regionFile, corruptedPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
LOGGER.at(Level.WARNING).log("Renamed corrupted file to: %s", corruptedPath);
|
||||
} catch (IOException moveErr) {
|
||||
LOGGER.at(Level.SEVERE).withCause(moveErr).log("Failed to rename corrupted file: %s", regionFile);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
if (file != null) {
|
||||
trackFile(regionKey, file);
|
||||
recordAccess(regionKey);
|
||||
}
|
||||
return file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks a region file as corrupted by renaming it with .corrupted suffix.
|
||||
* The file will be regenerated on next access.
|
||||
*/
|
||||
public void markRegionCorrupted(int regionX, int regionZ) {
|
||||
long key = ChunkUtil.indexChunk(regionX, regionZ);
|
||||
IndexedStorageFile file = this.cache.remove(key);
|
||||
CachedFile cached = trackedFiles.remove(key);
|
||||
if (cached != null) {
|
||||
cached.markedForClose = true;
|
||||
}
|
||||
if (file != null) {
|
||||
try {
|
||||
file.close();
|
||||
} catch (IOException e) {
|
||||
// Ignore close errors
|
||||
}
|
||||
}
|
||||
Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ));
|
||||
if (Files.exists(regionFile)) {
|
||||
try {
|
||||
Path corruptedPath = regionFile.resolveSibling(regionFile.getFileName() + ".corrupted");
|
||||
Files.move(regionFile, corruptedPath, StandardCopyOption.REPLACE_EXISTING);
|
||||
LOGGER.at(Level.WARNING).log("Marked region %d.%d as corrupted: %s", regionX, regionZ, corruptedPath);
|
||||
} catch (IOException e) {
|
||||
LOGGER.at(Level.SEVERE).withCause(e).log("Failed to mark region %d.%d as corrupted", regionX, regionZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public IndexedStorageFile getOrCreate(int regionX, int regionZ, boolean flushOnWrite) {
|
||||
long regionKey = ChunkUtil.indexChunk(regionX, regionZ);
|
||||
IndexedStorageFile file = this.cache.computeIfAbsent(regionKey, k -> {
|
||||
startCleanupTask();
|
||||
try {
|
||||
if (!Files.exists(this.path)) {
|
||||
try {
|
||||
Files.createDirectory(this.path);
|
||||
} catch (FileAlreadyExistsException var8) {
|
||||
}
|
||||
}
|
||||
|
||||
Path regionFile = this.path.resolve(IndexedStorageChunkStorageProvider.toFileName(regionX, regionZ));
|
||||
IndexedStorageFile open = IndexedStorageFile.open(regionFile, StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE);
|
||||
open.setFlushOnWrite(flushOnWrite);
|
||||
return open;
|
||||
} catch (IOException var9) {
|
||||
throw SneakyThrow.sneakyThrow(var9);
|
||||
}
|
||||
});
|
||||
trackFile(regionKey, file);
|
||||
recordAccess(regionKey);
|
||||
return file;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public LongSet getIndexes() throws IOException {
|
||||
if (!Files.exists(this.path)) {
|
||||
return LongSets.EMPTY_SET;
|
||||
} else {
|
||||
LongOpenHashSet chunkIndexes = new LongOpenHashSet();
|
||||
|
||||
try (Stream<Path> stream = Files.list(this.path)) {
|
||||
stream.forEach(path -> {
|
||||
if (!Files.isDirectory(path)) {
|
||||
long regionIndex;
|
||||
try {
|
||||
regionIndex = IndexedStorageChunkStorageProvider.fromFileName(path.getFileName().toString());
|
||||
} catch (IllegalArgumentException var15) {
|
||||
return;
|
||||
}
|
||||
|
||||
int regionX = ChunkUtil.xOfChunkIndex(regionIndex);
|
||||
int regionZ = ChunkUtil.zOfChunkIndex(regionIndex);
|
||||
IndexedStorageFile regionFile = this.getOrTryOpen(regionX, regionZ, true);
|
||||
if (regionFile != null) {
|
||||
IntList blobIndexes = regionFile.keys();
|
||||
IntListIterator iterator = blobIndexes.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
int blobIndex = iterator.nextInt();
|
||||
int localX = ChunkUtil.xFromColumn(blobIndex);
|
||||
int localZ = ChunkUtil.zFromColumn(blobIndex);
|
||||
int chunkX = regionX << 5 | localX;
|
||||
int chunkZ = regionZ << 5 | localZ;
|
||||
chunkIndexes.add(ChunkUtil.indexChunk(chunkX, chunkZ));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return chunkIndexes;
|
||||
}
|
||||
}
|
||||
|
||||
public void flush() throws IOException {
|
||||
IOException exception = null;
|
||||
|
||||
for (IndexedStorageFile indexedStorageFile : this.cache.values()) {
|
||||
try {
|
||||
indexedStorageFile.force(false);
|
||||
} catch (Exception var5) {
|
||||
if (exception == null) {
|
||||
exception = new IOException("Failed to close one or more loaders!");
|
||||
}
|
||||
|
||||
exception.addSuppressed(var5);
|
||||
}
|
||||
}
|
||||
|
||||
if (exception != null) {
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MetricResults toMetricResults() {
|
||||
return METRICS_REGISTRY.toMetricResults(this);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Resource<ChunkStore> clone() {
|
||||
return new IndexedStorageChunkStorageProvider.IndexedStorageCache();
|
||||
}
|
||||
|
||||
private static class CacheEntryMetricData {
|
||||
@Nonnull
|
||||
private static final Codec<IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData> CODEC = BuilderCodec.builder(
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData.class,
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache.CacheEntryMetricData::new
|
||||
)
|
||||
.append(new KeyedCodec<>("Key", Codec.LONG), (entry, o) -> entry.key = o, entry -> entry.key)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("File", IndexedStorageFile.METRICS_REGISTRY), (entry, o) -> entry.value = o, entry -> entry.value)
|
||||
.add()
|
||||
.build();
|
||||
private long key;
|
||||
private IndexedStorageFile value;
|
||||
|
||||
public CacheEntryMetricData() {
|
||||
}
|
||||
|
||||
public CacheEntryMetricData(@Nonnull Entry<IndexedStorageFile> entry) {
|
||||
this.key = entry.getLongKey();
|
||||
this.value = entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class IndexedStorageCacheSetupSystem extends StoreSystem<ChunkStore> {
|
||||
public IndexedStorageCacheSetupSystem() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<ChunkStore> getGroup() {
|
||||
return ChunkStore.INIT_GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemAddedToStore(@Nonnull Store<ChunkStore> store) {
|
||||
World world = store.getExternalData().getWorld();
|
||||
store.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).path = world.getSavePath().resolve("chunks");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemRemovedFromStore(@Nonnull Store<ChunkStore> store) {
|
||||
}
|
||||
}
|
||||
|
||||
public static class IndexedStorageChunkLoader extends BufferChunkLoader implements MetricProvider {
|
||||
private final boolean flushOnWrite;
|
||||
|
||||
public IndexedStorageChunkLoader(@Nonnull Store<ChunkStore> store, boolean flushOnWrite) {
|
||||
super(store);
|
||||
this.flushOnWrite = flushOnWrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).close();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CompletableFuture<ByteBuffer> loadBuffer(int x, int z) {
|
||||
int regionX = x >> 5;
|
||||
int regionZ = z >> 5;
|
||||
int localX = x & 31;
|
||||
int localZ = z & 31;
|
||||
int index = ChunkUtil.indexColumn(localX, localZ);
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
|
||||
.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
|
||||
return CompletableFuture.supplyAsync(SneakyThrow.sneakySupplier(() -> {
|
||||
IndexedStorageFile chunks = indexedStorageCache.getOrTryOpen(regionX, regionZ, this.flushOnWrite);
|
||||
return chunks == null ? null : chunks.readBlob(index);
|
||||
}));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public LongSet getIndexes() throws IOException {
|
||||
return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).getIndexes();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public MetricResults toMetricResults() {
|
||||
return this.getStore().getExternalData().getSaver() instanceof IndexedStorageChunkStorageProvider.IndexedStorageChunkSaver
|
||||
? null
|
||||
: this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).toMetricResults();
|
||||
}
|
||||
}
|
||||
|
||||
public static class IndexedStorageChunkSaver extends BufferChunkSaver implements MetricProvider {
|
||||
private final boolean flushOnWrite;
|
||||
|
||||
protected IndexedStorageChunkSaver(@Nonnull Store<ChunkStore> store, boolean flushOnWrite) {
|
||||
super(store);
|
||||
this.flushOnWrite = flushOnWrite;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
|
||||
.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
|
||||
indexedStorageCache.close();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CompletableFuture<Void> saveBuffer(int x, int z, @Nonnull ByteBuffer buffer) {
|
||||
int regionX = x >> 5;
|
||||
int regionZ = z >> 5;
|
||||
int localX = x & 31;
|
||||
int localZ = z & 31;
|
||||
int index = ChunkUtil.indexColumn(localX, localZ);
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
|
||||
.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
|
||||
return CompletableFuture.runAsync(SneakyThrow.sneakyRunnable(() -> {
|
||||
IndexedStorageFile chunks = indexedStorageCache.getOrCreate(regionX, regionZ, this.flushOnWrite);
|
||||
chunks.writeBlob(index, buffer);
|
||||
}));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CompletableFuture<Void> removeBuffer(int x, int z) {
|
||||
int regionX = x >> 5;
|
||||
int regionZ = z >> 5;
|
||||
int localX = x & 31;
|
||||
int localZ = z & 31;
|
||||
int index = ChunkUtil.indexColumn(localX, localZ);
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
|
||||
.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
|
||||
return CompletableFuture.runAsync(SneakyThrow.sneakyRunnable(() -> {
|
||||
IndexedStorageFile chunks = indexedStorageCache.getOrTryOpen(regionX, regionZ, this.flushOnWrite);
|
||||
if (chunks != null) {
|
||||
chunks.removeBlob(index);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public CompletableFuture<Void> deleteRegionFile(int x, int z) {
|
||||
int regionX = x >> 5;
|
||||
int regionZ = z >> 5;
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache indexedStorageCache = this.getStore()
|
||||
.getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType());
|
||||
return CompletableFuture.runAsync(() -> {
|
||||
indexedStorageCache.markRegionCorrupted(regionX, regionZ);
|
||||
});
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public LongSet getIndexes() throws IOException {
|
||||
return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).getIndexes();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void flush() throws IOException {
|
||||
this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).flush();
|
||||
}
|
||||
|
||||
@Override
|
||||
public MetricResults toMetricResults() {
|
||||
return this.getStore().getResource(IndexedStorageChunkStorageProvider.IndexedStorageCache.getResourceType()).toMetricResults();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,8 @@
|
||||
package com.hypixel.hytale.server.core.util.thread;
|
||||
|
||||
import com.hypixel.hytale.common.plugin.PluginIdentifier;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.metrics.metric.HistoricMetric;
|
||||
import com.hypixel.hytale.server.core.HytaleServer;
|
||||
import com.hypixel.hytale.server.core.ShutdownReason;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
@@ -27,6 +26,10 @@ public abstract class TickingThread implements Runnable {
|
||||
private Thread thread;
|
||||
@Nonnull
|
||||
private CompletableFuture<Void> startedFuture = new CompletableFuture<>();
|
||||
@Nullable
|
||||
private PluginIdentifier possibleFailureCause;
|
||||
@Nullable
|
||||
private Throwable failureException;
|
||||
|
||||
public TickingThread(String threadName) {
|
||||
this(threadName, 30, false);
|
||||
@@ -77,8 +80,16 @@ public abstract class TickingThread implements Runnable {
|
||||
} catch (InterruptedException var9) {
|
||||
Thread.currentThread().interrupt();
|
||||
} catch (Throwable var10) {
|
||||
HytaleLogger.getLogger().at(Level.SEVERE).withCause(var10).log("Exception in thread %s:", this.thread);
|
||||
HytaleServer.get().shutdownServer(ShutdownReason.CRASH.withMessage("TickingThread crash: " + var10.getMessage()));
|
||||
this.failureException = var10;
|
||||
this.possibleFailureCause = PluginIdentifier.identifyThirdPartyPlugin(var10);
|
||||
if (this.possibleFailureCause == null) {
|
||||
HytaleLogger.getLogger().at(Level.SEVERE).withCause(var10).log("Exception in thread %s:", this.thread);
|
||||
} else {
|
||||
HytaleLogger.getLogger()
|
||||
.at(Level.SEVERE)
|
||||
.withCause(var10)
|
||||
.log("Exception in thread %s potentially caused by %s:", this.thread, this.possibleFailureCause);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.needsShutdown.getAndSet(false)) {
|
||||
@@ -119,7 +130,6 @@ public abstract class TickingThread implements Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
public void stop() {
|
||||
Thread thread = this.thread;
|
||||
if (thread != null) {
|
||||
@@ -138,15 +148,7 @@ public abstract class TickingThread implements Runnable {
|
||||
}
|
||||
|
||||
HytaleLogger.getLogger().at(Level.SEVERE).log("Forcing TickingThread %s to stop:\n%s", thread, sb.toString());
|
||||
|
||||
// HyFix #32: Handle Java 21+ where Thread.stop() throws UnsupportedOperationException
|
||||
try {
|
||||
thread.stop();
|
||||
} catch (UnsupportedOperationException e) {
|
||||
HytaleLogger.getLogger().at(Level.WARNING).log("[HyFix] Thread.stop() not supported on Java 21+, using interrupt() instead");
|
||||
thread.interrupt();
|
||||
}
|
||||
|
||||
thread.stop();
|
||||
Thread var9 = null;
|
||||
if (this.needsShutdown.getAndSet(false)) {
|
||||
this.onShutdown();
|
||||
@@ -208,6 +210,16 @@ public abstract class TickingThread implements Runnable {
|
||||
return this.thread != null && this.thread.isAlive() && this.needsShutdown.get();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PluginIdentifier getPossibleFailureCause() {
|
||||
return this.possibleFailureCause;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Throwable getFailureException() {
|
||||
return this.failureException;
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
protected void setThread(Thread thread) {
|
||||
this.thread = thread;
|
||||
|
||||
@@ -52,6 +52,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.function.BiPredicate;
|
||||
@@ -1044,10 +1045,6 @@ public abstract class MotionControllerBase implements MotionController {
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean isDebugMode(RoleDebugFlags mode) {
|
||||
return this.getRole() != null && this.getRole().getDebugSupport().getDebugFlags().contains(mode);
|
||||
}
|
||||
|
||||
public boolean isProcessTriggersHasMoved() {
|
||||
return this.processTriggersHasMoved;
|
||||
}
|
||||
@@ -1056,16 +1053,21 @@ public abstract class MotionControllerBase implements MotionController {
|
||||
return !componentAccessor.getArchetype(ref).contains(DeathComponent.getComponentType());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDebugFlagsChanged(EnumSet<RoleDebugFlags> newFlags) {
|
||||
MotionController.super.onDebugFlagsChanged(newFlags);
|
||||
this.debugModeSteer = newFlags.contains(RoleDebugFlags.MotionControllerSteer);
|
||||
this.debugModeMove = newFlags.contains(RoleDebugFlags.MotionControllerMove);
|
||||
this.debugModeCollisions = newFlags.contains(RoleDebugFlags.Collisions);
|
||||
this.debugModeBlockCollisions = newFlags.contains(RoleDebugFlags.BlockCollisions);
|
||||
this.debugModeProbeBlockCollisions = newFlags.contains(RoleDebugFlags.ProbeBlockCollisions);
|
||||
this.debugModeValidatePositions = newFlags.contains(RoleDebugFlags.ValidatePositions);
|
||||
this.debugModeOverlaps = newFlags.contains(RoleDebugFlags.Overlaps);
|
||||
this.debugModeValidateMath = newFlags.contains(RoleDebugFlags.ValidateMath);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void activate() {
|
||||
this.debugModeSteer = this.isDebugMode(RoleDebugFlags.MotionControllerSteer);
|
||||
this.debugModeMove = this.isDebugMode(RoleDebugFlags.MotionControllerMove);
|
||||
this.debugModeCollisions = this.isDebugMode(RoleDebugFlags.Collisions);
|
||||
this.debugModeBlockCollisions = this.isDebugMode(RoleDebugFlags.BlockCollisions);
|
||||
this.debugModeProbeBlockCollisions = this.isDebugMode(RoleDebugFlags.ProbeBlockCollisions);
|
||||
this.debugModeValidatePositions = this.isDebugMode(RoleDebugFlags.ValidatePositions);
|
||||
this.debugModeOverlaps = this.isDebugMode(RoleDebugFlags.Overlaps);
|
||||
this.debugModeValidateMath = this.isDebugMode(RoleDebugFlags.ValidateMath);
|
||||
this.resetObstructedFlags();
|
||||
this.resetNavState();
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.logging.Level;
|
||||
@@ -213,11 +214,11 @@ public class Role implements IAnnotatedComponentCollection {
|
||||
this.collisionRadius = builder.getCollisionRadius();
|
||||
this.collisionViewAngle = builder.getCollisionViewAngle();
|
||||
this.collisionViewHalfAngleCosine = TrigMathUtil.cos(this.collisionViewAngle / 2.0F);
|
||||
this.separationDistance = builder.getSeparationDistance();
|
||||
this.separationWeight = builder.getSeparationWeight();
|
||||
this.separationDistanceTarget = builder.getSeparationDistanceTarget();
|
||||
this.separationNearRadiusTarget = builder.getSeparationNearRadiusTarget();
|
||||
this.separationFarRadiusTarget = builder.getSeparationFarRadiusTarget();
|
||||
this.separationDistance = builder.getSeparationDistance(builderSupport);
|
||||
this.separationWeight = builder.getSeparationWeight(builderSupport);
|
||||
this.separationDistanceTarget = builder.getSeparationDistanceTarget(builderSupport);
|
||||
this.separationNearRadiusTarget = builder.getSeparationNearRadiusTarget(builderSupport);
|
||||
this.separationFarRadiusTarget = builder.getSeparationFarRadiusTarget(builderSupport);
|
||||
this.applySeparation = builder.isApplySeparation(builderSupport);
|
||||
if (builder.isOverridingHeadPitchAngle(builderSupport)) {
|
||||
this.headPitchAngleRange = builder.getHeadPitchAngleRange(builderSupport);
|
||||
@@ -566,6 +567,11 @@ public class Role implements IAnnotatedComponentCollection {
|
||||
@Nonnull NPCEntity npcComponent, @Nonnull Map<String, MotionController> motionControllers, @Nullable String initialMotionController
|
||||
) {
|
||||
this.motionControllers = motionControllers;
|
||||
|
||||
for (Entry<String, MotionController> entry : this.motionControllers.entrySet()) {
|
||||
this.debugSupport.registerDebugFlagsListener(entry.getValue());
|
||||
}
|
||||
|
||||
this.updateMotionControllers(null, null, null, null);
|
||||
if (!this.motionControllers.isEmpty()) {
|
||||
if (initialMotionController != null && this.setActiveMotionController(null, npcComponent, initialMotionController, null)) {
|
||||
@@ -624,6 +630,10 @@ public class Role implements IAnnotatedComponentCollection {
|
||||
protected void computeActionsAndSteering(
|
||||
@Nonnull Ref<EntityStore> ref, double tickTime, @Nonnull Steering bodySteering, @Nonnull Steering headSteering, @Nonnull Store<EntityStore> store
|
||||
) {
|
||||
if (this.debugSupport.isVisSensorRanges()) {
|
||||
this.debugSupport.beginSensorVisualization();
|
||||
}
|
||||
|
||||
boolean isDead = store.getArchetype(ref).contains(DeathComponent.getComponentType());
|
||||
if (isDead) {
|
||||
if (this.deathInstruction != null) {
|
||||
|
||||
@@ -15,10 +15,17 @@ import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.HolderSystem;
|
||||
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
|
||||
import com.hypixel.hytale.component.system.tick.TickingSystem;
|
||||
import com.hypixel.hytale.math.matrix.Matrix4d;
|
||||
import com.hypixel.hytale.math.shape.Box;
|
||||
import com.hypixel.hytale.math.vector.Transform;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.hypixel.hytale.math.vector.Vector3f;
|
||||
import com.hypixel.hytale.protocol.GameMode;
|
||||
import com.hypixel.hytale.server.core.entity.Frozen;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.modules.debug.DebugUtils;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.BoundingBox;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.HeadRotation;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.NewSpawnComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
@@ -29,18 +36,23 @@ import com.hypixel.hytale.server.core.modules.entity.system.TransformSystems;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.InteractionModule;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.core.util.TargetUtil;
|
||||
import com.hypixel.hytale.server.npc.NPCPlugin;
|
||||
import com.hypixel.hytale.server.npc.components.StepComponent;
|
||||
import com.hypixel.hytale.server.npc.entities.NPCEntity;
|
||||
import com.hypixel.hytale.server.npc.movement.controllers.MotionController;
|
||||
import com.hypixel.hytale.server.npc.role.Role;
|
||||
import com.hypixel.hytale.server.npc.role.RoleDebugDisplay;
|
||||
import com.hypixel.hytale.server.npc.role.RoleDebugFlags;
|
||||
import com.hypixel.hytale.server.npc.role.support.DebugSupport;
|
||||
import com.hypixel.hytale.server.npc.role.support.EntitySupport;
|
||||
import com.hypixel.hytale.server.npc.role.support.MarkedEntitySupport;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@@ -117,13 +129,14 @@ public class RoleSystems {
|
||||
}
|
||||
|
||||
try {
|
||||
Role role2 = npcComponent.getRole();
|
||||
boolean benchmarking = NPCPlugin.get().isBenchmarkingRole();
|
||||
if (benchmarking) {
|
||||
long start = System.nanoTime();
|
||||
role.tick(entityReference, tickLength, store);
|
||||
NPCPlugin.get().collectRoleTick(role.getRoleIndex(), System.nanoTime() - start);
|
||||
role2.tick(entityReference, tickLength, store);
|
||||
NPCPlugin.get().collectRoleTick(role2.getRoleIndex(), System.nanoTime() - start);
|
||||
} else {
|
||||
role.tick(entityReference, tickLength, store);
|
||||
role2.tick(entityReference, tickLength, store);
|
||||
}
|
||||
} catch (IllegalArgumentException | IllegalStateException | NullPointerException var15) {
|
||||
NPCPlugin.get().getLogger().at(Level.SEVERE).withCause(var15).log("Failed to tick NPC: %s", npcComponent.getRoleName());
|
||||
@@ -329,7 +342,7 @@ public class RoleSystems {
|
||||
|
||||
Role role = npcComponent.getRole();
|
||||
role.getStateSupport().activate();
|
||||
role.getDebugSupport().activate();
|
||||
role.getDebugSupport().notifyDebugFlagsListeners(role.getDebugSupport().getDebugFlags());
|
||||
ModelComponent modelComponent = holder.getComponent(this.modelComponentType);
|
||||
|
||||
assert modelComponent != null;
|
||||
@@ -357,6 +370,15 @@ public class RoleSystems {
|
||||
}
|
||||
|
||||
public static class RoleDebugSystem extends SteppableTickingSystem {
|
||||
private static final float DEBUG_SHAPE_TIME = 0.1F;
|
||||
private static final float SENSOR_VIS_OPACITY = 0.4F;
|
||||
private static final double FULL_CIRCLE_EPSILON = 0.01;
|
||||
private static final float LEASH_SPHERE_RADIUS = 0.3F;
|
||||
private static final float LEASH_RING_OUTER_RADIUS = 0.5F;
|
||||
private static final float LEASH_RING_INNER_RADIUS = 0.4F;
|
||||
private static final float NPC_RING_THICKNESS = 0.1F;
|
||||
private static final float NPC_RING_OFFSET = 0.1F;
|
||||
private static final float LEASH_LINE_THICKNESS = 0.05F;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
@@ -381,7 +403,7 @@ public class RoleSystems {
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.npcComponentType;
|
||||
return Query.and(this.npcComponentType, TransformComponent.getComponentType(), BoundingBox.getComponentType());
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -397,10 +419,235 @@ public class RoleSystems {
|
||||
assert npcComponent != null;
|
||||
|
||||
Role role = npcComponent.getRole();
|
||||
RoleDebugDisplay debugDisplay = role.getDebugSupport().getDebugDisplay();
|
||||
if (debugDisplay != null) {
|
||||
debugDisplay.display(role, index, archetypeChunk, commandBuffer);
|
||||
if (role != null) {
|
||||
DebugSupport debugSupport = role.getDebugSupport();
|
||||
RoleDebugDisplay debugDisplay = debugSupport.getDebugDisplay();
|
||||
if (debugDisplay != null) {
|
||||
debugDisplay.display(role, index, archetypeChunk, commandBuffer);
|
||||
}
|
||||
|
||||
if (debugSupport.isDebugFlagSet(RoleDebugFlags.VisMarkedTargets)) {
|
||||
renderMarkedTargetArrows(role, index, archetypeChunk, commandBuffer);
|
||||
}
|
||||
|
||||
boolean hasSensorVis = debugSupport.hasSensorVisData();
|
||||
boolean hasLeashVis = debugSupport.isDebugFlagSet(RoleDebugFlags.VisLeashPosition);
|
||||
if (hasSensorVis || hasLeashVis) {
|
||||
Ref<EntityStore> npcRef = archetypeChunk.getReferenceTo(index);
|
||||
TransformComponent transformComponent = archetypeChunk.getComponent(index, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
BoundingBox boundingBoxComponent = archetypeChunk.getComponent(index, BoundingBox.getComponentType());
|
||||
|
||||
assert boundingBoxComponent != null;
|
||||
|
||||
World world = commandBuffer.getExternalData().getWorld();
|
||||
if (hasSensorVis) {
|
||||
renderSensorVisualization(debugSupport, npcRef, transformComponent, boundingBoxComponent, world, commandBuffer);
|
||||
}
|
||||
|
||||
if (hasLeashVis) {
|
||||
renderLeashPositionVisualization(npcComponent, npcRef, transformComponent, boundingBoxComponent, world);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderMarkedTargetArrows(
|
||||
@Nonnull Role role, int index, @Nonnull ArchetypeChunk<EntityStore> archetypeChunk, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
Ref<EntityStore> npcRef = archetypeChunk.getReferenceTo(index);
|
||||
Transform npcLook = TargetUtil.getLook(npcRef, commandBuffer);
|
||||
Vector3d npcEyePosition = npcLook.getPosition();
|
||||
World world = commandBuffer.getExternalData().getWorld();
|
||||
MarkedEntitySupport markedEntitySupport = role.getMarkedEntitySupport();
|
||||
Ref<EntityStore>[] entityTargets = markedEntitySupport.getEntityTargets();
|
||||
|
||||
for (int slotIndex = 0; slotIndex < entityTargets.length; slotIndex++) {
|
||||
Ref<EntityStore> targetRef = entityTargets[slotIndex];
|
||||
if (targetRef != null && targetRef.isValid()) {
|
||||
Transform targetLook = TargetUtil.getLook(targetRef, commandBuffer);
|
||||
Vector3d targetEyePosition = targetLook.getPosition();
|
||||
Vector3d direction = new Vector3d(
|
||||
targetEyePosition.x - npcEyePosition.x, targetEyePosition.y - npcEyePosition.y, targetEyePosition.z - npcEyePosition.z
|
||||
);
|
||||
Vector3f color = DebugUtils.INDEXED_COLORS[slotIndex % DebugUtils.INDEXED_COLORS.length];
|
||||
DebugUtils.addArrow(world, npcEyePosition, direction, color, 0.1F, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderSensorVisualization(
|
||||
@Nonnull DebugSupport debugSupport,
|
||||
@Nonnull Ref<EntityStore> npcRef,
|
||||
@Nonnull TransformComponent transformComponent,
|
||||
@Nonnull BoundingBox boundingBoxComponent,
|
||||
@Nonnull World world,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
List<DebugSupport.SensorVisData> sensorDataList = debugSupport.getSensorVisData();
|
||||
if (sensorDataList != null) {
|
||||
Vector3d npcPosition = transformComponent.getPosition();
|
||||
double npcMidHeight = boundingBoxComponent.getBoundingBox().max.y / 2.0;
|
||||
HeadRotation headRotation = commandBuffer.getComponent(npcRef, HeadRotation.getComponentType());
|
||||
double heading = headRotation != null ? headRotation.getRotation().getYaw() : transformComponent.getRotation().getYaw();
|
||||
sensorDataList.sort((a, b) -> Double.compare(b.range(), a.range()));
|
||||
double discStackOffset = 0.1;
|
||||
|
||||
for (int i = 0; i < sensorDataList.size(); i++) {
|
||||
DebugSupport.SensorVisData sensorData = sensorDataList.get(i);
|
||||
Vector3f color = DebugUtils.INDEXED_COLORS[sensorData.colorIndex() % DebugUtils.INDEXED_COLORS.length];
|
||||
double height = npcPosition.y + npcMidHeight + i * 0.1;
|
||||
if (sensorData.viewAngle() > 0.0 && sensorData.viewAngle() < 6.273185482025147) {
|
||||
double sectorHeading = -heading + Math.PI;
|
||||
DebugUtils.addSector(
|
||||
world,
|
||||
npcPosition.x,
|
||||
height,
|
||||
npcPosition.z,
|
||||
sectorHeading,
|
||||
sensorData.range(),
|
||||
sensorData.viewAngle(),
|
||||
sensorData.minRange(),
|
||||
color,
|
||||
0.4F,
|
||||
0.1F,
|
||||
false
|
||||
);
|
||||
} else {
|
||||
DebugUtils.addDisc(world, npcPosition.x, height, npcPosition.z, sensorData.range(), sensorData.minRange(), color, 0.4F, 0.1F, false);
|
||||
}
|
||||
}
|
||||
|
||||
Map<Ref<EntityStore>, List<DebugSupport.EntityVisData>> entityDataMap = debugSupport.getEntityVisData();
|
||||
if (entityDataMap != null) {
|
||||
double markerOffset = 0.3;
|
||||
double sphereStackOffset = 0.3;
|
||||
double defaultEntityHeight = 2.0;
|
||||
|
||||
for (Entry<Ref<EntityStore>, List<DebugSupport.EntityVisData>> entry : entityDataMap.entrySet()) {
|
||||
Ref<EntityStore> entityRef = entry.getKey();
|
||||
List<DebugSupport.EntityVisData> checks = entry.getValue();
|
||||
if (!checks.isEmpty() && entityRef.isValid()) {
|
||||
TransformComponent entityTransform = commandBuffer.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
if (entityTransform != null) {
|
||||
Vector3d entityPosition = entityTransform.getPosition();
|
||||
BoundingBox entityBoundingBox = commandBuffer.getComponent(entityRef, BoundingBox.getComponentType());
|
||||
double entityHeight = entityBoundingBox != null ? entityBoundingBox.getBoundingBox().max.y : 2.0;
|
||||
double markerBaseHeight = entityHeight + 0.3;
|
||||
boolean anyMatched = false;
|
||||
|
||||
for (DebugSupport.EntityVisData check : checks) {
|
||||
if (check.matched()) {
|
||||
anyMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
int sphereCount = 0;
|
||||
|
||||
for (DebugSupport.EntityVisData checkx : checks) {
|
||||
if (checkx.matched()) {
|
||||
Vector3f sensorColor = DebugUtils.INDEXED_COLORS[checkx.sensorColorIndex() % DebugUtils.INDEXED_COLORS.length];
|
||||
double sphereHeight = markerBaseHeight + sphereCount * 0.3;
|
||||
DebugUtils.addSphere(world, entityPosition.x, entityPosition.y + sphereHeight, entityPosition.z, sensorColor, 0.2, 0.1F);
|
||||
sphereCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (!anyMatched) {
|
||||
DebugUtils.addCube(world, entityPosition.x, entityPosition.y + markerBaseHeight, entityPosition.z, DebugUtils.COLOR_GRAY, 0.2, 0.1F);
|
||||
}
|
||||
|
||||
DebugUtils.addLine(
|
||||
world,
|
||||
npcPosition.x,
|
||||
npcPosition.y + npcMidHeight,
|
||||
npcPosition.z,
|
||||
entityPosition.x,
|
||||
entityPosition.y + markerBaseHeight,
|
||||
entityPosition.z,
|
||||
DebugUtils.COLOR_GRAY,
|
||||
0.03,
|
||||
0.1F,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
debugSupport.clearSensorVisData();
|
||||
}
|
||||
}
|
||||
|
||||
private static void renderLeashPositionVisualization(
|
||||
@Nonnull NPCEntity npcComponent,
|
||||
@Nonnull Ref<EntityStore> npcRef,
|
||||
@Nonnull TransformComponent transformComponent,
|
||||
@Nonnull BoundingBox boundingBoxComponent,
|
||||
@Nonnull World world
|
||||
) {
|
||||
if (npcComponent.requiresLeashPosition()) {
|
||||
Box boundingBox = boundingBoxComponent.getBoundingBox();
|
||||
double npcWidth = boundingBox.max.x - boundingBox.min.x;
|
||||
double npcDepth = boundingBox.max.z - boundingBox.min.z;
|
||||
double npcRingOuterRadius = Math.max(npcWidth, npcDepth) / 2.0 + 0.1F;
|
||||
double npcRingInnerRadius = npcRingOuterRadius - 0.1F;
|
||||
int colorIndex = Math.abs(npcRef.getIndex()) % DebugUtils.INDEXED_COLORS.length;
|
||||
Vector3f color = DebugUtils.INDEXED_COLORS[colorIndex];
|
||||
Vector3d leashPoint = npcComponent.getLeashPoint();
|
||||
DebugUtils.addSphere(world, leashPoint, color, 0.3F, 0.1F);
|
||||
Vector3d npcPosition = transformComponent.getPosition();
|
||||
double npcMidHeight = boundingBox.max.y / 2.0;
|
||||
double npcMidY = npcPosition.y + npcMidHeight;
|
||||
double dirX = npcPosition.x - leashPoint.x;
|
||||
double dirZ = npcPosition.z - leashPoint.z;
|
||||
double horizontalDist = Math.sqrt(dirX * dirX + dirZ * dirZ);
|
||||
if (horizontalDist > 0.001) {
|
||||
double verticalDist = npcMidY - leashPoint.y;
|
||||
double pitchAngle = Math.atan2(verticalDist, horizontalDist);
|
||||
double yawAngle = Math.atan2(dirZ, dirX);
|
||||
addChainRing(world, leashPoint.x, leashPoint.y, leashPoint.z, 0.5, 0.4F, yawAngle, -pitchAngle, color);
|
||||
addChainRing(world, npcPosition.x, npcMidY, npcPosition.z, npcRingOuterRadius, npcRingInnerRadius, yawAngle + Math.PI, pitchAngle, color);
|
||||
double hDirX = dirX / horizontalDist;
|
||||
double hDirZ = dirZ / horizontalDist;
|
||||
double cosPitch = Math.cos(pitchAngle);
|
||||
double sinPitch = Math.sin(pitchAngle);
|
||||
double leashEdgeX = leashPoint.x + hDirX * 0.5 * cosPitch;
|
||||
double leashEdgeY = leashPoint.y + sinPitch * 0.5;
|
||||
double leashEdgeZ = leashPoint.z + hDirZ * 0.5 * cosPitch;
|
||||
double npcEdgeX = npcPosition.x - hDirX * npcRingOuterRadius * cosPitch;
|
||||
double npcEdgeY = npcMidY - sinPitch * npcRingOuterRadius;
|
||||
double npcEdgeZ = npcPosition.z - hDirZ * npcRingOuterRadius * cosPitch;
|
||||
DebugUtils.addLine(world, leashEdgeX, leashEdgeY, leashEdgeZ, npcEdgeX, npcEdgeY, npcEdgeZ, color, 0.05F, 0.1F, false);
|
||||
} else {
|
||||
DebugUtils.addDisc(world, leashPoint.x, leashPoint.y, leashPoint.z, 0.5, 0.4F, color, 0.8F, 0.1F, false);
|
||||
DebugUtils.addDisc(world, npcPosition.x, npcMidY, npcPosition.z, npcRingOuterRadius, npcRingInnerRadius, color, 0.8F, 0.1F, false);
|
||||
DebugUtils.addLine(world, leashPoint.x, leashPoint.y, leashPoint.z, npcPosition.x, npcMidY, npcPosition.z, color, 0.05F, 0.1F, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addChainRing(
|
||||
@Nonnull World world,
|
||||
double x,
|
||||
double y,
|
||||
double z,
|
||||
double outerRadius,
|
||||
double innerRadius,
|
||||
double yawAngle,
|
||||
double pitchAngle,
|
||||
@Nonnull Vector3f color
|
||||
) {
|
||||
Matrix4d matrix = new Matrix4d();
|
||||
matrix.identity();
|
||||
matrix.translate(x, y, z);
|
||||
Matrix4d tmp = new Matrix4d();
|
||||
matrix.rotateAxis(yawAngle, 0.0, 1.0, 0.0, tmp);
|
||||
matrix.rotateAxis(pitchAngle, 0.0, 0.0, 1.0, tmp);
|
||||
DebugUtils.addDisc(world, matrix, outerRadius, innerRadius, color, 0.8F, 0.1F, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,430 +0,0 @@
|
||||
package com.hypixel.hytale.server.npc.systems;
|
||||
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
import com.hypixel.hytale.component.Archetype;
|
||||
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.RemoveReason;
|
||||
import com.hypixel.hytale.component.ResourceType;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.dependency.Dependency;
|
||||
import com.hypixel.hytale.component.dependency.Order;
|
||||
import com.hypixel.hytale.component.dependency.SystemDependency;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.RefSystem;
|
||||
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||
import com.hypixel.hytale.server.core.entity.reference.InvalidatablePersistentRef;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.WorldGenId;
|
||||
import com.hypixel.hytale.server.core.modules.entity.damage.DeathSystems;
|
||||
import com.hypixel.hytale.server.core.modules.time.WorldTimeResource;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.flock.StoredFlock;
|
||||
import com.hypixel.hytale.server.npc.components.SpawnBeaconReference;
|
||||
import com.hypixel.hytale.server.npc.components.SpawnMarkerReference;
|
||||
import com.hypixel.hytale.server.npc.entities.NPCEntity;
|
||||
import com.hypixel.hytale.server.spawning.SpawningPlugin;
|
||||
import com.hypixel.hytale.server.spawning.assets.spawnmarker.config.SpawnMarker;
|
||||
import com.hypixel.hytale.server.spawning.beacons.LegacySpawnBeaconEntity;
|
||||
import com.hypixel.hytale.server.spawning.controllers.BeaconSpawnController;
|
||||
import com.hypixel.hytale.server.spawning.spawnmarkers.SpawnMarkerEntity;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class SpawnReferenceSystems {
|
||||
public SpawnReferenceSystems() {
|
||||
}
|
||||
|
||||
public static class BeaconAddRemoveSystem extends RefSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, LegacySpawnBeaconEntity> legacySpawnBeaconComponent;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public BeaconAddRemoveSystem(
|
||||
@Nonnull ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType,
|
||||
@Nonnull ComponentType<EntityStore, LegacySpawnBeaconEntity> legacySpawnBeaconComponent
|
||||
) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.legacySpawnBeaconComponent = legacySpawnBeaconComponent;
|
||||
this.npcComponentType = NPCEntity.getComponentType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case LOAD:
|
||||
SpawnBeaconReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
Ref<EntityStore> markerReference = spawnReferenceComponent.getReference().getEntity(store);
|
||||
if (markerReference == null) {
|
||||
return;
|
||||
} else {
|
||||
LegacySpawnBeaconEntity legacySpawnBeaconComponent = store.getComponent(markerReference, this.legacySpawnBeaconComponent);
|
||||
|
||||
assert legacySpawnBeaconComponent != null;
|
||||
|
||||
NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
spawnReferenceComponent.getReference().setEntity(markerReference, store);
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
BeaconSpawnController spawnController = legacySpawnBeaconComponent.getSpawnController();
|
||||
|
||||
// HyFix: Add null check for spawnController before calling hasSlots()
|
||||
if (spawnController == null) {
|
||||
System.out.println("[HyFix] WARNING: null spawnController in BeaconAddRemoveSystem - despawning NPC (missing beacon type?)");
|
||||
npcComponent.setToDespawn();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!spawnController.hasSlots()) {
|
||||
npcComponent.setToDespawn();
|
||||
return;
|
||||
} else {
|
||||
spawnController.notifySpawnedEntityExists(markerReference, commandBuffer);
|
||||
}
|
||||
}
|
||||
case SPAWN:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case REMOVE:
|
||||
SpawnBeaconReference spawnReference = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
if (spawnReference == null) {
|
||||
return;
|
||||
} else {
|
||||
Ref<EntityStore> spawnBeaconRef = spawnReference.getReference().getEntity(store);
|
||||
if (spawnBeaconRef == null) {
|
||||
return;
|
||||
} else {
|
||||
LegacySpawnBeaconEntity legacySpawnBeaconComponent = store.getComponent(spawnBeaconRef, this.legacySpawnBeaconComponent);
|
||||
if (legacySpawnBeaconComponent == null) {
|
||||
return;
|
||||
} else {
|
||||
legacySpawnBeaconComponent.getSpawnController().notifyNPCRemoval(ref, store);
|
||||
}
|
||||
}
|
||||
}
|
||||
case UNLOAD:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MarkerAddRemoveSystem extends RefSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerEntity> spawnMarkerEntityComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, WorldGenId> worldGenIdComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, UUIDComponent> uuidComponentComponentType;
|
||||
@Nonnull
|
||||
private final ResourceType<EntityStore, WorldTimeResource> worldTimeResourceResourceType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public MarkerAddRemoveSystem(
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType,
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerEntity> spawnMarkerEntityComponentType
|
||||
) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.spawnMarkerEntityComponentType = spawnMarkerEntityComponentType;
|
||||
this.npcComponentType = NPCEntity.getComponentType();
|
||||
this.worldGenIdComponentType = WorldGenId.getComponentType();
|
||||
this.uuidComponentComponentType = UUIDComponent.getComponentType();
|
||||
this.worldTimeResourceResourceType = WorldTimeResource.getResourceType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcComponentType, this.uuidComponentComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case LOAD:
|
||||
SpawnMarkerReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
Ref<EntityStore> markerReference = spawnReferenceComponent.getReference().getEntity(store);
|
||||
if (markerReference == null) {
|
||||
return;
|
||||
} else {
|
||||
SpawnMarkerEntity markerTypeComponent = store.getComponent(markerReference, this.spawnMarkerEntityComponentType);
|
||||
|
||||
assert markerTypeComponent != null;
|
||||
|
||||
NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
spawnReferenceComponent.getReference().setEntity(markerReference, store);
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
markerTypeComponent.refreshTimeout();
|
||||
WorldGenId worldGenIdComponent = commandBuffer.getComponent(markerReference, this.worldGenIdComponentType);
|
||||
int worldGenId = worldGenIdComponent != null ? worldGenIdComponent.getWorldGenId() : 0;
|
||||
commandBuffer.putComponent(markerReference, WorldGenId.getComponentType(), new WorldGenId(worldGenId));
|
||||
HytaleLogger.Api context = SpawningPlugin.get().getLogger().at(Level.FINE);
|
||||
if (context.isEnabled()) {
|
||||
UUIDComponent uuidComponent = commandBuffer.getComponent(markerReference, this.uuidComponentComponentType);
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
context.log("%s synced up with marker %s", npcComponent.getRoleName(), uuid);
|
||||
}
|
||||
}
|
||||
case SPAWN:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case REMOVE:
|
||||
SpawnMarkerReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
if (spawnReferenceComponent == null) {
|
||||
return;
|
||||
} else {
|
||||
Ref<EntityStore> spawnMarkerRef = spawnReferenceComponent.getReference().getEntity(store);
|
||||
if (spawnMarkerRef == null) {
|
||||
return;
|
||||
} else {
|
||||
SpawnMarkerEntity spawnMarkerComponent = store.getComponent(spawnMarkerRef, this.spawnMarkerEntityComponentType);
|
||||
|
||||
assert spawnMarkerComponent != null;
|
||||
|
||||
UUIDComponent uuidComponent = store.getComponent(ref, this.uuidComponentComponentType);
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
int spawnCount = spawnMarkerComponent.decrementAndGetSpawnCount();
|
||||
SpawnMarker cachedMarker = spawnMarkerComponent.getCachedMarker();
|
||||
if (spawnCount > 0 && cachedMarker.getDeactivationDistance() > 0.0) {
|
||||
InvalidatablePersistentRef[] newReferences = new InvalidatablePersistentRef[spawnCount];
|
||||
int pos = 0;
|
||||
InvalidatablePersistentRef[] npcReferences = spawnMarkerComponent.getNpcReferences();
|
||||
|
||||
for (InvalidatablePersistentRef npcRef : npcReferences) {
|
||||
if (!npcRef.getUuid().equals(uuid)) {
|
||||
newReferences[pos++] = npcRef;
|
||||
}
|
||||
}
|
||||
|
||||
spawnMarkerComponent.setNpcReferences(newReferences);
|
||||
}
|
||||
|
||||
if (spawnCount <= 0 && !cachedMarker.isRealtimeRespawn()) {
|
||||
Instant instant = store.getResource(this.worldTimeResourceResourceType).getGameTime();
|
||||
Duration gameTimeRespawn = spawnMarkerComponent.pollGameTimeRespawn();
|
||||
if (gameTimeRespawn != null) {
|
||||
instant = instant.plus(gameTimeRespawn);
|
||||
}
|
||||
|
||||
spawnMarkerComponent.setSpawnAfter(instant);
|
||||
spawnMarkerComponent.setNpcReferences(null);
|
||||
StoredFlock storedFlock = spawnMarkerComponent.getStoredFlock();
|
||||
if (storedFlock != null) {
|
||||
storedFlock.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case UNLOAD:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TickingSpawnBeaconSystem extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, NPCPreTickSystem.class), new SystemDependency<>(Order.BEFORE, DeathSystems.CorpseRemoval.class)
|
||||
);
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcEntityComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public TickingSpawnBeaconSystem(@Nonnull ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.npcEntityComponentType = NPCEntity.getComponentType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcEntityComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcEntityComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
if (!npcComponent.isDespawning() && !npcComponent.isPlayingDespawnAnim()) {
|
||||
SpawnBeaconReference spawnReferenceComponent = archetypeChunk.getComponent(index, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
if (spawnReferenceComponent.tickMarkerLostTimeoutCounter(dt)) {
|
||||
Ref<EntityStore> spawnBeaconRef = spawnReferenceComponent.getReference().getEntity(commandBuffer);
|
||||
if (spawnBeaconRef != null) {
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
} else if (npcComponent.getRole().getStateSupport().isInBusyState()) {
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
} else {
|
||||
npcComponent.setToDespawn();
|
||||
HytaleLogger.Api context = SpawningPlugin.get().getLogger().at(Level.WARNING);
|
||||
if (context.isEnabled()) {
|
||||
context.log("NPCEntity despawning due to lost marker: %s", archetypeChunk.getReferenceTo(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TickingSpawnMarkerSystem extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, NPCPreTickSystem.class), new SystemDependency<>(Order.BEFORE, DeathSystems.CorpseRemoval.class)
|
||||
);
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerEntity> markerTypeComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcEntityComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public TickingSpawnMarkerSystem(
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType,
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerEntity> markerTypeComponentType
|
||||
) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.markerTypeComponentType = markerTypeComponentType;
|
||||
this.npcEntityComponentType = NPCEntity.getComponentType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcEntityComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcEntityComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
if (!npcComponent.isDespawning() && !npcComponent.isPlayingDespawnAnim()) {
|
||||
SpawnMarkerReference spawnReferenceComponent = archetypeChunk.getComponent(index, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
if (spawnReferenceComponent.tickMarkerLostTimeoutCounter(dt)) {
|
||||
Ref<EntityStore> spawnMarkerRef = spawnReferenceComponent.getReference().getEntity(commandBuffer);
|
||||
if (spawnMarkerRef != null) {
|
||||
SpawnMarkerEntity spawnMarkerComponent = commandBuffer.getComponent(spawnMarkerRef, this.markerTypeComponentType);
|
||||
|
||||
assert spawnMarkerComponent != null;
|
||||
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
spawnMarkerComponent.refreshTimeout();
|
||||
} else if (npcComponent.getRole().getStateSupport().isInBusyState()) {
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
} else {
|
||||
npcComponent.setToDespawn();
|
||||
HytaleLogger.Api context = SpawningPlugin.get().getLogger().at(Level.WARNING);
|
||||
if (context.isEnabled()) {
|
||||
context.log("NPCEntity despawning due to lost marker: %s", archetypeChunk.getReferenceTo(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,262 +0,0 @@
|
||||
package com.hypixel.hytale.server.spawning.controllers;
|
||||
|
||||
import com.hypixel.hytale.component.ComponentAccessor;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.npc.NPCPlugin;
|
||||
import com.hypixel.hytale.server.spawning.assets.spawns.config.BeaconNPCSpawn;
|
||||
import com.hypixel.hytale.server.spawning.assets.spawns.config.RoleSpawnParameters;
|
||||
import com.hypixel.hytale.server.spawning.beacons.LegacySpawnBeaconEntity;
|
||||
import com.hypixel.hytale.server.spawning.jobs.NPCBeaconSpawnJob;
|
||||
import com.hypixel.hytale.server.spawning.wrappers.BeaconSpawnWrapper;
|
||||
import it.unimi.dsi.fastutil.ints.IntOpenHashSet;
|
||||
import it.unimi.dsi.fastutil.ints.IntSet;
|
||||
import it.unimi.dsi.fastutil.objects.Object2DoubleMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2DoubleOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntMap;
|
||||
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.time.Duration;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class BeaconSpawnController extends SpawnController<NPCBeaconSpawnJob> {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final int MAX_ATTEMPTS_PER_TICK = 5;
|
||||
public static final double ROUNDING_BREAK_POINT = 0.25;
|
||||
@Nonnull
|
||||
private final Ref<EntityStore> ownerRef;
|
||||
private final List<Ref<EntityStore>> spawnedEntities = new ObjectArrayList<>();
|
||||
private final List<PlayerRef> playersInRegion = new ObjectArrayList<>();
|
||||
private int nextPlayerIndex = 0;
|
||||
private final Object2IntMap<UUID> entitiesPerPlayer = new Object2IntOpenHashMap<>();
|
||||
private final Object2DoubleMap<Ref<EntityStore>> entityTimeoutCounter = new Object2DoubleOpenHashMap<>();
|
||||
private final IntSet unspawnableRoles = new IntOpenHashSet();
|
||||
private final Comparator<PlayerRef> threatComparator = Comparator.comparingInt(playerRef -> this.entitiesPerPlayer.getOrDefault(playerRef.getUuid(), 0));
|
||||
private int baseMaxTotalSpawns;
|
||||
private int currentScaledMaxTotalSpawns;
|
||||
private int[] baseMaxConcurrentSpawns;
|
||||
private int currentScaledMaxConcurrentSpawns;
|
||||
private int spawnsThisRound;
|
||||
private int remainingSpawns;
|
||||
private boolean roundStart = true;
|
||||
private double beaconRadiusSquared;
|
||||
private double spawnRadiusSquared;
|
||||
private double despawnNPCAfterTimeout;
|
||||
private Duration despawnBeaconAfterTimeout;
|
||||
private boolean despawnNPCsIfIdle;
|
||||
|
||||
public BeaconSpawnController(@Nonnull World world, @Nonnull Ref<EntityStore> ownerRef) {
|
||||
super(world);
|
||||
this.ownerRef = ownerRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxActiveJobs() {
|
||||
return Math.min(this.remainingSpawns, this.baseMaxActiveJobs);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public NPCBeaconSpawnJob createRandomSpawnJob(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
LegacySpawnBeaconEntity legacySpawnBeaconComponent = componentAccessor.getComponent(this.ownerRef, LegacySpawnBeaconEntity.getComponentType());
|
||||
|
||||
assert legacySpawnBeaconComponent != null;
|
||||
|
||||
BeaconSpawnWrapper wrapper = legacySpawnBeaconComponent.getSpawnWrapper();
|
||||
RoleSpawnParameters spawn = wrapper.pickRole(ThreadLocalRandom.current());
|
||||
|
||||
// HyFix: Null check for spawn parameter - prevents NPE when beacon has misconfigured spawn types
|
||||
if (spawn == null) {
|
||||
System.out.println("[HyFix] WARNING: null spawn from pickRole() - returning null (missing spawn config in beacon?)");
|
||||
return null;
|
||||
} else {
|
||||
String spawnId = spawn.getId();
|
||||
int roleIndex = NPCPlugin.get().getIndex(spawnId);
|
||||
if (roleIndex >= 0 && !this.unspawnableRoles.contains(roleIndex)) {
|
||||
NPCBeaconSpawnJob job = null;
|
||||
int predictedTotal = this.spawnedEntities.size() + this.activeJobs.size();
|
||||
if (this.activeJobs.size() < this.getMaxActiveJobs()
|
||||
&& this.nextPlayerIndex < this.playersInRegion.size()
|
||||
&& predictedTotal < this.currentScaledMaxTotalSpawns) {
|
||||
job = this.idleJobs.isEmpty() ? new NPCBeaconSpawnJob() : this.idleJobs.pop();
|
||||
job.beginProbing(this.playersInRegion.get(this.nextPlayerIndex++), this.currentScaledMaxConcurrentSpawns, roleIndex, spawn.getFlockDefinition());
|
||||
this.activeJobs.add(job);
|
||||
if (this.nextPlayerIndex >= this.playersInRegion.size()) {
|
||||
this.nextPlayerIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return job;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void initialise(@Nonnull BeaconSpawnWrapper spawnWrapper) {
|
||||
BeaconNPCSpawn spawn = spawnWrapper.getSpawn();
|
||||
this.baseMaxTotalSpawns = spawn.getMaxSpawnedNpcs();
|
||||
this.baseMaxConcurrentSpawns = spawn.getConcurrentSpawnsRange();
|
||||
double beaconRadius = spawn.getBeaconRadius();
|
||||
this.beaconRadiusSquared = beaconRadius * beaconRadius;
|
||||
double spawnRadius = spawn.getSpawnRadius();
|
||||
this.spawnRadiusSquared = spawnRadius * spawnRadius;
|
||||
this.despawnNPCAfterTimeout = spawn.getNpcIdleDespawnTimeSeconds();
|
||||
this.despawnBeaconAfterTimeout = spawn.getBeaconVacantDespawnTime();
|
||||
this.despawnNPCsIfIdle = spawn.getNpcSpawnState() != null;
|
||||
}
|
||||
|
||||
public int getSpawnsThisRound() {
|
||||
return this.spawnsThisRound;
|
||||
}
|
||||
|
||||
public void setRemainingSpawns(int remainingSpawns) {
|
||||
this.remainingSpawns = remainingSpawns;
|
||||
}
|
||||
|
||||
public void addRoundSpawn() {
|
||||
this.spawnsThisRound++;
|
||||
this.remainingSpawns--;
|
||||
}
|
||||
|
||||
public boolean isRoundStart() {
|
||||
return this.roundStart;
|
||||
}
|
||||
|
||||
public void setRoundStart(boolean roundStart) {
|
||||
this.roundStart = roundStart;
|
||||
}
|
||||
|
||||
public Ref<EntityStore> getOwnerRef() {
|
||||
return this.ownerRef;
|
||||
}
|
||||
|
||||
public int[] getBaseMaxConcurrentSpawns() {
|
||||
return this.baseMaxConcurrentSpawns;
|
||||
}
|
||||
|
||||
public List<PlayerRef> getPlayersInRegion() {
|
||||
return this.playersInRegion;
|
||||
}
|
||||
|
||||
public int getCurrentScaledMaxConcurrentSpawns() {
|
||||
return this.currentScaledMaxConcurrentSpawns;
|
||||
}
|
||||
|
||||
public void setCurrentScaledMaxConcurrentSpawns(int currentScaledMaxConcurrentSpawns) {
|
||||
this.currentScaledMaxConcurrentSpawns = currentScaledMaxConcurrentSpawns;
|
||||
}
|
||||
|
||||
public Duration getDespawnBeaconAfterTimeout() {
|
||||
return this.despawnBeaconAfterTimeout;
|
||||
}
|
||||
|
||||
public double getSpawnRadiusSquared() {
|
||||
return this.spawnRadiusSquared;
|
||||
}
|
||||
|
||||
public double getBeaconRadiusSquared() {
|
||||
return this.beaconRadiusSquared;
|
||||
}
|
||||
|
||||
public int getBaseMaxTotalSpawns() {
|
||||
return this.baseMaxTotalSpawns;
|
||||
}
|
||||
|
||||
public void setCurrentScaledMaxTotalSpawns(int currentScaledMaxTotalSpawns) {
|
||||
this.currentScaledMaxTotalSpawns = currentScaledMaxTotalSpawns;
|
||||
}
|
||||
|
||||
public List<Ref<EntityStore>> getSpawnedEntities() {
|
||||
return this.spawnedEntities;
|
||||
}
|
||||
|
||||
public void setNextPlayerIndex(int nextPlayerIndex) {
|
||||
this.nextPlayerIndex = nextPlayerIndex;
|
||||
}
|
||||
|
||||
public Object2DoubleMap<Ref<EntityStore>> getEntityTimeoutCounter() {
|
||||
return this.entityTimeoutCounter;
|
||||
}
|
||||
|
||||
public Object2IntMap<UUID> getEntitiesPerPlayer() {
|
||||
return this.entitiesPerPlayer;
|
||||
}
|
||||
|
||||
public boolean isDespawnNPCsIfIdle() {
|
||||
return this.despawnNPCsIfIdle;
|
||||
}
|
||||
|
||||
public double getDespawnNPCAfterTimeout() {
|
||||
return this.despawnNPCAfterTimeout;
|
||||
}
|
||||
|
||||
public Comparator<PlayerRef> getThreatComparator() {
|
||||
return this.threatComparator;
|
||||
}
|
||||
|
||||
public void notifySpawnedEntityExists(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
this.spawnedEntities.add(ref);
|
||||
HytaleLogger.Api context = LOGGER.at(Level.FINE);
|
||||
if (context.isEnabled()) {
|
||||
UUIDComponent ownerUuidComponent = componentAccessor.getComponent(this.ownerRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert ownerUuidComponent != null;
|
||||
|
||||
context.log("Registering NPC with reference %s with Spawn Beacon %s", ref, ownerUuidComponent.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
public void onJobFinished(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (++this.spawnsThisRound >= this.currentScaledMaxConcurrentSpawns) {
|
||||
this.onAllConcurrentSpawned(componentAccessor);
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyNPCRemoval(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
this.spawnedEntities.remove(ref);
|
||||
this.entityTimeoutCounter.removeDouble(ref);
|
||||
if (this.spawnedEntities.size() == this.currentScaledMaxTotalSpawns - 1) {
|
||||
LegacySpawnBeaconEntity.prepareNextSpawnTimer(this.ownerRef, componentAccessor);
|
||||
}
|
||||
|
||||
HytaleLogger.Api context = LOGGER.at(Level.FINE);
|
||||
if (context.isEnabled()) {
|
||||
UUIDComponent ownerUuidComponent = componentAccessor.getComponent(this.ownerRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert ownerUuidComponent != null;
|
||||
|
||||
context.log("Removing NPC with reference %s from Spawn Beacon %s", ref, ownerUuidComponent.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSlots() {
|
||||
return this.spawnedEntities.size() < this.currentScaledMaxTotalSpawns;
|
||||
}
|
||||
|
||||
public void markNPCUnspawnable(int roleIndex) {
|
||||
this.unspawnableRoles.add(roleIndex);
|
||||
}
|
||||
|
||||
public void clearUnspawnableNPCs() {
|
||||
this.unspawnableRoles.clear();
|
||||
}
|
||||
|
||||
public void onAllConcurrentSpawned(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
this.spawnsThisRound = 0;
|
||||
this.remainingSpawns = 0;
|
||||
LegacySpawnBeaconEntity.prepareNextSpawnTimer(this.ownerRef, componentAccessor);
|
||||
this.roundStart = true;
|
||||
}
|
||||
}
|
||||
@@ -1,547 +0,0 @@
|
||||
package com.hypixel.hytale.server.spawning.spawnmarkers;
|
||||
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.codec.KeyedCodec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.codec.codecs.array.ArrayCodec;
|
||||
import com.hypixel.hytale.component.Component;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.RemoveReason;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.spatial.SpatialResource;
|
||||
import com.hypixel.hytale.function.consumer.TriConsumer;
|
||||
import com.hypixel.hytale.math.vector.Vector3d;
|
||||
import com.hypixel.hytale.math.vector.Vector3f;
|
||||
import com.hypixel.hytale.server.core.asset.type.model.config.Model;
|
||||
import com.hypixel.hytale.server.core.asset.type.model.config.ModelAsset;
|
||||
import com.hypixel.hytale.server.core.entity.UUIDComponent;
|
||||
import com.hypixel.hytale.server.core.entity.group.EntityGroup;
|
||||
import com.hypixel.hytale.server.core.entity.reference.InvalidatablePersistentRef;
|
||||
import com.hypixel.hytale.server.core.modules.entity.EntityModule;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.WorldGenId;
|
||||
import com.hypixel.hytale.server.core.universe.world.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import com.hypixel.hytale.server.flock.FlockPlugin;
|
||||
import com.hypixel.hytale.server.flock.StoredFlock;
|
||||
import com.hypixel.hytale.server.npc.NPCPlugin;
|
||||
import com.hypixel.hytale.server.npc.asset.builder.Builder;
|
||||
import com.hypixel.hytale.server.npc.asset.builder.BuilderInfo;
|
||||
import com.hypixel.hytale.server.npc.components.SpawnMarkerReference;
|
||||
import com.hypixel.hytale.server.npc.entities.NPCEntity;
|
||||
import com.hypixel.hytale.server.spawning.ISpawnableWithModel;
|
||||
import com.hypixel.hytale.server.spawning.SpawnTestResult;
|
||||
import com.hypixel.hytale.server.spawning.SpawningContext;
|
||||
import com.hypixel.hytale.server.spawning.SpawningPlugin;
|
||||
import com.hypixel.hytale.server.spawning.assets.spawnmarker.config.SpawnMarker;
|
||||
import it.unimi.dsi.fastutil.Pair;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ThreadLocalRandom;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public class SpawnMarkerEntity implements Component<EntityStore> {
|
||||
private static final double SPAWN_LOST_TIMEOUT = 35.0;
|
||||
@Nonnull
|
||||
private static final InvalidatablePersistentRef[] EMPTY_REFERENCES = new InvalidatablePersistentRef[0];
|
||||
public static final ArrayCodec<InvalidatablePersistentRef> NPC_REFERENCES_CODEC = new ArrayCodec<>(
|
||||
InvalidatablePersistentRef.CODEC, InvalidatablePersistentRef[]::new
|
||||
);
|
||||
@Nonnull
|
||||
public static final BuilderCodec<SpawnMarkerEntity> CODEC = BuilderCodec.builder(SpawnMarkerEntity.class, SpawnMarkerEntity::new)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnMarker", Codec.STRING),
|
||||
(spawnMarkerEntity, s) -> spawnMarkerEntity.spawnMarkerId = s,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.spawnMarkerId
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("RespawnTime", Codec.DOUBLE),
|
||||
(spawnMarkerEntity, d) -> spawnMarkerEntity.respawnCounter = d,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.respawnCounter
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnCount", Codec.INTEGER),
|
||||
(spawnMarkerEntity, i) -> spawnMarkerEntity.spawnCount = i,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.spawnCount
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("GameTimeRespawn", Codec.DURATION),
|
||||
(spawnMarkerEntity, duration) -> spawnMarkerEntity.gameTimeRespawn = duration,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.gameTimeRespawn
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnAfter", Codec.INSTANT),
|
||||
(spawnMarkerEntity, instant) -> spawnMarkerEntity.spawnAfter = instant,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.spawnAfter
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("NPCReferences", NPC_REFERENCES_CODEC),
|
||||
(spawnMarkerEntity, array) -> spawnMarkerEntity.npcReferences = array,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.npcReferences
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("PersistedFlock", StoredFlock.CODEC),
|
||||
(spawnMarkerEntity, o) -> spawnMarkerEntity.storedFlock = o,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.storedFlock
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnPosition", Vector3d.CODEC),
|
||||
(spawnMarkerEntity, v) -> spawnMarkerEntity.spawnPosition.assign(v),
|
||||
spawnMarkerEntity -> spawnMarkerEntity.storedFlock == null ? null : spawnMarkerEntity.spawnPosition
|
||||
)
|
||||
.build();
|
||||
private static final int MAX_FAILED_SPAWNS = 5;
|
||||
private String spawnMarkerId;
|
||||
private SpawnMarker cachedMarker;
|
||||
private double respawnCounter;
|
||||
@Nullable
|
||||
private Duration gameTimeRespawn;
|
||||
@Nullable
|
||||
private Instant spawnAfter;
|
||||
private int spawnCount;
|
||||
@Nullable
|
||||
private Set<UUID> suppressedBy;
|
||||
private int failedSpawns;
|
||||
@Nonnull
|
||||
private final SpawningContext context;
|
||||
private final Vector3d spawnPosition = new Vector3d();
|
||||
private InvalidatablePersistentRef[] npcReferences;
|
||||
@Nullable
|
||||
private StoredFlock storedFlock;
|
||||
@Nullable
|
||||
private List<Pair<Ref<EntityStore>, NPCEntity>> tempStorageList;
|
||||
private double timeToDeactivation;
|
||||
private boolean despawnStarted;
|
||||
private double spawnLostTimeoutCounter;
|
||||
|
||||
public static ComponentType<EntityStore, SpawnMarkerEntity> getComponentType() {
|
||||
return SpawningPlugin.get().getSpawnMarkerComponentType();
|
||||
}
|
||||
|
||||
public SpawnMarkerEntity() {
|
||||
this.context = new SpawningContext();
|
||||
this.npcReferences = EMPTY_REFERENCES;
|
||||
}
|
||||
|
||||
public SpawnMarker getCachedMarker() {
|
||||
return this.cachedMarker;
|
||||
}
|
||||
|
||||
public void setCachedMarker(@Nonnull SpawnMarker marker) {
|
||||
this.cachedMarker = marker;
|
||||
}
|
||||
|
||||
public int getSpawnCount() {
|
||||
return this.spawnCount;
|
||||
}
|
||||
|
||||
public void setSpawnCount(int spawnCount) {
|
||||
this.spawnCount = spawnCount;
|
||||
}
|
||||
|
||||
public void setRespawnCounter(double respawnCounter) {
|
||||
this.respawnCounter = respawnCounter;
|
||||
}
|
||||
|
||||
public void setSpawnAfter(@Nullable Instant spawnAfter) {
|
||||
this.spawnAfter = spawnAfter;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Instant getSpawnAfter() {
|
||||
return this.spawnAfter;
|
||||
}
|
||||
|
||||
public void setGameTimeRespawn(@Nullable Duration gameTimeRespawn) {
|
||||
this.gameTimeRespawn = gameTimeRespawn;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Duration pollGameTimeRespawn() {
|
||||
Duration ret = this.gameTimeRespawn;
|
||||
this.gameTimeRespawn = null;
|
||||
return ret;
|
||||
}
|
||||
|
||||
public boolean tickRespawnTimer(float dt) {
|
||||
return (this.respawnCounter -= dt) <= 0.0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Set<UUID> getSuppressedBy() {
|
||||
return this.suppressedBy;
|
||||
}
|
||||
|
||||
public void setStoredFlock(@Nonnull StoredFlock storedFlock) {
|
||||
this.storedFlock = storedFlock;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredFlock getStoredFlock() {
|
||||
return this.storedFlock;
|
||||
}
|
||||
|
||||
public double getTimeToDeactivation() {
|
||||
return this.timeToDeactivation;
|
||||
}
|
||||
|
||||
public void setTimeToDeactivation(double timeToDeactivation) {
|
||||
this.timeToDeactivation = timeToDeactivation;
|
||||
}
|
||||
|
||||
public boolean tickTimeToDeactivation(float dt) {
|
||||
return (this.timeToDeactivation -= dt) <= 0.0;
|
||||
}
|
||||
|
||||
public boolean tickSpawnLostTimeout(float dt) {
|
||||
return (this.spawnLostTimeoutCounter -= dt) <= 0.0;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Vector3d getSpawnPosition() {
|
||||
return this.spawnPosition;
|
||||
}
|
||||
|
||||
public InvalidatablePersistentRef[] getNpcReferences() {
|
||||
return this.npcReferences;
|
||||
}
|
||||
|
||||
public void setNpcReferences(@Nullable InvalidatablePersistentRef[] npcReferences) {
|
||||
this.npcReferences = npcReferences != null ? npcReferences : EMPTY_REFERENCES;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Pair<Ref<EntityStore>, NPCEntity>> getTempStorageList() {
|
||||
return this.tempStorageList;
|
||||
}
|
||||
|
||||
public void setTempStorageList(@Nonnull List<Pair<Ref<EntityStore>, NPCEntity>> tempStorageList) {
|
||||
this.tempStorageList = tempStorageList;
|
||||
}
|
||||
|
||||
public boolean isDespawnStarted() {
|
||||
return this.despawnStarted;
|
||||
}
|
||||
|
||||
public void setDespawnStarted(boolean despawnStarted) {
|
||||
this.despawnStarted = despawnStarted;
|
||||
}
|
||||
|
||||
public void refreshTimeout() {
|
||||
this.spawnLostTimeoutCounter = 35.0;
|
||||
}
|
||||
|
||||
public boolean spawnNPC(@Nonnull Ref<EntityStore> ref, @Nonnull SpawnMarker marker, @Nonnull Store<EntityStore> store) {
|
||||
SpawnMarker.SpawnConfiguration spawn = marker.getWeightedConfigurations().get(ThreadLocalRandom.current());
|
||||
if (spawn == null) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s has no spawn configuration to spawn", ref);
|
||||
this.refreshTimeout();
|
||||
return false;
|
||||
} else {
|
||||
boolean realtime = marker.isRealtimeRespawn();
|
||||
if (realtime) {
|
||||
this.respawnCounter = spawn.getRealtimeRespawnTime();
|
||||
} else {
|
||||
this.spawnAfter = null;
|
||||
this.gameTimeRespawn = spawn.getSpawnAfterGameTime();
|
||||
}
|
||||
|
||||
UUIDComponent uuidComponent = store.getComponent(ref, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
String roleName = spawn.getNpc();
|
||||
if (roleName != null && !roleName.isEmpty()) {
|
||||
NPCPlugin npcModule = NPCPlugin.get();
|
||||
int roleIndex = npcModule.getIndex(roleName);
|
||||
TransformComponent transformComponent = store.getComponent(ref, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
Vector3d position = transformComponent.getPosition();
|
||||
BuilderInfo builderInfo = npcModule.getRoleBuilderInfo(roleIndex);
|
||||
if (builderInfo == null) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s attempted to spawn non-existent NPC role '%s'", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.NONEXISTENT_ROLE);
|
||||
return false;
|
||||
} else {
|
||||
Builder<?> role = builderInfo.isValid() ? builderInfo.getBuilder() : null;
|
||||
if (role == null) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s attempted to spawn invalid NPC role '%s'", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.INVALID_ROLE);
|
||||
return false;
|
||||
} else if (!role.isSpawnable()) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s attempted to spawn a non-spawnable (abstract) role '%s'", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.INVALID_ROLE);
|
||||
return false;
|
||||
} else if (!this.context.setSpawnable((ISpawnableWithModel) role)) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.SEVERE)
|
||||
.log("Marker %s failed to spawn NPC role '%s' due to failed role validation", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.FAILED_ROLE_VALIDATION);
|
||||
return false;
|
||||
} else {
|
||||
ObjectList<Ref<EntityStore>> results = SpatialResource.getThreadLocalReferenceList();
|
||||
SpatialResource<Ref<EntityStore>, EntityStore> spatialResource = store.getResource(EntityModule.get().getPlayerSpatialResourceType());
|
||||
spatialResource.getSpatialStructure().collect(position, marker.getExclusionRadius(), results);
|
||||
boolean hasPlayersInRange = !results.isEmpty();
|
||||
if (hasPlayersInRange) {
|
||||
this.refreshTimeout();
|
||||
return false;
|
||||
} else {
|
||||
World world = store.getExternalData().getWorld();
|
||||
if (!this.context.set(world, position.x, position.y, position.z)) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s attempted to spawn NPC '%s' at %s but could not fit", uuid, roleName, position);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.NO_ROOM);
|
||||
return false;
|
||||
} else {
|
||||
SpawnTestResult testResult = this.context.canSpawn(true, false);
|
||||
if (testResult != SpawnTestResult.TEST_OK) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s attempted to spawn NPC '%s' at %s but could not fit: %s", uuid, roleName, position, testResult);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.NO_ROOM);
|
||||
return false;
|
||||
} else {
|
||||
this.spawnPosition.assign(this.context.xSpawn, this.context.ySpawn, this.context.zSpawn);
|
||||
if (this.spawnPosition.distanceSquaredTo(position) > marker.getMaxDropHeightSquared()) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s attempted to spawn NPC '%s' but was offset too far from the ground at %s", uuid, roleName, position);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.TOO_HIGH);
|
||||
return false;
|
||||
} else {
|
||||
TriConsumer<NPCEntity, Ref<EntityStore>, Store<EntityStore>> postSpawn = (_entity, _ref, _store) -> {
|
||||
SpawnMarkerReference spawnMarkerReference = _store.ensureAndGetComponent(_ref, SpawnMarkerReference.getComponentType());
|
||||
spawnMarkerReference.getReference().setEntity(ref, _store);
|
||||
spawnMarkerReference.refreshTimeoutCounter();
|
||||
WorldGenId worldGenIdComponent = _store.getComponent(ref, WorldGenId.getComponentType());
|
||||
int worldGenId = worldGenIdComponent != null ? worldGenIdComponent.getWorldGenId() : 0;
|
||||
_store.putComponent(_ref, WorldGenId.getComponentType(), new WorldGenId(worldGenId));
|
||||
};
|
||||
Vector3f rotation = transformComponent.getRotation();
|
||||
Pair<Ref<EntityStore>, NPCEntity> npcPair = npcModule.spawnEntity(store, roleIndex, this.spawnPosition, rotation, null, postSpawn);
|
||||
if (npcPair == null) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.SEVERE)
|
||||
.log("Marker %s failed to spawn NPC role '%s' due to an internal error", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.INVALID_ROLE);
|
||||
return false;
|
||||
} else {
|
||||
Ref<EntityStore> npcRef = npcPair.first();
|
||||
NPCEntity npcComponent = npcPair.second();
|
||||
Ref<EntityStore> flockReference = FlockPlugin.trySpawnFlock(
|
||||
npcRef, npcComponent, store, roleIndex, this.spawnPosition, rotation, spawn.getFlockDefinition(), postSpawn
|
||||
);
|
||||
EntityGroup group = flockReference == null ? null : store.getComponent(flockReference, EntityGroup.getComponentType());
|
||||
this.spawnCount = group != null ? group.size() : 1;
|
||||
if (this.storedFlock != null) {
|
||||
this.despawnStarted = false;
|
||||
this.npcReferences = new InvalidatablePersistentRef[this.spawnCount];
|
||||
if (group != null) {
|
||||
group.forEachMember((index, member, referenceArray) -> {
|
||||
InvalidatablePersistentRef referencex = new InvalidatablePersistentRef();
|
||||
referencex.setEntity(member, store);
|
||||
referenceArray[index] = referencex;
|
||||
}, this.npcReferences);
|
||||
} else {
|
||||
InvalidatablePersistentRef reference = new InvalidatablePersistentRef();
|
||||
reference.setEntity(npcRef, store);
|
||||
this.npcReferences[0] = reference;
|
||||
}
|
||||
|
||||
this.storedFlock.clear();
|
||||
}
|
||||
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log(
|
||||
"Marker %s spawned %s and set respawn to %s",
|
||||
uuid,
|
||||
npcComponent.getRoleName(),
|
||||
realtime ? this.respawnCounter : this.gameTimeRespawn
|
||||
);
|
||||
this.refreshTimeout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s performed noop spawn and set repawn to %s", uuid, realtime ? this.respawnCounter : this.gameTimeRespawn);
|
||||
this.refreshTimeout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(
|
||||
@Nonnull Ref<EntityStore> self,
|
||||
@Nonnull UUID uuid,
|
||||
@Nonnull String role,
|
||||
@Nonnull Vector3d position,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull SpawnMarkerEntity.FailReason reason
|
||||
) {
|
||||
if (++this.failedSpawns >= 5) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.WARNING)
|
||||
.log("Marker %s at %s removed due to repeated spawning fails of %s with reason: %s", uuid, position, role, reason);
|
||||
store.removeEntity(self, RemoveReason.REMOVE);
|
||||
} else {
|
||||
this.refreshTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSpawnMarker(@Nonnull SpawnMarker marker) {
|
||||
this.spawnMarkerId = marker.getId();
|
||||
this.cachedMarker = marker;
|
||||
if (this.cachedMarker.getDeactivationDistance() > 0.0) {
|
||||
this.storedFlock = new StoredFlock();
|
||||
this.tempStorageList = new ObjectArrayList<>();
|
||||
} else {
|
||||
this.storedFlock = null;
|
||||
this.tempStorageList = null;
|
||||
}
|
||||
}
|
||||
|
||||
public int decrementAndGetSpawnCount() {
|
||||
return --this.spawnCount;
|
||||
}
|
||||
|
||||
public String getSpawnMarkerId() {
|
||||
return this.spawnMarkerId;
|
||||
}
|
||||
|
||||
public boolean isManualTrigger() {
|
||||
return this.cachedMarker.isManualTrigger();
|
||||
}
|
||||
|
||||
public boolean trigger(@Nonnull Ref<EntityStore> markerRef, @Nonnull Store<EntityStore> store) {
|
||||
return this.cachedMarker.isManualTrigger() && this.spawnCount <= 0 ? this.spawnNPC(markerRef, this.cachedMarker, store) : false;
|
||||
}
|
||||
|
||||
public void suppress(@Nonnull UUID suppressor) {
|
||||
if (this.suppressedBy == null) {
|
||||
this.suppressedBy = new HashSet<>();
|
||||
}
|
||||
|
||||
this.suppressedBy.add(suppressor);
|
||||
}
|
||||
|
||||
public void releaseSuppression(@Nonnull UUID suppressor) {
|
||||
if (this.suppressedBy != null) {
|
||||
this.suppressedBy.remove(suppressor);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearAllSuppressions() {
|
||||
if (this.suppressedBy != null) {
|
||||
this.suppressedBy.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<EntityStore> clone() {
|
||||
SpawnMarkerEntity spawnMarker = new SpawnMarkerEntity();
|
||||
spawnMarker.spawnMarkerId = this.spawnMarkerId;
|
||||
spawnMarker.cachedMarker = this.cachedMarker;
|
||||
spawnMarker.respawnCounter = this.respawnCounter;
|
||||
spawnMarker.gameTimeRespawn = this.gameTimeRespawn;
|
||||
spawnMarker.spawnAfter = this.spawnAfter;
|
||||
spawnMarker.spawnCount = this.spawnCount;
|
||||
spawnMarker.suppressedBy = this.suppressedBy != null ? new HashSet<>(this.suppressedBy) : null;
|
||||
spawnMarker.failedSpawns = this.failedSpawns;
|
||||
spawnMarker.spawnPosition.assign(this.spawnPosition);
|
||||
spawnMarker.npcReferences = this.npcReferences;
|
||||
spawnMarker.storedFlock = this.storedFlock != null ? this.storedFlock.clone() : null;
|
||||
spawnMarker.timeToDeactivation = this.timeToDeactivation;
|
||||
spawnMarker.despawnStarted = this.despawnStarted;
|
||||
spawnMarker.spawnLostTimeoutCounter = this.spawnLostTimeoutCounter;
|
||||
return spawnMarker;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SpawnMarkerEntity{spawnMarkerId='"
|
||||
+ this.spawnMarkerId
|
||||
+ "', cachedMarker="
|
||||
+ this.cachedMarker
|
||||
+ ", respawnCounter="
|
||||
+ this.respawnCounter
|
||||
+ ", gameTimeRespawn="
|
||||
+ this.gameTimeRespawn
|
||||
+ ", spawnAfter="
|
||||
+ this.spawnAfter
|
||||
+ ", spawnCount="
|
||||
+ this.spawnCount
|
||||
+ ", spawnLostTimeoutCounter="
|
||||
+ this.spawnLostTimeoutCounter
|
||||
+ ", failedSpawns="
|
||||
+ this.failedSpawns
|
||||
+ ", context="
|
||||
+ this.context
|
||||
+ ", spawnPosition="
|
||||
+ this.spawnPosition
|
||||
+ ", storedFlock="
|
||||
+ this.storedFlock
|
||||
+ "} "
|
||||
+ super.toString();
|
||||
}
|
||||
|
||||
public static Model getModel(@Nonnull SpawnMarker marker) {
|
||||
String modelName = marker.getModel();
|
||||
ModelAsset modelAsset = null;
|
||||
if (modelName != null && !modelName.isEmpty()) {
|
||||
modelAsset = ModelAsset.getAssetMap().getAsset(modelName);
|
||||
}
|
||||
|
||||
Model model;
|
||||
if (modelAsset == null) {
|
||||
model = SpawningPlugin.get().getSpawnMarkerModel();
|
||||
} else {
|
||||
model = Model.createUnitScaleModel(modelAsset);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private static enum FailReason {
|
||||
INVALID_ROLE,
|
||||
NONEXISTENT_ROLE,
|
||||
FAILED_ROLE_VALIDATION,
|
||||
NO_ROOM,
|
||||
TOO_HIGH;
|
||||
|
||||
private FailReason() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,912 +0,0 @@
|
||||
package com.hypixel.hytale.storage;
|
||||
|
||||
import com.github.luben.zstd.Zstd;
|
||||
import com.hypixel.hytale.codec.Codec;
|
||||
import com.hypixel.hytale.metrics.MetricsRegistry;
|
||||
import com.hypixel.hytale.unsafe.UnsafeUtil;
|
||||
import it.unimi.dsi.fastutil.ints.IntArrayList;
|
||||
import it.unimi.dsi.fastutil.ints.IntList;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.Closeable;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.MappedByteBuffer;
|
||||
import java.nio.channels.FileChannel;
|
||||
import java.nio.channels.FileChannel.MapMode;
|
||||
import java.nio.channels.FileLock;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.OpenOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
import java.nio.file.attribute.FileAttribute;
|
||||
import java.util.Arrays;
|
||||
import java.util.BitSet;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.locks.StampedLock;
|
||||
|
||||
public class IndexedStorageFile implements Closeable {
|
||||
public static final StampedLock[] EMPTY_STAMPED_LOCKS = new StampedLock[0];
|
||||
public static final MetricsRegistry<IndexedStorageFile> METRICS_REGISTRY = new MetricsRegistry<IndexedStorageFile>()
|
||||
.register("Size", file -> {
|
||||
try {
|
||||
return file.size();
|
||||
} catch (IOException var2) {
|
||||
return -1L;
|
||||
}
|
||||
}, Codec.LONG)
|
||||
.register("CompressionLevel", file -> file.getCompressionLevel(), Codec.INTEGER)
|
||||
.register("BlobCount", file -> file.getBlobCount(), Codec.INTEGER)
|
||||
.register("UsedBlobCount", file -> file.keys().size(), Codec.INTEGER)
|
||||
.register("SegmentSize", file -> file.segmentSize(), Codec.INTEGER)
|
||||
.register("SegmentCount", file -> file.segmentCount(), Codec.INTEGER);
|
||||
public static final String MAGIC_STRING = "HytaleIndexedStorage";
|
||||
public static final int VERSION = 1;
|
||||
public static final int DEFAULT_BLOB_COUNT = 1024;
|
||||
public static final int DEFAULT_SEGMENT_SIZE = 4096;
|
||||
public static final int DEFAULT_COMPRESSION_LEVEL = 3;
|
||||
static final IndexedStorageFile.OffsetHelper HOH = new IndexedStorageFile.OffsetHelper();
|
||||
public static final int MAGIC_LENGTH = 20;
|
||||
public static final int MAGIC_OFFSET = HOH.next(20);
|
||||
public static final int VERSION_OFFSET = HOH.next(4);
|
||||
public static final int BLOB_COUNT_OFFSET = HOH.next(4);
|
||||
public static final int SEGMENT_SIZE_OFFSET = HOH.next(4);
|
||||
public static final int HEADER_LENGTH = HOH.length();
|
||||
static final IndexedStorageFile.OffsetHelper BOH = new IndexedStorageFile.OffsetHelper();
|
||||
public static final int SRC_LENGTH_OFFSET = BOH.next(4);
|
||||
public static final int COMPRESSED_LENGTH_OFFSET = BOH.next(4);
|
||||
public static final int BLOB_HEADER_LENGTH = BOH.length();
|
||||
public static final int INDEX_SIZE = 4;
|
||||
public static final int UNASSIGNED_INDEX = 0;
|
||||
public static final int FIRST_SEGMENT_INDEX = 1;
|
||||
public static final FileAttribute<?>[] NO_ATTRIBUTES = new FileAttribute[0];
|
||||
static final byte[] MAGIC_BYTES = "HytaleIndexedStorage".getBytes(StandardCharsets.UTF_8);
|
||||
private static final ByteBuffer MAGIC_BUFFER = ByteBuffer.wrap(MAGIC_BYTES);
|
||||
private static final ThreadLocal<ByteBuffer> CACHED_TEMP_BUFFER = ThreadLocal.withInitial(() -> ByteBuffer.allocateDirect(HEADER_LENGTH));
|
||||
@Nonnull
|
||||
private final Path path;
|
||||
private final FileChannel fileChannel;
|
||||
private boolean flushOnWrite = false;
|
||||
private int compressionLevel = 3;
|
||||
private int version;
|
||||
private int blobCount;
|
||||
private int segmentSize;
|
||||
private StampedLock[] indexLocks;
|
||||
private MappedByteBuffer mappedBlobIndexes;
|
||||
private final StampedLock segmentLocksLock = new StampedLock();
|
||||
private volatile StampedLock[] segmentLocks = EMPTY_STAMPED_LOCKS;
|
||||
private final StampedLock usedSegmentsLock = new StampedLock();
|
||||
private final BitSet usedSegments = new BitSet();
|
||||
|
||||
@Nonnull
|
||||
private static ByteBuffer getTempBuffer(int length) {
|
||||
ByteBuffer buffer = CACHED_TEMP_BUFFER.get();
|
||||
buffer.position(0);
|
||||
buffer.limit(length);
|
||||
return buffer;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static ByteBuffer allocateDirect(int length) {
|
||||
return ByteBuffer.allocateDirect(length);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static IndexedStorageFile open(@Nonnull Path path, OpenOption... options) throws IOException {
|
||||
return open(path, 1024, 4096, Set.of(options), NO_ATTRIBUTES);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static IndexedStorageFile open(@Nonnull Path path, @Nonnull Set<? extends OpenOption> options, FileAttribute<?>... attrs) throws IOException {
|
||||
return open(path, 1024, 4096, options, attrs);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static IndexedStorageFile open(@Nonnull Path path, int blobCount, int segmentSize, OpenOption... options) throws IOException {
|
||||
return open(path, blobCount, segmentSize, Set.of(options), NO_ATTRIBUTES);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static IndexedStorageFile open(
|
||||
@Nonnull Path path, int blobCount, int segmentSize, @Nonnull Set<? extends OpenOption> options, FileAttribute<?>... attrs
|
||||
) throws IOException {
|
||||
IndexedStorageFile storageFile = new IndexedStorageFile(path, FileChannel.open(path, options, attrs));
|
||||
if (options.contains(StandardOpenOption.CREATE_NEW)) {
|
||||
storageFile.create(blobCount, segmentSize);
|
||||
return storageFile;
|
||||
} else {
|
||||
if (options.contains(StandardOpenOption.CREATE) && storageFile.fileChannel.size() == 0L) {
|
||||
storageFile.create(blobCount, segmentSize);
|
||||
} else {
|
||||
if (storageFile.fileChannel.size() == 0L) {
|
||||
throw new IOException("file channel is empty");
|
||||
}
|
||||
|
||||
try {
|
||||
storageFile.readHeader();
|
||||
storageFile.memoryMapBlobIndexes();
|
||||
if (storageFile.version == 0) {
|
||||
storageFile = migrateV0(path, blobCount, segmentSize, options, attrs, storageFile);
|
||||
} else {
|
||||
storageFile.readUsedSegments();
|
||||
}
|
||||
} catch (CorruptedStorageException e) {
|
||||
// Corruption detected - delete and recreate the file
|
||||
System.err.println("[IndexedStorageFile] Corruption detected in " + path + ": " + e.getMessage() + " - deleting and recreating");
|
||||
storageFile.close();
|
||||
Files.delete(path);
|
||||
storageFile = new IndexedStorageFile(path, FileChannel.open(path, options, attrs));
|
||||
storageFile.create(blobCount, segmentSize);
|
||||
}
|
||||
}
|
||||
|
||||
return storageFile;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Exception thrown when storage file corruption is detected.
|
||||
*/
|
||||
public static class CorruptedStorageException extends IOException {
|
||||
public CorruptedStorageException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
||||
private static IndexedStorageFile migrateV0(
|
||||
Path path, int blobCount, int segmentSize, Set<? extends OpenOption> options, FileAttribute<?>[] attrs, IndexedStorageFile storageFile
|
||||
) throws IOException {
|
||||
storageFile.close();
|
||||
Path tempFile = path.resolveSibling(path.getFileName().toString() + ".old");
|
||||
Path tempPath = Files.move(path, tempFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
HashSet<OpenOption> newOptions = new HashSet<>(options);
|
||||
newOptions.add(StandardOpenOption.CREATE);
|
||||
storageFile = new IndexedStorageFile(path, FileChannel.open(path, newOptions, attrs));
|
||||
storageFile.create(blobCount, segmentSize);
|
||||
|
||||
try (IndexedStorageFile_v0 oldStorageFile = new IndexedStorageFile_v0(tempPath, FileChannel.open(tempPath, options, attrs))) {
|
||||
oldStorageFile.open();
|
||||
|
||||
for (int blobIndex = 0; blobIndex < blobCount; blobIndex++) {
|
||||
ByteBuffer blob = oldStorageFile.readBlob(blobIndex);
|
||||
if (blob != null) {
|
||||
storageFile.writeBlob(blobIndex, blob);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
Files.delete(tempFile);
|
||||
}
|
||||
|
||||
return storageFile;
|
||||
}
|
||||
|
||||
private IndexedStorageFile(@Nonnull Path path, @Nonnull FileChannel fileChannel) {
|
||||
this.path = path;
|
||||
this.fileChannel = fileChannel;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Path getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
public int getBlobCount() {
|
||||
return this.blobCount;
|
||||
}
|
||||
|
||||
public int getSegmentSize() {
|
||||
return this.segmentSize;
|
||||
}
|
||||
|
||||
public int getCompressionLevel() {
|
||||
return this.compressionLevel;
|
||||
}
|
||||
|
||||
public void setFlushOnWrite(boolean flushOnWrite) {
|
||||
this.flushOnWrite = flushOnWrite;
|
||||
}
|
||||
|
||||
public void setCompressionLevel(int compressionLevel) {
|
||||
this.compressionLevel = compressionLevel;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected IndexedStorageFile create(int blobCount, int segmentSize) throws IOException {
|
||||
if (blobCount <= 0) {
|
||||
throw new IllegalArgumentException("blobCount must be > 0");
|
||||
} else if (segmentSize <= 0) {
|
||||
throw new IllegalArgumentException("segmentSize must be > 0");
|
||||
} else {
|
||||
this.blobCount = blobCount;
|
||||
this.segmentSize = segmentSize;
|
||||
if (this.fileChannel.size() != 0L) {
|
||||
throw new IOException("file channel is not empty");
|
||||
} else {
|
||||
this.writeHeader(blobCount, segmentSize);
|
||||
this.memoryMapBlobIndexes();
|
||||
return this;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void writeHeader(int blobCount, int segmentSize) throws IOException {
|
||||
ByteBuffer header = getTempBuffer(HEADER_LENGTH);
|
||||
header.put(MAGIC_BYTES);
|
||||
header.putInt(VERSION_OFFSET, 1);
|
||||
header.putInt(BLOB_COUNT_OFFSET, blobCount);
|
||||
header.putInt(SEGMENT_SIZE_OFFSET, segmentSize);
|
||||
header.position(0);
|
||||
if (this.fileChannel.write(header, 0L) != HEADER_LENGTH) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
protected void readHeader() throws IOException {
|
||||
ByteBuffer header = getTempBuffer(HEADER_LENGTH);
|
||||
if (this.fileChannel.read(header, 0L) != HEADER_LENGTH) {
|
||||
throw new IllegalStateException();
|
||||
} else {
|
||||
header.position(0);
|
||||
header.limit(20);
|
||||
if (!MAGIC_BUFFER.equals(header)) {
|
||||
header.position(0);
|
||||
byte[] dst = new byte[20];
|
||||
header.get(dst);
|
||||
throw new IOException("Invalid MAGIC! " + header + ", " + Arrays.toString(dst) + " expected " + Arrays.toString(MAGIC_BYTES));
|
||||
} else {
|
||||
header.limit(HEADER_LENGTH);
|
||||
this.version = header.getInt(VERSION_OFFSET);
|
||||
if (this.version >= 0 && this.version <= 1) {
|
||||
this.blobCount = header.getInt(BLOB_COUNT_OFFSET);
|
||||
this.segmentSize = header.getInt(SEGMENT_SIZE_OFFSET);
|
||||
} else {
|
||||
throw new IOException("Invalid version! " + this.version);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void memoryMapBlobIndexes() throws IOException {
|
||||
this.indexLocks = new StampedLock[this.blobCount];
|
||||
|
||||
for (int i = 0; i < this.blobCount; i++) {
|
||||
this.indexLocks[i] = new StampedLock();
|
||||
}
|
||||
|
||||
this.mappedBlobIndexes = this.fileChannel.map(MapMode.READ_WRITE, HEADER_LENGTH, this.blobCount * 4L);
|
||||
}
|
||||
|
||||
private static final int MAX_COMPRESSED_LENGTH_LIMIT = 256 * 1024 * 1024; // 256MB
|
||||
private static final int MAX_SEGMENT_INDEX = 10_000_000; // ~40GB at 4KB segments
|
||||
|
||||
protected void readUsedSegments() throws IOException {
|
||||
long stamp = this.usedSegmentsLock.writeLock();
|
||||
|
||||
try {
|
||||
for (int blobIndex = 0; blobIndex < this.blobCount; blobIndex++) {
|
||||
int indexPos = blobIndex * 4;
|
||||
long segmentStamp = this.indexLocks[blobIndex].readLock();
|
||||
|
||||
int firstSegmentIndex;
|
||||
int compressedLength;
|
||||
try {
|
||||
firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (firstSegmentIndex == 0) {
|
||||
compressedLength = 0;
|
||||
} else if (firstSegmentIndex < 0 || firstSegmentIndex > MAX_SEGMENT_INDEX) {
|
||||
// Corrupted segment index - file is corrupt
|
||||
throw new CorruptedStorageException(
|
||||
"Invalid segment index " + firstSegmentIndex + " for blob " + blobIndex);
|
||||
} else {
|
||||
ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
|
||||
compressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET);
|
||||
if (compressedLength < 0 || compressedLength > MAX_COMPRESSED_LENGTH_LIMIT) {
|
||||
throw new CorruptedStorageException(
|
||||
"Invalid compressed length " + compressedLength + " for blob " + blobIndex);
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.indexLocks[blobIndex].unlockRead(segmentStamp);
|
||||
}
|
||||
|
||||
if (compressedLength > 0 && firstSegmentIndex > 0) {
|
||||
int segmentsCount = this.requiredSegments(BLOB_HEADER_LENGTH + compressedLength);
|
||||
int toIndex = firstSegmentIndex + segmentsCount;
|
||||
if (segmentsCount > 0 && toIndex > firstSegmentIndex) {
|
||||
this.usedSegments.set(firstSegmentIndex, toIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.usedSegmentsLock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
public long size() throws IOException {
|
||||
return this.fileChannel.size();
|
||||
}
|
||||
|
||||
public int segmentSize() {
|
||||
try {
|
||||
return this.requiredSegments(this.fileChannel.size() - this.segmentsBase()) + 1;
|
||||
} catch (IOException var2) {
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public int segmentCount() {
|
||||
long stamp = this.usedSegmentsLock.tryOptimisticRead();
|
||||
int count = this.usedSegments.cardinality();
|
||||
if (this.usedSegmentsLock.validate(stamp)) {
|
||||
return count;
|
||||
} else {
|
||||
stamp = this.usedSegmentsLock.readLock();
|
||||
|
||||
int var4;
|
||||
try {
|
||||
var4 = this.usedSegments.cardinality();
|
||||
} finally {
|
||||
this.usedSegmentsLock.unlockRead(stamp);
|
||||
}
|
||||
|
||||
return var4;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public IntList keys() {
|
||||
IntArrayList list = new IntArrayList(this.blobCount);
|
||||
|
||||
for (int blobIndex = 0; blobIndex < this.blobCount; blobIndex++) {
|
||||
int indexPos = blobIndex * 4;
|
||||
StampedLock lock = this.indexLocks[blobIndex];
|
||||
long stamp = lock.tryOptimisticRead();
|
||||
int segmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (lock.validate(stamp)) {
|
||||
if (segmentIndex != 0) {
|
||||
list.add(blobIndex);
|
||||
}
|
||||
} else {
|
||||
stamp = lock.readLock();
|
||||
|
||||
try {
|
||||
if (this.mappedBlobIndexes.getInt(indexPos) != 0) {
|
||||
list.add(blobIndex);
|
||||
}
|
||||
} finally {
|
||||
lock.unlockRead(stamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
public int readBlobLength(int blobIndex) throws IOException {
|
||||
if (blobIndex >= 0 && blobIndex < this.blobCount) {
|
||||
int indexPos = blobIndex * 4;
|
||||
long stamp = this.indexLocks[blobIndex].readLock();
|
||||
|
||||
byte blobHeaderBuffer;
|
||||
try {
|
||||
int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (firstSegmentIndex != 0) {
|
||||
ByteBuffer blobHeaderBufferx = this.readBlobHeader(firstSegmentIndex);
|
||||
return blobHeaderBufferx.getInt(SRC_LENGTH_OFFSET);
|
||||
}
|
||||
|
||||
blobHeaderBuffer = 0;
|
||||
} finally {
|
||||
this.indexLocks[blobIndex].unlockRead(stamp);
|
||||
}
|
||||
|
||||
return blobHeaderBuffer;
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
|
||||
}
|
||||
}
|
||||
|
||||
public int readBlobCompressedLength(int blobIndex) throws IOException {
|
||||
if (blobIndex >= 0 && blobIndex < this.blobCount) {
|
||||
int indexPos = blobIndex * 4;
|
||||
long stamp = this.indexLocks[blobIndex].readLock();
|
||||
|
||||
byte blobHeaderBuffer;
|
||||
try {
|
||||
int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (firstSegmentIndex != 0) {
|
||||
ByteBuffer blobHeaderBufferx = this.readBlobHeader(firstSegmentIndex);
|
||||
return blobHeaderBufferx.getInt(COMPRESSED_LENGTH_OFFSET);
|
||||
}
|
||||
|
||||
blobHeaderBuffer = 0;
|
||||
} finally {
|
||||
this.indexLocks[blobIndex].unlockRead(stamp);
|
||||
}
|
||||
|
||||
return blobHeaderBuffer;
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ByteBuffer readBlob(int blobIndex) throws IOException {
|
||||
if (blobIndex >= 0 && blobIndex < this.blobCount) {
|
||||
int indexPos = blobIndex * 4;
|
||||
long stamp = this.indexLocks[blobIndex].readLock();
|
||||
|
||||
ByteBuffer src;
|
||||
int srcLength;
|
||||
label43:
|
||||
{
|
||||
ByteBuffer blobHeaderBuffer;
|
||||
try {
|
||||
int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (firstSegmentIndex > 0) { // Changed from != 0 to > 0 to reject negative indices
|
||||
blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
|
||||
srcLength = blobHeaderBuffer.getInt(SRC_LENGTH_OFFSET);
|
||||
int compressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET);
|
||||
// Handle empty chunks (newly generated with no data)
|
||||
if (compressedLength == 0 && srcLength == 0) {
|
||||
return allocateDirect(0);
|
||||
}
|
||||
src = this.readSegments(firstSegmentIndex, compressedLength);
|
||||
break label43;
|
||||
}
|
||||
|
||||
blobHeaderBuffer = null;
|
||||
} finally {
|
||||
this.indexLocks[blobIndex].unlockRead(stamp);
|
||||
}
|
||||
|
||||
return blobHeaderBuffer;
|
||||
}
|
||||
|
||||
src.position(0);
|
||||
return Zstd.decompress(src, srcLength);
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void readBlob(int blobIndex, @Nonnull ByteBuffer dest) throws IOException {
|
||||
if (blobIndex >= 0 && blobIndex < this.blobCount) {
|
||||
int indexPos = blobIndex * 4;
|
||||
long stamp = this.indexLocks[blobIndex].readLock();
|
||||
|
||||
ByteBuffer src;
|
||||
int srcLength;
|
||||
try {
|
||||
int firstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (firstSegmentIndex <= 0) { // Changed from == 0 to <= 0 to reject negative indices
|
||||
return;
|
||||
}
|
||||
|
||||
ByteBuffer blobHeaderBuffer = this.readBlobHeader(firstSegmentIndex);
|
||||
srcLength = blobHeaderBuffer.getInt(SRC_LENGTH_OFFSET);
|
||||
int compressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET);
|
||||
// Handle empty chunks (newly generated with no data)
|
||||
if (compressedLength == 0 && srcLength == 0) {
|
||||
return;
|
||||
}
|
||||
if (srcLength > dest.remaining()) {
|
||||
throw new IllegalArgumentException("dest buffer is not large enough! required dest.remaining() >= " + srcLength);
|
||||
}
|
||||
|
||||
src = this.readSegments(firstSegmentIndex, compressedLength);
|
||||
} finally {
|
||||
this.indexLocks[blobIndex].unlockRead(stamp);
|
||||
}
|
||||
|
||||
src.position(0);
|
||||
if (dest.isDirect()) {
|
||||
Zstd.decompress(dest, src);
|
||||
} else {
|
||||
ByteBuffer tempDest = allocateDirect(srcLength);
|
||||
|
||||
try {
|
||||
Zstd.decompress(tempDest, src);
|
||||
tempDest.position(0);
|
||||
dest.put(tempDest);
|
||||
} finally {
|
||||
if (UnsafeUtil.UNSAFE != null) {
|
||||
UnsafeUtil.UNSAFE.invokeCleaner(tempDest);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected ByteBuffer readBlobHeader(int firstSegmentIndex) throws IOException {
|
||||
if (firstSegmentIndex == 0) {
|
||||
throw new IllegalArgumentException("Invalid segment index!");
|
||||
} else {
|
||||
ByteBuffer blobHeaderBuffer = getTempBuffer(BLOB_HEADER_LENGTH);
|
||||
if (this.fileChannel.read(blobHeaderBuffer, this.segmentPosition(firstSegmentIndex)) != BLOB_HEADER_LENGTH) {
|
||||
throw new IllegalStateException();
|
||||
} else {
|
||||
return blobHeaderBuffer;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected ByteBuffer readSegments(int firstSegmentIndex, int compressedLength) throws IOException {
|
||||
if (compressedLength <= 0 || compressedLength > MAX_COMPRESSED_LENGTH_LIMIT) {
|
||||
throw new IOException("Invalid compressed length: " + compressedLength);
|
||||
}
|
||||
if (firstSegmentIndex <= 0) {
|
||||
throw new IOException("Invalid segment index: " + firstSegmentIndex);
|
||||
}
|
||||
ByteBuffer buffer = allocateDirect(compressedLength);
|
||||
long segmentPosition = this.segmentPosition(firstSegmentIndex);
|
||||
if (this.fileChannel.read(buffer, segmentPosition + BLOB_HEADER_LENGTH) != compressedLength) {
|
||||
throw new IllegalStateException();
|
||||
} else if (buffer.remaining() != 0) {
|
||||
throw new IOException("Failed to read segments: " + firstSegmentIndex + ", " + compressedLength + ", " + buffer);
|
||||
} else {
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
public void writeBlob(int blobIndex, @Nonnull ByteBuffer src) throws IOException {
|
||||
if (blobIndex >= 0 && blobIndex < this.blobCount) {
|
||||
int srcLength = src.remaining();
|
||||
int maxCompressedLength = (int) Zstd.compressBound(srcLength);
|
||||
ByteBuffer dest = allocateDirect(BLOB_HEADER_LENGTH + maxCompressedLength);
|
||||
dest.putInt(SRC_LENGTH_OFFSET, srcLength);
|
||||
dest.position(BLOB_HEADER_LENGTH);
|
||||
int compressedLength;
|
||||
if (src.isDirect()) {
|
||||
compressedLength = Zstd.compress(dest, src, this.compressionLevel);
|
||||
} else {
|
||||
ByteBuffer tempSrc = allocateDirect(srcLength);
|
||||
|
||||
try {
|
||||
tempSrc.put(src);
|
||||
tempSrc.position(0);
|
||||
compressedLength = Zstd.compress(dest, tempSrc, this.compressionLevel);
|
||||
} finally {
|
||||
if (UnsafeUtil.UNSAFE != null) {
|
||||
UnsafeUtil.UNSAFE.invokeCleaner(tempSrc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dest.putInt(COMPRESSED_LENGTH_OFFSET, compressedLength);
|
||||
dest.limit(dest.position());
|
||||
dest.position(0);
|
||||
int indexPos = blobIndex * 4;
|
||||
long stamp = this.indexLocks[blobIndex].writeLock();
|
||||
|
||||
try {
|
||||
int oldSegmentLength = 0;
|
||||
int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (oldFirstSegmentIndex != 0) {
|
||||
try {
|
||||
ByteBuffer blobHeaderBuffer = this.readBlobHeader(oldFirstSegmentIndex);
|
||||
int oldCompressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET);
|
||||
// Validate to prevent corruption from causing invalid segment ranges
|
||||
if (oldCompressedLength >= 0) {
|
||||
oldSegmentLength = this.requiredSegments(BLOB_HEADER_LENGTH + oldCompressedLength);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
// Old blob header is corrupted/unreadable - skip clearing old segments
|
||||
// This leaks segments but prevents corruption from propagating
|
||||
oldSegmentLength = 0;
|
||||
}
|
||||
}
|
||||
|
||||
int firstSegmentIndex = this.writeSegments(dest);
|
||||
if (this.flushOnWrite) {
|
||||
this.fileChannel.force(false);
|
||||
}
|
||||
|
||||
this.mappedBlobIndexes.putInt(indexPos, firstSegmentIndex);
|
||||
if (this.flushOnWrite) {
|
||||
this.mappedBlobIndexes.force(indexPos, 4);
|
||||
}
|
||||
|
||||
if (oldSegmentLength > 0 && oldFirstSegmentIndex > 0) {
|
||||
int toIndex = oldFirstSegmentIndex + oldSegmentLength;
|
||||
// Validate range to prevent IndexOutOfBoundsException
|
||||
if (toIndex > oldFirstSegmentIndex) {
|
||||
long usedSegmentsStamp = this.usedSegmentsLock.writeLock();
|
||||
|
||||
try {
|
||||
this.usedSegments.clear(oldFirstSegmentIndex, toIndex);
|
||||
} finally {
|
||||
this.usedSegmentsLock.unlockWrite(usedSegmentsStamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.indexLocks[blobIndex].unlockWrite(stamp);
|
||||
}
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeBlob(int blobIndex) throws IOException {
|
||||
if (blobIndex >= 0 && blobIndex < this.blobCount) {
|
||||
int indexPos = blobIndex * 4;
|
||||
long stamp = this.indexLocks[blobIndex].writeLock();
|
||||
|
||||
try {
|
||||
int oldFirstSegmentIndex = this.mappedBlobIndexes.getInt(indexPos);
|
||||
if (oldFirstSegmentIndex != 0) {
|
||||
// Clear the index first - this ensures the blob is marked as removed
|
||||
// even if we can't clear its segments due to corruption
|
||||
this.mappedBlobIndexes.putInt(indexPos, 0);
|
||||
if (this.flushOnWrite) {
|
||||
this.mappedBlobIndexes.force(indexPos, 4);
|
||||
}
|
||||
|
||||
// Try to clear the segments, but handle corrupted blob headers gracefully
|
||||
try {
|
||||
ByteBuffer blobHeaderBuffer = this.readBlobHeader(oldFirstSegmentIndex);
|
||||
int oldCompressedLength = blobHeaderBuffer.getInt(COMPRESSED_LENGTH_OFFSET);
|
||||
|
||||
// Validate values to prevent IndexOutOfBoundsException from corrupted data
|
||||
if (oldCompressedLength >= 0 && oldFirstSegmentIndex > 0) {
|
||||
int oldSegmentLength = this.requiredSegments(BLOB_HEADER_LENGTH + oldCompressedLength);
|
||||
int toIndex = oldFirstSegmentIndex + oldSegmentLength;
|
||||
|
||||
// Only clear if the range is valid
|
||||
if (oldSegmentLength > 0 && toIndex > oldFirstSegmentIndex) {
|
||||
long usedSegmentsStamp = this.usedSegmentsLock.writeLock();
|
||||
try {
|
||||
this.usedSegments.clear(oldFirstSegmentIndex, toIndex);
|
||||
} finally {
|
||||
this.usedSegmentsLock.unlockWrite(usedSegmentsStamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If values are invalid (corrupted blob header), we skip clearing segments
|
||||
// The segments will be leaked but the blob index is cleared, preventing further errors
|
||||
} catch (Exception e) {
|
||||
// Blob header is unreadable/corrupted - segments will be leaked but
|
||||
// the blob index has already been cleared, so the corrupted data won't cause issues
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.indexLocks[blobIndex].unlockWrite(stamp);
|
||||
}
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException("Index out of range: " + blobIndex + " blobCount: " + this.blobCount);
|
||||
}
|
||||
}
|
||||
|
||||
protected int writeSegments(@Nonnull ByteBuffer data) throws IOException {
|
||||
int dataRemaining = data.remaining();
|
||||
int segmentsCount = this.requiredSegments(dataRemaining);
|
||||
IndexedStorageFile.SegmentRangeWriteLock segmentLock = this.findFreeSegment(segmentsCount);
|
||||
|
||||
int var8;
|
||||
try {
|
||||
int firstSegmentIndex = segmentLock.segmentIndex;
|
||||
if (this.fileChannel.write(data, this.segmentPosition(firstSegmentIndex)) != dataRemaining) {
|
||||
throw new IllegalStateException();
|
||||
}
|
||||
|
||||
long stamp = this.usedSegmentsLock.writeLock();
|
||||
|
||||
try {
|
||||
this.usedSegments.set(firstSegmentIndex, firstSegmentIndex + segmentsCount);
|
||||
} finally {
|
||||
this.usedSegmentsLock.unlockWrite(stamp);
|
||||
}
|
||||
|
||||
var8 = firstSegmentIndex;
|
||||
} finally {
|
||||
segmentLock.unlock();
|
||||
}
|
||||
|
||||
return var8;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private IndexedStorageFile.SegmentRangeWriteLock findFreeSegment(int count) {
|
||||
long[] stamps = new long[count];
|
||||
int index = 1;
|
||||
|
||||
label98:
|
||||
while (true) {
|
||||
long indexesStamp = this.usedSegmentsLock.readLock();
|
||||
|
||||
try {
|
||||
int start = 0;
|
||||
int found = 0;
|
||||
|
||||
while (found < count) {
|
||||
int nextUsedIndex = this.usedSegments.nextSetBit(index);
|
||||
if (nextUsedIndex < 0) {
|
||||
start = index;
|
||||
break;
|
||||
}
|
||||
|
||||
if (index == nextUsedIndex) {
|
||||
start = this.usedSegments.nextClearBit(index);
|
||||
nextUsedIndex = this.usedSegments.nextSetBit(start + 1);
|
||||
if (nextUsedIndex < 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
found = nextUsedIndex - start;
|
||||
index = nextUsedIndex + 1;
|
||||
} else {
|
||||
start = index;
|
||||
found = nextUsedIndex - index;
|
||||
index = nextUsedIndex + 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = count - 1; i >= 0; i--) {
|
||||
stamps[i] = this.getSegmentLock(start + i).tryWriteLock();
|
||||
if (stamps[i] == 0L) {
|
||||
for (int j = count - 1; j > i; j--) {
|
||||
this.getSegmentLock(start + j).unlockWrite(stamps[j]);
|
||||
}
|
||||
|
||||
index = start + i + 1;
|
||||
continue label98;
|
||||
}
|
||||
}
|
||||
|
||||
return new IndexedStorageFile.SegmentRangeWriteLock(start, count, stamps);
|
||||
} finally {
|
||||
this.usedSegmentsLock.unlockRead(indexesStamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected StampedLock getSegmentLock(int segmentIndex) {
|
||||
// First try optimistic read - no locking overhead in common case
|
||||
StampedLock[] locks = this.segmentLocks;
|
||||
if (segmentIndex < locks.length) {
|
||||
return locks[segmentIndex];
|
||||
}
|
||||
|
||||
// Need to expand array - acquire write lock
|
||||
long stamp = this.segmentLocksLock.writeLock();
|
||||
try {
|
||||
// Re-check under lock - another thread may have expanded
|
||||
if (segmentIndex < this.segmentLocks.length) {
|
||||
return this.segmentLocks[segmentIndex];
|
||||
}
|
||||
|
||||
int newLength = segmentIndex + 1;
|
||||
StampedLock[] newArray = Arrays.copyOf(this.segmentLocks, newLength);
|
||||
|
||||
for (int i = this.segmentLocks.length; i < newLength; i++) {
|
||||
newArray[i] = new StampedLock();
|
||||
}
|
||||
|
||||
this.segmentLocks = newArray;
|
||||
return newArray[segmentIndex];
|
||||
} finally {
|
||||
this.segmentLocksLock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
protected long segmentsBase() {
|
||||
return HEADER_LENGTH + this.blobCount * 4L;
|
||||
}
|
||||
|
||||
protected long segmentOffset(int segmentIndex) {
|
||||
if (segmentIndex == 0) {
|
||||
throw new IllegalArgumentException("Invalid segment index!");
|
||||
} else {
|
||||
return (long) (segmentIndex - 1) * this.segmentSize;
|
||||
}
|
||||
}
|
||||
|
||||
protected long segmentPosition(int segmentIndex) {
|
||||
return this.segmentOffset(segmentIndex) + this.segmentsBase();
|
||||
}
|
||||
|
||||
protected int positionToSegment(long position) {
|
||||
long segmentOffset = position - this.segmentsBase();
|
||||
if (segmentOffset < 0L) {
|
||||
throw new IllegalArgumentException("position is before the segments start");
|
||||
} else {
|
||||
return (int) (segmentOffset / this.segmentSize) + 1;
|
||||
}
|
||||
}
|
||||
|
||||
protected int requiredSegments(long dataLength) {
|
||||
return (int) ((dataLength + this.segmentSize - 1L) / this.segmentSize);
|
||||
}
|
||||
|
||||
public FileLock lock() throws IOException {
|
||||
return this.fileChannel.lock();
|
||||
}
|
||||
|
||||
public void force(boolean metaData) throws IOException {
|
||||
this.fileChannel.force(metaData);
|
||||
this.mappedBlobIndexes.force();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
this.fileChannel.close();
|
||||
if (UnsafeUtil.UNSAFE != null) {
|
||||
UnsafeUtil.UNSAFE.invokeCleaner(this.mappedBlobIndexes);
|
||||
}
|
||||
|
||||
this.mappedBlobIndexes = null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "IndexedStorageFile{fileChannel="
|
||||
+ this.fileChannel
|
||||
+ ", compressionLevel="
|
||||
+ this.compressionLevel
|
||||
+ ", blobCount="
|
||||
+ this.blobCount
|
||||
+ ", segmentSize="
|
||||
+ this.segmentSize
|
||||
+ ", mappedBlobIndexes="
|
||||
+ this.mappedBlobIndexes
|
||||
+ ", usedSegments="
|
||||
+ this.usedSegments
|
||||
+ "}";
|
||||
}
|
||||
|
||||
static {
|
||||
MAGIC_BUFFER.position(0);
|
||||
}
|
||||
|
||||
static class OffsetHelper {
|
||||
private int index;
|
||||
|
||||
OffsetHelper() {
|
||||
}
|
||||
|
||||
public int next(int len) {
|
||||
int cur = this.index;
|
||||
this.index += len;
|
||||
return cur;
|
||||
}
|
||||
|
||||
public int length() {
|
||||
return this.index;
|
||||
}
|
||||
}
|
||||
|
||||
protected class SegmentRangeWriteLock {
|
||||
private final int segmentIndex;
|
||||
private final int count;
|
||||
private final long[] stamps;
|
||||
|
||||
public SegmentRangeWriteLock(int segmentIndex, int count, long[] stamps) {
|
||||
if (segmentIndex == 0) {
|
||||
throw new IllegalArgumentException("Invalid segment index!");
|
||||
} else if (count == 0) {
|
||||
throw new IllegalArgumentException("Invalid count!");
|
||||
} else {
|
||||
this.segmentIndex = segmentIndex;
|
||||
this.count = count;
|
||||
this.stamps = stamps;
|
||||
}
|
||||
}
|
||||
|
||||
protected void unlock() {
|
||||
for (int i = 0; i < this.count; i++) {
|
||||
IndexedStorageFile.this.getSegmentLock(this.segmentIndex + i).unlockWrite(this.stamps[i]);
|
||||
this.stamps[i] = 0L;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user