commit 0ad4b55303d9049bea2dbffb5a3caf09d728b97e Author: luk Date: Sun Jan 25 21:02:19 2026 +0000 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..34f33557 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.gradle/ +build/ +repo/ +patcher +Assets +.hytale-downloader-credentials.json diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..80bf620e --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,150 @@ +import java.util.zip.CRC32 +import java.util.zip.ZipEntry +import java.util.zip.ZipFile +import java.util.zip.ZipOutputStream + +// Text file extensions for LF normalization +val textExtensions = setOf(".kt", ".java", ".properties", ".json", ".toml", ".xml", ".txt", ".MF") +fun isTextFile(name: String) = textExtensions.any { name.endsWith(it) } || name.startsWith("META-INF/services/") + +fun normalizeContent(name: String, content: ByteArray): ByteArray { + if (!isTextFile(name)) return content + return content.toString(Charsets.UTF_8) + .replace("\r\n", "\n").replace("\r", "\n") + .toByteArray(Charsets.UTF_8) +} + +fun writeStoredEntry(output: ZipOutputStream, name: String, content: ByteArray) { + val normalized = normalizeContent(name, content) + val entry = ZipEntry(name).apply { + time = 0 + size = normalized.size.toLong() + compressedSize = normalized.size.toLong() + crc = CRC32().apply { update(normalized) }.value + method = ZipEntry.STORED + } + output.putNextEntry(entry) + output.write(normalized) + output.closeEntry() +} + +plugins { + id("java") +} + +java { + toolchain { + languageVersion.set(JavaLanguageVersion.of(21)) + } +} + +repositories { + mavenCentral() + maven { + name = "hytale-release" + url = uri("https://maven.hytale.com/release") + } + maven { + name = "hytale-pre-release" + url = uri("https://maven.hytale.com/pre-release") + } +} + +val hytaleServer: Configuration by configurations.creating + +val patchedJar = rootDir.resolve("repo/applications/HytaleServerPatched.jar") + +dependencies { + hytaleServer("com.hypixel.hytale:Server:2026.01.22-6f8bdbdc4") +} + +configurations { + compileOnly.get().extendsFrom(hytaleServer) +} + +tasks.withType { + options.compilerArgs.addAll(listOf( + "--add-exports", "java.base/jdk.internal.vm.annotation=ALL-UNNAMED" + )) +} + +sourceSets { + main { + java { + // Patch files go in runtime/hytale-server/src/ + // Use the same package structure as the original JAR + // e.g., src/com/hypixel/hytale/server/SomeClass.java + srcDir("src") + } + } +} + +// Task to create the patched JAR by overlaying compiled classes on top of original +tasks.register("patchJar") { + group = "coldfusion" + description = "Creates a patched HytaleServer JAR with compiled modifications" + + dependsOn(tasks.compileJava) + + inputs.files(hytaleServer) + inputs.files(tasks.compileJava.get().outputs.files).optional() + outputs.file(patchedJar) + + doLast { + val compiledClassesDir = tasks.compileJava.get().destinationDirectory.get().asFile + val originalJar = hytaleServer.singleFile + + // Collect all compiled class files with their relative paths + val patchedFiles = mutableMapOf() + if (compiledClassesDir.exists()) { + compiledClassesDir.walkTopDown() + .filter { it.isFile } + .forEach { file -> + val relativePath = file.relativeTo(compiledClassesDir).path.replace(File.separatorChar, '/') + patchedFiles[relativePath] = file.readBytes() + } + } + + logger.lifecycle("Patching JAR with ${patchedFiles.size} compiled files") + + // Collect all entries: patched files override original + val allEntries = mutableMapOf() // null = directory + + ZipFile(originalJar).use { original -> + original.entries().asSequence().forEach { entry -> + allEntries[entry.name] = if (entry.isDirectory) null else original.getInputStream(entry).readBytes() + } + } + + // Override with patched files + patchedFiles.forEach { (path, content) -> + logger.lifecycle(" Patching: $path") + allEntries[path] = content + } + + // Write sorted entries with STORED compression and LF normalization + patchedJar.outputStream().buffered().let { ZipOutputStream(it) }.use { output -> + output.setMethod(ZipOutputStream.STORED) + + allEntries.keys.sorted().forEach { name -> + val content = allEntries[name] + if (content == null) { + // Directory + val entry = ZipEntry(name).apply { + time = 0 + size = 0 + compressedSize = 0 + crc = 0 + method = ZipEntry.STORED + } + output.putNextEntry(entry) + output.closeEntry() + } else { + writeStoredEntry(output, name, content) + } + } + } + + logger.lifecycle("Created patched JAR: $patchedJar") + } +} \ No newline at end of file diff --git a/scripts/hytale-downloader-linux-amd64 b/scripts/hytale-downloader-linux-amd64 new file mode 100755 index 00000000..e303ef70 Binary files /dev/null and b/scripts/hytale-downloader-linux-amd64 differ diff --git a/scripts/pull_generated_into_src.sh b/scripts/pull_generated_into_src.sh new file mode 100755 index 00000000..c1e4fa44 --- /dev/null +++ b/scripts/pull_generated_into_src.sh @@ -0,0 +1,42 @@ +#!/bin/bash +# Pull files from repo/hytale-server/src into vendor/hytale-server/src +# Only overrides files that exist in both locations + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HYTALE_SERVER_ROOT="$(dirname "$SCRIPT_DIR")" +PROJECT_ROOT="$(dirname "$(dirname "$HYTALE_SERVER_ROOT")")" + +SRC_DIR="$HYTALE_SERVER_ROOT/src" +REPO_SRC="$PROJECT_ROOT/repo/hytale-server/src" + +if [ ! -d "$SRC_DIR" ]; then + echo "Error: src does not exist" + exit 1 +fi + +if [ ! -d "$REPO_SRC" ]; then + echo "Error: repo/hytale-server/src does not exist" + exit 1 +fi + +count=0 + +# Find all files in src +while IFS= read -r -d '' file; do + # Get relative path from SRC_DIR + rel_path="${file#$SRC_DIR/}" + + # Check if corresponding file exists in repo + repo_file="$REPO_SRC/$rel_path" + + if [ -f "$repo_file" ]; then + cp "$repo_file" "$file" + echo "Updated: $rel_path" + count=$((count + 1)) + fi +done < <(find "$SRC_DIR" -type f -print0) + +echo "" +echo "Done. Updated $count file(s)." diff --git a/scripts/update-server.sh b/scripts/update-server.sh new file mode 100755 index 00000000..b7c5ee22 --- /dev/null +++ b/scripts/update-server.sh @@ -0,0 +1,38 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +HYTALE_SERVER_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECT_ROOT="$(cd "$HYTALE_SERVER_ROOT/../.." && pwd)" +DOWNLOADER="$SCRIPT_DIR/hytale-downloader-linux-amd64" +OUTPUT_DIR="$PROJECT_ROOT/repo/hytale-server" + +if [ ! -f "$DOWNLOADER" ]; then + echo "Error: Downloader not found at $DOWNLOADER" + exit 1 +fi + +echo "[update-server] Creating output directory..." +mkdir -p "$OUTPUT_DIR" + +echo "[update-server] Downloading Hytale server files..." +cd "$SCRIPT_DIR" +"$DOWNLOADER" -download-path "$OUTPUT_DIR/game.zip" + +echo "[update-server] Extracting server files..." +unzip -o -q "$OUTPUT_DIR/game.zip" -d "$OUTPUT_DIR" +rm -f "$OUTPUT_DIR/game.zip" + +echo "[update-server] Verifying server files..." +if [ ! -f "$OUTPUT_DIR/Server/HytaleServer.jar" ]; then + echo "Error: HytaleServer.jar not found" + exit 1 +fi + +if [ ! -f "$OUTPUT_DIR/Assets.zip" ]; then + echo "Error: Assets.zip not found" + exit 1 +fi + +echo "[update-server] Server update completed successfully" +echo "[update-server] Files located at: $OUTPUT_DIR" diff --git a/src/coldfusion/hytaleserver/InventoryOwnershipGuard.java b/src/coldfusion/hytaleserver/InventoryOwnershipGuard.java new file mode 100644 index 00000000..643c5e31 --- /dev/null +++ b/src/coldfusion/hytaleserver/InventoryOwnershipGuard.java @@ -0,0 +1,295 @@ +package coldfusion.hytaleserver; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Guards against inventory sharing between players (Issue #45). + *

+ * This class is called by the patched LivingEntity.setInventory() method + * to validate that an Inventory isn't being shared between multiple players. + *

+ * If a shared reference is detected, the inventory is deep-cloned to break + * the shared reference before assignment. + */ +public class InventoryOwnershipGuard { + + private static final Logger LOGGER = Logger.getLogger("HyFixes"); + + // Track inventory -> owner UUID + private static final Map inventoryOwners = new ConcurrentHashMap<>(); + private static final Map containerOwners = new ConcurrentHashMap<>(); + + // Statistics + private static volatile int sharedReferencesDetected = 0; + private static volatile int inventoriesCloned = 0; + private static volatile int validationCalls = 0; + + // Cached reflection objects + private static Method getPlayerRefMethod; + private static Method getUuidMethod; + private static Class playerClass; + private static boolean reflectionInitialized = false; + private static boolean reflectionFailed = false; + + /** + * Called by patched LivingEntity.setInventory() BEFORE inventory assignment. + */ + public static Object validateAndClone(Object livingEntity, Object inventory) { + if (inventory == null || livingEntity == null) { + return inventory; + } + + validationCalls++; + + try { + if (!reflectionInitialized && !reflectionFailed) { + initializeReflection(); + } + + if (reflectionFailed) { + return inventory; + } + + // Only validate for Player entities + if (!playerClass.isInstance(livingEntity)) { + return inventory; + } + + UUID newOwnerUuid = getEntityUuid(livingEntity); + if (newOwnerUuid == null) { + return inventory; + } + + int inventoryId = System.identityHashCode(inventory); + UUID currentOwner = inventoryOwners.get(inventoryId); + + if (currentOwner != null && !currentOwner.equals(newOwnerUuid)) { + // SHARED REFERENCE DETECTED! + sharedReferencesDetected++; + + LOGGER.log(Level.WARNING, + "[HyFix #45] INVENTORY SHARING DETECTED!\n" + + " Inventory @{0} owned by {1} being assigned to {2}\n" + + " Forcing deep clone to prevent inventory sync.", + new Object[]{ + Integer.toHexString(inventoryId), + currentOwner.toString().substring(0, 8) + "...", + newOwnerUuid.toString().substring(0, 8) + "..." + } + ); + + Object clonedInventory = cloneInventory(inventory); + if (clonedInventory != null) { + inventoriesCloned++; + int clonedId = System.identityHashCode(clonedInventory); + inventoryOwners.put(clonedId, newOwnerUuid); + registerContainers(clonedInventory, newOwnerUuid); + return clonedInventory; + } else { + LOGGER.log(Level.SEVERE, + "[HyFix #45] CRITICAL: Failed to clone inventory!"); + } + } else { + inventoryOwners.put(inventoryId, newOwnerUuid); + registerContainers(inventory, newOwnerUuid); + } + + } catch (Exception e) { + if (validationCalls < 5) { + LOGGER.log(Level.WARNING, + "[HyFix] Error in inventory validation: " + e.getMessage()); + } + } + + return inventory; + } + + private static synchronized void initializeReflection() { + if (reflectionInitialized || reflectionFailed) { + return; + } + + try { + playerClass = Class.forName("com.hypixel.hytale.server.core.entity.entities.Player"); + getPlayerRefMethod = playerClass.getMethod("getPlayerRef"); + + Class playerRefClass = Class.forName("com.hypixel.hytale.server.core.universe.PlayerRef"); + getUuidMethod = playerRefClass.getMethod("getUuid"); + + reflectionInitialized = true; + LOGGER.log(Level.INFO, "[HyFix] InventoryOwnershipGuard initialized"); + + } catch (Exception e) { + reflectionFailed = true; + LOGGER.log(Level.WARNING, + "[HyFix] Failed to initialize InventoryOwnershipGuard: " + e.getMessage()); + } + } + + private static UUID getEntityUuid(Object livingEntity) { + try { + Object playerRef = getPlayerRefMethod.invoke(livingEntity); + if (playerRef == null) { + return null; + } + return (UUID) getUuidMethod.invoke(playerRef); + } catch (Exception e) { + return null; + } + } + + private static void registerContainers(Object inventory, UUID ownerUuid) { + try { + String[] containerGetters = {"getStorage", "getArmor", "getHotbar", "getUtility", "getTools", "getBackpack"}; + + for (String getter : containerGetters) { + try { + Method method = inventory.getClass().getMethod(getter); + Object container = method.invoke(inventory); + if (container != null) { + containerOwners.put(System.identityHashCode(container), ownerUuid); + } + } catch (NoSuchMethodException ignored) { + } + } + } catch (Exception e) { + } + } + + private static Object cloneInventory(Object inventory) { + Object cloned = cloneViaCodec(inventory); + if (cloned != null) { + return cloned; + } + + cloned = cloneViaContainers(inventory); + if (cloned != null) { + return cloned; + } + + LOGGER.log(Level.SEVERE, "[HyFix] All inventory clone methods failed!"); + return null; + } + + private static Object cloneViaCodec(Object inventory) { + try { + Class inventoryClass = inventory.getClass(); + Field codecField = inventoryClass.getField("CODEC"); + Object codec = codecField.get(null); + + Class extraInfoClass = Class.forName("com.hypixel.hytale.codec.ExtraInfo"); + Field threadLocalField = extraInfoClass.getField("THREAD_LOCAL"); + ThreadLocal threadLocal = (ThreadLocal) threadLocalField.get(null); + Object extraInfo = threadLocal.get(); + + if (extraInfo == null) { + return null; + } + + Method encodeMethod = findMethod(codec.getClass(), "encode", Object.class, extraInfoClass); + if (encodeMethod == null) { + return null; + } + Object bsonValue = encodeMethod.invoke(codec, inventory, extraInfo); + + Constructor defaultConstructor = inventoryClass.getDeclaredConstructor(); + defaultConstructor.setAccessible(true); + Object newInventory = defaultConstructor.newInstance(); + + Class bsonValueClass = Class.forName("org.bson.BsonValue"); + Method decodeMethod = findMethod(codec.getClass(), "decode", bsonValueClass, Object.class, extraInfoClass); + if (decodeMethod == null) { + return null; + } + decodeMethod.invoke(codec, bsonValue, newInventory, extraInfo); + + return newInventory; + + } catch (Exception e) { + return null; + } + } + + private static Object cloneViaContainers(Object inventory) { + try { + Class inventoryClass = inventory.getClass(); + + Object storage = invokeGetter(inventory, "getStorage"); + Object armor = invokeGetter(inventory, "getArmor"); + Object hotbar = invokeGetter(inventory, "getHotbar"); + Object utility = invokeGetter(inventory, "getUtility"); + Object tools = invokeGetter(inventory, "getTools"); + Object backpack = invokeGetter(inventory, "getBackpack"); + + Object clonedStorage = cloneContainer(storage); + Object clonedArmor = cloneContainer(armor); + Object clonedHotbar = cloneContainer(hotbar); + Object clonedUtility = cloneContainer(utility); + Object clonedTools = cloneContainer(tools); + Object clonedBackpack = cloneContainer(backpack); + + Class containerClass = Class.forName("com.hypixel.hytale.server.core.inventory.container.ItemContainer"); + Constructor constructor = inventoryClass.getConstructor( + containerClass, containerClass, containerClass, + containerClass, containerClass, containerClass + ); + + return constructor.newInstance( + clonedStorage, clonedArmor, clonedHotbar, + clonedUtility, clonedTools, clonedBackpack + ); + + } catch (Exception e) { + return null; + } + } + + private static Object cloneContainer(Object container) { + if (container == null) { + return null; + } + try { + Method cloneMethod = container.getClass().getMethod("clone"); + return cloneMethod.invoke(container); + } catch (Exception e) { + return container; + } + } + + private static Object invokeGetter(Object obj, String methodName) { + try { + Method method = obj.getClass().getMethod(methodName); + return method.invoke(obj); + } catch (Exception e) { + return null; + } + } + + private static Method findMethod(Class clazz, String name, Class... paramTypes) { + try { + return clazz.getMethod(name, paramTypes); + } catch (NoSuchMethodException e) { + for (Method m : clazz.getMethods()) { + if (m.getName().equals(name) && m.getParameterCount() == paramTypes.length) { + return m; + } + } + return null; + } + } + + public static void onPlayerDisconnect(UUID playerUuid) { + if (playerUuid == null) { + return; + } + inventoryOwners.entrySet().removeIf(entry -> playerUuid.equals(entry.getValue())); + containerOwners.entrySet().removeIf(entry -> playerUuid.equals(entry.getValue())); + } +} diff --git a/src/com/hypixel/hytale/builtin/adventure/memories/interactions/SetMemoriesCapacityInteraction.java b/src/com/hypixel/hytale/builtin/adventure/memories/interactions/SetMemoriesCapacityInteraction.java new file mode 100644 index 00000000..7e6b3c80 --- /dev/null +++ b/src/com/hypixel/hytale/builtin/adventure/memories/interactions/SetMemoriesCapacityInteraction.java @@ -0,0 +1,86 @@ +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 CODEC = BuilderCodec.builder( + SetMemoriesCapacityInteraction.class, SetMemoriesCapacityInteraction::new, SimpleInstantInteraction.CODEC + ) + .documentation("Sets how many memories a player can store.") + .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 ref = context.getEntity(); + CommandBuffer 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(); + } +} diff --git a/src/com/hypixel/hytale/builtin/adventure/objectives/task/GatherObjectiveTask.java b/src/com/hypixel/hytale/builtin/adventure/objectives/task/GatherObjectiveTask.java new file mode 100644 index 00000000..6921bc6f --- /dev/null +++ b/src/com/hypixel/hytale/builtin/adventure/objectives/task/GatherObjectiveTask.java @@ -0,0 +1,114 @@ +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 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 store) { + Set 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 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 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 participatingPlayers, @Nonnull ComponentAccessor componentAccessor) { + int count = 0; + BlockTagOrItemIdField blockTypeOrSet = this.getAsset().getBlockTagOrItemIdField(); + + for (UUID playerUUID : participatingPlayers) { + PlayerRef playerRefComponent = Universe.get().getPlayer(playerUUID); + if (playerRefComponent != null) { + Ref 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(); + } +} diff --git a/src/com/hypixel/hytale/builtin/crafting/component/CraftingManager.java b/src/com/hypixel/hytale/builtin/crafting/component/CraftingManager.java new file mode 100644 index 00000000..ab9a04a6 --- /dev/null +++ b/src/com/hypixel/hytale/builtin/crafting/component/CraftingManager.java @@ -0,0 +1,982 @@ +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 { + @Nonnull + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + @Nonnull + private final BlockingQueue 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 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 ref, @Nonnull ComponentAccessor 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 ref, + @Nonnull ComponentAccessor 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 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 ref, + @Nonnull ComponentAccessor 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 ref, @Nonnull ComponentAccessor 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 ref, @Nonnull ComponentAccessor componentAccessor) { + LOGGER.at(Level.FINE).log("Cancel Crafting!"); + ObjectList 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 ref, @Nonnull ComponentAccessor 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 ref, @Nonnull ComponentAccessor 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 ref, @Nonnull ComponentAccessor 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 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 materialsToRemove = getInputMaterials(craftingRecipe); + if (materialsToRemove.isEmpty()) { + return true; + } else { + LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", job, materialsToRemove); + ObjectList itemStackList = new ObjectArrayList<>(); + + boolean succeeded = switch (job.inputRemovalType) { + case NORMAL -> { + ListTransaction 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 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 materialsToRemove = getInputMaterials(craftingRecipe, quantity); + if (materialsToRemove.isEmpty()) { + return true; + } else { + LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", craftingRecipe, materialsToRemove); + ListTransaction materialTransactions = itemContainer.removeMaterials(materialsToRemove, true, true, true); + return materialTransactions.succeeded(); + } + } + + private static void refundInputToInventory( + @Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor, @Nonnull CraftingManager.CraftingJob job, int currentItemId + ) { + Objects.requireNonNull(job, "Job can't be null!"); + List 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 getOutputItemStacks(@Nonnull CraftingRecipe recipe) { + return getOutputItemStacks(recipe, 1); + } + + @Nonnull + public static List getOutputItemStacks(@Nonnull CraftingRecipe recipe, int quantity) { + Objects.requireNonNull(recipe); + MaterialQuantity[] output = recipe.getOutputs(); + if (output == null) { + return List.of(); + } else { + ObjectList 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 getInputMaterials(@Nonnull CraftingRecipe recipe) { + return getInputMaterials(recipe, 1); + } + + @Nonnull + private static List getInputMaterials(@Nonnull MaterialQuantity[] input) { + return getInputMaterials(input, 1); + } + + @Nonnull + public static List getInputMaterials(@Nonnull CraftingRecipe recipe, int quantity) { + Objects.requireNonNull(recipe); + return recipe.getInput() == null ? Collections.emptyList() : getInputMaterials(recipe.getInput(), quantity); + } + + @Nonnull + private static List getInputMaterials(@Nonnull MaterialQuantity[] input, int quantity) { + ObjectList 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 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 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 ref, @Nonnull ComponentAccessor 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 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 ref, @Nonnull ComponentAccessor 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 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 materialTransactions = combined.removeMaterials(input); + if (materialTransactions.succeeded()) { + List 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 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 chests = result.containers; + List chestStates = result.states; + ItemContainer itemContainer = EmptyItemContainer.INSTANCE; + if (!chests.isEmpty()) { + itemContainer = new CombinedItemContainer(chests.stream().map(container -> { + DelegateItemContainer delegate = new DelegateItemContainer<>(container); + delegate.setGlobalFilter(FilterType.ALLOW_OUTPUT_ONLY); + return delegate; + }).toArray(ItemContainer[]::new)); + } + + Map 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 containers = new ObjectArrayList<>(); + List states = new ObjectArrayList<>(); + List spatialResults = new ObjectArrayList<>(); + List filteredOut = new ObjectArrayList<>(); + World world = benchState.getChunk().getWorld(); + Store 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, ChunkStore> blockStateSpatialStructure = store.getResource(BlockStateModule.get().getItemContainerSpatialResourceType()); + ObjectList> 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 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 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 containers, + List states, + List spatialResults, + List 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> 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() { + } + } +} diff --git a/src/com/hypixel/hytale/builtin/crafting/state/ProcessingBenchState.java b/src/com/hypixel/hytale/builtin/crafting/state/ProcessingBenchState.java new file mode 100644 index 00000000..d68c61e6 --- /dev/null +++ b/src/com/hypixel/hytale/builtin/crafting/state/ProcessingBenchState.java @@ -0,0 +1,840 @@ +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 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 processingSlots = new HashSet<>(); + private final Set 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 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 store = world.getEntityStore().getStore(); + Holder[] 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 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 validIngredients = new ObjectArrayList<>(); + + for (CraftingRecipe recipe : CraftingPlugin.getBenchRecipes(this.bench.getType(), this.bench.getId())) { + if (!recipe.isRestrictedByBenchTierLevel(this.bench.getId(), tierLevel)) { + List 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 store = world.getEntityStore().getStore(); + Holder[] 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 archetypeChunk, @Nonnull Store store, CommandBuffer commandBuffer) { + World world = store.getExternalData().getWorld(); + Store entityStore = world.getEntityStore().getStore(); + BlockType blockType = this.getBlockType(); + String currentState = BlockAccessor.getCurrentInteractionState(blockType); + List outputItemStacks = null; + List 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 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 extraItemStacks = new ObjectArrayList<>(extra.getOutputs().length); + + for (MaterialQuantity e : extra.getOutputs()) { + extraItemStacks.add(e.toItemStack()); + } + + ListTransaction addTransaction = this.outputContainer.addItemStacks(extraItemStacks, false, false, false); + List 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[] 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 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 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[] itemEntityHolders = this.ejectItems(entityStore, remainderItems); + entityStore.addEntities(itemEntityHolders, AddReason.SPAWN); + } + + return; + } + } + + List remainderItems = new ObjectArrayList<>(); + ListTransaction 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 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[] 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[] ejectItems(@Nonnull ComponentAccessor accessor, @Nonnull List 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> 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 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 itemStacks = this.combinedItemContainer.dropAllItemStacks(); + this.dropFuelItems(itemStacks); + World world = this.getChunk().getWorld(); + Store entityStore = world.getEntityStore().getStore(); + Vector3d dropPosition = this.getBlockPosition().toVector3d().add(0.5, 0.0, 0.5); + Holder[] 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 recipes = CraftingPlugin.getBenchRecipes(this.bench.getType(), this.bench.getId()); + if (recipes.isEmpty()) { + this.clearRecipe(); + } else { + List 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 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 playerRef, + @Nonnull String blockTypeKey, + @Nonnull BlockState blockState, + @Nonnull ComponentAccessor 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 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(); + } +} diff --git a/src/com/hypixel/hytale/builtin/instances/InstancesPlugin.java b/src/com/hypixel/hytale/builtin/instances/InstancesPlugin.java new file mode 100644 index 00000000..f7e3c551 --- /dev/null +++ b/src/com/hypixel/hytale/builtin/instances/InstancesPlugin.java @@ -0,0 +1,644 @@ +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 instanceDataResourceType; + private ComponentType instanceEntityConfigComponentType; + private ComponentType instanceBlockComponentType; + private ComponentType 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 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 holder = ChunkStore.REGISTRY.newHolder(); + holder.ensureComponent(ConfigurableInstanceBlock.getComponentType()); + return holder; + } + ); + this.getCodecRegistry(WorldConfig.PLUGIN_CODEC).register(InstanceWorldConfig.class, "Instance", InstanceWorldConfig.CODEC); + } + + @Nonnull + public CompletableFuture spawnInstance(@Nonnull String name, @Nonnull World forWorld, @Nonnull Transform returnPoint) { + return this.spawnInstance(name, null, forWorld, returnPoint); + } + + @Nonnull + public CompletableFuture 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 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 entityRef, + @Nonnull ComponentAccessor componentAccessor, + @Nonnull CompletableFuture 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 playerRef, + @Nonnull ComponentAccessor 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 targetRef, @Nonnull ComponentAccessor 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 = 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 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 getInstanceAssets() { + final List 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() { + @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 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 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 discoveredInstances = new HashSet<>(playerConfigData.getDiscoveredInstances()); + discoveredInstances.add(instanceUuid); + playerConfigData.setDiscoveredInstances(discoveredInstances); + Ref playerRef = event.getPlayerRef(); + if (playerRef.isValid()) { + world.execute(() -> { + Store store = world.getEntityStore().getStore(); + showInstanceDiscovery(playerRef, store, instanceUuid, discoveryConfig); + }); + } + } + } + } + } + } + + private static void showInstanceDiscovery( + @Nonnull Ref ref, @Nonnull Store 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 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 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 getInstanceDataResourceType() { + return this.instanceDataResourceType; + } + + @Nonnull + public ComponentType getInstanceEntityConfigComponentType() { + return this.instanceEntityConfigComponentType; + } + + @Nonnull + public ComponentType getInstanceBlockComponentType() { + return this.instanceBlockComponentType; + } + + @Nonnull + public ComponentType getConfigurableInstanceBlockComponentType() { + return this.configurableInstanceBlockComponentType; + } +} diff --git a/src/com/hypixel/hytale/component/ArchetypeChunk.java b/src/com/hypixel/hytale/component/ArchetypeChunk.java new file mode 100644 index 00000000..2e5adad1 --- /dev/null +++ b/src/com/hypixel/hytale/component/ArchetypeChunk.java @@ -0,0 +1,356 @@ +package com.hypixel.hytale.component; + +import com.hypixel.hytale.common.util.ArrayUtil; +import com.hypixel.hytale.function.consumer.IntObjectConsumer; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.function.Consumer; +import java.util.function.IntPredicate; + +public class ArchetypeChunk { + @Nonnull + private static final ArchetypeChunk[] EMPTY_ARRAY = new ArchetypeChunk[0]; + @Nonnull + protected final Store store; + @Nonnull + protected final Archetype archetype; + protected int entitiesSize; + @Nonnull + protected Ref[] refs = new Ref[16]; + protected Component[][] components; + + public static ArchetypeChunk[] emptyArray() { + return EMPTY_ARRAY; + } + + public ArchetypeChunk(@Nonnull Store store, @Nonnull Archetype archetype) { + this.store = store; + this.archetype = archetype; + this.components = new Component[archetype.length()][]; + + for (int i = archetype.getMinIndex(); i < archetype.length(); i++) { + ComponentType> componentType = (ComponentType>) archetype.get(i); + if (componentType != null) { + this.components[componentType.getIndex()] = new Component[16]; + } + } + } + + @Nonnull + public Archetype getArchetype() { + return this.archetype; + } + + public int size() { + return this.entitiesSize; + } + + @Nonnull + public Ref getReferenceTo(int index) { + if (index >= 0 && index < this.entitiesSize) { + return this.refs[index]; + } else { + throw new IndexOutOfBoundsException(index); + } + } + + public > void setComponent(int index, @Nonnull ComponentType componentType, @Nonnull T component) { + componentType.validateRegistry(this.store.getRegistry()); + if (index < 0 || index >= this.entitiesSize) { + throw new IndexOutOfBoundsException(index); + } else if (!this.archetype.contains(componentType)) { + throw new IllegalArgumentException("Entity doesn't have component type " + componentType); + } else { + this.components[componentType.getIndex()][index] = component; + } + } + + @Nullable + public > T getComponent(int index, @Nonnull ComponentType 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; + } + } + + public int addEntity(@Nonnull Ref ref, @Nonnull Holder holder) { + if (!this.archetype.equals(holder.getArchetype())) { + throw new IllegalArgumentException("EntityHolder is not for this archetype chunk!"); + } else { + int entityIndex = this.entitiesSize++; + int oldLength = this.refs.length; + if (oldLength <= entityIndex) { + int newLength = ArrayUtil.grow(entityIndex); + this.refs = Arrays.copyOf(this.refs, newLength); + + for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(i); + if (componentType != null) { + int componentTypeIndex = componentType.getIndex(); + this.components[componentTypeIndex] = Arrays.copyOf(this.components[componentTypeIndex], newLength); + } + } + } + + this.refs[entityIndex] = ref; + + for (int ix = this.archetype.getMinIndex(); ix < this.archetype.length(); ix++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(ix); + if (componentType != null) { + this.components[componentType.getIndex()][entityIndex] = holder.getComponent((ComponentType>) componentType); + } + } + + return entityIndex; + } + } + + @Nonnull + public Holder copyEntity(int entityIndex, @Nonnull Holder target) { + if (entityIndex >= this.entitiesSize) { + throw new IndexOutOfBoundsException(entityIndex); + } else { + Component[] entityComponents = target.ensureComponentsSize(this.archetype.length()); + + for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(i); + if (componentType != null) { + int componentTypeIndex = componentType.getIndex(); + Component component = this.components[componentTypeIndex][entityIndex]; + entityComponents[componentTypeIndex] = component.clone(); + } + } + + target.init(this.archetype, entityComponents); + return target; + } + } + + @Nonnull + public Holder copySerializableEntity(@Nonnull ComponentRegistry.Data data, int entityIndex, @Nonnull Holder target) { + // HyFix #29: Handle IndexOutOfBoundsException during chunk saving gracefully + try { + if (entityIndex >= this.entitiesSize) { + throw new IndexOutOfBoundsException(entityIndex); + } else { + Archetype serializableArchetype = this.archetype.getSerializableArchetype(data); + Component[] entityComponents = target.ensureComponentsSize(serializableArchetype.length()); + + for (int i = serializableArchetype.getMinIndex(); i < serializableArchetype.length(); i++) { + ComponentType> componentType = (ComponentType>) serializableArchetype.get( + i + ); + if (componentType != null) { + int componentTypeIndex = componentType.getIndex(); + Component 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)"); + return target; + } + } + + @Nonnull + public Holder removeEntity(int entityIndex, @Nonnull Holder target) { + if (entityIndex >= this.entitiesSize) { + throw new IndexOutOfBoundsException(entityIndex); + } else { + Component[] entityComponents = target.ensureComponentsSize(this.archetype.length()); + + for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(i); + if (componentType != null) { + int componentTypeIndex = componentType.getIndex(); + entityComponents[componentTypeIndex] = this.components[componentTypeIndex][entityIndex]; + } + } + + int lastIndex = this.entitiesSize - 1; + if (entityIndex != lastIndex) { + this.fillEmptyIndex(entityIndex, lastIndex); + } + + this.refs[lastIndex] = null; + + for (int ix = this.archetype.getMinIndex(); ix < this.archetype.length(); ix++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(ix); + if (componentType != null) { + this.components[componentType.getIndex()][lastIndex] = null; + } + } + + this.entitiesSize = lastIndex; + target.init(this.archetype, entityComponents); + return target; + } + } + + public void transferTo( + @Nonnull Holder tempInternalEntityHolder, + @Nonnull ArchetypeChunk chunk, + @Nonnull Consumer> modification, + @Nonnull IntObjectConsumer> referenceConsumer + ) { + Component[] entityComponents = new Component[this.archetype.length()]; + + for (int entityIndex = 0; entityIndex < this.entitiesSize; entityIndex++) { + Ref ref = this.refs[entityIndex]; + this.refs[entityIndex] = null; + + for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(i); + if (componentType != null) { + int componentTypeIndex = componentType.getIndex(); + entityComponents[componentTypeIndex] = this.components[componentTypeIndex][entityIndex]; + this.components[componentTypeIndex][entityIndex] = null; + } + } + + tempInternalEntityHolder._internal_init(this.archetype, entityComponents, this.store.getRegistry().getUnknownComponentType()); + modification.accept(tempInternalEntityHolder); + int newEntityIndex = chunk.addEntity(ref, tempInternalEntityHolder); + referenceConsumer.accept(newEntityIndex, ref); + } + + this.entitiesSize = 0; + } + + public void transferSomeTo( + @Nonnull Holder tempInternalEntityHolder, + @Nonnull ArchetypeChunk chunk, + @Nonnull IntPredicate shouldTransfer, + @Nonnull Consumer> modification, + @Nonnull IntObjectConsumer> referenceConsumer + ) { + int firstTransfered = Integer.MIN_VALUE; + int lastTransfered = Integer.MIN_VALUE; + Component[] entityComponents = new Component[this.archetype.length()]; + + for (int entityIndex = 0; entityIndex < this.entitiesSize; entityIndex++) { + if (shouldTransfer.test(entityIndex)) { + if (firstTransfered == Integer.MIN_VALUE) { + firstTransfered = entityIndex; + } + + lastTransfered = entityIndex; + Ref ref = this.refs[entityIndex]; + this.refs[entityIndex] = null; + + for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(i); + if (componentType != null) { + int componentTypeIndex = componentType.getIndex(); + entityComponents[componentTypeIndex] = this.components[componentTypeIndex][entityIndex]; + this.components[componentTypeIndex][entityIndex] = null; + } + } + + tempInternalEntityHolder.init(this.archetype, entityComponents); + modification.accept(tempInternalEntityHolder); + int newEntityIndex = chunk.addEntity(ref, tempInternalEntityHolder); + referenceConsumer.accept(newEntityIndex, ref); + } + } + + if (firstTransfered != Integer.MIN_VALUE) { + if (lastTransfered == this.entitiesSize - 1) { + this.entitiesSize = firstTransfered; + return; + } + + int newSize = this.entitiesSize - (lastTransfered - firstTransfered + 1); + + for (int entityIndexx = firstTransfered; entityIndexx <= lastTransfered; entityIndexx++) { + if (this.refs[entityIndexx] == null) { + int lastIndex = this.entitiesSize - 1; + if (lastIndex == lastTransfered) { + break; + } + + if (entityIndexx != lastIndex) { + this.fillEmptyIndex(entityIndexx, lastIndex); + } + + this.entitiesSize--; + } + } + + this.entitiesSize = newSize; + } + } + + protected void fillEmptyIndex(int entityIndex, int lastIndex) { + Ref ref = this.refs[lastIndex]; + this.store.setEntityChunkIndex(ref, entityIndex); + + for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) { + ComponentType> componentType = (ComponentType>) this.archetype.get(i); + if (componentType != null) { + Component[] componentArr = this.components[componentType.getIndex()]; + componentArr[entityIndex] = componentArr[lastIndex]; + } + } + + this.refs[entityIndex] = ref; + } + + public void appendDump(@Nonnull String prefix, @Nonnull StringBuilder sb) { + sb.append(prefix).append("archetype=").append(this.archetype).append("\n"); + sb.append(prefix).append("entitiesSize=").append(this.entitiesSize).append("\n"); + + for (int i = 0; i < this.entitiesSize; i++) { + sb.append(prefix).append("\t- ").append(this.refs[i]).append("\n"); + sb.append(prefix).append("\t").append("components=").append("\n"); + + for (int x = this.archetype.getMinIndex(); x < this.archetype.length(); x++) { + ComponentType> componentType = (ComponentType>) this.archetype + .get(x); + if (componentType != null) { + sb.append(prefix) + .append("\t\t- ") + .append(componentType.getIndex()) + .append("\t") + .append(this.components[componentType.getIndex()][x]) + .append("\n"); + } + } + } + } + + @Nonnull + @Override + public String toString() { + return "ArchetypeChunk{archetype=" + + this.archetype + + ", entitiesSize=" + + this.entitiesSize + + ", entityReferences=" + + Arrays.toString((Object[]) this.refs) + + ", components=" + + Arrays.toString((Object[]) this.components) + + "}"; + } +} diff --git a/src/com/hypixel/hytale/component/CommandBuffer.java b/src/com/hypixel/hytale/component/CommandBuffer.java new file mode 100644 index 00000000..ce5afa31 --- /dev/null +++ b/src/com/hypixel/hytale/component/CommandBuffer.java @@ -0,0 +1,375 @@ +package com.hypixel.hytale.component; + +import com.hypixel.hytale.component.event.EntityEventType; +import com.hypixel.hytale.component.event.WorldEventType; +import com.hypixel.hytale.component.system.EcsEvent; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.ArrayDeque; +import java.util.Deque; +import java.util.function.Consumer; + +public class CommandBuffer implements ComponentAccessor { + @Nonnull + private final Store store; + @Nonnull + private final Deque>> queue = new ArrayDeque<>(); + @Nullable + private Ref trackedRef; + private boolean trackedRefRemoved; + @Nullable + private CommandBuffer parentBuffer; + @Nullable + private Thread thread; + + protected CommandBuffer(@Nonnull Store store) { + this.store = store; + + assert this.setThread(); + } + + @Nonnull + public Store getStore() { + return this.store; + } + + public void run(@Nonnull Consumer> consumer) { + assert Thread.currentThread() == this.thread; + + this.queue.add(consumer); + } + + @Override + public > T getComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType) { + assert Thread.currentThread() == this.thread; + + return this.store.__internal_getComponent(ref, componentType); + } + + @Nonnull + @Override + public Archetype getArchetype(@Nonnull Ref ref) { + assert Thread.currentThread() == this.thread; + + return this.store.__internal_getArchetype(ref); + } + + @Nonnull + @Override + public > T getResource(@Nonnull ResourceType resourceType) { + assert Thread.currentThread() == this.thread; + + return this.store.__internal_getResource(resourceType); + } + + @Nonnull + @Override + public ECS_TYPE getExternalData() { + return this.store.getExternalData(); + } + + @Nonnull + @Override + public Ref[] addEntities(@Nonnull Holder[] holders, @Nonnull AddReason reason) { + assert Thread.currentThread() == this.thread; + + Ref[] refs = new Ref[holders.length]; + + for (int i = 0; i < holders.length; i++) { + refs[i] = new Ref<>(this.store); + } + + this.queue.add(chunk -> chunk.addEntities(holders, refs, reason)); + return refs; + } + + @Nonnull + @Override + public Ref addEntity(@Nonnull Holder holder, @Nonnull AddReason reason) { + assert Thread.currentThread() == this.thread; + + Ref ref = new Ref<>(this.store); + this.queue.add(chunk -> chunk.addEntity(holder, ref, reason)); + return ref; + } + + public void addEntities( + @Nonnull Holder[] holders, int holderStart, @Nonnull Ref[] refs, int refStart, int length, @Nonnull AddReason reason + ) { + assert Thread.currentThread() == this.thread; + + for (int i = refStart; i < refStart + length; i++) { + refs[i] = new Ref<>(this.store); + } + + this.queue.add(chunk -> chunk.addEntities(holders, holderStart, refs, refStart, length, reason)); + } + + @Nonnull + public Ref addEntity(@Nonnull Holder holder, @Nonnull Ref ref, @Nonnull AddReason reason) { + if (ref.isValid()) { + throw new IllegalArgumentException("EntityReference is already in use!"); + } else if (ref.getStore() != this.store) { + throw new IllegalArgumentException("EntityReference is not for the correct store!"); + } else { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> chunk.addEntity(holder, ref, reason)); + return ref; + } + } + + @Nonnull + public Holder copyEntity(@Nonnull Ref ref, @Nonnull Holder target) { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> chunk.copyEntity(ref, target)); + return target; + } + + public void tryRemoveEntity(@Nonnull Ref ref, @Nonnull RemoveReason reason) { + assert Thread.currentThread() == this.thread; + + Throwable source = new Throwable(); + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.removeEntity(ref, chunk.getRegistry().newHolder(), reason, source); + } + }); + if (ref.equals(this.trackedRef)) { + this.trackedRefRemoved = true; + } + + if (this.parentBuffer != null) { + this.parentBuffer.testRemovedTracked(ref); + } + } + + public void removeEntity(@Nonnull Ref ref, @Nonnull RemoveReason reason) { + assert Thread.currentThread() == this.thread; + + Throwable source = new Throwable(); + this.queue.add(chunk -> chunk.removeEntity(ref, chunk.getRegistry().newHolder(), reason, source)); + if (ref.equals(this.trackedRef)) { + this.trackedRefRemoved = true; + } + + if (this.parentBuffer != null) { + this.parentBuffer.testRemovedTracked(ref); + } + } + + @Nonnull + @Override + public Holder removeEntity(@Nonnull Ref ref, @Nonnull Holder target, @Nonnull RemoveReason reason) { + assert Thread.currentThread() == this.thread; + + Throwable source = new Throwable(); + this.queue.add(chunk -> chunk.removeEntity(ref, target, reason, source)); + if (ref.equals(this.trackedRef)) { + this.trackedRefRemoved = true; + } + + if (this.parentBuffer != null) { + this.parentBuffer.testRemovedTracked(ref); + } + + return target; + } + + public > void ensureComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType) { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.ensureComponent(ref, componentType); + } + }); + } + + @Nonnull + @Override + public > T ensureAndGetComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType) { + assert Thread.currentThread() == this.thread; + + T component = this.store.__internal_getComponent(ref, componentType); + if (component != null) { + return component; + } else { + T newComponent = this.store.getRegistry()._internal_getData().createComponent(componentType); + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.addComponent(ref, componentType, newComponent); + } + }); + return newComponent; + } + } + + @Nonnull + @Override + public > T addComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType) { + assert Thread.currentThread() == this.thread; + + T component = this.store.getRegistry()._internal_getData().createComponent(componentType); + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.addComponent(ref, componentType, component); + } + }); + return component; + } + + @Override + public > void addComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType, @Nonnull T component) { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.addComponent(ref, componentType, component); + } + }); + } + + public > void replaceComponent( + @Nonnull Ref ref, @Nonnull ComponentType componentType, @Nonnull T component + ) { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.replaceComponent(ref, componentType, component); + } + }); + } + + @Override + public > void removeComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType) { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> { + if (ref.isValid()) { + // HyFix #12: Use tryRemoveComponent to prevent race condition when multiple systems queue removal + chunk.tryRemoveComponent(ref, componentType); + } + }); + } + + @Override + public > void tryRemoveComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType) { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.tryRemoveComponent(ref, componentType); + } + }); + } + + @Override + public > void putComponent(@Nonnull Ref ref, @Nonnull ComponentType componentType, @Nonnull T component) { + assert Thread.currentThread() == this.thread; + + this.queue.add(chunk -> { + if (ref.isValid()) { + chunk.putComponent(ref, componentType, component); + } + }); + } + + @Override + public void invoke(@Nonnull Ref ref, @Nonnull Event param) { + assert Thread.currentThread() == this.thread; + + this.store.internal_invoke(this, ref, param); + } + + @Override + public void invoke(@Nonnull EntityEventType systemType, @Nonnull Ref ref, @Nonnull Event param) { + assert Thread.currentThread() == this.thread; + + this.store.internal_invoke(this, systemType, ref, param); + } + + @Override + public void invoke(@Nonnull Event param) { + assert Thread.currentThread() == this.thread; + + this.store.internal_invoke(this, param); + } + + @Override + public void invoke(@Nonnull WorldEventType systemType, @Nonnull Event param) { + assert Thread.currentThread() == this.thread; + + this.store.internal_invoke(this, systemType, param); + } + + void track(@Nonnull Ref ref) { + this.trackedRef = ref; + } + + private void testRemovedTracked(@Nonnull Ref ref) { + if (ref.equals(this.trackedRef)) { + this.trackedRefRemoved = true; + } + + if (this.parentBuffer != null) { + this.parentBuffer.testRemovedTracked(ref); + } + } + + boolean consumeWasTrackedRefRemoved() { + if (this.trackedRef == null) { + throw new IllegalStateException("Not tracking any ref!"); + } else { + boolean wasRemoved = this.trackedRefRemoved; + this.trackedRefRemoved = false; + return wasRemoved; + } + } + + void consume() { + this.trackedRef = null; + this.trackedRefRemoved = false; + + assert Thread.currentThread() == this.thread; + + while (!this.queue.isEmpty()) { + this.queue.pop().accept(this.store); + } + + this.store.storeCommandBuffer(this); + } + + @Nonnull + public CommandBuffer fork() { + CommandBuffer forkedBuffer = this.store.takeCommandBuffer(); + forkedBuffer.parentBuffer = this; + return forkedBuffer; + } + + public void mergeParallel(@Nonnull CommandBuffer commandBuffer) { + this.trackedRef = null; + this.trackedRefRemoved = false; + this.parentBuffer = null; + + while (!this.queue.isEmpty()) { + commandBuffer.queue.add(this.queue.pop()); + } + + this.store.storeCommandBuffer(this); + } + + public boolean setThread() { + this.thread = Thread.currentThread(); + return true; + } + + public void validateEmpty() { + if (!this.queue.isEmpty()) { + throw new AssertionError("CommandBuffer must be empty when returned to store!"); + } + } +} diff --git a/src/com/hypixel/hytale/component/ComponentType.java b/src/com/hypixel/hytale/component/ComponentType.java new file mode 100644 index 00000000..e0171797 --- /dev/null +++ b/src/com/hypixel/hytale/component/ComponentType.java @@ -0,0 +1,107 @@ +package com.hypixel.hytale.component; + +import com.hypixel.hytale.component.query.Query; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ComponentType> implements Comparable>, Query { + @Nonnull + public static final ComponentType[] EMPTY_ARRAY = new ComponentType[0]; + private ComponentRegistry registry; + private Class tClass; + private int index; + private boolean invalid = true; + + public ComponentType() { + } + + void init(@Nonnull ComponentRegistry registry, @Nonnull Class tClass, int index) { + this.registry = registry; + this.tClass = tClass; + this.index = index; + this.invalid = false; + } + + @Nonnull + public ComponentRegistry getRegistry() { + return this.registry; + } + + @Nonnull + public Class getTypeClass() { + return this.tClass; + } + + public int getIndex() { + return this.index; + } + + void invalidate() { + this.invalid = true; + } + + public boolean isValid() { + return !this.invalid; + } + + @Override + public boolean test(@Nonnull Archetype archetype) { + return archetype.contains(this); + } + + @Override + public boolean requiresComponentType(ComponentType componentType) { + return this.equals(componentType); + } + + @Override + public void validateRegistry(@Nonnull ComponentRegistry registry) { + if (!this.registry.equals(registry)) { + throw new IllegalArgumentException("ComponentType is for a different registry! " + this); + } + } + + @Override + public void validate() { + if (this.invalid) { + throw new IllegalStateException("ComponentType is invalid!"); + } + } + + public int compareTo(@Nonnull ComponentType o) { + return Integer.compare(this.index, o.index); + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } else if (o != null && this.getClass() == o.getClass()) { + ComponentType that = (ComponentType) o; + return this.index != that.index ? false : this.registry.equals(that.registry); + } else { + return false; + } + } + + @Override + public int hashCode() { + int result = this.registry.hashCode(); + return 31 * result + this.index; + } + + @Nonnull + @Override + public String toString() { + return "ComponentType{registry=" + + this.registry.getClass() + + "@" + + this.registry.hashCode() + + ", typeClass=" + + this.tClass + + ", index=" + + this.index + + "}"; + } +} diff --git a/src/com/hypixel/hytale/component/system/tick/EntityTickingSystem.java b/src/com/hypixel/hytale/component/system/tick/EntityTickingSystem.java new file mode 100644 index 00000000..dc81c333 --- /dev/null +++ b/src/com/hypixel/hytale/component/system/tick/EntityTickingSystem.java @@ -0,0 +1,129 @@ +package com.hypixel.hytale.component.system.tick; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.task.ParallelRangeTask; +import com.hypixel.hytale.component.task.ParallelTask; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.function.IntConsumer; + +public abstract class EntityTickingSystem extends ArchetypeTickingSystem { + public EntityTickingSystem() { + } + + protected static boolean maybeUseParallel(int archetypeChunkSize, int taskCount) { + return false; + } + + protected static boolean useParallel(int archetypeChunkSize, int taskCount) { + return taskCount > 0 || archetypeChunkSize > ParallelRangeTask.PARALLELISM; + } + + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return false; + } + + @Override + public void tick(float dt, @Nonnull ArchetypeChunk archetypeChunk, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer) { + doTick(this, dt, archetypeChunk, store, commandBuffer); + } + + public abstract void tick(float var1, int var2, @Nonnull ArchetypeChunk var3, @Nonnull Store var4, @Nonnull CommandBuffer var5); + + static long lastError = 0; + + public static void doTick( + @Nonnull EntityTickingSystem system, + float dt, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + try { + int archetypeChunkSize = archetypeChunk.size(); + if (archetypeChunkSize != 0) { + ParallelTask> task = store.getParallelTask(); + if (system.isParallel(archetypeChunkSize, task.size())) { + ParallelRangeTask> systemTask = task.appendTask(); + systemTask.init(0, archetypeChunkSize); + int i = 0; + + for (int systemTaskSize = systemTask.size(); i < systemTaskSize; i++) { + systemTask.get(i).init(system, dt, archetypeChunk, store, commandBuffer.fork()); + } + } else { + for (int index = 0; index < archetypeChunkSize; index++) { + system.tick(dt, index, archetypeChunk, store, commandBuffer); + } + } + } + } catch (Exception e) { + if (lastError - System.currentTimeMillis() > 1000) { + e.printStackTrace(); + lastError = System.currentTimeMillis(); + } + } + } + + public static class SystemTaskData implements IntConsumer { + @Nullable + private EntityTickingSystem system; + private float dt; + @Nullable + private ArchetypeChunk archetypeChunk; + @Nullable + private Store store; + @Nullable + private CommandBuffer commandBuffer; + + public SystemTaskData() { + } + + public void init( + EntityTickingSystem system, float dt, ArchetypeChunk archetypeChunk, Store store, CommandBuffer commandBuffer + ) { + this.system = system; + this.dt = dt; + this.archetypeChunk = archetypeChunk; + this.store = store; + this.commandBuffer = commandBuffer; + } + + @Override + public void accept(int index) { + assert this.commandBuffer.setThread(); + + this.system.tick(this.dt, index, this.archetypeChunk, this.store, this.commandBuffer); + } + + public void clear() { + this.system = null; + this.archetypeChunk = null; + this.store = null; + this.commandBuffer = null; + } + + public static void invokeParallelTask( + @Nonnull ParallelTask> parallelTask, @Nonnull CommandBuffer commandBuffer + ) { + int parallelTaskSize = parallelTask.size(); + if (parallelTaskSize > 0) { + parallelTask.doInvoke(); + + for (int x = 0; x < parallelTaskSize; x++) { + ParallelRangeTask> systemTask = parallelTask.get(x); + int i = 0; + + for (int systemTaskSize = systemTask.size(); i < systemTaskSize; i++) { + EntityTickingSystem.SystemTaskData taskData = systemTask.get(i); + taskData.commandBuffer.mergeParallel(commandBuffer); + taskData.clear(); + } + } + } + } + } +} diff --git a/src/com/hypixel/hytale/math/shape/Box.java b/src/com/hypixel/hytale/math/shape/Box.java new file mode 100644 index 00000000..31791f44 --- /dev/null +++ b/src/com/hypixel/hytale/math/shape/Box.java @@ -0,0 +1,517 @@ +package com.hypixel.hytale.math.shape; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.function.predicate.TriIntObjPredicate; +import com.hypixel.hytale.function.predicate.TriIntPredicate; +import com.hypixel.hytale.math.Axis; +import com.hypixel.hytale.math.util.MathUtil; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.math.vector.Vector3i; +import jdk.internal.vm.annotation.ForceInline; + +import javax.annotation.Nonnull; + +public class Box implements Shape { + public static final Codec CODEC = BuilderCodec.builder(Box.class, Box::new) + .append(new KeyedCodec<>("Min", Vector3d.CODEC), (box, v) -> box.min.assign(v), box -> box.min) + .add() + .append(new KeyedCodec<>("Max", Vector3d.CODEC), (box, v) -> box.max.assign(v), box -> box.max) + .add() + .validator((box, results) -> { + if (box.width() <= 0.0) { + results.fail("Width is <= 0! Given: " + box.width()); + } + + if (box.height() <= 0.0) { + results.fail("Height is <= 0! Given: " + box.height()); + } + + if (box.depth() <= 0.0) { + results.fail("Depth is <= 0! Given: " + box.depth()); + } + }) + .build(); + public static final Box UNIT = new Box(Vector3d.ZERO, Vector3d.ALL_ONES); + @Nonnull + public final Vector3d min = new Vector3d(); + @Nonnull + public final Vector3d max = new Vector3d(); + + @Nonnull + public static Box horizontallyCentered(double width, double height, double depth) { + return new Box(-width / 2.0, 0.0, -depth / 2.0, width / 2.0, height, depth / 2.0); + } + + public Box() { + } + + public Box(@Nonnull Box box) { + this(); + this.min.assign(box.min); + this.max.assign(box.max); + } + + public Box(@Nonnull Vector3d min, @Nonnull Vector3d max) { + this(); + this.min.assign(min); + this.max.assign(max); + } + + public Box(double xMin, double yMin, double zMin, double xMax, double yMax, double zMax) { + this(); + this.min.assign(xMin, yMin, zMin); + this.max.assign(xMax, yMax, zMax); + } + + public static Box cube(@Nonnull Vector3d min, double side) { + return new Box(min.x, min.y, min.z, min.x + side, min.y + side, min.z + side); + } + + public static Box centeredCube(@Nonnull Vector3d center, double inradius) { + return new Box(center.x - inradius, center.y - inradius, center.z - inradius, center.x + inradius, center.y + inradius, center.z + inradius); + } + + @Nonnull + public Box setMinMax(@Nonnull Vector3d min, @Nonnull Vector3d max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box setMinMax(@Nonnull double[] min, @Nonnull double[] max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box setMinMax(@Nonnull float[] min, @Nonnull float[] max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box setEmpty() { + this.setMinMax(Double.MAX_VALUE, -Double.MAX_VALUE); + return this; + } + + @Nonnull + public Box setMinMax(double min, double max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box union(@Nonnull Box bb) { + if (this.min.x > bb.min.x) { + this.min.x = bb.min.x; + } + + if (this.min.y > bb.min.y) { + this.min.y = bb.min.y; + } + + if (this.min.z > bb.min.z) { + this.min.z = bb.min.z; + } + + if (this.max.x < bb.max.x) { + this.max.x = bb.max.x; + } + + if (this.max.y < bb.max.y) { + this.max.y = bb.max.y; + } + + if (this.max.z < bb.max.z) { + this.max.z = bb.max.z; + } + + return this; + } + + @Nonnull + public Box assign(@Nonnull Box other) { + this.min.assign(other.min); + this.max.assign(other.max); + return this; + } + + @Nonnull + public Box assign(double minX, double minY, double minZ, double maxX, double maxY, double maxZ) { + this.min.assign(minX, minY, minZ); + this.max.assign(maxX, maxY, maxZ); + return this; + } + + @Nonnull + public Box minkowskiSum(@Nonnull Box bb) { + this.min.subtract(bb.max); + this.max.subtract(bb.min); + return this; + } + + @Nonnull + public Box scale(float scale) { + this.min.scale(scale); + this.max.scale(scale); + return this; + } + + @Nonnull + public Box normalize() { + if (this.min.x > this.max.x) { + double t = this.min.x; + this.min.x = this.max.x; + this.max.x = t; + } + + if (this.min.y > this.max.y) { + double t = this.min.y; + this.min.y = this.max.y; + this.max.y = t; + } + + if (this.min.z > this.max.z) { + double t = this.min.z; + this.min.z = this.max.z; + this.max.z = t; + } + + return this; + } + + @Nonnull + public Box rotateX(float angleInRadians) { + this.min.rotateX(angleInRadians); + this.max.rotateX(angleInRadians); + return this; + } + + @Nonnull + public Box rotateY(float angleInRadians) { + this.min.rotateY(angleInRadians); + this.max.rotateY(angleInRadians); + return this; + } + + @Nonnull + public Box rotateZ(float angleInRadians) { + this.min.rotateZ(angleInRadians); + this.max.rotateZ(angleInRadians); + return this; + } + + @Nonnull + public Box offset(double x, double y, double z) { + this.min.add(x, y, z); + this.max.add(x, y, z); + return this; + } + + @Nonnull + public Box offset(@Nonnull Vector3d pos) { + this.min.add(pos); + this.max.add(pos); + return this; + } + + @Nonnull + public Box sweep(@Nonnull Vector3d v) { + if (v.x < 0.0) { + this.min.x = this.min.x + v.x; + } else if (v.x > 0.0) { + this.max.x = this.max.x + v.x; + } + + if (v.y < 0.0) { + this.min.y = this.min.y + v.y; + } else if (v.y > 0.0) { + this.max.y = this.max.y + v.y; + } + + if (v.z < 0.0) { + this.min.z = this.min.z + v.z; + } else if (v.z > 0.0) { + this.max.z = this.max.z + v.z; + } + + return this; + } + + @Nonnull + public Box extend(double extentX, double extentY, double extentZ) { + this.min.subtract(extentX, extentY, extentZ); + this.max.add(extentX, extentY, extentZ); + return this; + } + + public double width() { + return this.max.x - this.min.x; + } + + public double height() { + return this.max.y - this.min.y; + } + + public double depth() { + return this.max.z - this.min.z; + } + + public double dimension(@Nonnull Axis axis) { + return switch (axis) { + case X -> this.width(); + case Y -> this.height(); + case Z -> this.depth(); + }; + } + + public double getThickness() { + return MathUtil.minValue(this.width(), this.height(), this.depth()); + } + + public double getMaximumThickness() { + return MathUtil.maxValue(this.width(), this.height(), this.depth()); + } + + public double getVolume() { + double w = this.width(); + if (w <= 0.0) { + return 0.0; + } else { + double h = this.height(); + if (h <= 0.0) { + return 0.0; + } else { + double d = this.depth(); + return d <= 0.0 ? 0.0 : w * h * d; + } + } + } + + public boolean hasVolume() { + return this.min.x <= this.max.x && this.min.y <= this.max.y && this.min.z <= this.max.z; + } + + public boolean isIntersecting(@Nonnull Box other) { + return !(this.min.x > other.max.x) + && !(other.min.x > this.max.x) + && !(this.min.y > other.max.y) + && !(other.min.y > this.max.y) + && !(this.min.z > other.max.z) + && !(other.min.z > this.max.z); + } + + public boolean isUnitBox() { + return this.min.equals(Vector3d.ZERO) && this.max.equals(Vector3d.ALL_ONES); + } + + public double middleX() { + return (this.min.x + this.max.x) / 2.0; + } + + public double middleY() { + return (this.min.y + this.max.y) / 2.0; + } + + public double middleZ() { + return (this.min.z + this.max.z) / 2.0; + } + + @Nonnull + public Box clone() { + Box box = new Box(); + box.assign(this); + return box; + } + + @Nonnull + public Vector3d getMin() { + return this.min; + } + + @Nonnull + public Vector3d getMax() { + return this.max; + } + + @Nonnull + @Override + public Box getBox(double x, double y, double z) { + return new Box(this.min.getX() + x, this.min.getY() + y, this.min.getZ() + z, this.max.getX() + x, this.max.getY() + y, this.max.getZ() + z); + } + + @Override + public boolean containsPosition(double x, double y, double z) { + return x >= this.min.getX() && x <= this.max.getX() && y >= this.min.getY() && y <= this.max.getY() && z >= this.min.getZ() && z <= this.max.getZ(); + } + + @Override + public void expand(double radius) { + this.extend(radius, radius, radius); + } + + @ForceInline + public boolean containsBlock(int x, int y, int z) { + int minX = MathUtil.floor(this.min.getX()); + int minY = MathUtil.floor(this.min.getY()); + int minZ = MathUtil.floor(this.min.getZ()); + int maxX = MathUtil.ceil(this.max.getX()); + int maxY = MathUtil.ceil(this.max.getY()); + int maxZ = MathUtil.ceil(this.max.getZ()); + return x >= minX && x < maxX && y >= minY && y < maxY && z >= minZ && z < maxZ; + } + + public boolean containsBlock(@Nonnull Vector3i origin, int x, int y, int z) { + return this.containsBlock(x - origin.getX(), y - origin.getY(), z - origin.getZ()); + } + + @Override + @ForceInline + public boolean forEachBlock(double x, double y, double z, double epsilon, @Nonnull TriIntPredicate consumer) { + int minX = MathUtil.floor(x + this.min.getX() - epsilon); + int minY = MathUtil.floor(y + this.min.getY() - epsilon); + int minZ = MathUtil.floor(z + this.min.getZ() - epsilon); + int maxX = MathUtil.floor(x + this.max.getX() + epsilon); + int maxY = MathUtil.floor(y + this.max.getY() + epsilon); + int maxZ = MathUtil.floor(z + this.max.getZ() + epsilon); + + for (int _x = minX; _x <= maxX; _x++) { + for (int _y = minY; _y <= maxY; _y++) { + for (int _z = minZ; _z <= maxZ; _z++) { + if (!consumer.test(_x, _y, _z)) { + return false; + } + } + } + } + + return true; + } + + @Override + @ForceInline + public boolean forEachBlock(double x, double y, double z, double epsilon, T t, @Nonnull TriIntObjPredicate consumer) { + int minX = MathUtil.floor(x + this.min.getX() - epsilon); + int minY = MathUtil.floor(y + this.min.getY() - epsilon); + int minZ = MathUtil.floor(z + this.min.getZ() - epsilon); + int maxX = MathUtil.floor(x + this.max.getX() + epsilon); + int maxY = MathUtil.floor(y + this.max.getY() + epsilon); + int maxZ = MathUtil.floor(z + this.max.getZ() + epsilon); + + for (int _x = minX; _x <= maxX; _x++) { + for (int _y = minY; _y <= maxY; _y++) { + for (int _z = minZ; _z <= maxZ; _z++) { + if (!consumer.test(_x, _y, _z, t)) { + return false; + } + } + } + } + + return true; + } + + public double getMaximumExtent() { + double maximumExtent = 0.0; + if (-this.min.x > maximumExtent) { + maximumExtent = -this.min.x; + } + + if (-this.min.y > maximumExtent) { + maximumExtent = -this.min.y; + } + + if (-this.min.z > maximumExtent) { + maximumExtent = -this.min.z; + } + + if (this.max.x - 1.0 > maximumExtent) { + maximumExtent = this.max.x - 1.0; + } + + if (this.max.y - 1.0 > maximumExtent) { + maximumExtent = this.max.y - 1.0; + } + + if (this.max.z - 1.0 > maximumExtent) { + maximumExtent = this.max.z - 1.0; + } + + return maximumExtent; + } + + @ForceInline + public boolean intersectsLine(@Nonnull Vector3d start, @Nonnull Vector3d end) { + Vector3d direction = end.clone().subtract(start); + double tmin = 0.0; + double tmax = 1.0; + if (Math.abs(direction.x) < 1.0E-10) { + if (start.x < this.min.x || start.x > this.max.x) { + return false; + } + } else { + double t1 = (this.min.x - start.x) / direction.x; + double t2 = (this.max.x - start.x) / direction.x; + if (t1 > t2) { + double temp = t1; + t1 = t2; + t2 = temp; + } + + tmin = Math.max(tmin, t1); + tmax = Math.min(tmax, t2); + if (tmin > tmax) { + return false; + } + } + + if (Math.abs(direction.y) < 1.0E-10) { + if (start.y < this.min.y || start.y > this.max.y) { + return false; + } + } else { + double t1x = (this.min.y - start.y) / direction.y; + double t2x = (this.max.y - start.y) / direction.y; + if (t1x > t2x) { + double temp = t1x; + t1x = t2x; + t2x = temp; + } + + tmin = Math.max(tmin, t1x); + tmax = Math.min(tmax, t2x); + if (tmin > tmax) { + return false; + } + } + + if (!(Math.abs(direction.z) < 1.0E-10)) { + double t1xx = (this.min.z - start.z) / direction.z; + double t2xx = (this.max.z - start.z) / direction.z; + if (t1xx > t2xx) { + double temp = t1xx; + t1xx = t2xx; + t2xx = temp; + } + + tmin = Math.max(tmin, t1xx); + tmax = Math.min(tmax, t2xx); + return !(tmin > tmax); + } else { + return !(start.z < this.min.z) && !(start.z > this.max.z); + } + } + + @Nonnull + @Override + public String toString() { + return "Box{min=" + this.min + ", max=" + this.max + "}"; + } +} diff --git a/src/com/hypixel/hytale/math/shape/Box2D.java b/src/com/hypixel/hytale/math/shape/Box2D.java new file mode 100644 index 00000000..2490df2b --- /dev/null +++ b/src/com/hypixel/hytale/math/shape/Box2D.java @@ -0,0 +1,203 @@ +package com.hypixel.hytale.math.shape; + +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.math.vector.Vector2d; + +import javax.annotation.Nonnull; + +public class Box2D implements Shape2D { + public static final BuilderCodec CODEC = BuilderCodec.builder(Box2D.class, Box2D::new) + .append(new KeyedCodec<>("Min", Vector2d.CODEC), (shape, min) -> shape.min.assign(min), shape -> shape.min) + .add() + .append(new KeyedCodec<>("Max", Vector2d.CODEC), (shape, max) -> shape.max.assign(max), shape -> shape.max) + .add() + .build(); + @Nonnull + public final Vector2d min = new Vector2d(); + @Nonnull + public final Vector2d max = new Vector2d(); + + public Box2D() { + } + + public Box2D(@Nonnull Box2D box) { + this(); + this.min.assign(box.min); + this.max.assign(box.max); + } + + public Box2D(@Nonnull Vector2d min, @Nonnull Vector2d max) { + this(); + this.min.assign(min); + this.max.assign(max); + } + + public Box2D(double xMin, double yMin, double xMax, double yMax) { + this(); + this.min.assign(xMin, yMin); + this.max.assign(xMax, yMax); + } + + @Nonnull + public Box2D setMinMax(@Nonnull Vector2d min, @Nonnull Vector2d max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box2D setMinMax(@Nonnull double[] min, @Nonnull double[] max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box2D setMinMax(@Nonnull float[] min, @Nonnull float[] max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box2D setEmpty() { + this.setMinMax(Double.MAX_VALUE, -Double.MAX_VALUE); + return this; + } + + @Nonnull + public Box2D setMinMax(double min, double max) { + this.min.assign(min); + this.max.assign(max); + return this; + } + + @Nonnull + public Box2D union(@Nonnull Box2D bb) { + if (this.min.x > bb.min.x) { + this.min.x = bb.min.x; + } + + if (this.min.y > bb.min.y) { + this.min.y = bb.min.y; + } + + if (this.max.x < bb.max.x) { + this.max.x = bb.max.x; + } + + if (this.max.y < bb.max.y) { + this.max.y = bb.max.y; + } + + return this; + } + + @Nonnull + public Box2D assign(@Nonnull Box2D other) { + this.min.assign(other.min); + this.max.assign(other.max); + return this; + } + + @Nonnull + public Box2D minkowskiSum(@Nonnull Box2D bb) { + this.min.subtract(bb.max); + this.max.subtract(bb.min); + return this; + } + + @Nonnull + public Box2D normalize() { + if (this.min.x > this.max.x) { + double t = this.min.x; + this.min.x = this.max.x; + this.max.x = t; + } + + if (this.min.y > this.max.y) { + double t = this.min.y; + this.min.y = this.max.y; + this.max.y = t; + } + + return this; + } + + @Nonnull + public Box2D offset(@Nonnull Vector2d pos) { + this.min.add(pos); + this.max.add(pos); + return this; + } + + @Nonnull + public Box2D sweep(@Nonnull Vector2d v) { + if (v.x < 0.0) { + this.min.x = this.min.x + v.x; + } else if (v.x > 0.0) { + this.max.x = this.max.x + v.x; + } + + if (v.y < 0.0) { + this.min.y = this.min.y + v.y; + } else if (v.y > 0.0) { + this.max.y = this.max.y + v.y; + } + + return this; + } + + @Nonnull + public Box2D extendToInt() { + this.min.floor(); + this.max.ceil(); + return this; + } + + @Nonnull + public Box2D extend(double extentX, double extentY) { + this.min.subtract(extentX, extentY); + this.max.add(extentX, extentY); + return this; + } + + public double width() { + return this.max.x - this.min.x; + } + + public double height() { + return this.max.y - this.min.y; + } + + public boolean isIntersecting(@Nonnull Box2D other) { + return !(this.min.x > other.max.x) && !(other.min.x > this.max.x) && !(this.min.y > other.max.y) && !(other.min.y > this.max.y); + } + + @Nonnull + @Override + public Box2D getBox(double x, double y) { + return new Box2D(this.min.getX() + x, this.min.getY() + y, this.max.getX() + x, this.max.getY() + y); + } + + @Override + public boolean containsPosition(@Nonnull Vector2d origin, @Nonnull Vector2d position) { + double x = position.getX() - origin.getX(); + double y = position.getY() - origin.getY(); + return x >= this.min.getX() && x <= this.max.getX() && y >= this.min.getY() && y <= this.max.getY(); + } + + @Override + public boolean containsPosition(@Nonnull Vector2d origin, double xx, double yy) { + double x = xx - origin.getX(); + double y = yy - origin.getY(); + return x >= this.min.getX() && x <= this.max.getX() && y >= this.min.getY() && y <= this.max.getY(); + } + + @Nonnull + @Override + public String toString() { + return "Box2D{min=" + this.min + ", max=" + this.max + "}"; + } +} diff --git a/src/com/hypixel/hytale/server/core/HytaleServer.java b/src/com/hypixel/hytale/server/core/HytaleServer.java new file mode 100644 index 00000000..439905e7 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/HytaleServer.java @@ -0,0 +1,544 @@ +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 METRICS_REGISTRY = new MetricsRegistry() + .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 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 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 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 pluginsContext = new HashMap<>(); + + for (PluginBase plugin : this.pluginManager.getPlugins()) { + PluginManifest manifestxx = plugin.getManifest(); + HashMap 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 packsContext = new HashMap<>(); + + for (AssetPack pack : assetModule.getAssetPacks()) { + HashMap 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 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() + .dispatchFor(LoadAssetEvent.class) + .dispatch(new LoadAssetEvent(this.bootStart)); + if (this.isShuttingDown()) { + return; + } + + if (loadAssetEvent.isShouldShutdown()) { + List 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 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 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 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; + } +} diff --git a/src/com/hypixel/hytale/server/core/entity/InteractionChain.java b/src/com/hypixel/hytale/server/core/entity/InteractionChain.java new file mode 100644 index 00000000..4314c0f3 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/entity/InteractionChain.java @@ -0,0 +1,851 @@ +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 forkedChains = new Long2ObjectOpenHashMap<>(); + @Nonnull + private final Long2ObjectMap tempForkedChainData = new Long2ObjectOpenHashMap<>(); + @Nonnull + private final Long2LongMap forkedChainsMap = new Long2LongOpenHashMap(); + @Nonnull + private final List newForks = new ObjectArrayList<>(); + @Nonnull + private final RootInteraction initialRootInteraction; + private RootInteraction rootInteraction; + private int operationCounter = 0; + @Nonnull + private final List 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 interactions = new ObjectArrayList<>(); + @Nonnull + private final List 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> it = Long2ObjectMaps.fastIterator(this.getTempForkedChainData()); + + while (it.hasNext()) { + Entry 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 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 getForkedChains() { + return this.forkedChains; + } + + @Nonnull + public Long2ObjectMap 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 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 tempForkedChainData = new Long2ObjectOpenHashMap<>(); + final List 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 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 + "}"; + } + } +} diff --git a/src/com/hypixel/hytale/server/core/entity/InteractionManager.java b/src/com/hypixel/hytale/server/core/entity/InteractionManager.java new file mode 100644 index 00000000..696c6c0f --- /dev/null +++ b/src/com/hypixel/hytale/server/core/entity/InteractionManager.java @@ -0,0 +1,1534 @@ +package com.hypixel.hytale.server.core.entity; + +import com.hypixel.hytale.common.util.FormatUtil; +import com.hypixel.hytale.common.util.ListUtil; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.Component; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.function.function.TriFunction; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.math.vector.Vector4d; +import com.hypixel.hytale.metrics.metric.HistoricMetric; +import com.hypixel.hytale.protocol.BlockPosition; +import com.hypixel.hytale.protocol.ForkedChainId; +import com.hypixel.hytale.protocol.GameMode; +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.RootInteractionSettings; +import com.hypixel.hytale.protocol.Vector3f; +import com.hypixel.hytale.protocol.WaitForDataFrom; +import com.hypixel.hytale.protocol.packets.interaction.CancelInteractionChain; +import com.hypixel.hytale.protocol.packets.interaction.SyncInteractionChain; +import com.hypixel.hytale.protocol.packets.inventory.SetActiveSlot; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.io.handlers.game.GamePacketHandler; +import com.hypixel.hytale.server.core.modules.interaction.IInteractionSimulationHandler; +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.InteractionTypeUtils; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.RootInteraction; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.data.Collector; +import com.hypixel.hytale.server.core.modules.interaction.interaction.config.data.CollectorTag; +import com.hypixel.hytale.server.core.modules.interaction.interaction.operation.Operation; +import com.hypixel.hytale.server.core.modules.time.TimeResource; +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.WorldChunk; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.core.util.UUIDUtil; +import io.sentry.Sentry; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.protocol.Message; +import io.sentry.protocol.SentryId; +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectMaps; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.Deque; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; +import java.util.function.Predicate; +import java.util.logging.Level; + +public class InteractionManager implements Component { + public static final double MAX_REACH_DISTANCE = 8.0; + public static final float[] DEFAULT_CHARGE_TIMES = new float[]{0.0F}; + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + @Nonnull + private final Int2ObjectMap chains = new Int2ObjectOpenHashMap<>(); + @Nonnull + private final Int2ObjectMap unmodifiableChains = Int2ObjectMaps.unmodifiable(this.chains); + @Nonnull + private final CooldownHandler cooldownHandler = new CooldownHandler(); + @Nonnull + private final LivingEntity entity; + @Nullable + private final PlayerRef playerRef; + private boolean hasRemoteClient; + @Nonnull + private final IInteractionSimulationHandler interactionSimulationHandler; + @Nonnull + private final ObjectList tempSyncDataList = new ObjectArrayList<>(); + private int lastServerChainId; + private int lastClientChainId; + private long packetQueueTime; + private final float[] globalTimeShift = new float[InteractionType.VALUES.length]; + private final boolean[] globalTimeShiftDirty = new boolean[InteractionType.VALUES.length]; + private boolean timeShiftsDirty; + private final ObjectList syncPackets = new ObjectArrayList<>(); + private long currentTime = 1L; + @Nonnull + private final ObjectList chainStartQueue = new ObjectArrayList<>(); + @Nonnull + private final Predicate cachedTickChain = this::tickChain; + @Nullable + protected CommandBuffer commandBuffer; + + public InteractionManager(@Nonnull LivingEntity entity, @Nullable PlayerRef playerRef, @Nonnull IInteractionSimulationHandler simulationHandler) { + this.entity = entity; + this.playerRef = playerRef; + this.hasRemoteClient = playerRef != null; + this.interactionSimulationHandler = simulationHandler; + } + + @Nonnull + public Int2ObjectMap getChains() { + return this.unmodifiableChains; + } + + @Nonnull + public IInteractionSimulationHandler getInteractionSimulationHandler() { + return this.interactionSimulationHandler; + } + + private long getOperationTimeoutThreshold() { + if (this.playerRef != null) { + return this.playerRef.getPacketHandler().getOperationTimeoutThreshold(); + } else { + assert this.commandBuffer != null; + + World world = this.commandBuffer.getExternalData().getWorld(); + return world.getTickStepNanos() / 1000000 * 10; + } + } + + private boolean waitingForClient(@Nonnull Ref ref) { + assert this.commandBuffer != null; + + Player playerComponent = this.commandBuffer.getComponent(ref, Player.getComponentType()); + return playerComponent != null ? playerComponent.isWaitingForClientReady() : false; + } + + @Deprecated(forRemoval = true) + public void setHasRemoteClient(boolean hasRemoteClient) { + this.hasRemoteClient = hasRemoteClient; + } + + @Deprecated + public void copyFrom(@Nonnull InteractionManager interactionManager) { + this.chains.putAll(interactionManager.chains); + } + + public void tick(@Nonnull Ref ref, @Nonnull CommandBuffer commandBuffer, float dt) { + this.currentTime = this.currentTime + commandBuffer.getExternalData().getWorld().getTickStepNanos(); + this.commandBuffer = commandBuffer; + this.clearAllGlobalTimeShift(dt); + this.cooldownHandler.tick(dt); + + for (InteractionChain interactionChain : this.chainStartQueue) { + this.executeChain0(ref, interactionChain); + } + + this.chainStartQueue.clear(); + Deque packetQueue = null; + if (this.playerRef != null) { + packetQueue = ((GamePacketHandler) this.playerRef.getPacketHandler()).getInteractionPacketQueue(); + } + + if (packetQueue != null && !packetQueue.isEmpty()) { + for (boolean first = true; this.tryConsumePacketQueue(ref, packetQueue) || first; first = false) { + if (!this.chains.isEmpty()) { + this.chains.values().removeIf(this.cachedTickChain); + } + + float cooldownDt = 0.0F; + + for (float shift : this.globalTimeShift) { + cooldownDt = Math.max(cooldownDt, shift); + } + + if (cooldownDt > 0.0F) { + this.cooldownHandler.tick(cooldownDt); + } + } + + this.commandBuffer = null; + } else { + if (!this.chains.isEmpty()) { + this.chains.values().removeIf(this.cachedTickChain); + } + + this.commandBuffer = null; + } + } + + private boolean tryConsumePacketQueue(@Nonnull Ref ref, @Nonnull Deque packetQueue) { + Iterator it = packetQueue.iterator(); + boolean finished = false; + boolean desynced = false; + int highestChainId = -1; + boolean changed = false; + + label99: + while (it.hasNext()) { + SyncInteractionChain packet = it.next(); + if (packet.desync) { + HytaleLogger.Api context = LOGGER.at(Level.FINE); + if (context.isEnabled()) { + context.log("Client packet flagged as desync"); + } + + desynced = true; + } + + InteractionChain chain = this.chains.get(packet.chainId); + if (chain != null && packet.forkedId != null) { + for (ForkedChainId id = packet.forkedId; id != null; id = id.forkedId) { + InteractionChain subChain = chain.getForkedChain(id); + if (subChain == null) { + InteractionChain.TempChain tempChain = chain.getTempForkedChain(id); + if (tempChain != null) { + tempChain.setBaseForkedChainId(id); + ForkedChainId lastId = id; + + for (ForkedChainId var17 = id.forkedId; var17 != null; var17 = var17.forkedId) { + tempChain = tempChain.getOrCreateTempForkedChain(var17); + tempChain.setBaseForkedChainId(var17); + lastId = var17; + } + + tempChain.setForkedChainId(packet.forkedId); + tempChain.setBaseForkedChainId(lastId); + tempChain.setChainData(packet.data); + this.sync(ref, tempChain, packet); + changed = true; + it.remove(); + this.packetQueueTime = 0L; + } + continue label99; + } + + chain = subChain; + } + } + + highestChainId = Math.max(highestChainId, packet.chainId); + if (chain == null && !finished) { + if (this.syncStart(ref, packet)) { + changed = true; + it.remove(); + this.packetQueueTime = 0L; + } else { + if (!this.waitingForClient(ref)) { + long queuedTime; + if (this.packetQueueTime == 0L) { + this.packetQueueTime = this.currentTime; + queuedTime = 0L; + } else { + queuedTime = this.currentTime - this.packetQueueTime; + } + + HytaleLogger.Api context = LOGGER.at(Level.FINE); + if (context.isEnabled()) { + context.log("Queued chain %d for %s", packet.chainId, FormatUtil.nanosToString(queuedTime)); + } + + if (queuedTime > TimeUnit.MILLISECONDS.toNanos(this.getOperationTimeoutThreshold())) { + this.sendCancelPacket(packet.chainId, packet.forkedId); + it.remove(); + context = LOGGER.at(Level.FINE); + if (context.isEnabled()) { + context.log("Discarding packet due to queuing for too long: %s", packet); + } + } + } + + if (!desynced) { + finished = true; + } + } + } else if (chain != null) { + this.sync(ref, chain, packet); + changed = true; + it.remove(); + this.packetQueueTime = 0L; + } else if (desynced) { + this.sendCancelPacket(packet.chainId, packet.forkedId); + it.remove(); + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + ctx.log("Discarding packet due to desync: %s", packet); + } + } + + if (desynced && !packetQueue.isEmpty()) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Discarding previous packets in queue: (before) %d", packetQueue.size()); + } + + packetQueue.removeIf(v -> { + boolean shouldRemove = this.getChain(v.chainId, v.forkedId) == null && UUIDUtil.isEmptyOrNull(v.data.proxyId) && v.initial; + if (shouldRemove) { + HytaleLogger.Api ctx1 = LOGGER.at(Level.FINE); + if (ctx1.isEnabled()) { + ctx1.log("Discarding: %s", v); + } + + this.sendCancelPacket(v.chainId, v.forkedId); + } + + return shouldRemove; + }); + ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Discarded previous packets in queue: (after) %d", packetQueue.size()); + } + } + + return changed; + } + + @Nullable + private InteractionChain getChain(int chainId, @Nullable ForkedChainId forkedChainId) { + InteractionChain chain = this.chains.get(chainId); + if (chain != null && forkedChainId != null) { + for (ForkedChainId id = forkedChainId; id != null; id = id.forkedId) { + InteractionChain subChain = chain.getForkedChain(id); + if (subChain == null) { + return null; + } + + chain = subChain; + } + } + + return chain; + } + + private boolean tickChain(@Nonnull InteractionChain chain) { + // HyFix: Validate chain context and owningEntity before ticking + // Prevents NPE crash in TickInteractionManagerSystem when chains have null context + // or invalid owningEntity refs, which can kick players from the server + InteractionContext context = chain.getContext(); + if (context == null) { + LOGGER.at(Level.WARNING).log("Removing chain with null context to prevent crash"); + return true; // Remove this chain + } + Ref owningEntity = context.getOwningEntity(); + if (owningEntity == null || !owningEntity.isValid()) { + LOGGER.at(Level.WARNING).log("Removing chain with null/invalid owningEntity to prevent crash"); + return true; // Remove this chain + } + + if (chain.wasPreTicked()) { + chain.setPreTicked(false); + return false; + } else { + if (!this.hasRemoteClient) { + chain.updateSimulatedState(); + } + + chain.getForkedChains().values().removeIf(this.cachedTickChain); + Ref ref = this.entity.getReference(); + + assert ref != null; + + if (chain.getServerState() != InteractionState.NotFinished) { + if (chain.requiresClient() && chain.getClientState() == InteractionState.NotFinished) { + if (!this.waitingForClient(ref)) { + if (chain.getWaitingForClientFinished() == 0L) { + chain.setWaitingForClientFinished(this.currentTime); + } + + long waitMillis = TimeUnit.NANOSECONDS.toMillis(this.currentTime - chain.getWaitingForClientFinished()); + HytaleLogger.Api context2 = LOGGER.at(Level.FINE); + if (context2.isEnabled()) { + context2.log("Server finished chain but client hasn't! %d, %s, %s", chain.getChainId(), chain, waitMillis); + } + + long threshold = this.getOperationTimeoutThreshold(); + TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType()); + if (timeResource.getTimeDilationModifier() == 1.0F && waitMillis > threshold) { + this.sendCancelPacket(chain); + return chain.getForkedChains().isEmpty(); + } + } + + return false; + } else { + LOGGER.at(Level.FINE).log("Remove Chain: %d, %s", chain.getChainId(), chain); + this.handleCancelledChain(ref, chain); + chain.onCompletion(this.cooldownHandler, this.hasRemoteClient); + return chain.getForkedChains().isEmpty(); + } + } else { + int baseOpIndex = chain.getOperationIndex(); + + try { + this.doTickChain(ref, chain); + } catch (InteractionManager.ChainCancelledException var9) { + chain.setServerState(var9.state); + chain.setClientState(var9.state); + chain.updateServerState(); + if (!this.hasRemoteClient) { + chain.updateSimulatedState(); + } + + if (chain.requiresClient()) { + this.sendSyncPacket(chain, baseOpIndex, this.tempSyncDataList); + this.sendCancelPacket(chain); + } + } + + if (chain.getServerState() != InteractionState.NotFinished) { + HytaleLogger.Api contextx = LOGGER.at(Level.FINE); + if (contextx.isEnabled()) { + contextx.log("Server finished chain: %d-%s, %s in %fs", chain.getChainId(), chain.getForkedChainId(), chain, chain.getTimeInSeconds()); + } + + if (!chain.requiresClient() || chain.getClientState() != InteractionState.NotFinished) { + contextx = LOGGER.at(Level.FINE); + if (contextx.isEnabled()) { + contextx.log("Remove Chain: %d-%s, %s", chain.getChainId(), chain.getForkedChainId(), chain); + } + + this.handleCancelledChain(ref, chain); + chain.onCompletion(this.cooldownHandler, this.hasRemoteClient); + return chain.getForkedChains().isEmpty(); + } + } else if (chain.getClientState() != InteractionState.NotFinished && !this.waitingForClient(ref)) { + if (chain.getWaitingForServerFinished() == 0L) { + chain.setWaitingForServerFinished(this.currentTime); + } + + long waitMillisx = TimeUnit.NANOSECONDS.toMillis(this.currentTime - chain.getWaitingForServerFinished()); + HytaleLogger.Api contextxx = LOGGER.at(Level.FINE); + if (contextxx.isEnabled()) { + contextxx.log("Client finished chain but server hasn't! %d, %s, %s", chain.getChainId(), chain, waitMillisx); + } + + long threshold = this.getOperationTimeoutThreshold(); + if (waitMillisx > threshold) { + LOGGER.at(Level.SEVERE).log("Client finished chain earlier than server! %d, %s", chain.getChainId(), chain); + } + } + + return false; + } + } + } + + private void handleCancelledChain(@Nonnull Ref ref, @Nonnull InteractionChain chain) { + assert this.commandBuffer != null; + + RootInteraction root = chain.getRootInteraction(); + int maxOperations = root.getOperationMax(); + if (chain.getOperationCounter() < maxOperations) { + InteractionEntry entry = chain.getInteraction(chain.getOperationIndex()); + if (entry != null) { + Operation operation = root.getOperation(chain.getOperationCounter()); + if (operation == null) { + throw new IllegalStateException("Failed to find operation during simulation tick of chain '" + root.getId() + "'"); + } else { + InteractionContext context = chain.getContext(); + entry.getServerState().state = InteractionState.Failed; + if (entry.getClientState() != null) { + entry.getClientState().state = InteractionState.Failed; + } + + try { + context.initEntry(chain, entry, this.entity); + TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType()); + operation.handle(ref, false, entry.getTimeInSeconds(this.currentTime) * timeResource.getTimeDilationModifier(), chain.getType(), context); + } finally { + context.deinitEntry(chain, entry, this.entity); + } + + chain.setOperationCounter(maxOperations); + } + } + } + } + + private void doTickChain(@Nonnull Ref ref, @Nonnull InteractionChain chain) { + ObjectList interactionData = this.tempSyncDataList; + interactionData.clear(); + RootInteraction root = chain.getRootInteraction(); + int maxOperations = root.getOperationMax(); + int currentOp = chain.getOperationCounter(); + int baseOpIndex = chain.getOperationIndex(); + int callDepth = chain.getCallDepth(); + if (chain.consumeFirstRun()) { + if (chain.getForkedChainId() == null) { + chain.setTimeShift(this.getGlobalTimeShift(chain.getType())); + } else { + InteractionChain parent = this.chains.get(chain.getChainId()); + chain.setFirstRun(parent != null && parent.isFirstRun()); + } + } else { + chain.setTimeShift(0.0F); + } + + if (!chain.getContext().getEntity().isValid()) { + throw new InteractionManager.ChainCancelledException(chain.getServerState()); + } else { + while (true) { + Operation simOp = !this.hasRemoteClient ? root.getOperation(chain.getSimulatedOperationCounter()) : null; + WaitForDataFrom simWaitFrom = simOp != null ? simOp.getWaitForDataFrom() : null; + long tickTime = this.currentTime; + if (!this.hasRemoteClient && simWaitFrom != WaitForDataFrom.Server) { + this.simulationTick(ref, chain, tickTime); + } + + interactionData.add(this.serverTick(ref, chain, tickTime)); + if (!chain.getContext().getEntity().isValid() + && chain.getServerState() != InteractionState.Finished + && chain.getServerState() != InteractionState.Failed) { + throw new InteractionManager.ChainCancelledException(chain.getServerState()); + } + + if (!this.hasRemoteClient && simWaitFrom == WaitForDataFrom.Server) { + this.simulationTick(ref, chain, tickTime); + } + + if (!this.hasRemoteClient) { + if (chain.getRootInteraction() != chain.getSimulatedRootInteraction()) { + throw new IllegalStateException( + "Simulation and server tick are not in sync (root interaction).\n" + + chain.getRootInteraction().getId() + + " vs " + + chain.getSimulatedRootInteraction() + ); + } + + if (chain.getOperationCounter() != chain.getSimulatedOperationCounter()) { + throw new IllegalStateException( + "Simulation and server tick are not in sync (operation position).\nRoot: " + + chain.getRootInteraction().getId() + + "\nCounter: " + + chain.getOperationCounter() + + " vs " + + chain.getSimulatedOperationCounter() + + "\nIndex: " + + chain.getOperationIndex() + ); + } + } + + if (callDepth != chain.getCallDepth()) { + callDepth = chain.getCallDepth(); + root = chain.getRootInteraction(); + maxOperations = root.getOperationMax(); + } else if (currentOp == chain.getOperationCounter()) { + break; + } + + chain.nextOperationIndex(); + currentOp = chain.getOperationCounter(); + if (currentOp >= maxOperations) { + while (callDepth > 0) { + chain.popRoot(); + callDepth = chain.getCallDepth(); + currentOp = chain.getOperationCounter(); + root = chain.getRootInteraction(); + maxOperations = root.getOperationMax(); + if (currentOp < maxOperations || callDepth == 0) { + break; + } + } + + if (callDepth == 0 && currentOp >= maxOperations) { + break; + } + } + } + + chain.updateServerState(); + if (!this.hasRemoteClient) { + chain.updateSimulatedState(); + } + + if (chain.requiresClient()) { + this.sendSyncPacket(chain, baseOpIndex, interactionData); + } + } + } + + @Nullable + private InteractionSyncData serverTick(@Nonnull Ref ref, @Nonnull InteractionChain chain, long tickTime) { + assert this.commandBuffer != null; + + RootInteraction root = chain.getRootInteraction(); + Operation operation = root.getOperation(chain.getOperationCounter()); + + assert operation != null; + + InteractionEntry entry = chain.getOrCreateInteractionEntry(chain.getOperationIndex()); + InteractionSyncData returnData = null; + boolean wasWrong = entry.consumeDesyncFlag(); + if (entry.getClientState() == null) { + wasWrong |= !entry.setClientState(chain.removeInteractionSyncData(chain.getOperationIndex())); + } + + if (wasWrong) { + returnData = entry.getServerState(); + chain.flagDesync(); + chain.clearInteractionSyncData(chain.getOperationIndex()); + } + + TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType()); + float tickTimeDilation = timeResource.getTimeDilationModifier(); + if (operation.getWaitForDataFrom() != WaitForDataFrom.Client || entry.getClientState() != null) { + int serverDataHashCode = entry.getServerDataHashCode(); + InteractionContext context = chain.getContext(); + float time = entry.getTimeInSeconds(tickTime); + boolean firstRun = false; + if (entry.getTimestamp() == 0L) { + time = chain.getTimeShift(); + entry.setTimestamp(tickTime, time); + firstRun = true; + } + + time *= tickTimeDilation; + + try { + context.initEntry(chain, entry, this.entity); + operation.tick(ref, this.entity, firstRun, time, chain.getType(), context, this.cooldownHandler); + } finally { + context.deinitEntry(chain, entry, this.entity); + } + + InteractionSyncData serverData = entry.getServerState(); + if (firstRun || serverDataHashCode != entry.getServerDataHashCode()) { + returnData = serverData; + } + + try { + context.initEntry(chain, entry, this.entity); + operation.handle(ref, firstRun, time, chain.getType(), context); + } finally { + context.deinitEntry(chain, entry, this.entity); + } + + this.removeInteractionIfFinished(ref, chain, entry); + return returnData; + } else if (this.waitingForClient(ref)) { + return null; + } else { + if (entry.getWaitingForSyncData() == 0L) { + entry.setWaitingForSyncData(this.currentTime); + } + + long waitMillis = TimeUnit.NANOSECONDS.toMillis(this.currentTime - entry.getWaitingForSyncData()); + HytaleLogger.Api contextx = LOGGER.at(Level.FINE); + if (contextx.isEnabled()) { + contextx.log("Wait for interaction clientData: %d, %s, %s", chain.getOperationIndex(), entry, waitMillis); + } + + long threshold = this.getOperationTimeoutThreshold(); + if (tickTimeDilation == 1.0F && waitMillis > threshold) { + SentryEvent event = new SentryEvent(); + event.setLevel(SentryLevel.ERROR); + Message message = new Message(); + message.setMessage("Client failed to send client data, ending early to prevent desync"); + HashMap unknown = new HashMap<>(); + unknown.put("Threshold", threshold); + unknown.put("Wait Millis", waitMillis); + unknown.put("Current Root", chain.getRootInteraction() != null ? chain.getRootInteraction().getId() : ""); + Operation innerOp = operation.getInnerOperation(); + unknown.put("Current Op", innerOp.getClass().getName()); + if (innerOp instanceof Interaction interaction) { + unknown.put("Current Interaction", interaction.getId()); + } + + unknown.put("Current Index", chain.getOperationIndex()); + unknown.put("Current Op Counter", chain.getOperationCounter()); + HistoricMetric metric = ref.getStore().getExternalData().getWorld().getBufferedTickLengthMetricSet(); + long[] periods = metric.getPeriodsNanos(); + + for (int i = 0; i < periods.length; i++) { + String length = FormatUtil.timeUnitToString(periods[i], TimeUnit.NANOSECONDS, true); + double average = metric.getAverage(i); + long min = metric.calculateMin(i); + long max = metric.calculateMax(i); + String value = FormatUtil.simpleTimeUnitFormat(min, average, max, TimeUnit.NANOSECONDS, TimeUnit.MILLISECONDS, 3); + unknown.put(String.format("World Perf %s", length), value); + } + + event.setExtras(unknown); + event.setMessage(message); + SentryId eventId = Sentry.captureEvent(event); + LOGGER.atWarning().log("Client failed to send client data, ending early to prevent desync. %s", eventId); + chain.setServerState(InteractionState.Failed); + chain.setClientState(InteractionState.Failed); + this.sendCancelPacket(chain); + return null; + } else { + if (entry.consumeSendInitial() || wasWrong) { + returnData = entry.getServerState(); + } + + return returnData; + } + } + } + + private void removeInteractionIfFinished(@Nonnull Ref ref, @Nonnull InteractionChain chain, @Nonnull InteractionEntry entry) { + if (chain.getOperationIndex() == entry.getIndex() && entry.getServerState().state != InteractionState.NotFinished) { + chain.setFinalState(entry.getServerState().state); + } + + if (entry.getServerState().state != InteractionState.NotFinished) { + LOGGER.at(Level.FINE).log("Server finished interaction: %d, %s", entry.getIndex(), entry); + if (!chain.requiresClient() || entry.getClientState() != null && entry.getClientState().state != InteractionState.NotFinished) { + LOGGER.at(Level.FINER).log("Remove Interaction: %d, %s", entry.getIndex(), entry); + chain.removeInteractionEntry(this, entry.getIndex()); + } + } else if (entry.getClientState() != null && entry.getClientState().state != InteractionState.NotFinished && !this.waitingForClient(ref)) { + if (entry.getWaitingForServerFinished() == 0L) { + entry.setWaitingForServerFinished(this.currentTime); + } + + long waitMillis = TimeUnit.NANOSECONDS.toMillis(this.currentTime - entry.getWaitingForServerFinished()); + HytaleLogger.Api context = LOGGER.at(Level.FINE); + if (context.isEnabled()) { + context.log("Client finished interaction but server hasn't! %s, %d, %s, %s", entry.getClientState().state, entry.getIndex(), entry, waitMillis); + } + + long threshold = this.getOperationTimeoutThreshold(); + if (waitMillis > threshold) { + HytaleLogger.Api ctx = LOGGER.at(Level.SEVERE); + if (ctx.isEnabled()) { + ctx.log("Client finished interaction earlier than server! %d, %s", entry.getIndex(), entry); + } + } + } + } + + private void simulationTick(@Nonnull Ref ref, @Nonnull InteractionChain chain, long tickTime) { + assert this.commandBuffer != null; + + RootInteraction rootInteraction = chain.getRootInteraction(); + Operation operation = rootInteraction.getOperation(chain.getSimulatedOperationCounter()); + if (operation == null) { + throw new IllegalStateException("Failed to find operation during simulation tick of chain '" + rootInteraction.getId() + "'"); + } else { + InteractionEntry entry = chain.getOrCreateInteractionEntry(chain.getClientOperationIndex()); + InteractionContext context = chain.getContext(); + entry.setUseSimulationState(true); + + try { + context.initEntry(chain, entry, this.entity); + float time = entry.getTimeInSeconds(tickTime); + boolean firstRun = false; + if (entry.getTimestamp() == 0L) { + time = chain.getTimeShift(); + entry.setTimestamp(tickTime, time); + firstRun = true; + } + + TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType()); + float tickTimeDilation = timeResource.getTimeDilationModifier(); + time *= tickTimeDilation; + operation.simulateTick(ref, this.entity, firstRun, time, chain.getType(), context, this.cooldownHandler); + } finally { + context.deinitEntry(chain, entry, this.entity); + entry.setUseSimulationState(false); + } + + if (!entry.setClientState(entry.getSimulationState())) { + throw new RuntimeException("Simulation failed"); + } else { + this.removeInteractionIfFinished(ref, chain, entry); + } + } + } + + private boolean syncStart(@Nonnull Ref ref, @Nonnull SyncInteractionChain packet) { + assert this.commandBuffer != null; + + int index = packet.chainId; + if (!packet.initial) { + if (packet.forkedId == null) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Got syncStart for %d-%s but packet wasn't the first.", index, packet.forkedId); + } + } + + return true; + } else if (packet.forkedId != null) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Can't start a forked chain from the client: %d %s", index, packet.forkedId); + } + + return true; + } else { + InteractionType type = packet.interactionType; + if (index <= 0) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Invalid client chainId! Got %d but client id's should be > 0", index); + } + + this.sendCancelPacket(index, packet.forkedId); + return true; + } else if (index <= this.lastClientChainId) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Invalid client chainId! The last clientChainId was %d but just got %d", this.lastClientChainId, index); + } + + this.sendCancelPacket(index, packet.forkedId); + return true; + } else { + UUID proxyId = packet.data.proxyId; + InteractionContext context; + if (!UUIDUtil.isEmptyOrNull(proxyId)) { + World world = this.commandBuffer.getExternalData().getWorld(); + Ref proxyTarget = world.getEntityStore().getRefFromUUID(proxyId); + if (proxyTarget == null) { + if (this.packetQueueTime != 0L + && this.currentTime - this.packetQueueTime > TimeUnit.MILLISECONDS.toNanos(this.getOperationTimeoutThreshold()) / 2L) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Proxy entity never spawned"); + } + + this.sendCancelPacket(index, packet.forkedId); + return true; + } + + return false; + } + + context = InteractionContext.forProxyEntity(this, this.entity, proxyTarget); + } else { + context = InteractionContext.forInteraction(this, ref, type, packet.equipSlot, this.commandBuffer); + } + + String rootInteractionId = context.getRootInteractionId(type); + if (rootInteractionId == null) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log("Missing root interaction: %d, %s, %s", index, this.entity.getInventory().getItemInHand(), type); + } + + this.sendCancelPacket(index, packet.forkedId); + return true; + } else { + RootInteraction rootInteraction = RootInteraction.getRootInteractionOrUnknown(rootInteractionId); + if (rootInteraction == null) { + return false; + } else if (!this.applyRules(context, packet.data, type, rootInteraction)) { + return false; + } else { + Inventory entityInventory = this.entity.getInventory(); + ItemStack itemInHand = entityInventory.getActiveHotbarItem(); + ItemStack utilityItem = entityInventory.getUtilityItem(); + String serverItemInHandId = itemInHand != null ? itemInHand.getItemId() : null; + String serverUtilityItemId = utilityItem != null ? utilityItem.getItemId() : null; + if (packet.activeHotbarSlot != entityInventory.getActiveHotbarSlot()) { + HytaleLogger.Api ctx = LOGGER.at(Level.FINE); + if (ctx.isEnabled()) { + ctx.log( + "Active slot miss match: %d, %d != %d, %s, %s, %s", + index, + entityInventory.getActiveHotbarSlot(), + packet.activeHotbarSlot, + serverItemInHandId, + packet.itemInHandId, + type + ); + } + + this.sendCancelPacket(index, packet.forkedId); + if (this.playerRef != null) { + this.playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-1, entityInventory.getActiveHotbarSlot())); + } + + return true; + } else if (packet.activeUtilitySlot != entityInventory.getActiveUtilitySlot()) { + HytaleLogger.Api ctxx = LOGGER.at(Level.FINE); + if (ctxx.isEnabled()) { + ctxx.log( + "Active slot miss match: %d, %d != %d, %s, %s, %s", + index, + entityInventory.getActiveUtilitySlot(), + packet.activeUtilitySlot, + serverItemInHandId, + packet.itemInHandId, + type + ); + } + + this.sendCancelPacket(index, packet.forkedId); + if (this.playerRef != null) { + this.playerRef.getPacketHandler().writeNoCache(new SetActiveSlot(-5, entityInventory.getActiveUtilitySlot())); + } + + return true; + } else if (!Objects.equals(serverItemInHandId, packet.itemInHandId)) { + HytaleLogger.Api ctxxx = LOGGER.at(Level.FINE); + if (ctxxx.isEnabled()) { + ctxxx.log("ItemInHand miss match: %d, %s, %s, %s", index, serverItemInHandId, packet.itemInHandId, type); + } + + this.sendCancelPacket(index, packet.forkedId); + return true; + } else if (!Objects.equals(serverUtilityItemId, packet.utilityItemId)) { + HytaleLogger.Api ctxxx = LOGGER.at(Level.FINE); + if (ctxxx.isEnabled()) { + ctxxx.log("UtilityItem miss match: %d, %s, %s, %s", index, serverUtilityItemId, packet.utilityItemId, type); + } + + this.sendCancelPacket(index, packet.forkedId); + return true; + } else if (this.isOnCooldown(ref, type, rootInteraction, true)) { + return false; + } else { + InteractionChain chain = this.initChain(packet.data, type, context, rootInteraction, null, true); + chain.setChainId(index); + this.sync(ref, chain, packet); + World world = this.commandBuffer.getExternalData().getWorld(); + if (packet.data.blockPosition != null) { + BlockPosition targetBlock = world.getBaseBlock(packet.data.blockPosition); + context.getMetaStore().putMetaObject(Interaction.TARGET_BLOCK, targetBlock); + context.getMetaStore().putMetaObject(Interaction.TARGET_BLOCK_RAW, packet.data.blockPosition); + if (!packet.data.blockPosition.equals(targetBlock)) { + WorldChunk otherChunk = world.getChunkIfInMemory( + ChunkUtil.indexChunkFromBlock(packet.data.blockPosition.x, packet.data.blockPosition.z) + ); + if (otherChunk == null) { + HytaleLogger.Api ctxxx = LOGGER.at(Level.FINE); + if (ctxxx.isEnabled()) { + ctxxx.log("Unloaded chunk interacted with: %d, %s", index, type); + } + + this.sendCancelPacket(index, packet.forkedId); + return true; + } + + int blockId = world.getBlock(targetBlock.x, targetBlock.y, targetBlock.z); + int otherBlockId = world.getBlock(packet.data.blockPosition.x, packet.data.blockPosition.y, packet.data.blockPosition.z); + if (blockId != otherBlockId) { + otherChunk.setBlock( + packet.data.blockPosition.x, packet.data.blockPosition.y, packet.data.blockPosition.z, 0, BlockType.EMPTY, 0, 0, 1052 + ); + } + } + } + + if (packet.data.entityId >= 0) { + EntityStore entityComponentStore = world.getEntityStore(); + Ref entityReference = entityComponentStore.getRefFromNetworkId(packet.data.entityId); + if (entityReference != null) { + context.getMetaStore().putMetaObject(Interaction.TARGET_ENTITY, entityReference); + } + } + + if (packet.data.targetSlot != Integer.MIN_VALUE) { + context.getMetaStore().putMetaObject(Interaction.TARGET_SLOT, packet.data.targetSlot); + } + + if (packet.data.hitLocation != null) { + Vector3f hit = packet.data.hitLocation; + context.getMetaStore().putMetaObject(Interaction.HIT_LOCATION, new Vector4d(hit.x, hit.y, hit.z, 1.0)); + } + + if (packet.data.hitDetail != null) { + context.getMetaStore().putMetaObject(Interaction.HIT_DETAIL, packet.data.hitDetail); + } + + this.lastClientChainId = index; + if (!this.tickChain(chain)) { + chain.setPreTicked(true); + this.chains.put(index, chain); + } + + return true; + } + } + } + } + } + } + + public void sync(@Nonnull Ref ref, @Nonnull ChainSyncStorage chainSyncStorage, @Nonnull SyncInteractionChain packet) { + assert this.commandBuffer != null; + + if (packet.newForks != null) { + for (SyncInteractionChain fork : packet.newForks) { + chainSyncStorage.syncFork(ref, this, fork); + } + } + + if (packet.interactionData == null) { + chainSyncStorage.setClientState(packet.state); + } else { + for (int i = 0; i < packet.interactionData.length; i++) { + InteractionSyncData syncData = packet.interactionData[i]; + if (syncData != null) { + int index = packet.operationBaseIndex + i; + if (!chainSyncStorage.isSyncDataOutOfOrder(index)) { + InteractionEntry interaction = chainSyncStorage.getInteraction(index); + if (interaction != null && chainSyncStorage instanceof InteractionChain interactionChain) { + if (interaction.getClientState() != null + && interaction.getClientState().state != InteractionState.NotFinished + && syncData.state == InteractionState.NotFinished + || !interaction.setClientState(syncData)) { + chainSyncStorage.clearInteractionSyncData(index); + interaction.flagDesync(); + interactionChain.flagDesync(); + return; + } + + chainSyncStorage.updateSyncPosition(index); + HytaleLogger.Api context = LOGGER.at(Level.FINEST); + if (context.isEnabled()) { + TimeResource timeResource = this.commandBuffer.getResource(TimeResource.getResourceType()); + float tickTimeDilation = timeResource.getTimeDilationModifier(); + context.log( + "%d, %d: Time (Sync) - Server: %s vs Client: %s", + packet.chainId, + index, + interaction.getTimeInSeconds(this.currentTime) * tickTimeDilation, + interaction.getClientState().progress + ); + } + + this.removeInteractionIfFinished(ref, interactionChain, interaction); + } else { + chainSyncStorage.putInteractionSyncData(index, syncData); + } + } + } + } + + int last = packet.operationBaseIndex + packet.interactionData.length; + chainSyncStorage.clearInteractionSyncData(last); + chainSyncStorage.setClientState(packet.state); + } + } + + public boolean canRun(@Nonnull InteractionType type, @Nonnull RootInteraction rootInteraction) { + return this.canRun(type, (short) -1, rootInteraction); + } + + public boolean canRun(@Nonnull InteractionType type, short equipSlot, @Nonnull RootInteraction rootInteraction) { + return applyRules(null, type, equipSlot, rootInteraction, this.chains, null); + } + + public boolean applyRules( + @Nonnull InteractionContext context, @Nonnull InteractionChainData data, @Nonnull InteractionType type, @Nonnull RootInteraction rootInteraction + ) { + List chainsToCancel = new ObjectArrayList<>(); + if (!applyRules(data, type, context.getHeldItemSlot(), rootInteraction, this.chains, chainsToCancel)) { + return false; + } else { + for (InteractionChain interactionChain : chainsToCancel) { + this.cancelChains(interactionChain); + } + + return true; + } + } + + public void cancelChains(@Nonnull InteractionChain chain) { + chain.setServerState(InteractionState.Failed); + chain.setClientState(InteractionState.Failed); + this.sendCancelPacket(chain); + + for (InteractionChain fork : chain.getForkedChains().values()) { + this.cancelChains(fork); + } + } + + private static boolean applyRules( + @Nullable InteractionChainData data, + @Nonnull InteractionType type, + int heldItemSlot, + @Nullable RootInteraction rootInteraction, + @Nonnull Map chains, + @Nullable List chainsToCancel + ) { + if (!chains.isEmpty() && rootInteraction != null) { + for (InteractionChain chain : chains.values()) { + if ((chain.getForkedChainId() == null || chain.isPredicted()) + && (data == null || Objects.equals(chain.getChainData().proxyId, data.proxyId)) + && (type != InteractionType.Equipped || chain.getType() != InteractionType.Equipped || chain.getContext().getHeldItemSlot() == heldItemSlot)) { + if (chain.getServerState() == InteractionState.NotFinished) { + RootInteraction currentRoot = chain.getRootInteraction(); + Operation currentOp = currentRoot.getOperation(chain.getOperationCounter()); + if (rootInteraction.getRules() + .validateInterrupts(type, rootInteraction.getData().getTags(), chain.getType(), currentRoot.getData().getTags(), currentRoot.getRules())) { + if (chainsToCancel != null) { + chainsToCancel.add(chain); + } + } else if (currentOp != null + && currentOp.getRules() != null + && rootInteraction.getRules() + .validateInterrupts(type, rootInteraction.getData().getTags(), chain.getType(), currentOp.getTags(), currentOp.getRules())) { + if (chainsToCancel != null) { + chainsToCancel.add(chain); + } + } else { + if (rootInteraction.getRules() + .validateBlocked(type, rootInteraction.getData().getTags(), chain.getType(), currentRoot.getData().getTags(), currentRoot.getRules())) { + return false; + } + + if (currentOp != null + && currentOp.getRules() != null + && rootInteraction.getRules() + .validateBlocked(type, rootInteraction.getData().getTags(), chain.getType(), currentOp.getTags(), currentOp.getRules())) { + return false; + } + } + } + + if ((chainsToCancel == null || chainsToCancel.isEmpty()) + && !applyRules(data, type, heldItemSlot, rootInteraction, chain.getForkedChains(), chainsToCancel)) { + return false; + } + } + } + + return true; + } else { + return true; + } + } + + public boolean tryStartChain( + @Nonnull Ref ref, + @Nonnull CommandBuffer commandBuffer, + @Nonnull InteractionType type, + @Nonnull InteractionContext context, + @Nonnull RootInteraction rootInteraction + ) { + InteractionChain chain = this.initChain(type, context, rootInteraction, false); + if (!this.applyRules(context, chain.getChainData(), type, rootInteraction)) { + return false; + } else { + this.executeChain(ref, commandBuffer, chain); + return true; + } + } + + public void startChain( + @Nonnull Ref ref, + @Nonnull CommandBuffer commandBuffer, + @Nonnull InteractionType type, + @Nonnull InteractionContext context, + @Nonnull RootInteraction rootInteraction + ) { + InteractionChain chain = this.initChain(type, context, rootInteraction, false); + this.executeChain(ref, commandBuffer, chain); + } + + @Nonnull + public InteractionChain initChain( + @Nonnull InteractionType type, @Nonnull InteractionContext context, @Nonnull RootInteraction rootInteraction, boolean forceRemoteSync + ) { + return this.initChain(type, context, rootInteraction, -1, null, forceRemoteSync); + } + + @Nonnull + public InteractionChain initChain( + @Nonnull InteractionType type, + @Nonnull InteractionContext context, + @Nonnull RootInteraction rootInteraction, + int entityId, + @Nullable BlockPosition blockPosition, + boolean forceRemoteSync + ) { + InteractionChainData data = new InteractionChainData(entityId, UUIDUtil.EMPTY_UUID, null, null, blockPosition, Integer.MIN_VALUE, null); + return this.initChain(data, type, context, rootInteraction, null, forceRemoteSync); + } + + @Nonnull + public InteractionChain initChain( + @Nonnull InteractionChainData data, + @Nonnull InteractionType type, + @Nonnull InteractionContext context, + @Nonnull RootInteraction rootInteraction, + @Nullable Runnable onCompletion, + boolean forceRemoteSync + ) { + return new InteractionChain(type, context, data, rootInteraction, onCompletion, forceRemoteSync || !this.hasRemoteClient); + } + + public void queueExecuteChain(@Nonnull InteractionChain chain) { + this.chainStartQueue.add(chain); + } + + public void executeChain(@Nonnull Ref ref, @Nonnull CommandBuffer commandBuffer, @Nonnull InteractionChain chain) { + this.commandBuffer = commandBuffer; + this.executeChain0(ref, chain); + this.commandBuffer = null; + } + + private void executeChain0(@Nonnull Ref ref, @Nonnull InteractionChain chain) { + if (this.isOnCooldown(ref, chain.getType(), chain.getInitialRootInteraction(), false)) { + chain.setServerState(InteractionState.Failed); + chain.setClientState(InteractionState.Failed); + } else { + int index = --this.lastServerChainId; + if (index >= 0) { + index = this.lastServerChainId = -1; + } + + chain.setChainId(index); + if (!this.tickChain(chain)) { + LOGGER.at(Level.FINE).log("Add Chain: %d, %s", index, chain); + chain.setPreTicked(true); + this.chains.put(index, chain); + } + } + } + + private boolean isOnCooldown(@Nonnull Ref ref, @Nonnull InteractionType type, @Nonnull RootInteraction root, boolean remote) { + assert this.commandBuffer != null; + + InteractionCooldown cooldown = root.getCooldown(); + String cooldownId = root.getId(); + float cooldownTime = InteractionTypeUtils.getDefaultCooldown(type); + float[] cooldownChargeTimes = DEFAULT_CHARGE_TIMES; + boolean interruptRecharge = false; + if (cooldown != null) { + cooldownTime = cooldown.cooldown; + if (cooldown.chargeTimes != null && cooldown.chargeTimes.length > 0) { + cooldownChargeTimes = cooldown.chargeTimes; + } + + if (cooldown.cooldownId != null) { + cooldownId = cooldown.cooldownId; + } + + if (cooldown.interruptRecharge) { + interruptRecharge = true; + } + + if (cooldown.clickBypass && remote) { + this.cooldownHandler.resetCooldown(cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge); + return false; + } + } + + Player playerComponent = this.commandBuffer.getComponent(ref, Player.getComponentType()); + GameMode gameMode = playerComponent != null ? playerComponent.getGameMode() : GameMode.Adventure; + RootInteractionSettings settings = root.getSettings().get(gameMode); + if (settings != null) { + cooldown = settings.cooldown; + if (cooldown != null) { + cooldownTime = cooldown.cooldown; + if (cooldown.chargeTimes != null && cooldown.chargeTimes.length > 0) { + cooldownChargeTimes = cooldown.chargeTimes; + } + + if (cooldown.cooldownId != null) { + cooldownId = cooldown.cooldownId; + } + + if (cooldown.interruptRecharge) { + interruptRecharge = true; + } + + if (cooldown.clickBypass && remote) { + this.cooldownHandler.resetCooldown(cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge); + return false; + } + } + + if (settings.allowSkipChainOnClick && remote) { + this.cooldownHandler.resetCooldown(cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge); + return false; + } + } + + return this.cooldownHandler.isOnCooldown(root, cooldownId, cooldownTime, cooldownChargeTimes, interruptRecharge); + } + + public void tryRunHeldInteraction(@Nonnull Ref ref, @Nonnull CommandBuffer commandBuffer, @Nonnull InteractionType type) { + this.tryRunHeldInteraction(ref, commandBuffer, type, (short) -1); + } + + public void tryRunHeldInteraction( + @Nonnull Ref ref, @Nonnull CommandBuffer commandBuffer, @Nonnull InteractionType type, short equipSlot + ) { + Inventory inventory = this.entity.getInventory(); + ItemStack itemStack; + + itemStack = switch (type) { + case Held -> inventory.getItemInHand(); + case HeldOffhand -> inventory.getUtilityItem(); + case Equipped -> { + if (equipSlot == -1) { + throw new IllegalArgumentException(); + } + yield inventory.getArmor().getItemStack(equipSlot); + } + default -> throw new IllegalArgumentException(); + }; + + if (itemStack != null && !itemStack.isEmpty()) { + String rootId = itemStack.getItem().getInteractions().get(type); + if (rootId == null) { + return; + } + RootInteraction root = RootInteraction.getAssetMap().getAsset(rootId); + if (root != null && this.canRun(type, equipSlot, root)) { + InteractionContext context = InteractionContext.forInteraction(this, ref, type, equipSlot, commandBuffer); + this.startChain(ref, commandBuffer, type, context, root); + } + } + } + + public void sendSyncPacket(@Nonnull InteractionChain chain, int operationBaseIndex, @Nullable List interactionData) { + if (!chain.hasSentInitial() || interactionData != null && !ListUtil.emptyOrAllNull(interactionData) || !chain.getNewForks().isEmpty()) { + if (this.playerRef != null) { + SyncInteractionChain packet = makeSyncPacket(chain, operationBaseIndex, interactionData); + this.syncPackets.add(packet); + } + } + } + + @Nonnull + private static SyncInteractionChain makeSyncPacket( + @Nonnull InteractionChain chain, int operationBaseIndex, @Nullable List interactionData + ) { + SyncInteractionChain[] forks = null; + List newForks = chain.getNewForks(); + if (!newForks.isEmpty()) { + forks = new SyncInteractionChain[newForks.size()]; + + for (int i = 0; i < newForks.size(); i++) { + InteractionChain fc = newForks.get(i); + forks[i] = makeSyncPacket(fc, 0, null); + } + + newForks.clear(); + } + + SyncInteractionChain packet = new SyncInteractionChain( + 0, + 0, + 0, + null, + null, + null, + !chain.hasSentInitial(), + false, + chain.hasSentInitial() ? Integer.MIN_VALUE : RootInteraction.getRootInteractionIdOrUnknown(chain.getInitialRootInteraction().getId()), + chain.getType(), + chain.getContext().getHeldItemSlot(), + chain.getChainId(), + chain.getForkedChainId(), + chain.getChainData(), + chain.getServerState(), + forks, + operationBaseIndex, + interactionData == null ? null : interactionData.toArray(InteractionSyncData[]::new) + ); + chain.setSentInitial(true); + return packet; + } + + private void sendCancelPacket(@Nonnull InteractionChain chain) { + this.sendCancelPacket(chain.getChainId(), chain.getForkedChainId()); + } + + public void sendCancelPacket(int chainId, @Nonnull ForkedChainId forkedChainId) { + if (this.playerRef != null) { + this.playerRef.getPacketHandler().writeNoCache(new CancelInteractionChain(chainId, forkedChainId)); + } + } + + public void clear() { + this.forEachInteraction((chain, _i, _a) -> { + chain.setServerState(InteractionState.Failed); + chain.setClientState(InteractionState.Failed); + this.sendCancelPacket(chain); + return null; + }, null); + this.chainStartQueue.clear(); + } + + public void clearAllGlobalTimeShift(float dt) { + if (this.timeShiftsDirty) { + boolean clearFlag = true; + + for (int i = 0; i < this.globalTimeShift.length; i++) { + if (!this.globalTimeShiftDirty[i]) { + this.globalTimeShift[i] = 0.0F; + } else { + clearFlag = false; + this.globalTimeShift[i] = this.globalTimeShift[i] + dt; + } + } + + Arrays.fill(this.globalTimeShiftDirty, false); + if (clearFlag) { + this.timeShiftsDirty = false; + } + } + } + + public void setGlobalTimeShift(@Nonnull InteractionType type, float shift) { + if (shift < 0.0F) { + throw new IllegalArgumentException("Can't shift backwards"); + } else { + this.globalTimeShift[type.ordinal()] = shift; + this.globalTimeShiftDirty[type.ordinal()] = true; + this.timeShiftsDirty = true; + } + } + + public float getGlobalTimeShift(@Nonnull InteractionType type) { + return this.globalTimeShift[type.ordinal()]; + } + + public T forEachInteraction(@Nonnull TriFunction func, @Nonnull T val) { + return forEachInteraction(this.chains, func, val); + } + + private static T forEachInteraction( + @Nonnull Map chains, @Nonnull TriFunction func, @Nonnull T val + ) { + if (chains.isEmpty()) { + return val; + } else { + for (InteractionChain chain : chains.values()) { + Operation operation = chain.getRootInteraction().getOperation(chain.getOperationCounter()); + if (operation != null && operation.getInnerOperation() instanceof Interaction interaction) { + val = func.apply(chain, interaction, val); + } + + val = forEachInteraction(chain.getForkedChains(), func, val); + } + + return val; + } + } + + public void walkChain( + @Nonnull Ref ref, @Nonnull Collector collector, @Nonnull InteractionType type, @Nonnull ComponentAccessor componentAccessor + ) { + this.walkChain(ref, collector, type, null, componentAccessor); + } + + public void walkChain( + @Nonnull Ref ref, + @Nonnull Collector collector, + @Nonnull InteractionType type, + @Nullable RootInteraction rootInteraction, + @Nonnull ComponentAccessor componentAccessor + ) { + walkChain(collector, type, InteractionContext.forInteraction(this, ref, type, componentAccessor), rootInteraction); + } + + public static void walkChain( + @Nonnull Collector collector, @Nonnull InteractionType type, @Nonnull InteractionContext context, @Nullable RootInteraction rootInteraction + ) { + if (rootInteraction == null) { + String rootInteractionId = context.getRootInteractionId(type); + if (rootInteractionId == null) { + throw new IllegalArgumentException("No interaction ID found for " + type + ", " + context); + } + + rootInteraction = RootInteraction.getAssetMap().getAsset(rootInteractionId); + } + + if (rootInteraction == null) { + throw new IllegalArgumentException("No interactions are defined for " + type + ", " + context); + } else { + collector.start(); + collector.into(context, null); + walkInteractions(collector, context, CollectorTag.ROOT, rootInteraction.getInteractionIds()); + collector.outof(); + collector.finished(); + } + } + + public static boolean walkInteractions( + @Nonnull Collector collector, @Nonnull InteractionContext context, @Nonnull CollectorTag tag, @Nonnull String[] interactionIds + ) { + for (String id : interactionIds) { + if (walkInteraction(collector, context, tag, id)) { + return true; + } + } + + return false; + } + + public static boolean walkInteraction(@Nonnull Collector collector, @Nonnull InteractionContext context, @Nonnull CollectorTag tag, @Nullable String id) { + if (id == null) { + return false; + } else { + Interaction interaction = Interaction.getAssetMap().getAsset(id); + if (interaction == null) { + throw new IllegalArgumentException("Failed to find interaction: " + id); + } else if (collector.collect(tag, context, interaction)) { + return true; + } else { + collector.into(context, interaction); + interaction.walk(collector, context); + collector.outof(); + return false; + } + } + } + + public ObjectList getSyncPackets() { + return this.syncPackets; + } + + @Nonnull + @Override + public Component clone() { + InteractionManager manager = new InteractionManager(this.entity, this.playerRef, this.interactionSimulationHandler); + manager.copyFrom(this); + return manager; + } + + public static class ChainCancelledException extends RuntimeException { + @Nonnull + private final InteractionState state; + + public ChainCancelledException(@Nonnull InteractionState state) { + this.state = state; + } + } +} diff --git a/src/com/hypixel/hytale/server/core/entity/LivingEntity.java b/src/com/hypixel/hytale/server/core/entity/LivingEntity.java new file mode 100644 index 00000000..754480bc --- /dev/null +++ b/src/com/hypixel/hytale/server/core/entity/LivingEntity.java @@ -0,0 +1,251 @@ +package com.hypixel.hytale.server.core.entity; + +import coldfusion.hytaleserver.InventoryOwnershipGuard; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.event.EventRegistration; +import com.hypixel.hytale.math.util.ChunkUtil; +import com.hypixel.hytale.math.util.MathUtil; +import com.hypixel.hytale.math.vector.Transform; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.protocol.BlockMaterial; +import com.hypixel.hytale.protocol.MovementStates; +import com.hypixel.hytale.server.core.asset.type.item.config.Item; +import com.hypixel.hytale.server.core.entity.movement.MovementStatesComponent; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.inventory.transaction.ItemStackSlotTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ItemStackTransaction; +import com.hypixel.hytale.server.core.inventory.transaction.ListTransaction; +import com.hypixel.hytale.server.core.modules.collision.WorldUtil; +import com.hypixel.hytale.server.core.modules.entity.BlockMigrationExtraInfo; +import com.hypixel.hytale.server.core.modules.entity.component.Invulnerable; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.universe.world.World; +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.TargetUtil; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; + +public abstract class LivingEntity extends Entity { + @Nonnull + public static final BuilderCodec CODEC = BuilderCodec.abstractBuilder(LivingEntity.class, Entity.CODEC) + .append(new KeyedCodec<>("Inventory", Inventory.CODEC), (livingEntity, inventory, extraInfo) -> { + livingEntity.setInventory(inventory); + if (extraInfo instanceof BlockMigrationExtraInfo) { + livingEntity.inventory.doMigration(((BlockMigrationExtraInfo) extraInfo).getBlockMigration()); + } + }, (livingEntity, extraInfo) -> livingEntity.inventory) + .add() + .afterDecode(livingEntity -> { + if (livingEntity.inventory == null) { + livingEntity.setInventory(livingEntity.createDefaultInventory()); + } + }) + .build(); + public static final int DEFAULT_ITEM_THROW_SPEED = 6; + @Nonnull + private final StatModifiersManager statModifiersManager = new StatModifiersManager(); + private Inventory inventory; + protected double currentFallDistance; + private EventRegistration armorInventoryChangeEventRegistration; + private boolean isEquipmentNetworkOutdated; + + public LivingEntity() { + this.setInventory(this.createDefaultInventory()); + } + + public LivingEntity(@Nonnull World world) { + super(world); + this.setInventory(this.createDefaultInventory()); + } + + protected abstract Inventory createDefaultInventory(); + + public boolean canBreathe( + @Nonnull Ref ref, @Nonnull BlockMaterial breathingMaterial, int fluidId, @Nonnull ComponentAccessor componentAccessor + ) { + boolean invulnerable = componentAccessor.getArchetype(ref).contains(Invulnerable.getComponentType()); + return invulnerable || breathingMaterial == BlockMaterial.Empty && fluidId == 0; + } + + public static long getPackedMaterialAndFluidAtBreathingHeight(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { + World world = componentAccessor.getExternalData().getWorld(); + Transform lookVec = TargetUtil.getLook(ref, componentAccessor); + Vector3d position = lookVec.getPosition(); + ChunkStore chunkStore = world.getChunkStore(); + long chunkIndex = ChunkUtil.indexChunkFromBlock(position.x, position.z); + Ref chunkRef = chunkStore.getChunkReference(chunkIndex); + return chunkRef != null && chunkRef.isValid() + ? WorldUtil.getPackedMaterialAndFluidAtPosition(chunkRef, chunkStore.getStore(), position.x, position.y, position.z) + : MathUtil.packLong(BlockMaterial.Empty.ordinal(), 0); + } + + public Inventory getInventory() { + return this.inventory; + } + + @Nonnull + public Inventory setInventory(Inventory inventory) { + // HyFix #45: Validate inventory ownership before assignment + inventory = (Inventory) InventoryOwnershipGuard.validateAndClone(this, inventory); + return this.setInventory(inventory, false); + } + + @Nonnull + public Inventory setInventory(Inventory inventory, boolean ensureCapacity) { + // HyFix #45: Validate inventory ownership before assignment + inventory = (Inventory) InventoryOwnershipGuard.validateAndClone(this, inventory); + List remainder = ensureCapacity ? new ObjectArrayList<>() : null; + inventory = this.setInventory(inventory, ensureCapacity, remainder); + if (remainder != null && !remainder.isEmpty()) { + ListTransaction transactionList = inventory.getCombinedHotbarFirst().addItemStacks(remainder); + + for (ItemStackTransaction var6 : transactionList.getList()) { + ; + } + } + + return inventory; + } + + @Nonnull + public Inventory setInventory(Inventory inventory, boolean ensureCapacity, List remainder) { + // HyFix #45: Validate inventory ownership before assignment + inventory = (Inventory) InventoryOwnershipGuard.validateAndClone(this, inventory); + + if (this.inventory != null) { + this.inventory.unregister(); + } + + if (this.armorInventoryChangeEventRegistration != null) { + this.armorInventoryChangeEventRegistration.unregister(); + } + + if (ensureCapacity) { + inventory = Inventory.ensureCapacity(inventory, remainder); + } + + inventory.setEntity(this); + this.armorInventoryChangeEventRegistration = inventory.getArmor().registerChangeEvent(event -> this.statModifiersManager.setRecalculate(true)); + this.inventory = inventory; + return inventory; + } + + @Override + public void moveTo(@Nonnull Ref ref, double locX, double locY, double locZ, @Nonnull ComponentAccessor componentAccessor) { + TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); + + assert transformComponent != null; + + MovementStatesComponent movementStatesComponent = componentAccessor.getComponent(ref, MovementStatesComponent.getComponentType()); + + assert movementStatesComponent != null; + + MovementStates movementStates = movementStatesComponent.getMovementStates(); + boolean fallDamageActive = !movementStates.inFluid && !movementStates.climbing && !movementStates.flying && !movementStates.gliding; + if (fallDamageActive) { + Vector3d position = transformComponent.getPosition(); + if (!movementStates.onGround) { + if (position.getY() > locY) { + this.currentFallDistance = this.currentFallDistance + (position.getY() - locY); + } + } else { + this.currentFallDistance = 0.0; + } + } else { + this.currentFallDistance = 0.0; + } + + super.moveTo(ref, locX, locY, locZ, componentAccessor); + } + + public boolean canDecreaseItemStackDurability(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { + return false; + } + + public boolean canApplyItemStackPenalties(Ref ref, ComponentAccessor componentAccessor) { + return true; + } + + @Nullable + public ItemStackSlotTransaction decreaseItemStackDurability( + @Nonnull Ref ref, @Nullable ItemStack itemStack, int inventoryId, int slotId, @Nonnull ComponentAccessor componentAccessor + ) { + if (!this.canDecreaseItemStackDurability(ref, componentAccessor)) { + return null; + } else if (itemStack == null || itemStack.isEmpty() || itemStack.getItem() == null) { + return null; + } else if (itemStack.isBroken()) { + return null; + } else { + Item item = itemStack.getItem(); + ItemContainer section = this.inventory.getSectionById(inventoryId); + if (section == null) { + return null; + } else if (item.getArmor() != null) { + ItemStackSlotTransaction transaction = this.updateItemStackDurability( + ref, itemStack, section, slotId, -item.getDurabilityLossOnHit(), componentAccessor + ); + if (transaction.getSlotAfter().isBroken()) { + this.statModifiersManager.setRecalculate(true); + } + + return transaction; + } else { + return item.getWeapon() != null + ? this.updateItemStackDurability(ref, itemStack, section, slotId, -item.getDurabilityLossOnHit(), componentAccessor) + : null; + } + } + } + + @Nullable + public ItemStackSlotTransaction updateItemStackDurability( + @Nonnull Ref ref, + @Nonnull ItemStack itemStack, + ItemContainer container, + int slotId, + double durabilityChange, + @Nonnull ComponentAccessor componentAccessor + ) { + ItemStack updatedItemStack = itemStack.withIncreasedDurability(durabilityChange); + return container.replaceItemStackInSlot((short) slotId, itemStack, updatedItemStack); + } + + public void invalidateEquipmentNetwork() { + this.isEquipmentNetworkOutdated = true; + } + + public boolean consumeEquipmentNetworkOutdated() { + boolean temp = this.isEquipmentNetworkOutdated; + this.isEquipmentNetworkOutdated = false; + return temp; + } + + @Nonnull + public StatModifiersManager getStatModifiersManager() { + return this.statModifiersManager; + } + + public double getCurrentFallDistance() { + return this.currentFallDistance; + } + + public void setCurrentFallDistance(double currentFallDistance) { + this.currentFallDistance = currentFallDistance; + } + + @Nonnull + @Override + public String toString() { + return "LivingEntity{, " + super.toString() + "}"; + } +} diff --git a/src/com/hypixel/hytale/server/core/io/PacketHandler.java b/src/com/hypixel/hytale/server/core/io/PacketHandler.java new file mode 100644 index 00000000..888141a8 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/io/PacketHandler.java @@ -0,0 +1,568 @@ +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 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 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 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 getClientReadyForChunksFuture() { + return this.clientReadyForChunksFuture; + } + + public static void logConnectionTimings(@Nonnull Channel channel, @Nonnull String message, @Nonnull Level level) { + Attribute loginStartAttribute = channel.attr(LOGIN_START_ATTRIBUTE_KEY); + long now = System.nanoTime(); + Long before = loginStartAttribute.getAndSet(now); + if (before == null) { + LOGIN_TIMING_LOGGER.at(level).log(message); + } else { + LOGIN_TIMING_LOGGER.at(level).log("%s took %s", 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 METRICS_REGISTRY = new MetricsRegistry() + .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(); + } + } + } +} diff --git a/src/com/hypixel/hytale/server/core/modules/entity/tracker/EntityTrackerSystems.java b/src/com/hypixel/hytale/server/core/modules/entity/tracker/EntityTrackerSystems.java new file mode 100644 index 00000000..c42fa94a --- /dev/null +++ b/src/com/hypixel/hytale/server/core/modules/entity/tracker/EntityTrackerSystems.java @@ -0,0 +1,768 @@ +package com.hypixel.hytale.server.core.modules.entity.tracker; + +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.ComponentType; +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.SystemGroup; +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.dependency.SystemGroupDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.spatial.SpatialResource; +import com.hypixel.hytale.component.spatial.SpatialStructure; +import com.hypixel.hytale.component.system.HolderSystem; +import com.hypixel.hytale.component.system.tick.EntityTickingSystem; +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.packets.entities.EntityUpdates; +import com.hypixel.hytale.server.core.entity.effect.EffectControllerComponent; +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.system.NetworkSendableSpatialSystem; +import com.hypixel.hytale.server.core.receiver.IPacketReceiver; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import it.unimi.dsi.fastutil.ints.IntArrayList; +import it.unimi.dsi.fastutil.ints.IntList; +import it.unimi.dsi.fastutil.objects.Object2IntMap; +import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectIterator; +import it.unimi.dsi.fastutil.objects.ObjectList; +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.locks.StampedLock; + +public class EntityTrackerSystems { + public static final SystemGroup FIND_VISIBLE_ENTITIES_GROUP = EntityStore.REGISTRY.registerSystemGroup(); + public static final SystemGroup QUEUE_UPDATE_GROUP = EntityStore.REGISTRY.registerSystemGroup(); + + public EntityTrackerSystems() { + } + + public static boolean despawnAll(@Nonnull Ref viewerRef, @Nonnull Store store) { + EntityTrackerSystems.EntityViewer viewer = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType()); + if (viewer == null) { + 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; + } + } + + public static boolean clear(@Nonnull Ref viewerRef, @Nonnull Store store) { + EntityTrackerSystems.EntityViewer viewer = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType()); + if (viewer == null) { + return false; + } else { + for (Ref ref : viewer.sent.keySet()) { + EntityTrackerSystems.Visible visible = store.getComponent(ref, EntityTrackerSystems.Visible.getComponentType()); + if (visible != null) { + visible.visibleTo.remove(viewerRef); + } + } + + viewer.sent.clear(); + return true; + } + } + + public static class AddToVisible extends EntityTickingSystem { + public static final Set> DEPENDENCIES = Collections.singleton( + new SystemDependency<>(Order.AFTER, EntityTrackerSystems.EnsureVisibleComponent.class) + ); + private final ComponentType entityViewerComponentType; + private final ComponentType visibleComponentType; + + public AddToVisible( + ComponentType entityViewerComponentType, + ComponentType visibleComponentType + ) { + this.entityViewerComponentType = entityViewerComponentType; + this.visibleComponentType = visibleComponentType; + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Override + public Query getQuery() { + return this.entityViewerComponentType; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Override + public void tick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + Ref viewerRef = archetypeChunk.getReferenceTo(index); + EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.entityViewerComponentType); + + for (Ref ref : viewer.visible) { + commandBuffer.getComponent(ref, this.visibleComponentType).addViewerParallel(viewerRef, viewer); + } + } + } + + public static class ClearEntityViewers extends EntityTickingSystem { + public static final Set> DEPENDENCIES = Collections.singleton( + new SystemGroupDependency<>(Order.BEFORE, EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP) + ); + private final ComponentType componentType; + + public ClearEntityViewers(ComponentType componentType) { + this.componentType = componentType; + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Override + public Query getQuery() { + return this.componentType; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Override + public void tick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.componentType); + viewer.visible.clear(); + viewer.lodExcludedCount = 0; + viewer.hiddenCount = 0; + } + } + + public static class ClearPreviouslyVisible extends EntityTickingSystem { + public static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, EntityTrackerSystems.ClearEntityViewers.class), + new SystemGroupDependency(Order.AFTER, EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP) + ); + private final ComponentType componentType; + + public ClearPreviouslyVisible(ComponentType componentType) { + this.componentType = componentType; + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Override + public Query getQuery() { + return this.componentType; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Override + public void tick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.Visible visible = archetypeChunk.getComponent(index, this.componentType); + Map, EntityTrackerSystems.EntityViewer> oldVisibleTo = visible.previousVisibleTo; + visible.previousVisibleTo = visible.visibleTo; + visible.visibleTo = oldVisibleTo; + visible.visibleTo.clear(); + visible.newlyVisibleTo.clear(); + } + } + + public static class CollectVisible extends EntityTickingSystem { + private final ComponentType componentType; + private final Query query; + @Nonnull + private final Set> dependencies; + + public CollectVisible(ComponentType componentType) { + this.componentType = componentType; + this.query = Archetype.of(componentType, TransformComponent.getComponentType()); + this.dependencies = Collections.singleton(new SystemDependency<>(Order.AFTER, NetworkSendableSpatialSystem.class)); + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP; + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + TransformComponent transform = archetypeChunk.getComponent(index, TransformComponent.getComponentType()); + Vector3d position = transform.getPosition(); + EntityTrackerSystems.EntityViewer entityViewer = archetypeChunk.getComponent(index, this.componentType); + SpatialStructure> spatialStructure = store.getResource(EntityModule.get().getNetworkSendableSpatialResourceType()) + .getSpatialStructure(); + ObjectList> results = SpatialResource.getThreadLocalReferenceList(); + spatialStructure.collect(position, entityViewer.viewRadiusBlocks, results); + entityViewer.visible.addAll(results); + } + } + + public static class EffectControllerSystem extends EntityTickingSystem { + @Nonnull + private final ComponentType visibleComponentType; + @Nonnull + private final ComponentType effectControllerComponentType; + @Nonnull + private final Query query; + + public EffectControllerSystem( + @Nonnull ComponentType visibleComponentType, + @Nonnull ComponentType effectControllerComponentType + ) { + this.visibleComponentType = visibleComponentType; + this.effectControllerComponentType = effectControllerComponentType; + this.query = Query.and(visibleComponentType, effectControllerComponentType); + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityTrackerSystems.QUEUE_UPDATE_GROUP; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.Visible visibleComponent = archetypeChunk.getComponent(index, this.visibleComponentType); + + assert visibleComponent != null; + + Ref entityRef = archetypeChunk.getReferenceTo(index); + EffectControllerComponent effectControllerComponent = archetypeChunk.getComponent(index, this.effectControllerComponentType); + + assert effectControllerComponent != null; + + if (!visibleComponent.newlyVisibleTo.isEmpty()) { + queueFullUpdate(entityRef, effectControllerComponent, visibleComponent.newlyVisibleTo); + } + + if (effectControllerComponent.consumeNetworkOutdated()) { + queueUpdatesFor(entityRef, effectControllerComponent, visibleComponent.visibleTo, visibleComponent.newlyVisibleTo); + } + } + + private static void queueFullUpdate( + @Nonnull Ref ref, + @Nonnull EffectControllerComponent effectControllerComponent, + @Nonnull Map, EntityTrackerSystems.EntityViewer> visibleTo + ) { + ComponentUpdate update = new ComponentUpdate(); + update.type = ComponentUpdateType.EntityEffects; + update.entityEffectUpdates = effectControllerComponent.createInitUpdates(); + + for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) { + viewer.queueUpdate(ref, update); + } + } + + private static void queueUpdatesFor( + @Nonnull Ref ref, + @Nonnull EffectControllerComponent effectControllerComponent, + @Nonnull Map, EntityTrackerSystems.EntityViewer> visibleTo, + @Nonnull Map, EntityTrackerSystems.EntityViewer> exclude + ) { + ComponentUpdate update = new ComponentUpdate(); + update.type = ComponentUpdateType.EntityEffects; + update.entityEffectUpdates = effectControllerComponent.consumeChanges(); + if (!exclude.isEmpty()) { + for (Entry, EntityTrackerSystems.EntityViewer> entry : visibleTo.entrySet()) { + if (!exclude.containsKey(entry.getKey())) { + entry.getValue().queueUpdate(ref, update); + } + } + } else { + for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) { + viewer.queueUpdate(ref, update); + } + } + } + } + + public static class EnsureVisibleComponent extends EntityTickingSystem { + public static final Set> DEPENDENCIES = Collections.singleton( + new SystemDependency<>(Order.AFTER, EntityTrackerSystems.ClearPreviouslyVisible.class) + ); + private final ComponentType entityViewerComponentType; + private final ComponentType visibleComponentType; + + public EnsureVisibleComponent( + ComponentType entityViewerComponentType, + ComponentType visibleComponentType + ) { + this.entityViewerComponentType = entityViewerComponentType; + this.visibleComponentType = visibleComponentType; + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Override + public Query getQuery() { + return this.entityViewerComponentType; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Override + public void tick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + for (Ref ref : archetypeChunk.getComponent(index, this.entityViewerComponentType).visible) { + if (!commandBuffer.getArchetype(ref).contains(this.visibleComponentType)) { + commandBuffer.ensureComponent(ref, this.visibleComponentType); + } + } + } + } + + public static class EntityUpdate { + @Nonnull + private final StampedLock removeLock = new StampedLock(); + @Nonnull + private final EnumSet removed; + @Nonnull + private final StampedLock updatesLock = new StampedLock(); + @Nonnull + private final List updates; + + public EntityUpdate() { + this.removed = EnumSet.noneOf(ComponentUpdateType.class); + this.updates = new ObjectArrayList<>(); + } + + public EntityUpdate(@Nonnull EntityTrackerSystems.EntityUpdate other) { + this.removed = EnumSet.copyOf(other.removed); + this.updates = new ObjectArrayList<>(other.updates); + } + + @Nonnull + public EntityTrackerSystems.EntityUpdate clone() { + return new EntityTrackerSystems.EntityUpdate(this); + } + + public void queueRemove(@Nonnull ComponentUpdateType type) { + long stamp = this.removeLock.writeLock(); + + try { + this.removed.add(type); + } finally { + this.removeLock.unlockWrite(stamp); + } + } + + public void queueUpdate(@Nonnull ComponentUpdate update) { + long stamp = this.updatesLock.writeLock(); + + try { + this.updates.add(update); + } finally { + this.updatesLock.unlockWrite(stamp); + } + } + + @Nullable + public ComponentUpdateType[] toRemovedArray() { + return this.removed.isEmpty() ? null : this.removed.toArray(ComponentUpdateType[]::new); + } + + @Nullable + public ComponentUpdate[] toUpdatesArray() { + return this.updates.isEmpty() ? null : this.updates.toArray(ComponentUpdate[]::new); + } + } + + public static class EntityViewer implements Component { + private static final float VISIBILITY_UPDATE_INTERVAL = 0.2f; + + public int viewRadiusBlocks; + public IPacketReceiver packetReceiver; + public Set> visible; + public Map, EntityTrackerSystems.EntityUpdate> updates; + public Object2IntMap> sent; + public int lodExcludedCount; + public int hiddenCount; + public float visibilityTimeAccumulator; + + public static ComponentType getComponentType() { + return EntityModule.get().getEntityViewerComponentType(); + } + + public EntityViewer(int viewRadiusBlocks, IPacketReceiver packetReceiver) { + this.viewRadiusBlocks = viewRadiusBlocks; + this.packetReceiver = packetReceiver; + this.visible = new ObjectOpenHashSet<>(); + this.updates = new ConcurrentHashMap<>(); + this.sent = new Object2IntOpenHashMap<>(); + this.sent.defaultReturnValue(-1); + } + + public EntityViewer(@Nonnull EntityTrackerSystems.EntityViewer other) { + this.viewRadiusBlocks = other.viewRadiusBlocks; + this.packetReceiver = other.packetReceiver; + this.visible = new HashSet<>(other.visible); + this.updates = new ConcurrentHashMap<>(other.updates.size()); + + for (Entry, EntityTrackerSystems.EntityUpdate> entry : other.updates.entrySet()) { + this.updates.put(entry.getKey(), entry.getValue().clone()); + } + + this.sent = new Object2IntOpenHashMap<>(other.sent); + this.sent.defaultReturnValue(-1); + } + + @Nonnull + @Override + public Component clone() { + return new EntityTrackerSystems.EntityViewer(this); + } + + public void queueRemove(Ref ref, ComponentUpdateType type) { + if (!this.visible.contains(ref)) { + throw new IllegalArgumentException("Entity is not visible!"); + } else { + this.updates.computeIfAbsent(ref, k -> new EntityTrackerSystems.EntityUpdate()).queueRemove(type); + } + } + + public void queueUpdate(Ref ref, ComponentUpdate update) { + if (!this.visible.contains(ref)) { + throw new IllegalArgumentException("Entity is not visible!"); + } else { + this.updates.computeIfAbsent(ref, k -> new EntityTrackerSystems.EntityUpdate()).queueUpdate(update); + } + } + + /** + * Accumulates dt and returns true if visibility should be processed this tick. + * Called by ClearEntityViewers; CollectVisible checks visibilityTimeAccumulator == 0. + */ + public boolean shouldProcessVisibility(float dt) { + this.visibilityTimeAccumulator += dt; + if (this.visibilityTimeAccumulator >= VISIBILITY_UPDATE_INTERVAL) { + this.visibilityTimeAccumulator = 0; + return true; + } + return false; + } + + /** + * Resets all visibility state. Call on world switch to force immediate full refresh. + */ + public void resetVisibility() { + this.visible.clear(); + this.sent.clear(); + this.updates.clear(); + this.visibilityTimeAccumulator = 0; + this.lodExcludedCount = 0; + this.hiddenCount = 0; + } + } + + public static class RemoveEmptyVisibleComponent extends EntityTickingSystem { + public static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, EntityTrackerSystems.AddToVisible.class), + new SystemGroupDependency(Order.BEFORE, EntityTrackerSystems.QUEUE_UPDATE_GROUP) + ); + private final ComponentType componentType; + + public RemoveEmptyVisibleComponent(ComponentType componentType) { + this.componentType = componentType; + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Override + public Query getQuery() { + return this.componentType; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Override + public void tick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + if (archetypeChunk.getComponent(index, this.componentType).visibleTo.isEmpty()) { + commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), this.componentType); + } + } + } + + public static class RemoveVisibleComponent extends HolderSystem { + private final ComponentType componentType; + + public RemoveVisibleComponent(ComponentType componentType) { + this.componentType = componentType; + } + + @Override + public Query getQuery() { + return this.componentType; + } + + @Override + public void onEntityAdd(@Nonnull Holder holder, @Nonnull AddReason reason, @Nonnull Store store) { + } + + @Override + public void onEntityRemoved(@Nonnull Holder holder, @Nonnull RemoveReason reason, @Nonnull Store store) { + holder.removeComponent(this.componentType); + } + } + + public static class SendPackets extends EntityTickingSystem { + public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + public static final ThreadLocal INT_LIST_THREAD_LOCAL = ThreadLocal.withInitial(IntArrayList::new); + public static final Set> DEPENDENCIES = Set.of(new SystemGroupDependency<>(Order.AFTER, EntityTrackerSystems.QUEUE_UPDATE_GROUP)); + private final ComponentType componentType; + + public SendPackets(ComponentType componentType) { + this.componentType = componentType; + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityStore.SEND_PACKET_GROUP; + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Override + public Query getQuery() { + return this.componentType; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Override + public void tick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.componentType); + 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()); + } + + ObjectIterator>> iterator = viewer.sent.object2IntEntrySet().iterator(); + + while (iterator.hasNext()) { + it.unimi.dsi.fastutil.objects.Object2IntMap.Entry> entry = iterator.next(); + Ref ref = entry.getKey(); + if (!ref.isValid() || !viewer.visible.contains(ref)) { + removedEntities.add(entry.getIntValue()); + iterator.remove(); + if (viewer.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> iteratorx = viewer.updates.keySet().iterator(); + + while (iteratorx.hasNext()) { + Ref ref = iteratorx.next(); + if (!ref.isValid() || ref.getStore() != store) { + iteratorx.remove(); + } else if (!viewer.sent.containsKey(ref)) { + int networkId = commandBuffer.getComponent(ref, NetworkId.getComponentType()).getId(); + if (networkId == -1) { + throw new IllegalArgumentException("Invalid entity network id: " + ref); + } + + viewer.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()]; + int i = 0; + + for (Entry, EntityTrackerSystems.EntityUpdate> entry : viewer.updates.entrySet()) { + com.hypixel.hytale.protocol.EntityUpdate entityUpdate = packet.updates[i++] = new com.hypixel.hytale.protocol.EntityUpdate(); + entityUpdate.networkId = viewer.sent.getInt(entry.getKey()); + EntityTrackerSystems.EntityUpdate update = entry.getValue(); + entityUpdate.removed = update.toRemovedArray(); + entityUpdate.updates = update.toUpdatesArray(); + } + + viewer.updates.clear(); + viewer.packetReceiver.writeNoCache(packet); + } + } + } + + public static class Visible implements Component { + @Nonnull + private final StampedLock lock = new StampedLock(); + @Nonnull + public Map, EntityTrackerSystems.EntityViewer> previousVisibleTo = new Object2ObjectOpenHashMap<>(); + @Nonnull + public Map, EntityTrackerSystems.EntityViewer> visibleTo = new Object2ObjectOpenHashMap<>(); + @Nonnull + public Map, EntityTrackerSystems.EntityViewer> newlyVisibleTo = new Object2ObjectOpenHashMap<>(); + + public Visible() { + } + + @Nonnull + public static ComponentType getComponentType() { + return EntityModule.get().getVisibleComponentType(); + } + + @Nonnull + @Override + public Component clone() { + return new EntityTrackerSystems.Visible(); + } + + public void addViewerParallel(Ref ref, EntityTrackerSystems.EntityViewer entityViewer) { + long stamp = this.lock.writeLock(); + + try { + this.visibleTo.put(ref, entityViewer); + if (!this.previousVisibleTo.containsKey(ref)) { + this.newlyVisibleTo.put(ref, entityViewer); + } + } finally { + this.lock.unlockWrite(stamp); + } + } + } +} diff --git a/src/com/hypixel/hytale/server/core/modules/entity/tracker/LegacyEntityTrackerSystems.java b/src/com/hypixel/hytale/server/core/modules/entity/tracker/LegacyEntityTrackerSystems.java new file mode 100644 index 00000000..6cdabf6f --- /dev/null +++ b/src/com/hypixel/hytale/server/core/modules/entity/tracker/LegacyEntityTrackerSystems.java @@ -0,0 +1,457 @@ +package com.hypixel.hytale.server.core.modules.entity.tracker; + +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.SystemGroup; +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.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.ItemArmorSlot; +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; +import com.hypixel.hytale.server.core.entity.LivingEntity; +import com.hypixel.hytale.server.core.entity.entities.Player; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.modules.entity.AllLegacyLivingEntityTypesQuery; +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.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.player.PlayerSettings; +import com.hypixel.hytale.server.core.modules.entity.player.PlayerSkinComponent; +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.Arrays; +import java.util.Collections; +import java.util.Iterator; +import java.util.Map; +import java.util.Set; + +public class LegacyEntityTrackerSystems { + public LegacyEntityTrackerSystems() { + } + + @Deprecated + public static boolean clear(@Nonnull Player player, @Nonnull Holder holder) { + World world = player.getWorld(); + if (world != null && world.isInThread()) { + Ref ref = player.getReference(); + return ref != null && ref.isValid() ? EntityTrackerSystems.clear(ref, world.getEntityStore().getStore()) : false; + } else { + EntityTrackerSystems.EntityViewer entityViewerComponent = holder.getComponent(EntityTrackerSystems.EntityViewer.getComponentType()); + if (entityViewerComponent == null) { + return false; + } else { + entityViewerComponent.sent.clear(); + return true; + } + } + } + + public static class LegacyEntityModel extends EntityTickingSystem { + private final ComponentType componentType; + private final ComponentType modelComponentType; + @Nonnull + private final Query query; + + public LegacyEntityModel(ComponentType componentType) { + this.componentType = componentType; + this.modelComponentType = ModelComponent.getComponentType(); + this.query = Query.and(componentType, this.modelComponentType); + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityTrackerSystems.QUEUE_UPDATE_GROUP; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.Visible visibleComponent = archetypeChunk.getComponent(index, this.componentType); + + assert visibleComponent != null; + + ModelComponent modelComponent = archetypeChunk.getComponent(index, this.modelComponentType); + + assert modelComponent != null; + + float entityScale = 0.0F; + boolean scaleOutdated = false; + EntityScaleComponent entityScaleComponent = archetypeChunk.getComponent(index, EntityScaleComponent.getComponentType()); + if (entityScaleComponent != null) { + entityScale = entityScaleComponent.getScale(); + scaleOutdated = entityScaleComponent.consumeNetworkOutdated(); + } + + boolean modelOutdated = modelComponent.consumeNetworkOutdated(); + if (modelOutdated || scaleOutdated) { + queueUpdatesFor(archetypeChunk.getReferenceTo(index), modelComponent, entityScale, visibleComponent.visibleTo); + } else if (!visibleComponent.newlyVisibleTo.isEmpty()) { + queueUpdatesFor(archetypeChunk.getReferenceTo(index), modelComponent, entityScale, visibleComponent.newlyVisibleTo); + } + } + + private static void queueUpdatesFor( + Ref ref, @Nullable ModelComponent model, float entityScale, @Nonnull Map, EntityTrackerSystems.EntityViewer> visibleTo + ) { + ComponentUpdate update = new ComponentUpdate(); + update.type = ComponentUpdateType.Model; + update.model = model != null ? model.getModel().toPacket() : null; + update.entityScale = entityScale; + + for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) { + viewer.queueUpdate(ref, update); + } + } + } + + public static class LegacyEntitySkin extends EntityTickingSystem { + private final ComponentType playerSkinComponentComponentType; + private final ComponentType visibleComponentType; + @Nonnull + private final Query query; + + public LegacyEntitySkin( + ComponentType visibleComponentType, + ComponentType playerSkinComponentComponentType + ) { + this.visibleComponentType = visibleComponentType; + this.playerSkinComponentComponentType = playerSkinComponentComponentType; + this.query = Query.and(visibleComponentType, playerSkinComponentComponentType); + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityTrackerSystems.QUEUE_UPDATE_GROUP; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.Visible visibleComponent = archetypeChunk.getComponent(index, this.visibleComponentType); + + assert visibleComponent != null; + + PlayerSkinComponent playerSkinComponent = archetypeChunk.getComponent(index, this.playerSkinComponentComponentType); + + assert playerSkinComponent != null; + + if (playerSkinComponent.consumeNetworkOutdated()) { + queueUpdatesFor(archetypeChunk.getReferenceTo(index), playerSkinComponent, visibleComponent.visibleTo); + } else if (!visibleComponent.newlyVisibleTo.isEmpty()) { + queueUpdatesFor(archetypeChunk.getReferenceTo(index), playerSkinComponent, visibleComponent.newlyVisibleTo); + } + } + + private static void queueUpdatesFor( + Ref ref, @Nonnull PlayerSkinComponent component, @Nonnull Map, EntityTrackerSystems.EntityViewer> visibleTo + ) { + ComponentUpdate update = new ComponentUpdate(); + update.type = ComponentUpdateType.PlayerSkin; + update.skin = component.getPlayerSkin(); + + for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) { + viewer.queueUpdate(ref, update); + } + } + } + + public static class LegacyEquipment extends EntityTickingSystem { + private final ComponentType componentType; + @Nonnull + private final Query query; + + public LegacyEquipment(ComponentType componentType) { + this.componentType = componentType; + this.query = Query.and(componentType, AllLegacyLivingEntityTypesQuery.INSTANCE); + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityTrackerSystems.QUEUE_UPDATE_GROUP; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.Visible visibleComponent = archetypeChunk.getComponent(index, this.componentType); + + assert visibleComponent != null; + + LivingEntity entity = (LivingEntity) EntityUtils.getEntity(index, archetypeChunk); + + assert entity != null; + + if (entity.consumeEquipmentNetworkOutdated()) { + queueUpdatesFor(archetypeChunk.getReferenceTo(index), entity, visibleComponent.visibleTo); + } else if (!visibleComponent.newlyVisibleTo.isEmpty()) { + queueUpdatesFor(archetypeChunk.getReferenceTo(index), entity, visibleComponent.newlyVisibleTo); + } + } + + private static void queueUpdatesFor( + @Nonnull Ref ref, @Nonnull LivingEntity entity, @Nonnull Map, EntityTrackerSystems.EntityViewer> visibleTo + ) { + ComponentUpdate update = new ComponentUpdate(); + update.type = ComponentUpdateType.Equipment; + update.equipment = new Equipment(); + 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); + Store store = ref.getStore(); + PlayerSettings playerSettings = store.getComponent(ref, PlayerSettings.getComponentType()); + if (playerSettings != null) { + PlayerConfig.ArmorVisibilityOption armorVisibilityOption = store.getExternalData() + .getWorld() + .getGameplayConfig() + .getPlayerConfig() + .getArmorVisibilityOption(); + if (armorVisibilityOption.canHideHelmet() && playerSettings.hideHelmet()) { + update.equipment.armorIds[ItemArmorSlot.Head.ordinal()] = ""; + } + + if (armorVisibilityOption.canHideCuirass() && playerSettings.hideCuirass()) { + update.equipment.armorIds[ItemArmorSlot.Chest.ordinal()] = ""; + } + + if (armorVisibilityOption.canHideGauntlets() && playerSettings.hideGauntlets()) { + update.equipment.armorIds[ItemArmorSlot.Hands.ordinal()] = ""; + } + + if (armorVisibilityOption.canHidePants() && playerSettings.hidePants()) { + update.equipment.armorIds[ItemArmorSlot.Legs.ordinal()] = ""; + } + } + + ItemStack itemInHand = inventory.getItemInHand(); + update.equipment.rightHandItemId = itemInHand != null ? itemInHand.getItemId() : "Empty"; + ItemStack utilityItem = inventory.getUtilityItem(); + update.equipment.leftHandItemId = utilityItem != null ? utilityItem.getItemId() : "Empty"; + + for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) { + viewer.queueUpdate(ref, update); + } + } + } + + public static class LegacyHideFromEntity extends EntityTickingSystem { + private final ComponentType entityViewerComponentType; + private final ComponentType playerSettingsComponentType; + @Nonnull + private final Query query; + @Nonnull + private final Set> dependencies; + + public LegacyHideFromEntity(ComponentType entityViewerComponentType) { + this.entityViewerComponentType = entityViewerComponentType; + this.playerSettingsComponentType = EntityModule.get().getPlayerSettingsComponentType(); + this.query = Query.and(entityViewerComponentType, AllLegacyLivingEntityTypesQuery.INSTANCE); + this.dependencies = Collections.singleton(new SystemDependency<>(Order.AFTER, EntityTrackerSystems.CollectVisible.class)); + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP; + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + Ref viewerRef = archetypeChunk.getReferenceTo(index); + PlayerSettings settings = archetypeChunk.getComponent(index, this.playerSettingsComponentType); + if (settings == null) { + settings = PlayerSettings.defaults(); + } + + EntityTrackerSystems.EntityViewer entityViewerComponent = archetypeChunk.getComponent(index, this.entityViewerComponentType); + + assert entityViewerComponent != null; + + Iterator> iterator = entityViewerComponent.visible.iterator(); + + while (iterator.hasNext()) { + Ref ref = iterator.next(); + Entity entity = EntityUtils.getEntity(ref, commandBuffer); + if (entity != null && entity.isHiddenFromLivingEntity(ref, viewerRef, commandBuffer) && canHideEntities(entity, settings)) { + entityViewerComponent.hiddenCount++; + iterator.remove(); + } + } + } + + private static boolean canHideEntities(Entity entity, @Nonnull PlayerSettings settings) { + return entity instanceof Player && !settings.showEntityMarkers(); + } + } + + public static class LegacyLODCull extends EntityTickingSystem { + public static final double ENTITY_LOD_RATIO_DEFAULT = 3.5E-5; + public static double ENTITY_LOD_RATIO = 3.5E-5; + private final ComponentType entityViewerComponentType; + private final ComponentType boundingBoxComponentType; + @Nonnull + private final Query query; + @Nonnull + private final Set> dependencies; + + public LegacyLODCull(ComponentType entityViewerComponentType) { + this.entityViewerComponentType = entityViewerComponentType; + this.boundingBoxComponentType = BoundingBox.getComponentType(); + this.query = Query.and(entityViewerComponentType, TransformComponent.getComponentType()); + this.dependencies = Collections.singleton(new SystemDependency<>(Order.AFTER, EntityTrackerSystems.CollectVisible.class)); + } + + @Nullable + @Override + public SystemGroup getGroup() { + return EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP; + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + EntityTrackerSystems.EntityViewer entityViewerComponent = archetypeChunk.getComponent(index, this.entityViewerComponentType); + + assert entityViewerComponent != null; + + TransformComponent transformComponent = archetypeChunk.getComponent(index, TransformComponent.getComponentType()); + + assert transformComponent != null; + + Vector3d position = transformComponent.getPosition(); + Iterator> iterator = entityViewerComponent.visible.iterator(); + + while (iterator.hasNext()) { + Ref targetRef = iterator.next(); + BoundingBox targetBoundingBoxComponent = commandBuffer.getComponent(targetRef, this.boundingBoxComponentType); + if (targetBoundingBoxComponent != null) { + TransformComponent targetTransformComponent = commandBuffer.getComponent(targetRef, TransformComponent.getComponentType()); + if (targetTransformComponent != null) { + double distanceSq = targetTransformComponent.getPosition().distanceSquaredTo(position); + double maximumThickness = targetBoundingBoxComponent.getBoundingBox().getMaximumThickness(); + if (maximumThickness < ENTITY_LOD_RATIO * distanceSq) { + entityViewerComponent.lodExcludedCount++; + iterator.remove(); + } + } + } + } + } + } +} diff --git a/src/com/hypixel/hytale/server/core/modules/interaction/blocktrack/TrackedPlacement.java b/src/com/hypixel/hytale/server/core/modules/interaction/blocktrack/TrackedPlacement.java new file mode 100644 index 00000000..4aa11d13 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/modules/interaction/blocktrack/TrackedPlacement.java @@ -0,0 +1,124 @@ +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 { + public static final BuilderCodec 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 getComponentType() { + return InteractionModule.get().getTrackedPlacementComponentType(); + } + + public TrackedPlacement() { + } + + public TrackedPlacement(String blockName) { + this.blockName = blockName; + } + + @Nullable + @Override + public Component clone() { + return new TrackedPlacement(this.blockName); + } + + public static class OnAddRemove extends RefSystem { + private static final ComponentType COMPONENT_TYPE = TrackedPlacement.getComponentType(); + private static final ResourceType BLOCK_COUNTER_RESOURCE_TYPE = BlockCounter.getResourceType(); + + public OnAddRemove() { + } + + @Override + public void onEntityAdded( + @Nonnull Ref ref, @Nonnull AddReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + if (reason == AddReason.SPAWN) { + BlockModule.BlockStateInfo blockInfo = commandBuffer.getComponent(ref, BlockModule.BlockStateInfo.getComponentType()); + + assert blockInfo != null; + + Ref 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 ref, @Nonnull RemoveReason reason, @Nonnull Store store, @Nonnull CommandBuffer 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 getQuery() { + return COMPONENT_TYPE; + } + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/Universe.java b/src/com/hypixel/hytale/server/core/universe/Universe.java new file mode 100644 index 00000000..3dffb17f --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/Universe.java @@ -0,0 +1,962 @@ +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 LEGACY_BLOCK_ID_MAP = Collections.emptyMap(); + @Nonnull + public static final MetricsRegistry METRICS_REGISTRY = new MetricsRegistry() + .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 playerRefComponentType; + @Nonnull + private final Path path = Constants.UNIVERSE_PATH; + @Nonnull + private final Map players = new ConcurrentHashMap<>(); + @Nonnull + private final Map worlds = new ConcurrentHashMap<>(); + @Nonnull + private final Map worldsByUuid = new ConcurrentHashMap<>(); + @Nonnull + private final Map unmodifiableWorlds = Collections.unmodifiableMap(this.worlds); + private PlayerStorage playerStorage; + private WorldConfigProvider worldConfigProvider; + private ResourceType indexedStorageCacheResourceType; + private CompletableFuture 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 runBackup() { + return CompletableFuture.allOf(this.worlds.values().stream().map(world -> CompletableFuture.supplyAsync(() -> { + Store 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 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 chunkStoreRegistry = this.getChunkStoreRegistry(); + ComponentRegistryProxy 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() + .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 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> loadingWorlds = new ObjectArrayList<>(); + + try { + Path worldsPath = this.path.resolve("worlds"); + Files.createDirectories(worldsPath); + + try (DirectoryStream 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 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 getUniverseReady() { + return this.universeReady; + } + + public ResourceType 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 addWorld(@Nonnull String name) { + return this.addWorld(name, null, null); + } + + @Nonnull + @Deprecated + @CheckReturnValue + public CompletableFuture 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 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 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 makeWorld(@Nonnull String name, @Nonnull Path savePath, @Nonnull WorldConfig worldConfig) { + return this.makeWorld(name, savePath, worldConfig, true); + } + + @Nonnull + @CheckReturnValue + public CompletableFuture makeWorld(@Nonnull String name, @Nonnull Path savePath, @Nonnull WorldConfig worldConfig, boolean start) { + Map map = worldConfig.getRequiredPlugins(); + if (map != null) { + PluginManager pluginManager = PluginManager.get(); + + for (Entry 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 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 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 getWorlds() { + return this.unmodifiableWorlds; + } + + @Nonnull + public List 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 comparator, @Nonnull BiPredicate 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 comparator, @Nonnull BiPredicate equality) { + return NameMatching.find(this.players.values(), value, PlayerRef::getUsername, comparator, equality); + } + + public int getPlayerCount() { + return this.players.size(); + } + + @Nonnull + public CompletableFuture 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) 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() + .dispatchFor(PlayerConnectEvent.class) + .dispatch(new PlayerConnectEvent((Holder) holder, playerRefComponent, lastWorld != null ? lastWorld : this.getDefaultWorld())); + 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())); + return world.addPlayer(playerRefComponent, null, false, false).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 eventDispatcher = HytaleServer.get() + .getEventBus() + .dispatchFor(PlayerDisconnectEvent.class); + if (eventDispatcher.hasListener()) { + eventDispatcher.dispatch(new PlayerDisconnectEvent(playerRef)); + } + + Ref 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 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 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) holder)); + } + + @Nonnull + public CompletableFuture resetPlayer(@Nonnull PlayerRef oldPlayer, @Nonnull Holder holder) { + return this.resetPlayer(oldPlayer, holder, null, null); + } + + @Nonnull + public CompletableFuture resetPlayer( + @Nonnull PlayerRef playerRef, @Nonnull Holder 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 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 getPlayerRefComponentType() { + return this.playerRefComponentType; + } + + @Nonnull + @Deprecated + public static Map 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; + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/World.java b/src/com/hypixel/hytale/server/core/universe/world/World.java new file mode 100644 index 00000000..2ab5a402 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/World.java @@ -0,0 +1,1247 @@ +package com.hypixel.hytale.server.core.universe.world; + +import com.hypixel.hytale.assetstore.AssetRegistry; +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.common.util.FormatUtil; +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ComponentAccessor; +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.Store; +import com.hypixel.hytale.component.data.unknown.UnknownComponents; +import com.hypixel.hytale.event.EventRegistry; +import com.hypixel.hytale.event.IEventDispatcher; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.logger.sentry.SkipSentryException; +import com.hypixel.hytale.math.block.BlockUtil; +import com.hypixel.hytale.math.util.ChunkUtil; +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.metrics.ExecutorMetricsRegistry; +import com.hypixel.hytale.metrics.metric.HistoricMetric; +import com.hypixel.hytale.protocol.packets.entities.SetEntitySeed; +import com.hypixel.hytale.protocol.packets.player.JoinWorld; +import com.hypixel.hytale.protocol.packets.player.SetClientId; +import com.hypixel.hytale.protocol.packets.setup.ClientFeature; +import com.hypixel.hytale.protocol.packets.setup.SetTimeDilation; +import com.hypixel.hytale.protocol.packets.setup.SetUpdateRate; +import com.hypixel.hytale.protocol.packets.setup.UpdateFeatures; +import com.hypixel.hytale.protocol.packets.setup.ViewRadius; +import com.hypixel.hytale.protocol.packets.world.ServerSetPaused; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.Message; +import com.hypixel.hytale.server.core.ShutdownReason; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.BlockType; +import com.hypixel.hytale.server.core.asset.type.gameplay.CombatConfig; +import com.hypixel.hytale.server.core.asset.type.gameplay.DeathConfig; +import com.hypixel.hytale.server.core.asset.type.gameplay.GameplayConfig; +import com.hypixel.hytale.server.core.asset.type.gameplay.PlayerConfig; +import com.hypixel.hytale.server.core.blocktype.component.BlockPhysics; +import com.hypixel.hytale.server.core.console.ConsoleModule; +import com.hypixel.hytale.server.core.entity.Entity; +import com.hypixel.hytale.server.core.entity.EntityUtils; +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.io.PacketHandler; +import com.hypixel.hytale.server.core.modules.entity.EntityModule; +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.player.ChunkTracker; +import com.hypixel.hytale.server.core.modules.entity.tracker.LegacyEntityTrackerSystems; +import com.hypixel.hytale.server.core.modules.time.TimeResource; +import com.hypixel.hytale.server.core.modules.time.WorldTimeResource; +import com.hypixel.hytale.server.core.prefab.selection.buffer.impl.IPrefabBuffer; +import com.hypixel.hytale.server.core.receiver.IMessageReceiver; +import com.hypixel.hytale.server.core.universe.PlayerRef; +import com.hypixel.hytale.server.core.universe.Universe; +import com.hypixel.hytale.server.core.universe.world.accessor.ChunkAccessor; +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.chunk.section.BlockSection; +import com.hypixel.hytale.server.core.universe.world.chunk.section.ChunkSection; +import com.hypixel.hytale.server.core.universe.world.events.StartWorldEvent; +import com.hypixel.hytale.server.core.universe.world.lighting.ChunkLightingManager; +import com.hypixel.hytale.server.core.universe.world.path.WorldPathConfig; +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.resources.DiskResourceStorageProvider; +import com.hypixel.hytale.server.core.universe.world.worldgen.IWorldGen; +import com.hypixel.hytale.server.core.universe.world.worldgen.WorldGenLoadException; +import com.hypixel.hytale.server.core.universe.world.worldmap.IWorldMap; +import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapLoadException; +import com.hypixel.hytale.server.core.universe.world.worldmap.WorldMapManager; +import com.hypixel.hytale.server.core.util.FillerBlockUtil; +import com.hypixel.hytale.server.core.util.MessageUtil; +import com.hypixel.hytale.server.core.util.io.FileUtil; +import com.hypixel.hytale.server.core.util.thread.TickingThread; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Collections; +import java.util.Deque; +import java.util.EnumMap; +import java.util.EnumSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.Executor; +import java.util.concurrent.LinkedBlockingDeque; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.IntUnaryOperator; +import java.util.logging.Level; + +public class World extends TickingThread implements Executor, ExecutorMetricsRegistry.ExecutorMetric, ChunkAccessor, IWorldChunks, IMessageReceiver { + public static final float SAVE_INTERVAL = 10.0F; + public static final String DEFAULT = "default"; + @Nonnull + public static final ExecutorMetricsRegistry METRICS_REGISTRY = new ExecutorMetricsRegistry() + .register("Name", world -> world.name, Codec.STRING) + .register("Alive", world -> world.alive.get(), Codec.BOOLEAN) + .register("TickLength", TickingThread::getBufferedTickLengthMetricSet, HistoricMetric.METRICS_CODEC) + .register("EntityStore", World::getEntityStore, EntityStore.METRICS_REGISTRY) + .register("ChunkStore", World::getChunkStore, ChunkStore.METRICS_REGISTRY); + @Nonnull + private final HytaleLogger logger; + @Nonnull + private final String name; + @Nonnull + private final Path savePath; + @Nonnull + private final WorldConfig worldConfig; + @Nonnull + private final ChunkStore chunkStore = new ChunkStore(this); + @Nonnull + private final EntityStore entityStore = new EntityStore(this); + @Nonnull + private final ChunkLightingManager chunkLighting; + @Nonnull + private final WorldMapManager worldMapManager; + private WorldPathConfig worldPathConfig; + private final AtomicBoolean acceptingTasks = new AtomicBoolean(true); + @Nonnull + private final Deque taskQueue = new LinkedBlockingDeque<>(); + @Nonnull + private final AtomicBoolean alive = new AtomicBoolean(true); + @Nonnull + private final EventRegistry eventRegistry = new EventRegistry(new CopyOnWriteArrayList<>(), () -> true, null, HytaleServer.get().getEventBus()); + @Nonnull + private final WorldNotificationHandler notificationHandler = new WorldNotificationHandler(this); + private boolean isTicking; + private boolean isPaused; + private long tick; + @Nonnull + private final Random random = new Random(); + @Nonnull + private final AtomicInteger entitySeed = new AtomicInteger(); + @Nonnull + private final Map players = new ConcurrentHashMap<>(); + @Nonnull + private final Collection playerRefs = Collections.unmodifiableCollection(this.players.values()); + @Nonnull + private final Map features = Collections.synchronizedMap(new EnumMap<>(ClientFeature.class)); + private volatile boolean gcHasRun; + + public World(@Nonnull String name, @Nonnull Path savePath, @Nonnull WorldConfig worldConfig) throws IOException { + super("WorldThread - " + name); + this.name = name; + this.logger = HytaleLogger.get("World|" + name); + this.savePath = savePath; + this.worldConfig = worldConfig; + this.logger + .at(Level.INFO) + .log( + "Loading world '%s' with generator type: '%s' and chunk storage: '%s'...", + name, + worldConfig.getWorldGenProvider(), + worldConfig.getChunkStorageProvider() + ); + this.worldMapManager = new WorldMapManager(this); + this.chunkLighting = new ChunkLightingManager(this); + this.isTicking = worldConfig.isTicking(); + + for (ClientFeature feature : ClientFeature.VALUES) { + this.features.put(feature, true); + } + + GameplayConfig gameplayConfig = this.getGameplayConfig(); + CombatConfig combatConfig = gameplayConfig.getCombatConfig(); + this.features.put(ClientFeature.DisplayHealthBars, combatConfig.isDisplayHealthBars()); + this.features.put(ClientFeature.DisplayCombatText, combatConfig.isDisplayCombatText()); + PlayerConfig.ArmorVisibilityOption armorVisibilityOption = gameplayConfig.getPlayerConfig().getArmorVisibilityOption(); + this.features.put(ClientFeature.CanHideHelmet, armorVisibilityOption.canHideHelmet()); + this.features.put(ClientFeature.CanHideCuirass, armorVisibilityOption.canHideCuirass()); + this.features.put(ClientFeature.CanHideGauntlets, armorVisibilityOption.canHideGauntlets()); + this.features.put(ClientFeature.CanHidePants, armorVisibilityOption.canHidePants()); + this.logger.at(Level.INFO).log("Added world '%s' - Seed: %s, GameTime: %s", name, Long.toString(worldConfig.getSeed()), worldConfig.getGameTime()); + } + + @Nonnull + public CompletableFuture init() { + CompletableFuture savingFuture; + if (this.worldConfig.isSavingConfig()) { + savingFuture = Universe.get().getWorldConfigProvider().save(this.savePath, this.worldConfig, this); + } else { + savingFuture = CompletableFuture.completedFuture(null); + } + + CompletableFuture loadWorldGen = CompletableFuture.supplyAsync(() -> { + try { + IWorldGen worldGen = this.worldConfig.getWorldGenProvider().getGenerator(); + this.chunkStore.setGenerator(worldGen); + this.worldConfig.setDefaultSpawnProvider(worldGen); + IWorldMap worldMap = this.worldConfig.getWorldMapProvider().getGenerator(this); + this.worldMapManager.setGenerator(worldMap); + return this; + } catch (WorldGenLoadException var3x) { + if (this.name.equals(HytaleServer.get().getConfig().getDefaults().getWorld())) { + HytaleServer.get().shutdownServer(ShutdownReason.WORLD_GEN.withMessage(var3x.getTraceMessage("\n"))); + } + + throw new SkipSentryException("Failed to load WorldGen!", var3x); + } catch (WorldMapLoadException var4) { + if (this.name.equals(HytaleServer.get().getConfig().getDefaults().getWorld())) { + HytaleServer.get().shutdownServer(ShutdownReason.WORLD_GEN.withMessage(var4.getTraceMessage("\n"))); + } + + throw new SkipSentryException("Failed to load WorldGen!", var4); + } + }); + CompletableFuture loadPaths = WorldPathConfig.load(this).thenAccept(config -> this.worldPathConfig = config); + return this.worldConfig.getSpawnProvider() != null + ? CompletableFuture.allOf(savingFuture, loadPaths).thenApply(v -> this) + : CompletableFuture.allOf(savingFuture, loadPaths).thenCompose(v -> loadWorldGen); + } + + @Override + protected void onStart() { + DiskResourceStorageProvider.migrateFiles(this); + IResourceStorage resourceStorage = this.worldConfig.getResourceStorageProvider().getResourceStorage(this); + this.chunkStore.start(resourceStorage); + this.entityStore.start(resourceStorage); + this.chunkLighting.start(); + this.worldMapManager.updateTickingState(this.worldMapManager.isStarted()); + Path rffPath = this.savePath.resolve("rff"); + if (Files.exists(rffPath)) { + throw new RuntimeException(rffPath + " directory exists but this version of the server doesn't support migrating RFF worlds!"); + } else { + IEventDispatcher dispatcher = HytaleServer.get().getEventBus().dispatchFor(StartWorldEvent.class, this.name); + if (dispatcher.hasListener()) { + dispatcher.dispatch(new StartWorldEvent(this)); + } + } + } + + public void stopIndividualWorld() { + this.logger.at(Level.INFO).log("Removing individual world: %s", this.name); + World defaultWorld = Universe.get().getDefaultWorld(); + if (defaultWorld != null) { + this.drainPlayersTo(defaultWorld).join(); + } else { + for (PlayerRef playerRef : this.players.values()) { + playerRef.getPacketHandler().disconnect("The world you were in was shutdown and there was no default world to move you to!"); + } + } + + if (this.alive.getAndSet(false)) { + try { + super.stop(); + } catch (Throwable var4) { + this.logger.at(Level.SEVERE).withCause(var4).log("Exception while shutting down world:"); + } + } + } + + public void validateDeleteOnRemove() { + if (this.worldConfig.isDeleteOnRemove()) { + try { + FileUtil.deleteDirectory(this.getSavePath()); + } catch (Throwable var2) { + this.logger.at(Level.SEVERE).withCause(var2).log("Exception while deleting world on remove:"); + } + } + } + + @Override + protected boolean isIdle() { + return this.players.isEmpty(); + } + + @Override + protected void tick(float dt) { + if (this.alive.get()) { + TimeResource worldTimeResource = this.entityStore.getStore().getResource(TimeResource.getResourceType()); + dt *= worldTimeResource.getTimeDilationModifier(); + AssetRegistry.ASSET_LOCK.readLock().lock(); + + try { + this.consumeTaskQueue(); + if (!this.isPaused) { + this.entityStore.getStore().tick(dt); + } else { + this.entityStore.getStore().pausedTick(dt); + } + + if (this.isTicking && !this.isPaused) { + this.chunkStore.getStore().tick(dt); + } else { + this.chunkStore.getStore().pausedTick(dt); + } + + this.consumeTaskQueue(); + } finally { + AssetRegistry.ASSET_LOCK.readLock().unlock(); + } + + this.tick++; + } + } + + @Override + protected void onShutdown() { + this.logger.at(Level.INFO).log("Stopping world %s...", this.name); + this.logger.at(Level.INFO).log("Stopping background threads..."); + long start = System.nanoTime(); + + while (this.chunkLighting.interrupt() || this.worldMapManager.interrupt()) { + this.consumeTaskQueue(); + if (System.nanoTime() - start > 5000000000L) { + break; + } + } + + this.chunkLighting.stop(); + this.worldMapManager.stop(); + this.logger.at(Level.INFO).log("Removing players..."); + + for (PlayerRef playerRef : this.playerRefs) { + if (playerRef.getReference() != null) { + playerRef.removeFromStore(); + } + } + + this.consumeTaskQueue(); + this.logger.at(Level.INFO).log("Waiting for loading chunks..."); + this.chunkStore.waitForLoadingChunks(); + + try { + this.logger.at(Level.INFO).log("Shutting down stores..."); + HytaleServer.get().reportSingleplayerStatus("Saving world '" + this.name + "'"); + this.chunkStore.shutdown(); + this.consumeTaskQueue(); + this.entityStore.shutdown(); + this.consumeTaskQueue(); + } finally { + this.logger.at(Level.INFO).log("Saving Config..."); + if (this.worldConfig.isSavingConfig()) { + try { + Universe.get().getWorldConfigProvider().save(this.savePath, this.worldConfig, this).join(); + } catch (Exception e) { + e.printStackTrace(); + } + } + } + + this.acceptingTasks.set(false); + if (this.alive.getAndSet(false)) { + Universe.get().removeWorldExceptionally(this.name); + } + + HytaleServer.get().reportSingleplayerStatus("Closing world '" + this.name + "'"); + } + + @Override + public void setTps(int tps) { + super.setTps(tps); + SetUpdateRate setUpdateRatePacket = new SetUpdateRate(tps); + this.entityStore + .getStore() + .forEachEntityParallel( + PlayerRef.getComponentType(), + (index, archetypeChunk, commandBuffer) -> archetypeChunk.getComponent(index, PlayerRef.getComponentType()) + .getPacketHandler() + .writeNoCache(setUpdateRatePacket) + ); + } + + public static void setTimeDilation(float timeDilationModifier, @Nonnull ComponentAccessor componentAccessor) { + World world = componentAccessor.getExternalData().getWorld(); + if (!(timeDilationModifier <= 0.01) && !(timeDilationModifier > 4.0F)) { + TimeResource worldTimeResource = componentAccessor.getResource(TimeResource.getResourceType()); + worldTimeResource.setTimeDilationModifier(timeDilationModifier); + SetTimeDilation setTimeDilationPacket = new SetTimeDilation(timeDilationModifier); + + for (PlayerRef playerRef : world.playerRefs) { + playerRef.getPacketHandler().writeNoCache(setTimeDilationPacket); + } + } else { + throw new IllegalArgumentException("TimeDilation is out of bounds (<=0.01 or >4)"); + } + } + + @Nonnull + public String getName() { + return this.name; + } + + public boolean isAlive() { + return this.alive.get(); + } + + @Nonnull + public WorldConfig getWorldConfig() { + return this.worldConfig; + } + + @Nonnull + public DeathConfig getDeathConfig() { + DeathConfig override = this.worldConfig.getDeathConfigOverride(); + return override != null ? override : this.getGameplayConfig().getDeathConfig(); + } + + public int getDaytimeDurationSeconds() { + Integer override = this.worldConfig.getDaytimeDurationSecondsOverride(); + return override != null ? override : this.getGameplayConfig().getWorldConfig().getDaytimeDurationSeconds(); + } + + public int getNighttimeDurationSeconds() { + Integer override = this.worldConfig.getNighttimeDurationSecondsOverride(); + return override != null ? override : this.getGameplayConfig().getWorldConfig().getNighttimeDurationSeconds(); + } + + public boolean isTicking() { + return this.isTicking; + } + + public void setTicking(boolean ticking) { + this.isTicking = ticking; + this.worldConfig.setTicking(ticking); + this.worldConfig.markChanged(); + } + + public boolean isPaused() { + return this.isPaused; + } + + public void setPaused(boolean paused) { + if (this.isPaused != paused) { + this.isPaused = paused; + ServerSetPaused setPaused = new ServerSetPaused(paused); + PlayerUtil.broadcastPacketToPlayersNoCache(this.entityStore.getStore(), setPaused); + } + } + + public long getTick() { + return this.tick; + } + + @Nonnull + public HytaleLogger getLogger() { + return this.logger; + } + + public boolean isCompassUpdating() { + return this.worldConfig.isCompassUpdating(); + } + + public void setCompassUpdating(boolean compassUpdating) { + boolean before = this.worldMapManager.shouldTick(); + this.worldConfig.setCompassUpdating(compassUpdating); + this.worldConfig.markChanged(); + this.worldMapManager.updateTickingState(before); + } + + public void getBlockBulkRelative( + @Nonnull Long2ObjectMap blocks, + @Nonnull IntUnaryOperator xConvert, + @Nonnull IntUnaryOperator yConvert, + @Nonnull IntUnaryOperator zConvert, + @Nonnull World.GenericBlockBulkUpdater consumer + ) { + Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + blocks.forEach((a, b) -> { + int localX = BlockUtil.unpackX(a); + int localY = BlockUtil.unpackY(a); + int localZ = BlockUtil.unpackZ(a); + int x = xConvert.applyAsInt(localX); + int y = yConvert.applyAsInt(localY); + int z = zConvert.applyAsInt(localZ); + long chunkIndex = ChunkUtil.indexChunkFromBlock(x, z); + WorldChunk chunk = chunks.get(chunkIndex); + if (chunk == null) { + chunk = this.getNonTickingChunk(chunkIndex); + chunks.put(chunkIndex, chunk); + } + + consumer.apply(this, (T) b, chunkIndex, chunk, x, y, z, localX, localY, localZ); + }); + } + + @Nullable + public WorldChunk loadChunkIfInMemory(long index) { + if (!this.isInThread()) { + return CompletableFuture.supplyAsync(() -> this.loadChunkIfInMemory(index), this).join(); + } else { + Ref reference = this.chunkStore.getChunkReference(index); + if (reference == null) { + return null; + } else { + WorldChunk worldChunkComponent = this.chunkStore.getStore().getComponent(reference, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + worldChunkComponent.setFlag(ChunkFlag.TICKING, true); + return worldChunkComponent; + } + } + } + + @Nullable + public WorldChunk getChunkIfInMemory(long index) { + Ref reference = this.chunkStore.getChunkReference(index); + if (reference == null) { + return null; + } else { + return !this.isInThread() + ? CompletableFuture.supplyAsync(() -> this.getChunkIfInMemory(index), this).join() + : this.chunkStore.getStore().getComponent(reference, WorldChunk.getComponentType()); + } + } + + @Nullable + public WorldChunk getChunkIfLoaded(long index) { + if (!this.isInThread()) { + return CompletableFuture.supplyAsync(() -> this.getChunkIfLoaded(index), this).join(); + } else { + Ref reference = this.chunkStore.getChunkReference(index); + if (reference == null) { + return null; + } else { + WorldChunk worldChunkComponent = this.chunkStore.getStore().getComponent(reference, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + return worldChunkComponent.is(ChunkFlag.TICKING) ? worldChunkComponent : null; + } + } + } + + @Nullable + public WorldChunk getChunkIfNonTicking(long index) { + if (!this.isInThread()) { + return CompletableFuture.supplyAsync(() -> this.getChunkIfNonTicking(index), this).join(); + } else { + Ref reference = this.chunkStore.getChunkReference(index); + if (reference == null) { + return null; + } else { + WorldChunk worldChunkComponent = this.chunkStore.getStore().getComponent(reference, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + return worldChunkComponent.is(ChunkFlag.TICKING) ? null : worldChunkComponent; + } + } + } + + @Nonnull + @Override + public CompletableFuture getChunkAsync(long index) { + return this.chunkStore + .getChunkReferenceAsync(index, 4) + .thenApplyAsync( + reference -> reference == null ? null : this.chunkStore.getStore().getComponent((Ref) reference, WorldChunk.getComponentType()), this + ); + } + + @Nonnull + @Override + public CompletableFuture getNonTickingChunkAsync(long index) { + return this.chunkStore + .getChunkReferenceAsync(index) + .thenApplyAsync( + reference -> reference == null ? null : this.chunkStore.getStore().getComponent((Ref) reference, WorldChunk.getComponentType()), this + ); + } + + @Deprecated(forRemoval = true) + public List getPlayers() { + if (!this.isInThread()) { + return !this.isStarted() ? Collections.emptyList() : CompletableFuture.supplyAsync(this::getPlayers, this).join(); + } else { + ObjectArrayList players = new ObjectArrayList<>(32); + this.entityStore.getStore().forEachChunk(Player.getComponentType(), (archetypeChunk, commandBuffer) -> { + players.ensureCapacity(players.size() + archetypeChunk.size()); + + for (int index = 0; index < archetypeChunk.size(); index++) { + players.add(archetypeChunk.getComponent(index, Player.getComponentType())); + } + }); + return players; + } + } + + @Nullable + @Deprecated + public Entity getEntity(@Nonnull UUID uuid) { + if (!this.isInThread()) { + return CompletableFuture.supplyAsync(() -> this.getEntity(uuid), this).join(); + } else { + Ref ref = this.entityStore.getRefFromUUID(uuid); + return EntityUtils.getEntity(ref, this.entityStore.getStore()); + } + } + + @Nullable + public Ref getEntityRef(@Nonnull UUID uuid) { + return !this.isInThread() + ? CompletableFuture.>supplyAsync(() -> this.getEntityRef(uuid), this).join() + : this.entityStore.getRefFromUUID(uuid); + } + + public int getPlayerCount() { + return this.players.size(); + } + + @Nonnull + public Collection getPlayerRefs() { + return this.playerRefs; + } + + public void trackPlayerRef(@Nonnull PlayerRef playerRef) { + this.players.put(playerRef.getUuid(), playerRef); + } + + public void untrackPlayerRef(@Nonnull PlayerRef playerRef) { + this.players.remove(playerRef.getUuid(), playerRef); + } + + @Deprecated + @Nullable + public T spawnEntity(T entity, @Nonnull Vector3d position, Vector3f rotation) { + return this.addEntity(entity, position, rotation, AddReason.SPAWN); + } + + @Deprecated + @Nullable + public T addEntity(T entity, @Nonnull Vector3d position, @Nullable Vector3f rotation, @Nonnull AddReason reason) { + if (!EntityModule.get().isKnown(entity)) { + throw new IllegalArgumentException("Unknown entity"); + } else if (entity instanceof Player) { + throw new IllegalArgumentException("Entity can't be a Player!"); + } else if (entity.getNetworkId() == -1) { + throw new IllegalArgumentException("Entity id can't be Entity.UNASSIGNED_ID (-1)!"); + } else if (!this.equals(entity.getWorld())) { + throw new IllegalStateException("Expected entity to already have its world set to " + this.getName() + " but it has " + entity.getWorld()); + } else if (entity.getReference() != null && entity.getReference().isValid()) { + throw new IllegalArgumentException("Entity already has a valid EntityReference: " + entity.getReference()); + } else if (position.getY() < -32.0) { + throw new IllegalArgumentException("Unable to spawn entity below the world! -32 < " + position); + } else if (!this.isInThread()) { + this.logger.at(Level.WARNING).withCause(new SkipSentryException()).log("Warning addEntity was called off thread!"); + this.execute(() -> this.addEntity(entity, position, rotation, reason)); + return entity; + } else { + entity.unloadFromWorld(); + Holder holder = entity.toHolder(); + HeadRotation headRotation = holder.ensureAndGetComponent(HeadRotation.getComponentType()); + if (rotation != null) { + headRotation.teleportRotation(rotation); + } + + holder.addComponent(TransformComponent.getComponentType(), new TransformComponent(position, rotation)); + holder.ensureComponent(UUIDComponent.getComponentType()); + this.entityStore.getStore().addEntity(holder, reason); + return entity; + } + } + + @Override + public void sendMessage(@Nonnull Message message) { + if (!this.isInThread()) { + this.execute(() -> this.sendMessage(message)); + } else { + this.entityStore.getStore().forEachEntityParallel(PlayerRef.getComponentType(), (index, archetypeChunk, commandBuffer) -> { + PlayerRef playerRefComponent = archetypeChunk.getComponent(index, PlayerRef.getComponentType()); + + assert playerRefComponent != null; + + playerRefComponent.sendMessage(message); + }); + this.logger.at(Level.INFO).log("[Broadcast] [Message] %s", MessageUtil.toAnsiString(message).toAnsi(ConsoleModule.get().getTerminal())); + } + } + + @Override + public void execute(@Nonnull Runnable command) { + if (!this.acceptingTasks.get()) { + throw new SkipSentryException(new IllegalThreadStateException("World thread is not accepting tasks: " + this.name + ", " + this.getThread())); + } else { + this.taskQueue.offer(command); + } + } + + @Override + public void consumeTaskQueue() { + this.debugAssertInTickingThread(); + int tickStepNanos = this.getTickStepNanos(); + + Runnable runnable; + while ((runnable = this.taskQueue.poll()) != null) { + try { + long before = System.nanoTime(); + runnable.run(); + long after = System.nanoTime(); + long diff = after - before; + if (diff > tickStepNanos) { + this.logger.at(Level.WARNING).log("Task took %s ns: %s", FormatUtil.nanosToString(diff), runnable); + } + } catch (Exception var9) { + this.logger.at(Level.SEVERE).withCause(var9).log("Failed to run task!"); + } + } + } + + @Nonnull + public ChunkStore getChunkStore() { + return this.chunkStore; + } + + @Nonnull + public EntityStore getEntityStore() { + return this.entityStore; + } + + @Nonnull + public ChunkLightingManager getChunkLighting() { + return this.chunkLighting; + } + + @Nonnull + public WorldMapManager getWorldMapManager() { + return this.worldMapManager; + } + + public WorldPathConfig getWorldPathConfig() { + return this.worldPathConfig; + } + + @Nonnull + public WorldNotificationHandler getNotificationHandler() { + return this.notificationHandler; + } + + @Nonnull + public EventRegistry getEventRegistry() { + return this.eventRegistry; + } + + @Nullable + public CompletableFuture addPlayer(@Nonnull PlayerRef playerRef) { + return this.addPlayer(playerRef, null); + } + + @Nullable + public CompletableFuture addPlayer(@Nonnull PlayerRef playerRef, @Nullable Transform transform) { + return this.addPlayer(playerRef, transform, null, null); + } + + @Nullable + public CompletableFuture addPlayer( + @Nonnull PlayerRef playerRef, + @Deprecated(forRemoval = true) @Nullable Transform transform, + @Nullable Boolean clearWorldOverride, + @Nullable Boolean fadeInOutOverride + ) { + if (!this.alive.get()) { + return CompletableFuture.failedFuture(new IllegalStateException("This world has already been shutdown!")); + } + + // HyFix: Retry loop for race condition when player is being drained from old world + if (playerRef.getReference() != null) { + for (int retry = 0; retry < 5; retry++) { + java.util.concurrent.locks.LockSupport.parkNanos(20_000_000L); // 20ms + if (playerRef.getReference() == null) { + break; + } + } + if (playerRef.getReference() != null) { + throw new IllegalStateException("Player is already in a world"); + } + } + + { + PacketHandler packetHandler = playerRef.getPacketHandler(); + if (!packetHandler.stillActive()) { + return null; + } else { + Holder holder = playerRef.getHolder(); + + assert holder != null; + + TransformComponent transformComponent = holder.getComponent(TransformComponent.getComponentType()); + if (transformComponent == null && transform == null) { + transformComponent = SpawnUtil.applyFirstSpawnTransform(holder, this, this.worldConfig, playerRef.getUuid()); + if (transformComponent == null) { + return CompletableFuture.failedFuture(new IllegalStateException("Spawn provider cannot be null for positioning new entities!")); + } + } + + assert transformComponent != null; + + Player playerComponent = holder.getComponent(Player.getComponentType()); + + assert playerComponent != null; + + boolean firstSpawn = !playerComponent.getPlayerConfigData().getPerWorldData().containsKey(this.name); + playerComponent.setFirstSpawn(firstSpawn); + if (transform != null) { + SpawnUtil.applyTransform(holder, transform); + } + + AddPlayerToWorldEvent event = HytaleServer.get() + .getEventBus() + .dispatchFor(AddPlayerToWorldEvent.class, this.name) + .dispatch(new AddPlayerToWorldEvent(holder, this)); + ChunkTracker chunkTrackerComponent = holder.getComponent(ChunkTracker.getComponentType()); + boolean clearWorld = clearWorldOverride != null ? clearWorldOverride : true; + boolean fadeInOut = fadeInOutOverride != null ? fadeInOutOverride : true; + if (chunkTrackerComponent != null && (clearWorld || fadeInOut)) { + chunkTrackerComponent.setReadyForChunks(false); + } + + Vector3d spawnPosition = transformComponent.getPosition(); + long chunkIndex = ChunkUtil.indexChunkFromBlock(spawnPosition.getX(), spawnPosition.getZ()); + CompletableFuture loadTargetChunkFuture = this.chunkStore + .getChunkReferenceAsync(chunkIndex) + .thenAccept(v -> playerComponent.startClientReadyTimeout()); + CompletableFuture clientReadyFuture = new CompletableFuture<>(); + packetHandler.setClientReadyForChunksFuture(clientReadyFuture); + CompletableFuture setupPlayerFuture = CompletableFuture.runAsync( + () -> this.onSetupPlayerJoining(holder, playerComponent, playerRef, packetHandler, transform, clearWorld, fadeInOut) + ); + CompletableFuture playerReadyFuture = clientReadyFuture.orTimeout(30L, TimeUnit.SECONDS); + return CompletableFuture.allOf(setupPlayerFuture, playerReadyFuture, loadTargetChunkFuture) + .thenApplyAsync(aVoid -> this.onFinishPlayerJoining(playerComponent, playerRef, packetHandler, event.shouldBroadcastJoinMessage()), this) + .exceptionally(throwable -> { + this.logger.at(Level.WARNING).withCause(throwable).log("Exception when adding player to world!"); + playerRef.getPacketHandler().disconnect("Exception when adding player to world!"); + throw new RuntimeException("Exception when adding player '" + playerRef.getUsername() + "' to world '" + this.name + "'", throwable); + }); + } + } + } + + @Nonnull + private PlayerRef onFinishPlayerJoining( + @Nonnull Player playerComponent, @Nonnull PlayerRef playerRefComponent, @Nonnull PacketHandler packetHandler, boolean broadcastJoin + ) { + TimeResource timeResource = this.entityStore.getStore().getResource(TimeResource.getResourceType()); + float timeDilationModifier = timeResource.getTimeDilationModifier(); + int maxViewRadius = HytaleServer.get().getConfig().getMaxViewRadius(); + packetHandler.write( + new ViewRadius(maxViewRadius * 32), + new SetEntitySeed(this.entitySeed.get()), + new SetClientId(playerComponent.getNetworkId()), + new SetTimeDilation(timeDilationModifier) + ); + packetHandler.write(new UpdateFeatures(this.features)); + packetHandler.write(this.worldConfig.getClientEffects().createSunSettingsPacket()); + packetHandler.write(this.worldConfig.getClientEffects().createPostFxSettingsPacket()); + UUID playerUuid = playerRefComponent.getUuid(); + Store store = this.entityStore.getStore(); + WorldTimeResource worldTimeResource = store.getResource(WorldTimeResource.getResourceType()); + World world = store.getExternalData().getWorld(); + packetHandler.writeNoCache(new SetUpdateRate(this.getTps())); + if (this.isPaused) { + this.setPaused(false); + } + + Ref ref = playerRefComponent.addToStore(store); + if (ref != null && ref.isValid()) { + worldTimeResource.sendTimePackets(playerRefComponent); + WorldMapTracker worldMapTracker = playerComponent.getWorldMapTracker(); + worldMapTracker.clear(); + worldMapTracker.sendSettings(world); + if (broadcastJoin) { + Message message = Message.translation("server.general.playerJoinedWorld") + .param("username", playerRefComponent.getUsername()) + .param("world", this.worldConfig.getDisplayName() != null ? this.worldConfig.getDisplayName() : WorldConfig.formatDisplayName(this.name)); + PlayerUtil.broadcastMessageToPlayers(playerUuid, message, store); + } + + TransformComponent transformComponent = store.getComponent(ref, TransformComponent.getComponentType()); + + assert transformComponent != null; + + String position = transformComponent.getPosition().toString(); + this.logger.at(Level.INFO).log("Player '%s' joined world '%s' at location %s (%s)", playerRefComponent.getUsername(), this.name, position, playerUuid); + HeadRotation headRotationComponent = store.getComponent(ref, HeadRotation.getComponentType()); + + assert headRotationComponent != null; + + return playerRefComponent; + } else { + throw new IllegalStateException("Failed to add player ref of joining player to the world store"); + } + } + + private void onSetupPlayerJoining( + @Nonnull Holder holder, + @Nonnull Player playerComponent, + @Nonnull PlayerRef playerRefComponent, + @Nonnull PacketHandler packetHandler, + @Nullable Transform transform, + boolean clearWorld, + boolean fadeInOut + ) { + UUID playerUuid = playerRefComponent.getUuid(); + this.logger + .at(Level.INFO) + .log("Adding player '%s' to world '%s' at location %s (%s)", playerRefComponent.getUsername(), this.name, transform, playerUuid); + int entityId = this.entityStore.takeNextNetworkId(); + playerComponent.setNetworkId(entityId); + PlayerConfigData configData = playerComponent.getPlayerConfigData(); + configData.setWorld(this.name); + if (clearWorld) { + LegacyEntityTrackerSystems.clear(playerComponent, holder); + ChunkTracker chunkTrackerComponent = holder.getComponent(ChunkTracker.getComponentType()); + if (chunkTrackerComponent != null) { + chunkTrackerComponent.clear(); + } + } + + playerComponent.getPageManager().clearCustomPageAcknowledgements(); + JoinWorld packet = new JoinWorld(clearWorld, fadeInOut, this.worldConfig.getUuid()); + packetHandler.write(packet); + packetHandler.tryFlush(); + HytaleLogger.getLogger().at(Level.INFO).log("%s: Sent %s", packetHandler.getIdentifier(), packet); + packetHandler.setQueuePackets(true); + } + + @Nonnull + public CompletableFuture drainPlayersTo(@Nonnull World fallbackTargetWorld) { + return CompletableFuture.completedFuture((Void) null) + .thenComposeAsync( + aVoid -> { + ObjectArrayList> futures = new ObjectArrayList<>(); + + for (PlayerRef playerRef : this.playerRefs) { + Holder holder = playerRef.removeFromStore(); + DrainPlayerFromWorldEvent event = HytaleServer.get() + .getEventBus() + .dispatchFor(DrainPlayerFromWorldEvent.class, this.name) + .dispatch(new DrainPlayerFromWorldEvent(holder, fallbackTargetWorld, null)); + futures.add(event.getWorld().addPlayer(playerRef, event.getTransform())); + } + + return CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new)); + }, + this + ); + } + + @Nonnull + public GameplayConfig getGameplayConfig() { + String gameplayConfigId = this.worldConfig.getGameplayConfig(); + GameplayConfig gameplayConfig = GameplayConfig.getAssetMap().getAsset(gameplayConfigId); + if (gameplayConfig == null) { + gameplayConfig = GameplayConfig.DEFAULT; + } + + return gameplayConfig; + } + + @Nonnull + public Map getFeatures() { + return Collections.unmodifiableMap(this.features); + } + + public boolean isFeatureEnabled(@Nonnull ClientFeature feature) { + return this.features.getOrDefault(feature, false); + } + + public void registerFeature(@Nonnull ClientFeature feature, boolean enabled) { + this.features.put(feature, enabled); + this.broadcastFeatures(); + } + + public void broadcastFeatures() { + UpdateFeatures packet = new UpdateFeatures(this.features); + + for (PlayerRef playerRef : this.playerRefs) { + playerRef.getPacketHandler().write(packet); + } + } + + @Nonnull + public Path getSavePath() { + return this.savePath; + } + + public void updateEntitySeed(@Nonnull Store store) { + int newEntitySeed = this.random.nextInt(); + this.entitySeed.set(newEntitySeed); + PlayerUtil.broadcastPacketToPlayers(store, new SetEntitySeed(newEntitySeed)); + } + + public void markGCHasRun() { + this.gcHasRun = true; + } + + public boolean consumeGCHasRun() { + boolean gcHasRun = this.gcHasRun; + this.gcHasRun = false; + return gcHasRun; + } + + @Override + public int hashCode() { + return this.name != null ? this.name.hashCode() : 0; + } + + @Override + public boolean equals(@Nullable Object o) { + if (this == o) { + return true; + } else if (o != null && this.getClass() == o.getClass()) { + World world = (World) o; + return this.name.equals(world.name); + } else { + return false; + } + } + + @Nonnull + @Override + public String toString() { + return "World{name='" + + this.name + + "', alive=" + + this.alive.get() + + ", loadedChunksCount=" + + this.chunkStore.getLoadedChunksCount() + + ", totalLoadedChunksCount=" + + this.chunkStore.getTotalLoadedChunksCount() + + ", totalGeneratedChunksCount=" + + this.chunkStore.getTotalGeneratedChunksCount() + + ", entityCount=" + + this.entityStore.getStore().getEntityCount() + + "}"; + } + + public void validate(@Nonnull StringBuilder errors, @Nonnull IPrefabBuffer.RawBlockConsumer blockValidator, @Nonnull EnumSet options) throws IOException { + this.setThread(Thread.currentThread()); + this.onStart(); + Store store = this.chunkStore.getStore(); + StringBuilder tempBuilder = new StringBuilder(); + + for (long index : this.chunkStore.getLoader().getIndexes()) { + int chunkX = ChunkUtil.xOfChunkIndex(index); + int chunkZ = ChunkUtil.zOfChunkIndex(index); + + try { + CompletableFuture> future = this.chunkStore.getChunkReferenceAsync(index, 2); + + while (!future.isDone()) { + this.consumeTaskQueue(); + } + + Ref reference = future.join(); + if (reference != null && reference.isValid()) { + WorldChunk chunk = store.getComponent(reference, WorldChunk.getComponentType()); + ChunkColumn chunkColumn = store.getComponent(reference, ChunkColumn.getComponentType()); + if (chunkColumn != null) { + for (Ref section : chunkColumn.getSections()) { + final ChunkSection sectionInfo = store.getComponent(section, ChunkSection.getComponentType()); + BlockSection blockSection = store.getComponent(section, BlockSection.getComponentType()); + if (blockSection != null) { + BlockPhysics blockPhys = store.getComponent(section, BlockPhysics.getComponentType()); + + for (int y = 0; y < 32; y++) { + int worldY = (sectionInfo.getY() << 5) + y; + + for (int z = 0; z < 32; z++) { + for (int x = 0; x < 32; x++) { + int blockId = blockSection.get(x, y, z); + int filler = blockSection.getFiller(x, y, z); + int rotation = blockSection.getRotationIndex(x, y, z); + Holder holder = chunk.getBlockComponentHolder(x, worldY, z); + int worldX = ChunkUtil.minBlock(chunk.getX()) + x; + int worldZ = ChunkUtil.minBlock(chunk.getZ()) + z; + blockValidator.accept( + worldX, worldY, worldZ, blockId, 0, 1.0F, holder, blockPhys != null ? blockPhys.get(x, y, z) : 0, rotation, filler, null + ); + if (options.contains(ValidationOption.BLOCK_FILLER)) { + var fetcher = new FillerBlockUtil.FillerFetcher() { + public int getBlock(BlockSection blockSection, ChunkStore chunkStore, int xx, int yx, int zx) { + if (xx >= 0 && yx >= 0 && zx >= 0 && xx < 32 && yx < 32 && zx < 32) { + return blockSection.get(xx, yx, zx); + } else { + int nx = sectionInfo.getX() + ChunkUtil.chunkCoordinate(xx); + int ny = sectionInfo.getY() + ChunkUtil.chunkCoordinate(yx); + int nz = sectionInfo.getZ() + ChunkUtil.chunkCoordinate(zx); + CompletableFuture> refFuture = chunkStore.getChunkSectionReferenceAsync(nx, ny, nz); + + while (!refFuture.isDone()) { + World.this.consumeTaskQueue(); + } + + Ref ref = refFuture.join(); + BlockSection blocks = chunkStore.getStore().getComponent(ref, BlockSection.getComponentType()); + return blocks == null ? Integer.MIN_VALUE : blocks.get(xx, yx, zx); + } + } + + public int getFiller(BlockSection blockSection, ChunkStore chunkStore, int xx, int yx, int zx) { + if (xx >= 0 && yx >= 0 && zx >= 0 && xx < 32 && yx < 32 && zx < 32) { + return blockSection.getFiller(xx, yx, zx); + } else { + int nx = sectionInfo.getX() + ChunkUtil.chunkCoordinate(xx); + int ny = sectionInfo.getY() + ChunkUtil.chunkCoordinate(yx); + int nz = sectionInfo.getZ() + ChunkUtil.chunkCoordinate(zx); + CompletableFuture> refFuture = chunkStore.getChunkSectionReferenceAsync(nx, ny, nz); + + while (!refFuture.isDone()) { + World.this.consumeTaskQueue(); + } + + Ref ref = refFuture.join(); + BlockSection blocks = chunkStore.getStore().getComponent(ref, BlockSection.getComponentType()); + return blocks == null ? Integer.MIN_VALUE : blocks.getFiller(xx, yx, zx); + } + } + + public int getRotationIndex(BlockSection blockSection, ChunkStore chunkStore, int xx, int yx, int zx) { + if (xx >= 0 && yx >= 0 && zx >= 0 && xx < 32 && yx < 32 && zx < 32) { + return blockSection.getFiller(xx, yx, zx); + } else { + int nx = sectionInfo.getX() + ChunkUtil.chunkCoordinate(xx); + int ny = sectionInfo.getY() + ChunkUtil.chunkCoordinate(yx); + int nz = sectionInfo.getZ() + ChunkUtil.chunkCoordinate(zx); + CompletableFuture> refFuture = chunkStore.getChunkSectionReferenceAsync(nx, ny, nz); + + while (!refFuture.isDone()) { + World.this.consumeTaskQueue(); + } + + Ref ref = refFuture.join(); + BlockSection blocks = chunkStore.getStore().getComponent(ref, BlockSection.getComponentType()); + return blocks == null ? Integer.MIN_VALUE : blocks.getRotationIndex(xx, yx, zx); + } + } + }; + FillerBlockUtil.ValidationResult fillerResult = FillerBlockUtil.validateBlock( + x, y, z, blockId, rotation, filler, blockSection, this.chunkStore, fetcher + ); + switch (fillerResult) { + case OK: + default: + break; + case INVALID_BLOCK: { + BlockType blockType = BlockType.getAssetMap().getAsset(blockId); + tempBuilder.append("\tBlock ") + .append(blockType != null ? blockType.getId() : "") + .append(" at ") + .append(x) + .append(", ") + .append(y) + .append(", ") + .append(z) + .append(" is not valid filler") + .append('\n'); + break; + } + case INVALID_FILLER: { + BlockType blockType = BlockType.getAssetMap().getAsset(blockId); + tempBuilder.append("\tBlock ") + .append(blockType != null ? blockType.getId() : "") + .append(" at ") + .append(x) + .append(", ") + .append(y) + .append(", ") + .append(z) + .append(" has invalid/missing filler blocks") + .append('\n'); + } + } + } + } + } + } + + if (!tempBuilder.isEmpty()) { + errors.append("\tChunk ") + .append(sectionInfo.getX()) + .append(", ") + .append(sectionInfo.getY()) + .append(", ") + .append(sectionInfo.getZ()) + .append(" validation errors:") + .append((CharSequence) tempBuilder); + tempBuilder.setLength(0); + } + } + } + + if (options.contains(ValidationOption.ENTITIES)) { + ComponentType> unknownComponentType = EntityStore.REGISTRY.getUnknownComponentType(); + + for (Holder entityHolder : chunk.getEntityChunk().getEntityHolders()) { + UnknownComponents unknownComponents = entityHolder.getComponent(unknownComponentType); + if (unknownComponents != null && !unknownComponents.getUnknownComponents().isEmpty()) { + errors.append("\tUnknown Entity Components: ").append(unknownComponents.getUnknownComponents()).append("\n"); + } + } + } + + store.tick(1.0F); + } + } + } catch (CompletionException var35) { + this.getLogger().at(Level.SEVERE).withCause(var35).log("Failed to validate chunk: %d, %d", chunkX, chunkZ); + errors.append('\t') + .append("Exception validating chunk: ") + .append(chunkX) + .append(", ") + .append(chunkZ) + .append('\n') + .append("\t\t") + .append(var35.getCause().getMessage()) + .append('\n'); + } + } + + if (this.alive.getAndSet(false)) { + this.onShutdown(); + } + + this.setThread(null); + } + + @FunctionalInterface + public interface GenericBlockBulkUpdater { + void apply(World var1, T var2, long var3, WorldChunk var5, int var6, int var7, int var8, int var9, int var10, int var11); + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/WorldMapTracker.java b/src/com/hypixel/hytale/server/core/universe/world/WorldMapTracker.java new file mode 100644 index 00000000..8d2b1d30 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/WorldMapTracker.java @@ -0,0 +1,660 @@ +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> 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 ref, + @Nullable WorldMapTracker.ZoneDiscoveryInfo zoneDiscoveryInfo, + @Nullable String biomeName, + @Nonnull ComponentAccessor 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 ref, @Nonnull WorldMapTracker.ZoneDiscoveryInfo zoneDiscoveryInfo, @Nonnull ComponentAccessor 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 currentUnloadList = null; + List> 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 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 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 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 currentLoadList = null; + List> 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 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 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 currentLoadList = null; + List> 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 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 unloadList : allLoadLists) { + this.writeUpdatePacket(unloadList); + } + } + + this.writeUpdatePacket(currentLoadList); + return maxGeneration; + } + + private void writeUpdatePacket(@Nullable List 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 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 store = world.getEntityStore().getStore(); + Ref 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 discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones(); + if (!discoveredZones.contains(zoneName)) { + Set 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 discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones(); + if (discoveredZones.contains(zoneName)) { + Set 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 zoneNames) { + Set discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones(); + if (!discoveredZones.containsAll(zoneNames)) { + Set 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 zoneNames) { + Set discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones(); + if (discoveredZones.containsAll(zoneNames)) { + Set 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 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 + ); + } + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/chunk/BlockComponentChunk.java b/src/com/hypixel/hytale/server/core/universe/world/chunk/BlockComponentChunk.java new file mode 100644 index 00000000..425ee11e --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/chunk/BlockComponentChunk.java @@ -0,0 +1,429 @@ +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 { + public static final BuilderCodec 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> map = new Int2ObjectOpenHashMap<>(entityChunk.entityHolders.size() + entityChunk.entityReferences.size()); + map.putAll(entityChunk.entityHolders); + + for (Entry> entry : entityChunk.entityReferences.int2ObjectEntrySet()) { + Ref reference = entry.getValue(); + Store 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> entityHolders; + @Nonnull + private final Int2ObjectMap> entityReferences; + @Nonnull + private final Int2ObjectMap> entityHoldersUnmodifiable; + @Nonnull + private final Int2ObjectMap> entityReferencesUnmodifiable; + private boolean needsSaving; + + public static ComponentType 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> entityHolders, @Nonnull Int2ObjectMap> entityReferences) { + this.entityHolders = entityHolders; + this.entityReferences = entityReferences; + this.entityHoldersUnmodifiable = Int2ObjectMaps.unmodifiable(entityHolders); + this.entityReferencesUnmodifiable = Int2ObjectMaps.unmodifiable(entityReferences); + } + + @Nonnull + @Override + public Component clone() { + Int2ObjectOpenHashMap> entityHoldersClone = new Int2ObjectOpenHashMap<>(this.entityHolders.size() + this.entityReferences.size()); + + for (Entry> entry : this.entityHolders.int2ObjectEntrySet()) { + entityHoldersClone.put(entry.getIntKey(), entry.getValue().clone()); + } + + for (Entry> entry : this.entityReferences.int2ObjectEntrySet()) { + Ref reference = entry.getValue(); + entityHoldersClone.put(entry.getIntKey(), reference.getStore().copyEntity(reference)); + } + + return new BlockComponentChunk(entityHoldersClone, new Int2ObjectOpenHashMap<>()); + } + + @Nonnull + @Override + public Component cloneSerializable() { + ComponentRegistry.Data data = ChunkStore.REGISTRY.getData(); + Int2ObjectOpenHashMap> entityHoldersClone = new Int2ObjectOpenHashMap<>(this.entityHolders.size() + this.entityReferences.size()); + + for (Entry> entry : this.entityHolders.int2ObjectEntrySet()) { + Holder holder = entry.getValue(); + if (holder.getArchetype().hasSerializableComponents(data)) { + entityHoldersClone.put(entry.getIntKey(), holder.cloneSerializable(data)); + } + } + + for (Entry> entryx : this.entityReferences.int2ObjectEntrySet()) { + Ref reference = entryx.getValue(); + Store 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> getEntityHolders() { + return this.entityHoldersUnmodifiable; + } + + @Nullable + public Holder getEntityHolder(int index) { + return this.entityHolders.get(index); + } + + public void addEntityHolder(int index, @Nonnull Holder 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 holder) { + if (this.entityHolders.putIfAbsent(index, Objects.requireNonNull(holder)) != null) { + throw new IllegalArgumentException("Duplicate block components (entity holder) at: " + index); + } + } + + @Nullable + public Holder removeEntityHolder(int index) { + Holder reference = this.entityHolders.remove(index); + if (reference != null) { + this.markNeedsSaving(); + } + + return reference; + } + + @Nonnull + public Int2ObjectMap> getEntityReferences() { + return this.entityReferencesUnmodifiable; + } + + @Nullable + public Ref getEntityReference(int index) { + return this.entityReferences.get(index); + } + + public void addEntityReference(int index, @Nonnull Ref 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 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 reference) { + if (this.entityReferences.remove(index, reference)) { + this.markNeedsSaving(); + } + } + + public void unloadEntityReference(int index, Ref reference) { + this.entityReferences.remove(index, reference); + } + + @Nullable + public Int2ObjectMap> takeEntityHolders() { + if (this.entityHolders.isEmpty()) { + return null; + } else { + Int2ObjectOpenHashMap> holders = new Int2ObjectOpenHashMap<>(this.entityHolders); + this.entityHolders.clear(); + return holders; + } + } + + @Nullable + public Int2ObjectMap> takeEntityReferences() { + if (this.entityReferences.isEmpty()) { + return null; + } else { + Int2ObjectOpenHashMap> holders = new Int2ObjectOpenHashMap<>(this.entityReferences); + this.entityReferences.clear(); + return holders; + } + } + + @Nullable + public > T getComponent(int index, @Nonnull ComponentType componentType) { + Ref reference = this.entityReferences.get(index); + if (reference != null) { + return reference.getStore().getComponent(reference, componentType); + } else { + Holder 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> { + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + private final Archetype archetype = Archetype.of(WorldChunk.getComponentType(), BlockComponentChunk.getComponentType()); + + public BlockComponentChunkLoadingSystem() { + } + + @Override + public Query getQuery() { + return this.archetype; + } + + @Nonnull + @Override + public ComponentType> componentType() { + return ChunkStore.REGISTRY.getNonTickingComponentType(); + } + + public void onComponentAdded( + @Nonnull Ref ref, + @Nonnull NonTicking component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + BlockComponentChunk blockComponentChunk = store.getComponent(ref, BlockComponentChunk.getComponentType()); + Int2ObjectMap> entityReferences = blockComponentChunk.takeEntityReferences(); + if (entityReferences != null) { + int size = entityReferences.size(); + int[] indexes = new int[size]; + Ref[] references = new Ref[size]; + int j = 0; + + for (Entry> entry : entityReferences.int2ObjectEntrySet()) { + indexes[j] = entry.getIntKey(); + references[j] = entry.getValue(); + j++; + } + + ComponentRegistry.Data data = ChunkStore.REGISTRY.getData(); + + for (int i = 0; i < size; i++) { + if (store.getArchetype(references[i]).hasSerializableComponents(data)) { + Holder 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 ref, + NonTicking oldComponent, + @Nonnull NonTicking newComponent, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + } + + public void onComponentRemoved( + @Nonnull Ref ref, + @Nonnull NonTicking component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + WorldChunk chunk = store.getComponent(ref, WorldChunk.getComponentType()); + BlockComponentChunk blockComponentChunk = store.getComponent(ref, BlockComponentChunk.getComponentType()); + Int2ObjectMap> entityHolders = blockComponentChunk.takeEntityHolders(); + if (entityHolders != null) { + int holderCount = entityHolders.size(); + int[] indexes = new int[holderCount]; + Holder[] holders = new Holder[holderCount]; + int j = 0; + + for (Entry> entry : entityHolders.int2ObjectEntrySet()) { + indexes[j] = entry.getIntKey(); + holders[j] = entry.getValue(); + j++; + } + + for (int i = holderCount - 1; i >= 0; i--) { + Holder 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 componentType; + + public LoadBlockComponentPacketSystem(ComponentType blockComponentChunkComponentType) { + this.componentType = blockComponentChunkComponentType; + } + + @Override + public Query getQuery() { + return this.componentType; + } + + public void fetch( + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + CommandBuffer commandBuffer, + PlayerRef player, + @Nonnull List results + ) { + BlockComponentChunk component = archetypeChunk.getComponent(index, this.componentType); + ObjectCollection> references = component.entityReferences.values(); + Store 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 componentType; + + public UnloadBlockComponentPacketSystem(ComponentType blockComponentChunkComponentType) { + this.componentType = blockComponentChunkComponentType; + } + + @Override + public Query getQuery() { + return this.componentType; + } + + public void fetch( + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + CommandBuffer commandBuffer, + PlayerRef player, + @Nonnull List results + ) { + BlockComponentChunk component = archetypeChunk.getComponent(index, this.componentType); + ObjectCollection> references = component.entityReferences.values(); + Store componentStore = store.getExternalData().getWorld().getChunkStore().getStore(); + componentStore.fetch(references, ChunkStore.UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE, player, results); + } + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/lighting/ChunkLightingManager.java b/src/com/hypixel/hytale/server/core/universe/world/lighting/ChunkLightingManager.java new file mode 100644 index 00000000..811b98c7 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/lighting/ChunkLightingManager.java @@ -0,0 +1,241 @@ +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 set = ConcurrentHashMap.newKeySet(); + private final ObjectArrayFIFOQueue 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)); + } + }); + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/meta/state/RespawnBlock.java b/src/com/hypixel/hytale/server/core/universe/world/meta/state/RespawnBlock.java new file mode 100644 index 00000000..c2b1f559 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/meta/state/RespawnBlock.java @@ -0,0 +1,157 @@ +package com.hypixel.hytale.server.core.universe.world.meta.state; + +import com.hypixel.hytale.codec.Codec; +import com.hypixel.hytale.codec.KeyedCodec; +import com.hypixel.hytale.codec.builder.BuilderCodec; +import com.hypixel.hytale.common.util.ArrayUtil; +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.Store; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.system.RefSystem; +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.entity.entities.Player; +import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerRespawnPointData; +import com.hypixel.hytale.server.core.entity.entities.player.data.PlayerWorldData; +import com.hypixel.hytale.server.core.modules.block.BlockModule; +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.chunk.WorldChunk; +import com.hypixel.hytale.server.core.universe.world.storage.ChunkStore; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.UUID; +import java.util.logging.Level; + +public class RespawnBlock implements Component { + public static final BuilderCodec CODEC = BuilderCodec.builder(RespawnBlock.class, RespawnBlock::new) + .append( + new KeyedCodec<>("OwnerUUID", Codec.UUID_BINARY), + (respawnBlockState, uuid) -> respawnBlockState.ownerUUID = uuid, + respawnBlockState -> respawnBlockState.ownerUUID + ) + .add() + .build(); + private UUID ownerUUID; + + public static ComponentType getComponentType() { + return BlockModule.get().getRespawnBlockComponentType(); + } + + public RespawnBlock() { + } + + public RespawnBlock(UUID ownerUUID) { + this.ownerUUID = ownerUUID; + } + + public UUID getOwnerUUID() { + return this.ownerUUID; + } + + public void setOwnerUUID(UUID ownerUUID) { + this.ownerUUID = ownerUUID; + } + + @Nullable + @Override + public Component clone() { + return new RespawnBlock(this.ownerUUID); + } + + public static class OnRemove extends RefSystem { + @Nonnull + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + public static final ComponentType COMPONENT_TYPE_RESPAWN_BLOCK = RespawnBlock.getComponentType(); + public static final ComponentType COMPONENT_TYPE_BLOCK_STATE_INFO = BlockModule.BlockStateInfo.getComponentType(); + @Nonnull + public static final Query QUERY = Query.and(COMPONENT_TYPE_RESPAWN_BLOCK, COMPONENT_TYPE_BLOCK_STATE_INFO); + + public OnRemove() { + } + + @Override + public void onEntityAdded( + @Nonnull Ref ref, @Nonnull AddReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + } + + @Override + public void onEntityRemove( + @Nonnull Ref ref, @Nonnull RemoveReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + if (reason != RemoveReason.UNLOAD) { + RespawnBlock respawnState = commandBuffer.getComponent(ref, COMPONENT_TYPE_RESPAWN_BLOCK); + + assert respawnState != null; + + if (respawnState.ownerUUID != null) { + BlockModule.BlockStateInfo blockStateInfoComponent = commandBuffer.getComponent(ref, COMPONENT_TYPE_BLOCK_STATE_INFO); + + assert blockStateInfoComponent != null; + + PlayerRef playerRef = Universe.get().getPlayer(respawnState.ownerUUID); + if (playerRef == null) { + LOGGER.at(Level.WARNING).log("Failed to fetch player ref during removal of respawn block entity."); + } else { + Player playerComponent = playerRef.getComponent(Player.getComponentType()); + if (playerComponent == null) { + LOGGER.at(Level.WARNING).log("Failed to fetch player component during removal of respawn block entity."); + } else { + Ref chunkRef = blockStateInfoComponent.getChunkRef(); + if (chunkRef.isValid()) { + World world = commandBuffer.getExternalData().getWorld(); + PlayerWorldData playerWorldData = playerComponent.getPlayerConfigData().getPerWorldData(world.getName()); + PlayerRespawnPointData[] respawnPoints = playerWorldData.getRespawnPoints(); + if (respawnPoints == null) { + LOGGER.at(Level.WARNING) + .log("Failed to find valid respawn points for player " + respawnState.ownerUUID + " during removal of respawn block entity."); + } else { + WorldChunk worldChunkComponent = commandBuffer.getComponent(chunkRef, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + Vector3i blockPosition = new Vector3i( + ChunkUtil.worldCoordFromLocalCoord(worldChunkComponent.getX(), ChunkUtil.xFromBlockInColumn(blockStateInfoComponent.getIndex())), + ChunkUtil.yFromBlockInColumn(blockStateInfoComponent.getIndex()), + ChunkUtil.worldCoordFromLocalCoord(worldChunkComponent.getZ(), ChunkUtil.zFromBlockInColumn(blockStateInfoComponent.getIndex())) + ); + + for (int i = 0; i < respawnPoints.length; i++) { + PlayerRespawnPointData respawnPoint = respawnPoints[i]; + if (respawnPoint.getBlockPosition().equals(blockPosition)) { + LOGGER.at(Level.INFO) + .log( + "Removing respawn point for player " + + respawnState.ownerUUID + + " at position " + + blockPosition + + " due to respawn block removal." + ); + playerWorldData.setRespawnPoints(ArrayUtil.remove(respawnPoints, i)); + return; + } + } + } + } + } + } + } + } + } + + @Nullable + @Override + public Query getQuery() { + return QUERY; + } + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java b/src/com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java new file mode 100644 index 00000000..05b507bc --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/storage/ChunkStore.java @@ -0,0 +1,922 @@ +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.LongSet; +import it.unimi.dsi.fastutil.longs.LongSets; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap.Entry; +import java.io.BufferedWriter; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.io.PrintWriter; +import java.io.StringWriter; +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; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +public class ChunkStore implements WorldProvider { + @Nonnull + public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + public static final MetricsRegistry METRICS_REGISTRY = new MetricsRegistry() + .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 REGISTRY = new ComponentRegistry<>(); + public static final CodecKey> HOLDER_CODEC_KEY = new CodecKey<>("ChunkHolder"); + @Nonnull + public static final SystemType LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( + ChunkStore.LoadPacketDataQuerySystem.class + ); + @Nonnull + public static final SystemType LOAD_FUTURE_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( + ChunkStore.LoadFuturePacketDataQuerySystem.class + ); + @Nonnull + public static final SystemType UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType( + ChunkStore.UnloadPacketDataQuerySystem.class + ); + @Nonnull + public static final ResourceType UNLOAD_RESOURCE = REGISTRY.registerResource( + ChunkUnloadingSystem.Data.class, ChunkUnloadingSystem.Data::new + ); + @Nonnull + public static final ResourceType SAVE_RESOURCE = REGISTRY.registerResource( + ChunkSavingSystems.Data.class, ChunkSavingSystems.Data::new + ); + public static final SystemGroup INIT_GROUP = REGISTRY.registerSystemGroup(); + @Nonnull + private final World world; + @Nonnull + private final Long2ObjectConcurrentHashMap chunks = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND); + private Store store; + @Nullable + private IChunkLoader loader; + @Nullable + private IChunkSaver saver; + @Nullable + private IWorldGen generator; + @Nonnull + private CompletableFuture 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 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 entry : this.chunks.long2ObjectEntrySet()) { + ChunkStore.ChunkLoadState chunkState = entry.getValue(); + long stamp = chunkState.lock.readLock(); + + try { + CompletableFuture> 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 add(@Nonnull Holder 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 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 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 getChunkReference(long index) { + ChunkStore.ChunkLoadState chunkState = this.chunks.get(index); + if (chunkState == null) { + return null; + } else { + long stamp = chunkState.lock.tryOptimisticRead(); + Ref 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 getChunkSectionReference(int x, int y, int z) { + Ref 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 getChunkSectionReference(@Nonnull ComponentAccessor commandBuffer, int x, int y, int z) { + Ref 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> 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 store = ref.getStore(); + ChunkColumn chunkColumnComponent = store.getComponent((Ref)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 getChunkComponent(long index, @Nonnull ComponentType componentType) { + Ref reference = this.getChunkReference(index); + return reference != null && reference.isValid() ? this.store.getComponent(reference, componentType) : null; + } + + @Nonnull + public CompletableFuture> getChunkReferenceAsync(long index) { + return this.getChunkReferenceAsync(index, 0); + } + + @Nonnull + public CompletableFuture> 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 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)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)reference) + : this.loader.loadHolder(x, z).thenApplyAsync(holder -> { + if (holder != null && !this.store.isShutdown()) { + this.totalLoadedChunksCount.getAndIncrement(); + return this.preLoadChunkAsync(index, (Holder)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 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.>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)reference); + } else { + long readStampx = this.generatorLock.readLock(); + + CompletableFuture 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.>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.>thenApplyAsync(reference -> { + if (reference != null) { + WorldChunk worldChunkComponent = this.store.getComponent((Ref)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, removing the corrupted chunk data, + * and triggering regeneration. + */ + @Nonnull + private CompletableFuture> 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' - removing and regenerating. See logs/corrupted_chunks.log for details.", + x, z, this.world.getName() + ); + + // Remove the corrupted chunk data + CompletableFuture removeFuture; + if (this.saver != null) { + removeFuture = this.saver.removeHolder(x, z).exceptionally(removeError -> { + LOGGER.at(Level.WARNING).withCause(removeError).log( + "Failed to remove corrupted chunk data at (%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 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 preLoadChunkAsync(long index, @Nonnull Holder 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 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 postLoadChunk(@Nullable Holder 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 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> future; + @Nullable + private Ref 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 { + public ChunkLoaderSaverSetupSystem() { + } + + @Nullable + @Override + public SystemGroup getGroup() { + return ChunkStore.INIT_GROUP; + } + + @Override + public void onSystemAddedToStore(@Nonnull Store 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 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> { + public LoadFuturePacketDataQuerySystem() { + } + } + + public abstract static class LoadPacketDataQuerySystem extends EntityDataSystem { + public LoadPacketDataQuerySystem() { + } + } + + public abstract static class UnloadPacketDataQuerySystem extends EntityDataSystem { + public UnloadPacketDataQuerySystem() { + } + } +} diff --git a/src/com/hypixel/hytale/server/core/universe/world/storage/EntityStore.java b/src/com/hypixel/hytale/server/core/universe/world/storage/EntityStore.java new file mode 100644 index 00000000..27f3d420 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/universe/world/storage/EntityStore.java @@ -0,0 +1,183 @@ +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 METRICS_REGISTRY = new MetricsRegistry() + .register("Store", EntityStore::getStore, Store.METRICS_REGISTRY); + @Nonnull + public static final ComponentRegistry REGISTRY = new ComponentRegistry<>(); + @Nonnull + public static final CodecKey> HOLDER_CODEC_KEY = new CodecKey<>("EntityHolder"); + @Nonnull + public static final SystemGroup SEND_PACKET_GROUP = REGISTRY.registerSystemGroup(); + @Nonnull + private final AtomicInteger networkIdCounter = new AtomicInteger(1); + @Nonnull + private final World world; + private Store store; + @Nonnull + private final Map> entitiesByUuid = new ConcurrentHashMap<>(); + @Nonnull + private final Int2ObjectMap> 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 getStore() { + return this.store; + } + + @Nullable + public Ref getRefFromUUID(@Nonnull UUID uuid) { + return this.entitiesByUuid.get(uuid); + } + + @Nullable + public Ref 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 { + public NetworkIdSystem() { + } + + @Nonnull + @Override + public Query getQuery() { + return NetworkId.getComponentType(); + } + + @Override + public void onEntityAdded( + @Nonnull Ref ref, @Nonnull AddReason reason, @Nonnull Store store, @Nonnull CommandBuffer 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 ref, @Nonnull RemoveReason reason, @Nonnull Store store, @Nonnull CommandBuffer 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 { + @Nonnull + private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass(); + + public UUIDSystem() { + } + + @Nonnull + @Override + public Query getQuery() { + return UUIDComponent.getComponentType(); + } + + @Override + public void onEntityAdded( + @Nonnull Ref ref, @Nonnull AddReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + UUIDComponent uuidComponent = commandBuffer.getComponent(ref, UUIDComponent.getComponentType()); + + assert uuidComponent != null; + + Ref 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 ref, @Nonnull RemoveReason reason, @Nonnull Store store, @Nonnull CommandBuffer 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); + } + } +} diff --git a/src/com/hypixel/hytale/server/core/util/thread/TickingThread.java b/src/com/hypixel/hytale/server/core/util/thread/TickingThread.java new file mode 100644 index 00000000..d7124c43 --- /dev/null +++ b/src/com/hypixel/hytale/server/core/util/thread/TickingThread.java @@ -0,0 +1,217 @@ +package com.hypixel.hytale.server.core.util.thread; + +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.metrics.metric.HistoricMetric; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.logging.Level; + +public abstract class TickingThread implements Runnable { + public static final int NANOS_IN_ONE_MILLI = 1000000; + public static final int NANOS_IN_ONE_SECOND = 1000000000; + public static final int TPS = 30; + public static long SLEEP_OFFSET = 3000000L; + private final String threadName; + private final boolean daemon; + private final AtomicBoolean needsShutdown = new AtomicBoolean(true); + private int tps; + private int tickStepNanos; + private HistoricMetric bufferedTickLengthMetricSet; + @Nullable + private Thread thread; + @Nonnull + private CompletableFuture startedFuture = new CompletableFuture<>(); + + public TickingThread(String threadName) { + this(threadName, 30, false); + } + + public TickingThread(String threadName, int tps, boolean daemon) { + this.threadName = threadName; + this.daemon = daemon; + this.tps = tps; + this.tickStepNanos = 1000000000 / tps; + this.bufferedTickLengthMetricSet = HistoricMetric.builder(this.tickStepNanos, TimeUnit.NANOSECONDS) + .addPeriod(10L, TimeUnit.SECONDS) + .addPeriod(1L, TimeUnit.MINUTES) + .addPeriod(5L, TimeUnit.MINUTES) + .build(); + } + + @Override + public void run() { + try { + this.onStart(); + this.startedFuture.complete(null); + long beforeTick = System.nanoTime() - this.tickStepNanos; + + while (this.thread != null && !this.thread.isInterrupted()) { + long delta; + if (!this.isIdle()) { + while ((delta = System.nanoTime() - beforeTick) < this.tickStepNanos) { + Thread.onSpinWait(); + } + } else { + delta = System.nanoTime() - beforeTick; + } + + beforeTick = System.nanoTime(); + this.tick((float) delta / 1.0E9F); + long tickLength = System.nanoTime() - beforeTick; + this.bufferedTickLengthMetricSet.add(System.nanoTime(), tickLength); + long sleepLength = this.tickStepNanos - tickLength; + if (!this.isIdle()) { + sleepLength -= SLEEP_OFFSET; + } + + if (sleepLength > 0L) { + Thread.sleep(sleepLength / 1000000L); + } + } + } catch (InterruptedException var9) { + Thread.currentThread().interrupt(); + } catch (Throwable var10) { + HytaleLogger.getLogger().at(Level.SEVERE).withCause(var10).log("Exception in thread %s:", this.thread); + } + + if (this.needsShutdown.getAndSet(false)) { + this.onShutdown(); + } + } + + protected boolean isIdle() { + return false; + } + + protected abstract void tick(float var1); + + protected void onStart() { + } + + protected abstract void onShutdown(); + + @Nonnull + public CompletableFuture start() { + if (this.thread == null) { + this.thread = new Thread(this, this.threadName); + this.thread.setDaemon(this.daemon); + } else if (this.thread.isAlive()) { + throw new IllegalStateException("Thread '" + this.thread.getName() + "' is already started!"); + } + + this.thread.start(); + return this.startedFuture; + } + + public boolean interrupt() { + if (this.thread != null && this.thread.isAlive()) { + this.thread.interrupt(); + return true; + } else { + return false; + } + } + + @SuppressWarnings("deprecation") + public void stop() { + Thread thread = this.thread; + if (thread != null) { + try { + int i = 0; + + while (thread.isAlive()) { + thread.interrupt(); + thread.join(this.tickStepNanos / 1000000); + i += this.tickStepNanos / 1000000; + if (i > 30000) { + StringBuilder sb = new StringBuilder(); + + for (StackTraceElement traceElement : thread.getStackTrace()) { + sb.append("\tat ").append(traceElement).append('\n'); + } + + 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 var9 = null; + if (this.needsShutdown.getAndSet(false)) { + this.onShutdown(); + } + + return; + } + } + + Thread var10 = null; + } catch (InterruptedException var8) { + Thread.currentThread().interrupt(); + } + } + } + + public void setTps(int tps) { + this.debugAssertInTickingThread(); + if (tps > 0 && tps <= 2048) { + this.tps = tps; + this.tickStepNanos = 1000000000 / tps; + this.bufferedTickLengthMetricSet = HistoricMetric.builder(this.tickStepNanos, TimeUnit.NANOSECONDS) + .addPeriod(10L, TimeUnit.SECONDS) + .addPeriod(1L, TimeUnit.MINUTES) + .addPeriod(5L, TimeUnit.MINUTES) + .build(); + } else { + throw new IllegalArgumentException("UpdatesPerSecond is out of bounds (<=0 or >2048): " + tps); + } + } + + public int getTps() { + return this.tps; + } + + public int getTickStepNanos() { + return this.tickStepNanos; + } + + public HistoricMetric getBufferedTickLengthMetricSet() { + return this.bufferedTickLengthMetricSet; + } + + public void clearMetrics() { + this.bufferedTickLengthMetricSet.clear(); + } + + public void debugAssertInTickingThread() { + if (!Thread.currentThread().equals(this.thread) && this.thread != null) { + throw new AssertionError("Assert not in ticking thread!"); + } + } + + public boolean isInThread() { + return Thread.currentThread().equals(this.thread); + } + + public boolean isStarted() { + return this.thread != null && this.thread.isAlive() && this.needsShutdown.get(); + } + + @Deprecated + protected void setThread(Thread thread) { + this.thread = thread; + } + + @Nullable + protected Thread getThread() { + return this.thread; + } +} diff --git a/src/com/hypixel/hytale/server/npc/movement/controllers/MotionControllerBase.java b/src/com/hypixel/hytale/server/npc/movement/controllers/MotionControllerBase.java new file mode 100644 index 00000000..d4df3b09 --- /dev/null +++ b/src/com/hypixel/hytale/server/npc/movement/controllers/MotionControllerBase.java @@ -0,0 +1,1299 @@ +package com.hypixel.hytale.server.npc.movement.controllers; + +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.logger.HytaleLogger; +import com.hypixel.hytale.math.shape.Box; +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.math.vector.Vector3f; +import com.hypixel.hytale.protocol.GameMode; +import com.hypixel.hytale.protocol.MovementSettings; +import com.hypixel.hytale.protocol.MovementStates; +import com.hypixel.hytale.protocol.Rangef; +import com.hypixel.hytale.server.core.asset.modifiers.MovementEffects; +import com.hypixel.hytale.server.core.asset.type.blocktype.config.RotationTuple; +import com.hypixel.hytale.server.core.asset.type.model.config.Model; +import com.hypixel.hytale.server.core.asset.type.model.config.camera.CameraSettings; +import com.hypixel.hytale.server.core.entity.InteractionManager; +import com.hypixel.hytale.server.core.entity.entities.player.movement.MovementManager; +import com.hypixel.hytale.server.core.modules.collision.BlockCollisionData; +import com.hypixel.hytale.server.core.modules.collision.CollisionModule; +import com.hypixel.hytale.server.core.modules.collision.CollisionModuleConfig; +import com.hypixel.hytale.server.core.modules.collision.CollisionResult; +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.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.damage.Damage; +import com.hypixel.hytale.server.core.modules.entity.damage.DamageCause; +import com.hypixel.hytale.server.core.modules.entity.damage.DamageSystems; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; +import com.hypixel.hytale.server.core.modules.interaction.InteractionModule; +import com.hypixel.hytale.server.core.modules.physics.component.PhysicsValues; +import com.hypixel.hytale.server.core.modules.physics.util.PhysicsMath; +import com.hypixel.hytale.server.core.modules.splitvelocity.SplitVelocity; +import com.hypixel.hytale.server.core.modules.splitvelocity.VelocityConfig; +import com.hypixel.hytale.server.core.universe.world.World; +import com.hypixel.hytale.server.core.universe.world.chunk.WorldChunk; +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.npc.NPCPlugin; +import com.hypixel.hytale.server.npc.asset.builder.BuilderSupport; +import com.hypixel.hytale.server.npc.entities.NPCEntity; +import com.hypixel.hytale.server.npc.movement.MotionKind; +import com.hypixel.hytale.server.npc.movement.NavState; +import com.hypixel.hytale.server.npc.movement.Steering; +import com.hypixel.hytale.server.npc.movement.controllers.builders.BuilderMotionControllerBase; +import com.hypixel.hytale.server.npc.role.Role; +import com.hypixel.hytale.server.npc.role.RoleDebugFlags; +import com.hypixel.hytale.server.npc.util.NPCPhysicsMath; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Objects; +import java.util.function.BiPredicate; +import java.util.logging.Level; + +public abstract class MotionControllerBase implements MotionController { + public static final double FORCE_SCALE = 5.0; + public static final double BISECT_DIST = 0.05; + public static final double FILTER_COEFFICIENT = 0.7; + public static final double DOT_PRODUCT_EPSILON = 0.001; + public static final double DEFAULT_BLOCK_DRAG = 0.82; + protected static final HytaleLogger LOGGER = NPCPlugin.get().getLogger(); + public static final boolean DEBUG_APPLIED_FORCES = false; + @Nonnull + protected final NPCEntity entity; + protected final String type; + protected final double epsilonSpeed; + protected final float epsilonAngle; + protected final double forceVelocityDamping; + protected final double maxHorizontalSpeed; + protected final double fastMotionThreshold; + protected final double fastMotionThresholdRange; + protected final float maxHeadRotationSpeed; + protected Role role; + protected double inertia; + protected double knockbackScale; + protected double gravity; + @Nullable + protected float[] headPitchAngleRange; + protected boolean debugModeSteer; + protected boolean debugModeMove; + protected boolean debugModeCollisions; + protected boolean debugModeBlockCollisions; + protected boolean debugModeProbeBlockCollisions; + protected boolean debugModeValidatePositions; + protected boolean debugModeOverlaps; + protected boolean debugModeValidateMath; + protected final Vector3d position = new Vector3d(); + protected final Box collisionBoundingBox = new Box(); + protected final CollisionResult collisionResult = new CollisionResult(); + protected final Vector3d translation = new Vector3d(); + protected final Vector3d bisectValidPosition = new Vector3d(); + protected final Vector3d bisectInvalidPosition = new Vector3d(); + protected final Vector3d lastValidPosition = new Vector3d(); + protected final Vector3d forceVelocity = new Vector3d(); + protected final Vector3d appliedForce = new Vector3d(); + protected boolean ignoreDamping; + protected final List appliedVelocities = new ObjectArrayList<>(); + protected boolean isObstructed; + protected NavState navState; + protected double throttleDuration; + protected double targetDeltaSquared; + protected boolean recomputePath; + protected final Vector3d worldNormal = Vector3d.UP; + protected final Vector3d worldAntiNormal = Vector3d.DOWN; + protected final Vector3d componentSelector = new Vector3d(1.0, 0.0, 1.0); + protected final Vector3d planarComponentSelector = new Vector3d(1.0, 0.0, 1.0); + protected boolean enableTriggers = true; + protected boolean enableBlockDamage = true; + protected boolean isReceivingBlockDamage; + protected boolean isAvoidingBlockDamage = true; + protected boolean requiresPreciseMovement; + protected boolean requiresDepthProbing; + protected boolean havePreciseMovementTarget; + @Nonnull + protected Vector3d preciseMovementTarget = new Vector3d(); + protected boolean isRelaxedMoveConstraints; + protected boolean isBlendingHeading; + protected double blendHeading; + protected boolean haveBlendHeadingPosition; + @Nonnull + protected Vector3d blendHeadingPosition = new Vector3d(); + protected double blendLevelAtTargetPosition = 0.5; + protected boolean fastMotionKind; + protected boolean idleMotionKind; + protected boolean horizontalIdleKind; + protected double moveSpeed; + protected double previousSpeed; + protected MotionKind motionKind; + protected MotionKind lastMovementStateUpdatedMotionKind; + protected MotionKind previousMotionKind; + protected double effectHorizontalSpeedMultiplier; + protected boolean cachedMovementBlocked; + private float yaw; + private float pitch; + private float roll; + private final Vector3d beforeTriggerForce = new Vector3d(); + private final Vector3d beforeTriggerPosition = new Vector3d(); + private boolean processTriggersHasMoved; + protected MovementSettings movementSettings; + + public MotionControllerBase(@Nonnull BuilderSupport builderSupport, @Nonnull BuilderMotionControllerBase builder) { + this.entity = builderSupport.getEntity(); + this.type = builder.getType(); + this.epsilonSpeed = builder.getEpsilonSpeed(); + this.epsilonAngle = builder.getEpsilonAngle(); + this.forceVelocityDamping = builder.getForceVelocityDamping(); + this.maxHorizontalSpeed = builder.getMaxHorizontalSpeed(builderSupport); + this.fastMotionThreshold = builder.getFastHorizontalThreshold(builderSupport); + this.fastMotionThresholdRange = builder.getFastHorizontalThresholdRange(); + this.maxHeadRotationSpeed = builder.getMaxHeadRotationSpeed(builderSupport); + this.setInertia(1.0); + this.setKnockbackScale(1.0); + this.setGravity(10.0); + } + + @Override + public Role getRole() { + return this.role; + } + + @Override + public void setRole(Role role) { + this.role = role; + } + + @Override + public void setInertia(double inertia) { + this.inertia = Math.max(inertia, 1.0E-4); + } + + @Override + public void setKnockbackScale(double knockbackScale) { + this.knockbackScale = Math.max(0.0, knockbackScale); + } + + @Override + public void updateModelParameters(Ref ref, Model model, @Nonnull Box boundingBox, ComponentAccessor componentAccessor) { + Objects.requireNonNull(boundingBox, "updateModelParameters: MotionController needs a bounding box"); + this.collisionBoundingBox.assign(boundingBox); + } + + @Override + public void setHeadPitchAngleRange(float[] headPitchAngleRange) { + if (headPitchAngleRange == null) { + this.headPitchAngleRange = null; + } else { + assert headPitchAngleRange.length == 2; + + this.headPitchAngleRange = (float[]) headPitchAngleRange.clone(); + } + } + + protected void readEntityPosition(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { + TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); + + assert transformComponent != null; + + Vector3f bodyRotation = transformComponent.getRotation(); + this.position.assign(transformComponent.getPosition()); + this.yaw = bodyRotation.getY(); + this.pitch = bodyRotation.getPitch(); + this.roll = bodyRotation.getRoll(); + this.adjustReadPosition(ref, componentAccessor); + this.postReadPosition(ref, componentAccessor); + } + + public void postReadPosition(Ref ref, ComponentAccessor componentAccessor) { + } + + public void moveEntity(@Nonnull Ref ref, double dt, @Nonnull ComponentAccessor componentAccessor) { + TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); + + assert transformComponent != null; + + this.adjustWritePosition(ref, dt, componentAccessor); + Vector3f bodyRotation = transformComponent.getRotation(); + bodyRotation.setYaw(this.yaw); + bodyRotation.setPitch(this.pitch); + bodyRotation.setRoll(this.roll); + this.entity.moveTo(ref, this.position.x, this.position.y, this.position.z, componentAccessor); + } + + public float getYaw() { + return this.yaw; + } + + public float getPitch() { + return this.pitch; + } + + public float getRoll() { + return this.roll; + } + + public boolean touchesWater(boolean defaultValue, @Nonnull ComponentAccessor componentAccessor) { + World world = componentAccessor.getExternalData().getWorld(); + ChunkStore chunkStore = world.getChunkStore(); + long chunkIndex = ChunkUtil.indexChunkFromBlock(this.position.getX(), this.position.getZ()); + Ref chunkRef = chunkStore.getChunkReference(chunkIndex); + if (chunkRef != null && chunkRef.isValid()) { + WorldChunk worldChunkComponent = chunkStore.getStore().getComponent(chunkRef, WorldChunk.getComponentType()); + + assert worldChunkComponent != null; + + int blockX = MathUtil.floor(this.position.getX()); + int blockY = MathUtil.floor(this.position.getY() + this.collisionBoundingBox.min.y); + int blockZ = MathUtil.floor(this.position.getZ()); + int fluidId = worldChunkComponent.getFluidId(blockX, blockY, blockZ); + return fluidId != 0; + } else { + return defaultValue; + } + } + + @Override + public void updateMovementState( + @Nonnull Ref ref, + @Nonnull MovementStates movementStates, + @Nonnull Steering steering, + @Nonnull Vector3d velocity, + @Nonnull ComponentAccessor componentAccessor + ) { + // we skipped a tick + if (this.motionKind == null) return; + + boolean lastFastMotion = movementStates.running; + movementStates.climbing = false; + movementStates.swimJumping = false; + movementStates.inFluid = this.touchesWater(movementStates.inFluid, componentAccessor); + movementStates.onGround = this.role.isOnGround(); + double speed = this.waypointDistance(Vector3d.ZERO, velocity); + speed = 0.7 * this.previousSpeed + 0.30000000000000004 * speed; + this.previousSpeed = speed; + this.fastMotionKind = this.isFastMotionKind(speed); + this.idleMotionKind = steering.getTranslation().equals(Vector3d.ZERO); + this.horizontalIdleKind = this.isHorizontalIdle(speed); + if (this.motionKind != this.lastMovementStateUpdatedMotionKind + || lastFastMotion != this.fastMotionKind + || movementStates.idle != this.idleMotionKind + || movementStates.horizontalIdle != this.horizontalIdleKind) { + switch (this.motionKind) { + case FLYING: + this.updateFlyingStates(movementStates, this.idleMotionKind, this.fastMotionKind); + break; + case SWIMMING: + this.updateSwimmingStates(movementStates, this.idleMotionKind, this.fastMotionKind, this.horizontalIdleKind); + break; + case SWIMMING_TURNING: + this.updateSwimmingStates(movementStates, false, true, false); + break; + case ASCENDING: + this.updateAscendingStates(ref, movementStates, this.fastMotionKind, this.horizontalIdleKind, componentAccessor); + break; + case MOVING: + updateMovingStates(ref, movementStates, this.fastMotionKind, componentAccessor); + break; + case DESCENDING: + NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); + + assert npcComponent != null; + + this.updateDescendingStates(ref, movementStates, this.fastMotionKind, npcComponent.getHoverHeight() > 0.0, componentAccessor); + break; + case DROPPING: + this.updateDroppingStates(movementStates); + break; + case STANDING: + default: + NPCEntity npcComponent2 = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); + + assert npcComponent2 != null; + + this.updateStandingStates(movementStates, this.motionKind, npcComponent2.getHoverHeight() > 0.0); + } + } + + this.lastMovementStateUpdatedMotionKind = this.motionKind; + } + + protected abstract boolean isFastMotionKind(double var1); + + protected void updateFlyingStates(@Nonnull MovementStates movementStates, boolean idle, boolean fastMotionKind) { + movementStates.flying = true; + movementStates.idle = idle; + movementStates.horizontalIdle = false; + movementStates.walking = !fastMotionKind; + movementStates.running = fastMotionKind; + movementStates.falling = false; + movementStates.swimming = false; + movementStates.jumping = false; + } + + protected void updateSwimmingStates(@Nonnull MovementStates movementStates, boolean idle, boolean fastMotionKind, boolean horizontalIdleKind) { + movementStates.flying = false; + movementStates.idle = idle; + movementStates.horizontalIdle = horizontalIdleKind; + movementStates.walking = !fastMotionKind; + movementStates.running = fastMotionKind; + movementStates.falling = false; + movementStates.swimming = true; + movementStates.jumping = false; + } + + protected static void updateMovingStates( + @Nonnull Ref ref, @Nonnull MovementStates movementStates, boolean fastMotionKind, @Nonnull ComponentAccessor componentAccessor + ) { + NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); + + assert npcComponent != null; + + movementStates.flying = npcComponent.getHoverHeight() > 0.0; + movementStates.idle = false; + movementStates.horizontalIdle = false; + movementStates.falling = false; + movementStates.walking = !fastMotionKind; + movementStates.running = fastMotionKind; + movementStates.swimming = false; + movementStates.jumping = false; + } + + protected void updateAscendingStates( + @Nonnull Ref ref, + @Nonnull MovementStates movementStates, + boolean fastMotionKind, + boolean horizontalIdleKind, + @Nonnull ComponentAccessor componentAccessor + ) { + updateMovingStates(ref, movementStates, fastMotionKind, componentAccessor); + } + + protected void updateDescendingStates( + @Nonnull Ref ref, + @Nonnull MovementStates movementStates, + boolean fastMotionKind, + boolean hovering, + @Nonnull ComponentAccessor componentAccessor + ) { + updateMovingStates(ref, movementStates, fastMotionKind, componentAccessor); + } + + protected void updateDroppingStates(@Nonnull MovementStates movementStates) { + movementStates.falling = true; + } + + protected void updateStandingStates(@Nonnull MovementStates movementStates, @Nonnull MotionKind motionKind, boolean hovering) { + movementStates.flying = hovering; + movementStates.idle = true; + movementStates.horizontalIdle = true; + movementStates.walking = false; + movementStates.running = false; + movementStates.falling = false; + movementStates.swimming = false; + movementStates.jumping = false; + } + + @Override + public double steer( + @Nonnull Ref ref, + @Nonnull Role role, + @Nonnull Steering bodySteering, + @Nonnull Steering headSteering, + double interval, + @Nonnull ComponentAccessor componentAccessor + ) { + this.readEntityPosition(ref, componentAccessor); + if (this.debugModeSteer) { + double dx = this.position.x; + double dz = this.position.z; + double st = this.steer0(ref, role, bodySteering, headSteering, interval, componentAccessor); + double t = interval - st; + dx = this.position.x - dx; + dz = this.position.z - dz; + double l = Math.sqrt(dx * dx + dz * dz); + double v = t > 0.0 ? l / t : 0.0; + LOGGER.at(Level.INFO) + .log( + "== Steer %s = t =%.4f dt=%.4f h =%.4f l =%.4f v =%.4f motion=%s", + this.getType(), + interval, + t, + (180.0F / (float) Math.PI) * this.yaw, + l, + v, + role.getSteeringMotionName() + ); + return st; + } else { + return this.steer0(ref, role, bodySteering, headSteering, interval, componentAccessor); + } + } + + public double steer0( + @Nonnull Ref ref, + @Nonnull Role role, + @Nonnull Steering bodySteering, + @Nonnull Steering headSteering, + double interval, + @Nonnull ComponentAccessor componentAccessor + ) { + World world = componentAccessor.getExternalData().getWorld(); + NPCEntity npcComponent = componentAccessor.getComponent(ref, NPCEntity.getComponentType()); + + assert npcComponent != null; + + this.effectHorizontalSpeedMultiplier = npcComponent.getCurrentHorizontalSpeedMultiplier(ref, componentAccessor); + this.setAvoidingBlockDamage(this.isAvoidingBlockDamage && !this.isReceivingBlockDamage); + this.translation.assign(0.0); + this.cachedMovementBlocked = this.isMovementBlocked(ref, componentAccessor); + this.computeMove(ref, role, bodySteering, interval, this.translation, componentAccessor); + if (this.debugModeValidateMath && !NPCPhysicsMath.isValid(this.translation)) { + throw new IllegalArgumentException(String.valueOf(this.translation)); + } else { + if (this.translation.squaredLength() > 1000000.0) { + if (this.debugModeValidateMath) { + LOGGER.at(Level.WARNING) + .log("NPC with role %s has abnormal high speed! (Distance=%s, MotionController=%s)", role.getRoleName(), this.translation.length(), this.type); + } + + this.translation.assign(Vector3d.ZERO); + } + + this.executeMove(ref, role, interval, this.translation, componentAccessor); + this.postExecuteMove(); + this.clearRequirePreciseMovement(); + this.clearRequireDepthProbing(); + this.clearBlendHeading(); + this.setAvoidingBlockDamage(!this.isReceivingBlockDamage); + this.setRelaxedMoveConstraints(false); + float maxBodyRotation = (float) (interval * this.getCurrentMaxBodyRotationSpeed() * bodySteering.getRelativeTurnSpeed()); + float maxHeadRotation = (float) (interval * this.maxHeadRotationSpeed * headSteering.getRelativeTurnSpeed() * this.effectHorizontalSpeedMultiplier); + this.calculateYaw(ref, bodySteering, headSteering, maxHeadRotation, maxBodyRotation, componentAccessor); + this.calculatePitch(ref, bodySteering, headSteering, maxHeadRotation, componentAccessor); + this.calculateRoll(bodySteering, headSteering); + this.moveEntity(ref, interval, componentAccessor); + HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType()); + + assert headRotationComponent != null; + + Vector3f headRotation = headRotationComponent.getRotation(); + headRotation.setYaw(headSteering.getYaw()); + headRotation.setPitch(headSteering.getPitch()); + headRotation.setRoll(headSteering.getRoll()); + if (!this.forceVelocity.equals(Vector3d.ZERO) && !this.ignoreDamping) { + double movementThresholdSquared = 1.0000000000000002E-10; + if (this.forceVelocity.squaredLength() >= movementThresholdSquared) { + this.dampForceVelocity(this.forceVelocity, this.forceVelocityDamping, interval, componentAccessor); + } else { + this.forceVelocity.assign(Vector3d.ZERO); + } + } + + double clientTps = 60.0; + int serverTps = world.getTps(); + double rate = clientTps / serverTps; + boolean dampenY = this.shouldDampenAppliedVelocitiesY(); + boolean useGroundResistance = this.shouldAlwaysUseGroundResistance() || this.onGround(); + + for (int i = 0; i < this.appliedVelocities.size(); i++) { + MotionControllerBase.AppliedVelocity entry = this.appliedVelocities.get(i); + float min; + float max; + if (useGroundResistance) { + min = entry.config.getGroundResistance(); + max = entry.config.getGroundResistanceMax(); + } else { + min = entry.config.getAirResistance(); + max = entry.config.getAirResistanceMax(); + } + + float resistance = min; + if (max >= 0.0F) { + resistance = switch (entry.config.getStyle()) { + case Linear -> { + float len = (float) entry.velocity.length(); + if (len < entry.config.getThreshold()) { + float mul = len / entry.config.getThreshold(); + yield min * mul + max * (1.0F - mul); + } else { + yield min; + } + } + case Exp -> { + float len = (float) entry.velocity.squaredLength(); + if (len < entry.config.getThreshold() * entry.config.getThreshold()) { + float mul = len / (entry.config.getThreshold() * entry.config.getThreshold()); + yield min * mul + max * (1.0F - mul); + } else { + yield min; + } + } + }; + } + + double resistanceScale = Math.pow(resistance, rate); + entry.velocity.x *= resistanceScale; + entry.velocity.z *= resistanceScale; + if (dampenY) { + entry.velocity.y *= resistanceScale; + } + } + + this.appliedVelocities.removeIf(v -> v.velocity.squaredLength() < 0.001); + return interval; + } + } + + protected boolean shouldDampenAppliedVelocitiesY() { + return false; + } + + protected boolean shouldAlwaysUseGroundResistance() { + return false; + } + + protected void calculateYaw( + @Nonnull Ref ref, + @Nonnull Steering bodySteering, + @Nonnull Steering headSteering, + float maxHeadRotation, + float maxBodyRotation, + @Nonnull ComponentAccessor componentAccessor + ) { + if (bodySteering.hasYaw()) { + this.yaw = bodySteering.getYaw(); + } else if (NPCPhysicsMath.dotProduct(this.translation.x, 0.0, this.translation.z) > 0.001) { + this.yaw = PhysicsMath.headingFromDirection(this.translation.x, this.translation.z); + } + + boolean hasHeadSteering = headSteering.hasYaw(); + if (!hasHeadSteering) { + headSteering.setYaw(this.yaw); + } + + HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType()); + + assert headRotationComponent != null; + + ModelComponent modelComponent = componentAccessor.getComponent(ref, ModelComponent.getComponentType()); + + assert modelComponent != null; + + Vector3f headRotation = headRotationComponent.getRotation(); + float currentYaw = headRotation.getYaw(); + float targetYaw = headSteering.getYaw(); + float turnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentYaw, targetYaw), -maxHeadRotation, maxHeadRotation); + headSteering.setYaw(PhysicsMath.normalizeTurnAngle(currentYaw + turnAngle)); + if (hasHeadSteering) { + float yawOffset = MathUtil.wrapAngle(headSteering.getYaw() - this.yaw); + CameraSettings headRotationRestrictions = modelComponent.getModel().getCamera(); + float yawMin; + float yawMax; + if (headRotationRestrictions != null && headRotationRestrictions.getYaw() != null && headRotationRestrictions.getYaw().getAngleRange() != null) { + Rangef yawRange = headRotationRestrictions.getYaw().getAngleRange(); + yawMin = yawRange.min * (float) (Math.PI / 180.0); + yawMax = yawRange.max * (float) (Math.PI / 180.0); + } else { + yawMin = (float) (-Math.PI / 4); + yawMax = (float) (Math.PI / 4); + } + + if (yawOffset > yawMax) { + float initialBodyYaw = this.yaw; + if (!bodySteering.hasYaw()) { + this.yaw = this.blendBodyYaw(ref, yawOffset, maxBodyRotation, componentAccessor); + } + + headSteering.setYaw(MathUtil.wrapAngle(initialBodyYaw + yawMax)); + } else if (yawOffset < yawMin) { + float initialBodyYaw = this.yaw; + if (!bodySteering.hasYaw()) { + this.yaw = this.blendBodyYaw(ref, yawOffset, maxBodyRotation, componentAccessor); + } + + headSteering.setYaw(MathUtil.wrapAngle(initialBodyYaw + yawMin)); + } + } + } + + protected float blendBodyYaw( + @Nonnull Ref ref, float yawOffset, float maxBodyRotation, @Nonnull ComponentAccessor componentAccessor + ) { + TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); + + assert transformComponent != null; + + Vector3f bodyRotation = transformComponent.getRotation(); + float currentBodyYaw = bodyRotation.getYaw(); + float targetBodyYaw = MathUtil.wrapAngle(this.yaw + yawOffset); + float bodyTurnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentBodyYaw, targetBodyYaw), -maxBodyRotation, maxBodyRotation); + return MathUtil.wrapAngle(this.yaw + bodyTurnAngle); + } + + protected void calculatePitch( + @Nonnull Ref ref, + @Nonnull Steering bodySteering, + @Nonnull Steering headSteering, + float maxHeadRotation, + @Nonnull ComponentAccessor componentAccessor + ) { + if (bodySteering.hasPitch()) { + this.pitch = bodySteering.getPitch(); + } else if (NPCPhysicsMath.dotProduct(this.translation.x, this.translation.y, this.translation.z) > 0.001) { + this.pitch = PhysicsMath.pitchFromDirection(this.translation.x, this.translation.y, this.translation.z); + } + + boolean hasHeadSteering = headSteering.hasPitch(); + if (!hasHeadSteering) { + headSteering.setPitch(this.pitch); + } + + HeadRotation headRotationComponent = componentAccessor.getComponent(ref, HeadRotation.getComponentType()); + + assert headRotationComponent != null; + + Vector3f headRotation = headRotationComponent.getRotation(); + float currentPitch = headRotation.getPitch(); + float targetPitch = headSteering.getPitch(); + float turnAngle = MathUtil.clamp(NPCPhysicsMath.turnAngle(currentPitch, targetPitch), -maxHeadRotation, maxHeadRotation); + headSteering.setPitch(PhysicsMath.normalizeTurnAngle(currentPitch + turnAngle)); + if (hasHeadSteering) { + ModelComponent modelComponent = componentAccessor.getComponent(ref, ModelComponent.getComponentType()); + + assert modelComponent != null; + + float bodyPitch = this.pitch; + float pitchOffset = MathUtil.wrapAngle(headSteering.getPitch() - bodyPitch); + CameraSettings headRotationRestrictions = modelComponent.getModel().getCamera(); + float pitchMin; + float pitchMax; + if (this.headPitchAngleRange != null) { + pitchMin = this.headPitchAngleRange[0]; + pitchMax = this.headPitchAngleRange[1]; + } else if (headRotationRestrictions != null + && headRotationRestrictions.getPitch() != null + && headRotationRestrictions.getPitch().getAngleRange() != null) { + Rangef pitchRange = headRotationRestrictions.getPitch().getAngleRange(); + pitchMin = pitchRange.min * (float) (Math.PI / 180.0); + pitchMax = pitchRange.max * (float) (Math.PI / 180.0); + } else { + pitchMin = (float) (-Math.PI / 4); + pitchMax = (float) (Math.PI / 4); + } + + if (pitchOffset > pitchMax) { + headSteering.setPitch(MathUtil.wrapAngle(bodyPitch + pitchMax)); + } else if (pitchOffset < pitchMin) { + headSteering.setPitch(MathUtil.wrapAngle(bodyPitch + pitchMin)); + } + } + } + + protected void calculateRoll(@Nonnull Steering bodySteering, @Nonnull Steering headSteering) { + if (bodySteering.hasRoll()) { + this.roll = bodySteering.getRoll(); + } + + if (!headSteering.hasRoll()) { + headSteering.setRoll(this.roll); + } + } + + protected void dampForceVelocity( + @Nonnull Vector3d forceVelocity, double forceVelocityDamping, double interval, ComponentAccessor componentAccessor + ) { + World world = componentAccessor.getExternalData().getWorld(); + double drag = 0.0; + if (this.motionKind != MotionKind.FLYING) { + if (!this.onGround() && this.motionKind != MotionKind.SWIMMING && this.motionKind != MotionKind.SWIMMING_TURNING) { + double horizontalSpeed = Math.sqrt(forceVelocity.x * forceVelocity.x + forceVelocity.z * forceVelocity.z); + drag = convertToNewRange( + horizontalSpeed, + this.movementSettings.airDragMinSpeed, + this.movementSettings.airDragMaxSpeed, + this.movementSettings.airDragMin, + this.movementSettings.airDragMax + ); + } else { + drag = 0.82; + } + } + + double clientTps = 60.0; + int serverTps = world.getTps(); + double rate = 60.0 / serverTps; + drag = Math.pow(drag, rate); + forceVelocity.x *= drag; + forceVelocity.z *= drag; + float velocityEpsilon = 0.1F; + if (Math.abs(forceVelocity.x) <= velocityEpsilon) { + forceVelocity.x = 0.0; + } + + if (Math.abs(forceVelocity.y) <= velocityEpsilon) { + forceVelocity.y = 0.0; + } + + if (Math.abs(forceVelocity.z) <= velocityEpsilon) { + forceVelocity.z = 0.0; + } + } + + private static double convertToNewRange(double value, double oldMinRange, double oldMaxRange, double newMinRange, double newMaxRange) { + if (newMinRange != newMaxRange && oldMinRange != oldMaxRange) { + double newValue = (value - oldMinRange) * (newMaxRange - newMinRange) / (oldMaxRange - oldMinRange) + newMinRange; + return MathUtil.clamp(newValue, Math.min(newMinRange, newMaxRange), Math.max(newMinRange, newMaxRange)); + } else { + return newMinRange; + } + } + + @Override + public double probeMove( + @Nonnull Ref ref, + @Nonnull Vector3d position, + @Nonnull Vector3d direction, + @Nonnull ProbeMoveData probeMoveData, + @Nonnull ComponentAccessor componentAccessor + ) { + probeMoveData.setPosition(position).setDirection(direction); + return this.probeMove(ref, probeMoveData, componentAccessor); + } + + protected void postExecuteMove() { + } + + protected void adjustReadPosition(Ref ref, ComponentAccessor componentAccessor) { + } + + protected void adjustWritePosition(Ref ref, double dt, @Nonnull ComponentAccessor componentAccessor) { + } + + @Override + public boolean isInProgress() { + return false; + } + + @Override + public boolean isObstructed() { + return this.isObstructed; + } + + @Override + public NavState getNavState() { + return this.navState; + } + + @Override + public double getThrottleDuration() { + return this.throttleDuration; + } + + @Override + public double getTargetDeltaSquared() { + return this.targetDeltaSquared; + } + + @Override + public void setNavState(NavState navState, double throttleDuration, double targetDeltaSquared) { + this.navState = navState; + this.throttleDuration = throttleDuration; + this.targetDeltaSquared = targetDeltaSquared; + } + + @Override + public boolean isForceRecomputePath() { + return this.recomputePath; + } + + @Override + public void setForceRecomputePath(boolean recomputePath) { + this.recomputePath = recomputePath; + } + + @Override + public void beforeInstructionSensorsAndActions(double physicsTickDuration) { + this.recomputePath = false; + } + + @Override + public void beforeInstructionMotion(double physicsTickDuration) { + this.resetNavState(); + } + + public boolean isHorizontalIdle(double speed) { + return speed == 0.0; + } + + @Override + public boolean canAct(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { + return this.isAlive(ref, componentAccessor) + && this.role.couldBreatheCached() + && this.forceVelocity.equals(Vector3d.ZERO) + && this.appliedVelocities.isEmpty(); + } + + public boolean isMovementBlocked(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { + InteractionManager interactionManager = componentAccessor.getComponent(ref, InteractionModule.get().getInteractionManagerComponent()); + if (interactionManager != null) { + Boolean movementBlocked = interactionManager.forEachInteraction((chain, interaction, val) -> { + if (val) { + return Boolean.TRUE; + } else { + MovementEffects movementEffects = interaction.getEffects().getMovementEffects(); + return movementEffects != null ? movementEffects.isDisableAll() : Boolean.FALSE; + } + }, Boolean.FALSE); + return movementBlocked; + } else { + return false; + } + } + + protected abstract double computeMove( + @Nonnull Ref var1, @Nonnull Role var2, Steering var3, double var4, Vector3d var6, @Nonnull ComponentAccessor var7 + ); + + protected abstract double executeMove( + @Nonnull Ref var1, @Nonnull Role var2, double var3, Vector3d var5, @Nonnull ComponentAccessor var6 + ); + + public double bisect( + @Nonnull Vector3d validPosition, @Nonnull Vector3d invalidPosition, @Nonnull T t, @Nonnull BiPredicate validate, @Nonnull Vector3d result + ) { + return this.bisect(validPosition, invalidPosition, t, validate, 0.05, result); + } + + public double bisect( + @Nonnull Vector3d validPosition, + @Nonnull Vector3d invalidPosition, + @Nonnull T t, + @Nonnull BiPredicate validate, + double maxDistance, + @Nonnull Vector3d result + ) { + double validDistance = 0.0; + double invalidDistance = 1.0; + this.bisectValidPosition.assign(validPosition); + this.bisectInvalidPosition.assign(invalidPosition); + maxDistance *= maxDistance; + double validWeight = 0.1; + double invalidWeight = 0.9; + + while (this.bisectValidPosition.distanceSquaredTo(this.bisectInvalidPosition) > maxDistance) { + double distance = validWeight * validDistance + invalidWeight * invalidDistance; + result.x = validWeight * this.bisectValidPosition.x + invalidWeight * this.bisectInvalidPosition.x; + result.y = validWeight * this.bisectValidPosition.y + invalidWeight * this.bisectInvalidPosition.y; + result.z = validWeight * this.bisectValidPosition.z + invalidWeight * this.bisectInvalidPosition.z; + if (validate.test(t, result)) { + validDistance = distance; + this.bisectValidPosition.assign(result); + } else { + invalidDistance = distance; + this.bisectInvalidPosition.assign(result); + validWeight = 0.5; + invalidWeight = 0.5; + } + } + + result.assign(this.bisectValidPosition); + return validDistance; + } + + @Nonnull + @Override + public Vector3d getForce() { + return this.forceVelocity; + } + + @Override + public void addForce(@Nonnull Vector3d force, VelocityConfig velocityConfig) { + double scale = this.knockbackScale; + if (!SplitVelocity.SHOULD_MODIFY_VELOCITY && velocityConfig != null) { + this.appliedVelocities.add(new MotionControllerBase.AppliedVelocity(new Vector3d(force.x * scale, force.y * scale, force.z * scale), velocityConfig)); + } else { + double horzMul = 0.18000000000000005 * this.movementSettings.velocityResistance; + this.forceVelocity.add(force.x * scale * horzMul, force.y * scale, force.z * scale * horzMul); + this.appliedForce.assign(this.forceVelocity); + this.ignoreDamping = false; + } + } + + @Override + public void forceVelocity(@Nonnull Vector3d velocity, @Nullable VelocityConfig velocityConfig, boolean ignoreDamping) { + if (!SplitVelocity.SHOULD_MODIFY_VELOCITY && velocityConfig != null) { + this.appliedVelocities.clear(); + this.appliedVelocities.add(new MotionControllerBase.AppliedVelocity(velocity.clone(), velocityConfig)); + } else { + this.forceVelocity.assign(velocity); + this.ignoreDamping = ignoreDamping; + } + } + + public void clearForce() { + this.forceVelocity.assign(Vector3d.ZERO); + } + + protected void dumpCollisionResults() { + String slideString = ""; + if (this.collisionResult.isSliding) { + slideString = String.format("SLIDE: start/end=%f/%f", this.collisionResult.slideStart, this.collisionResult.slideEnd); + } + + LOGGER.at(Level.INFO) + .log( + "CollRes: pos=%s yaw=%f count=%d %s", + Vector3d.formatShortString(this.position), + (180.0F / (float) Math.PI) * this.yaw, + this.collisionResult.getBlockCollisionCount(), + slideString + ); + if (this.collisionResult.getBlockCollisionCount() > 0) { + for (int i = 0; i < this.collisionResult.getBlockCollisionCount(); i++) { + BlockCollisionData cd = this.collisionResult.getBlockCollision(i); + String materialName = cd.blockMaterial != null ? cd.blockMaterial.name() : "none"; + String typeName = cd.blockType != null ? cd.blockType.getId() : "none"; + String hitboxName = cd.blockType != null ? cd.blockType.getHitboxType() : "none"; + String rotation; + if (cd.blockType != null) { + RotationTuple blockRotation = RotationTuple.get(cd.rotation); + rotation = blockRotation.yaw() + " " + blockRotation.pitch(); + } else { + rotation = "none"; + } + + LOGGER.at(Level.INFO) + .log( + " COLL: blk=%s/%s/%s start=%f norm=%s pos=%s mat=%s block=%s hitbox=%s rot=%s", + cd.x, + cd.y, + cd.z, + cd.collisionStart, + Vector3d.formatShortString(cd.collisionNormal), + Vector3d.formatShortString(cd.collisionPoint), + materialName, + typeName, + hitboxName, + rotation + ); + } + } + } + + public void setEnableTriggers(boolean enableTriggers) { + this.enableTriggers = enableTriggers; + } + + public void setEnableBlockDamage(boolean enableBlockDamage) { + this.enableBlockDamage = enableBlockDamage; + } + + @Override + public boolean willReceiveBlockDamage() { + return this.isReceivingBlockDamage; + } + + @Override + public void setAvoidingBlockDamage(boolean avoid) { + this.isAvoidingBlockDamage = avoid; + } + + @Override + public boolean isAvoidingBlockDamage() { + return this.isAvoidingBlockDamage; + } + + public void processTriggers( + @Nonnull Ref ref, @Nonnull CollisionResult collisionResult, double t, @Nonnull ComponentAccessor componentAccessor + ) { + this.processTriggersHasMoved = false; + this.isReceivingBlockDamage = false; + if (this.enableTriggers || this.enableBlockDamage) { + collisionResult.pruneTriggerBlocks(t); + int count = collisionResult.getTriggerBlocks().size(); + if (count != 0) { + if (this.enableTriggers) { + this.beforeTriggerForce.assign(this.getForce()); + this.beforeTriggerPosition.assign(this.position); + } + + this.moveEntity(ref, 0.0, componentAccessor); + InteractionManager interactionManagerComponent = componentAccessor.getComponent(ref, InteractionModule.get().getInteractionManagerComponent()); + + assert interactionManagerComponent != null; + + int damageToEntity = collisionResult.defaultTriggerBlocksProcessing( + interactionManagerComponent, this.entity, ref, this.enableTriggers, componentAccessor + ); + if (this.enableBlockDamage && damageToEntity > 0) { + Damage damage = new Damage(Damage.NULL_SOURCE, DamageCause.ENVIRONMENT, damageToEntity); + DamageSystems.executeDamage(ref, componentAccessor, damage); + this.isReceivingBlockDamage = true; + } + + this.readEntityPosition(ref, componentAccessor); + if (this.enableTriggers) { + this.processTriggersHasMoved = !this.beforeTriggerForce.equals(this.getForce()) || !this.beforeTriggerPosition.equals(this.position); + } + } + } + } + + protected boolean isDebugMode(RoleDebugFlags mode) { + return this.getRole() != null && this.getRole().getDebugSupport().getDebugFlags().contains(mode); + } + + public boolean isProcessTriggersHasMoved() { + return this.processTriggersHasMoved; + } + + protected boolean isAlive(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { + return !componentAccessor.getArchetype(ref).contains(DeathComponent.getComponentType()); + } + + @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(); + } + + public void resetNavState() { + this.navState = NavState.AT_GOAL; + this.throttleDuration = 0.0; + this.targetDeltaSquared = 0.0; + } + + public void resetObstructedFlags() { + this.isObstructed = false; + } + + @Override + public void deactivate() { + } + + public double getEpsilonSpeed() { + return this.epsilonSpeed; + } + + public float getEpsilonAngle() { + return this.epsilonAngle; + } + + @Nonnull + @Override + public Vector3d getComponentSelector() { + return this.componentSelector; + } + + @Nonnull + @Override + public Vector3d getPlanarComponentSelector() { + return this.planarComponentSelector; + } + + @Override + public void setComponentSelector(@Nonnull Vector3d componentSelector) { + this.componentSelector.assign(componentSelector); + } + + @Override + public Vector3d getWorldNormal() { + return this.worldNormal; + } + + @Override + public Vector3d getWorldAntiNormal() { + return this.worldAntiNormal; + } + + @Override + public double waypointDistance(@Nonnull Vector3d p, @Nonnull Vector3d q) { + return Math.sqrt(this.waypointDistanceSquared(p, q)); + } + + @Override + public double waypointDistanceSquared(@Nonnull Vector3d p, @Nonnull Vector3d q) { + double dx = (p.x - q.x) * this.getComponentSelector().x; + double dy = (p.y - q.y) * this.getComponentSelector().y; + double dz = (p.z - q.z) * this.getComponentSelector().z; + return dx * dx + dy * dy + dz * dz; + } + + @Override + public double waypointDistance(@Nonnull Ref ref, @Nonnull Vector3d p, @Nonnull ComponentAccessor componentAccessor) { + return Math.sqrt(this.waypointDistanceSquared(ref, p, componentAccessor)); + } + + @Override + public double waypointDistanceSquared(@Nonnull Ref ref, @Nonnull Vector3d p, @Nonnull ComponentAccessor componentAccessor) { + TransformComponent transformComponent = componentAccessor.getComponent(ref, TransformComponent.getComponentType()); + + assert transformComponent != null; + + Vector3d position = transformComponent.getPosition(); + double dx = (p.x - position.getX()) * this.getComponentSelector().x; + double dy = (p.y - position.getY()) * this.getComponentSelector().y; + double dz = (p.z - position.getZ()) * this.getComponentSelector().z; + return dx * dx + dy * dy + dz * dz; + } + + @Override + public boolean isValidPosition(@Nonnull Vector3d position, @Nonnull ComponentAccessor componentAccessor) { + return this.isValidPosition(position, this.collisionResult, componentAccessor); + } + + public boolean isValidPosition( + @Nonnull Vector3d position, @Nonnull CollisionResult collisionResult, @Nonnull ComponentAccessor componentAccessor + ) { + World world = componentAccessor.getExternalData().getWorld(); + CollisionModule module = CollisionModule.get(); + CollisionModuleConfig config = module.getConfig(); + boolean saveDebugModeOverlaps = config.isDumpInvalidBlocks(); + config.setDumpInvalidBlocks(this.debugModeOverlaps); + boolean isValid = module.validatePosition( + world, + this.collisionBoundingBox, + position, + this.getInvalidOverlapMaterials(), + null, + (_this, collisionCode, collision, collisionConfig) -> collisionConfig.blockId != Integer.MIN_VALUE, + collisionResult + ) + != -1; + config.setDumpInvalidBlocks(saveDebugModeOverlaps); + return isValid; + } + + public int getInvalidOverlapMaterials() { + return 4; + } + + protected void saveMotionKind() { + this.previousMotionKind = this.getMotionKind(); + } + + protected boolean switchedToMotionKind(MotionKind motionKind) { + return this.getMotionKind() == motionKind && this.previousMotionKind != motionKind; + } + + public MotionKind getMotionKind() { + return this.motionKind; + } + + public void setMotionKind(MotionKind motionKind) { + this.motionKind = motionKind; + } + + @Override + public double getGravity() { + return this.gravity; + } + + public void setGravity(double gravity) { + this.gravity = gravity; + } + + @Override + public boolean translateToAccessiblePosition( + Vector3d position, Box boundingBox, double minYValue, double maxYValue, ComponentAccessor componentAccessor + ) { + return true; + } + + @Override + public boolean standingOnBlockOfType(int blockSet) { + return false; + } + + @Override + public void requirePreciseMovement(@Nullable Vector3d positionHint) { + this.requiresPreciseMovement = true; + this.havePreciseMovementTarget = positionHint != null; + if (this.havePreciseMovementTarget) { + this.preciseMovementTarget.assign(positionHint); + } + } + + public void clearRequirePreciseMovement() { + this.requiresPreciseMovement = false; + this.havePreciseMovementTarget = false; + } + + public boolean isRequiresPreciseMovement() { + return this.requiresPreciseMovement; + } + + @Override + public void requireDepthProbing() { + this.requiresDepthProbing = true; + } + + public void clearRequireDepthProbing() { + this.requiresDepthProbing = false; + } + + public boolean isRequiresDepthProbing() { + return this.requiresDepthProbing; + } + + @Override + public void enableHeadingBlending(double heading, @Nullable Vector3d targetPosition, double blendLevel) { + this.isBlendingHeading = true; + this.blendHeading = heading; + this.haveBlendHeadingPosition = targetPosition != null; + if (this.haveBlendHeadingPosition) { + this.blendHeadingPosition.assign(targetPosition); + } + + this.blendLevelAtTargetPosition = blendLevel; + } + + @Override + public void enableHeadingBlending() { + this.enableHeadingBlending(Double.NaN, null, 0.0); + } + + public void clearBlendHeading() { + this.isBlendingHeading = false; + this.haveBlendHeadingPosition = false; + } + + @Override + public void setRelaxedMoveConstraints(boolean relax) { + this.isRelaxedMoveConstraints = relax; + } + + @Override + public boolean isRelaxedMoveConstraints() { + return this.isRelaxedMoveConstraints; + } + + @Override + public void updatePhysicsValues(PhysicsValues values) { + this.movementSettings = MovementManager.MASTER_DEFAULT.apply(values, GameMode.Adventure); + } + + protected static class AppliedVelocity { + protected final Vector3d velocity; + protected final VelocityConfig config; + protected boolean canClear; + + public AppliedVelocity(Vector3d velocity, VelocityConfig config) { + this.velocity = velocity; + this.config = config; + } + } +} diff --git a/src/com/hypixel/hytale/server/npc/role/Role.java b/src/com/hypixel/hytale/server/npc/role/Role.java new file mode 100644 index 00000000..fc2e909c --- /dev/null +++ b/src/com/hypixel/hytale/server/npc/role/Role.java @@ -0,0 +1,1169 @@ +package com.hypixel.hytale.server.npc.role; + +import com.hypixel.hytale.common.util.ArrayUtil; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentAccessor; +import com.hypixel.hytale.component.ComponentType; +import com.hypixel.hytale.component.Holder; +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.math.random.RandomExtra; +import com.hypixel.hytale.math.shape.Box; +import com.hypixel.hytale.math.util.TrigMathUtil; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.protocol.BlockMaterial; +import com.hypixel.hytale.protocol.MovementStates; +import com.hypixel.hytale.server.core.asset.type.model.config.Model; +import com.hypixel.hytale.server.core.inventory.Inventory; +import com.hypixel.hytale.server.core.inventory.ItemStack; +import com.hypixel.hytale.server.core.inventory.container.ItemContainer; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; +import com.hypixel.hytale.server.core.modules.item.ItemModule; +import com.hypixel.hytale.server.core.modules.splitvelocity.VelocityConfig; +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.npc.asset.builder.BuilderSupport; +import com.hypixel.hytale.server.npc.entities.NPCEntity; +import com.hypixel.hytale.server.npc.instructions.BodyMotion; +import com.hypixel.hytale.server.npc.instructions.Instruction; +import com.hypixel.hytale.server.npc.movement.GroupSteeringAccumulator; +import com.hypixel.hytale.server.npc.movement.Steering; +import com.hypixel.hytale.server.npc.movement.controllers.MotionController; +import com.hypixel.hytale.server.npc.movement.steeringforces.SteeringForceAvoidCollision; +import com.hypixel.hytale.server.npc.role.builders.BuilderRole; +import com.hypixel.hytale.server.npc.role.support.CombatSupport; +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 com.hypixel.hytale.server.npc.role.support.PositionCache; +import com.hypixel.hytale.server.npc.role.support.RoleStats; +import com.hypixel.hytale.server.npc.role.support.StateSupport; +import com.hypixel.hytale.server.npc.role.support.WorldSupport; +import com.hypixel.hytale.server.npc.statetransition.StateTransitionController; +import com.hypixel.hytale.server.npc.systems.TickSkipState; +import com.hypixel.hytale.server.npc.util.ComponentInfo; +import com.hypixel.hytale.server.npc.util.IAnnotatedComponent; +import com.hypixel.hytale.server.npc.util.IAnnotatedComponentCollection; +import com.hypixel.hytale.server.npc.util.InventoryHelper; +import com.hypixel.hytale.server.npc.util.NPCPhysicsMath; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.Supplier; +import java.util.logging.Level; + +public class Role implements IAnnotatedComponentCollection { + public static final double INTERACTION_PLAYER_DISTANCE = 10.0; + public static final boolean DEBUG_APPLIED_FORCES = false; + @Nonnull + protected final CombatSupport combatSupport; + @Nonnull + protected final StateSupport stateSupport; + @Nonnull + protected final MarkedEntitySupport markedEntitySupport; + @Nonnull + protected final WorldSupport worldSupport; + @Nonnull + protected final EntitySupport entitySupport; + @Nonnull + protected final PositionCache positionCache; + @Nonnull + protected final DebugSupport debugSupport; + protected final int initialMaxHealth; + protected final double collisionProbeDistance; + protected final double collisionRadius; + protected final double collisionForceFalloff; + protected final float collisionViewAngle; + protected final float collisionViewHalfAngleCosine; + protected final Steering bodySteering = new Steering(); + protected final Steering headSteering = new Steering(); + protected final SteeringForceAvoidCollision steeringForceAvoidCollision = new SteeringForceAvoidCollision(); + protected final GroupSteeringAccumulator groupSteeringAccumulator = new GroupSteeringAccumulator(); + protected final Vector3d separation = new Vector3d(); + protected final Set> ignoredEntitiesForAvoidance = new HashSet<>(); + protected final double entityAvoidanceStrength; + protected final Role.AvoidanceMode avoidanceMode; + protected final boolean isAvoidingEntities; + protected final double separationDistance; + protected final double separationWeight; + protected final double separationDistanceTarget; + protected final double separationNearRadiusTarget; + protected final double separationFarRadiusTarget; + protected final boolean applySeparation; + protected final Vector3d lastSeparationSteering = new Vector3d(); + @Nullable + protected final float[] headPitchAngleRange; + protected final boolean stayInEnvironment; + protected final String allowedEnvironments; + @Nullable + protected final String[] flockSpawnTypes; + protected final boolean flockSpawnTypesRandom; + @Nonnull + protected final String[] flockAllowedRoles; + protected final boolean canLeadFlock; + protected final double flockWeightAlignment; + protected final double flockWeightSeparation; + protected final double flockWeightCohesion; + protected final double flockInfluenceRange; + protected final boolean corpseStaysInFlock; + protected final double inertia; + protected final double knockbackScale; + protected final boolean breathesInAir; + protected final boolean breathesInWater; + protected final boolean pickupDropOnDeath; + @Nullable + protected final String[] hotbarItems; + @Nullable + protected final String[] offHandItems; + protected final double deathAnimationTime; + protected final float despawnAnimationTime; + protected final String dropListId; + @Nullable + protected final String deathInteraction; + protected final boolean invulnerable; + protected final int inventorySlots; + protected final String inventoryContentsDropList; + protected final int hotbarSlots; + protected final int offHandSlots; + protected final byte defaultOffHandSlot; + protected final List deferredActions = new ObjectArrayList<>(); + protected final RoleStats roleStats; + @Nullable + protected final String balanceAsset; + @Nullable + protected final Map interactionVars; + protected int roleIndex; + protected String roleName; + protected String appearance; + protected boolean isActivated; + @Nonnull + protected Map motionControllers = new HashMap<>(); + protected MotionController activeMotionController; + protected int[] flockSpawnTypeIndices; + protected boolean requiresLeashPosition; + protected boolean hasReachedTerminalAction; + @Nullable + protected String[] armor; + protected boolean[] flags; + protected Instruction rootInstruction; + @Nullable + protected Instruction lastBodyMotionStep; + @Nullable + protected Instruction lastHeadMotionStep; + protected Instruction[] indexedInstructions; + @Nullable + protected Instruction interactionInstruction; + @Nullable + protected Instruction deathInstruction; + protected Instruction currentTreeModeStep; + protected boolean roleChangeRequested; + protected final boolean isMemory; + protected final String memoriesNameOverride; + protected final boolean isMemoriesNameOverriden; + protected final float spawnLockTime; + protected final String nameTranslationKey; + protected boolean backingAway; + protected final TickSkipState steeringTickSkip = new TickSkipState(); + protected final TickSkipState behaviourTickSkip = new TickSkipState(); + + public Role(@Nonnull BuilderRole builder, @Nonnull BuilderSupport builderSupport) { + NPCEntity npcComponent = builderSupport.getEntity(); + this.combatSupport = new CombatSupport(npcComponent, builder, builderSupport); + this.stateSupport = new StateSupport(builder, builderSupport); + this.markedEntitySupport = new MarkedEntitySupport(npcComponent); + this.worldSupport = new WorldSupport(npcComponent, builder, builderSupport); + this.entitySupport = new EntitySupport(npcComponent, builder); + this.positionCache = new PositionCache(this); + this.debugSupport = new DebugSupport(npcComponent, builder); + this.initialMaxHealth = builder.getMaxHealth(builderSupport); + this.nameTranslationKey = builder.getNameTranslationKey(builderSupport); + this.appearance = builder.getAppearance(builderSupport); + this.hotbarItems = builder.getHotbarItems(builderSupport); + this.offHandItems = builder.getOffHandItems(builderSupport); + this.defaultOffHandSlot = builder.getDefaultOffHandSlot(builderSupport); + this.inventoryContentsDropList = builder.getInventoryItemsDropList(builderSupport); + this.armor = builder.getArmor(); + this.inertia = builder.getInertia(); + + for (MotionController motionController : this.motionControllers.values()) { + motionController.setInertia(this.inertia); + } + + this.knockbackScale = builder.getKnockbackScale(builderSupport); + + for (MotionController motionController : this.motionControllers.values()) { + motionController.setKnockbackScale(this.knockbackScale); + } + + this.positionCache.setOpaqueBlockSet(builder.getOpaqueBlockSet()); + this.dropListId = builder.getDropListId(builderSupport); + this.isAvoidingEntities = builder.isAvoidingEntities(); + this.avoidanceMode = builder.getAvoidanceMode(); + this.collisionProbeDistance = builder.getCollisionDistance(); + this.collisionForceFalloff = builder.getCollisionForceFalloff(); + 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.applySeparation = builder.isApplySeparation(builderSupport); + if (builder.isOverridingHeadPitchAngle(builderSupport)) { + this.headPitchAngleRange = builder.getHeadPitchAngleRange(builderSupport); + } else { + this.headPitchAngleRange = null; + } + + this.stayInEnvironment = builder.isStayingInEnvironment(); + this.allowedEnvironments = builder.getAllowedEnvironments(); + this.entityAvoidanceStrength = builder.getEntityAvoidanceStrength(); + this.flockSpawnTypes = builder.getFlockSpawnTypes(builderSupport); + this.flockSpawnTypesRandom = builder.isFlockSpawnTypeRandom(builderSupport); + this.flockAllowedRoles = builder.getFlockAllowedRoles(builderSupport); + this.canLeadFlock = builder.isCanLeadFlock(builderSupport); + this.flockWeightAlignment = builder.getFlockWeightAlignment(); + this.flockWeightSeparation = builder.getFlockWeightSeparation(); + this.flockWeightCohesion = builder.getFlockWeightCohesion(); + this.flockInfluenceRange = builder.getFlockInfluenceRange(); + this.invulnerable = builder.isInvulnerable(builderSupport); + this.breathesInAir = builder.isBreathesInAir(builderSupport); + this.breathesInWater = builder.isBreathesInWater(builderSupport); + this.pickupDropOnDeath = builder.isPickupDropOnDeath(); + this.deathAnimationTime = builder.getDeathAnimationTime(); + this.deathInteraction = builder.getDeathInteraction(builderSupport); + this.despawnAnimationTime = builder.getDespawnAnimationTime(); + this.inventorySlots = builder.getInventorySlots(); + this.hotbarSlots = builder.getHotbarSlots(); + this.offHandSlots = builder.getOffHandSlots(); + this.corpseStaysInFlock = builder.isCorpseStaysInFlock(); + this.roleStats = builderSupport.getRoleStats(); + this.balanceAsset = builder.getBalanceAsset(builderSupport); + this.interactionVars = builder.getInteractionVars(builderSupport); + this.isMemory = builder.isMemory(builderSupport.getExecutionContext()); + this.memoriesNameOverride = builder.getMemoriesNameOverride(builderSupport.getExecutionContext()); + this.isMemoriesNameOverriden = this.memoriesNameOverride != null && !this.memoriesNameOverride.isEmpty(); + this.spawnLockTime = builder.getSpawnLockTime(builderSupport); + this.entitySupport.pickRandomDisplayName(builderSupport.getHolder(), false); + List instructionList = builder.getInstructionList(builderSupport); + if (instructionList == null) { + instructionList = new ObjectArrayList<>(); + } + + Instruction[] instructions = instructionList.toArray(Instruction[]::new); + this.rootInstruction = Instruction.createRootInstruction(instructions, builderSupport); + this.interactionInstruction = builder.getInteractionInstruction(builderSupport); + this.deathInstruction = builder.getDeathInstruction(builderSupport); + builder.registerStateEvaluator(builderSupport); + this.setMotionControllers(builderSupport.getEntity(), builder.getMotionControllerMap(builderSupport), builder.getInitialMotionController(builderSupport)); + if (this.interactionInstruction != null) { + builderSupport.trackInteractions(); + } + } + + public int getInitialMaxHealth() { + return this.initialMaxHealth; + } + + public boolean isAvoidingEntities() { + return this.isAvoidingEntities; + } + + public double getCollisionProbeDistance() { + return this.collisionProbeDistance; + } + + public boolean isApplySeparation() { + return this.applySeparation; + } + + public double getSeparationDistance() { + return this.separationDistance; + } + + public Instruction getRootInstruction() { + return this.rootInstruction; + } + + @Nullable + public Instruction getInteractionInstruction() { + return this.interactionInstruction; + } + + @Nullable + public Instruction getDeathInstruction() { + return this.deathInstruction; + } + + @Nonnull + public Steering getBodySteering() { + return this.bodySteering; + } + + @Nonnull + public Steering getHeadSteering() { + return this.headSteering; + } + + @Nonnull + public Set> getIgnoredEntitiesForAvoidance() { + return this.ignoredEntitiesForAvoidance; + } + + public String getDropListId() { + return this.dropListId; + } + + @Nullable + public String getBalanceAsset() { + return this.balanceAsset; + } + + @Nullable + public Map getInteractionVars() { + return this.interactionVars; + } + + public boolean isMemory() { + return this.isMemory; + } + + public String getMemoriesNameOverride() { + return this.memoriesNameOverride; + } + + public String getNameTranslationKey() { + return this.nameTranslationKey; + } + + public boolean isMemoriesNameOverriden() { + return this.isMemoriesNameOverriden; + } + + public float getSpawnLockTime() { + return this.spawnLockTime; + } + + public void postRoleBuilt(@Nonnull BuilderSupport builderSupport) { + this.requiresLeashPosition = builderSupport.requiresLeashPosition(); + this.flags = builderSupport.allocateFlags(); + this.indexedInstructions = builderSupport.getInstructionSlotMappings(); + this.stateSupport.postRoleBuilt(builderSupport); + this.worldSupport.postRoleBuilt(builderSupport); + this.entitySupport.postRoleBuilt(builderSupport); + this.markedEntitySupport.postRoleBuilder(builderSupport); + this.rootInstruction.setContext(this, 0); + } + + public void loaded() { + this.rootInstruction.loaded(this); + if (this.interactionInstruction != null) { + this.interactionInstruction.loaded(this); + } + + if (this.deathInstruction != null) { + this.deathInstruction.loaded(this); + } + + StateTransitionController stateTransitions = this.stateSupport.getStateTransitionController(); + if (stateTransitions != null) { + stateTransitions.loaded(this); + } + } + + public void spawned(@Nonnull Holder holder, @Nonnull NPCEntity npcComponent) { + MotionController activeMotionController = this.getActiveMotionController(); + if (activeMotionController != null) { + activeMotionController.spawned(); + } + + this.entitySupport.pickRandomDisplayName(holder, true); + this.rootInstruction.spawned(this); + if (this.interactionInstruction != null) { + this.interactionInstruction.spawned(this); + } + + if (this.deathInstruction != null) { + this.deathInstruction.spawned(this); + } + + StateTransitionController stateTransitions = this.stateSupport.getStateTransitionController(); + if (stateTransitions != null) { + stateTransitions.spawned(this); + } + + this.initialiseInventories(npcComponent); + } + + public void unloaded() { + this.worldSupport.unloaded(); + this.markedEntitySupport.unloaded(); + this.deferredActions.clear(); + this.rootInstruction.unloaded(this); + if (this.interactionInstruction != null) { + this.interactionInstruction.unloaded(this); + } + + if (this.deathInstruction != null) { + this.deathInstruction.unloaded(this); + } + + StateTransitionController stateTransitions = this.stateSupport.getStateTransitionController(); + if (stateTransitions != null) { + stateTransitions.unloaded(this); + } + } + + public void removed() { + this.worldSupport.resetAllBlockSensors(); + this.rootInstruction.removed(this); + if (this.interactionInstruction != null) { + this.interactionInstruction.removed(this); + } + + if (this.deathInstruction != null) { + this.deathInstruction.removed(this); + } + + StateTransitionController stateTransitions = this.stateSupport.getStateTransitionController(); + if (stateTransitions != null) { + stateTransitions.removed(this); + } + } + + public void teleported(@Nonnull World from, @Nonnull World to) { + this.rootInstruction.teleported(this, from, to); + if (this.interactionInstruction != null) { + this.interactionInstruction.teleported(this, from, to); + } + + if (this.deathInstruction != null) { + this.deathInstruction.teleported(this, from, to); + } + + StateTransitionController stateTransitions = this.stateSupport.getStateTransitionController(); + if (stateTransitions != null) { + stateTransitions.teleported(this, from, to); + } + } + + public String getAppearanceName() { + return this.appearance; + } + + public MotionController getActiveMotionController() { + return this.activeMotionController; + } + + @Nonnull + public CombatSupport getCombatSupport() { + return this.combatSupport; + } + + @Nonnull + public StateSupport getStateSupport() { + return this.stateSupport; + } + + @Nonnull + public WorldSupport getWorldSupport() { + return this.worldSupport; + } + + @Nonnull + public MarkedEntitySupport getMarkedEntitySupport() { + return this.markedEntitySupport; + } + + @Nonnull + public PositionCache getPositionCache() { + return this.positionCache; + } + + @Nonnull + public EntitySupport getEntitySupport() { + return this.entitySupport; + } + + @Nonnull + public DebugSupport getDebugSupport() { + return this.debugSupport; + } + + public boolean isRoleChangeRequested() { + return this.roleChangeRequested; + } + + public void setRoleChangeRequested() { + this.roleChangeRequested = true; + } + + public boolean setActiveMotionController( + @Nullable Ref ref, @Nonnull NPCEntity npcComponent, @Nonnull String name, @Nullable ComponentAccessor componentAccessor + ) { + MotionController motionController = this.motionControllers.get(name); + if (motionController == null) { + NPCPlugin.get() + .getLogger() + .at(Level.SEVERE) + .log("Failed to set MotionController for NPC of type '%s': MotionController '%s' not found! ", this.getRoleName(), name); + return false; + } else { + this.setActiveMotionController(ref, npcComponent, motionController, componentAccessor); + return true; + } + } + + public void setActiveMotionController( + @Nullable Ref ref, + @Nonnull NPCEntity npcComponent, + @Nonnull MotionController motionController, + @Nullable ComponentAccessor componentAccessor + ) { + if (this.activeMotionController != motionController) { + if (this.activeMotionController != null) { + this.activeMotionController.deactivate(); + } + + this.activeMotionController = motionController; + this.activeMotionController.activate(); + this.motionControllerChanged(ref, npcComponent, this.activeMotionController, componentAccessor); + } + } + + protected void motionControllerChanged( + @Nullable Ref ref, + @Nonnull NPCEntity npcComponent, + @Nullable MotionController motionController, + @Nullable ComponentAccessor componentAccessor + ) { + this.rootInstruction.motionControllerChanged(ref, npcComponent, motionController, componentAccessor); + if (this.deathInstruction != null) { + this.deathInstruction.motionControllerChanged(ref, npcComponent, motionController, componentAccessor); + } + + if (this.interactionInstruction != null) { + this.interactionInstruction.motionControllerChanged(ref, npcComponent, motionController, componentAccessor); + } + + StateTransitionController stateTransitions = this.stateSupport.getStateTransitionController(); + if (stateTransitions != null) { + stateTransitions.motionControllerChanged(ref, npcComponent, motionController, componentAccessor); + } + } + + public void setMotionControllers( + @Nonnull NPCEntity npcComponent, @Nonnull Map motionControllers, @Nullable String initialMotionController + ) { + this.motionControllers = motionControllers; + this.updateMotionControllers(null, null, null, null); + if (!this.motionControllers.isEmpty()) { + if (initialMotionController != null && this.setActiveMotionController(null, npcComponent, initialMotionController, null)) { + return; + } + + this.setActiveMotionController(null, npcComponent, RandomExtra.randomElement(new ObjectArrayList<>(motionControllers.values())), null); + } + } + + public void updateMotionControllers( + @Nullable Ref ref, @Nullable Model model, @Nullable Box boundingBox, @Nullable ComponentAccessor componentAccessor + ) { + for (MotionController motionController : this.motionControllers.values()) { + motionController.setRole(this); + motionController.setInertia(this.inertia); + motionController.setKnockbackScale(this.knockbackScale); + motionController.setHeadPitchAngleRange(this.headPitchAngleRange); + if (boundingBox != null && model != null) { + motionController.updateModelParameters(ref, model, boundingBox, componentAccessor); + motionController.updatePhysicsValues(model.getPhysicsValues()); + } + } + } + + public void updateMovementState( + @Nonnull Ref ref, + @Nonnull MovementStates movementStates, + @Nonnull Vector3d velocity, + @Nonnull ComponentAccessor componentAccessor + ) { + if (this.activeMotionController != null) { + this.activeMotionController.updateMovementState(ref, movementStates, this.bodySteering, velocity, componentAccessor); + } + } + + public void tick(@Nonnull Ref ref, float tickTime, @Nonnull Store store) { + int i = 0; + + while (i < this.deferredActions.size()) { + Role.DeferredAction action = this.deferredActions.get(i); + if (action.tick(ref, this, tickTime, store)) { + this.deferredActions.remove(i); + } else { + i++; + } + } + + this.computeActionsAndSteering(ref, tickTime, this.bodySteering, this.headSteering, store); + } + + public void addDeferredAction(@Nonnull Role.DeferredAction handler) { + this.deferredActions.add(handler); + } + + protected void computeActionsAndSteering( + @Nonnull Ref ref, double tickTime, @Nonnull Steering bodySteering, @Nonnull Steering headSteering, @Nonnull Store store + ) { + boolean isDead = store.getArchetype(ref).contains(DeathComponent.getComponentType()); + if (isDead) { + if (this.deathInstruction != null) { + this.deathInstruction.execute(ref, this, tickTime, store); + } + } else { + if (this.interactionInstruction != null) { + this.positionCache.forEachPlayer((d, _playerRef, _this, _selfRef, _store) -> { + _this.stateSupport.setInteractionIterationTarget(_playerRef); + _this.interactionInstruction.execute(_selfRef, _this, d, _store); + }, this, ref, store, tickTime, store); + this.stateSupport.setInteractionIterationTarget(null); + this.entitySupport.clearTargetPlayerActiveTasks(); + } + + this.getActiveMotionController().beforeInstructionSensorsAndActions(tickTime); + this.entitySupport.clearNextBodyMotionStep(); + this.entitySupport.clearNextHeadMotionStep(); + if (!this.stateSupport.runTransitionActions(ref, this, tickTime, store)) { + this.rootInstruction.execute(ref, this, tickTime, store); + } + + NPCEntity npcComponent = store.getComponent(ref, NPCEntity.getComponentType()); + + assert npcComponent != null; + + if (!npcComponent.isPlayingDespawnAnim()) { + this.getActiveMotionController().beforeInstructionMotion(tickTime); + Instruction nextBodyMotionStep = this.entitySupport.getNextBodyMotionStep(); + if (nextBodyMotionStep != this.lastBodyMotionStep) { + if (this.lastBodyMotionStep != null) { + this.lastBodyMotionStep.getBodyMotion().deactivate(ref, this, store); + this.lastBodyMotionStep.onEndMotion(); + } + + if (nextBodyMotionStep != null) { + nextBodyMotionStep.getBodyMotion().activate(ref, this, store); + } + } + + this.lastBodyMotionStep = nextBodyMotionStep; + Instruction nextHeadMotionStep = this.entitySupport.getNextHeadMotionStep(); + if (nextHeadMotionStep != this.lastHeadMotionStep) { + if (this.lastHeadMotionStep != null) { + this.lastHeadMotionStep.getHeadMotion().deactivate(ref, this, store); + this.lastHeadMotionStep.onEndMotion(); + } + + if (nextHeadMotionStep != null) { + nextHeadMotionStep.getHeadMotion().activate(ref, this, store); + } + } + + this.lastHeadMotionStep = nextHeadMotionStep; + if (nextBodyMotionStep != null) { + nextBodyMotionStep.getBodyMotion().computeSteering(ref, this, nextBodyMotionStep.getSensor().getSensorInfo(), tickTime, bodySteering, store); + } + + if (nextHeadMotionStep != null) { + nextHeadMotionStep.getHeadMotion().computeSteering(ref, this, nextHeadMotionStep.getSensor().getSensorInfo(), tickTime, headSteering, store); + } + } + } + } + + public void blendSeparation( + @Nonnull Ref selfRef, + @Nonnull Vector3d position, + @Nonnull Steering steering, + @Nonnull ComponentType transformComponentType, + @Nonnull CommandBuffer commandBuffer + ) { + this.lastSeparationSteering.assign(Vector3d.ZERO); + double maxRange = this.separationDistance; + Ref targetRef = this.markedEntitySupport.getTargetReferenceToIgnoreForAvoidance(); + if (targetRef != null && targetRef.isValid()) { + TransformComponent targetTransformComponent = commandBuffer.getComponent(targetRef, transformComponentType); + + assert targetTransformComponent != null; + + double distance = targetTransformComponent.getPosition().distanceSquaredTo(position); + if (distance <= this.separationNearRadiusTarget * this.separationNearRadiusTarget) { + maxRange = this.separationDistanceTarget; + } else if (distance < this.separationFarRadiusTarget * this.separationFarRadiusTarget) { + double s = (Math.sqrt(distance) - this.separationNearRadiusTarget) / (this.separationFarRadiusTarget - this.separationNearRadiusTarget); + maxRange = NPCPhysicsMath.lerp(this.separationDistanceTarget, this.separationDistance, s); + } + } + + this.groupSteeringAccumulator.setComponentSelector(this.activeMotionController.getComponentSelector()); + this.groupSteeringAccumulator.setMaxRange(maxRange); + this.groupSteeringAccumulator.setViewConeHalfAngleCosine(this.collisionViewHalfAngleCosine); + this.groupSteeringAccumulator.begin(selfRef, commandBuffer); + this.positionCache + .forEachEntityInAvoidanceRange( + this.ignoredEntitiesForAvoidance, + (ref, _groupSteeringAccumulator, _role, _buffer) -> _groupSteeringAccumulator.processEntity(ref, this.separationWeight, 1.0, 1.0, _buffer), + this.groupSteeringAccumulator, + this, + commandBuffer + ); + this.groupSteeringAccumulator.end(); + if (this.groupSteeringAccumulator.getCount() > 0) { + Vector3d sumOfDistances = this.groupSteeringAccumulator.getSumOfDistances(); + if (sumOfDistances.squaredLength() > 1.0000000000000002E-10) { + double speed = steering.getSpeed(); + this.separation.assign(sumOfDistances).setLength(-0.5); + this.lastSeparationSteering.assign(this.separation); + if (speed > 0.0) { + this.separation.add(steering.getTranslation()); + this.separation.setLength(speed); + } + + steering.setTranslation(this.separation); + } + } + } + + @Nonnull + public Vector3d getLastSeparationSteering() { + return this.lastSeparationSteering; + } + + public void blendAvoidance( + @Nonnull Ref ref, @Nonnull Vector3d position, @Nonnull Steering steering, @Nonnull CommandBuffer commandBuffer + ) { + this.steeringForceAvoidCollision.setDebug(this.debugSupport.isDebugRoleSteering()); + this.steeringForceAvoidCollision.setAvoidanceMode(this.getAvoidanceMode()); + this.steeringForceAvoidCollision.setSelf(ref, position, commandBuffer); + if (!this.activeMotionController.estimateVelocity(steering, this.steeringForceAvoidCollision.getSelfVelocity())) { + this.steeringForceAvoidCollision.setVelocityFromEntity(ref, commandBuffer); + } + + if (this.collisionRadius >= 0.0) { + this.steeringForceAvoidCollision.setSelfRadius(this.collisionRadius); + } + + this.steeringForceAvoidCollision.setMaxDistance(this.collisionProbeDistance); + this.steeringForceAvoidCollision.setFalloff(this.collisionForceFalloff); + this.steeringForceAvoidCollision.setComponentSelector(this.activeMotionController.getComponentSelector()); + this.steeringForceAvoidCollision.reset(); + this.positionCache + .forEachEntityInAvoidanceRange( + this.ignoredEntitiesForAvoidance, + (_ref, _steeringForceAvoidCollision, _buffer) -> _steeringForceAvoidCollision.add(_ref, _buffer), + this.steeringForceAvoidCollision, + commandBuffer + ); + this.steeringForceAvoidCollision.compute(steering); + } + + @Nonnull + public Vector3d getLastAvoidanceSteering() { + return this.steeringForceAvoidCollision.getLastSteeringDirection(); + } + + public void resetInstruction(int instruction) { + this.indexedInstructions[instruction].reset(); + } + + public String getRoleName() { + return this.roleName; + } + + public int getRoleIndex() { + return this.roleIndex; + } + + public void setRoleIndex(int roleIndex, @Nonnull String roleName) { + this.roleIndex = roleIndex; + this.roleName = roleName; + } + + public boolean isInvulnerable() { + return this.invulnerable; + } + + public boolean isBreathesInAir() { + return this.breathesInAir; + } + + public boolean isBreathesInWater() { + return this.breathesInWater; + } + + public double getInertia() { + return this.inertia; + } + + public double getKnockbackScale() { + return this.knockbackScale; + } + + public boolean canBreathe(@Nonnull BlockMaterial breathingMaterial, int fluidId) { + return this.isInvulnerable() ? true : this.couldBreathe(breathingMaterial, fluidId); + } + + public boolean couldBreathe(@Nonnull BlockMaterial breathingMaterial, int fluidId) { + if (fluidId != 0) { + return this.breathesInWater; + } else { + return breathingMaterial == BlockMaterial.Empty ? this.breathesInAir : false; + } + } + + public boolean couldBreatheCached() { + return this.positionCache.couldBreatheCached(); + } + + public void addForce(@Nonnull Vector3d velocity, @Nullable VelocityConfig velocityConfig) { + if (this.activeMotionController != null) { + this.activeMotionController.addForce(velocity, velocityConfig); + } + } + + public void forceVelocity(@Nonnull Vector3d velocity, @Nullable VelocityConfig velocityConfig, boolean ignoreDamping) { + if (this.activeMotionController != null) { + this.activeMotionController.forceVelocity(velocity, velocityConfig, ignoreDamping); + } + } + + public void processAddVelocityInstruction(@Nonnull Vector3d velocity, @Nullable VelocityConfig velocityConfig) { + if (this.activeMotionController != null) { + this.activeMotionController.addForce(velocity, velocityConfig); + } + } + + public void processSetVelocityInstruction(@Nonnull Vector3d velocity, @Nullable VelocityConfig velocityConfig) { + if (this.activeMotionController != null) { + this.activeMotionController.forceVelocity(Vector3d.ZERO, null, false); + this.activeMotionController.addForce(velocity, velocityConfig); + } + } + + public boolean isOnGround() { + return this.getActiveMotionController() != null && this.getActiveMotionController().onGround(); + } + + public void setArmor(@Nonnull NPCEntity npcComponent, @Nullable String[] armor) { + this.armor = armor; + if (armor != null) { + for (String s : armor) { + RoleUtils.setArmor(npcComponent, s); + } + } + } + + public boolean isPickupDropOnDeath() { + return this.pickupDropOnDeath; + } + + public boolean requiresLeashPosition() { + return this.requiresLeashPosition; + } + + public void clearOnce() { + this.rootInstruction.clearOnce(); + if (this.interactionInstruction != null) { + this.interactionInstruction.clearOnce(); + } + + if (this.deathInstruction != null) { + this.deathInstruction.clearOnce(); + } + + this.stateSupport.pollNeedClearOnce(); + } + + public void clearOnceIfNeeded() { + if (this.stateSupport.pollNeedClearOnce()) { + this.clearOnce(); + this.stateSupport.resetLocalStateMachines(); + } + } + + public void setMarkedTarget(@Nonnull String targetSlot, @Nonnull Ref target) { + this.markedEntitySupport.setMarkedEntity(targetSlot, target); + } + + public boolean isFriendly(@Nonnull Ref ref, @Nonnull ComponentAccessor componentAccessor) { + return !this.combatSupport.getCanCauseDamage(ref, componentAccessor); + } + + public boolean isIgnoredForAvoidance(@Nonnull Ref entityReference) { + return this.ignoredEntitiesForAvoidance.contains(entityReference); + } + + public Role.AvoidanceMode getAvoidanceMode() { + return this.avoidanceMode; + } + + public double getCollisionRadius() { + return this.collisionRadius; + } + + public int[] getFlockSpawnTypes() { + if (this.flockSpawnTypeIndices != null) { + return this.flockSpawnTypeIndices; + } else { + int length = this.flockSpawnTypes == null ? 0 : this.flockSpawnTypes.length; + this.flockSpawnTypeIndices = new int[length]; + + for (int i = 0; i < length; i++) { + int index = NPCPlugin.get().getIndex(this.flockSpawnTypes[i]); + if (index == Integer.MIN_VALUE) { + throw new IllegalStateException(String.format("Role %s contains unknown FlockSpawnTypes NPC %s", this.roleName, this.flockSpawnTypes[i])); + } + + this.flockSpawnTypeIndices[i] = index; + } + + return this.flockSpawnTypeIndices; + } + } + + @Nonnull + public String[] getFlockAllowedRoles() { + return this.flockAllowedRoles != null ? Arrays.copyOf(this.flockAllowedRoles, this.flockAllowedRoles.length) : ArrayUtil.EMPTY_STRING_ARRAY; + } + + public boolean isFlockSpawnTypesRandom() { + return this.flockSpawnTypesRandom; + } + + public boolean isCanLeadFlock() { + return this.canLeadFlock; + } + + public double getFlockInfluenceRange() { + return this.flockInfluenceRange; + } + + public double getDeathAnimationTime() { + return this.deathAnimationTime; + } + + @Nullable + public String getDeathInteraction() { + return this.deathInteraction; + } + + public float getDespawnAnimationTime() { + return this.despawnAnimationTime; + } + + public void setReachedTerminalAction(boolean hasReached) { + this.hasReachedTerminalAction = hasReached; + } + + public boolean hasReachedTerminalAction() { + return this.hasReachedTerminalAction; + } + + public void setFlag(int index, boolean value) { + if (this.flags == null) { + throw new NullPointerException(String.format("Trying to set a flag in role %s but flags are null", this.getRoleName())); + } else if (index >= 0 && index < this.flags.length) { + this.flags[index] = value; + } else { + throw new IllegalArgumentException( + String.format("Flag value cannot be less than 0 and must be less than array length %s. Value was %s", this.flags.length, index) + ); + } + } + + public boolean isFlagSet(int index) { + return this.flags != null && index >= 0 && index < this.flags.length ? this.flags[index] : false; + } + + public boolean isBackingAway() { + return this.backingAway; + } + + public void setBackingAway(boolean backingAway) { + this.backingAway = backingAway; + } + + public TickSkipState getSteeringTickSkip() { + return this.steeringTickSkip; + } + + public TickSkipState getBehaviourTickSkip() { + return this.behaviourTickSkip; + } + + public Instruction swapTreeModeSteps(Instruction newStep) { + Instruction old = this.currentTreeModeStep; + this.currentTreeModeStep = newStep; + return old; + } + + public void notifySensorMatch() { + if (this.currentTreeModeStep != null) { + this.currentTreeModeStep.notifyChildSensorMatch(); + } + } + + public void resetAllInstructions() { + for (Instruction instruction : this.indexedInstructions) { + instruction.reset(); + } + } + + @Nullable + public String getSteeringMotionName() { + if (this.lastBodyMotionStep == null) { + return null; + } else { + BodyMotion motion = this.lastBodyMotionStep.getBodyMotion(); + if (motion != null) { + motion = motion.getSteeringMotion(); + } + + return motion == null ? null : motion.getClass().getSimpleName(); + } + } + + @Override + public int componentCount() { + return 1; + } + + @Override + public IAnnotatedComponent getComponent(int index) { + return this.rootInstruction; + } + + @Override + public void getInfo(Role role, ComponentInfo holder) { + } + + @Override + public int getIndex() { + throw new UnsupportedOperationException("Roles do not have component indexes!"); + } + + @Override + public void setContext(IAnnotatedComponent parent, int index) { + throw new UnsupportedOperationException("Roles do not have parent contexts!"); + } + + @Nullable + @Override + public IAnnotatedComponent getParent() { + return null; + } + + @Override + public String getLabel() { + return this.roleName; + } + + private void initialiseInventories(@Nonnull NPCEntity npcComponent) { + List inventoryItems = null; + if (this.inventoryContentsDropList != null) { + ItemModule itemModule = ItemModule.get(); + if (itemModule.isEnabled()) { + inventoryItems = itemModule.getRandomItemDrops(this.inventoryContentsDropList); + } + } + + int inventorySlots = inventoryItems != null && inventoryItems.size() > this.inventorySlots ? inventoryItems.size() : this.inventorySlots; + if (inventorySlots > 0 || this.hotbarSlots > 3 || this.offHandSlots > 0) { + npcComponent.setInventorySize(this.hotbarSlots, inventorySlots, this.offHandSlots); + } + + if (inventoryItems != null) { + ItemContainer inventory = npcComponent.getInventory().getStorage(); + + for (ItemStack item : inventoryItems) { + inventory.addItemStack(item); + } + } + + if (this.hotbarItems != null && this.hotbarItems.length > 0 && npcComponent.getInventory().getHotbar().isEmpty()) { + Inventory inventory = npcComponent.getInventory(); + ItemContainer hotbar = inventory.getHotbar(); + + for (byte i = 0; i < this.hotbarItems.length; i++) { + if (this.hotbarItems[i] != null) { + if (this.hotbarItems[i].startsWith("Droplist:")) { + if (!InventoryHelper.checkHotbarSlot(inventory, i)) { + continue; + } + + List items = ItemModule.get().getRandomItemDrops(this.hotbarItems[i].substring("Droplist:".length())); + hotbar.setItemStackForSlot(i, items.get(RandomExtra.randomRange(items.size()))); + } + + InventoryHelper.setHotbarItem(inventory, this.hotbarItems[i], i); + } + } + } + + if (this.offHandItems != null && this.offHandItems.length > 0) { + RoleUtils.setOffHandItems(npcComponent, this.offHandItems); + } + + if (this.defaultOffHandSlot >= 0) { + InventoryHelper.setOffHandSlot(npcComponent.getInventory(), this.defaultOffHandSlot); + } + + this.setArmor(npcComponent, this.armor); + } + + public boolean isCorpseStaysInFlock() { + return this.corpseStaysInFlock; + } + + public void onLoadFromWorldGenOrPrefab( + @Nonnull Ref ref, @Nonnull NPCEntity npcComponent, @Nonnull ComponentAccessor componentAccessor + ) { + this.entitySupport.pickRandomDisplayName(ref, true, componentAccessor); + this.initialiseInventories(npcComponent); + } + + public RoleStats getRoleStats() { + return this.roleStats; + } + + public static enum AvoidanceMode implements Supplier { + Slowdown("Only slow down NPC"), + Evade("Only evade"), + Any("Any avoidance allowed"); + + @Nonnull + private final String description; + + private AvoidanceMode(@Nonnull final String description) { + this.description = description; + } + + @Nonnull + public String get() { + return this.description; + } + } + + @FunctionalInterface + public interface DeferredAction { + boolean tick(@Nonnull Ref var1, @Nonnull Role var2, double var3, @Nonnull Store var5); + } +} diff --git a/src/com/hypixel/hytale/server/npc/systems/PositionCacheSystems.java b/src/com/hypixel/hytale/server/npc/systems/PositionCacheSystems.java new file mode 100644 index 00000000..86287c32 --- /dev/null +++ b/src/com/hypixel/hytale/server/npc/systems/PositionCacheSystems.java @@ -0,0 +1,390 @@ +package com.hypixel.hytale.server.npc.systems; + +import com.hypixel.hytale.common.collection.BucketItemPool; +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.Holder; +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.OrderPriority; +import com.hypixel.hytale.component.dependency.SystemDependency; +import com.hypixel.hytale.component.query.Query; +import com.hypixel.hytale.component.spatial.SpatialResource; +import com.hypixel.hytale.component.system.HolderSystem; +import com.hypixel.hytale.component.system.RefChangeSystem; +import com.hypixel.hytale.math.util.MathUtil; +import com.hypixel.hytale.math.vector.Vector3d; +import com.hypixel.hytale.protocol.BlockMaterial; +import com.hypixel.hytale.server.core.entity.LivingEntity; +import com.hypixel.hytale.server.core.modules.entity.EntityModule; +import com.hypixel.hytale.server.core.modules.entity.component.ModelComponent; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.system.PlayerSpatialSystem; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.flock.FlockMembership; +import com.hypixel.hytale.server.npc.NPCPlugin; +import com.hypixel.hytale.server.npc.decisionmaker.stateevaluator.StateEvaluator; +import com.hypixel.hytale.server.npc.entities.NPCEntity; +import com.hypixel.hytale.server.npc.instructions.Instruction; +import com.hypixel.hytale.server.npc.role.Role; +import com.hypixel.hytale.server.npc.role.support.EntityList; +import com.hypixel.hytale.server.npc.role.support.PositionCache; +import com.hypixel.hytale.server.npc.statetransition.StateTransitionController; +import com.hypixel.hytale.server.spawning.SpawningPlugin; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; + +public class PositionCacheSystems { + public PositionCacheSystems() { + } + + public static void initialisePositionCache(@Nonnull Role role, @Nullable StateEvaluator stateEvaluator, double flockInfluenceRange) { + PositionCache positionCache = role.getPositionCache(); + positionCache.reset(true); + if (role.isAvoidingEntities()) { + double collisionProbeDistance = role.getCollisionProbeDistance(); + positionCache.requireEntityDistanceAvoidance(collisionProbeDistance); + positionCache.requirePlayerDistanceAvoidance(collisionProbeDistance); + } + + if (role.isApplySeparation()) { + double separationDistance = role.getSeparationDistance(); + positionCache.requireEntityDistanceAvoidance(separationDistance); + positionCache.requirePlayerDistanceAvoidance(separationDistance); + } + + if (flockInfluenceRange > 0.0) { + positionCache.requireEntityDistanceAvoidance(flockInfluenceRange); + positionCache.requirePlayerDistanceAvoidance(flockInfluenceRange); + } + + Instruction instruction = role.getRootInstruction(); + instruction.registerWithSupport(role); + Instruction interactionInstruction = role.getInteractionInstruction(); + if (interactionInstruction != null) { + interactionInstruction.registerWithSupport(role); + positionCache.requirePlayerDistanceUnsorted(10.0); + } + + Instruction deathInstruction = role.getDeathInstruction(); + if (deathInstruction != null) { + deathInstruction.registerWithSupport(role); + } + + StateTransitionController stateTransitions = role.getStateSupport().getStateTransitionController(); + if (stateTransitions != null) { + stateTransitions.registerWithSupport(role); + } + + if (stateEvaluator != null) { + stateEvaluator.setupNPC(role); + } + + for (Consumer registration : positionCache.getExternalRegistrations()) { + registration.accept(role); + } + + positionCache.finalizeConfiguration(); + } + + public static class OnFlockJoinSystem extends RefChangeSystem { + @Nonnull + private final ComponentType flockMembershipComponentType; + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType stateEvaluatorComponentType; + @Nonnull + private final Query query; + + public OnFlockJoinSystem( + @Nonnull ComponentType npcComponentType, @Nonnull ComponentType flockMembershipComponentType + ) { + this.flockMembershipComponentType = flockMembershipComponentType; + this.npcComponentType = npcComponentType; + this.stateEvaluatorComponentType = StateEvaluator.getComponentType(); + this.query = Archetype.of(npcComponentType, flockMembershipComponentType); + } + + @Nonnull + @Override + public Query getQuery() { + return this.query; + } + + @Nonnull + @Override + public ComponentType componentType() { + return this.flockMembershipComponentType; + } + + public void onComponentAdded( + @Nonnull Ref ref, + @Nonnull FlockMembership component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + PositionCacheSystems.initialisePositionCache(role, store.getComponent(ref, this.stateEvaluatorComponentType), role.getFlockInfluenceRange()); + } + + public void onComponentSet( + @Nonnull Ref ref, + FlockMembership oldComponent, + @Nonnull FlockMembership newComponent, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + PositionCacheSystems.initialisePositionCache(role, store.getComponent(ref, this.stateEvaluatorComponentType), role.getFlockInfluenceRange()); + } + + public void onComponentRemoved( + @Nonnull Ref ref, + @Nonnull FlockMembership component, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + } + } + + public static class RoleActivateSystem extends HolderSystem { + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType stateEvaluatorComponentType; + + public RoleActivateSystem( + @Nonnull ComponentType npcComponentType, @Nonnull ComponentType stateEvaluatorComponentType + ) { + this.npcComponentType = npcComponentType; + this.stateEvaluatorComponentType = stateEvaluatorComponentType; + } + + @Override + public void onEntityAdd(@Nonnull Holder holder, @Nonnull AddReason reason, @Nonnull Store store) { + NPCEntity npcComponent = holder.getComponent(this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + double influenceRadius; + if (holder.getComponent(FlockMembership.getComponentType()) != null) { + influenceRadius = role.getFlockInfluenceRange(); + } else { + influenceRadius = 0.0; + } + + StateEvaluator stateEvaluator = holder.getComponent(this.stateEvaluatorComponentType); + if (stateEvaluator != null) { + stateEvaluator.setupNPC(holder); + } + + PositionCacheSystems.initialisePositionCache(role, stateEvaluator, influenceRadius); + } + + @Override + public void onEntityRemoved(@Nonnull Holder holder, @Nonnull RemoveReason reason, @Nonnull Store store) { + NPCEntity npcComponent = holder.getComponent(this.npcComponentType); + + assert npcComponent != null; + + npcComponent.getRole().getPositionCache().reset(false); + } + + @Nonnull + @Override + public Query getQuery() { + return this.npcComponentType; + } + } + + public static class UpdateSystem extends SteppableTickingSystem { + @Nonnull + private static final ThreadLocal>> BUCKET_POOL_THREAD_LOCAL = ThreadLocal.withInitial(BucketItemPool::new); + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType modelComponentType; + @Nonnull + private final ComponentType transformComponentType; + @Nonnull + private final ResourceType, EntityStore>> playerSpatialResource; + @Nonnull + private final ResourceType, EntityStore>> npcSpatialResource; + @Nonnull + private final ResourceType, EntityStore>> itemSpatialResource; + @Nonnull + private final Query query; + @Nonnull + private final Set> dependencies = Set.of( + new SystemDependency<>(Order.AFTER, PlayerSpatialSystem.class, OrderPriority.CLOSEST), + new SystemDependency<>(Order.BEFORE, RoleSystems.PreBehaviourSupportTickSystem.class) + ); + + public UpdateSystem( + @Nonnull ComponentType npcComponentType, + @Nonnull ResourceType, EntityStore>> npcSpatialResource + ) { + this.npcComponentType = npcComponentType; + this.modelComponentType = ModelComponent.getComponentType(); + this.transformComponentType = TransformComponent.getComponentType(); + this.playerSpatialResource = EntityModule.get().getPlayerSpatialResourceType(); + this.npcSpatialResource = npcSpatialResource; + this.itemSpatialResource = EntityModule.get().getItemSpatialResourceType(); + this.query = Query.and(npcComponentType, this.transformComponentType, this.modelComponentType); + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return false; + } + + @Nonnull + @Override + public Query getQuery() { + return this.query; + } + + @Override + public void steppedTick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + Ref ref = archetypeChunk.getReferenceTo(index); + NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + PositionCache positionCache = role.getPositionCache(); + positionCache.setBenchmarking(NPCPlugin.get().isBenchmarkingSensorSupport()); + if (positionCache.tickPositionCacheNextUpdate(dt)) { + positionCache.resetPositionCacheNextUpdate(); + + long packed = LivingEntity.getPackedMaterialAndFluidAtBreathingHeight(ref, commandBuffer); + BlockMaterial material = BlockMaterial.VALUES[MathUtil.unpackLeft(packed)]; + int fluidId = MathUtil.unpackRight(packed); + positionCache.setCouldBreathe(role.canBreathe(material, fluidId)); + + TransformComponent transformComponent = archetypeChunk.getComponent(index, this.transformComponentType); + + assert transformComponent != null; + + Vector3d position = transformComponent.getPosition(); + EntityList players = positionCache.getPlayers(); + if (players.getSearchRadius() > 0) { + SpatialResource, EntityStore> spatialResource = store.getResource(this.playerSpatialResource); + players.setBucketItemPool(BUCKET_POOL_THREAD_LOCAL.get()); + if (positionCache.isBenchmarking()) { + long startTime = System.nanoTime(); + addEntities(ref, position, players, spatialResource, commandBuffer); + long getTime = System.nanoTime(); + NPCPlugin.get() + .collectSensorSupportPlayerList( + role.getRoleIndex(), + getTime - startTime, + players.getMaxDistanceSorted(), + players.getMaxDistanceUnsorted(), + players.getMaxDistanceAvoidance(), + 0 + ); + } else { + addEntities(ref, position, players, spatialResource, commandBuffer); + } + } + + EntityList npcEntities = positionCache.getNpcs(); + if (npcEntities.getSearchRadius() > 0) { + SpatialResource, EntityStore> spatialResource = store.getResource(this.npcSpatialResource); + npcEntities.setBucketItemPool(BUCKET_POOL_THREAD_LOCAL.get()); + if (positionCache.isBenchmarking()) { + long startTime = System.nanoTime(); + addEntities(ref, position, npcEntities, spatialResource, commandBuffer); + long getTime = System.nanoTime(); + NPCPlugin.get() + .collectSensorSupportEntityList( + role.getRoleIndex(), + getTime - startTime, + npcEntities.getMaxDistanceSorted(), + npcEntities.getMaxDistanceUnsorted(), + npcEntities.getMaxDistanceAvoidance(), + 0 + ); + } else { + addEntities(ref, position, npcEntities, spatialResource, commandBuffer); + } + } + + double maxDroppedItemDistance = positionCache.getMaxDroppedItemDistance(); + if (maxDroppedItemDistance > 0.0) { + SpatialResource, EntityStore> spatialResource = store.getResource(this.itemSpatialResource); + List> list = positionCache.getDroppedItemList(); + list.clear(); + spatialResource.getSpatialStructure().ordered(position, (int) maxDroppedItemDistance + 1, list); + } + + double maxSpawnMarkerDistance = positionCache.getMaxSpawnMarkerDistance(); + if (maxSpawnMarkerDistance > 0.0) { + SpatialResource, EntityStore> spatialResource = store.getResource(SpawningPlugin.get().getSpawnMarkerSpatialResource()); + List> list = positionCache.getSpawnMarkerList(); + list.clear(); + spatialResource.getSpatialStructure().collect(position, (int) maxSpawnMarkerDistance + 1, list); + } + + int maxSpawnBeaconDistance = positionCache.getMaxSpawnBeaconDistance(); + if (maxSpawnBeaconDistance > 0) { + SpatialResource, EntityStore> spatialResource = store.getResource(SpawningPlugin.get().getManualSpawnBeaconSpatialResource()); + List> list = positionCache.getSpawnBeaconList(); + list.clear(); + spatialResource.getSpatialStructure().ordered(position, maxSpawnBeaconDistance + 1, list); + } + } + } + + private static void addEntities( + @Nonnull Ref self, + @Nonnull Vector3d position, + @Nonnull EntityList entityList, + @Nonnull SpatialResource, EntityStore> spatialResource, + @Nonnull CommandBuffer commandBuffer + ) { + List> results = SpatialResource.getThreadLocalReferenceList(); + spatialResource.getSpatialStructure().collect(position, entityList.getSearchRadius(), results); + + for (Ref result : results) { + if (result.isValid() && !result.equals(self)) { + entityList.add(result, position, commandBuffer); + } + } + } + } +} diff --git a/src/com/hypixel/hytale/server/npc/systems/RoleSystems.java b/src/com/hypixel/hytale/server/npc/systems/RoleSystems.java new file mode 100644 index 00000000..aba8d37c --- /dev/null +++ b/src/com/hypixel/hytale/server/npc/systems/RoleSystems.java @@ -0,0 +1,406 @@ +package com.hypixel.hytale.server.npc.systems; + +import com.hypixel.hytale.component.AddReason; +import com.hypixel.hytale.component.ArchetypeChunk; +import com.hypixel.hytale.component.CommandBuffer; +import com.hypixel.hytale.component.ComponentType; +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.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.TickingSystem; +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.entity.component.BoundingBox; +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; +import com.hypixel.hytale.server.core.modules.entity.damage.DeathComponent; +import com.hypixel.hytale.server.core.modules.entity.player.PlayerSettings; +import com.hypixel.hytale.server.core.modules.entity.system.ModelSystems; +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.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.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.Set; +import java.util.logging.Level; + +public class RoleSystems { + private static final ThreadLocal>> ENTITY_LIST = ThreadLocal.withInitial(ArrayList::new); + + public RoleSystems() { + } + + public static class BehaviourTickSystem extends TickingSystem { + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType stepComponentType; + @Nonnull + private final ComponentType frozenComponentType; + @Nonnull + private final ComponentType newSpawnComponentType; + @Nonnull + private final ComponentType transformComponentType; + + public BehaviourTickSystem( + @Nonnull ComponentType npcComponentType, @Nonnull ComponentType stepComponentType + ) { + this.npcComponentType = npcComponentType; + this.stepComponentType = stepComponentType; + this.frozenComponentType = Frozen.getComponentType(); + this.newSpawnComponentType = NewSpawnComponent.getComponentType(); + this.transformComponentType = TransformComponent.getComponentType(); + } + + @Override + public void tick(float dt, int systemIndex, @Nonnull Store store) { + List> entities = RoleSystems.ENTITY_LIST.get(); + store.forEachChunk(this.npcComponentType, (archetypeChunk, commandBuffer) -> { + for (int index = 0; index < archetypeChunk.size(); index++) { + entities.add(archetypeChunk.getReferenceTo(index)); + } + }); + World world = store.getExternalData().getWorld(); + boolean isAllNpcFrozen = world.getWorldConfig().isAllNPCFrozen(); + + for (Ref entityReference : entities) { + if (entityReference.isValid() && store.getComponent(entityReference, this.newSpawnComponentType) == null) { + float tickLength; + if (store.getComponent(entityReference, this.frozenComponentType) == null && !isAllNpcFrozen) { + tickLength = dt; + } else { + StepComponent stepComponent = store.getComponent(entityReference, this.stepComponentType); + if (stepComponent == null) { + continue; + } + + tickLength = stepComponent.getTickLength(); + } + + NPCEntity npcComponent = store.getComponent(entityReference, this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + + // Distance-based tick skipping optimization + TransformComponent transform = store.getComponent(entityReference, this.transformComponentType); + if (transform != null) { + TickSkipState tickSkip = role.getBehaviourTickSkip(); + tickSkip.add(tickLength); + + if (!tickSkip.shouldProcess(transform.getPosition(), store, tickLength)) { + continue; + } + + tickLength = tickSkip.consume(); + } + + try { + boolean benchmarking = NPCPlugin.get().isBenchmarkingRole(); + if (benchmarking) { + long start = System.nanoTime(); + role.tick(entityReference, tickLength, store); + NPCPlugin.get().collectRoleTick(role.getRoleIndex(), System.nanoTime() - start); + } else { + role.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()); + store.removeEntity(entityReference, RemoveReason.REMOVE); + } + } + } + + entities.clear(); + } + } + + public static class PostBehaviourSupportTickSystem extends SteppableTickingSystem { + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType transformComponentType; + @Nonnull + private final Query query; + @Nonnull + private final Set> dependencies = Set.of( + new SystemDependency<>(Order.AFTER, SteeringSystem.class), new SystemDependency<>(Order.BEFORE, TransformSystems.EntityTrackerUpdate.class) + ); + + public PostBehaviourSupportTickSystem(@Nonnull ComponentType npcComponentType) { + this.npcComponentType = npcComponentType; + this.transformComponentType = TransformComponent.getComponentType(); + this.query = Query.and(npcComponentType, this.transformComponentType); + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Nonnull + @Override + public Query getQuery() { + return this.query; + } + + @Override + public void steppedTick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcComponentType); + + assert npcComponent != null; + + Ref ref = archetypeChunk.getReferenceTo(index); + Role role = npcComponent.getRole(); + MotionController activeMotionController = role.getActiveMotionController(); + activeMotionController.clearOverrides(); + activeMotionController.constrainRotations(role, archetypeChunk.getComponent(index, this.transformComponentType)); + role.getCombatSupport().tick(dt); + role.getWorldSupport().tick(dt); + EntitySupport entitySupport = role.getEntitySupport(); + entitySupport.tick(dt); + entitySupport.handleNominatedDisplayName(ref, commandBuffer); + role.getStateSupport().update(commandBuffer); + npcComponent.clearDamageData(); + role.getMarkedEntitySupport().setTargetSlotToIgnoreForAvoidance(Integer.MIN_VALUE); + role.setReachedTerminalAction(false); + role.getPositionCache().clear(dt); + } + } + + public static class PreBehaviourSupportTickSystem extends SteppableTickingSystem { + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType playerComponentType; + @Nonnull + private final ComponentType deathComponentType; + @Nonnull + private final Set> dependencies; + + public PreBehaviourSupportTickSystem(@Nonnull ComponentType npcComponentType) { + this.npcComponentType = npcComponentType; + this.playerComponentType = Player.getComponentType(); + this.deathComponentType = DeathComponent.getComponentType(); + this.dependencies = Set.of(new SystemDependency<>(Order.BEFORE, RoleSystems.BehaviourTickSystem.class)); + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Nonnull + @Override + public Query getQuery() { + return this.npcComponentType; + } + + @Override + public void steppedTick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + MarkedEntitySupport markedEntitySupport = role.getMarkedEntitySupport(); + Ref[] entityTargets = markedEntitySupport.getEntityTargets(); + + for (int i = 0; i < entityTargets.length; i++) { + Ref targetReference = entityTargets[i]; + if (targetReference != null) { + if (!targetReference.isValid()) { + entityTargets[i] = null; + } else { + Player playerComponent = commandBuffer.getComponent(targetReference, this.playerComponentType); + if (playerComponent != null && playerComponent.getGameMode() != GameMode.Adventure) { + if (playerComponent.getGameMode() != GameMode.Creative) { + entityTargets[i] = null; + continue; + } + + PlayerSettings playerSettingsComponent = commandBuffer.getComponent(targetReference, PlayerSettings.getComponentType()); + if (playerSettingsComponent == null || !playerSettingsComponent.creativeSettings().allowNPCDetection()) { + entityTargets[i] = null; + continue; + } + } + + DeathComponent deathComponent = commandBuffer.getComponent(targetReference, this.deathComponentType); + if (deathComponent != null) { + entityTargets[i] = null; + } + } + } + } + + role.clearOnceIfNeeded(); + role.getBodySteering().clear(); + role.getHeadSteering().clear(); + role.getIgnoredEntitiesForAvoidance().clear(); + npcComponent.invalidateCachedHorizontalSpeedMultiplier(); + } + } + + public static class RoleActivateSystem extends HolderSystem { + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType modelComponentType; + @Nonnull + private final ComponentType boundingBoxComponentType; + @Nonnull + private final Query query; + @Nonnull + private final Set> dependencies; + + public RoleActivateSystem(@Nonnull ComponentType npcComponentType) { + this.npcComponentType = npcComponentType; + this.modelComponentType = ModelComponent.getComponentType(); + this.boundingBoxComponentType = BoundingBox.getComponentType(); + this.query = Query.and(npcComponentType, this.modelComponentType, this.boundingBoxComponentType); + this.dependencies = Set.of( + new SystemDependency<>(Order.AFTER, BalancingInitialisationSystem.class), new SystemDependency<>(Order.AFTER, ModelSystems.ModelSpawned.class) + ); + } + + @Nonnull + @Override + public Query getQuery() { + return this.query; + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Override + public void onEntityAdd(@Nonnull Holder holder, @Nonnull AddReason reason, @Nonnull Store store) { + NPCEntity npcComponent = holder.getComponent(this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + role.getStateSupport().activate(); + role.getDebugSupport().activate(); + ModelComponent modelComponent = holder.getComponent(this.modelComponentType); + + assert modelComponent != null; + + BoundingBox boundingBoxComponent = holder.getComponent(this.boundingBoxComponentType); + + assert boundingBoxComponent != null; + + role.updateMotionControllers(null, modelComponent.getModel(), boundingBoxComponent.getBoundingBox(), null); + role.clearOnce(); + role.getActiveMotionController().activate(); + holder.ensureComponent(InteractionModule.get().getChainingDataComponent()); + } + + @Override + public void onEntityRemoved(@Nonnull Holder holder, @Nonnull RemoveReason reason, @Nonnull Store store) { + NPCEntity npcComponent = holder.getComponent(this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + role.getActiveMotionController().deactivate(); + role.getWorldSupport().resetAllBlockSensors(); + } + } + + public static class RoleDebugSystem extends SteppableTickingSystem { + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final Set> dependencies; + + public RoleDebugSystem(@Nonnull ComponentType npcComponentType, @Nonnull Set> dependencies) { + this.npcComponentType = npcComponentType; + this.dependencies = dependencies; + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount); + } + + @Nonnull + @Override + public Query getQuery() { + return this.npcComponentType; + } + + @Override + public void steppedTick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcComponentType); + + assert npcComponent != null; + + Role role = npcComponent.getRole(); + RoleDebugDisplay debugDisplay = role.getDebugSupport().getDebugDisplay(); + if (debugDisplay != null) { + debugDisplay.display(role, index, archetypeChunk, commandBuffer); + } + } + } +} diff --git a/src/com/hypixel/hytale/server/npc/systems/SpawnReferenceSystems.java b/src/com/hypixel/hytale/server/npc/systems/SpawnReferenceSystems.java new file mode 100644 index 00000000..11b74827 --- /dev/null +++ b/src/com/hypixel/hytale/server/npc/systems/SpawnReferenceSystems.java @@ -0,0 +1,430 @@ +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 { + @Nonnull + private final ComponentType spawnReferenceComponentType; + @Nonnull + private final ComponentType legacySpawnBeaconComponent; + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final Query query; + + public BeaconAddRemoveSystem( + @Nonnull ComponentType spawnReferenceComponentType, + @Nonnull ComponentType legacySpawnBeaconComponent + ) { + this.spawnReferenceComponentType = spawnReferenceComponentType; + this.legacySpawnBeaconComponent = legacySpawnBeaconComponent; + this.npcComponentType = NPCEntity.getComponentType(); + this.query = Archetype.of(spawnReferenceComponentType, this.npcComponentType); + } + + @Nonnull + @Override + public Query getQuery() { + return this.query; + } + + @Override + public void onEntityAdded( + @Nonnull Ref ref, @Nonnull AddReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + switch (reason) { + case LOAD: + SpawnBeaconReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType); + + assert spawnReferenceComponent != null; + + Ref 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 ref, @Nonnull RemoveReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + switch (reason) { + case REMOVE: + SpawnBeaconReference spawnReference = store.getComponent(ref, this.spawnReferenceComponentType); + if (spawnReference == null) { + return; + } else { + Ref 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 { + @Nonnull + private final ComponentType spawnReferenceComponentType; + @Nonnull + private final ComponentType spawnMarkerEntityComponentType; + @Nonnull + private final ComponentType npcComponentType; + @Nonnull + private final ComponentType worldGenIdComponentType; + @Nonnull + private final ComponentType uuidComponentComponentType; + @Nonnull + private final ResourceType worldTimeResourceResourceType; + @Nonnull + private final Query query; + + public MarkerAddRemoveSystem( + @Nonnull ComponentType spawnReferenceComponentType, + @Nonnull ComponentType 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 getQuery() { + return this.query; + } + + @Override + public void onEntityAdded( + @Nonnull Ref ref, @Nonnull AddReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + switch (reason) { + case LOAD: + SpawnMarkerReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType); + + assert spawnReferenceComponent != null; + + Ref 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 ref, @Nonnull RemoveReason reason, @Nonnull Store store, @Nonnull CommandBuffer commandBuffer + ) { + switch (reason) { + case REMOVE: + SpawnMarkerReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType); + if (spawnReferenceComponent == null) { + return; + } else { + Ref 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 { + @Nonnull + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, NPCPreTickSystem.class), new SystemDependency<>(Order.BEFORE, DeathSystems.CorpseRemoval.class) + ); + @Nonnull + private final ComponentType spawnReferenceComponentType; + @Nonnull + private final ComponentType npcEntityComponentType; + @Nonnull + private final Query query; + + public TickingSpawnBeaconSystem(@Nonnull ComponentType spawnReferenceComponentType) { + this.spawnReferenceComponentType = spawnReferenceComponentType; + this.npcEntityComponentType = NPCEntity.getComponentType(); + this.query = Archetype.of(spawnReferenceComponentType, this.npcEntityComponentType); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer 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 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 { + @Nonnull + private static final Set> DEPENDENCIES = Set.of( + new SystemDependency<>(Order.AFTER, NPCPreTickSystem.class), new SystemDependency<>(Order.BEFORE, DeathSystems.CorpseRemoval.class) + ); + @Nonnull + private final ComponentType spawnReferenceComponentType; + @Nonnull + private final ComponentType markerTypeComponentType; + @Nonnull + private final ComponentType npcEntityComponentType; + @Nonnull + private final Query query; + + public TickingSpawnMarkerSystem( + @Nonnull ComponentType spawnReferenceComponentType, + @Nonnull ComponentType markerTypeComponentType + ) { + this.spawnReferenceComponentType = spawnReferenceComponentType; + this.markerTypeComponentType = markerTypeComponentType; + this.npcEntityComponentType = NPCEntity.getComponentType(); + this.query = Archetype.of(spawnReferenceComponentType, this.npcEntityComponentType); + } + + @Nonnull + @Override + public Set> getDependencies() { + return DEPENDENCIES; + } + + @Nonnull + @Override + public Query 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 archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer 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 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)); + } + } + } + } + } + } +} diff --git a/src/com/hypixel/hytale/server/npc/systems/SteeringSystem.java b/src/com/hypixel/hytale/server/npc/systems/SteeringSystem.java new file mode 100644 index 00000000..5d7fe70b --- /dev/null +++ b/src/com/hypixel/hytale/server/npc/systems/SteeringSystem.java @@ -0,0 +1,128 @@ +package com.hypixel.hytale.server.npc.systems; + +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.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.math.vector.Vector3d; +import com.hypixel.hytale.server.core.entity.knockback.KnockbackSystems; +import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent; +import com.hypixel.hytale.server.core.modules.entity.system.TransformSystems; +import com.hypixel.hytale.server.core.modules.physics.util.PhysicsMath; +import com.hypixel.hytale.server.core.universe.world.storage.EntityStore; +import com.hypixel.hytale.server.npc.NPCPlugin; +import com.hypixel.hytale.server.npc.entities.NPCEntity; +import com.hypixel.hytale.server.npc.role.Role; + +import javax.annotation.Nonnull; +import java.util.Set; +import java.util.logging.Level; + +public class SteeringSystem extends SteppableTickingSystem { + @Nonnull + private final ComponentType npcEntityComponent; + @Nonnull + private final Set> dependencies; + @Nonnull + private final Query query; + + public SteeringSystem(@Nonnull ComponentType npcEntityComponent) { + this.npcEntityComponent = npcEntityComponent; + this.dependencies = Set.of( + new SystemDependency<>(Order.AFTER, AvoidanceSystem.class), + new SystemDependency<>(Order.AFTER, KnockbackSystems.ApplyKnockback.class), + new SystemDependency<>(Order.BEFORE, TransformSystems.EntityTrackerUpdate.class) + ); + this.query = Query.and(npcEntityComponent, TransformComponent.getComponentType()); + } + + @Nonnull + @Override + public Set> getDependencies() { + return this.dependencies; + } + + @Override + public boolean isParallel(int archetypeChunkSize, int taskCount) { + return false; + } + + @Nonnull + @Override + public Query getQuery() { + return this.query; + } + + @Override + public void steppedTick( + float dt, + int index, + @Nonnull ArchetypeChunk archetypeChunk, + @Nonnull Store store, + @Nonnull CommandBuffer commandBuffer + ) { + NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcEntityComponent); + + assert npcComponent != null; + + TransformComponent transformComponent = archetypeChunk.getComponent(index, TransformComponent.getComponentType()); + + assert transformComponent != null; + + Role role = npcComponent.getRole(); + if (role != null) { + // Distance-based tick skipping optimization + TickSkipState tickSkip = role.getSteeringTickSkip(); + tickSkip.add(dt); + + if (!tickSkip.shouldProcess(transformComponent.getPosition(), store, dt)) { + return; + } + + float effectiveDt = tickSkip.consume(); + + Ref ref = archetypeChunk.getReferenceTo(index); + + try { + if (role.getDebugSupport().isDebugMotionSteering()) { + Vector3d position = transformComponent.getPosition(); + double x = position.getX(); + double z = position.getZ(); + float yaw = transformComponent.getRotation().getYaw(); + role.getActiveMotionController().steer(ref, role, role.getBodySteering(), role.getHeadSteering(), effectiveDt, commandBuffer); + x = position.getX() - x; + z = position.getZ() - z; + double l = Math.sqrt(x * x + z * z); + double v = l / dt; + double vx = x / dt; + double vz = z / dt; + double vh = l > 0.0 ? PhysicsMath.normalizeTurnAngle(PhysicsMath.headingFromDirection(x, z)) : 0.0; + NPCPlugin.get() + .getLogger() + .at(Level.FINER) + .log( + "= Role = t =%.4f v =%.4f vx=%.4f vz=%.4f h =%.4f nh=%.4f vh=%.4f", + dt, + v, + vx, + vz, + (180.0F / (float) Math.PI) * yaw, + (180.0F / (float) Math.PI) * yaw, + 180.0F / (float) Math.PI * vh + ); + } else { + role.getActiveMotionController().steer(ref, role, role.getBodySteering(), role.getHeadSteering(), effectiveDt, commandBuffer); + } + } catch (IllegalStateException | IllegalArgumentException var26) { + NPCPlugin.get().getLogger().at(Level.SEVERE).withCause(var26).log(); + commandBuffer.removeEntity(archetypeChunk.getReferenceTo(index), RemoveReason.REMOVE); + } + } + } +} diff --git a/src/com/hypixel/hytale/server/npc/systems/TickSkipState.java b/src/com/hypixel/hytale/server/npc/systems/TickSkipState.java new file mode 100644 index 00000000..c0ab1e4d --- /dev/null +++ b/src/com/hypixel/hytale/server/npc/systems/TickSkipState.java @@ -0,0 +1,91 @@ +package com.hypixel.hytale.server.npc.systems; + +import com.hypixel.hytale.component.Ref; +import com.hypixel.hytale.component.ResourceType; +import com.hypixel.hytale.component.Store; +import com.hypixel.hytale.component.spatial.SpatialResource; +import com.hypixel.hytale.math.vector.Vector3d; +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.universe.world.storage.EntityStore; + +import javax.annotation.Nonnull; + +/** + * Per-NPC state for distance-based tick skipping optimization. + * Caches player distance (100ms TTL) and accumulates skipped delta time. + */ +public final class TickSkipState { + private static final float DISTANCE_CACHE_TTL = 0.1f; // 100ms + private static final float MIN_DISTANCE = 32.0f; + private static final float MAX_DISTANCE = 96.0f; + private static final float DISTANCE_RANGE = MAX_DISTANCE - MIN_DISTANCE; + private static final int MAX_TICK_INTERVAL = 20; + + private static final ResourceType, EntityStore>> PLAYER_SPATIAL = + EntityModule.get().getPlayerSpatialResourceType(); + + private float accumulated; + private float cachedDistanceSq; + private float distanceCacheAge; + + public void add(float dt) { + this.accumulated += dt; + this.distanceCacheAge += dt; + } + + public float get() { + return this.accumulated; + } + + public float consume() { + float value = this.accumulated; + this.accumulated = 0f; + return value; + } + + /** + * Updates cached distance if stale, then checks if tick should be processed. + */ + public boolean shouldProcess(@Nonnull Vector3d position, @Nonnull Store store, float dt) { + // Refresh distance cache if stale + if (this.distanceCacheAge >= DISTANCE_CACHE_TTL) { + this.cachedDistanceSq = getDistanceSquaredToNearestPlayer(position, store); + this.distanceCacheAge = 0f; + } + + int interval = calculateTickInterval(this.cachedDistanceSq); + if (interval <= 1) { + return true; + } + return this.accumulated >= dt * interval; + } + + private static float getDistanceSquaredToNearestPlayer(Vector3d position, Store store) { + SpatialResource, EntityStore> spatial = store.getResource(PLAYER_SPATIAL); + if (spatial == null) { + return 0f; + } + Ref closest = spatial.getSpatialStructure().closest(position); + if (closest == null || !closest.isValid()) { + return Float.MAX_VALUE; + } + TransformComponent transform = store.getComponent(closest, TransformComponent.getComponentType()); + if (transform == null) { + return Float.MAX_VALUE; + } + return (float) position.distanceSquaredTo(transform.getPosition()); + } + + private static int calculateTickInterval(float distanceSq) { + float distance = (float) Math.sqrt(distanceSq); + if (distance <= MIN_DISTANCE) { + return 1; + } + if (distance >= MAX_DISTANCE) { + return MAX_TICK_INTERVAL; + } + float t = (distance - MIN_DISTANCE) / DISTANCE_RANGE; + return 1 + (int) ((MAX_TICK_INTERVAL - 1) * t * t); + } +} diff --git a/src/com/hypixel/hytale/server/spawning/controllers/BeaconSpawnController.java b/src/com/hypixel/hytale/server/spawning/controllers/BeaconSpawnController.java new file mode 100644 index 00000000..aab2f15b --- /dev/null +++ b/src/com/hypixel/hytale/server/spawning/controllers/BeaconSpawnController.java @@ -0,0 +1,262 @@ +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 { + @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 ownerRef; + private final List> spawnedEntities = new ObjectArrayList<>(); + private final List playersInRegion = new ObjectArrayList<>(); + private int nextPlayerIndex = 0; + private final Object2IntMap entitiesPerPlayer = new Object2IntOpenHashMap<>(); + private final Object2DoubleMap> entityTimeoutCounter = new Object2DoubleOpenHashMap<>(); + private final IntSet unspawnableRoles = new IntOpenHashSet(); + private final Comparator 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 ownerRef) { + super(world); + this.ownerRef = ownerRef; + } + + @Override + public int getMaxActiveJobs() { + return Math.min(this.remainingSpawns, this.baseMaxActiveJobs); + } + + @Nullable + public NPCBeaconSpawnJob createRandomSpawnJob(@Nonnull ComponentAccessor 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 getOwnerRef() { + return this.ownerRef; + } + + public int[] getBaseMaxConcurrentSpawns() { + return this.baseMaxConcurrentSpawns; + } + + public List 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> getSpawnedEntities() { + return this.spawnedEntities; + } + + public void setNextPlayerIndex(int nextPlayerIndex) { + this.nextPlayerIndex = nextPlayerIndex; + } + + public Object2DoubleMap> getEntityTimeoutCounter() { + return this.entityTimeoutCounter; + } + + public Object2IntMap getEntitiesPerPlayer() { + return this.entitiesPerPlayer; + } + + public boolean isDespawnNPCsIfIdle() { + return this.despawnNPCsIfIdle; + } + + public double getDespawnNPCAfterTimeout() { + return this.despawnNPCAfterTimeout; + } + + public Comparator getThreatComparator() { + return this.threatComparator; + } + + public void notifySpawnedEntityExists(@Nonnull Ref ref, @Nonnull ComponentAccessor 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 componentAccessor) { + if (++this.spawnsThisRound >= this.currentScaledMaxConcurrentSpawns) { + this.onAllConcurrentSpawned(componentAccessor); + } + } + + public void notifyNPCRemoval(@Nonnull Ref ref, @Nonnull ComponentAccessor 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 componentAccessor) { + this.spawnsThisRound = 0; + this.remainingSpawns = 0; + LegacySpawnBeaconEntity.prepareNextSpawnTimer(this.ownerRef, componentAccessor); + this.roundStart = true; + } +} diff --git a/src/com/hypixel/hytale/server/spawning/spawnmarkers/SpawnMarkerEntity.java b/src/com/hypixel/hytale/server/spawning/spawnmarkers/SpawnMarkerEntity.java new file mode 100644 index 00000000..2cf10374 --- /dev/null +++ b/src/com/hypixel/hytale/server/spawning/spawnmarkers/SpawnMarkerEntity.java @@ -0,0 +1,547 @@ +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 { + private static final double SPAWN_LOST_TIMEOUT = 35.0; + @Nonnull + private static final InvalidatablePersistentRef[] EMPTY_REFERENCES = new InvalidatablePersistentRef[0]; + public static final ArrayCodec NPC_REFERENCES_CODEC = new ArrayCodec<>( + InvalidatablePersistentRef.CODEC, InvalidatablePersistentRef[]::new + ); + @Nonnull + public static final BuilderCodec 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 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, NPCEntity>> tempStorageList; + private double timeToDeactivation; + private boolean despawnStarted; + private double spawnLostTimeoutCounter; + + public static ComponentType 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 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, NPCEntity>> getTempStorageList() { + return this.tempStorageList; + } + + public void setTempStorageList(@Nonnull List, 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 ref, @Nonnull SpawnMarker marker, @Nonnull Store 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> results = SpatialResource.getThreadLocalReferenceList(); + SpatialResource, 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, Store> 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, 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 npcRef = npcPair.first(); + NPCEntity npcComponent = npcPair.second(); + Ref 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 self, + @Nonnull UUID uuid, + @Nonnull String role, + @Nonnull Vector3d position, + @Nonnull Store 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 markerRef, @Nonnull Store 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 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() { + } + } +}