Initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
.gradle/
|
||||
build/
|
||||
repo/
|
||||
patcher
|
||||
Assets
|
||||
.hytale-downloader-credentials.json
|
||||
150
build.gradle.kts
Normal file
150
build.gradle.kts
Normal file
@@ -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<JavaCompile> {
|
||||
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<String, ByteArray>()
|
||||
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<String, ByteArray?>() // 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")
|
||||
}
|
||||
}
|
||||
BIN
scripts/hytale-downloader-linux-amd64
Executable file
BIN
scripts/hytale-downloader-linux-amd64
Executable file
Binary file not shown.
42
scripts/pull_generated_into_src.sh
Executable file
42
scripts/pull_generated_into_src.sh
Executable file
@@ -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)."
|
||||
38
scripts/update-server.sh
Executable file
38
scripts/update-server.sh
Executable file
@@ -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"
|
||||
295
src/coldfusion/hytaleserver/InventoryOwnershipGuard.java
Normal file
295
src/coldfusion/hytaleserver/InventoryOwnershipGuard.java
Normal file
@@ -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).
|
||||
* <p>
|
||||
* This class is called by the patched LivingEntity.setInventory() method
|
||||
* to validate that an Inventory isn't being shared between multiple players.
|
||||
* <p>
|
||||
* 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<Integer, UUID> inventoryOwners = new ConcurrentHashMap<>();
|
||||
private static final Map<Integer, UUID> 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()));
|
||||
}
|
||||
}
|
||||
@@ -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<SetMemoriesCapacityInteraction> CODEC = BuilderCodec.builder(
|
||||
SetMemoriesCapacityInteraction.class, SetMemoriesCapacityInteraction::new, SimpleInstantInteraction.CODEC
|
||||
)
|
||||
.documentation("Sets how many memories a player can store.")
|
||||
.<Integer>appendInherited(
|
||||
new KeyedCodec<>("Capacity", Codec.INTEGER), (i, s) -> i.capacity = s, i -> i.capacity, (i, parent) -> i.capacity = parent.capacity
|
||||
)
|
||||
.documentation("Defines the amount of memories that a player can store.")
|
||||
.add()
|
||||
.build();
|
||||
private int capacity;
|
||||
|
||||
public SetMemoriesCapacityInteraction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void firstRun(@Nonnull InteractionType type, @Nonnull InteractionContext context, @Nonnull CooldownHandler cooldownHandler) {
|
||||
// HyFix #52: Validate ComponentType before use to prevent crash when memories module isn't loaded
|
||||
if (!PlayerMemories.getComponentType().isValid()) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
Ref<EntityStore> ref = context.getEntity();
|
||||
CommandBuffer<EntityStore> commandBuffer = context.getCommandBuffer();
|
||||
|
||||
assert commandBuffer != null;
|
||||
|
||||
PlayerMemories memoriesComponent = commandBuffer.ensureAndGetComponent(ref, PlayerMemories.getComponentType());
|
||||
if (this.capacity <= memoriesComponent.getMemoriesCapacity()) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
} else {
|
||||
int previousCapacity = memoriesComponent.getMemoriesCapacity();
|
||||
memoriesComponent.setMemoriesCapacity(this.capacity);
|
||||
if (previousCapacity <= 0) {
|
||||
PlayerRef playerRefComponent = commandBuffer.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
PacketHandler playerConnection = playerRefComponent.getPacketHandler();
|
||||
playerConnection.writeNoCache(new UpdateMemoriesFeatureStatus(true));
|
||||
NotificationUtil.sendNotification(
|
||||
playerConnection, Message.translation("server.memories.general.featureUnlockedNotification"), null, "NotificationIcons/MemoriesIcon.png"
|
||||
);
|
||||
playerRefComponent.sendMessage(Message.translation("server.memories.general.featureUnlockedMessage"));
|
||||
}
|
||||
|
||||
context.getState().state = InteractionState.Finished;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public WaitForDataFrom getWaitForDataFrom() {
|
||||
return WaitForDataFrom.Server;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return super.toString();
|
||||
}
|
||||
}
|
||||
@@ -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<GatherObjectiveTask> CODEC = BuilderCodec.builder(
|
||||
GatherObjectiveTask.class, GatherObjectiveTask::new, CountObjectiveTask.CODEC
|
||||
)
|
||||
.build();
|
||||
|
||||
public GatherObjectiveTask(@Nonnull GatherObjectiveTaskAsset asset, int taskSetIndex, int taskIndex) {
|
||||
super(asset, taskSetIndex, taskIndex);
|
||||
}
|
||||
|
||||
protected GatherObjectiveTask() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public GatherObjectiveTaskAsset getAsset() {
|
||||
return (GatherObjectiveTaskAsset) super.getAsset();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
protected TransactionRecord[] setup0(@Nonnull Objective objective, @Nonnull World world, @Nonnull Store<EntityStore> store) {
|
||||
Set<UUID> participatingPlayers = objective.getPlayerUUIDs();
|
||||
int countItem = this.countObjectiveItemInInventories(participatingPlayers, store);
|
||||
if (this.areTaskConditionsFulfilled(null, null, participatingPlayers)) {
|
||||
this.count = MathUtil.clamp(countItem, 0, this.getAsset().getCount());
|
||||
if (this.checkCompletion()) {
|
||||
this.consumeTaskConditions(null, null, participatingPlayers);
|
||||
this.complete = true;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
this.eventRegistry.register(LivingEntityInventoryChangeEvent.class, world.getName(), event -> {
|
||||
LivingEntity livingEntity = event.getEntity();
|
||||
if (livingEntity instanceof Player) {
|
||||
Ref<EntityStore> ref = livingEntity.getReference();
|
||||
World refWorld = store.getExternalData().getWorld();
|
||||
refWorld.execute(() -> {
|
||||
// HyFix: Validate ref before use - prevents NPE crash when player disconnects
|
||||
// between event dispatch and lambda execution
|
||||
if (ref == null || !ref.isValid()) {
|
||||
return;
|
||||
}
|
||||
UUIDComponent uuidComponent = store.getComponent(ref, UUIDComponent.getComponentType());
|
||||
if (uuidComponent == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
Set<UUID> activePlayerUUIDs = objective.getActivePlayerUUIDs();
|
||||
if (activePlayerUUIDs.contains(uuidComponent.getUuid())) {
|
||||
int count = this.countObjectiveItemInInventories(activePlayerUUIDs, store);
|
||||
this.setTaskCompletion(store, ref, count, objective);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
return RegistrationTransactionRecord.wrap(this.eventRegistry);
|
||||
}
|
||||
|
||||
private int countObjectiveItemInInventories(@Nonnull Set<UUID> participatingPlayers, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
int count = 0;
|
||||
BlockTagOrItemIdField blockTypeOrSet = this.getAsset().getBlockTagOrItemIdField();
|
||||
|
||||
for (UUID playerUUID : participatingPlayers) {
|
||||
PlayerRef playerRefComponent = Universe.get().getPlayer(playerUUID);
|
||||
if (playerRefComponent != null) {
|
||||
Ref<EntityStore> playerRef = playerRefComponent.getReference();
|
||||
if (playerRef != null && playerRef.isValid()) {
|
||||
Player playerComponent = componentAccessor.getComponent(playerRef, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
CombinedItemContainer inventory = playerComponent.getInventory().getCombinedHotbarFirst();
|
||||
count += inventory.countItemStacks(itemStack -> blockTypeOrSet.isBlockTypeIncluded(itemStack.getItemId()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "GatherObjectiveTask{} " + super.toString();
|
||||
}
|
||||
}
|
||||
@@ -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<EntityStore> {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
@Nonnull
|
||||
private final BlockingQueue<CraftingManager.CraftingJob> queuedCraftingJobs = new LinkedBlockingQueue<>();
|
||||
@Nullable
|
||||
private CraftingManager.BenchUpgradingJob upgradingJob;
|
||||
private int x;
|
||||
private int y;
|
||||
private int z;
|
||||
@Nullable
|
||||
private BlockType blockType;
|
||||
|
||||
@Nonnull
|
||||
public static ComponentType<EntityStore, CraftingManager> getComponentType() {
|
||||
return CraftingPlugin.get().getCraftingManagerComponentType();
|
||||
}
|
||||
|
||||
public CraftingManager() {
|
||||
}
|
||||
|
||||
private CraftingManager(@Nonnull CraftingManager other) {
|
||||
this.x = other.x;
|
||||
this.y = other.y;
|
||||
this.z = other.z;
|
||||
this.blockType = other.blockType;
|
||||
this.queuedCraftingJobs.addAll(other.queuedCraftingJobs);
|
||||
this.upgradingJob = other.upgradingJob;
|
||||
}
|
||||
|
||||
public boolean hasBenchSet() {
|
||||
return this.blockType != null;
|
||||
}
|
||||
|
||||
public void setBench(int x, int y, int z, @Nonnull BlockType blockType) {
|
||||
Bench bench = blockType.getBench();
|
||||
Objects.requireNonNull(bench, "blockType isn't a bench!");
|
||||
if (bench.getType() != BenchType.Crafting
|
||||
&& bench.getType() != BenchType.DiagramCrafting
|
||||
&& bench.getType() != BenchType.StructuralCrafting
|
||||
&& bench.getType() != BenchType.Processing) {
|
||||
throw new IllegalArgumentException("blockType isn't a crafting bench!");
|
||||
}
|
||||
// HyFix: Auto-clear stale bench reference instead of throwing exception
|
||||
// This prevents "Bench blockType is already set!" crashes when player rapidly
|
||||
// opens multiple benches or previous interaction didn't properly clean up
|
||||
else if (this.blockType != null) {
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.z = 0;
|
||||
this.blockType = null;
|
||||
this.queuedCraftingJobs.clear();
|
||||
this.upgradingJob = null;
|
||||
}
|
||||
if (!this.queuedCraftingJobs.isEmpty()) {
|
||||
throw new IllegalArgumentException("Queue already has jobs!");
|
||||
} else if (this.upgradingJob != null) {
|
||||
throw new IllegalArgumentException("Upgrading job is already set!");
|
||||
} else {
|
||||
this.x = x;
|
||||
this.y = y;
|
||||
this.z = z;
|
||||
this.blockType = blockType;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean clearBench(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
boolean result = this.cancelAllCrafting(ref, componentAccessor);
|
||||
this.x = 0;
|
||||
this.y = 0;
|
||||
this.z = 0;
|
||||
this.blockType = null;
|
||||
this.upgradingJob = null;
|
||||
return result;
|
||||
}
|
||||
|
||||
public boolean craftItem(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull CraftingRecipe recipe,
|
||||
int quantity,
|
||||
@Nonnull ItemContainer itemContainer
|
||||
) {
|
||||
if (this.upgradingJob != null) {
|
||||
return false;
|
||||
} else {
|
||||
Objects.requireNonNull(recipe, "Recipe can't be null");
|
||||
CraftRecipeEvent.Pre preEvent = new CraftRecipeEvent.Pre(recipe, quantity);
|
||||
componentAccessor.invoke(ref, preEvent);
|
||||
if (preEvent.isCancelled()) {
|
||||
return false;
|
||||
} else if (!this.isValidBenchForRecipe(ref, componentAccessor, recipe)) {
|
||||
return false;
|
||||
} else {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
if (playerComponent.getGameMode() != GameMode.Creative && !removeInputFromInventory(itemContainer, recipe, quantity)) {
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
String translationKey = getRecipeOutputTranslationKey(recipe);
|
||||
if (translationKey != null) {
|
||||
NotificationUtil.sendNotification(
|
||||
playerRefComponent.getPacketHandler(),
|
||||
Message.translation("server.general.crafting.missingIngredient").param("item", Message.translation(translationKey)),
|
||||
NotificationStyle.Danger
|
||||
);
|
||||
}
|
||||
|
||||
LOGGER.at(Level.FINE).log("Missing items required to craft the item: %s", recipe);
|
||||
return false;
|
||||
} else {
|
||||
CraftRecipeEvent.Post postEvent = new CraftRecipeEvent.Post(recipe, quantity);
|
||||
componentAccessor.invoke(ref, postEvent);
|
||||
if (postEvent.isCancelled()) {
|
||||
return true;
|
||||
} else {
|
||||
giveOutput(ref, componentAccessor, recipe, quantity);
|
||||
IEventDispatcher<PlayerCraftEvent, PlayerCraftEvent> dispatcher = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(PlayerCraftEvent.class, world.getName());
|
||||
if (dispatcher.hasListener()) {
|
||||
dispatcher.dispatch(new PlayerCraftEvent(ref, playerComponent, recipe, quantity));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static String getRecipeOutputTranslationKey(@Nonnull CraftingRecipe recipe) {
|
||||
String itemId = recipe.getPrimaryOutput().getItemId();
|
||||
if (itemId == null) {
|
||||
return null;
|
||||
} else {
|
||||
Item itemAsset = Item.getAssetMap().getAsset(itemId);
|
||||
return itemAsset != null ? itemAsset.getTranslationKey() : null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean queueCraft(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull CraftingWindow window,
|
||||
int transactionId,
|
||||
@Nonnull CraftingRecipe recipe,
|
||||
int quantity,
|
||||
@Nonnull ItemContainer inputItemContainer,
|
||||
@Nonnull CraftingManager.InputRemovalType inputRemovalType
|
||||
) {
|
||||
if (this.upgradingJob != null) {
|
||||
return false;
|
||||
} else {
|
||||
Objects.requireNonNull(recipe, "Recipe can't be null");
|
||||
if (!this.isValidBenchForRecipe(ref, componentAccessor, recipe)) {
|
||||
return false;
|
||||
} else {
|
||||
float recipeTime = recipe.getTimeSeconds();
|
||||
if (recipeTime > 0.0F) {
|
||||
int level = this.getBenchTierLevel(componentAccessor);
|
||||
if (level > 1) {
|
||||
BenchTierLevel tierLevelData = this.getBenchTierLevelData(level);
|
||||
if (tierLevelData != null) {
|
||||
recipeTime -= recipeTime * tierLevelData.getCraftingTimeReductionModifier();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.queuedCraftingJobs
|
||||
.offer(new CraftingManager.CraftingJob(window, transactionId, recipe, quantity, recipeTime, inputItemContainer, inputRemovalType));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void tick(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, float dt) {
|
||||
if (this.upgradingJob != null) {
|
||||
if (dt > 0.0F) {
|
||||
this.upgradingJob.timeSecondsCompleted += dt;
|
||||
}
|
||||
|
||||
this.upgradingJob.window.updateBenchUpgradeJob(this.upgradingJob.computeLoadingPercent());
|
||||
if (this.upgradingJob.timeSecondsCompleted >= this.upgradingJob.timeSeconds) {
|
||||
this.upgradingJob.window.updateBenchTierLevel(this.finishTierUpgrade(ref, componentAccessor));
|
||||
this.upgradingJob = null;
|
||||
}
|
||||
} else {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
while (dt > 0.0F && !this.queuedCraftingJobs.isEmpty()) {
|
||||
CraftingManager.CraftingJob currentJob = this.queuedCraftingJobs.peek();
|
||||
boolean isCreativeMode = playerComponent.getGameMode() == GameMode.Creative;
|
||||
if (currentJob != null && currentJob.quantityStarted < currentJob.quantity && currentJob.quantityStarted <= currentJob.quantityCompleted) {
|
||||
LOGGER.at(Level.FINE).log("Removing Items for next quantity: %s", currentJob);
|
||||
int currentItemId = currentJob.quantityStarted++;
|
||||
if (!isCreativeMode && !removeInputFromInventory(currentJob, currentItemId)) {
|
||||
String translationKey = getRecipeOutputTranslationKey(currentJob.recipe);
|
||||
if (translationKey != null) {
|
||||
NotificationUtil.sendNotification(
|
||||
playerRefComponent.getPacketHandler(),
|
||||
Message.translation("server.general.crafting.missingIngredient").param("item", Message.translation(translationKey)),
|
||||
NotificationStyle.Danger
|
||||
);
|
||||
}
|
||||
|
||||
LOGGER.at(Level.FINE).log("Missing items required to craft the item: %s", currentJob);
|
||||
currentJob = null;
|
||||
this.queuedCraftingJobs.poll();
|
||||
}
|
||||
|
||||
if (!isCreativeMode
|
||||
&& currentJob != null
|
||||
&& currentJob.quantityStarted < currentJob.quantity
|
||||
&& currentJob.quantityStarted <= currentJob.quantityCompleted) {
|
||||
NotificationUtil.sendNotification(
|
||||
playerRefComponent.getPacketHandler(),
|
||||
Message.translation("server.general.crafting.failedTakingCorrectQuantity"),
|
||||
NotificationStyle.Danger
|
||||
);
|
||||
LOGGER.at(Level.SEVERE).log("Failed to remove the correct quantity of input, removing crafting job %s", currentJob);
|
||||
currentJob = null;
|
||||
this.queuedCraftingJobs.poll();
|
||||
}
|
||||
}
|
||||
|
||||
if (currentJob != null) {
|
||||
currentJob.timeSecondsCompleted += dt;
|
||||
float percent = currentJob.timeSeconds <= 0.0F ? 1.0F : currentJob.timeSecondsCompleted / currentJob.timeSeconds;
|
||||
if (percent > 1.0F) {
|
||||
percent = 1.0F;
|
||||
}
|
||||
|
||||
currentJob.window.updateCraftingJob(percent);
|
||||
LOGGER.at(Level.FINEST).log("Update time: %s", currentJob);
|
||||
dt = 0.0F;
|
||||
if (currentJob.timeSecondsCompleted >= currentJob.timeSeconds) {
|
||||
dt = currentJob.timeSecondsCompleted - currentJob.timeSeconds;
|
||||
int currentCompletedItemId = currentJob.quantityCompleted++;
|
||||
currentJob.timeSecondsCompleted = 0.0F;
|
||||
LOGGER.at(Level.FINE).log("Crafted 1 Quantity: %s", currentJob);
|
||||
if (currentJob.quantityCompleted == currentJob.quantity) {
|
||||
giveOutput(ref, componentAccessor, currentJob, currentCompletedItemId);
|
||||
LOGGER.at(Level.FINE).log("Crafting Finished: %s", currentJob);
|
||||
this.queuedCraftingJobs.poll();
|
||||
} else {
|
||||
if (currentJob.quantityCompleted > currentJob.quantity) {
|
||||
this.queuedCraftingJobs.poll();
|
||||
throw new RuntimeException("QuantityCompleted is greater than the Quality! " + currentJob);
|
||||
}
|
||||
|
||||
giveOutput(ref, componentAccessor, currentJob, currentCompletedItemId);
|
||||
}
|
||||
|
||||
if (this.queuedCraftingJobs.isEmpty()) {
|
||||
currentJob.window.setBlockInteractionState("default", componentAccessor.getExternalData().getWorld());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public boolean cancelAllCrafting(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
LOGGER.at(Level.FINE).log("Cancel Crafting!");
|
||||
ObjectList<CraftingManager.CraftingJob> oldJobs = new ObjectArrayList<>(this.queuedCraftingJobs.size());
|
||||
this.queuedCraftingJobs.drainTo(oldJobs);
|
||||
if (!oldJobs.isEmpty()) {
|
||||
CraftingManager.CraftingJob currentJob = oldJobs.getFirst();
|
||||
LOGGER.at(Level.FINE).log("Refunding Items for: %s", currentJob);
|
||||
refundInputToInventory(ref, componentAccessor, currentJob, currentJob.quantityStarted - 1);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isValidBenchForRecipe(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingRecipe recipe
|
||||
) {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerConfigData playerConfigData = playerComponent.getPlayerConfigData();
|
||||
String primaryOutputItemId = recipe.getPrimaryOutput() != null ? recipe.getPrimaryOutput().getItemId() : null;
|
||||
if (!recipe.isKnowledgeRequired() || primaryOutputItemId != null && playerConfigData.getKnownRecipes().contains(primaryOutputItemId)) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
if (recipe.getRequiredMemoriesLevel() > 1 && MemoriesPlugin.get().getMemoriesLevel(world.getGameplayConfig()) < recipe.getRequiredMemoriesLevel()) {
|
||||
LOGGER.at(Level.WARNING).log("Attempted to craft %s but doesn't have the required world memories level!", recipe.getId());
|
||||
return false;
|
||||
} else {
|
||||
BenchType benchType = this.blockType != null ? this.blockType.getBench().getType() : BenchType.Crafting;
|
||||
String benchName = this.blockType != null ? this.blockType.getBench().getId() : "Fieldcraft";
|
||||
boolean meetsRequirements = false;
|
||||
BlockState state = world.getState(this.x, this.y, this.z, true);
|
||||
int benchTierLevel = state instanceof BenchState ? ((BenchState) state).getTierLevel() : 0;
|
||||
BenchRequirement[] requirements = recipe.getBenchRequirement();
|
||||
if (requirements != null) {
|
||||
for (BenchRequirement benchRequirement : requirements) {
|
||||
if (benchRequirement.type == benchType && benchName.equals(benchRequirement.id) && benchRequirement.requiredTierLevel <= benchTierLevel) {
|
||||
meetsRequirements = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!meetsRequirements) {
|
||||
LOGGER.at(Level.WARNING)
|
||||
.log("Attempted to craft %s using %s, %s but requires bench %s but a bench is NOT set!", recipe.getId(), benchType, benchName, requirements);
|
||||
return false;
|
||||
} else if (benchType == BenchType.Crafting && !"Fieldcraft".equals(benchName)) {
|
||||
CraftingManager.CraftingJob craftingJob = this.queuedCraftingJobs.peek();
|
||||
return craftingJob == null || craftingJob.recipe.getId().equals(recipe.getId());
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LOGGER.at(Level.WARNING).log("%s - Attempted to craft %s but doesn't know the recipe!", recipe.getId());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void giveOutput(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingManager.CraftingJob job, int currentItemId
|
||||
) {
|
||||
job.removedItems.remove(currentItemId);
|
||||
String recipeId = job.recipe.getId();
|
||||
CraftingRecipe recipeAsset = CraftingRecipe.getAssetMap().getAsset(recipeId);
|
||||
if (recipeAsset == null) {
|
||||
throw new RuntimeException("A non-existent item ID was provided! " + recipeId);
|
||||
} else {
|
||||
giveOutput(ref, componentAccessor, recipeAsset, 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void giveOutput(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingRecipe craftingRecipe, int quantity
|
||||
) {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
if (playerComponent == null) {
|
||||
LOGGER.at(Level.WARNING).log("Attempted to give output to a non-player entity: %s", ref);
|
||||
} else {
|
||||
List<ItemStack> itemStacks = getOutputItemStacks(craftingRecipe, quantity);
|
||||
Inventory inventory = playerComponent.getInventory();
|
||||
SimpleItemContainer.addOrDropItemStacks(componentAccessor, ref, inventory.getCombinedArmorHotbarStorage(), itemStacks);
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean removeInputFromInventory(@Nonnull CraftingManager.CraftingJob job, int currentItemId) {
|
||||
Objects.requireNonNull(job, "Job can't be null!");
|
||||
CraftingRecipe craftingRecipe = job.recipe;
|
||||
Objects.requireNonNull(craftingRecipe, "CraftingRecipe can't be null!");
|
||||
List<MaterialQuantity> materialsToRemove = getInputMaterials(craftingRecipe);
|
||||
if (materialsToRemove.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", job, materialsToRemove);
|
||||
ObjectList<ItemStack> itemStackList = new ObjectArrayList<>();
|
||||
|
||||
boolean succeeded = switch (job.inputRemovalType) {
|
||||
case NORMAL -> {
|
||||
ListTransaction<MaterialTransaction> materialTransactions = job.inputItemContainer.removeMaterials(materialsToRemove, true, true, true);
|
||||
|
||||
for (MaterialTransaction transaction : materialTransactions.getList()) {
|
||||
for (MaterialSlotTransaction slotTransaction : transaction.getList()) {
|
||||
if (!ItemStack.isEmpty(slotTransaction.getOutput())) {
|
||||
itemStackList.add(slotTransaction.getOutput());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
yield materialTransactions.succeeded();
|
||||
}
|
||||
case ORDERED -> {
|
||||
ListTransaction<MaterialSlotTransaction> materialTransactions = job.inputItemContainer
|
||||
.removeMaterialsOrdered(materialsToRemove, true, true, true);
|
||||
|
||||
for (MaterialSlotTransaction transaction : materialTransactions.getList()) {
|
||||
if (!ItemStack.isEmpty(transaction.getOutput())) {
|
||||
itemStackList.add(transaction.getOutput());
|
||||
}
|
||||
}
|
||||
|
||||
yield materialTransactions.succeeded();
|
||||
}
|
||||
default -> throw new IllegalArgumentException("Unknown enum: " + job.inputRemovalType);
|
||||
};
|
||||
job.removedItems.put(currentItemId, itemStackList);
|
||||
job.window.invalidateExtraResources();
|
||||
return succeeded;
|
||||
}
|
||||
}
|
||||
|
||||
private static boolean removeInputFromInventory(@Nonnull ItemContainer itemContainer, @Nonnull CraftingRecipe craftingRecipe, int quantity) {
|
||||
List<MaterialQuantity> materialsToRemove = getInputMaterials(craftingRecipe, quantity);
|
||||
if (materialsToRemove.isEmpty()) {
|
||||
return true;
|
||||
} else {
|
||||
LOGGER.at(Level.FINEST).log("Removing Materials: %s - %s", craftingRecipe, materialsToRemove);
|
||||
ListTransaction<MaterialTransaction> materialTransactions = itemContainer.removeMaterials(materialsToRemove, true, true, true);
|
||||
return materialTransactions.succeeded();
|
||||
}
|
||||
}
|
||||
|
||||
private static void refundInputToInventory(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull CraftingManager.CraftingJob job, int currentItemId
|
||||
) {
|
||||
Objects.requireNonNull(job, "Job can't be null!");
|
||||
List<ItemStack> itemStacks = job.removedItems.get(currentItemId);
|
||||
if (itemStacks != null) {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
SimpleItemContainer.addOrDropItemStacks(componentAccessor, ref, playerComponent.getInventory().getCombinedHotbarFirst(), itemStacks);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<ItemStack> getOutputItemStacks(@Nonnull CraftingRecipe recipe) {
|
||||
return getOutputItemStacks(recipe, 1);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<ItemStack> getOutputItemStacks(@Nonnull CraftingRecipe recipe, int quantity) {
|
||||
Objects.requireNonNull(recipe);
|
||||
MaterialQuantity[] output = recipe.getOutputs();
|
||||
if (output == null) {
|
||||
return List.of();
|
||||
} else {
|
||||
ObjectList<ItemStack> outputItemStacks = new ObjectArrayList<>();
|
||||
|
||||
for (MaterialQuantity outputMaterial : output) {
|
||||
ItemStack outputItemStack = getOutputItemStack(outputMaterial, quantity);
|
||||
if (outputItemStack != null) {
|
||||
outputItemStacks.add(outputItemStack);
|
||||
}
|
||||
}
|
||||
|
||||
return outputItemStacks;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ItemStack getOutputItemStack(@Nonnull MaterialQuantity outputMaterial, @Nonnull String id) {
|
||||
return getOutputItemStack(outputMaterial, 1);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static ItemStack getOutputItemStack(@Nonnull MaterialQuantity outputMaterial, int quantity) {
|
||||
String itemId = outputMaterial.getItemId();
|
||||
if (itemId == null) {
|
||||
return null;
|
||||
} else {
|
||||
int materialQuantity = outputMaterial.getQuantity() <= 0 ? 1 : outputMaterial.getQuantity();
|
||||
return new ItemStack(itemId, materialQuantity * quantity, outputMaterial.getMetadata());
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<MaterialQuantity> getInputMaterials(@Nonnull CraftingRecipe recipe) {
|
||||
return getInputMaterials(recipe, 1);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static List<MaterialQuantity> getInputMaterials(@Nonnull MaterialQuantity[] input) {
|
||||
return getInputMaterials(input, 1);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static List<MaterialQuantity> getInputMaterials(@Nonnull CraftingRecipe recipe, int quantity) {
|
||||
Objects.requireNonNull(recipe);
|
||||
return recipe.getInput() == null ? Collections.emptyList() : getInputMaterials(recipe.getInput(), quantity);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static List<MaterialQuantity> getInputMaterials(@Nonnull MaterialQuantity[] input, int quantity) {
|
||||
ObjectList<MaterialQuantity> materials = new ObjectArrayList<>();
|
||||
|
||||
for (MaterialQuantity craftingMaterial : input) {
|
||||
String itemId = craftingMaterial.getItemId();
|
||||
String resourceTypeId = craftingMaterial.getResourceTypeId();
|
||||
int materialQuantity = craftingMaterial.getQuantity();
|
||||
BsonDocument metadata = craftingMaterial.getMetadata();
|
||||
materials.add(new MaterialQuantity(itemId, resourceTypeId, null, materialQuantity * quantity, metadata));
|
||||
}
|
||||
|
||||
return materials;
|
||||
}
|
||||
|
||||
public static boolean matches(@Nonnull MaterialQuantity craftingMaterial, @Nonnull ItemStack itemStack) {
|
||||
String itemId = craftingMaterial.getItemId();
|
||||
if (itemId != null) {
|
||||
return itemId.equals(itemStack.getItemId());
|
||||
} else {
|
||||
String resourceTypeId = craftingMaterial.getResourceTypeId();
|
||||
if (resourceTypeId != null && itemStack.getItem().getResourceTypes() != null) {
|
||||
for (ItemResourceType itemResourceType : itemStack.getItem().getResourceTypes()) {
|
||||
if (resourceTypeId.equals(itemResourceType.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static JsonArray generateInventoryHints(@Nonnull List<CraftingRecipe> recipes, int inputSlotIndex, @Nonnull ItemContainer container) {
|
||||
JsonArray inventoryHints = new JsonArray();
|
||||
short storageSlotIndex = 0;
|
||||
|
||||
for (short bound = container.getCapacity(); storageSlotIndex < bound; storageSlotIndex++) {
|
||||
ItemStack itemStack = container.getItemStack(storageSlotIndex);
|
||||
if (itemStack != null && !itemStack.isEmpty() && matchesAnyRecipe(recipes, inputSlotIndex, itemStack)) {
|
||||
inventoryHints.add(storageSlotIndex);
|
||||
}
|
||||
}
|
||||
|
||||
return inventoryHints;
|
||||
}
|
||||
|
||||
public static boolean matchesAnyRecipe(@Nonnull List<CraftingRecipe> recipes, int inputSlotIndex, @Nonnull ItemStack slotItemStack) {
|
||||
for (CraftingRecipe recipe : recipes) {
|
||||
MaterialQuantity[] input = recipe.getInput();
|
||||
if (inputSlotIndex < input.length) {
|
||||
MaterialQuantity slotCraftingMaterial = input[inputSlotIndex];
|
||||
if (slotCraftingMaterial.getItemId() != null && slotCraftingMaterial.getItemId().equals(slotItemStack.getItemId())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (slotCraftingMaterial.getResourceTypeId() != null && slotItemStack.getItem().getResourceTypes() != null) {
|
||||
for (ItemResourceType itemResourceType : slotItemStack.getItem().getResourceTypes()) {
|
||||
if (slotCraftingMaterial.getResourceTypeId().equals(itemResourceType.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean startTierUpgrade(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor, @Nonnull BenchWindow window) {
|
||||
if (this.upgradingJob != null) {
|
||||
return false;
|
||||
} else {
|
||||
BenchUpgradeRequirement requirements = this.getBenchUpgradeRequirement(this.getBenchTierLevel(componentAccessor));
|
||||
if (requirements == null) {
|
||||
return false;
|
||||
} else {
|
||||
List<MaterialQuantity> input = getInputMaterials(requirements.getInput());
|
||||
if (input.isEmpty()) {
|
||||
return false;
|
||||
} else {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
if (playerComponent.getGameMode() != GameMode.Creative) {
|
||||
CombinedItemContainer combined = new CombinedItemContainer(
|
||||
playerComponent.getInventory().getCombinedBackpackStorageHotbar(), window.getExtraResourcesSection().getItemContainer()
|
||||
);
|
||||
if (!combined.canRemoveMaterials(input)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
this.upgradingJob = new CraftingManager.BenchUpgradingJob(window, requirements.getTimeSeconds());
|
||||
this.cancelAllCrafting(ref, componentAccessor);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private int finishTierUpgrade(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (this.upgradingJob == null) {
|
||||
return 0;
|
||||
} else {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
BlockState state = world.getState(this.x, this.y, this.z, true);
|
||||
BenchState benchState = state instanceof BenchState ? (BenchState) state : null;
|
||||
if (benchState != null && benchState.getTierLevel() != 0) {
|
||||
BenchUpgradeRequirement requirements = this.getBenchUpgradeRequirement(benchState.getTierLevel());
|
||||
if (requirements == null) {
|
||||
return benchState.getTierLevel();
|
||||
} else {
|
||||
List<MaterialQuantity> input = getInputMaterials(requirements.getInput());
|
||||
if (input.isEmpty()) {
|
||||
return benchState.getTierLevel();
|
||||
} else {
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
boolean canUpgrade = playerComponent.getGameMode() == GameMode.Creative;
|
||||
if (!canUpgrade) {
|
||||
CombinedItemContainer combined = new CombinedItemContainer(
|
||||
playerComponent.getInventory().getCombinedBackpackStorageHotbar(),
|
||||
this.upgradingJob.window.getExtraResourcesSection().getItemContainer()
|
||||
);
|
||||
combined = new CombinedItemContainer(combined, this.upgradingJob.window.getExtraResourcesSection().getItemContainer());
|
||||
ListTransaction<MaterialTransaction> materialTransactions = combined.removeMaterials(input);
|
||||
if (materialTransactions.succeeded()) {
|
||||
List<ItemStack> consumed = new ObjectArrayList<>();
|
||||
|
||||
for (MaterialTransaction transaction : materialTransactions.getList()) {
|
||||
for (MaterialSlotTransaction matSlot : transaction.getList()) {
|
||||
consumed.add(matSlot.getOutput());
|
||||
}
|
||||
}
|
||||
|
||||
benchState.addUpgradeItems(consumed);
|
||||
canUpgrade = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (canUpgrade) {
|
||||
benchState.setTierLevel(benchState.getTierLevel() + 1);
|
||||
if (benchState.getBench().getBenchUpgradeCompletedSoundEventIndex() != 0) {
|
||||
SoundUtil.playSoundEvent3d(
|
||||
benchState.getBench().getBenchUpgradeCompletedSoundEventIndex(),
|
||||
SoundCategory.SFX,
|
||||
this.x + 0.5,
|
||||
this.y + 0.5,
|
||||
this.z + 0.5,
|
||||
componentAccessor
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return benchState.getTierLevel();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BenchTierLevel getBenchTierLevelData(int level) {
|
||||
if (this.blockType == null) {
|
||||
return null;
|
||||
} else {
|
||||
Bench bench = this.blockType.getBench();
|
||||
return bench == null ? null : bench.getTierLevel(level);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private BenchUpgradeRequirement getBenchUpgradeRequirement(int tierLevel) {
|
||||
BenchTierLevel tierData = this.getBenchTierLevelData(tierLevel);
|
||||
return tierData == null ? null : tierData.getUpgradeRequirement();
|
||||
}
|
||||
|
||||
private int getBenchTierLevel(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
BlockState state = world.getState(this.x, this.y, this.z, true);
|
||||
return state instanceof BenchState ? ((BenchState) state).getTierLevel() : 0;
|
||||
}
|
||||
|
||||
public static int feedExtraResourcesSection(@Nonnull BenchState benchState, @Nonnull MaterialExtraResourcesSection extraResourcesSection) {
|
||||
try {
|
||||
CraftingManager.ChestLookupResult result = getContainersAroundBench(benchState);
|
||||
List<ItemContainer> chests = result.containers;
|
||||
List<ItemContainerState> chestStates = result.states;
|
||||
ItemContainer itemContainer = EmptyItemContainer.INSTANCE;
|
||||
if (!chests.isEmpty()) {
|
||||
itemContainer = new CombinedItemContainer(chests.stream().map(container -> {
|
||||
DelegateItemContainer<ItemContainer> delegate = new DelegateItemContainer<>(container);
|
||||
delegate.setGlobalFilter(FilterType.ALLOW_OUTPUT_ONLY);
|
||||
return delegate;
|
||||
}).toArray(ItemContainer[]::new));
|
||||
}
|
||||
|
||||
Map<String, ItemQuantity> materials = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
for (ItemContainer chest : chests) {
|
||||
chest.forEach((i, itemStack) -> {
|
||||
if (CraftingPlugin.isValidUpgradeMaterialForBench(benchState, itemStack) || CraftingPlugin.isValidCraftingMaterialForBench(benchState, itemStack)) {
|
||||
ItemQuantity var10000 = materials.computeIfAbsent(itemStack.getItemId(), k -> new ItemQuantity(itemStack.getItemId(), 0));
|
||||
var10000.quantity = var10000.quantity + itemStack.getQuantity();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
extraResourcesSection.setItemContainer(itemContainer);
|
||||
extraResourcesSection.setExtraMaterials(materials.values().toArray(new ItemQuantity[0]));
|
||||
extraResourcesSection.setValid(true);
|
||||
return chestStates.size();
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected static CraftingManager.ChestLookupResult getContainersAroundBench(@Nonnull BenchState benchState) {
|
||||
List<ItemContainer> containers = new ObjectArrayList<>();
|
||||
List<ItemContainerState> states = new ObjectArrayList<>();
|
||||
List<ItemContainerState> spatialResults = new ObjectArrayList<>();
|
||||
List<ItemContainerState> filteredOut = new ObjectArrayList<>();
|
||||
World world = benchState.getChunk().getWorld();
|
||||
Store<ChunkStore> store = world.getChunkStore().getStore();
|
||||
int limit = world.getGameplayConfig().getCraftingConfig().getBenchMaterialChestLimit();
|
||||
double horizontalRadius = world.getGameplayConfig().getCraftingConfig().getBenchMaterialHorizontalChestSearchRadius();
|
||||
double verticalRadius = world.getGameplayConfig().getCraftingConfig().getBenchMaterialVerticalChestSearchRadius();
|
||||
Vector3d blockPos = benchState.getBlockPosition().toVector3d();
|
||||
BlockBoundingBoxes hitboxAsset = BlockBoundingBoxes.getAssetMap().getAsset(benchState.getBlockType().getHitboxTypeIndex());
|
||||
BlockBoundingBoxes.RotatedVariantBoxes rotatedHitbox = hitboxAsset.get(benchState.getRotationIndex());
|
||||
Box boundingBox = rotatedHitbox.getBoundingBox();
|
||||
double benchWidth = boundingBox.width();
|
||||
double benchHeight = boundingBox.height();
|
||||
double benchDepth = boundingBox.depth();
|
||||
double extraSearchRadius = Math.max(benchWidth, Math.max(benchDepth, benchHeight)) - 1.0;
|
||||
SpatialResource<Ref<ChunkStore>, ChunkStore> blockStateSpatialStructure = store.getResource(BlockStateModule.get().getItemContainerSpatialResourceType());
|
||||
ObjectList<Ref<ChunkStore>> results = SpatialResource.getThreadLocalReferenceList();
|
||||
blockStateSpatialStructure.getSpatialStructure()
|
||||
.ordered3DAxis(blockPos, horizontalRadius + extraSearchRadius, verticalRadius + extraSearchRadius, horizontalRadius + extraSearchRadius, results);
|
||||
if (!results.isEmpty()) {
|
||||
int benchMinBlockX = (int) Math.floor(boundingBox.min.x);
|
||||
int benchMinBlockY = (int) Math.floor(boundingBox.min.y);
|
||||
int benchMinBlockZ = (int) Math.floor(boundingBox.min.z);
|
||||
int benchMaxBlockX = (int) Math.ceil(boundingBox.max.x) - 1;
|
||||
int benchMaxBlockY = (int) Math.ceil(boundingBox.max.y) - 1;
|
||||
int benchMaxBlockZ = (int) Math.ceil(boundingBox.max.z) - 1;
|
||||
double minX = blockPos.x + benchMinBlockX - horizontalRadius;
|
||||
double minY = blockPos.y + benchMinBlockY - verticalRadius;
|
||||
double minZ = blockPos.z + benchMinBlockZ - horizontalRadius;
|
||||
double maxX = blockPos.x + benchMaxBlockX + horizontalRadius;
|
||||
double maxY = blockPos.y + benchMaxBlockY + verticalRadius;
|
||||
double maxZ = blockPos.z + benchMaxBlockZ + horizontalRadius;
|
||||
|
||||
for (Ref<ChunkStore> ref : results) {
|
||||
if (BlockState.getBlockState(ref, ref.getStore()) instanceof ItemContainerState chest) {
|
||||
spatialResults.add(chest);
|
||||
}
|
||||
}
|
||||
|
||||
for (ItemContainerState chest : spatialResults) {
|
||||
Vector3d chestBlockPos = chest.getBlockPosition().toVector3d();
|
||||
if (chestBlockPos.x >= minX
|
||||
&& chestBlockPos.x <= maxX
|
||||
&& chestBlockPos.y >= minY
|
||||
&& chestBlockPos.y <= maxY
|
||||
&& chestBlockPos.z >= minZ
|
||||
&& chestBlockPos.z <= maxZ) {
|
||||
containers.add(chest.getItemContainer());
|
||||
states.add(chest);
|
||||
if (containers.size() >= limit) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
filteredOut.add(chest);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new CraftingManager.ChestLookupResult(containers, states, spatialResults, filteredOut, blockPos);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CraftingManager{queuedCraftingJobs="
|
||||
+ this.queuedCraftingJobs
|
||||
+ ", x="
|
||||
+ this.x
|
||||
+ ", y="
|
||||
+ this.y
|
||||
+ ", z="
|
||||
+ this.z
|
||||
+ ", blockType="
|
||||
+ this.blockType
|
||||
+ "}";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<EntityStore> clone() {
|
||||
return new CraftingManager(this);
|
||||
}
|
||||
|
||||
private static class BenchUpgradingJob {
|
||||
@Nonnull
|
||||
private final BenchWindow window;
|
||||
private final float timeSeconds;
|
||||
private float timeSecondsCompleted;
|
||||
private float lastSentPercent;
|
||||
|
||||
private BenchUpgradingJob(@Nonnull BenchWindow window, float timeSeconds) {
|
||||
this.window = window;
|
||||
this.timeSeconds = timeSeconds;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "BenchUpgradingJob{window=" + this.window + ", timeSeconds=" + this.timeSeconds + "}";
|
||||
}
|
||||
|
||||
public float computeLoadingPercent() {
|
||||
return this.timeSeconds <= 0.0F ? 1.0F : Math.min(this.timeSecondsCompleted / this.timeSeconds, 1.0F);
|
||||
}
|
||||
}
|
||||
|
||||
protected record ChestLookupResult(
|
||||
List<ItemContainer> containers,
|
||||
List<ItemContainerState> states,
|
||||
List<ItemContainerState> spatialResults,
|
||||
List<ItemContainerState> filteredOut,
|
||||
Vector3d benchCenteredPos
|
||||
) {
|
||||
}
|
||||
|
||||
private static class CraftingJob {
|
||||
@Nonnull
|
||||
private final CraftingWindow window;
|
||||
private final int transactionId;
|
||||
@Nonnull
|
||||
private final CraftingRecipe recipe;
|
||||
private final int quantity;
|
||||
private final float timeSeconds;
|
||||
@Nonnull
|
||||
private final ItemContainer inputItemContainer;
|
||||
@Nonnull
|
||||
private final CraftingManager.InputRemovalType inputRemovalType;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<List<ItemStack>> removedItems = new Int2ObjectOpenHashMap<>();
|
||||
private int quantityStarted;
|
||||
private int quantityCompleted;
|
||||
private float timeSecondsCompleted;
|
||||
|
||||
public CraftingJob(
|
||||
@Nonnull CraftingWindow window,
|
||||
int transactionId,
|
||||
@Nonnull CraftingRecipe recipe,
|
||||
int quantity,
|
||||
float timeSeconds,
|
||||
@Nonnull ItemContainer inputItemContainer,
|
||||
@Nonnull CraftingManager.InputRemovalType inputRemovalType
|
||||
) {
|
||||
this.window = window;
|
||||
this.transactionId = transactionId;
|
||||
this.recipe = recipe;
|
||||
this.quantity = quantity;
|
||||
this.timeSeconds = timeSeconds;
|
||||
this.inputItemContainer = inputItemContainer;
|
||||
this.inputRemovalType = inputRemovalType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "CraftingJob{window="
|
||||
+ this.window
|
||||
+ ", transactionId="
|
||||
+ this.transactionId
|
||||
+ ", recipe="
|
||||
+ this.recipe
|
||||
+ ", quantity="
|
||||
+ this.quantity
|
||||
+ ", timeSeconds="
|
||||
+ this.timeSeconds
|
||||
+ ", inputItemContainer="
|
||||
+ this.inputItemContainer
|
||||
+ ", inputRemovalType="
|
||||
+ this.inputRemovalType
|
||||
+ ", removedItems="
|
||||
+ this.removedItems
|
||||
+ ", quantityStarted="
|
||||
+ this.quantityStarted
|
||||
+ ", quantityCompleted="
|
||||
+ this.quantityCompleted
|
||||
+ ", timeSecondsCompleted="
|
||||
+ this.timeSecondsCompleted
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
|
||||
public static enum InputRemovalType {
|
||||
NORMAL,
|
||||
ORDERED;
|
||||
|
||||
private InputRemovalType() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ProcessingBenchState> CODEC = BuilderCodec.builder(ProcessingBenchState.class, ProcessingBenchState::new, BenchState.CODEC)
|
||||
.append(new KeyedCodec<>("InputContainer", ItemContainer.CODEC), (state, o) -> state.inputContainer = o, state -> state.inputContainer)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("FuelContainer", ItemContainer.CODEC), (state, o) -> state.fuelContainer = o, state -> state.fuelContainer)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("OutputContainer", ItemContainer.CODEC), (state, o) -> state.outputContainer = o, state -> state.outputContainer)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("Progress", Codec.DOUBLE), (state, d) -> state.inputProgress = d.floatValue(), state -> (double) state.inputProgress)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("FuelTime", Codec.DOUBLE), (state, d) -> state.fuelTime = d.floatValue(), state -> (double) state.fuelTime)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("Active", Codec.BOOLEAN), (state, b) -> state.active = b, state -> state.active)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("NextExtra", Codec.INTEGER), (state, b) -> state.nextExtra = b, state -> state.nextExtra)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("Marker", WorldMapManager.MarkerReference.CODEC), (state, o) -> state.marker = o, state -> state.marker)
|
||||
.add()
|
||||
.append(new KeyedCodec<>("RecipeId", Codec.STRING), (state, o) -> state.recipeId = o, state -> state.recipeId)
|
||||
.add()
|
||||
.build();
|
||||
private static final float EJECT_VELOCITY = 2.0F;
|
||||
private static final float EJECT_SPREAD_VELOCITY = 1.0F;
|
||||
private static final float EJECT_VERTICAL_VELOCITY = 3.25F;
|
||||
public static final String PROCESSING = "Processing";
|
||||
public static final String PROCESS_COMPLETED = "ProcessCompleted";
|
||||
protected WorldMapManager.MarkerReference marker;
|
||||
private ProcessingBench processingBench;
|
||||
private ItemContainer inputContainer;
|
||||
private ItemContainer fuelContainer;
|
||||
private ItemContainer outputContainer;
|
||||
private CombinedItemContainer combinedItemContainer;
|
||||
private float inputProgress;
|
||||
private float fuelTime;
|
||||
private int lastConsumedFuelTotal;
|
||||
private int nextExtra = -1;
|
||||
private final Set<Short> processingSlots = new HashSet<>();
|
||||
private final Set<Short> processingFuelSlots = new HashSet<>();
|
||||
@Nullable
|
||||
private String recipeId;
|
||||
@Nullable
|
||||
private CraftingRecipe recipe;
|
||||
private boolean active = false;
|
||||
|
||||
public ProcessingBenchState() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean initialize(@Nonnull BlockType blockType) {
|
||||
if (!super.initialize(blockType)) {
|
||||
if (this.bench == null) {
|
||||
List<ItemStack> itemStacks = new ObjectArrayList<>();
|
||||
if (this.inputContainer != null) {
|
||||
itemStacks.addAll(this.inputContainer.dropAllItemStacks());
|
||||
}
|
||||
|
||||
if (this.fuelContainer != null) {
|
||||
itemStacks.addAll(this.fuelContainer.dropAllItemStacks());
|
||||
}
|
||||
|
||||
if (this.outputContainer != null) {
|
||||
itemStacks.addAll(this.outputContainer.dropAllItemStacks());
|
||||
}
|
||||
|
||||
World world = this.getChunk().getWorld();
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(store, itemStacks);
|
||||
if (itemEntityHolders.length > 0) {
|
||||
world.execute(() -> store.addEntities(itemEntityHolders, AddReason.SPAWN));
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
} else if (!(this.bench instanceof ProcessingBench)) {
|
||||
LOGGER.at(Level.SEVERE).log("Wrong bench type for processing. Got %s", this.bench.getClass().getName());
|
||||
return false;
|
||||
} else {
|
||||
this.processingBench = (ProcessingBench) this.bench;
|
||||
if (this.nextExtra == -1) {
|
||||
this.nextExtra = this.processingBench.getExtraOutput() != null ? this.processingBench.getExtraOutput().getPerFuelItemsConsumed() : 0;
|
||||
}
|
||||
|
||||
this.setupSlots();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private void setupSlots() {
|
||||
List<ItemStack> remainder = new ObjectArrayList<>();
|
||||
int tierLevel = this.getTierLevel();
|
||||
ProcessingBench.ProcessingSlot[] input = this.processingBench.getInput(tierLevel);
|
||||
short inputSlotsCount = (short) input.length;
|
||||
this.inputContainer = ItemContainer.ensureContainerCapacity(this.inputContainer, inputSlotsCount, SimpleItemContainer::getNewContainer, remainder);
|
||||
this.inputContainer.registerChangeEvent(EventPriority.LAST, this::onItemChange);
|
||||
|
||||
for (short slot = 0; slot < inputSlotsCount; slot++) {
|
||||
ProcessingBench.ProcessingSlot inputSlot = input[slot];
|
||||
String resourceTypeId = inputSlot.getResourceTypeId();
|
||||
boolean shouldFilterValidIngredients = inputSlot.shouldFilterValidIngredients();
|
||||
if (resourceTypeId != null) {
|
||||
this.inputContainer.setSlotFilter(FilterActionType.ADD, slot, new ResourceFilter(new ResourceQuantity(resourceTypeId, 1)));
|
||||
} else if (shouldFilterValidIngredients) {
|
||||
ObjectArrayList<MaterialQuantity> validIngredients = new ObjectArrayList<>();
|
||||
|
||||
for (CraftingRecipe recipe : CraftingPlugin.getBenchRecipes(this.bench.getType(), this.bench.getId())) {
|
||||
if (!recipe.isRestrictedByBenchTierLevel(this.bench.getId(), tierLevel)) {
|
||||
List<MaterialQuantity> inputMaterials = CraftingManager.getInputMaterials(recipe);
|
||||
validIngredients.addAll(inputMaterials);
|
||||
}
|
||||
}
|
||||
|
||||
this.inputContainer.setSlotFilter(FilterActionType.ADD, slot, (actionType, container, slotIndex, itemStack) -> {
|
||||
if (itemStack == null) {
|
||||
return true;
|
||||
} else {
|
||||
for (MaterialQuantity ingredient : validIngredients) {
|
||||
if (CraftingManager.matches(ingredient, itemStack)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
input = this.processingBench.getFuel();
|
||||
inputSlotsCount = (short) (input != null ? input.length : 0);
|
||||
this.fuelContainer = ItemContainer.ensureContainerCapacity(this.fuelContainer, inputSlotsCount, SimpleItemContainer::getNewContainer, remainder);
|
||||
this.fuelContainer.registerChangeEvent(EventPriority.LAST, this::onItemChange);
|
||||
if (inputSlotsCount > 0) {
|
||||
for (int i = 0; i < input.length; i++) {
|
||||
ProcessingBench.ProcessingSlot fuel = input[i];
|
||||
String resourceTypeId = fuel.getResourceTypeId();
|
||||
if (resourceTypeId != null) {
|
||||
this.fuelContainer.setSlotFilter(FilterActionType.ADD, (short) i, new ResourceFilter(new ResourceQuantity(resourceTypeId, 1)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
short outputSlotsCount = (short) this.processingBench.getOutputSlotsCount(tierLevel);
|
||||
this.outputContainer = ItemContainer.ensureContainerCapacity(this.outputContainer, outputSlotsCount, SimpleItemContainer::getNewContainer, remainder);
|
||||
this.outputContainer.registerChangeEvent(EventPriority.LAST, this::onItemChange);
|
||||
if (outputSlotsCount > 0) {
|
||||
this.outputContainer.setGlobalFilter(FilterType.ALLOW_OUTPUT_ONLY);
|
||||
}
|
||||
|
||||
this.combinedItemContainer = new CombinedItemContainer(this.fuelContainer, this.inputContainer, this.outputContainer);
|
||||
World world = this.getChunk().getWorld();
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(store, remainder);
|
||||
if (itemEntityHolders.length > 0) {
|
||||
world.execute(() -> store.addEntities(itemEntityHolders, AddReason.SPAWN));
|
||||
}
|
||||
|
||||
this.inputContainer.registerChangeEvent(EventPriority.LAST, event -> this.updateRecipe());
|
||||
if (this.processingBench.getFuel() == null) {
|
||||
this.setActive(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(float dt, int index, ArchetypeChunk<ChunkStore> archetypeChunk, @Nonnull Store<ChunkStore> store, CommandBuffer<ChunkStore> commandBuffer) {
|
||||
World world = store.getExternalData().getWorld();
|
||||
Store<EntityStore> entityStore = world.getEntityStore().getStore();
|
||||
BlockType blockType = this.getBlockType();
|
||||
String currentState = BlockAccessor.getCurrentInteractionState(blockType);
|
||||
List<ItemStack> outputItemStacks = null;
|
||||
List<MaterialQuantity> inputMaterials = null;
|
||||
this.processingSlots.clear();
|
||||
this.checkForRecipeUpdate();
|
||||
if (this.recipe != null) {
|
||||
outputItemStacks = CraftingManager.getOutputItemStacks(this.recipe);
|
||||
if (!this.outputContainer.canAddItemStacks(outputItemStacks, false, false)) {
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
}
|
||||
|
||||
this.setActive(false);
|
||||
return;
|
||||
}
|
||||
|
||||
inputMaterials = CraftingManager.getInputMaterials(this.recipe);
|
||||
List<TestRemoveItemSlotResult> result = this.inputContainer.getSlotMaterialsToRemove(inputMaterials, true, true);
|
||||
if (result.isEmpty()) {
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
}
|
||||
|
||||
this.inputProgress = 0.0F;
|
||||
this.setActive(false);
|
||||
this.recipeId = null;
|
||||
this.recipe = null;
|
||||
return;
|
||||
}
|
||||
|
||||
for (TestRemoveItemSlotResult item : result) {
|
||||
this.processingSlots.addAll(item.getPickedSlots());
|
||||
}
|
||||
|
||||
this.sendProcessingSlots();
|
||||
} else {
|
||||
if (this.processingBench.getFuel() == null) {
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
boolean allowNoInputProcessing = this.processingBench.shouldAllowNoInputProcessing();
|
||||
if (!allowNoInputProcessing && "Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getEndSoundEventIndex(), entityStore);
|
||||
this.setActive(false);
|
||||
this.sendProgress(0.0F);
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendProgress(0.0F);
|
||||
if (!allowNoInputProcessing) {
|
||||
this.setActive(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
boolean needsUpdate = false;
|
||||
if (this.fuelTime > 0.0F && this.active) {
|
||||
this.fuelTime -= dt;
|
||||
if (this.fuelTime < 0.0F) {
|
||||
this.fuelTime = 0.0F;
|
||||
}
|
||||
|
||||
needsUpdate = true;
|
||||
}
|
||||
|
||||
ProcessingBench.ProcessingSlot[] fuelSlots = this.processingBench.getFuel();
|
||||
boolean hasFuelSlots = fuelSlots != null && fuelSlots.length > 0;
|
||||
if ((this.processingBench.getMaxFuel() <= 0 || this.fuelTime < this.processingBench.getMaxFuel()) && !this.fuelContainer.isEmpty()) {
|
||||
if (!hasFuelSlots) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.active) {
|
||||
if (this.fuelTime > 0.0F) {
|
||||
for (int i = 0; i < fuelSlots.length; i++) {
|
||||
ItemStack itemInSlot = this.fuelContainer.getItemStack((short) i);
|
||||
if (itemInSlot != null) {
|
||||
this.processingFuelSlots.add((short) i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (this.fuelTime < 0.0F) {
|
||||
this.fuelTime = 0.0F;
|
||||
}
|
||||
|
||||
this.processingFuelSlots.clear();
|
||||
|
||||
for (int ix = 0; ix < fuelSlots.length; ix++) {
|
||||
ProcessingBench.ProcessingSlot fuelSlot = fuelSlots[ix];
|
||||
String resourceTypeId = fuelSlot.getResourceTypeId() != null ? fuelSlot.getResourceTypeId() : "Fuel";
|
||||
ResourceQuantity resourceQuantity = new ResourceQuantity(resourceTypeId, 1);
|
||||
ItemStack slot = this.fuelContainer.getItemStack((short) ix);
|
||||
if (slot != null) {
|
||||
double fuelQuality = slot.getItem().getFuelQuality();
|
||||
ResourceTransaction transaction = this.fuelContainer.removeResource(resourceQuantity, true, true, true);
|
||||
this.processingFuelSlots.add((short) ix);
|
||||
if (transaction.getRemainder() <= 0) {
|
||||
ProcessingBench.ExtraOutput extra = this.processingBench.getExtraOutput();
|
||||
if (extra != null && !extra.isIgnoredFuelSource(slot.getItem())) {
|
||||
this.nextExtra--;
|
||||
if (this.nextExtra <= 0) {
|
||||
this.nextExtra = extra.getPerFuelItemsConsumed();
|
||||
ObjectArrayList<ItemStack> extraItemStacks = new ObjectArrayList<>(extra.getOutputs().length);
|
||||
|
||||
for (MaterialQuantity e : extra.getOutputs()) {
|
||||
extraItemStacks.add(e.toItemStack());
|
||||
}
|
||||
|
||||
ListTransaction<ItemStackTransaction> addTransaction = this.outputContainer.addItemStacks(extraItemStacks, false, false, false);
|
||||
List<ItemStack> remainderItems = new ObjectArrayList<>();
|
||||
|
||||
for (ItemStackTransaction itemStackTransaction : addTransaction.getList()) {
|
||||
ItemStack remainder = itemStackTransaction.getRemainder();
|
||||
if (remainder != null && !remainder.isEmpty()) {
|
||||
remainderItems.add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
if (!remainderItems.isEmpty()) {
|
||||
LOGGER.at(Level.WARNING).log("Dropping excess items at %s", this.getBlockPosition());
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(entityStore, remainderItems);
|
||||
entityStore.addEntities(itemEntityHolders, AddReason.SPAWN);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.fuelTime = (float) (this.fuelTime + transaction.getConsumed() * fuelQuality);
|
||||
needsUpdate = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (needsUpdate) {
|
||||
this.updateFuelValues();
|
||||
}
|
||||
|
||||
if (!hasFuelSlots || this.active && !(this.fuelTime <= 0.0F)) {
|
||||
if (!"Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("Processing", blockType);
|
||||
}
|
||||
|
||||
if (this.recipe != null && (this.fuelTime > 0.0F || this.processingBench.getFuel() == null)) {
|
||||
this.inputProgress += dt;
|
||||
}
|
||||
|
||||
if (this.recipe != null) {
|
||||
float recipeTime = this.recipe.getTimeSeconds();
|
||||
float craftingTimeReductionModifier = this.getCraftingTimeReductionModifier();
|
||||
if (craftingTimeReductionModifier > 0.0F) {
|
||||
recipeTime -= recipeTime * craftingTimeReductionModifier;
|
||||
}
|
||||
|
||||
if (this.inputProgress > recipeTime) {
|
||||
if (recipeTime > 0.0F) {
|
||||
this.inputProgress -= recipeTime;
|
||||
float progressPercent = this.inputProgress / recipeTime;
|
||||
this.sendProgress(progressPercent);
|
||||
} else {
|
||||
this.inputProgress = 0.0F;
|
||||
this.sendProgress(0.0F);
|
||||
}
|
||||
|
||||
LOGGER.at(Level.FINE).log("Do Process for %s %s", this.recipeId, this.recipe);
|
||||
if (inputMaterials != null) {
|
||||
List<ItemStack> remainderItems = new ObjectArrayList<>();
|
||||
int success = 0;
|
||||
IntArrayList slots = new IntArrayList();
|
||||
|
||||
for (int j = 0; j < this.inputContainer.getCapacity(); j++) {
|
||||
slots.add(j);
|
||||
}
|
||||
|
||||
for (MaterialQuantity material : inputMaterials) {
|
||||
for (int ixx = 0; ixx < slots.size(); ixx++) {
|
||||
int slot = slots.getInt(ixx);
|
||||
MaterialSlotTransaction transaction = this.inputContainer.removeMaterialFromSlot((short) slot, material, true, true, true);
|
||||
if (transaction.succeeded()) {
|
||||
success++;
|
||||
slots.removeInt(ixx);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListTransaction<ItemStackTransaction> addTransaction = this.outputContainer.addItemStacks(outputItemStacks, false, false, false);
|
||||
if (!addTransaction.succeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (ItemStackTransaction itemStackTransactionx : addTransaction.getList()) {
|
||||
ItemStack remainder = itemStackTransactionx.getRemainder();
|
||||
if (remainder != null && !remainder.isEmpty()) {
|
||||
remainderItems.add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
if (success == inputMaterials.size()) {
|
||||
this.setBlockInteractionState("ProcessCompleted", blockType);
|
||||
this.playSound(world, this.bench.getCompletedSoundEventIndex(), entityStore);
|
||||
if (!remainderItems.isEmpty()) {
|
||||
LOGGER.at(Level.WARNING).log("Dropping excess items at %s", this.getBlockPosition());
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(entityStore, remainderItems);
|
||||
entityStore.addEntities(itemEntityHolders, AddReason.SPAWN);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
List<ItemStack> remainderItems = new ObjectArrayList<>();
|
||||
ListTransaction<MaterialTransaction> transaction = this.inputContainer.removeMaterials(inputMaterials, true, true, true);
|
||||
if (!transaction.succeeded()) {
|
||||
LOGGER.at(Level.WARNING).log("Failed to remove input materials at %s", this.getBlockPosition());
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
return;
|
||||
}
|
||||
|
||||
this.setBlockInteractionState("ProcessCompleted", blockType);
|
||||
this.playSound(world, this.bench.getCompletedSoundEventIndex(), entityStore);
|
||||
ListTransaction<ItemStackTransaction> addTransactionx = this.outputContainer.addItemStacks(outputItemStacks, false, false, false);
|
||||
if (addTransactionx.succeeded()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.at(Level.WARNING).log("Dropping excess items at %s", this.getBlockPosition());
|
||||
|
||||
for (ItemStackTransaction itemStackTransactionxx : addTransactionx.getList()) {
|
||||
ItemStack remainder = itemStackTransactionxx.getRemainder();
|
||||
if (remainder != null && !remainder.isEmpty()) {
|
||||
remainderItems.add(remainder);
|
||||
}
|
||||
}
|
||||
|
||||
Holder<EntityStore>[] itemEntityHolders = this.ejectItems(entityStore, remainderItems);
|
||||
entityStore.addEntities(itemEntityHolders, AddReason.SPAWN);
|
||||
} else if (this.recipe != null && recipeTime > 0.0F) {
|
||||
float progressPercent = this.inputProgress / recipeTime;
|
||||
this.sendProgress(progressPercent);
|
||||
} else {
|
||||
this.sendProgress(0.0F);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
this.lastConsumedFuelTotal = 0;
|
||||
if ("Processing".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
if (this.processingBench.getFuel() != null) {
|
||||
this.setActive(false);
|
||||
}
|
||||
} else if ("ProcessCompleted".equals(currentState)) {
|
||||
this.setBlockInteractionState("default", blockType);
|
||||
this.playSound(world, this.processingBench.getFailedSoundEventIndex(), entityStore);
|
||||
if (this.processingBench.getFuel() != null) {
|
||||
this.setActive(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private float getCraftingTimeReductionModifier() {
|
||||
BenchTierLevel levelData = this.bench.getTierLevel(this.getTierLevel());
|
||||
return levelData != null ? levelData.getCraftingTimeReductionModifier() : 0.0F;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Holder<EntityStore>[] ejectItems(@Nonnull ComponentAccessor<EntityStore> accessor, @Nonnull List<ItemStack> itemStacks) {
|
||||
if (itemStacks.isEmpty()) {
|
||||
return Holder.emptyArray();
|
||||
} else {
|
||||
RotationTuple rotation = RotationTuple.get(this.getRotationIndex());
|
||||
Vector3d frontDir = new Vector3d(0.0, 0.0, 1.0);
|
||||
rotation.yaw().rotateY(frontDir, frontDir);
|
||||
BlockType blockType = this.getBlockType();
|
||||
Vector3d dropPosition;
|
||||
if (blockType == null) {
|
||||
dropPosition = this.getBlockPosition().toVector3d().add(0.5, 0.0, 0.5);
|
||||
} else {
|
||||
BlockBoundingBoxes hitboxAsset = BlockBoundingBoxes.getAssetMap().getAsset(blockType.getHitboxTypeIndex());
|
||||
if (hitboxAsset == null) {
|
||||
dropPosition = this.getBlockPosition().toVector3d().add(0.5, 0.0, 0.5);
|
||||
} else {
|
||||
double depth = hitboxAsset.get(0).getBoundingBox().depth();
|
||||
double frontOffset = depth / 2.0 + 0.1F;
|
||||
dropPosition = this.getCenteredBlockPosition();
|
||||
dropPosition.add(frontDir.x * frontOffset, 0.0, frontDir.z * frontOffset);
|
||||
}
|
||||
}
|
||||
|
||||
ThreadLocalRandom random = ThreadLocalRandom.current();
|
||||
ObjectArrayList<Holder<EntityStore>> result = new ObjectArrayList<>(itemStacks.size());
|
||||
|
||||
for (ItemStack item : itemStacks) {
|
||||
float velocityX = (float) (frontDir.x * 2.0 + 2.0 * (random.nextDouble() - 0.5));
|
||||
float velocityZ = (float) (frontDir.z * 2.0 + 2.0 * (random.nextDouble() - 0.5));
|
||||
Holder<EntityStore> holder = ItemComponent.generateItemDrop(accessor, item, dropPosition, Vector3f.ZERO, velocityX, 3.25F, velocityZ);
|
||||
if (holder != null) {
|
||||
result.add(holder);
|
||||
}
|
||||
}
|
||||
|
||||
return result.toArray(Holder[]::new);
|
||||
}
|
||||
}
|
||||
|
||||
private void sendProgress(float progress) {
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setProgress(progress));
|
||||
}
|
||||
|
||||
private void sendProcessingSlots() {
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setProcessingSlots(this.processingSlots));
|
||||
}
|
||||
|
||||
private void sendProcessingFuelSlots() {
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setProcessingFuelSlots(this.processingFuelSlots));
|
||||
}
|
||||
|
||||
public boolean isActive() {
|
||||
return this.active;
|
||||
}
|
||||
|
||||
public boolean setActive(boolean active) {
|
||||
if (this.active != active) {
|
||||
if (active && this.processingBench.getFuel() != null && this.fuelContainer.isEmpty()) {
|
||||
return false;
|
||||
} else {
|
||||
this.active = active;
|
||||
if (!active) {
|
||||
this.processingSlots.clear();
|
||||
this.processingFuelSlots.clear();
|
||||
this.sendProcessingSlots();
|
||||
this.sendProcessingFuelSlots();
|
||||
}
|
||||
|
||||
this.updateRecipe();
|
||||
this.windows.forEach((uuid, window) -> ((ProcessingBenchWindow) window).setActive(active));
|
||||
this.markNeedsSave();
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void updateFuelValues() {
|
||||
if (this.fuelTime > this.lastConsumedFuelTotal) {
|
||||
this.lastConsumedFuelTotal = MathUtil.ceil(this.fuelTime);
|
||||
}
|
||||
|
||||
float fuelPercent = this.lastConsumedFuelTotal > 0 ? this.fuelTime / this.lastConsumedFuelTotal : 0.0F;
|
||||
this.windows.forEach((uuid, window) -> {
|
||||
ProcessingBenchWindow processingBenchWindow = (ProcessingBenchWindow) window;
|
||||
processingBenchWindow.setFuelTime(fuelPercent);
|
||||
processingBenchWindow.setMaxFuel(this.lastConsumedFuelTotal);
|
||||
processingBenchWindow.setProcessingFuelSlots(this.processingFuelSlots);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
super.onDestroy();
|
||||
// HyFix: Clear windows map before closeAndRemoveAll to prevent NPE cascade
|
||||
// When a player breaks a bench while another player has it open, the close handlers
|
||||
// try to access block data that is already being destroyed, causing NPE in
|
||||
// BlockType.getDefaultStateKey() and BenchWindow.onClose0()
|
||||
if (this.windows != null) {
|
||||
this.windows.clear();
|
||||
}
|
||||
WindowManager.closeAndRemoveAll(this.windows);
|
||||
if (this.combinedItemContainer != null) {
|
||||
List<ItemStack> itemStacks = this.combinedItemContainer.dropAllItemStacks();
|
||||
this.dropFuelItems(itemStacks);
|
||||
World world = this.getChunk().getWorld();
|
||||
Store<EntityStore> entityStore = world.getEntityStore().getStore();
|
||||
Vector3d dropPosition = this.getBlockPosition().toVector3d().add(0.5, 0.0, 0.5);
|
||||
Holder<EntityStore>[] itemEntityHolders = ItemComponent.generateItemDrops(entityStore, itemStacks, dropPosition, Vector3f.ZERO);
|
||||
if (itemEntityHolders.length > 0) {
|
||||
world.execute(() -> entityStore.addEntities(itemEntityHolders, AddReason.SPAWN));
|
||||
}
|
||||
}
|
||||
|
||||
if (this.marker != null) {
|
||||
this.marker.remove();
|
||||
}
|
||||
}
|
||||
|
||||
public CombinedItemContainer getItemContainer() {
|
||||
return this.combinedItemContainer;
|
||||
}
|
||||
|
||||
private void checkForRecipeUpdate() {
|
||||
if (this.recipe == null && this.recipeId != null) {
|
||||
this.updateRecipe();
|
||||
}
|
||||
}
|
||||
|
||||
private void updateRecipe() {
|
||||
List<CraftingRecipe> recipes = CraftingPlugin.getBenchRecipes(this.bench.getType(), this.bench.getId());
|
||||
if (recipes.isEmpty()) {
|
||||
this.clearRecipe();
|
||||
} else {
|
||||
List<CraftingRecipe> matching = new ObjectArrayList<>();
|
||||
|
||||
for (CraftingRecipe recipe : recipes) {
|
||||
if (!recipe.isRestrictedByBenchTierLevel(this.bench.getId(), this.getTierLevel())) {
|
||||
MaterialQuantity[] input = recipe.getInput();
|
||||
int matches = 0;
|
||||
IntArrayList slots = new IntArrayList();
|
||||
|
||||
for (int j = 0; j < this.inputContainer.getCapacity(); j++) {
|
||||
slots.add(j);
|
||||
}
|
||||
|
||||
for (MaterialQuantity craftingMaterial : input) {
|
||||
String itemId = craftingMaterial.getItemId();
|
||||
String resourceTypeId = craftingMaterial.getResourceTypeId();
|
||||
int materialQuantity = craftingMaterial.getQuantity();
|
||||
BsonDocument metadata = craftingMaterial.getMetadata();
|
||||
MaterialQuantity material = new MaterialQuantity(itemId, resourceTypeId, null, materialQuantity, metadata);
|
||||
|
||||
for (int k = 0; k < slots.size(); k++) {
|
||||
int j = slots.getInt(k);
|
||||
int out = InternalContainerUtilMaterial.testRemoveMaterialFromSlot(this.inputContainer, (short) j, material, material.getQuantity(), true);
|
||||
if (out == 0) {
|
||||
matches++;
|
||||
slots.removeInt(k);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matches == input.length) {
|
||||
matching.add(recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matching.isEmpty()) {
|
||||
this.clearRecipe();
|
||||
} else {
|
||||
matching.sort(Comparator.comparingInt(o -> CraftingManager.getInputMaterials(o).size()));
|
||||
Collections.reverse(matching);
|
||||
if (this.recipeId != null) {
|
||||
for (CraftingRecipe rec : matching) {
|
||||
if (Objects.equals(this.recipeId, rec.getId())) {
|
||||
LOGGER.at(Level.FINE).log("%s - Keeping existing Recipe %s %s", LazyArgs.lazy(this::getBlockPosition), this.recipeId, rec);
|
||||
this.recipe = rec;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CraftingRecipe recipex = matching.getFirst();
|
||||
if (this.recipeId == null || !Objects.equals(this.recipeId, recipex.getId())) {
|
||||
this.inputProgress = 0.0F;
|
||||
this.sendProgress(0.0F);
|
||||
}
|
||||
|
||||
this.recipeId = recipex.getId();
|
||||
this.recipe = recipex;
|
||||
LOGGER.at(Level.FINE).log("%s - Found Recipe %s %s", LazyArgs.lazy(this::getBlockPosition), this.recipeId, this.recipe);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void clearRecipe() {
|
||||
this.recipeId = null;
|
||||
this.recipe = null;
|
||||
this.lastConsumedFuelTotal = 0;
|
||||
this.inputProgress = 0.0F;
|
||||
this.sendProgress(0.0F);
|
||||
LOGGER.at(Level.FINE).log("%s - Cleared Recipe", LazyArgs.lazy(this::getBlockPosition));
|
||||
}
|
||||
|
||||
public void dropFuelItems(@Nonnull List<ItemStack> itemStacks) {
|
||||
String fuelDropItemId = this.processingBench.getFuelDropItemId();
|
||||
if (fuelDropItemId != null) {
|
||||
Item item = Item.getAssetMap().getAsset(fuelDropItemId);
|
||||
int dropAmount = (int) this.fuelTime;
|
||||
this.fuelTime = 0.0F;
|
||||
|
||||
while (dropAmount > 0) {
|
||||
int quantity = Math.min(dropAmount, item.getMaxStack());
|
||||
itemStacks.add(new ItemStack(fuelDropItemId, quantity));
|
||||
dropAmount -= quantity;
|
||||
}
|
||||
} else {
|
||||
LOGGER.at(Level.WARNING).log("No FuelDropItemId defined for %s fuel value of %s will be lost!", this.bench.getId(), this.fuelTime);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CraftingRecipe getRecipe() {
|
||||
return this.recipe;
|
||||
}
|
||||
|
||||
public float getInputProgress() {
|
||||
return this.inputProgress;
|
||||
}
|
||||
|
||||
public void onItemChange(ItemContainer.ItemContainerChangeEvent event) {
|
||||
this.markNeedsSave();
|
||||
}
|
||||
|
||||
public void setBlockInteractionState(@Nonnull String state, @Nonnull BlockType blockType) {
|
||||
this.getChunk().setBlockInteractionState(this.getBlockPosition(), blockType, state);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setMarker(WorldMapManager.MarkerReference marker) {
|
||||
this.marker = marker;
|
||||
this.markNeedsSave();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void placedBy(
|
||||
@Nonnull Ref<EntityStore> playerRef,
|
||||
@Nonnull String blockTypeKey,
|
||||
@Nonnull BlockState blockState,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor
|
||||
) {
|
||||
if (blockTypeKey.equals(this.processingBench.getIconItem()) && this.processingBench.getIcon() != null) {
|
||||
Player playerComponent = componentAccessor.getComponent(playerRef, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
TransformComponent transformComponent = componentAccessor.getComponent(playerRef, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
Transform transformPacket = PositionUtil.toTransformPacket(transformComponent.getTransform());
|
||||
transformPacket.orientation.yaw = 0.0F;
|
||||
transformPacket.orientation.pitch = 0.0F;
|
||||
transformPacket.orientation.roll = 0.0F;
|
||||
MapMarker marker = new MapMarker(
|
||||
this.processingBench.getIconId() + "-" + UUID.randomUUID(),
|
||||
this.processingBench.getIconName(),
|
||||
this.processingBench.getIcon(),
|
||||
transformPacket,
|
||||
null
|
||||
);
|
||||
((MarkerBlockState) blockState).setMarker(WorldMapManager.createPlayerMarker(playerRef, marker, componentAccessor));
|
||||
}
|
||||
}
|
||||
|
||||
private void playSound(@Nonnull World world, int soundEventIndex, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (soundEventIndex != 0) {
|
||||
Vector3i pos = this.getBlockPosition();
|
||||
SoundUtil.playSoundEvent3d(soundEventIndex, SoundCategory.SFX, pos.x + 0.5, pos.y + 0.5, pos.z + 0.5, componentAccessor);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTierLevelChange() {
|
||||
super.onTierLevelChange();
|
||||
this.setupSlots();
|
||||
}
|
||||
}
|
||||
644
src/com/hypixel/hytale/builtin/instances/InstancesPlugin.java
Normal file
644
src/com/hypixel/hytale/builtin/instances/InstancesPlugin.java
Normal file
@@ -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<ChunkStore, InstanceDataResource> instanceDataResourceType;
|
||||
private ComponentType<EntityStore, InstanceEntityConfig> instanceEntityConfigComponentType;
|
||||
private ComponentType<ChunkStore, InstanceBlock> instanceBlockComponentType;
|
||||
private ComponentType<ChunkStore, ConfigurableInstanceBlock> configurableInstanceBlockComponentType;
|
||||
|
||||
public static InstancesPlugin get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public InstancesPlugin(@Nonnull JavaPluginInit init) {
|
||||
super(init);
|
||||
instance = this;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setup() {
|
||||
EventRegistry eventRegistry = this.getEventRegistry();
|
||||
ComponentRegistryProxy<ChunkStore> chunkStoreRegistry = this.getChunkStoreRegistry();
|
||||
this.getCommandRegistry().registerCommand(new InstancesCommand());
|
||||
eventRegistry.register((short) 64, LoadAssetEvent.class, this::validateInstanceAssets);
|
||||
eventRegistry.register(GenerateSchemaEvent.class, InstancesPlugin::generateSchema);
|
||||
eventRegistry.registerGlobal(AddPlayerToWorldEvent.class, InstancesPlugin::onPlayerAddToWorld);
|
||||
eventRegistry.registerGlobal(DrainPlayerFromWorldEvent.class, InstancesPlugin::onPlayerDrainFromWorld);
|
||||
eventRegistry.register(PlayerConnectEvent.class, InstancesPlugin::onPlayerConnect);
|
||||
eventRegistry.registerGlobal(PlayerReadyEvent.class, InstancesPlugin::onPlayerReady);
|
||||
this.instanceBlockComponentType = chunkStoreRegistry.registerComponent(InstanceBlock.class, "Instance", InstanceBlock.CODEC);
|
||||
chunkStoreRegistry.registerSystem(new InstanceBlock.OnRemove());
|
||||
this.configurableInstanceBlockComponentType = chunkStoreRegistry.registerComponent(
|
||||
ConfigurableInstanceBlock.class, "InstanceConfig", ConfigurableInstanceBlock.CODEC
|
||||
);
|
||||
chunkStoreRegistry.registerSystem(new ConfigurableInstanceBlock.OnRemove());
|
||||
this.instanceDataResourceType = chunkStoreRegistry.registerResource(InstanceDataResource.class, "InstanceData", InstanceDataResource.CODEC);
|
||||
chunkStoreRegistry.registerSystem(new RemovalSystem());
|
||||
this.instanceEntityConfigComponentType = this.getEntityStoreRegistry()
|
||||
.registerComponent(InstanceEntityConfig.class, "Instance", InstanceEntityConfig.CODEC);
|
||||
this.getCodecRegistry(RemovalCondition.CODEC)
|
||||
.register("WorldEmpty", WorldEmptyCondition.class, WorldEmptyCondition.CODEC)
|
||||
.register("IdleTimeout", IdleTimeoutCondition.class, IdleTimeoutCondition.CODEC)
|
||||
.register("Timeout", TimeoutCondition.class, TimeoutCondition.CODEC);
|
||||
this.getCodecRegistry(Interaction.CODEC)
|
||||
.register("TeleportInstance", TeleportInstanceInteraction.class, TeleportInstanceInteraction.CODEC)
|
||||
.register("TeleportConfigInstance", TeleportConfigInstanceInteraction.class, TeleportConfigInstanceInteraction.CODEC)
|
||||
.register("ExitInstance", ExitInstanceInteraction.class, ExitInstanceInteraction.CODEC);
|
||||
this.getCodecRegistry(RespawnController.CODEC).register("ExitInstance", ExitInstance.class, ExitInstance.CODEC);
|
||||
OpenCustomUIInteraction.registerBlockEntityCustomPage(
|
||||
this, ConfigureInstanceBlockPage.class, "ConfigInstanceBlock", ConfigureInstanceBlockPage::new, () -> {
|
||||
Holder<ChunkStore> holder = ChunkStore.REGISTRY.newHolder();
|
||||
holder.ensureComponent(ConfigurableInstanceBlock.getComponentType());
|
||||
return holder;
|
||||
}
|
||||
);
|
||||
this.getCodecRegistry(WorldConfig.PLUGIN_CODEC).register(InstanceWorldConfig.class, "Instance", InstanceWorldConfig.CODEC);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<World> spawnInstance(@Nonnull String name, @Nonnull World forWorld, @Nonnull Transform returnPoint) {
|
||||
return this.spawnInstance(name, null, forWorld, returnPoint);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<World> spawnInstance(@Nonnull String name, @Nullable String worldName, @Nonnull World forWorld, @Nonnull Transform returnPoint) {
|
||||
Universe universe = Universe.get();
|
||||
Path path = universe.getPath();
|
||||
Path assetPath = getInstanceAssetPath(name);
|
||||
UUID uuid = UUID.randomUUID();
|
||||
String worldKey = worldName;
|
||||
if (worldName == null) {
|
||||
worldKey = "instance-" + safeName(name) + "-" + uuid;
|
||||
}
|
||||
|
||||
Path worldPath = path.resolve("worlds").resolve(worldKey);
|
||||
String finalWorldKey = worldKey;
|
||||
return WorldConfig.load(assetPath.resolve("instance.bson"))
|
||||
.thenApplyAsync(
|
||||
SneakyThrow.sneakyFunction(
|
||||
config -> {
|
||||
config.setUuid(uuid);
|
||||
config.setDisplayName(WorldConfig.formatDisplayName(name));
|
||||
InstanceWorldConfig instanceConfig = InstanceWorldConfig.ensureAndGet(config);
|
||||
instanceConfig.setReturnPoint(
|
||||
new WorldReturnPoint(forWorld.getWorldConfig().getUuid(), returnPoint, instanceConfig.shouldPreventReconnection())
|
||||
);
|
||||
config.markChanged();
|
||||
long start = System.nanoTime();
|
||||
this.getLogger().at(Level.INFO).log("Copying instance files for %s to world %s", name, finalWorldKey);
|
||||
|
||||
try (Stream<Path> files = Files.walk(assetPath, FileUtil.DEFAULT_WALK_TREE_OPTIONS_ARRAY)) {
|
||||
files.forEach(SneakyThrow.sneakyConsumer(filePath -> {
|
||||
Path rel = assetPath.relativize(filePath);
|
||||
Path toPath = worldPath.resolve(rel.toString());
|
||||
if (Files.isDirectory(filePath)) {
|
||||
Files.createDirectories(toPath);
|
||||
} else {
|
||||
if (Files.isRegularFile(filePath)) {
|
||||
Files.copy(filePath, toPath);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
this.getLogger()
|
||||
.at(Level.INFO)
|
||||
.log("Completed instance files for %s to world %s in %s", name, finalWorldKey, FormatUtil.nanosToString(System.nanoTime() - start));
|
||||
return config;
|
||||
}
|
||||
)
|
||||
)
|
||||
.thenCompose(config -> universe.makeWorld(finalWorldKey, worldPath, config));
|
||||
}
|
||||
|
||||
public static void teleportPlayerToLoadingInstance(
|
||||
@Nonnull Ref<EntityStore> entityRef,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull CompletableFuture<World> worldFuture,
|
||||
@Nullable Transform overrideReturn
|
||||
) {
|
||||
World originalWorld = componentAccessor.getExternalData().getWorld();
|
||||
TransformComponent transformComponent = componentAccessor.getComponent(entityRef, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
Transform originalPosition = transformComponent.getTransform().clone();
|
||||
InstanceEntityConfig instanceEntityConfigComponent = componentAccessor.getComponent(entityRef, InstanceEntityConfig.getComponentType());
|
||||
if (instanceEntityConfigComponent == null) {
|
||||
instanceEntityConfigComponent = componentAccessor.addComponent(entityRef, InstanceEntityConfig.getComponentType());
|
||||
}
|
||||
|
||||
if (overrideReturn != null) {
|
||||
instanceEntityConfigComponent.setReturnPointOverride(new WorldReturnPoint(originalWorld.getWorldConfig().getUuid(), overrideReturn, false));
|
||||
} else {
|
||||
instanceEntityConfigComponent.setReturnPointOverride(null);
|
||||
}
|
||||
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(entityRef, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
UUIDComponent uuidComponent = componentAccessor.getComponent(entityRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID playerUUID = uuidComponent.getUuid();
|
||||
InstanceEntityConfig finalPlayerConfig = instanceEntityConfigComponent;
|
||||
CompletableFuture.runAsync(playerRefComponent::removeFromStore, originalWorld)
|
||||
.thenCombine(worldFuture.orTimeout(1L, TimeUnit.MINUTES), (ignored, world) -> (World) world)
|
||||
.thenCompose(world -> {
|
||||
ISpawnProvider spawnProvider = world.getWorldConfig().getSpawnProvider();
|
||||
Transform spawnPoint = spawnProvider != null ? spawnProvider.getSpawnPoint(world, playerUUID) : null;
|
||||
return world.addPlayer(playerRefComponent, spawnPoint, Boolean.TRUE, Boolean.FALSE);
|
||||
})
|
||||
.whenComplete((ret, ex) -> {
|
||||
if (ex != null) {
|
||||
get().getLogger().at(Level.SEVERE).withCause(ex).log("Failed to send %s to instance world", playerRefComponent.getUsername());
|
||||
finalPlayerConfig.setReturnPointOverride(null);
|
||||
}
|
||||
|
||||
if (ret == null) {
|
||||
if (originalWorld.isAlive()) {
|
||||
originalWorld.addPlayer(playerRefComponent, originalPosition, Boolean.TRUE, Boolean.FALSE);
|
||||
} else {
|
||||
World defaultWorld = Universe.get().getDefaultWorld();
|
||||
if (defaultWorld != null) {
|
||||
defaultWorld.addPlayer(playerRefComponent, null, Boolean.TRUE, Boolean.FALSE);
|
||||
} else {
|
||||
get().getLogger().at(Level.SEVERE).log("No fallback world for %s, disconnecting", playerRefComponent.getUsername());
|
||||
playerRefComponent.getPacketHandler().disconnect("Failed to teleport - no world available");
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public static void teleportPlayerToInstance(
|
||||
@Nonnull Ref<EntityStore> playerRef,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor,
|
||||
@Nonnull World targetWorld,
|
||||
@Nullable Transform overrideReturn
|
||||
) {
|
||||
World originalWorld = componentAccessor.getExternalData().getWorld();
|
||||
WorldConfig originalWorldConfig = originalWorld.getWorldConfig();
|
||||
if (overrideReturn != null) {
|
||||
InstanceEntityConfig instanceConfig = componentAccessor.ensureAndGetComponent(playerRef, InstanceEntityConfig.getComponentType());
|
||||
instanceConfig.setReturnPointOverride(new WorldReturnPoint(originalWorldConfig.getUuid(), overrideReturn, false));
|
||||
}
|
||||
|
||||
UUIDComponent uuidComponent = componentAccessor.getComponent(playerRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID playerUUID = uuidComponent.getUuid();
|
||||
WorldConfig targetWorldConfig = targetWorld.getWorldConfig();
|
||||
ISpawnProvider spawnProvider = targetWorldConfig.getSpawnProvider();
|
||||
if (spawnProvider == null) {
|
||||
throw new IllegalStateException("Spawn provider cannot be null when teleporting player to instance!");
|
||||
} else {
|
||||
Transform spawnTransform = spawnProvider.getSpawnPoint(targetWorld, playerUUID);
|
||||
Teleport teleportComponent = Teleport.createForPlayer(targetWorld, spawnTransform);
|
||||
componentAccessor.addComponent(playerRef, Teleport.getComponentType(), teleportComponent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void exitInstance(@Nonnull Ref<EntityStore> targetRef, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
InstanceEntityConfig entityConfig = componentAccessor.getComponent(targetRef, InstanceEntityConfig.getComponentType());
|
||||
WorldReturnPoint returnPoint = entityConfig != null ? entityConfig.getReturnPoint() : null;
|
||||
if (returnPoint == null) {
|
||||
WorldConfig config = world.getWorldConfig();
|
||||
InstanceWorldConfig instanceConfig = InstanceWorldConfig.get(config);
|
||||
returnPoint = instanceConfig != null ? instanceConfig.getReturnPoint() : null;
|
||||
if (returnPoint == null) {
|
||||
throw new IllegalArgumentException("Player is not in an instance");
|
||||
}
|
||||
}
|
||||
|
||||
Universe universe = Universe.get();
|
||||
World targetWorld = universe.getWorld(returnPoint.getWorld());
|
||||
if (targetWorld == null) {
|
||||
throw new IllegalArgumentException("Missing return world");
|
||||
} else {
|
||||
Teleport teleportComponent = Teleport.createForPlayer(targetWorld, returnPoint.getReturnPoint());
|
||||
componentAccessor.addComponent(targetRef, Teleport.getComponentType(), teleportComponent);
|
||||
}
|
||||
}
|
||||
|
||||
public static void safeRemoveInstance(@Nonnull String worldName) {
|
||||
safeRemoveInstance(Universe.get().getWorld(worldName));
|
||||
}
|
||||
|
||||
public static void safeRemoveInstance(@Nonnull UUID worldUUID) {
|
||||
safeRemoveInstance(Universe.get().getWorld(worldUUID));
|
||||
}
|
||||
|
||||
public static void safeRemoveInstance(@Nullable World instanceWorld) {
|
||||
if (instanceWorld != null) {
|
||||
Store<ChunkStore> chunkStore = instanceWorld.getChunkStore().getStore();
|
||||
chunkStore.getResource(InstanceDataResource.getResourceType()).setHadPlayer(true);
|
||||
WorldConfig config = instanceWorld.getWorldConfig();
|
||||
InstanceWorldConfig instanceConfig = InstanceWorldConfig.get(config);
|
||||
if (instanceConfig != null) {
|
||||
instanceConfig.setRemovalConditions(WorldEmptyCondition.REMOVE_WHEN_EMPTY);
|
||||
}
|
||||
|
||||
config.markChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static Path getInstanceAssetPath(@Nonnull String name) {
|
||||
for (AssetPack pack : AssetModule.get().getAssetPacks()) {
|
||||
Path path = pack.getRoot().resolve("Server").resolve("Instances").resolve(name);
|
||||
if (Files.exists(path)) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
|
||||
return AssetModule.get().getBaseAssetPack().getRoot().resolve("Server").resolve("Instances").resolve(name);
|
||||
}
|
||||
|
||||
public static boolean doesInstanceAssetExist(@Nonnull String name) {
|
||||
return Files.exists(getInstanceAssetPath(name).resolve("instance.bson"));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static CompletableFuture<World> loadInstanceAssetForEdit(@Nonnull String name) {
|
||||
Path path = getInstanceAssetPath(name);
|
||||
Universe universe = Universe.get();
|
||||
return WorldConfig.load(path.resolve("instance.bson")).thenCompose(config -> {
|
||||
config.setUuid(UUID.randomUUID());
|
||||
config.setSavingPlayers(false);
|
||||
config.setIsAllNPCFrozen(true);
|
||||
config.setTicking(false);
|
||||
config.setGameMode(GameMode.Creative);
|
||||
config.setDeleteOnRemove(false);
|
||||
InstanceWorldConfig.ensureAndGet(config).setRemovalConditions(RemovalCondition.EMPTY);
|
||||
config.markChanged();
|
||||
String worldName = "instance-edit-" + safeName(name);
|
||||
return universe.makeWorld(worldName, path, config);
|
||||
});
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<String> getInstanceAssets() {
|
||||
final List<String> instances = new ObjectArrayList<>();
|
||||
|
||||
for (AssetPack pack : AssetModule.get().getAssetPacks()) {
|
||||
final Path path = pack.getRoot().resolve("Server").resolve("Instances");
|
||||
if (Files.isDirectory(path)) {
|
||||
try {
|
||||
Files.walkFileTree(path, FileUtil.DEFAULT_WALK_TREE_OPTIONS_SET, Integer.MAX_VALUE, new SimpleFileVisitor<Path>() {
|
||||
@Nonnull
|
||||
public FileVisitResult preVisitDirectory(@Nonnull Path dir, @Nonnull BasicFileAttributes attrs) {
|
||||
if (Files.exists(dir.resolve("instance.bson"))) {
|
||||
Path relative = path.relativize(dir);
|
||||
String name = relative.toString();
|
||||
instances.add(name);
|
||||
return FileVisitResult.SKIP_SUBTREE;
|
||||
} else {
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (IOException var6) {
|
||||
throw SneakyThrow.sneakyThrow(var6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return instances;
|
||||
}
|
||||
|
||||
private static void onPlayerConnect(@Nonnull PlayerConnectEvent event) {
|
||||
Holder<EntityStore> holder = event.getHolder();
|
||||
Player playerComponent = holder.getComponent(Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerConfigData playerConfig = playerComponent.getPlayerConfigData();
|
||||
InstanceEntityConfig config = InstanceEntityConfig.ensureAndGet(holder);
|
||||
String lastWorldName = playerConfig.getWorld();
|
||||
World lastWorld = Universe.get().getWorld(lastWorldName);
|
||||
WorldReturnPoint fallbackWorld = config.getReturnPoint();
|
||||
if (fallbackWorld != null && (lastWorld == null || fallbackWorld.isReturnOnReconnect())) {
|
||||
lastWorld = Universe.get().getWorld(fallbackWorld.getWorld());
|
||||
if (lastWorld != null) {
|
||||
Transform transform = fallbackWorld.getReturnPoint();
|
||||
TransformComponent transformComponent = holder.ensureAndGetComponent(TransformComponent.getComponentType());
|
||||
transformComponent.setPosition(transform.getPosition());
|
||||
Vector3f rotationClone = transformComponent.getRotation().clone();
|
||||
rotationClone.setYaw(transform.getRotation().getYaw());
|
||||
transformComponent.setRotation(rotationClone);
|
||||
HeadRotation headRotationComponent = holder.ensureAndGetComponent(HeadRotation.getComponentType());
|
||||
headRotationComponent.teleportRotation(transform.getRotation());
|
||||
}
|
||||
} else if (lastWorld != null) {
|
||||
config.setReturnPointOverride(config.getReturnPoint());
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPlayerAddToWorld(@Nonnull AddPlayerToWorldEvent event) {
|
||||
Holder<EntityStore> holder = event.getHolder();
|
||||
InstanceWorldConfig worldConfig = InstanceWorldConfig.get(event.getWorld().getWorldConfig());
|
||||
if (worldConfig == null) {
|
||||
InstanceEntityConfig entityConfig = holder.getComponent(InstanceEntityConfig.getComponentType());
|
||||
if (entityConfig != null && entityConfig.getReturnPoint() != null) {
|
||||
entityConfig.setReturnPoint(null);
|
||||
}
|
||||
} else {
|
||||
InstanceEntityConfig entityConfig = InstanceEntityConfig.ensureAndGet(holder);
|
||||
if (entityConfig.getReturnPointOverride() == null) {
|
||||
entityConfig.setReturnPoint(worldConfig.getReturnPoint());
|
||||
} else {
|
||||
WorldReturnPoint override = entityConfig.getReturnPointOverride();
|
||||
override.setReturnOnReconnect(worldConfig.shouldPreventReconnection());
|
||||
entityConfig.setReturnPoint(override);
|
||||
entityConfig.setReturnPointOverride(null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPlayerReady(@Nonnull PlayerReadyEvent event) {
|
||||
Player player = event.getPlayer();
|
||||
World world = player.getWorld();
|
||||
if (world != null) {
|
||||
WorldConfig worldConfig = world.getWorldConfig();
|
||||
InstanceWorldConfig instanceWorldConfig = InstanceWorldConfig.get(worldConfig);
|
||||
if (instanceWorldConfig != null) {
|
||||
InstanceDiscoveryConfig discoveryConfig = instanceWorldConfig.getDiscovery();
|
||||
if (discoveryConfig != null) {
|
||||
PlayerConfigData playerConfigData = player.getPlayerConfigData();
|
||||
UUID instanceUuid = worldConfig.getUuid();
|
||||
if (discoveryConfig.alwaysDisplay() || !playerConfigData.getDiscoveredInstances().contains(instanceUuid)) {
|
||||
Set<UUID> discoveredInstances = new HashSet<>(playerConfigData.getDiscoveredInstances());
|
||||
discoveredInstances.add(instanceUuid);
|
||||
playerConfigData.setDiscoveredInstances(discoveredInstances);
|
||||
Ref<EntityStore> playerRef = event.getPlayerRef();
|
||||
if (playerRef.isValid()) {
|
||||
world.execute(() -> {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
showInstanceDiscovery(playerRef, store, instanceUuid, discoveryConfig);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void showInstanceDiscovery(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull Store<EntityStore> store, @Nonnull UUID instanceUuid, @Nonnull InstanceDiscoveryConfig discoveryConfig
|
||||
) {
|
||||
DiscoverInstanceEvent.Display discoverInstanceEvent = new DiscoverInstanceEvent.Display(instanceUuid, discoveryConfig.clone());
|
||||
store.invoke(ref, discoverInstanceEvent);
|
||||
discoveryConfig = discoverInstanceEvent.getDiscoveryConfig();
|
||||
if (!discoverInstanceEvent.isCancelled() && discoverInstanceEvent.shouldDisplay()) {
|
||||
PlayerRef playerRefComponent = store.getComponent(ref, PlayerRef.getComponentType());
|
||||
if (playerRefComponent != null) {
|
||||
String subtitleKey = discoveryConfig.getSubtitleKey();
|
||||
Message subtitle = subtitleKey != null ? Message.translation(subtitleKey) : Message.empty();
|
||||
EventTitleUtil.showEventTitleToPlayer(
|
||||
playerRefComponent,
|
||||
Message.translation(discoveryConfig.getTitleKey()),
|
||||
subtitle,
|
||||
discoveryConfig.isMajor(),
|
||||
discoveryConfig.getIcon(),
|
||||
discoveryConfig.getDuration(),
|
||||
discoveryConfig.getFadeInDuration(),
|
||||
discoveryConfig.getFadeOutDuration()
|
||||
);
|
||||
String discoverySoundEventId = discoveryConfig.getDiscoverySoundEventId();
|
||||
if (discoverySoundEventId != null) {
|
||||
int assetIndex = SoundEvent.getAssetMap().getIndex(discoverySoundEventId);
|
||||
if (assetIndex != Integer.MIN_VALUE) {
|
||||
SoundUtil.playSoundEvent2d(ref, assetIndex, SoundCategory.UI, store);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void onPlayerDrainFromWorld(@Nonnull DrainPlayerFromWorldEvent event) {
|
||||
InstanceEntityConfig config = InstanceEntityConfig.removeAndGet(event.getHolder());
|
||||
if (config != null) {
|
||||
WorldReturnPoint returnPoint = config.getReturnPoint();
|
||||
if (returnPoint != null) {
|
||||
World returnWorld = Universe.get().getWorld(returnPoint.getWorld());
|
||||
if (returnWorld != null) {
|
||||
event.setWorld(returnWorld);
|
||||
event.setTransform(returnPoint.getReturnPoint());
|
||||
return;
|
||||
}
|
||||
}
|
||||
// HyFix: Fallback to default world when return world is null or unloaded
|
||||
// Prevents "Missing return world" crash that kicks players from the server
|
||||
World defaultWorld = Universe.get().getDefaultWorld();
|
||||
if (defaultWorld != null) {
|
||||
event.setWorld(defaultWorld);
|
||||
// Use default spawn point if available
|
||||
if (defaultWorld.getWorldConfig().getSpawnProvider() != null) {
|
||||
event.setTransform(defaultWorld.getWorldConfig().getSpawnProvider().getSpawnPoint(defaultWorld, null));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void generateSchema(@Nonnull GenerateSchemaEvent event) {
|
||||
ObjectSchema worldConfig = WorldConfig.CODEC.toSchema(event.getContext());
|
||||
Map<String, Schema> props = worldConfig.getProperties();
|
||||
props.put("UUID", Schema.anyOf(new StringSchema(), new ObjectSchema()));
|
||||
worldConfig.setTitle("Instance Configuration");
|
||||
worldConfig.setId("InstanceConfig.json");
|
||||
Schema.HytaleMetadata hytaleMetadata = worldConfig.getHytale();
|
||||
if (hytaleMetadata != null) {
|
||||
hytaleMetadata.setPath("Instances");
|
||||
hytaleMetadata.setExtension("instance.bson");
|
||||
hytaleMetadata.setUiEditorIgnore(Boolean.TRUE);
|
||||
}
|
||||
|
||||
event.addSchema("InstanceConfig.json", worldConfig);
|
||||
event.addSchemaLink("InstanceConfig", List.of("Instances/**/instance.bson"), ".bson");
|
||||
}
|
||||
|
||||
private void validateInstanceAssets(@Nonnull LoadAssetEvent event) {
|
||||
Path path = AssetModule.get().getBaseAssetPack().getRoot().resolve("Server").resolve("Instances");
|
||||
if (Options.getOptionSet().has(Options.VALIDATE_ASSETS) && Files.isDirectory(path) && !event.isShouldShutdown()) {
|
||||
StringBuilder errors = new StringBuilder();
|
||||
|
||||
for (String name : this.getInstanceAssets()) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
Path instancePath = getInstanceAssetPath(name);
|
||||
Universe universe = Universe.get();
|
||||
WorldConfig config = WorldConfig.load(instancePath.resolve("instance.bson")).join();
|
||||
IChunkStorageProvider storage = config.getChunkStorageProvider();
|
||||
config.setChunkStorageProvider(new MigrationChunkStorageProvider(new IChunkStorageProvider[]{storage}, EmptyChunkStorageProvider.INSTANCE));
|
||||
config.setResourceStorageProvider(EmptyResourceStorageProvider.INSTANCE);
|
||||
config.setUuid(UUID.randomUUID());
|
||||
config.setSavingPlayers(false);
|
||||
config.setIsAllNPCFrozen(true);
|
||||
config.setSavingConfig(false);
|
||||
config.setTicking(false);
|
||||
config.setGameMode(GameMode.Creative);
|
||||
config.setDeleteOnRemove(false);
|
||||
config.setCompassUpdating(false);
|
||||
InstanceWorldConfig.ensureAndGet(config).setRemovalConditions(RemovalCondition.EMPTY);
|
||||
config.markChanged();
|
||||
String worldName = "instance-validate-" + safeName(name);
|
||||
|
||||
try {
|
||||
World world = universe.makeWorld(worldName, instancePath, config, false).join();
|
||||
EnumSet<ValidationOption> options = EnumSet.of(ValidationOption.BLOCK_STATES, ValidationOption.BLOCKS);
|
||||
world.validate(sb, WorldValidationUtil.blockValidator(errors, options), options);
|
||||
} catch (Exception var18) {
|
||||
sb.append("\t").append(var18.getMessage());
|
||||
this.getLogger().at(Level.SEVERE).withCause(var18).log("Failed to validate: " + name);
|
||||
} finally {
|
||||
if (!sb.isEmpty()) {
|
||||
errors.append("Instance: ").append(name).append('\n').append((CharSequence) sb).append('\n');
|
||||
}
|
||||
}
|
||||
|
||||
if (universe.getWorld(worldName) != null) {
|
||||
universe.removeWorld(worldName);
|
||||
}
|
||||
}
|
||||
|
||||
if (!errors.isEmpty()) {
|
||||
this.getLogger().at(Level.SEVERE).log("Failed to validate instances:\n" + errors);
|
||||
event.failed(true, "failed to validate instances");
|
||||
}
|
||||
|
||||
HytaleLogger.getLogger()
|
||||
.at(Level.INFO)
|
||||
.log("Loading Instance assets phase completed! Boot time %s", FormatUtil.nanosToString(System.nanoTime() - event.getBootStart()));
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static String safeName(@Nonnull String name) {
|
||||
return name.replace('/', '-');
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ResourceType<ChunkStore, InstanceDataResource> getInstanceDataResourceType() {
|
||||
return this.instanceDataResourceType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<EntityStore, InstanceEntityConfig> getInstanceEntityConfigComponentType() {
|
||||
return this.instanceEntityConfigComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<ChunkStore, InstanceBlock> getInstanceBlockComponentType() {
|
||||
return this.instanceBlockComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<ChunkStore, ConfigurableInstanceBlock> getConfigurableInstanceBlockComponentType() {
|
||||
return this.configurableInstanceBlockComponentType;
|
||||
}
|
||||
}
|
||||
356
src/com/hypixel/hytale/component/ArchetypeChunk.java
Normal file
356
src/com/hypixel/hytale/component/ArchetypeChunk.java
Normal file
@@ -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<ECS_TYPE> {
|
||||
@Nonnull
|
||||
private static final ArchetypeChunk[] EMPTY_ARRAY = new ArchetypeChunk[0];
|
||||
@Nonnull
|
||||
protected final Store<ECS_TYPE> store;
|
||||
@Nonnull
|
||||
protected final Archetype<ECS_TYPE> archetype;
|
||||
protected int entitiesSize;
|
||||
@Nonnull
|
||||
protected Ref<ECS_TYPE>[] refs = new Ref[16];
|
||||
protected Component<ECS_TYPE>[][] components;
|
||||
|
||||
public static <ECS_TYPE> ArchetypeChunk<ECS_TYPE>[] emptyArray() {
|
||||
return EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
public ArchetypeChunk(@Nonnull Store<ECS_TYPE> store, @Nonnull Archetype<ECS_TYPE> archetype) {
|
||||
this.store = store;
|
||||
this.archetype = archetype;
|
||||
this.components = new Component[archetype.length()][];
|
||||
|
||||
for (int i = archetype.getMinIndex(); i < archetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) archetype.get(i);
|
||||
if (componentType != null) {
|
||||
this.components[componentType.getIndex()] = new Component[16];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Archetype<ECS_TYPE> getArchetype() {
|
||||
return this.archetype;
|
||||
}
|
||||
|
||||
public int size() {
|
||||
return this.entitiesSize;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Ref<ECS_TYPE> getReferenceTo(int index) {
|
||||
if (index >= 0 && index < this.entitiesSize) {
|
||||
return this.refs[index];
|
||||
} else {
|
||||
throw new IndexOutOfBoundsException(index);
|
||||
}
|
||||
}
|
||||
|
||||
public <T extends Component<ECS_TYPE>> void setComponent(int index, @Nonnull ComponentType<ECS_TYPE, T> 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 extends Component<ECS_TYPE>> T getComponent(int index, @Nonnull ComponentType<ECS_TYPE, T> componentType) {
|
||||
// HyFix #20: Handle stale entity references gracefully
|
||||
try {
|
||||
componentType.validateRegistry(this.store.getRegistry());
|
||||
if (index < 0 || index >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(index);
|
||||
} else {
|
||||
return (T) (!this.archetype.contains(componentType) ? null : this.components[componentType.getIndex()][index]);
|
||||
}
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
System.out.println("[HyFix] WARNING: getComponent() IndexOutOfBounds - returning null (stale entity ref)");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public int addEntity(@Nonnull Ref<ECS_TYPE> ref, @Nonnull Holder<ECS_TYPE> 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<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) 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<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) this.archetype
|
||||
.get(ix);
|
||||
if (componentType != null) {
|
||||
this.components[componentType.getIndex()][entityIndex] = holder.getComponent((ComponentType<ECS_TYPE, Component<ECS_TYPE>>) componentType);
|
||||
}
|
||||
}
|
||||
|
||||
return entityIndex;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Holder<ECS_TYPE> copyEntity(int entityIndex, @Nonnull Holder<ECS_TYPE> target) {
|
||||
if (entityIndex >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(entityIndex);
|
||||
} else {
|
||||
Component<ECS_TYPE>[] entityComponents = target.ensureComponentsSize(this.archetype.length());
|
||||
|
||||
for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) this.archetype
|
||||
.get(i);
|
||||
if (componentType != null) {
|
||||
int componentTypeIndex = componentType.getIndex();
|
||||
Component<ECS_TYPE> component = this.components[componentTypeIndex][entityIndex];
|
||||
entityComponents[componentTypeIndex] = component.clone();
|
||||
}
|
||||
}
|
||||
|
||||
target.init(this.archetype, entityComponents);
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Holder<ECS_TYPE> copySerializableEntity(@Nonnull ComponentRegistry.Data<ECS_TYPE> data, int entityIndex, @Nonnull Holder<ECS_TYPE> target) {
|
||||
// HyFix #29: Handle IndexOutOfBoundsException during chunk saving gracefully
|
||||
try {
|
||||
if (entityIndex >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(entityIndex);
|
||||
} else {
|
||||
Archetype<ECS_TYPE> serializableArchetype = this.archetype.getSerializableArchetype(data);
|
||||
Component<ECS_TYPE>[] entityComponents = target.ensureComponentsSize(serializableArchetype.length());
|
||||
|
||||
for (int i = serializableArchetype.getMinIndex(); i < serializableArchetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) serializableArchetype.get(
|
||||
i
|
||||
);
|
||||
if (componentType != null) {
|
||||
int componentTypeIndex = componentType.getIndex();
|
||||
Component<ECS_TYPE> component = this.components[componentTypeIndex][entityIndex];
|
||||
entityComponents[componentTypeIndex] = component.cloneSerializable();
|
||||
}
|
||||
}
|
||||
|
||||
target.init(serializableArchetype, entityComponents);
|
||||
return target;
|
||||
}
|
||||
} catch (IndexOutOfBoundsException e) {
|
||||
System.out.println("[HyFix] WARNING: copySerializableEntity() IndexOutOfBounds - skipping (stale entity ref)");
|
||||
return target;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Holder<ECS_TYPE> removeEntity(int entityIndex, @Nonnull Holder<ECS_TYPE> target) {
|
||||
if (entityIndex >= this.entitiesSize) {
|
||||
throw new IndexOutOfBoundsException(entityIndex);
|
||||
} else {
|
||||
Component<ECS_TYPE>[] entityComponents = target.ensureComponentsSize(this.archetype.length());
|
||||
|
||||
for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) 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<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) 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<ECS_TYPE> tempInternalEntityHolder,
|
||||
@Nonnull ArchetypeChunk<ECS_TYPE> chunk,
|
||||
@Nonnull Consumer<Holder<ECS_TYPE>> modification,
|
||||
@Nonnull IntObjectConsumer<Ref<ECS_TYPE>> referenceConsumer
|
||||
) {
|
||||
Component<ECS_TYPE>[] entityComponents = new Component[this.archetype.length()];
|
||||
|
||||
for (int entityIndex = 0; entityIndex < this.entitiesSize; entityIndex++) {
|
||||
Ref<ECS_TYPE> ref = this.refs[entityIndex];
|
||||
this.refs[entityIndex] = null;
|
||||
|
||||
for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) 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<ECS_TYPE> tempInternalEntityHolder,
|
||||
@Nonnull ArchetypeChunk<ECS_TYPE> chunk,
|
||||
@Nonnull IntPredicate shouldTransfer,
|
||||
@Nonnull Consumer<Holder<ECS_TYPE>> modification,
|
||||
@Nonnull IntObjectConsumer<Ref<ECS_TYPE>> referenceConsumer
|
||||
) {
|
||||
int firstTransfered = Integer.MIN_VALUE;
|
||||
int lastTransfered = Integer.MIN_VALUE;
|
||||
Component<ECS_TYPE>[] 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<ECS_TYPE> ref = this.refs[entityIndex];
|
||||
this.refs[entityIndex] = null;
|
||||
|
||||
for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) 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<ECS_TYPE> ref = this.refs[lastIndex];
|
||||
this.store.setEntityChunkIndex(ref, entityIndex);
|
||||
|
||||
for (int i = this.archetype.getMinIndex(); i < this.archetype.length(); i++) {
|
||||
ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) this.archetype.get(i);
|
||||
if (componentType != null) {
|
||||
Component<ECS_TYPE>[] 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<ECS_TYPE, ? extends Component<ECS_TYPE>> componentType = (ComponentType<ECS_TYPE, ? extends Component<ECS_TYPE>>) 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)
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
375
src/com/hypixel/hytale/component/CommandBuffer.java
Normal file
375
src/com/hypixel/hytale/component/CommandBuffer.java
Normal file
@@ -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<ECS_TYPE> implements ComponentAccessor<ECS_TYPE> {
|
||||
@Nonnull
|
||||
private final Store<ECS_TYPE> store;
|
||||
@Nonnull
|
||||
private final Deque<Consumer<Store<ECS_TYPE>>> queue = new ArrayDeque<>();
|
||||
@Nullable
|
||||
private Ref<ECS_TYPE> trackedRef;
|
||||
private boolean trackedRefRemoved;
|
||||
@Nullable
|
||||
private CommandBuffer<ECS_TYPE> parentBuffer;
|
||||
@Nullable
|
||||
private Thread thread;
|
||||
|
||||
protected CommandBuffer(@Nonnull Store<ECS_TYPE> store) {
|
||||
this.store = store;
|
||||
|
||||
assert this.setThread();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Store<ECS_TYPE> getStore() {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
public void run(@Nonnull Consumer<Store<ECS_TYPE>> consumer) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.queue.add(consumer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Component<ECS_TYPE>> T getComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> componentType) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
return this.store.__internal_getComponent(ref, componentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Archetype<ECS_TYPE> getArchetype(@Nonnull Ref<ECS_TYPE> ref) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
return this.store.__internal_getArchetype(ref);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public <T extends Resource<ECS_TYPE>> T getResource(@Nonnull ResourceType<ECS_TYPE, T> 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<ECS_TYPE>[] addEntities(@Nonnull Holder<ECS_TYPE>[] holders, @Nonnull AddReason reason) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
Ref<ECS_TYPE>[] 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<ECS_TYPE> addEntity(@Nonnull Holder<ECS_TYPE> holder, @Nonnull AddReason reason) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
Ref<ECS_TYPE> ref = new Ref<>(this.store);
|
||||
this.queue.add(chunk -> chunk.addEntity(holder, ref, reason));
|
||||
return ref;
|
||||
}
|
||||
|
||||
public void addEntities(
|
||||
@Nonnull Holder<ECS_TYPE>[] holders, int holderStart, @Nonnull Ref<ECS_TYPE>[] 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<ECS_TYPE> addEntity(@Nonnull Holder<ECS_TYPE> holder, @Nonnull Ref<ECS_TYPE> 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<ECS_TYPE> copyEntity(@Nonnull Ref<ECS_TYPE> ref, @Nonnull Holder<ECS_TYPE> target) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.queue.add(chunk -> chunk.copyEntity(ref, target));
|
||||
return target;
|
||||
}
|
||||
|
||||
public void tryRemoveEntity(@Nonnull Ref<ECS_TYPE> 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<ECS_TYPE> 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<ECS_TYPE> removeEntity(@Nonnull Ref<ECS_TYPE> ref, @Nonnull Holder<ECS_TYPE> 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 <T extends Component<ECS_TYPE>> void ensureComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> componentType) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.queue.add(chunk -> {
|
||||
if (ref.isValid()) {
|
||||
chunk.ensureComponent(ref, componentType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public <T extends Component<ECS_TYPE>> T ensureAndGetComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> 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 extends Component<ECS_TYPE>> T addComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> 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 <T extends Component<ECS_TYPE>> void addComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> componentType, @Nonnull T component) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.queue.add(chunk -> {
|
||||
if (ref.isValid()) {
|
||||
chunk.addComponent(ref, componentType, component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public <T extends Component<ECS_TYPE>> void replaceComponent(
|
||||
@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> componentType, @Nonnull T component
|
||||
) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.queue.add(chunk -> {
|
||||
if (ref.isValid()) {
|
||||
chunk.replaceComponent(ref, componentType, component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Component<ECS_TYPE>> void removeComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> 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 <T extends Component<ECS_TYPE>> void tryRemoveComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> componentType) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.queue.add(chunk -> {
|
||||
if (ref.isValid()) {
|
||||
chunk.tryRemoveComponent(ref, componentType);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T extends Component<ECS_TYPE>> void putComponent(@Nonnull Ref<ECS_TYPE> ref, @Nonnull ComponentType<ECS_TYPE, T> componentType, @Nonnull T component) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.queue.add(chunk -> {
|
||||
if (ref.isValid()) {
|
||||
chunk.putComponent(ref, componentType, component);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Event extends EcsEvent> void invoke(@Nonnull Ref<ECS_TYPE> ref, @Nonnull Event param) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.store.internal_invoke(this, ref, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Event extends EcsEvent> void invoke(@Nonnull EntityEventType<ECS_TYPE, Event> systemType, @Nonnull Ref<ECS_TYPE> ref, @Nonnull Event param) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.store.internal_invoke(this, systemType, ref, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Event extends EcsEvent> void invoke(@Nonnull Event param) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.store.internal_invoke(this, param);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <Event extends EcsEvent> void invoke(@Nonnull WorldEventType<ECS_TYPE, Event> systemType, @Nonnull Event param) {
|
||||
assert Thread.currentThread() == this.thread;
|
||||
|
||||
this.store.internal_invoke(this, systemType, param);
|
||||
}
|
||||
|
||||
void track(@Nonnull Ref<ECS_TYPE> ref) {
|
||||
this.trackedRef = ref;
|
||||
}
|
||||
|
||||
private void testRemovedTracked(@Nonnull Ref<ECS_TYPE> 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<ECS_TYPE> fork() {
|
||||
CommandBuffer<ECS_TYPE> forkedBuffer = this.store.takeCommandBuffer();
|
||||
forkedBuffer.parentBuffer = this;
|
||||
return forkedBuffer;
|
||||
}
|
||||
|
||||
public void mergeParallel(@Nonnull CommandBuffer<ECS_TYPE> 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!");
|
||||
}
|
||||
}
|
||||
}
|
||||
107
src/com/hypixel/hytale/component/ComponentType.java
Normal file
107
src/com/hypixel/hytale/component/ComponentType.java
Normal file
@@ -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<ECS_TYPE, T extends Component<ECS_TYPE>> implements Comparable<ComponentType<ECS_TYPE, ?>>, Query<ECS_TYPE> {
|
||||
@Nonnull
|
||||
public static final ComponentType[] EMPTY_ARRAY = new ComponentType[0];
|
||||
private ComponentRegistry<ECS_TYPE> registry;
|
||||
private Class<? super T> tClass;
|
||||
private int index;
|
||||
private boolean invalid = true;
|
||||
|
||||
public ComponentType() {
|
||||
}
|
||||
|
||||
void init(@Nonnull ComponentRegistry<ECS_TYPE> registry, @Nonnull Class<? super T> tClass, int index) {
|
||||
this.registry = registry;
|
||||
this.tClass = tClass;
|
||||
this.index = index;
|
||||
this.invalid = false;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentRegistry<ECS_TYPE> getRegistry() {
|
||||
return this.registry;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Class<? super T> 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<ECS_TYPE> archetype) {
|
||||
return archetype.contains(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requiresComponentType(ComponentType<ECS_TYPE, ?> componentType) {
|
||||
return this.equals(componentType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void validateRegistry(@Nonnull ComponentRegistry<ECS_TYPE> 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<ECS_TYPE, ?> 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
|
||||
+ "}";
|
||||
}
|
||||
}
|
||||
@@ -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<ECS_TYPE> extends ArchetypeTickingSystem<ECS_TYPE> {
|
||||
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<ECS_TYPE> archetypeChunk, @Nonnull Store<ECS_TYPE> store, @Nonnull CommandBuffer<ECS_TYPE> commandBuffer) {
|
||||
doTick(this, dt, archetypeChunk, store, commandBuffer);
|
||||
}
|
||||
|
||||
public abstract void tick(float var1, int var2, @Nonnull ArchetypeChunk<ECS_TYPE> var3, @Nonnull Store<ECS_TYPE> var4, @Nonnull CommandBuffer<ECS_TYPE> var5);
|
||||
|
||||
static long lastError = 0;
|
||||
|
||||
public static <ECS_TYPE> void doTick(
|
||||
@Nonnull EntityTickingSystem<ECS_TYPE> system,
|
||||
float dt,
|
||||
@Nonnull ArchetypeChunk<ECS_TYPE> archetypeChunk,
|
||||
@Nonnull Store<ECS_TYPE> store,
|
||||
@Nonnull CommandBuffer<ECS_TYPE> commandBuffer
|
||||
) {
|
||||
try {
|
||||
int archetypeChunkSize = archetypeChunk.size();
|
||||
if (archetypeChunkSize != 0) {
|
||||
ParallelTask<EntityTickingSystem.SystemTaskData<ECS_TYPE>> task = store.getParallelTask();
|
||||
if (system.isParallel(archetypeChunkSize, task.size())) {
|
||||
ParallelRangeTask<EntityTickingSystem.SystemTaskData<ECS_TYPE>> 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<ECS_TYPE> implements IntConsumer {
|
||||
@Nullable
|
||||
private EntityTickingSystem<ECS_TYPE> system;
|
||||
private float dt;
|
||||
@Nullable
|
||||
private ArchetypeChunk<ECS_TYPE> archetypeChunk;
|
||||
@Nullable
|
||||
private Store<ECS_TYPE> store;
|
||||
@Nullable
|
||||
private CommandBuffer<ECS_TYPE> commandBuffer;
|
||||
|
||||
public SystemTaskData() {
|
||||
}
|
||||
|
||||
public void init(
|
||||
EntityTickingSystem<ECS_TYPE> system, float dt, ArchetypeChunk<ECS_TYPE> archetypeChunk, Store<ECS_TYPE> store, CommandBuffer<ECS_TYPE> 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 <ECS_TYPE> void invokeParallelTask(
|
||||
@Nonnull ParallelTask<EntityTickingSystem.SystemTaskData<ECS_TYPE>> parallelTask, @Nonnull CommandBuffer<ECS_TYPE> commandBuffer
|
||||
) {
|
||||
int parallelTaskSize = parallelTask.size();
|
||||
if (parallelTaskSize > 0) {
|
||||
parallelTask.doInvoke();
|
||||
|
||||
for (int x = 0; x < parallelTaskSize; x++) {
|
||||
ParallelRangeTask<EntityTickingSystem.SystemTaskData<ECS_TYPE>> systemTask = parallelTask.get(x);
|
||||
int i = 0;
|
||||
|
||||
for (int systemTaskSize = systemTask.size(); i < systemTaskSize; i++) {
|
||||
EntityTickingSystem.SystemTaskData<ECS_TYPE> taskData = systemTask.get(i);
|
||||
taskData.commandBuffer.mergeParallel(commandBuffer);
|
||||
taskData.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
517
src/com/hypixel/hytale/math/shape/Box.java
Normal file
517
src/com/hypixel/hytale/math/shape/Box.java
Normal file
@@ -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<Box> 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 <T> boolean forEachBlock(double x, double y, double z, double epsilon, T t, @Nonnull TriIntObjPredicate<T> 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 + "}";
|
||||
}
|
||||
}
|
||||
203
src/com/hypixel/hytale/math/shape/Box2D.java
Normal file
203
src/com/hypixel/hytale/math/shape/Box2D.java
Normal file
@@ -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<Box2D> 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 + "}";
|
||||
}
|
||||
}
|
||||
544
src/com/hypixel/hytale/server/core/HytaleServer.java
Normal file
544
src/com/hypixel/hytale/server/core/HytaleServer.java
Normal file
@@ -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<HytaleServer> METRICS_REGISTRY = new MetricsRegistry<HytaleServer>()
|
||||
.register("Time", server -> Instant.now(), Codec.INSTANT)
|
||||
.register("Boot", server -> server.boot, Codec.INSTANT)
|
||||
.register("BootStart", server -> server.bootStart, Codec.LONG)
|
||||
.register("Booting", server -> server.booting.get(), Codec.BOOLEAN)
|
||||
.register("ShutdownReason", server -> {
|
||||
ShutdownReason reason = server.shutdown.get();
|
||||
return reason == null ? null : reason.toString();
|
||||
}, Codec.STRING)
|
||||
.register("PluginManager", HytaleServer::getPluginManager, PluginManager.METRICS_REGISTRY)
|
||||
.register("Config", HytaleServer::getConfig, HytaleServerConfig.CODEC)
|
||||
.register("JVM", JVMMetrics.METRICS_REGISTRY);
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
private static HytaleServer instance;
|
||||
private final Semaphore aliveLock = new Semaphore(0);
|
||||
private final AtomicBoolean booting = new AtomicBoolean(false);
|
||||
private final AtomicBoolean booted = new AtomicBoolean(false);
|
||||
private final AtomicReference<ShutdownReason> shutdown = new AtomicReference<>();
|
||||
private final EventBus eventBus = new EventBus(Options.getOptionSet().has(Options.EVENT_DEBUG));
|
||||
private final PluginManager pluginManager = new PluginManager();
|
||||
private final CommandManager commandManager = new CommandManager();
|
||||
@Nonnull
|
||||
private final HytaleServerConfig hytaleServerConfig;
|
||||
private final Instant boot;
|
||||
private final long bootStart;
|
||||
private int pluginsProgress;
|
||||
|
||||
public HytaleServer() throws IOException {
|
||||
instance = this;
|
||||
Quic.ensureAvailability();
|
||||
HytaleLoggerBackend.setIndent(25);
|
||||
ThreadUtil.forceTimeHighResolution();
|
||||
ThreadUtil.createKeepAliveThread(this.aliveLock);
|
||||
this.boot = Instant.now();
|
||||
this.bootStart = System.nanoTime();
|
||||
LOGGER.at(Level.INFO).log("Starting HytaleServer");
|
||||
Constants.init();
|
||||
DataStoreProvider.CODEC.register("Disk", DiskDataStoreProvider.class, DiskDataStoreProvider.CODEC);
|
||||
LOGGER.at(Level.INFO).log("Loading config...");
|
||||
this.hytaleServerConfig = HytaleServerConfig.load();
|
||||
HytaleLoggerBackend.reloadLogLevels();
|
||||
System.setProperty("java.util.concurrent.ForkJoinPool.common.threadFactory", HytaleForkJoinThreadFactory.class.getName());
|
||||
OptionSet optionSet = Options.getOptionSet();
|
||||
LOGGER.at(Level.INFO).log("Authentication mode: %s", optionSet.valueOf(Options.AUTH_MODE));
|
||||
ServerAuthManager.getInstance().initialize();
|
||||
if (EarlyPluginLoader.hasTransformers()) {
|
||||
HytaleLogger.getLogger().at(Level.INFO).log("Early plugins loaded!! Disabling Sentry!!");
|
||||
} else if (!optionSet.has(Options.DISABLE_SENTRY)) {
|
||||
LOGGER.at(Level.INFO).log("Enabling Sentry");
|
||||
SentryOptions options = new SentryOptions();
|
||||
options.setDsn("https://6043a13c7b5c45b5c834b6d896fb378e@sentry.hytale.com/4");
|
||||
options.setRelease(ManifestUtil.getImplementationVersion());
|
||||
options.setDist(ManifestUtil.getImplementationRevisionId());
|
||||
options.setEnvironment("release");
|
||||
options.setTag("patchline", ManifestUtil.getPatchline());
|
||||
options.setServerName(NetworkUtil.getHostName());
|
||||
options.setBeforeSend((event, hint) -> {
|
||||
Throwable throwable = event.getThrowable();
|
||||
if (PluginClassLoader.isFromThirdPartyPlugin(throwable)) {
|
||||
return null;
|
||||
} else {
|
||||
Contexts contexts = event.getContexts();
|
||||
HashMap<String, Object> serverContext = new HashMap<>();
|
||||
serverContext.put("name", this.getServerName());
|
||||
serverContext.put("max-players", this.getConfig().getMaxPlayers());
|
||||
ServerManager serverManager = ServerManager.get();
|
||||
if (serverManager != null) {
|
||||
serverContext.put("listeners", serverManager.getListeners().stream().map(Object::toString).toList());
|
||||
}
|
||||
|
||||
contexts.put("server", serverContext);
|
||||
Universe universe = Universe.get();
|
||||
if (universe != null) {
|
||||
HashMap<String, Object> universeContext = new HashMap<>();
|
||||
universeContext.put("path", universe.getPath().toString());
|
||||
universeContext.put("player-count", universe.getPlayerCount());
|
||||
universeContext.put("worlds", universe.getWorlds().keySet().stream().toList());
|
||||
contexts.put("universe", universeContext);
|
||||
}
|
||||
|
||||
HashMap<String, Object> pluginsContext = new HashMap<>();
|
||||
|
||||
for (PluginBase plugin : this.pluginManager.getPlugins()) {
|
||||
PluginManifest manifestxx = plugin.getManifest();
|
||||
HashMap<String, Object> pluginInfo = new HashMap<>();
|
||||
pluginInfo.put("version", manifestxx.getVersion().toString());
|
||||
pluginInfo.put("state", plugin.getState().name());
|
||||
pluginsContext.put(plugin.getIdentifier().toString(), pluginInfo);
|
||||
}
|
||||
|
||||
contexts.put("plugins", pluginsContext);
|
||||
AssetModule assetModule = AssetModule.get();
|
||||
if (assetModule != null) {
|
||||
HashMap<String, Object> packsContext = new HashMap<>();
|
||||
|
||||
for (AssetPack pack : assetModule.getAssetPacks()) {
|
||||
HashMap<String, Object> packInfo = new HashMap<>();
|
||||
PluginManifest manifestx = pack.getManifest();
|
||||
if (manifestx != null && manifestx.getVersion() != null) {
|
||||
packInfo.put("version", manifestx.getVersion().toString());
|
||||
}
|
||||
|
||||
packInfo.put("immutable", pack.isImmutable());
|
||||
packsContext.put(pack.getName(), packInfo);
|
||||
}
|
||||
|
||||
contexts.put("packs", packsContext);
|
||||
}
|
||||
|
||||
User user = new User();
|
||||
HashMap<String, Object> unknown = new HashMap<>();
|
||||
user.setUnknown(unknown);
|
||||
UUID hardwareUUID = HardwareUtil.getUUID();
|
||||
if (hardwareUUID != null) {
|
||||
unknown.put("hardware-uuid", hardwareUUID.toString());
|
||||
}
|
||||
|
||||
ServerAuthManager authManager = ServerAuthManager.getInstance();
|
||||
unknown.put("auth-mode", authManager.getAuthMode().toString());
|
||||
SessionServiceClient.GameProfile profile = authManager.getSelectedProfile();
|
||||
if (profile != null) {
|
||||
user.setUsername(profile.username);
|
||||
user.setId(profile.uuid.toString());
|
||||
}
|
||||
|
||||
user.setIpAddress("{{auto}}");
|
||||
event.setUser(user);
|
||||
return event;
|
||||
}
|
||||
});
|
||||
Sentry.init(options);
|
||||
Sentry.configureScope(
|
||||
scope -> {
|
||||
UUID hardwareUUID = HardwareUtil.getUUID();
|
||||
if (hardwareUUID != null) {
|
||||
scope.setContexts("hardware", Map.of("uuid", hardwareUUID.toString()));
|
||||
}
|
||||
|
||||
OperatingSystem os = new OperatingSystem();
|
||||
os.setName(System.getProperty("os.name"));
|
||||
os.setVersion(System.getProperty("os.version"));
|
||||
scope.getContexts().setOperatingSystem(os);
|
||||
scope.setContexts(
|
||||
"build",
|
||||
Map.of(
|
||||
"version",
|
||||
String.valueOf(ManifestUtil.getImplementationVersion()),
|
||||
"revision-id",
|
||||
String.valueOf(ManifestUtil.getImplementationRevisionId()),
|
||||
"patchline",
|
||||
String.valueOf(ManifestUtil.getPatchline()),
|
||||
"environment",
|
||||
"release"
|
||||
)
|
||||
);
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
scope.setContexts(
|
||||
"singleplayer", Map.of("owner-uuid", String.valueOf(SingleplayerModule.getUuid()), "owner-name", SingleplayerModule.getUsername())
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
HytaleLogger.getLogger().setSentryClient(Sentry.getCurrentScopes());
|
||||
}
|
||||
|
||||
ServerAuthManager.getInstance().checkPendingFatalError();
|
||||
NettyUtil.init();
|
||||
float sin = TrigMathUtil.sin(0.0F);
|
||||
float atan2 = TrigMathUtil.atan2(0.0F, 0.0F);
|
||||
Thread shutdownHook = new Thread(() -> {
|
||||
if (this.shutdown.getAndSet(ShutdownReason.SIGINT) == null) {
|
||||
this.shutdown0(ShutdownReason.SIGINT);
|
||||
}
|
||||
}, "ShutdownHook");
|
||||
shutdownHook.setDaemon(false);
|
||||
Runtime.getRuntime().addShutdownHook(shutdownHook);
|
||||
AssetRegistryLoader.init();
|
||||
|
||||
for (PluginManifest manifest : Constants.CORE_PLUGINS) {
|
||||
this.pluginManager.registerCorePlugin(manifest);
|
||||
}
|
||||
|
||||
GCUtil.register(info -> {
|
||||
Universe universe = Universe.get();
|
||||
if (universe != null) {
|
||||
for (World world : universe.getWorlds().values()) {
|
||||
world.markGCHasRun();
|
||||
}
|
||||
}
|
||||
});
|
||||
this.boot();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public EventBus getEventBus() {
|
||||
return this.eventBus;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PluginManager getPluginManager() {
|
||||
return this.pluginManager;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CommandManager getCommandManager() {
|
||||
return this.commandManager;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public HytaleServerConfig getConfig() {
|
||||
return this.hytaleServerConfig;
|
||||
}
|
||||
|
||||
private void boot() {
|
||||
if (!this.booting.getAndSet(true)) {
|
||||
LOGGER.at(Level.INFO)
|
||||
.log("Booting up HytaleServer - Version: " + ManifestUtil.getImplementationVersion() + ", Revision: " + ManifestUtil.getImplementationRevisionId());
|
||||
|
||||
try {
|
||||
this.pluginsProgress = 0;
|
||||
this.sendSingleplayerProgress();
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.at(Level.INFO).log("Setup phase...");
|
||||
this.commandManager.registerCommands();
|
||||
this.pluginManager.setup();
|
||||
ServerAuthManager.getInstance().initializeCredentialStore();
|
||||
LOGGER.at(Level.INFO).log("Setup phase completed! Boot time %s", FormatUtil.nanosToString(System.nanoTime() - this.bootStart));
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LoadAssetEvent loadAssetEvent = get()
|
||||
.getEventBus()
|
||||
.<Void, LoadAssetEvent>dispatchFor(LoadAssetEvent.class)
|
||||
.dispatch(new LoadAssetEvent(this.bootStart));
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (loadAssetEvent.isShouldShutdown()) {
|
||||
List<String> reasons = loadAssetEvent.getReasons();
|
||||
String join = String.join(", ", reasons);
|
||||
LOGGER.at(Level.SEVERE).log("Asset validation FAILED with %d reason(s): %s", reasons.size(), join);
|
||||
this.shutdownServer(ShutdownReason.VALIDATE_ERROR.withMessage(join));
|
||||
return;
|
||||
}
|
||||
|
||||
if (Options.getOptionSet().has(Options.SHUTDOWN_AFTER_VALIDATE)) {
|
||||
LOGGER.at(Level.INFO).log("Asset validation passed");
|
||||
this.shutdownServer(ShutdownReason.SHUTDOWN);
|
||||
return;
|
||||
}
|
||||
|
||||
this.pluginsProgress = 0;
|
||||
this.sendSingleplayerProgress();
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
LOGGER.at(Level.INFO).log("Starting plugin manager...");
|
||||
this.pluginManager.start();
|
||||
LOGGER.at(Level.INFO).log("Plugin manager started! Startup time so far: %s", FormatUtil.nanosToString(System.nanoTime() - this.bootStart));
|
||||
if (this.isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.sendSingleplayerSignal("-=|Enabled|0");
|
||||
} catch (Throwable var6) {
|
||||
LOGGER.at(Level.SEVERE).withCause(var6).log("Failed to boot HytaleServer!");
|
||||
Throwable t = var6;
|
||||
|
||||
while (t.getCause() != null) {
|
||||
t = t.getCause();
|
||||
}
|
||||
|
||||
this.shutdownServer(ShutdownReason.CRASH.withMessage("Failed to start server! " + t.getMessage()));
|
||||
}
|
||||
|
||||
if (this.hytaleServerConfig.consumeHasChanged()) {
|
||||
HytaleServerConfig.save(this.hytaleServerConfig).join();
|
||||
}
|
||||
|
||||
SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
if (this.hytaleServerConfig.consumeHasChanged()) {
|
||||
HytaleServerConfig.save(this.hytaleServerConfig).join();
|
||||
}
|
||||
} catch (Exception var2x) {
|
||||
LOGGER.at(Level.SEVERE).withCause(var2x).log("Failed to save server config!");
|
||||
}
|
||||
}, 1L, 1L, TimeUnit.MINUTES);
|
||||
LOGGER.at(Level.INFO).log("Getting Hytale Universe ready...");
|
||||
Universe.get().getUniverseReady().join();
|
||||
LOGGER.at(Level.INFO).log("Universe ready!");
|
||||
List<String> tags = new ObjectArrayList<>();
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
tags.add("Singleplayer");
|
||||
} else {
|
||||
tags.add("Multiplayer");
|
||||
}
|
||||
|
||||
if (Constants.FRESH_UNIVERSE) {
|
||||
tags.add("Fresh Universe");
|
||||
}
|
||||
|
||||
this.booted.set(true);
|
||||
ServerManager.get().waitForBindComplete();
|
||||
this.eventBus.dispatch(BootEvent.class);
|
||||
List<String> bootCommands = Options.getOptionSet().valuesOf(Options.BOOT_COMMAND);
|
||||
if (!bootCommands.isEmpty()) {
|
||||
CommandManager.get().handleCommands(ConsoleSender.INSTANCE, new ArrayDeque<>(bootCommands)).join();
|
||||
}
|
||||
|
||||
String border = "\u001b[0;32m===============================================================================================";
|
||||
LOGGER.at(Level.INFO).log("\u001b[0;32m===============================================================================================");
|
||||
LOGGER.at(Level.INFO)
|
||||
.log(
|
||||
"%s Hytale Server Booted! [%s] took %s",
|
||||
"\u001b[0;32m",
|
||||
String.join(", ", tags),
|
||||
FormatUtil.nanosToString(System.nanoTime() - this.bootStart)
|
||||
);
|
||||
LOGGER.at(Level.INFO).log("\u001b[0;32m===============================================================================================");
|
||||
UpdateModule updateModule = UpdateModule.get();
|
||||
if (updateModule != null) {
|
||||
updateModule.onServerReady();
|
||||
}
|
||||
|
||||
ServerAuthManager authManager = ServerAuthManager.getInstance();
|
||||
if (!authManager.isSingleplayer() && authManager.getAuthMode() == ServerAuthManager.AuthMode.NONE) {
|
||||
LOGGER.at(Level.WARNING).log("%sNo server tokens configured. Use /auth login to authenticate.", "\u001b[0;31m");
|
||||
}
|
||||
|
||||
this.sendSingleplayerSignal(">> Singleplayer Ready <<");
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdownServer() {
|
||||
this.shutdownServer(ShutdownReason.SHUTDOWN);
|
||||
}
|
||||
|
||||
public void shutdownServer(@Nonnull ShutdownReason reason) {
|
||||
Objects.requireNonNull(reason, "Server shutdown reason can't be null!");
|
||||
if (this.shutdown.getAndSet(reason) == null) {
|
||||
if (reason.getMessage() != null) {
|
||||
this.sendSingleplayerSignal("-=|Shutdown|" + reason.getMessage());
|
||||
}
|
||||
|
||||
Thread shutdownThread = new Thread(() -> this.shutdown0(reason), "ShutdownThread");
|
||||
shutdownThread.setDaemon(false);
|
||||
shutdownThread.start();
|
||||
}
|
||||
}
|
||||
|
||||
void shutdown0(@Nonnull ShutdownReason reason) {
|
||||
LOGGER.at(Level.INFO).log("Shutdown triggered!!!");
|
||||
|
||||
try {
|
||||
LOGGER.at(Level.INFO).log("Shutting down... %d '%s'", reason.getExitCode(), reason.getMessage());
|
||||
this.eventBus.dispatch(ShutdownEvent.class);
|
||||
this.pluginManager.shutdown();
|
||||
this.commandManager.shutdown();
|
||||
this.eventBus.shutdown();
|
||||
ServerAuthManager.getInstance().shutdown();
|
||||
LOGGER.at(Level.INFO).log("Saving config...");
|
||||
if (this.hytaleServerConfig.consumeHasChanged()) {
|
||||
HytaleServerConfig.save(this.hytaleServerConfig).join();
|
||||
}
|
||||
|
||||
LOGGER.at(Level.INFO).log("Shutdown completed!");
|
||||
} catch (Throwable var3) {
|
||||
LOGGER.at(Level.SEVERE).withCause(var3).log("Exception while shutting down:");
|
||||
}
|
||||
|
||||
this.aliveLock.release();
|
||||
HytaleLogManager.resetFinally();
|
||||
SCHEDULED_EXECUTOR.schedule(() -> {
|
||||
LOGGER.at(Level.SEVERE).log("Forcing shutdown!");
|
||||
Runtime.getRuntime().halt(reason.getExitCode());
|
||||
}, 3L, TimeUnit.SECONDS);
|
||||
if (reason != ShutdownReason.SIGINT) {
|
||||
System.exit(reason.getExitCode());
|
||||
}
|
||||
}
|
||||
|
||||
public void doneSetup(PluginBase plugin) {
|
||||
this.pluginsProgress++;
|
||||
this.sendSingleplayerProgress();
|
||||
}
|
||||
|
||||
public void doneStart(PluginBase plugin) {
|
||||
this.pluginsProgress++;
|
||||
this.sendSingleplayerProgress();
|
||||
}
|
||||
|
||||
public void doneStop(PluginBase plugin) {
|
||||
this.pluginsProgress--;
|
||||
this.sendSingleplayerProgress();
|
||||
}
|
||||
|
||||
public void sendSingleplayerProgress() {
|
||||
List<PluginBase> plugins = this.pluginManager.getPlugins();
|
||||
if (this.shutdown.get() != null) {
|
||||
this.sendSingleplayerSignal("-=|Shutdown Modules|" + MathUtil.round((double) (plugins.size() - this.pluginsProgress) / plugins.size(), 2) * 100.0);
|
||||
} else if (this.pluginManager.getState() == PluginState.SETUP) {
|
||||
this.sendSingleplayerSignal("-=|Setup|" + MathUtil.round((double) this.pluginsProgress / plugins.size(), 2) * 100.0);
|
||||
} else if (this.pluginManager.getState() == PluginState.START) {
|
||||
this.sendSingleplayerSignal("-=|Starting|" + MathUtil.round((double) this.pluginsProgress / plugins.size(), 2) * 100.0);
|
||||
}
|
||||
}
|
||||
|
||||
public String getServerName() {
|
||||
return this.getConfig().getServerName();
|
||||
}
|
||||
|
||||
public boolean isBooting() {
|
||||
return this.booting.get();
|
||||
}
|
||||
|
||||
public boolean isBooted() {
|
||||
return this.booted.get();
|
||||
}
|
||||
|
||||
public boolean isShuttingDown() {
|
||||
return this.shutdown.get() != null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Instant getBoot() {
|
||||
return this.boot;
|
||||
}
|
||||
|
||||
public long getBootStart() {
|
||||
return this.bootStart;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ShutdownReason getShutdownReason() {
|
||||
return this.shutdown.get();
|
||||
}
|
||||
|
||||
private void sendSingleplayerSignal(String message) {
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
HytaleLoggerBackend.rawLog(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void reportSingleplayerStatus(String message) {
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
HytaleLoggerBackend.rawLog("-=|" + message + "|0");
|
||||
}
|
||||
}
|
||||
|
||||
public void reportSaveProgress(@Nonnull World world, int saved, int total) {
|
||||
if (this.isShuttingDown()) {
|
||||
double progress = MathUtil.round((double) saved / total, 2) * 100.0;
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
this.sendSingleplayerSignal("-=|Saving world " + world.getName() + " chunks|" + progress);
|
||||
} else if (total < 10 || saved % (total / 10) == 0) {
|
||||
world.getLogger().at(Level.INFO).log("Saving chunks: %.0f%%", progress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static HytaleServer get() {
|
||||
return instance;
|
||||
}
|
||||
}
|
||||
851
src/com/hypixel/hytale/server/core/entity/InteractionChain.java
Normal file
851
src/com/hypixel/hytale/server/core/entity/InteractionChain.java
Normal file
@@ -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<InteractionChain> forkedChains = new Long2ObjectOpenHashMap<>();
|
||||
@Nonnull
|
||||
private final Long2ObjectMap<InteractionChain.TempChain> tempForkedChainData = new Long2ObjectOpenHashMap<>();
|
||||
@Nonnull
|
||||
private final Long2LongMap forkedChainsMap = new Long2LongOpenHashMap();
|
||||
@Nonnull
|
||||
private final List<InteractionChain> newForks = new ObjectArrayList<>();
|
||||
@Nonnull
|
||||
private final RootInteraction initialRootInteraction;
|
||||
private RootInteraction rootInteraction;
|
||||
private int operationCounter = 0;
|
||||
@Nonnull
|
||||
private final List<InteractionChain.CallState> callStack = new ObjectArrayList<>();
|
||||
private int simulatedCallStack = 0;
|
||||
private final boolean requiresClient;
|
||||
private int simulatedOperationCounter = 0;
|
||||
private RootInteraction simulatedRootInteraction;
|
||||
private int operationIndex = 0;
|
||||
private int operationIndexOffset = 0;
|
||||
private int clientOperationIndex = 0;
|
||||
@Nonnull
|
||||
private final List<InteractionEntry> interactions = new ObjectArrayList<>();
|
||||
@Nonnull
|
||||
private final List<InteractionSyncData> tempSyncData = new ObjectArrayList<>();
|
||||
private int tempSyncDataOffset = 0;
|
||||
private long timestamp = System.nanoTime();
|
||||
private long waitingForServerFinished;
|
||||
private long waitingForClientFinished;
|
||||
private InteractionState clientState = InteractionState.NotFinished;
|
||||
private InteractionState serverState = InteractionState.NotFinished;
|
||||
private InteractionState finalState = InteractionState.Finished;
|
||||
@Nullable
|
||||
private Runnable onCompletion;
|
||||
private boolean sentInitial;
|
||||
private boolean desynced;
|
||||
private float timeShift;
|
||||
private boolean firstRun = true;
|
||||
private boolean isFirstRun = true;
|
||||
private boolean completed = false;
|
||||
private boolean preTicked;
|
||||
boolean skipChainOnClick;
|
||||
|
||||
public InteractionChain(
|
||||
InteractionType type,
|
||||
InteractionContext context,
|
||||
InteractionChainData chainData,
|
||||
@Nonnull RootInteraction rootInteraction,
|
||||
@Nullable Runnable onCompletion,
|
||||
boolean requiresClient
|
||||
) {
|
||||
this(null, null, type, context, chainData, rootInteraction, onCompletion, requiresClient);
|
||||
}
|
||||
|
||||
public InteractionChain(
|
||||
ForkedChainId forkedChainId,
|
||||
ForkedChainId baseForkedChainId,
|
||||
InteractionType type,
|
||||
InteractionContext context,
|
||||
InteractionChainData chainData,
|
||||
@Nonnull RootInteraction rootInteraction,
|
||||
@Nullable Runnable onCompletion,
|
||||
boolean requiresClient
|
||||
) {
|
||||
this.type = this.baseType = type;
|
||||
this.context = context;
|
||||
this.chainData = chainData;
|
||||
this.forkedChainId = forkedChainId;
|
||||
this.baseForkedChainId = baseForkedChainId;
|
||||
this.onCompletion = onCompletion;
|
||||
this.initialRootInteraction = this.rootInteraction = this.simulatedRootInteraction = rootInteraction;
|
||||
this.requiresClient = requiresClient || rootInteraction.needsRemoteSync();
|
||||
this.forkedChainsMap.defaultReturnValue(NULL_FORK_ID);
|
||||
}
|
||||
|
||||
public InteractionType getType() {
|
||||
return this.type;
|
||||
}
|
||||
|
||||
public int getChainId() {
|
||||
return this.chainId;
|
||||
}
|
||||
|
||||
public ForkedChainId getForkedChainId() {
|
||||
return this.forkedChainId;
|
||||
}
|
||||
|
||||
public ForkedChainId getBaseForkedChainId() {
|
||||
return this.baseForkedChainId;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public RootInteraction getInitialRootInteraction() {
|
||||
return this.initialRootInteraction;
|
||||
}
|
||||
|
||||
public boolean isPredicted() {
|
||||
return this.predicted;
|
||||
}
|
||||
|
||||
public InteractionContext getContext() {
|
||||
return this.context;
|
||||
}
|
||||
|
||||
public InteractionChainData getChainData() {
|
||||
return this.chainData;
|
||||
}
|
||||
|
||||
public InteractionState getServerState() {
|
||||
return this.serverState;
|
||||
}
|
||||
|
||||
public boolean requiresClient() {
|
||||
return this.requiresClient;
|
||||
}
|
||||
|
||||
public RootInteraction getRootInteraction() {
|
||||
return this.rootInteraction;
|
||||
}
|
||||
|
||||
public RootInteraction getSimulatedRootInteraction() {
|
||||
return this.simulatedRootInteraction;
|
||||
}
|
||||
|
||||
public int getOperationCounter() {
|
||||
return this.operationCounter;
|
||||
}
|
||||
|
||||
public void setOperationCounter(int operationCounter) {
|
||||
this.operationCounter = operationCounter;
|
||||
}
|
||||
|
||||
public int getSimulatedOperationCounter() {
|
||||
return this.simulatedOperationCounter;
|
||||
}
|
||||
|
||||
public void setSimulatedOperationCounter(int simulatedOperationCounter) {
|
||||
this.simulatedOperationCounter = simulatedOperationCounter;
|
||||
}
|
||||
|
||||
public boolean wasPreTicked() {
|
||||
return this.preTicked;
|
||||
}
|
||||
|
||||
public void setPreTicked(boolean preTicked) {
|
||||
this.preTicked = preTicked;
|
||||
}
|
||||
|
||||
public int getOperationIndex() {
|
||||
return this.operationIndex;
|
||||
}
|
||||
|
||||
public void nextOperationIndex() {
|
||||
this.operationIndex++;
|
||||
this.clientOperationIndex++;
|
||||
}
|
||||
|
||||
public int getClientOperationIndex() {
|
||||
return this.clientOperationIndex;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InteractionChain findForkedChain(@Nonnull ForkedChainId chainId, @Nullable InteractionChainData data) {
|
||||
long id = forkedIdToIndex(chainId);
|
||||
long altId = this.forkedChainsMap.get(id);
|
||||
if (altId != NULL_FORK_ID) {
|
||||
id = altId;
|
||||
}
|
||||
|
||||
InteractionChain chain = this.forkedChains.get(id);
|
||||
if (chain == null && chainId.subIndex < 0 && data != null) {
|
||||
InteractionEntry entry = this.getInteraction(chainId.entryIndex);
|
||||
if (entry == null) {
|
||||
return null;
|
||||
} else {
|
||||
int rootId = entry.getServerState().rootInteraction;
|
||||
int opCounter = entry.getServerState().operationCounter;
|
||||
RootInteraction root = RootInteraction.getAssetMap().getAsset(rootId);
|
||||
if (root.getOperation(opCounter).getInnerOperation() instanceof Interaction interaction) {
|
||||
this.context.initEntry(this, entry, null);
|
||||
chain = interaction.mapForkChain(this.context, data);
|
||||
this.context.deinitEntry(this, entry, null);
|
||||
if (chain != null) {
|
||||
this.forkedChainsMap.put(id, forkedIdToIndex(chain.getBaseForkedChainId()));
|
||||
}
|
||||
|
||||
return chain;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return chain;
|
||||
}
|
||||
}
|
||||
|
||||
public InteractionChain getForkedChain(@Nonnull ForkedChainId chainId) {
|
||||
long id = forkedIdToIndex(chainId);
|
||||
if (chainId.subIndex < 0) {
|
||||
long altId = this.forkedChainsMap.get(id);
|
||||
if (altId != NULL_FORK_ID) {
|
||||
id = altId;
|
||||
}
|
||||
}
|
||||
|
||||
return this.forkedChains.get(id);
|
||||
}
|
||||
|
||||
public void putForkedChain(@Nonnull ForkedChainId chainId, @Nonnull InteractionChain chain) {
|
||||
this.newForks.add(chain);
|
||||
this.forkedChains.put(forkedIdToIndex(chainId), chain);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InteractionChain.TempChain getTempForkedChain(@Nonnull ForkedChainId chainId) {
|
||||
InteractionEntry entry = this.getInteraction(chainId.entryIndex);
|
||||
if (entry != null) {
|
||||
if (chainId.subIndex < entry.getNextForkId()) {
|
||||
return null;
|
||||
}
|
||||
} else if (chainId.entryIndex < this.operationIndexOffset) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.tempForkedChainData.computeIfAbsent(forkedIdToIndex(chainId), i -> new InteractionChain.TempChain());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
InteractionChain.TempChain removeTempForkedChain(@Nonnull ForkedChainId chainId, InteractionChain forkChain) {
|
||||
long id = forkedIdToIndex(chainId);
|
||||
long altId = this.forkedChainsMap.get(id);
|
||||
if (altId != NULL_FORK_ID) {
|
||||
id = altId;
|
||||
}
|
||||
|
||||
InteractionChain.TempChain found = this.tempForkedChainData.remove(id);
|
||||
if (found != null) {
|
||||
return found;
|
||||
} else {
|
||||
InteractionEntry iEntry = this.context.getEntry();
|
||||
RootInteraction root = RootInteraction.getAssetMap().getAsset(iEntry.getState().rootInteraction);
|
||||
if (root.getOperation(iEntry.getState().operationCounter).getInnerOperation() instanceof Interaction interaction) {
|
||||
ObjectIterator<Entry<InteractionChain.TempChain>> it = Long2ObjectMaps.fastIterator(this.getTempForkedChainData());
|
||||
|
||||
while (it.hasNext()) {
|
||||
Entry<InteractionChain.TempChain> entry = it.next();
|
||||
InteractionChain.TempChain tempChain = entry.getValue();
|
||||
if (tempChain.baseForkedChainId != null) {
|
||||
int entryId = tempChain.baseForkedChainId.entryIndex;
|
||||
if (entryId == iEntry.getIndex()) {
|
||||
InteractionChain chain = interaction.mapForkChain(this.getContext(), tempChain.chainData);
|
||||
if (chain != null) {
|
||||
this.forkedChainsMap.put(forkedIdToIndex(tempChain.baseForkedChainId), forkedIdToIndex(chain.getBaseForkedChainId()));
|
||||
}
|
||||
|
||||
if (chain == forkChain) {
|
||||
it.remove();
|
||||
return tempChain;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSentInitial() {
|
||||
return this.sentInitial;
|
||||
}
|
||||
|
||||
public void setSentInitial(boolean sentInitial) {
|
||||
this.sentInitial = sentInitial;
|
||||
}
|
||||
|
||||
public float getTimeShift() {
|
||||
return this.timeShift;
|
||||
}
|
||||
|
||||
public void setTimeShift(float timeShift) {
|
||||
this.timeShift = timeShift;
|
||||
}
|
||||
|
||||
public boolean consumeFirstRun() {
|
||||
this.isFirstRun = this.firstRun;
|
||||
this.firstRun = false;
|
||||
return this.isFirstRun;
|
||||
}
|
||||
|
||||
public boolean isFirstRun() {
|
||||
return this.isFirstRun;
|
||||
}
|
||||
|
||||
public void setFirstRun(boolean firstRun) {
|
||||
this.isFirstRun = firstRun;
|
||||
}
|
||||
|
||||
public int getCallDepth() {
|
||||
return this.callStack.size();
|
||||
}
|
||||
|
||||
public int getSimulatedCallDepth() {
|
||||
return this.simulatedCallStack;
|
||||
}
|
||||
|
||||
public void pushRoot(RootInteraction nextInteraction, boolean simulate) {
|
||||
if (simulate) {
|
||||
this.simulatedRootInteraction = nextInteraction;
|
||||
this.simulatedOperationCounter = 0;
|
||||
this.simulatedCallStack++;
|
||||
} else {
|
||||
this.callStack.add(new InteractionChain.CallState(this.rootInteraction, this.operationCounter));
|
||||
this.operationCounter = 0;
|
||||
this.rootInteraction = nextInteraction;
|
||||
}
|
||||
}
|
||||
|
||||
public void popRoot() {
|
||||
InteractionChain.CallState state = this.callStack.removeLast();
|
||||
this.rootInteraction = state.rootInteraction;
|
||||
this.operationCounter = state.operationCounter + 1;
|
||||
this.simulatedRootInteraction = this.rootInteraction;
|
||||
this.simulatedOperationCounter = this.operationCounter;
|
||||
this.simulatedCallStack--;
|
||||
}
|
||||
|
||||
public float getTimeInSeconds() {
|
||||
if (this.timestamp == 0L) {
|
||||
return 0.0F;
|
||||
} else {
|
||||
long diff = System.nanoTime() - this.timestamp;
|
||||
return (float) diff / 1.0E9F;
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnCompletion(Runnable onCompletion) {
|
||||
this.onCompletion = onCompletion;
|
||||
}
|
||||
|
||||
void onCompletion(CooldownHandler cooldownHandler, boolean isRemote) {
|
||||
if (!this.completed) {
|
||||
this.completed = true;
|
||||
if (this.onCompletion != null) {
|
||||
this.onCompletion.run();
|
||||
this.onCompletion = null;
|
||||
}
|
||||
|
||||
if (isRemote) {
|
||||
InteractionCooldown cooldown = this.initialRootInteraction.getCooldown();
|
||||
String cooldownId = this.initialRootInteraction.getId();
|
||||
if (cooldown != null && cooldown.cooldownId != null) {
|
||||
cooldownId = cooldown.cooldownId;
|
||||
}
|
||||
|
||||
CooldownHandler.Cooldown cooldownTracker = cooldownHandler.getCooldown(cooldownId);
|
||||
if (cooldownTracker != null) {
|
||||
cooldownTracker.tick(0.016666668F);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateServerState() {
|
||||
if (this.serverState == InteractionState.NotFinished) {
|
||||
if (this.operationCounter >= this.rootInteraction.getOperationMax()) {
|
||||
this.serverState = this.finalState;
|
||||
} else {
|
||||
InteractionEntry entry = this.getOrCreateInteractionEntry(this.operationIndex);
|
||||
|
||||
this.serverState = switch (entry.getServerState().state) {
|
||||
case NotFinished, Finished -> InteractionState.NotFinished;
|
||||
default -> InteractionState.Failed;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void updateSimulatedState() {
|
||||
if (this.clientState == InteractionState.NotFinished) {
|
||||
if (this.simulatedOperationCounter >= this.rootInteraction.getOperationMax()) {
|
||||
this.clientState = this.finalState;
|
||||
} else {
|
||||
InteractionEntry entry = this.getOrCreateInteractionEntry(this.clientOperationIndex);
|
||||
|
||||
this.clientState = switch (entry.getSimulationState().state) {
|
||||
case NotFinished, Finished -> InteractionState.NotFinished;
|
||||
default -> InteractionState.Failed;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionState getClientState() {
|
||||
return this.clientState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientState(InteractionState state) {
|
||||
this.clientState = state;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public InteractionEntry getOrCreateInteractionEntry(int index) {
|
||||
int oIndex = index - this.operationIndexOffset;
|
||||
if (oIndex < 0) {
|
||||
throw new IllegalArgumentException("Trying to access removed interaction entry");
|
||||
} else {
|
||||
InteractionEntry entry = oIndex < this.interactions.size() ? this.interactions.get(oIndex) : null;
|
||||
if (entry == null) {
|
||||
if (oIndex != this.interactions.size()) {
|
||||
throw new IllegalArgumentException("Trying to add interaction entry at a weird location: " + oIndex + " " + this.interactions.size());
|
||||
}
|
||||
|
||||
entry = new InteractionEntry(index, this.operationCounter, RootInteraction.getRootInteractionIdOrUnknown(this.rootInteraction.getId()));
|
||||
this.interactions.add(entry);
|
||||
}
|
||||
|
||||
return entry;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InteractionEntry getInteraction(int index) {
|
||||
index -= this.operationIndexOffset;
|
||||
return index >= 0 && index < this.interactions.size() ? this.interactions.get(index) : null;
|
||||
}
|
||||
|
||||
// HyFix #40: Wrap in try-catch to handle out-of-order removal gracefully
|
||||
public void removeInteractionEntry(@Nonnull InteractionManager interactionManager, int index) {
|
||||
try {
|
||||
int oIndex = index - this.operationIndexOffset;
|
||||
if (oIndex != 0) {
|
||||
throw new IllegalArgumentException("Trying to remove out of order");
|
||||
} else {
|
||||
InteractionEntry entry = this.interactions.remove(oIndex);
|
||||
this.operationIndexOffset++;
|
||||
this.tempForkedChainData.values().removeIf(fork -> {
|
||||
if (fork.baseForkedChainId.entryIndex != entry.getIndex()) {
|
||||
return false;
|
||||
} else {
|
||||
interactionManager.sendCancelPacket(this.getChainId(), fork.forkedChainId);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
System.out.println("[HyFix] WARNING: Suppressed out-of-order removal in InteractionChain (Issue #40)");
|
||||
}
|
||||
}
|
||||
|
||||
// HyFix: Expand buffer backwards instead of dropping data when index < offset
|
||||
@Override
|
||||
public void putInteractionSyncData(int index, InteractionSyncData data) {
|
||||
int adjustedIndex = index - this.tempSyncDataOffset;
|
||||
|
||||
// HyFix: Handle negative indices by expanding buffer backwards
|
||||
if (adjustedIndex < 0) {
|
||||
int expansion = -adjustedIndex;
|
||||
for (int i = 0; i < expansion; i++) {
|
||||
this.tempSyncData.add(0, null); // prepend nulls
|
||||
}
|
||||
this.tempSyncDataOffset = this.tempSyncDataOffset + adjustedIndex; // shift offset down
|
||||
adjustedIndex = 0;
|
||||
}
|
||||
|
||||
if (adjustedIndex < this.tempSyncData.size()) {
|
||||
this.tempSyncData.set(adjustedIndex, data);
|
||||
} else if (adjustedIndex == this.tempSyncData.size()) {
|
||||
this.tempSyncData.add(data);
|
||||
} else {
|
||||
LOGGER.at(Level.WARNING).log("Temp sync data sent out of order: " + adjustedIndex + " " + this.tempSyncData.size());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearInteractionSyncData(int operationIndex) {
|
||||
int tempIdx = operationIndex - this.tempSyncDataOffset;
|
||||
if (!this.tempSyncData.isEmpty()) {
|
||||
for (int end = this.tempSyncData.size() - 1; end >= tempIdx && end >= 0; end--) {
|
||||
this.tempSyncData.remove(end);
|
||||
}
|
||||
}
|
||||
|
||||
int idx = operationIndex - this.operationIndexOffset;
|
||||
|
||||
for (int i = Math.max(idx, 0); i < this.interactions.size(); i++) {
|
||||
this.interactions.get(i).setClientState(null);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public InteractionSyncData removeInteractionSyncData(int index) {
|
||||
index -= this.tempSyncDataOffset;
|
||||
if (index != 0) {
|
||||
return null;
|
||||
} else if (this.tempSyncData.isEmpty()) {
|
||||
return null;
|
||||
} else if (this.tempSyncData.get(index) == null) {
|
||||
return null;
|
||||
} else {
|
||||
this.tempSyncDataOffset++;
|
||||
return this.tempSyncData.remove(index);
|
||||
}
|
||||
}
|
||||
|
||||
// HyFix: Handle sync gaps gracefully instead of throwing
|
||||
@Override
|
||||
public void updateSyncPosition(int index) {
|
||||
// HyFix: Accept any index >= offset, not just == offset
|
||||
if (index >= this.tempSyncDataOffset) {
|
||||
this.tempSyncDataOffset = index + 1;
|
||||
}
|
||||
// index < offset is silently ignored (already processed)
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSyncDataOutOfOrder(int index) {
|
||||
return index > this.tempSyncDataOffset + this.tempSyncData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncFork(@Nonnull Ref<EntityStore> ref, @Nonnull InteractionManager manager, @Nonnull SyncInteractionChain packet) {
|
||||
ForkedChainId baseId = packet.forkedId;
|
||||
|
||||
while (baseId.forkedId != null) {
|
||||
baseId = baseId.forkedId;
|
||||
}
|
||||
|
||||
InteractionChain fork = this.findForkedChain(baseId, packet.data);
|
||||
if (fork != null) {
|
||||
manager.sync(ref, fork, packet);
|
||||
} else {
|
||||
InteractionChain.TempChain temp = this.getTempForkedChain(baseId);
|
||||
if (temp == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
temp.setForkedChainId(packet.forkedId);
|
||||
temp.setBaseForkedChainId(baseId);
|
||||
temp.setChainData(packet.data);
|
||||
manager.sync(ref, temp, packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void copyTempFrom(@Nonnull InteractionChain.TempChain temp) {
|
||||
this.setClientState(temp.clientState);
|
||||
this.tempSyncData.addAll(temp.tempSyncData);
|
||||
this.getTempForkedChainData().putAll(temp.tempForkedChainData);
|
||||
}
|
||||
|
||||
private static long forkedIdToIndex(@Nonnull ForkedChainId chainId) {
|
||||
return (long) chainId.entryIndex << 32 | chainId.subIndex & 4294967295L;
|
||||
}
|
||||
|
||||
public void setChainId(int chainId) {
|
||||
this.chainId = chainId;
|
||||
}
|
||||
|
||||
public InteractionType getBaseType() {
|
||||
return this.baseType;
|
||||
}
|
||||
|
||||
public void setBaseType(InteractionType baseType) {
|
||||
this.baseType = baseType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Long2ObjectMap<InteractionChain> getForkedChains() {
|
||||
return this.forkedChains;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Long2ObjectMap<InteractionChain.TempChain> getTempForkedChainData() {
|
||||
return this.tempForkedChainData;
|
||||
}
|
||||
|
||||
public long getTimestamp() {
|
||||
return this.timestamp;
|
||||
}
|
||||
|
||||
public void setTimestamp(long timestamp) {
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
|
||||
public long getWaitingForServerFinished() {
|
||||
return this.waitingForServerFinished;
|
||||
}
|
||||
|
||||
public void setWaitingForServerFinished(long waitingForServerFinished) {
|
||||
this.waitingForServerFinished = waitingForServerFinished;
|
||||
}
|
||||
|
||||
public long getWaitingForClientFinished() {
|
||||
return this.waitingForClientFinished;
|
||||
}
|
||||
|
||||
public void setWaitingForClientFinished(long waitingForClientFinished) {
|
||||
this.waitingForClientFinished = waitingForClientFinished;
|
||||
}
|
||||
|
||||
public void setServerState(InteractionState serverState) {
|
||||
this.serverState = serverState;
|
||||
}
|
||||
|
||||
public InteractionState getFinalState() {
|
||||
return this.finalState;
|
||||
}
|
||||
|
||||
public void setFinalState(InteractionState finalState) {
|
||||
this.finalState = finalState;
|
||||
}
|
||||
|
||||
void setPredicted(boolean predicted) {
|
||||
this.predicted = predicted;
|
||||
}
|
||||
|
||||
public void flagDesync() {
|
||||
this.desynced = true;
|
||||
this.forkedChains.forEach((k, c) -> c.flagDesync());
|
||||
}
|
||||
|
||||
public boolean isDesynced() {
|
||||
return this.desynced;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<InteractionChain> getNewForks() {
|
||||
return this.newForks;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "InteractionChain{type="
|
||||
+ this.type
|
||||
+ ", chainData="
|
||||
+ this.chainData
|
||||
+ ", chainId="
|
||||
+ this.chainId
|
||||
+ ", forkedChainId="
|
||||
+ this.forkedChainId
|
||||
+ ", predicted="
|
||||
+ this.predicted
|
||||
+ ", context="
|
||||
+ this.context
|
||||
+ ", forkedChains="
|
||||
+ this.forkedChains
|
||||
+ ", tempForkedChainData="
|
||||
+ this.tempForkedChainData
|
||||
+ ", initialRootInteraction="
|
||||
+ this.initialRootInteraction
|
||||
+ ", rootInteraction="
|
||||
+ this.rootInteraction
|
||||
+ ", operationCounter="
|
||||
+ this.operationCounter
|
||||
+ ", callStack="
|
||||
+ this.callStack
|
||||
+ ", simulatedCallStack="
|
||||
+ this.simulatedCallStack
|
||||
+ ", requiresClient="
|
||||
+ this.requiresClient
|
||||
+ ", simulatedOperationCounter="
|
||||
+ this.simulatedOperationCounter
|
||||
+ ", simulatedRootInteraction="
|
||||
+ this.simulatedRootInteraction
|
||||
+ ", operationIndex="
|
||||
+ this.operationIndex
|
||||
+ ", operationIndexOffset="
|
||||
+ this.operationIndexOffset
|
||||
+ ", clientOperationIndex="
|
||||
+ this.clientOperationIndex
|
||||
+ ", interactions="
|
||||
+ this.interactions
|
||||
+ ", tempSyncData="
|
||||
+ this.tempSyncData
|
||||
+ ", tempSyncDataOffset="
|
||||
+ this.tempSyncDataOffset
|
||||
+ ", timestamp="
|
||||
+ this.timestamp
|
||||
+ ", waitingForServerFinished="
|
||||
+ this.waitingForServerFinished
|
||||
+ ", waitingForClientFinished="
|
||||
+ this.waitingForClientFinished
|
||||
+ ", clientState="
|
||||
+ this.clientState
|
||||
+ ", serverState="
|
||||
+ this.serverState
|
||||
+ ", onCompletion="
|
||||
+ this.onCompletion
|
||||
+ ", sentInitial="
|
||||
+ this.sentInitial
|
||||
+ ", desynced="
|
||||
+ this.desynced
|
||||
+ ", timeShift="
|
||||
+ this.timeShift
|
||||
+ ", firstRun="
|
||||
+ this.firstRun
|
||||
+ ", skipChainOnClick="
|
||||
+ this.skipChainOnClick
|
||||
+ "}";
|
||||
}
|
||||
|
||||
private record CallState(RootInteraction rootInteraction, int operationCounter) {
|
||||
}
|
||||
|
||||
static class TempChain implements ChainSyncStorage {
|
||||
final Long2ObjectMap<InteractionChain.TempChain> tempForkedChainData = new Long2ObjectOpenHashMap<>();
|
||||
final List<InteractionSyncData> tempSyncData = new ObjectArrayList<>();
|
||||
ForkedChainId forkedChainId;
|
||||
InteractionState clientState = InteractionState.NotFinished;
|
||||
ForkedChainId baseForkedChainId;
|
||||
InteractionChainData chainData;
|
||||
|
||||
TempChain() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public InteractionChain.TempChain getOrCreateTempForkedChain(@Nonnull ForkedChainId chainId) {
|
||||
return this.tempForkedChainData.computeIfAbsent(InteractionChain.forkedIdToIndex(chainId), i -> new InteractionChain.TempChain());
|
||||
}
|
||||
|
||||
@Override
|
||||
public InteractionState getClientState() {
|
||||
return this.clientState;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setClientState(InteractionState state) {
|
||||
this.clientState = state;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public InteractionEntry getInteraction(int index) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void putInteractionSyncData(int index, InteractionSyncData data) {
|
||||
if (index < this.tempSyncData.size()) {
|
||||
this.tempSyncData.set(index, data);
|
||||
} else {
|
||||
if (index != this.tempSyncData.size()) {
|
||||
throw new IllegalArgumentException("Temp sync data sent out of order: " + index + " " + this.tempSyncData.size());
|
||||
}
|
||||
|
||||
this.tempSyncData.add(data);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateSyncPosition(int index) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSyncDataOutOfOrder(int index) {
|
||||
return index > this.tempSyncData.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void syncFork(@Nonnull Ref<EntityStore> ref, @Nonnull InteractionManager manager, @Nonnull SyncInteractionChain packet) {
|
||||
ForkedChainId baseId = packet.forkedId;
|
||||
|
||||
while (baseId.forkedId != null) {
|
||||
baseId = baseId.forkedId;
|
||||
}
|
||||
|
||||
InteractionChain.TempChain temp = this.getOrCreateTempForkedChain(baseId);
|
||||
temp.setForkedChainId(packet.forkedId);
|
||||
temp.setBaseForkedChainId(baseId);
|
||||
temp.setChainData(packet.data);
|
||||
manager.sync(ref, temp, packet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void clearInteractionSyncData(int index) {
|
||||
for (int end = this.tempSyncData.size() - 1; end >= index; end--) {
|
||||
this.tempSyncData.remove(end);
|
||||
}
|
||||
}
|
||||
|
||||
public InteractionChainData getChainData() {
|
||||
return this.chainData;
|
||||
}
|
||||
|
||||
public void setChainData(InteractionChainData chainData) {
|
||||
this.chainData = chainData;
|
||||
}
|
||||
|
||||
public ForkedChainId getBaseForkedChainId() {
|
||||
return this.baseForkedChainId;
|
||||
}
|
||||
|
||||
public void setBaseForkedChainId(ForkedChainId baseForkedChainId) {
|
||||
this.baseForkedChainId = baseForkedChainId;
|
||||
}
|
||||
|
||||
public void setForkedChainId(ForkedChainId forkedChainId) {
|
||||
this.forkedChainId = forkedChainId;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "TempChain{tempForkedChainData=" + this.tempForkedChainData + ", tempSyncData=" + this.tempSyncData + ", clientState=" + this.clientState + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
1534
src/com/hypixel/hytale/server/core/entity/InteractionManager.java
Normal file
1534
src/com/hypixel/hytale/server/core/entity/InteractionManager.java
Normal file
File diff suppressed because it is too large
Load Diff
251
src/com/hypixel/hytale/server/core/entity/LivingEntity.java
Normal file
251
src/com/hypixel/hytale/server/core/entity/LivingEntity.java
Normal file
@@ -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<LivingEntity> 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<EntityStore> ref, @Nonnull BlockMaterial breathingMaterial, int fluidId, @Nonnull ComponentAccessor<EntityStore> componentAccessor
|
||||
) {
|
||||
boolean invulnerable = componentAccessor.getArchetype(ref).contains(Invulnerable.getComponentType());
|
||||
return invulnerable || breathingMaterial == BlockMaterial.Empty && fluidId == 0;
|
||||
}
|
||||
|
||||
public static long getPackedMaterialAndFluidAtBreathingHeight(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> 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<ChunkStore> 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<ItemStack> remainder = ensureCapacity ? new ObjectArrayList<>() : null;
|
||||
inventory = this.setInventory(inventory, ensureCapacity, remainder);
|
||||
if (remainder != null && !remainder.isEmpty()) {
|
||||
ListTransaction<ItemStackTransaction> transactionList = inventory.getCombinedHotbarFirst().addItemStacks(remainder);
|
||||
|
||||
for (ItemStackTransaction var6 : transactionList.getList()) {
|
||||
;
|
||||
}
|
||||
}
|
||||
|
||||
return inventory;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Inventory setInventory(Inventory inventory, boolean ensureCapacity, List<ItemStack> 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<EntityStore> ref, double locX, double locY, double locZ, @Nonnull ComponentAccessor<EntityStore> 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<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean canApplyItemStackPenalties(Ref<EntityStore> ref, ComponentAccessor<EntityStore> componentAccessor) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public ItemStackSlotTransaction decreaseItemStackDurability(
|
||||
@Nonnull Ref<EntityStore> ref, @Nullable ItemStack itemStack, int inventoryId, int slotId, @Nonnull ComponentAccessor<EntityStore> 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<EntityStore> ref,
|
||||
@Nonnull ItemStack itemStack,
|
||||
ItemContainer container,
|
||||
int slotId,
|
||||
double durabilityChange,
|
||||
@Nonnull ComponentAccessor<EntityStore> 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() + "}";
|
||||
}
|
||||
}
|
||||
568
src/com/hypixel/hytale/server/core/io/PacketHandler.java
Normal file
568
src/com/hypixel/hytale/server/core/io/PacketHandler.java
Normal file
@@ -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<Long> LOGIN_START_ATTRIBUTE_KEY = AttributeKey.newInstance("LOGIN_START");
|
||||
@Nonnull
|
||||
protected final Channel channel;
|
||||
@Nonnull
|
||||
protected final ProtocolVersion protocolVersion;
|
||||
@Nullable
|
||||
protected PlayerAuthentication auth;
|
||||
protected boolean queuePackets;
|
||||
protected final AtomicInteger queuedPackets = new AtomicInteger();
|
||||
protected final SecureRandom pingIdRandom = new SecureRandom();
|
||||
@Nonnull
|
||||
protected final PacketHandler.PingInfo[] pingInfo;
|
||||
private float pingTimer;
|
||||
protected boolean registered;
|
||||
private ScheduledFuture<?> timeoutTask;
|
||||
@Nullable
|
||||
protected Throwable clientReadyForChunksFutureStack;
|
||||
@Nullable
|
||||
protected CompletableFuture<Void> clientReadyForChunksFuture;
|
||||
@Nonnull
|
||||
protected final PacketHandler.DisconnectReason disconnectReason = new PacketHandler.DisconnectReason();
|
||||
|
||||
public PacketHandler(@Nonnull Channel channel, @Nonnull ProtocolVersion protocolVersion) {
|
||||
this.channel = channel;
|
||||
this.protocolVersion = protocolVersion;
|
||||
this.pingInfo = new PacketHandler.PingInfo[PongType.VALUES.length];
|
||||
|
||||
for (PongType pingType : PongType.VALUES) {
|
||||
this.pingInfo[pingType.ordinal()] = new PacketHandler.PingInfo(pingType);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Channel getChannel() {
|
||||
return this.channel;
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
public void setCompressionEnabled(boolean compressionEnabled) {
|
||||
HytaleLogger.getLogger().at(Level.INFO).log(this.getIdentifier() + " compression now handled by encoder");
|
||||
}
|
||||
|
||||
@Deprecated(forRemoval = true)
|
||||
public boolean isCompressionEnabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public abstract String getIdentifier();
|
||||
|
||||
@Nonnull
|
||||
public ProtocolVersion getProtocolVersion() {
|
||||
return this.protocolVersion;
|
||||
}
|
||||
|
||||
public final void registered(@Nullable PacketHandler oldHandler) {
|
||||
this.registered = true;
|
||||
this.registered0(oldHandler);
|
||||
}
|
||||
|
||||
protected void registered0(@Nullable PacketHandler oldHandler) {
|
||||
}
|
||||
|
||||
public final void unregistered(@Nullable PacketHandler newHandler) {
|
||||
this.registered = false;
|
||||
this.clearTimeout();
|
||||
this.unregistered0(newHandler);
|
||||
}
|
||||
|
||||
protected void unregistered0(@Nullable PacketHandler newHandler) {
|
||||
}
|
||||
|
||||
public void handle(@Nonnull Packet packet) {
|
||||
this.accept(packet);
|
||||
}
|
||||
|
||||
public abstract void accept(@Nonnull Packet var1);
|
||||
|
||||
public void logCloseMessage() {
|
||||
HytaleLogger.getLogger().at(Level.INFO).log("%s was closed.", this.getIdentifier());
|
||||
}
|
||||
|
||||
public void closed(ChannelHandlerContext ctx) {
|
||||
this.clearTimeout();
|
||||
}
|
||||
|
||||
public void setQueuePackets(boolean queuePackets) {
|
||||
this.queuePackets = queuePackets;
|
||||
}
|
||||
|
||||
public void tryFlush() {
|
||||
if (this.queuedPackets.getAndSet(0) > 0) {
|
||||
this.channel.flush();
|
||||
}
|
||||
}
|
||||
|
||||
public void write(@Nonnull Packet... packets) {
|
||||
Packet[] cachedPackets = new Packet[packets.length];
|
||||
this.handleOutboundAndCachePackets(packets, cachedPackets);
|
||||
if (this.queuePackets) {
|
||||
this.channel.write(cachedPackets, this.channel.voidPromise());
|
||||
this.queuedPackets.getAndIncrement();
|
||||
} else {
|
||||
this.channel.writeAndFlush(cachedPackets, this.channel.voidPromise());
|
||||
}
|
||||
}
|
||||
|
||||
public void write(@Nonnull Packet[] packets, @Nonnull Packet finalPacket) {
|
||||
Packet[] cachedPackets = new Packet[packets.length + 1];
|
||||
this.handleOutboundAndCachePackets(packets, cachedPackets);
|
||||
cachedPackets[cachedPackets.length - 1] = this.handleOutboundAndCachePacket(finalPacket);
|
||||
if (this.queuePackets) {
|
||||
this.channel.write(cachedPackets, this.channel.voidPromise());
|
||||
this.queuedPackets.getAndIncrement();
|
||||
} else {
|
||||
this.channel.writeAndFlush(cachedPackets, this.channel.voidPromise());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void write(@Nonnull Packet packet) {
|
||||
this.writePacket(packet, true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void writeNoCache(@Nonnull Packet packet) {
|
||||
this.writePacket(packet, false);
|
||||
}
|
||||
|
||||
public void writePacket(@Nonnull Packet packet, boolean cache) {
|
||||
if (!PacketAdapters.__handleOutbound(this, packet)) {
|
||||
Packet toSend;
|
||||
if (cache) {
|
||||
toSend = this.handleOutboundAndCachePacket(packet);
|
||||
} else {
|
||||
toSend = packet;
|
||||
}
|
||||
|
||||
if (this.queuePackets) {
|
||||
this.channel.write(toSend, this.channel.voidPromise());
|
||||
this.queuedPackets.getAndIncrement();
|
||||
} else {
|
||||
this.channel.writeAndFlush(toSend, this.channel.voidPromise());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void handleOutboundAndCachePackets(@Nonnull Packet[] packets, @Nonnull Packet[] cachedPackets) {
|
||||
for (int i = 0; i < packets.length; i++) {
|
||||
Packet packet = packets[i];
|
||||
if (!PacketAdapters.__handleOutbound(this, packet)) {
|
||||
cachedPackets[i] = this.handleOutboundAndCachePacket(packet);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Packet handleOutboundAndCachePacket(@Nonnull Packet packet) {
|
||||
return (Packet) (packet instanceof CachedPacket ? packet : CachedPacket.cache(packet));
|
||||
}
|
||||
|
||||
public void disconnect(@Nonnull String message) {
|
||||
this.disconnectReason.setServerDisconnectReason(message);
|
||||
HytaleLogger.getLogger().at(Level.INFO).log("Disconnecting %s with the message: %s", NettyUtil.formatRemoteAddress(this.channel), message);
|
||||
this.disconnect0(message);
|
||||
}
|
||||
|
||||
protected void disconnect0(@Nonnull String message) {
|
||||
this.channel.writeAndFlush(new Disconnect(message, DisconnectType.Disconnect)).addListener(ProtocolUtil.CLOSE_ON_COMPLETE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PacketStatsRecorder getPacketStatsRecorder() {
|
||||
return this.channel.attr(PacketStatsRecorder.CHANNEL_KEY).get();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PacketHandler.PingInfo getPingInfo(@Nonnull PongType pongType) {
|
||||
return this.pingInfo[pongType.ordinal()];
|
||||
}
|
||||
|
||||
public long getOperationTimeoutThreshold() {
|
||||
double average = this.getPingInfo(PongType.Tick).getPingMetricSet().getAverage(0);
|
||||
return PacketHandler.PingInfo.TIME_UNIT.toMillis(Math.round(average * 2.0)) + 3000L;
|
||||
}
|
||||
|
||||
public void tickPing(float dt) {
|
||||
this.pingTimer -= dt;
|
||||
if (this.pingTimer <= 0.0F) {
|
||||
this.pingTimer = 1.0F;
|
||||
this.sendPing();
|
||||
}
|
||||
}
|
||||
|
||||
public void sendPing() {
|
||||
int id = this.pingIdRandom.nextInt();
|
||||
Instant nowInstant = Instant.now();
|
||||
long nowTimestamp = System.nanoTime();
|
||||
|
||||
for (PacketHandler.PingInfo info : this.pingInfo) {
|
||||
info.recordSent(id, nowTimestamp);
|
||||
}
|
||||
|
||||
this.writeNoCache(
|
||||
new Ping(
|
||||
id,
|
||||
WorldTimeResource.instantToInstantData(nowInstant),
|
||||
(int) this.getPingInfo(PongType.Raw).getPingMetricSet().getLastValue(),
|
||||
(int) this.getPingInfo(PongType.Direct).getPingMetricSet().getLastValue(),
|
||||
(int) this.getPingInfo(PongType.Tick).getPingMetricSet().getLastValue()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public void handlePong(@Nonnull Pong packet) {
|
||||
this.pingInfo[packet.type.ordinal()].handlePacket(packet);
|
||||
}
|
||||
|
||||
protected void initStage(@Nonnull String stage, @Nonnull Duration timeout, @Nonnull BooleanSupplier condition) {
|
||||
NettyUtil.TimeoutContext.init(this.channel, stage, this.getIdentifier());
|
||||
this.setStageTimeout(stage, timeout, condition);
|
||||
}
|
||||
|
||||
protected void enterStage(@Nonnull String stage, @Nonnull Duration timeout, @Nonnull BooleanSupplier condition) {
|
||||
NettyUtil.TimeoutContext.update(this.channel, stage, this.getIdentifier());
|
||||
this.updatePacketTimeout(timeout);
|
||||
this.setStageTimeout(stage, timeout, condition);
|
||||
}
|
||||
|
||||
protected void enterStage(@Nonnull String stage, @Nonnull Duration timeout) {
|
||||
NettyUtil.TimeoutContext.update(this.channel, stage, this.getIdentifier());
|
||||
this.updatePacketTimeout(timeout);
|
||||
}
|
||||
|
||||
protected void continueStage(@Nonnull String stage, @Nonnull Duration timeout, @Nonnull BooleanSupplier condition) {
|
||||
NettyUtil.TimeoutContext.update(this.channel, stage);
|
||||
this.updatePacketTimeout(timeout);
|
||||
this.setStageTimeout(stage, timeout, condition);
|
||||
}
|
||||
|
||||
private void setStageTimeout(@Nonnull String stageId, @Nonnull Duration timeout, @Nonnull BooleanSupplier meets) {
|
||||
if (this.timeoutTask != null) {
|
||||
this.timeoutTask.cancel(false);
|
||||
}
|
||||
|
||||
if (this instanceof AuthenticationPacketHandler || !(this instanceof PasswordPacketHandler) || this.auth != null) {
|
||||
logConnectionTimings(this.channel, "Entering stage '" + stageId + "'", Level.FINEST);
|
||||
long timeoutMillis = timeout.toMillis();
|
||||
this.timeoutTask = this.channel
|
||||
.eventLoop()
|
||||
.schedule(
|
||||
() -> {
|
||||
if (this.channel.isOpen()) {
|
||||
if (!meets.getAsBoolean()) {
|
||||
NettyUtil.TimeoutContext context = this.channel.attr(NettyUtil.TimeoutContext.KEY).get();
|
||||
String duration = context != null ? FormatUtil.nanosToString(System.nanoTime() - context.connectionStartNs()) : "unknown";
|
||||
HytaleLogger.getLogger()
|
||||
.at(Level.WARNING)
|
||||
.log("Stage timeout for %s at stage '%s' after %s connected", this.getIdentifier(), stageId, duration);
|
||||
this.disconnect("Either you took too long to login or we took too long to process your request! Retry again in a moment.");
|
||||
}
|
||||
}
|
||||
},
|
||||
timeoutMillis,
|
||||
TimeUnit.MILLISECONDS
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private void updatePacketTimeout(@Nonnull Duration timeout) {
|
||||
this.channel.attr(ProtocolUtil.PACKET_TIMEOUT_KEY).set(timeout);
|
||||
}
|
||||
|
||||
protected void clearTimeout() {
|
||||
if (this.timeoutTask != null) {
|
||||
this.timeoutTask.cancel(false);
|
||||
}
|
||||
|
||||
if (this.clientReadyForChunksFuture != null) {
|
||||
this.clientReadyForChunksFuture.cancel(true);
|
||||
this.clientReadyForChunksFuture = null;
|
||||
this.clientReadyForChunksFutureStack = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerAuthentication getAuth() {
|
||||
return this.auth;
|
||||
}
|
||||
|
||||
public boolean stillActive() {
|
||||
return this.channel.isActive();
|
||||
}
|
||||
|
||||
public int getQueuedPacketsCount() {
|
||||
return this.queuedPackets.get();
|
||||
}
|
||||
|
||||
public boolean isLocalConnection() {
|
||||
SocketAddress socketAddress;
|
||||
if (this.channel instanceof QuicStreamChannel quicStreamChannel) {
|
||||
socketAddress = quicStreamChannel.parent().remoteSocketAddress();
|
||||
} else {
|
||||
socketAddress = this.channel.remoteAddress();
|
||||
}
|
||||
|
||||
if (socketAddress instanceof InetSocketAddress) {
|
||||
InetAddress address = ((InetSocketAddress) socketAddress).getAddress();
|
||||
return NetworkUtil.addressMatchesAny(address, NetworkUtil.AddressType.ANY_LOCAL, NetworkUtil.AddressType.LOOPBACK);
|
||||
} else {
|
||||
return socketAddress instanceof DomainSocketAddress || socketAddress instanceof LocalAddress;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isLANConnection() {
|
||||
SocketAddress socketAddress;
|
||||
if (this.channel instanceof QuicStreamChannel quicStreamChannel) {
|
||||
socketAddress = quicStreamChannel.parent().remoteSocketAddress();
|
||||
} else {
|
||||
socketAddress = this.channel.remoteAddress();
|
||||
}
|
||||
|
||||
if (socketAddress instanceof InetSocketAddress) {
|
||||
InetAddress address = ((InetSocketAddress) socketAddress).getAddress();
|
||||
return NetworkUtil.addressMatchesAny(address);
|
||||
} else {
|
||||
return socketAddress instanceof DomainSocketAddress || socketAddress instanceof LocalAddress;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public PacketHandler.DisconnectReason getDisconnectReason() {
|
||||
return this.disconnectReason;
|
||||
}
|
||||
|
||||
public void setClientReadyForChunksFuture(@Nonnull CompletableFuture<Void> clientReadyFuture) {
|
||||
if (this.clientReadyForChunksFuture != null) {
|
||||
throw new IllegalStateException("Tried to hook client ready but something is already waiting for it!", this.clientReadyForChunksFutureStack);
|
||||
} else {
|
||||
HytaleLogger.getLogger().at(Level.WARNING).log("%s Added future for ClientReady packet?", this.getIdentifier());
|
||||
this.clientReadyForChunksFutureStack = new Throwable();
|
||||
this.clientReadyForChunksFuture = clientReadyFuture;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public CompletableFuture<Void> getClientReadyForChunksFuture() {
|
||||
return this.clientReadyForChunksFuture;
|
||||
}
|
||||
|
||||
public static void logConnectionTimings(@Nonnull Channel channel, @Nonnull String message, @Nonnull Level level) {
|
||||
Attribute<Long> loginStartAttribute = channel.attr(LOGIN_START_ATTRIBUTE_KEY);
|
||||
long now = System.nanoTime();
|
||||
Long before = loginStartAttribute.getAndSet(now);
|
||||
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<PacketHandler.PingInfo> METRICS_REGISTRY = new MetricsRegistry<PacketHandler.PingInfo>()
|
||||
.register("PingType", pingInfo -> pingInfo.pingType, new EnumCodec<>(PongType.class))
|
||||
.register("PingMetrics", PacketHandler.PingInfo::getPingMetricSet, HistoricMetric.METRICS_CODEC)
|
||||
.register("PacketQueueMin", pingInfo -> pingInfo.packetQueueMetric.getMin(), Codec.LONG)
|
||||
.register("PacketQueueAvg", pingInfo -> pingInfo.packetQueueMetric.getAverage(), Codec.DOUBLE)
|
||||
.register("PacketQueueMax", pingInfo -> pingInfo.packetQueueMetric.getMax(), Codec.LONG);
|
||||
public static final TimeUnit TIME_UNIT = TimeUnit.MICROSECONDS;
|
||||
public static final int ONE_SECOND_INDEX = 0;
|
||||
public static final int ONE_MINUTE_INDEX = 1;
|
||||
public static final int FIVE_MINUTE_INDEX = 2;
|
||||
public static final double PERCENTILE = 0.99F;
|
||||
public static final int PING_FREQUENCY = 1;
|
||||
public static final TimeUnit PING_FREQUENCY_UNIT = TimeUnit.SECONDS;
|
||||
public static final int PING_FREQUENCY_MILLIS = 1000;
|
||||
public static final int PING_HISTORY_MILLIS = 15000;
|
||||
public static final int PING_HISTORY_LENGTH = 15;
|
||||
protected final PongType pingType;
|
||||
protected final Lock queueLock = new ReentrantLock();
|
||||
protected final IntPriorityQueue pingIdQueue = new IntArrayFIFOQueue(15);
|
||||
protected final LongPriorityQueue pingTimestampQueue = new LongArrayFIFOQueue(15);
|
||||
protected final Lock pingLock = new ReentrantLock();
|
||||
@Nonnull
|
||||
protected final HistoricMetric pingMetricSet;
|
||||
protected final Metric packetQueueMetric = new Metric();
|
||||
|
||||
public PingInfo(PongType pingType) {
|
||||
this.pingType = pingType;
|
||||
this.pingMetricSet = HistoricMetric.builder(1000L, TimeUnit.MILLISECONDS)
|
||||
.addPeriod(1L, TimeUnit.SECONDS)
|
||||
.addPeriod(1L, TimeUnit.MINUTES)
|
||||
.addPeriod(5L, TimeUnit.MINUTES)
|
||||
.build();
|
||||
}
|
||||
|
||||
protected void recordSent(int id, long timestamp) {
|
||||
this.queueLock.lock();
|
||||
|
||||
try {
|
||||
this.pingIdQueue.enqueue(id);
|
||||
this.pingTimestampQueue.enqueue(timestamp);
|
||||
} finally {
|
||||
this.queueLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
protected void handlePacket(@Nonnull Pong packet) {
|
||||
if (packet.type != this.pingType) {
|
||||
throw new IllegalArgumentException("Got packet for " + packet.type + " but expected " + this.pingType);
|
||||
} else {
|
||||
this.queueLock.lock();
|
||||
|
||||
int nextIdToHandle;
|
||||
long sentTimestamp;
|
||||
try {
|
||||
nextIdToHandle = this.pingIdQueue.dequeueInt();
|
||||
sentTimestamp = this.pingTimestampQueue.dequeueLong();
|
||||
} finally {
|
||||
this.queueLock.unlock();
|
||||
}
|
||||
|
||||
if (packet.id != nextIdToHandle) {
|
||||
throw new IllegalArgumentException(String.valueOf(packet.id));
|
||||
} else {
|
||||
long nanoTime = System.nanoTime();
|
||||
long pingValue = nanoTime - sentTimestamp;
|
||||
if (pingValue <= 0L) {
|
||||
throw new IllegalArgumentException(String.format("Ping must be received after its sent! %s", pingValue));
|
||||
} else {
|
||||
this.pingLock.lock();
|
||||
|
||||
try {
|
||||
this.pingMetricSet.add(nanoTime, TIME_UNIT.convert(pingValue, TimeUnit.NANOSECONDS));
|
||||
this.packetQueueMetric.add(packet.packetQueueSize);
|
||||
} finally {
|
||||
this.pingLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PongType getPingType() {
|
||||
return this.pingType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Metric getPacketQueueMetric() {
|
||||
return this.packetQueueMetric;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public HistoricMetric getPingMetricSet() {
|
||||
return this.pingMetricSet;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.pingLock.lock();
|
||||
|
||||
try {
|
||||
this.packetQueueMetric.clear();
|
||||
this.pingMetricSet.clear();
|
||||
} finally {
|
||||
this.pingLock.unlock();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EntityStore> FIND_VISIBLE_ENTITIES_GROUP = EntityStore.REGISTRY.registerSystemGroup();
|
||||
public static final SystemGroup<EntityStore> QUEUE_UPDATE_GROUP = EntityStore.REGISTRY.registerSystemGroup();
|
||||
|
||||
public EntityTrackerSystems() {
|
||||
}
|
||||
|
||||
public static boolean despawnAll(@Nonnull Ref<EntityStore> viewerRef, @Nonnull Store<EntityStore> store) {
|
||||
EntityTrackerSystems.EntityViewer viewer = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (viewer == null) {
|
||||
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<EntityStore> viewerRef, @Nonnull Store<EntityStore> store) {
|
||||
EntityTrackerSystems.EntityViewer viewer = store.getComponent(viewerRef, EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (viewer == null) {
|
||||
return false;
|
||||
} else {
|
||||
for (Ref<EntityStore> ref : viewer.sent.keySet()) {
|
||||
EntityTrackerSystems.Visible visible = store.getComponent(ref, EntityTrackerSystems.Visible.getComponentType());
|
||||
if (visible != null) {
|
||||
visible.visibleTo.remove(viewerRef);
|
||||
}
|
||||
}
|
||||
|
||||
viewer.sent.clear();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static class AddToVisible extends EntityTickingSystem<EntityStore> {
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Collections.singleton(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.EnsureVisibleComponent.class)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
|
||||
public AddToVisible(
|
||||
ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType,
|
||||
ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType
|
||||
) {
|
||||
this.entityViewerComponentType = entityViewerComponentType;
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> 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<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
Ref<EntityStore> viewerRef = archetypeChunk.getReferenceTo(index);
|
||||
EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.entityViewerComponentType);
|
||||
|
||||
for (Ref<EntityStore> ref : viewer.visible) {
|
||||
commandBuffer.getComponent(ref, this.visibleComponentType).addViewerParallel(viewerRef, viewer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ClearEntityViewers extends EntityTickingSystem<EntityStore> {
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Collections.singleton(
|
||||
new SystemGroupDependency<>(Order.BEFORE, EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType;
|
||||
|
||||
public ClearEntityViewers(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType) {
|
||||
this.componentType = componentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> 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<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityTrackerSystems.EntityViewer viewer = archetypeChunk.getComponent(index, this.componentType);
|
||||
viewer.visible.clear();
|
||||
viewer.lodExcludedCount = 0;
|
||||
viewer.hiddenCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public static class ClearPreviouslyVisible extends EntityTickingSystem<EntityStore> {
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.ClearEntityViewers.class),
|
||||
new SystemGroupDependency<EntityStore>(Order.AFTER, EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
|
||||
public ClearPreviouslyVisible(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> 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<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityTrackerSystems.Visible visible = archetypeChunk.getComponent(index, this.componentType);
|
||||
Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> oldVisibleTo = visible.previousVisibleTo;
|
||||
visible.previousVisibleTo = visible.visibleTo;
|
||||
visible.visibleTo = oldVisibleTo;
|
||||
visible.visibleTo.clear();
|
||||
visible.newlyVisibleTo.clear();
|
||||
}
|
||||
}
|
||||
|
||||
public static class CollectVisible extends EntityTickingSystem<EntityStore> {
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType;
|
||||
private final Query<EntityStore> query;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
|
||||
public CollectVisible(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType) {
|
||||
this.componentType = componentType;
|
||||
this.query = Archetype.of(componentType, TransformComponent.getComponentType());
|
||||
this.dependencies = Collections.singleton(new SystemDependency<>(Order.AFTER, NetworkSendableSpatialSystem.class));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<EntityStore> getGroup() {
|
||||
return EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
TransformComponent transform = archetypeChunk.getComponent(index, TransformComponent.getComponentType());
|
||||
Vector3d position = transform.getPosition();
|
||||
EntityTrackerSystems.EntityViewer entityViewer = archetypeChunk.getComponent(index, this.componentType);
|
||||
SpatialStructure<Ref<EntityStore>> spatialStructure = store.getResource(EntityModule.get().getNetworkSendableSpatialResourceType())
|
||||
.getSpatialStructure();
|
||||
ObjectList<Ref<EntityStore>> results = SpatialResource.getThreadLocalReferenceList();
|
||||
spatialStructure.collect(position, entityViewer.viewRadiusBlocks, results);
|
||||
entityViewer.visible.addAll(results);
|
||||
}
|
||||
}
|
||||
|
||||
public static class EffectControllerSystem extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, EffectControllerComponent> effectControllerComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public EffectControllerSystem(
|
||||
@Nonnull ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType,
|
||||
@Nonnull ComponentType<EntityStore, EffectControllerComponent> effectControllerComponentType
|
||||
) {
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
this.effectControllerComponentType = effectControllerComponentType;
|
||||
this.query = Query.and(visibleComponentType, effectControllerComponentType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<EntityStore> getGroup() {
|
||||
return EntityTrackerSystems.QUEUE_UPDATE_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityTrackerSystems.Visible visibleComponent = archetypeChunk.getComponent(index, this.visibleComponentType);
|
||||
|
||||
assert visibleComponent != null;
|
||||
|
||||
Ref<EntityStore> 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<EntityStore> ref,
|
||||
@Nonnull EffectControllerComponent effectControllerComponent,
|
||||
@Nonnull Map<Ref<EntityStore>, 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<EntityStore> ref,
|
||||
@Nonnull EffectControllerComponent effectControllerComponent,
|
||||
@Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo,
|
||||
@Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> exclude
|
||||
) {
|
||||
ComponentUpdate update = new ComponentUpdate();
|
||||
update.type = ComponentUpdateType.EntityEffects;
|
||||
update.entityEffectUpdates = effectControllerComponent.consumeChanges();
|
||||
if (!exclude.isEmpty()) {
|
||||
for (Entry<Ref<EntityStore>, 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<EntityStore> {
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Collections.singleton(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.ClearPreviouslyVisible.class)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
|
||||
public EnsureVisibleComponent(
|
||||
ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType,
|
||||
ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType
|
||||
) {
|
||||
this.entityViewerComponentType = entityViewerComponentType;
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> 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<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
for (Ref<EntityStore> ref : archetypeChunk.getComponent(index, this.entityViewerComponentType).visible) {
|
||||
if (!commandBuffer.getArchetype(ref).contains(this.visibleComponentType)) {
|
||||
commandBuffer.ensureComponent(ref, this.visibleComponentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class EntityUpdate {
|
||||
@Nonnull
|
||||
private final StampedLock removeLock = new StampedLock();
|
||||
@Nonnull
|
||||
private final EnumSet<ComponentUpdateType> removed;
|
||||
@Nonnull
|
||||
private final StampedLock updatesLock = new StampedLock();
|
||||
@Nonnull
|
||||
private final List<ComponentUpdate> 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<EntityStore> {
|
||||
private static final float VISIBILITY_UPDATE_INTERVAL = 0.2f;
|
||||
|
||||
public int viewRadiusBlocks;
|
||||
public IPacketReceiver packetReceiver;
|
||||
public Set<Ref<EntityStore>> visible;
|
||||
public Map<Ref<EntityStore>, EntityTrackerSystems.EntityUpdate> updates;
|
||||
public Object2IntMap<Ref<EntityStore>> sent;
|
||||
public int lodExcludedCount;
|
||||
public int hiddenCount;
|
||||
public float visibilityTimeAccumulator;
|
||||
|
||||
public static ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> 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<Ref<EntityStore>, 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<EntityStore> clone() {
|
||||
return new EntityTrackerSystems.EntityViewer(this);
|
||||
}
|
||||
|
||||
public void queueRemove(Ref<EntityStore> 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<EntityStore> 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<EntityStore> {
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, EntityTrackerSystems.AddToVisible.class),
|
||||
new SystemGroupDependency<EntityStore>(Order.BEFORE, EntityTrackerSystems.QUEUE_UPDATE_GROUP)
|
||||
);
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
|
||||
public RemoveEmptyVisibleComponent(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> 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<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
if (archetypeChunk.getComponent(index, this.componentType).visibleTo.isEmpty()) {
|
||||
commandBuffer.removeComponent(archetypeChunk.getReferenceTo(index), this.componentType);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class RemoveVisibleComponent extends HolderSystem<EntityStore> {
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
|
||||
public RemoveVisibleComponent(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.componentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdd(@Nonnull Holder<EntityStore> holder, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemoved(@Nonnull Holder<EntityStore> holder, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store) {
|
||||
holder.removeComponent(this.componentType);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SendPackets extends EntityTickingSystem<EntityStore> {
|
||||
public static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final ThreadLocal<IntList> INT_LIST_THREAD_LOCAL = ThreadLocal.withInitial(IntArrayList::new);
|
||||
public static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(new SystemGroupDependency<>(Order.AFTER, EntityTrackerSystems.QUEUE_UPDATE_GROUP));
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType;
|
||||
|
||||
public SendPackets(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> componentType) {
|
||||
this.componentType = componentType;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<EntityStore> getGroup() {
|
||||
return EntityStore.SEND_PACKET_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<EntityStore> 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<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> 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<it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<Ref<EntityStore>>> iterator = viewer.sent.object2IntEntrySet().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
it.unimi.dsi.fastutil.objects.Object2IntMap.Entry<Ref<EntityStore>> entry = iterator.next();
|
||||
Ref<EntityStore> ref = entry.getKey();
|
||||
if (!ref.isValid() || !viewer.visible.contains(ref)) {
|
||||
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<Ref<EntityStore>> iteratorx = viewer.updates.keySet().iterator();
|
||||
|
||||
while (iteratorx.hasNext()) {
|
||||
Ref<EntityStore> 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<Ref<EntityStore>, 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<EntityStore> {
|
||||
@Nonnull
|
||||
private final StampedLock lock = new StampedLock();
|
||||
@Nonnull
|
||||
public Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> previousVisibleTo = new Object2ObjectOpenHashMap<>();
|
||||
@Nonnull
|
||||
public Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo = new Object2ObjectOpenHashMap<>();
|
||||
@Nonnull
|
||||
public Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> newlyVisibleTo = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
public Visible() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static ComponentType<EntityStore, EntityTrackerSystems.Visible> getComponentType() {
|
||||
return EntityModule.get().getVisibleComponentType();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<EntityStore> clone() {
|
||||
return new EntityTrackerSystems.Visible();
|
||||
}
|
||||
|
||||
public void addViewerParallel(Ref<EntityStore> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EntityStore> holder) {
|
||||
World world = player.getWorld();
|
||||
if (world != null && world.isInThread()) {
|
||||
Ref<EntityStore> 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<EntityStore> {
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
private final ComponentType<EntityStore, ModelComponent> modelComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public LegacyEntityModel(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
this.modelComponentType = ModelComponent.getComponentType();
|
||||
this.query = Query.and(componentType, this.modelComponentType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<EntityStore> getGroup() {
|
||||
return EntityTrackerSystems.QUEUE_UPDATE_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
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<EntityStore> ref, @Nullable ModelComponent model, float entityScale, @Nonnull Map<Ref<EntityStore>, EntityTrackerSystems.EntityViewer> visibleTo
|
||||
) {
|
||||
ComponentUpdate update = new ComponentUpdate();
|
||||
update.type = ComponentUpdateType.Model;
|
||||
update.model = model != null ? model.getModel().toPacket() : null;
|
||||
update.entityScale = entityScale;
|
||||
|
||||
for (EntityTrackerSystems.EntityViewer viewer : visibleTo.values()) {
|
||||
viewer.queueUpdate(ref, update);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class LegacyEntitySkin extends EntityTickingSystem<EntityStore> {
|
||||
private final ComponentType<EntityStore, PlayerSkinComponent> playerSkinComponentComponentType;
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public LegacyEntitySkin(
|
||||
ComponentType<EntityStore, EntityTrackerSystems.Visible> visibleComponentType,
|
||||
ComponentType<EntityStore, PlayerSkinComponent> playerSkinComponentComponentType
|
||||
) {
|
||||
this.visibleComponentType = visibleComponentType;
|
||||
this.playerSkinComponentComponentType = playerSkinComponentComponentType;
|
||||
this.query = Query.and(visibleComponentType, playerSkinComponentComponentType);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<EntityStore> getGroup() {
|
||||
return EntityTrackerSystems.QUEUE_UPDATE_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
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<EntityStore> ref, @Nonnull PlayerSkinComponent component, @Nonnull Map<Ref<EntityStore>, 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<EntityStore> {
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public LegacyEquipment(ComponentType<EntityStore, EntityTrackerSystems.Visible> componentType) {
|
||||
this.componentType = componentType;
|
||||
this.query = Query.and(componentType, AllLegacyLivingEntityTypesQuery.INSTANCE);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<EntityStore> getGroup() {
|
||||
return EntityTrackerSystems.QUEUE_UPDATE_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
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<EntityStore> ref, @Nonnull LivingEntity entity, @Nonnull Map<Ref<EntityStore>, 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<EntityStore> 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<EntityStore> {
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
private final ComponentType<EntityStore, PlayerSettings> playerSettingsComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
|
||||
public LegacyHideFromEntity(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> 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<EntityStore> getGroup() {
|
||||
return EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
Ref<EntityStore> 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<Ref<EntityStore>> iterator = entityViewerComponent.visible.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Ref<EntityStore> 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<EntityStore> {
|
||||
public static final double ENTITY_LOD_RATIO_DEFAULT = 3.5E-5;
|
||||
public static double ENTITY_LOD_RATIO = 3.5E-5;
|
||||
private final ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> entityViewerComponentType;
|
||||
private final ComponentType<EntityStore, BoundingBox> boundingBoxComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
|
||||
public LegacyLODCull(ComponentType<EntityStore, EntityTrackerSystems.EntityViewer> 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<EntityStore> getGroup() {
|
||||
return EntityTrackerSystems.FIND_VISIBLE_ENTITIES_GROUP;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
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<Ref<EntityStore>> iterator = entityViewerComponent.visible.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
Ref<EntityStore> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ChunkStore> {
|
||||
public static final BuilderCodec<TrackedPlacement> CODEC = BuilderCodec.builder(TrackedPlacement.class, TrackedPlacement::new)
|
||||
.append(new KeyedCodec<>("BlockName", Codec.STRING), (o, v) -> o.blockName = v, o -> o.blockName)
|
||||
.add()
|
||||
.build();
|
||||
private String blockName;
|
||||
|
||||
public static ComponentType<ChunkStore, TrackedPlacement> getComponentType() {
|
||||
return InteractionModule.get().getTrackedPlacementComponentType();
|
||||
}
|
||||
|
||||
public TrackedPlacement() {
|
||||
}
|
||||
|
||||
public TrackedPlacement(String blockName) {
|
||||
this.blockName = blockName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Component<ChunkStore> clone() {
|
||||
return new TrackedPlacement(this.blockName);
|
||||
}
|
||||
|
||||
public static class OnAddRemove extends RefSystem<ChunkStore> {
|
||||
private static final ComponentType<ChunkStore, TrackedPlacement> COMPONENT_TYPE = TrackedPlacement.getComponentType();
|
||||
private static final ResourceType<ChunkStore, BlockCounter> BLOCK_COUNTER_RESOURCE_TYPE = BlockCounter.getResourceType();
|
||||
|
||||
public OnAddRemove() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<ChunkStore> ref, @Nonnull AddReason reason, @Nonnull Store<ChunkStore> store, @Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
if (reason == AddReason.SPAWN) {
|
||||
BlockModule.BlockStateInfo blockInfo = commandBuffer.getComponent(ref, BlockModule.BlockStateInfo.getComponentType());
|
||||
|
||||
assert blockInfo != null;
|
||||
|
||||
Ref<ChunkStore> chunkRef = blockInfo.getChunkRef();
|
||||
if (chunkRef != null && chunkRef.isValid()) {
|
||||
BlockChunk blockChunk = commandBuffer.getComponent(chunkRef, BlockChunk.getComponentType());
|
||||
|
||||
assert blockChunk != null;
|
||||
|
||||
int blockId = blockChunk.getBlock(
|
||||
ChunkUtil.xFromBlockInColumn(blockInfo.getIndex()),
|
||||
ChunkUtil.yFromBlockInColumn(blockInfo.getIndex()),
|
||||
ChunkUtil.zFromBlockInColumn(blockInfo.getIndex())
|
||||
);
|
||||
BlockType blockType = BlockType.getAssetMap().getAsset(blockId);
|
||||
if (blockType != null) {
|
||||
BlockCounter counter = commandBuffer.getResource(BLOCK_COUNTER_RESOURCE_TYPE);
|
||||
String blockName = blockType.getId();
|
||||
counter.trackBlock(blockName);
|
||||
TrackedPlacement tracked = commandBuffer.getComponent(ref, COMPONENT_TYPE);
|
||||
|
||||
assert tracked != null;
|
||||
|
||||
tracked.blockName = blockName;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<ChunkStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<ChunkStore> store, @Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
if (reason == RemoveReason.REMOVE) {
|
||||
TrackedPlacement tracked = commandBuffer.getComponent(ref, COMPONENT_TYPE);
|
||||
|
||||
// HyFix #11: Add null check for tracked component
|
||||
if (tracked == null) {
|
||||
System.out.println("[HyFix] WARNING: TrackedPlacement component was null on entity remove - BlockCounter not decremented");
|
||||
return;
|
||||
}
|
||||
|
||||
String blockName = tracked.blockName;
|
||||
|
||||
// HyFix #11: Add null/empty check for blockName
|
||||
if (blockName == null || blockName.isEmpty()) {
|
||||
System.out.println("[HyFix] WARNING: TrackedPlacement.blockName was null/empty on entity remove - BlockCounter not decremented");
|
||||
return;
|
||||
}
|
||||
|
||||
BlockCounter counter = commandBuffer.getResource(BLOCK_COUNTER_RESOURCE_TYPE);
|
||||
counter.untrackBlock(blockName);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return COMPONENT_TYPE;
|
||||
}
|
||||
}
|
||||
}
|
||||
962
src/com/hypixel/hytale/server/core/universe/Universe.java
Normal file
962
src/com/hypixel/hytale/server/core/universe/Universe.java
Normal file
@@ -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<Integer, String> LEGACY_BLOCK_ID_MAP = Collections.emptyMap();
|
||||
@Nonnull
|
||||
public static final MetricsRegistry<Universe> METRICS_REGISTRY = new MetricsRegistry<Universe>()
|
||||
.register("Worlds", universe -> universe.getWorlds().values().toArray(World[]::new), new ArrayCodec<>(World.METRICS_REGISTRY, World[]::new))
|
||||
.register("PlayerCount", Universe::getPlayerCount, Codec.INTEGER);
|
||||
private static Universe instance;
|
||||
private ComponentType<EntityStore, PlayerRef> playerRefComponentType;
|
||||
@Nonnull
|
||||
private final Path path = Constants.UNIVERSE_PATH;
|
||||
@Nonnull
|
||||
private final Map<UUID, PlayerRef> players = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Map<String, World> worlds = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Map<UUID, World> worldsByUuid = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Map<String, World> unmodifiableWorlds = Collections.unmodifiableMap(this.worlds);
|
||||
private PlayerStorage playerStorage;
|
||||
private WorldConfigProvider worldConfigProvider;
|
||||
private ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> indexedStorageCacheResourceType;
|
||||
private CompletableFuture<Void> universeReady;
|
||||
|
||||
public static Universe get() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
public Universe(@Nonnull JavaPluginInit init) {
|
||||
super(init);
|
||||
instance = this;
|
||||
if (!Files.isDirectory(this.path) && !Options.getOptionSet().has(Options.BARE)) {
|
||||
try {
|
||||
Files.createDirectories(this.path);
|
||||
} catch (IOException var3) {
|
||||
throw new RuntimeException("Failed to create universe directory", var3);
|
||||
}
|
||||
}
|
||||
|
||||
if (Options.getOptionSet().has(Options.BACKUP)) {
|
||||
int frequencyMinutes = Math.max(Options.getOptionSet().valueOf(Options.BACKUP_FREQUENCY_MINUTES), 1);
|
||||
this.getLogger().at(Level.INFO).log("Scheduled backup to run every %d minute(s)", frequencyMinutes);
|
||||
HytaleServer.SCHEDULED_EXECUTOR.scheduleWithFixedDelay(() -> {
|
||||
try {
|
||||
this.getLogger().at(Level.INFO).log("Backing up universe...");
|
||||
this.runBackup().thenAccept(aVoid -> this.getLogger().at(Level.INFO).log("Completed scheduled backup."));
|
||||
} catch (Exception var2x) {
|
||||
this.getLogger().at(Level.SEVERE).withCause(var2x).log("Error backing up universe");
|
||||
}
|
||||
}, frequencyMinutes, frequencyMinutes, TimeUnit.MINUTES);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Void> runBackup() {
|
||||
return CompletableFuture.allOf(this.worlds.values().stream().map(world -> CompletableFuture.<ChunkSavingSystems.Data>supplyAsync(() -> {
|
||||
Store<ChunkStore> componentStore = world.getChunkStore().getStore();
|
||||
ChunkSavingSystems.Data data = componentStore.getResource(ChunkStore.SAVE_RESOURCE);
|
||||
data.isSaving = false;
|
||||
return data;
|
||||
}, world).thenCompose(ChunkSavingSystems.Data::waitForSavingChunks)).toArray(CompletableFuture[]::new))
|
||||
.thenCompose(aVoid -> BackupTask.start(this.path, Options.getOptionSet().valueOf(Options.BACKUP_DIRECTORY)))
|
||||
.thenCompose(success -> CompletableFuture.allOf(this.worlds.values().stream().map(world -> CompletableFuture.runAsync(() -> {
|
||||
Store<ChunkStore> componentStore = world.getChunkStore().getStore();
|
||||
ChunkSavingSystems.Data data = componentStore.getResource(ChunkStore.SAVE_RESOURCE);
|
||||
data.isSaving = true;
|
||||
}, world)).toArray(CompletableFuture[]::new)).thenApply(aVoid -> success));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setup() {
|
||||
EventRegistry eventRegistry = this.getEventRegistry();
|
||||
ComponentRegistryProxy<ChunkStore> chunkStoreRegistry = this.getChunkStoreRegistry();
|
||||
ComponentRegistryProxy<EntityStore> entityStoreRegistry = this.getEntityStoreRegistry();
|
||||
CommandRegistry commandRegistry = this.getCommandRegistry();
|
||||
eventRegistry.register((short) -48, ShutdownEvent.class, event -> this.disconnectAllPLayers());
|
||||
eventRegistry.register((short) -32, ShutdownEvent.class, event -> this.shutdownAllWorlds());
|
||||
ISpawnProvider.CODEC.register("Global", GlobalSpawnProvider.class, GlobalSpawnProvider.CODEC);
|
||||
ISpawnProvider.CODEC.register("Individual", IndividualSpawnProvider.class, IndividualSpawnProvider.CODEC);
|
||||
ISpawnProvider.CODEC.register("FitToHeightMap", FitToHeightMapSpawnProvider.class, FitToHeightMapSpawnProvider.CODEC);
|
||||
IWorldGenProvider.CODEC.register("Flat", FlatWorldGenProvider.class, FlatWorldGenProvider.CODEC);
|
||||
IWorldGenProvider.CODEC.register("Dummy", DummyWorldGenProvider.class, DummyWorldGenProvider.CODEC);
|
||||
IWorldGenProvider.CODEC.register(Priority.DEFAULT, "Void", VoidWorldGenProvider.class, VoidWorldGenProvider.CODEC);
|
||||
IWorldMapProvider.CODEC.register("Disabled", DisabledWorldMapProvider.class, DisabledWorldMapProvider.CODEC);
|
||||
IWorldMapProvider.CODEC.register(Priority.DEFAULT, "WorldGen", WorldGenWorldMapProvider.class, WorldGenWorldMapProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register(Priority.DEFAULT, "Hytale", DefaultChunkStorageProvider.class, DefaultChunkStorageProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register("Migration", MigrationChunkStorageProvider.class, MigrationChunkStorageProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register("IndexedStorage", IndexedStorageChunkStorageProvider.class, IndexedStorageChunkStorageProvider.CODEC);
|
||||
IChunkStorageProvider.CODEC.register("Empty", EmptyChunkStorageProvider.class, EmptyChunkStorageProvider.CODEC);
|
||||
IResourceStorageProvider.CODEC.register(Priority.DEFAULT, "Hytale", DefaultResourceStorageProvider.class, DefaultResourceStorageProvider.CODEC);
|
||||
IResourceStorageProvider.CODEC.register("Disk", DiskResourceStorageProvider.class, DiskResourceStorageProvider.CODEC);
|
||||
IResourceStorageProvider.CODEC.register("Empty", EmptyResourceStorageProvider.class, EmptyResourceStorageProvider.CODEC);
|
||||
this.indexedStorageCacheResourceType = chunkStoreRegistry.registerResource(
|
||||
IndexedStorageChunkStorageProvider.IndexedStorageCache.class, IndexedStorageChunkStorageProvider.IndexedStorageCache::new
|
||||
);
|
||||
chunkStoreRegistry.registerSystem(new IndexedStorageChunkStorageProvider.IndexedStorageCacheSetupSystem());
|
||||
chunkStoreRegistry.registerSystem(new WorldPregenerateSystem());
|
||||
entityStoreRegistry.registerSystem(new WorldConfigSaveSystem());
|
||||
this.playerRefComponentType = entityStoreRegistry.registerComponent(PlayerRef.class, () -> {
|
||||
throw new UnsupportedOperationException();
|
||||
});
|
||||
entityStoreRegistry.registerSystem(new PlayerPingSystem());
|
||||
entityStoreRegistry.registerSystem(new PlayerConnectionFlushSystem(this.playerRefComponentType));
|
||||
entityStoreRegistry.registerSystem(new PlayerRefAddedSystem(this.playerRefComponentType));
|
||||
commandRegistry.registerCommand(new SetTickingCommand());
|
||||
commandRegistry.registerCommand(new BlockCommand());
|
||||
commandRegistry.registerCommand(new BlockSelectCommand());
|
||||
commandRegistry.registerCommand(new WorldCommand());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void start() {
|
||||
HytaleServerConfig config = HytaleServer.get().getConfig();
|
||||
if (config == null) {
|
||||
throw new IllegalStateException("Server config is not loaded!");
|
||||
} else {
|
||||
this.playerStorage = config.getPlayerStorageProvider().getPlayerStorage();
|
||||
WorldConfigProvider.Default defaultConfigProvider = new WorldConfigProvider.Default();
|
||||
PrepareUniverseEvent event = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.<Void, PrepareUniverseEvent>dispatchFor(PrepareUniverseEvent.class)
|
||||
.dispatch(new PrepareUniverseEvent(defaultConfigProvider));
|
||||
WorldConfigProvider worldConfigProvider = event.getWorldConfigProvider();
|
||||
if (worldConfigProvider == null) {
|
||||
worldConfigProvider = defaultConfigProvider;
|
||||
}
|
||||
|
||||
this.worldConfigProvider = worldConfigProvider;
|
||||
|
||||
try {
|
||||
Path blockIdMapPath = this.path.resolve("blockIdMap.json");
|
||||
Path path = this.path.resolve("blockIdMap.legacy.json");
|
||||
if (Files.isRegularFile(blockIdMapPath)) {
|
||||
Files.move(blockIdMapPath, path, StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
|
||||
Files.deleteIfExists(this.path.resolve("blockIdMap.json.bak"));
|
||||
if (Files.isRegularFile(path)) {
|
||||
Map<Integer, String> map = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
for (BsonValue bsonValue : BsonUtil.readDocument(path).thenApply(document -> document.getArray("Blocks")).join()) {
|
||||
BsonDocument bsonDocument = bsonValue.asDocument();
|
||||
map.put(bsonDocument.getNumber("Id").intValue(), bsonDocument.getString("BlockType").getValue());
|
||||
}
|
||||
|
||||
LEGACY_BLOCK_ID_MAP = Collections.unmodifiableMap(map);
|
||||
}
|
||||
} catch (IOException var14) {
|
||||
this.getLogger().at(Level.SEVERE).withCause(var14).log("Failed to delete blockIdMap.json");
|
||||
}
|
||||
|
||||
if (Options.getOptionSet().has(Options.BARE)) {
|
||||
this.universeReady = CompletableFuture.completedFuture(null);
|
||||
HytaleServer.get().getEventBus().dispatch(AllWorldsLoadedEvent.class);
|
||||
} else {
|
||||
ObjectArrayList<CompletableFuture<?>> loadingWorlds = new ObjectArrayList<>();
|
||||
|
||||
try {
|
||||
Path worldsPath = this.path.resolve("worlds");
|
||||
Files.createDirectories(worldsPath);
|
||||
|
||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(worldsPath)) {
|
||||
for (Path file : stream) {
|
||||
if (HytaleServer.get().isShuttingDown()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!file.equals(worldsPath) && Files.isDirectory(file)) {
|
||||
String name = file.getFileName().toString();
|
||||
if (this.getWorld(name) == null) {
|
||||
loadingWorlds.add(this.loadWorldFromStart(file, name).exceptionally(throwable -> {
|
||||
this.getLogger().at(Level.SEVERE).withCause(throwable).log("Failed to load world: %s", name);
|
||||
return null;
|
||||
}));
|
||||
} else {
|
||||
this.getLogger().at(Level.SEVERE).log("Skipping loading world '%s' because it already exists!", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.universeReady = CompletableFutureUtil._catch(
|
||||
CompletableFuture.allOf(loadingWorlds.toArray(CompletableFuture[]::new))
|
||||
.thenCompose(
|
||||
v -> {
|
||||
String worldName = config.getDefaults().getWorld();
|
||||
return worldName != null && !this.worlds.containsKey(worldName.toLowerCase())
|
||||
? CompletableFutureUtil._catch(this.addWorld(worldName))
|
||||
: CompletableFuture.completedFuture(null);
|
||||
}
|
||||
)
|
||||
.thenRun(() -> HytaleServer.get().getEventBus().dispatch(AllWorldsLoadedEvent.class))
|
||||
);
|
||||
} catch (IOException var13) {
|
||||
throw new RuntimeException("Failed to load Worlds", var13);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutdown() {
|
||||
this.disconnectAllPLayers();
|
||||
this.shutdownAllWorlds();
|
||||
}
|
||||
|
||||
public void disconnectAllPLayers() {
|
||||
this.players.values().forEach(player -> player.getPacketHandler().disconnect("Stopping server!"));
|
||||
}
|
||||
|
||||
public void shutdownAllWorlds() {
|
||||
Iterator<World> iterator = this.worlds.values().iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
World world = iterator.next();
|
||||
world.stop();
|
||||
iterator.remove();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public MetricResults toMetricResults() {
|
||||
return METRICS_REGISTRY.toMetricResults(this);
|
||||
}
|
||||
|
||||
public CompletableFuture<Void> getUniverseReady() {
|
||||
return this.universeReady;
|
||||
}
|
||||
|
||||
public ResourceType<ChunkStore, IndexedStorageChunkStorageProvider.IndexedStorageCache> getIndexedStorageCacheResourceType() {
|
||||
return this.indexedStorageCacheResourceType;
|
||||
}
|
||||
|
||||
public boolean isWorldLoadable(@Nonnull String name) {
|
||||
Path savePath = this.path.resolve("worlds").resolve(name);
|
||||
return Files.isDirectory(savePath) && (Files.exists(savePath.resolve("config.bson")) || Files.exists(savePath.resolve("config.json")));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> addWorld(@Nonnull String name) {
|
||||
return this.addWorld(name, null, null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Deprecated
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> addWorld(@Nonnull String name, @Nullable String generatorType, @Nullable String chunkStorageType) {
|
||||
if (this.worlds.containsKey(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already exists!");
|
||||
} else if (this.isWorldLoadable(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already exists on disk!");
|
||||
} else {
|
||||
Path savePath = this.path.resolve("worlds").resolve(name);
|
||||
return this.worldConfigProvider.load(savePath, name).thenCompose(worldConfig -> {
|
||||
if (generatorType != null && !"default".equals(generatorType)) {
|
||||
BuilderCodec<? extends IWorldGenProvider> providerCodec = IWorldGenProvider.CODEC.getCodecFor(generatorType);
|
||||
if (providerCodec == null) {
|
||||
throw new IllegalArgumentException("Unknown generatorType '" + generatorType + "'");
|
||||
}
|
||||
|
||||
IWorldGenProvider provider = providerCodec.getDefaultValue();
|
||||
worldConfig.setWorldGenProvider(provider);
|
||||
worldConfig.markChanged();
|
||||
}
|
||||
|
||||
if (chunkStorageType != null && !"default".equals(chunkStorageType)) {
|
||||
BuilderCodec<? extends IChunkStorageProvider> providerCodec = IChunkStorageProvider.CODEC.getCodecFor(chunkStorageType);
|
||||
if (providerCodec == null) {
|
||||
throw new IllegalArgumentException("Unknown chunkStorageType '" + chunkStorageType + "'");
|
||||
}
|
||||
|
||||
IChunkStorageProvider provider = providerCodec.getDefaultValue();
|
||||
worldConfig.setChunkStorageProvider(provider);
|
||||
worldConfig.markChanged();
|
||||
}
|
||||
|
||||
return this.makeWorld(name, savePath, worldConfig);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> makeWorld(@Nonnull String name, @Nonnull Path savePath, @Nonnull WorldConfig worldConfig) {
|
||||
return this.makeWorld(name, savePath, worldConfig, true);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> makeWorld(@Nonnull String name, @Nonnull Path savePath, @Nonnull WorldConfig worldConfig, boolean start) {
|
||||
Map<PluginIdentifier, SemverRange> map = worldConfig.getRequiredPlugins();
|
||||
if (map != null) {
|
||||
PluginManager pluginManager = PluginManager.get();
|
||||
|
||||
for (Entry<PluginIdentifier, SemverRange> entry : map.entrySet()) {
|
||||
if (!pluginManager.hasPlugin(entry.getKey(), entry.getValue())) {
|
||||
this.getLogger().at(Level.SEVERE).log("Failed to load world! Missing plugin: %s, Version: %s", entry.getKey(), entry.getValue());
|
||||
throw new IllegalStateException("Missing plugin");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (this.worlds.containsKey(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already exists!");
|
||||
} else {
|
||||
return CompletableFuture.supplyAsync(
|
||||
SneakyThrow.sneakySupplier(
|
||||
() -> {
|
||||
World world = new World(name, savePath, worldConfig);
|
||||
AddWorldEvent event = HytaleServer.get().getEventBus().dispatchFor(AddWorldEvent.class, name).dispatch(new AddWorldEvent(world));
|
||||
if (!event.isCancelled() && !HytaleServer.get().isShuttingDown()) {
|
||||
World oldWorldByName = this.worlds.putIfAbsent(name.toLowerCase(), world);
|
||||
if (oldWorldByName != null) {
|
||||
throw new ConcurrentModificationException(
|
||||
"World with name " + name + " already exists but didn't before! Looks like you have a race condition."
|
||||
);
|
||||
} else {
|
||||
World oldWorldByUuid = this.worldsByUuid.putIfAbsent(worldConfig.getUuid(), world);
|
||||
if (oldWorldByUuid != null) {
|
||||
throw new ConcurrentModificationException(
|
||||
"World with UUID " + worldConfig.getUuid() + " already exists but didn't before! Looks like you have a race condition."
|
||||
);
|
||||
} else {
|
||||
return world;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw new WorldLoadCancelledException();
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
.thenCompose(World::init)
|
||||
.thenCompose(
|
||||
world -> !Options.getOptionSet().has(Options.MIGRATIONS) && start
|
||||
? world.start().thenApply(v -> world)
|
||||
: CompletableFuture.completedFuture(world)
|
||||
)
|
||||
.whenComplete((world, throwable) -> {
|
||||
if (throwable != null) {
|
||||
String nameLower = name.toLowerCase();
|
||||
if (this.worlds.containsKey(nameLower)) {
|
||||
try {
|
||||
this.removeWorldExceptionally(name);
|
||||
} catch (Exception var6x) {
|
||||
this.getLogger().at(Level.WARNING).withCause(var6x).log("Failed to clean up world '%s' after init failure", name);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private CompletableFuture<Void> loadWorldFromStart(@Nonnull Path savePath, @Nonnull String name) {
|
||||
return this.worldConfigProvider
|
||||
.load(savePath, name)
|
||||
.thenCompose(worldConfig -> worldConfig.isDeleteOnUniverseStart() ? CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
FileUtil.deleteDirectory(savePath);
|
||||
this.getLogger().at(Level.INFO).log("Deleted world " + name + " from DeleteOnUniverseStart flag on universe start at " + savePath);
|
||||
} catch (Throwable var4) {
|
||||
throw new RuntimeException("Error deleting world directory on universe start", var4);
|
||||
}
|
||||
}) : this.makeWorld(name, savePath, worldConfig).thenApply(x -> null));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@CheckReturnValue
|
||||
public CompletableFuture<World> loadWorld(@Nonnull String name) {
|
||||
if (this.worlds.containsKey(name)) {
|
||||
throw new IllegalArgumentException("World " + name + " already loaded!");
|
||||
} else {
|
||||
Path savePath = this.path.resolve("worlds").resolve(name);
|
||||
if (!Files.isDirectory(savePath)) {
|
||||
throw new IllegalArgumentException("World " + name + " does not exist!");
|
||||
} else {
|
||||
return this.worldConfigProvider.load(savePath, name).thenCompose(worldConfig -> this.makeWorld(name, savePath, worldConfig));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public World getWorld(@Nullable String worldName) {
|
||||
return worldName == null ? null : this.worlds.get(worldName.toLowerCase());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public World getWorld(@Nonnull UUID uuid) {
|
||||
return this.worldsByUuid.get(uuid);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public World getDefaultWorld() {
|
||||
HytaleServerConfig config = HytaleServer.get().getConfig();
|
||||
if (config == null) {
|
||||
return null;
|
||||
} else {
|
||||
String worldName = config.getDefaults().getWorld();
|
||||
return worldName != null ? this.getWorld(worldName) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean removeWorld(@Nonnull String name) {
|
||||
Objects.requireNonNull(name, "Name can't be null!");
|
||||
String nameLower = name.toLowerCase();
|
||||
World world = this.worlds.get(nameLower);
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World " + name + " doesn't exist!");
|
||||
} else {
|
||||
RemoveWorldEvent event = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(RemoveWorldEvent.class, name)
|
||||
.dispatch(new RemoveWorldEvent(world, RemoveWorldEvent.RemovalReason.GENERAL));
|
||||
if (event.isCancelled()) {
|
||||
return false;
|
||||
} else {
|
||||
this.worlds.remove(nameLower);
|
||||
this.worldsByUuid.remove(world.getWorldConfig().getUuid());
|
||||
if (world.isAlive()) {
|
||||
world.stopIndividualWorld();
|
||||
}
|
||||
|
||||
world.validateDeleteOnRemove();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeWorldExceptionally(@Nonnull String name) {
|
||||
Objects.requireNonNull(name, "Name can't be null!");
|
||||
this.getLogger().at(Level.INFO).log("Removing world exceptionally: %s", name);
|
||||
String nameLower = name.toLowerCase();
|
||||
World world = this.worlds.get(nameLower);
|
||||
if (world == null) {
|
||||
throw new NullPointerException("World " + name + " doesn't exist!");
|
||||
} else {
|
||||
HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(RemoveWorldEvent.class, name)
|
||||
.dispatch(new RemoveWorldEvent(world, RemoveWorldEvent.RemovalReason.EXCEPTIONAL));
|
||||
this.worlds.remove(nameLower);
|
||||
this.worldsByUuid.remove(world.getWorldConfig().getUuid());
|
||||
if (world.isAlive()) {
|
||||
world.stopIndividualWorld();
|
||||
}
|
||||
|
||||
world.validateDeleteOnRemove();
|
||||
|
||||
// HyFix: Shut down server if default world crashes exceptionally
|
||||
// This prevents players from being stuck in a broken state
|
||||
String defaultWorldName = HytaleServer.get().getConfig().getDefaults().getWorld();
|
||||
if (defaultWorldName != null && defaultWorldName.equalsIgnoreCase(name)) {
|
||||
this.getLogger().at(Level.SEVERE).log("Default world '%s' crashed! Shutting down server.", name);
|
||||
HytaleServer.SCHEDULED_EXECUTOR.schedule(() -> {
|
||||
HytaleServer.get().shutdownServer();
|
||||
}, 1, TimeUnit.SECONDS);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Path getPath() {
|
||||
return this.path;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Map<String, World> getWorlds() {
|
||||
return this.unmodifiableWorlds;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public List<PlayerRef> getPlayers() {
|
||||
return new ObjectArrayList<>(this.players.values());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayer(@Nonnull UUID uuid) {
|
||||
return this.players.get(uuid);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayer(@Nonnull String value, @Nonnull NameMatching matching) {
|
||||
return matching.find(this.players.values(), value, v -> v.getComponent(PlayerRef.getComponentType()).getUsername());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayer(@Nonnull String value, @Nonnull Comparator<String> comparator, @Nonnull BiPredicate<String, String> equality) {
|
||||
return NameMatching.find(this.players.values(), value, v -> v.getComponent(PlayerRef.getComponentType()).getUsername(), comparator, equality);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayerByUsername(@Nonnull String value, @Nonnull NameMatching matching) {
|
||||
return matching.find(this.players.values(), value, PlayerRef::getUsername);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public PlayerRef getPlayerByUsername(@Nonnull String value, @Nonnull Comparator<String> comparator, @Nonnull BiPredicate<String, String> equality) {
|
||||
return NameMatching.find(this.players.values(), value, PlayerRef::getUsername, comparator, equality);
|
||||
}
|
||||
|
||||
public int getPlayerCount() {
|
||||
return this.players.size();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> addPlayer(
|
||||
@Nonnull Channel channel,
|
||||
@Nonnull String language,
|
||||
@Nonnull ProtocolVersion protocolVersion,
|
||||
@Nonnull UUID uuid,
|
||||
@Nonnull String username,
|
||||
@Nonnull PlayerAuthentication auth,
|
||||
int clientViewRadiusChunks,
|
||||
@Nullable PlayerSkin skin
|
||||
) {
|
||||
GamePacketHandler playerConnection = new GamePacketHandler(channel, protocolVersion, auth);
|
||||
playerConnection.setQueuePackets(false);
|
||||
this.getLogger().at(Level.INFO).log("Adding player '%s (%s)", username, uuid);
|
||||
return this.playerStorage
|
||||
.load(uuid)
|
||||
.exceptionally(throwable -> {
|
||||
throw new RuntimeException("Exception when adding player to universe:", throwable);
|
||||
})
|
||||
.thenCompose(
|
||||
holder -> {
|
||||
ChunkTracker chunkTrackerComponent = new ChunkTracker();
|
||||
PlayerRef playerRefComponent = new PlayerRef((Holder<EntityStore>) holder, uuid, username, language, playerConnection, chunkTrackerComponent);
|
||||
chunkTrackerComponent.setDefaultMaxChunksPerSecond(playerRefComponent);
|
||||
holder.putComponent(PlayerRef.getComponentType(), playerRefComponent);
|
||||
holder.putComponent(ChunkTracker.getComponentType(), chunkTrackerComponent);
|
||||
holder.putComponent(UUIDComponent.getComponentType(), new UUIDComponent(uuid));
|
||||
holder.ensureComponent(PositionDataComponent.getComponentType());
|
||||
holder.ensureComponent(MovementAudioComponent.getComponentType());
|
||||
Player playerComponent = holder.ensureAndGetComponent(Player.getComponentType());
|
||||
playerComponent.init(uuid, playerRefComponent);
|
||||
PlayerConfigData playerConfig = playerComponent.getPlayerConfigData();
|
||||
playerConfig.cleanup(this);
|
||||
PacketHandler.logConnectionTimings(channel, "Load Player Config", Level.FINEST);
|
||||
if (skin != null) {
|
||||
holder.putComponent(PlayerSkinComponent.getComponentType(), new PlayerSkinComponent(skin));
|
||||
holder.putComponent(ModelComponent.getComponentType(), new ModelComponent(CosmeticsModule.get().createModel(skin)));
|
||||
}
|
||||
|
||||
playerConnection.setPlayerRef(playerRefComponent, playerComponent);
|
||||
NettyUtil.setChannelHandler(channel, playerConnection);
|
||||
playerComponent.setClientViewRadius(clientViewRadiusChunks);
|
||||
EntityTrackerSystems.EntityViewer entityViewerComponent = holder.getComponent(EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (entityViewerComponent != null) {
|
||||
entityViewerComponent.viewRadiusBlocks = playerComponent.getViewRadius() * 32;
|
||||
} else {
|
||||
entityViewerComponent = new EntityTrackerSystems.EntityViewer(playerComponent.getViewRadius() * 32, playerConnection);
|
||||
holder.addComponent(EntityTrackerSystems.EntityViewer.getComponentType(), entityViewerComponent);
|
||||
}
|
||||
|
||||
PlayerRef existingPlayer = this.players.putIfAbsent(uuid, playerRefComponent);
|
||||
if (existingPlayer != null) {
|
||||
this.getLogger().at(Level.WARNING).log("Player '%s' (%s) already joining from another connection, rejecting duplicate", username, uuid);
|
||||
playerConnection.disconnect("A connection with this account is already in progress");
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
String lastWorldName = playerConfig.getWorld();
|
||||
World lastWorld = this.getWorld(lastWorldName);
|
||||
PlayerConnectEvent event = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.<Void, PlayerConnectEvent>dispatchFor(PlayerConnectEvent.class)
|
||||
.dispatch(new PlayerConnectEvent((Holder<EntityStore>) holder, playerRefComponent, lastWorld != null ? lastWorld : this.getDefaultWorld()));
|
||||
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<PlayerDisconnectEvent, PlayerDisconnectEvent> eventDispatcher = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(PlayerDisconnectEvent.class);
|
||||
if (eventDispatcher.hasListener()) {
|
||||
eventDispatcher.dispatch(new PlayerDisconnectEvent(playerRef));
|
||||
}
|
||||
|
||||
Ref<EntityStore> ref = playerRef.getReference();
|
||||
if (ref == null) {
|
||||
this.finalizePlayerRemoval(playerRef);
|
||||
} else {
|
||||
World world = ref.getStore().getExternalData().getWorld();
|
||||
if (world.isInThread()) {
|
||||
// HyFix: Wrap in try-catch for IllegalStateException with fallback cleanup
|
||||
try {
|
||||
Player playerComponent = ref.getStore().getComponent(ref, Player.getComponentType());
|
||||
if (playerComponent != null) {
|
||||
playerComponent.remove();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
System.err.println("[HyFix] Player ref invalid during removal - performing fallback cleanup");
|
||||
try {
|
||||
playerRef.getChunkTracker().clear();
|
||||
System.err.println("[HyFix] ChunkTracker cleared - memory leak prevented");
|
||||
} catch (Exception cleanupEx) {
|
||||
System.err.println("[HyFix] Fallback cleanup failed - memory may leak");
|
||||
}
|
||||
}
|
||||
|
||||
this.finalizePlayerRemoval(playerRef);
|
||||
} else {
|
||||
CompletableFuture<Void> removedFuture = new CompletableFuture<>();
|
||||
CompletableFuture.runAsync(() -> {
|
||||
// HyFix: Wrap in try-catch for IllegalStateException with fallback cleanup
|
||||
try {
|
||||
Player playerComponent = ref.getStore().getComponent(ref, Player.getComponentType());
|
||||
if (playerComponent != null) {
|
||||
playerComponent.remove();
|
||||
}
|
||||
} catch (IllegalStateException e) {
|
||||
System.err.println("[HyFix] Player ref invalid during async removal - performing fallback cleanup");
|
||||
try {
|
||||
playerRef.getChunkTracker().clear();
|
||||
System.err.println("[HyFix] ChunkTracker cleared - memory leak prevented");
|
||||
} catch (Exception cleanupEx) {
|
||||
System.err.println("[HyFix] Fallback cleanup failed - memory may leak");
|
||||
}
|
||||
}
|
||||
}, world).whenComplete((unused, throwable) -> {
|
||||
if (throwable != null) {
|
||||
removedFuture.completeExceptionally(throwable);
|
||||
} else {
|
||||
removedFuture.complete(unused);
|
||||
}
|
||||
});
|
||||
removedFuture.orTimeout(5L, TimeUnit.SECONDS)
|
||||
.whenComplete(
|
||||
(result, error) -> {
|
||||
if (error != null) {
|
||||
this.getLogger()
|
||||
.at(Level.WARNING)
|
||||
.withCause(error)
|
||||
.log("Timeout or error waiting for player '%s' removal from world store", playerRef.getUsername());
|
||||
// HyFix: Perform fallback cleanup on timeout
|
||||
try {
|
||||
playerRef.getChunkTracker().clear();
|
||||
System.err.println("[HyFix] ChunkTracker cleared on timeout - memory leak prevented");
|
||||
} catch (Exception cleanupEx) {
|
||||
System.err.println("[HyFix] Fallback cleanup on timeout failed - memory may leak");
|
||||
}
|
||||
}
|
||||
|
||||
this.finalizePlayerRemoval(playerRef);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void finalizePlayerRemoval(@Nonnull PlayerRef playerRef) {
|
||||
this.players.remove(playerRef.getUuid());
|
||||
if (Constants.SINGLEPLAYER) {
|
||||
if (this.players.isEmpty()) {
|
||||
this.getLogger().at(Level.INFO).log("No players left on singleplayer server shutting down!");
|
||||
HytaleServer.get().shutdownServer();
|
||||
} else if (SingleplayerModule.isOwner(playerRef)) {
|
||||
this.getLogger().at(Level.INFO).log("Owner left the singleplayer server shutting down!");
|
||||
this.getPlayers().forEach(p -> p.getPacketHandler().disconnect(playerRef.getUsername() + " left! Shutting down singleplayer world!"));
|
||||
HytaleServer.get().shutdownServer();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> resetPlayer(@Nonnull PlayerRef oldPlayer) {
|
||||
return this.playerStorage.load(oldPlayer.getUuid()).exceptionally(throwable -> {
|
||||
throw new RuntimeException("Exception when adding player to universe:", throwable);
|
||||
}).thenCompose(holder -> this.resetPlayer(oldPlayer, (Holder<EntityStore>) holder));
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> resetPlayer(@Nonnull PlayerRef oldPlayer, @Nonnull Holder<EntityStore> holder) {
|
||||
return this.resetPlayer(oldPlayer, holder, null, null);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<PlayerRef> resetPlayer(
|
||||
@Nonnull PlayerRef playerRef, @Nonnull Holder<EntityStore> holder, @Nullable World world, @Nullable Transform transform
|
||||
) {
|
||||
UUID uuid = playerRef.getUuid();
|
||||
Player oldPlayer = playerRef.getComponent(Player.getComponentType());
|
||||
World targetWorld;
|
||||
if (world == null) {
|
||||
targetWorld = oldPlayer.getWorld();
|
||||
} else {
|
||||
targetWorld = world;
|
||||
}
|
||||
|
||||
this.getLogger()
|
||||
.at(Level.INFO)
|
||||
.log(
|
||||
"Resetting player '%s', moving to world '%s' at location %s (%s)",
|
||||
playerRef.getUsername(),
|
||||
world != null ? world.getName() : null,
|
||||
transform,
|
||||
playerRef.getUuid()
|
||||
);
|
||||
GamePacketHandler playerConnection = (GamePacketHandler) playerRef.getPacketHandler();
|
||||
Player newPlayer = holder.ensureAndGetComponent(Player.getComponentType());
|
||||
newPlayer.init(uuid, playerRef);
|
||||
CompletableFuture<Void> leaveWorld = new CompletableFuture<>();
|
||||
if (oldPlayer.getWorld() != null) {
|
||||
oldPlayer.getWorld().execute(() -> {
|
||||
playerRef.removeFromStore();
|
||||
leaveWorld.complete(null);
|
||||
});
|
||||
} else {
|
||||
leaveWorld.complete(null);
|
||||
}
|
||||
|
||||
return leaveWorld.thenAccept(v -> {
|
||||
oldPlayer.resetManagers(holder);
|
||||
newPlayer.copyFrom(oldPlayer);
|
||||
EntityTrackerSystems.EntityViewer viewer = holder.getComponent(EntityTrackerSystems.EntityViewer.getComponentType());
|
||||
if (viewer != null) {
|
||||
viewer.viewRadiusBlocks = newPlayer.getViewRadius() * 32;
|
||||
} else {
|
||||
viewer = new EntityTrackerSystems.EntityViewer(newPlayer.getViewRadius() * 32, playerConnection);
|
||||
holder.addComponent(EntityTrackerSystems.EntityViewer.getComponentType(), viewer);
|
||||
}
|
||||
|
||||
playerConnection.setPlayerRef(playerRef, newPlayer);
|
||||
playerRef.replaceHolder(holder);
|
||||
holder.putComponent(PlayerRef.getComponentType(), playerRef);
|
||||
}).thenCompose(v -> targetWorld.addPlayer(playerRef, transform));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void sendMessage(@Nonnull Message message) {
|
||||
for (PlayerRef ref : this.players.values()) {
|
||||
ref.sendMessage(message);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPacket(@Nonnull Packet packet) {
|
||||
for (PlayerRef player : this.players.values()) {
|
||||
player.getPacketHandler().write(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPacketNoCache(@Nonnull Packet packet) {
|
||||
for (PlayerRef player : this.players.values()) {
|
||||
player.getPacketHandler().writeNoCache(packet);
|
||||
}
|
||||
}
|
||||
|
||||
public void broadcastPacket(@Nonnull Packet... packets) {
|
||||
for (PlayerRef player : this.players.values()) {
|
||||
player.getPacketHandler().write(packets);
|
||||
}
|
||||
}
|
||||
|
||||
public PlayerStorage getPlayerStorage() {
|
||||
return this.playerStorage;
|
||||
}
|
||||
|
||||
public void setPlayerStorage(@Nonnull PlayerStorage playerStorage) {
|
||||
this.playerStorage = playerStorage;
|
||||
}
|
||||
|
||||
public WorldConfigProvider getWorldConfigProvider() {
|
||||
return this.worldConfigProvider;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public ComponentType<EntityStore, PlayerRef> getPlayerRefComponentType() {
|
||||
return this.playerRefComponentType;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Deprecated
|
||||
public static Map<Integer, String> getLegacyBlockIdMap() {
|
||||
return LEGACY_BLOCK_ID_MAP;
|
||||
}
|
||||
|
||||
public static Path getWorldGenPath() {
|
||||
OptionSet optionSet = Options.getOptionSet();
|
||||
Path worldGenPath;
|
||||
if (optionSet.has(Options.WORLD_GEN_DIRECTORY)) {
|
||||
worldGenPath = optionSet.valueOf(Options.WORLD_GEN_DIRECTORY);
|
||||
} else {
|
||||
worldGenPath = AssetUtil.getHytaleAssetsPath().resolve("Server").resolve("World");
|
||||
}
|
||||
|
||||
return worldGenPath;
|
||||
}
|
||||
}
|
||||
1247
src/com/hypixel/hytale/server/core/universe/world/World.java
Normal file
1247
src/com/hypixel/hytale/server/core/universe/world/World.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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<CompletableFuture<MapImage>> pendingReloadFutures = new Long2ObjectOpenHashMap<>();
|
||||
private final MapMarkerTracker markerTracker;
|
||||
private float updateTimer;
|
||||
private Integer viewRadiusOverride;
|
||||
private boolean started;
|
||||
private int sentViewRadius;
|
||||
private int lastChunkX;
|
||||
private int lastChunkZ;
|
||||
@Nullable
|
||||
private String currentBiomeName;
|
||||
@Nullable
|
||||
private WorldMapTracker.ZoneDiscoveryInfo currentZone;
|
||||
private boolean clientHasWorldMapVisible;
|
||||
@Nullable
|
||||
private TransformComponent transformComponent;
|
||||
|
||||
public WorldMapTracker(@Nonnull Player player) {
|
||||
this.player = player;
|
||||
this.markerTracker = new MapMarkerTracker(this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(float dt) {
|
||||
if (!this.started) {
|
||||
this.started = true;
|
||||
LOGGER.at(Level.INFO).log("Started Generating Map!");
|
||||
}
|
||||
|
||||
World world = this.player.getWorld();
|
||||
if (world != null) {
|
||||
if (this.transformComponent == null) {
|
||||
this.transformComponent = this.player.getTransformComponent();
|
||||
if (this.transformComponent == null) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
WorldMapManager worldMapManager = world.getWorldMapManager();
|
||||
WorldMapSettings worldMapSettings = worldMapManager.getWorldMapSettings();
|
||||
int viewRadius;
|
||||
if (this.viewRadiusOverride != null) {
|
||||
viewRadius = this.viewRadiusOverride;
|
||||
} else {
|
||||
viewRadius = worldMapSettings.getViewRadius(this.player.getViewRadius());
|
||||
}
|
||||
|
||||
Vector3d position = this.transformComponent.getPosition();
|
||||
int playerX = MathUtil.floor(position.getX());
|
||||
int playerZ = MathUtil.floor(position.getZ());
|
||||
int playerChunkX = playerX >> 5;
|
||||
int playerChunkZ = playerZ >> 5;
|
||||
if (world.isCompassUpdating()) {
|
||||
this.markerTracker.updatePointsOfInterest(dt, world, viewRadius, playerChunkX, playerChunkZ);
|
||||
}
|
||||
|
||||
if (worldMapManager.isWorldMapEnabled()) {
|
||||
this.updateWorldMap(world, dt, worldMapSettings, viewRadius, playerChunkX, playerChunkZ);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void updateCurrentZoneAndBiome(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nullable WorldMapTracker.ZoneDiscoveryInfo zoneDiscoveryInfo,
|
||||
@Nullable String biomeName,
|
||||
@Nonnull ComponentAccessor<EntityStore> componentAccessor
|
||||
) {
|
||||
this.currentBiomeName = biomeName;
|
||||
this.currentZone = zoneDiscoveryInfo;
|
||||
Player playerComponent = componentAccessor.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
if (!playerComponent.isWaitingForClientReady()) {
|
||||
World world = componentAccessor.getExternalData().getWorld();
|
||||
if (zoneDiscoveryInfo != null && this.discoverZone(world, zoneDiscoveryInfo.regionName())) {
|
||||
this.onZoneDiscovered(ref, zoneDiscoveryInfo, componentAccessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void onZoneDiscovered(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull WorldMapTracker.ZoneDiscoveryInfo zoneDiscoveryInfo, @Nonnull ComponentAccessor<EntityStore> componentAccessor
|
||||
) {
|
||||
WorldMapTracker.ZoneDiscoveryInfo discoverZoneEventInfo = zoneDiscoveryInfo.clone();
|
||||
DiscoverZoneEvent.Display discoverZoneEvent = new DiscoverZoneEvent.Display(discoverZoneEventInfo);
|
||||
componentAccessor.invoke(ref, discoverZoneEvent);
|
||||
if (!discoverZoneEvent.isCancelled() && discoverZoneEventInfo.display()) {
|
||||
PlayerRef playerRefComponent = componentAccessor.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
EventTitleUtil.showEventTitleToPlayer(
|
||||
playerRefComponent,
|
||||
Message.translation(String.format("server.map.region.%s", discoverZoneEventInfo.regionName())),
|
||||
Message.translation(String.format("server.map.zone.%s", discoverZoneEventInfo.zoneName())),
|
||||
discoverZoneEventInfo.major(),
|
||||
discoverZoneEventInfo.icon(),
|
||||
discoverZoneEventInfo.duration(),
|
||||
discoverZoneEventInfo.fadeInDuration(),
|
||||
discoverZoneEventInfo.fadeOutDuration()
|
||||
);
|
||||
String discoverySoundEventId = discoverZoneEventInfo.discoverySoundEventId();
|
||||
if (discoverySoundEventId != null) {
|
||||
int assetIndex = SoundEvent.getAssetMap().getIndex(discoverySoundEventId);
|
||||
if (assetIndex != Integer.MIN_VALUE) {
|
||||
SoundUtil.playSoundEvent2d(ref, assetIndex, SoundCategory.UI, componentAccessor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void updateWorldMap(
|
||||
@Nonnull World world, float dt, @Nonnull WorldMapSettings worldMapSettings, int chunkViewRadius, int playerChunkX, int playerChunkZ
|
||||
) {
|
||||
this.processPendingReloadChunks(world);
|
||||
Box2D worldMapArea = worldMapSettings.getWorldMapArea();
|
||||
if (worldMapArea == null) {
|
||||
int xDiff = Math.abs(this.lastChunkX - playerChunkX);
|
||||
int zDiff = Math.abs(this.lastChunkZ - playerChunkZ);
|
||||
int chunkMoveDistance = xDiff <= 0 && zDiff <= 0 ? 0 : (int) Math.ceil(Math.sqrt(xDiff * xDiff + zDiff * zDiff));
|
||||
this.sentViewRadius = Math.max(0, this.sentViewRadius - chunkMoveDistance);
|
||||
this.lastChunkX = playerChunkX;
|
||||
this.lastChunkZ = playerChunkZ;
|
||||
this.updateTimer -= dt;
|
||||
if (this.updateTimer > 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sentViewRadius != chunkViewRadius) {
|
||||
if (this.sentViewRadius > chunkViewRadius) {
|
||||
this.sentViewRadius = chunkViewRadius;
|
||||
}
|
||||
|
||||
this.unloadImages(chunkViewRadius, playerChunkX, playerChunkZ);
|
||||
if (this.sentViewRadius < chunkViewRadius) {
|
||||
this.loadImages(world, chunkViewRadius, playerChunkX, playerChunkZ, 20);
|
||||
}
|
||||
} else {
|
||||
this.updateTimer = 1.0F;
|
||||
}
|
||||
} else {
|
||||
this.updateTimer -= dt;
|
||||
if (this.updateTimer > 0.0F) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadWorldMap(world, worldMapArea, 20);
|
||||
}
|
||||
}
|
||||
|
||||
private void unloadImages(int chunkViewRadius, int playerChunkX, int playerChunkZ) {
|
||||
// HyFix #16: Wrap in try-catch for iterator corruption NPE under high load
|
||||
try {
|
||||
List<MapChunk> currentUnloadList = null;
|
||||
List<List<MapChunk>> allUnloadLists = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
int packetSize = 2621427;
|
||||
LongIterator iterator = this.loaded.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
long chunkCoordinates = iterator.nextLong();
|
||||
int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
if (!shouldBeVisible(chunkViewRadius, playerChunkX, playerChunkZ, mapChunkX, mapChunkZ)) {
|
||||
if (currentUnloadList == null) {
|
||||
currentUnloadList = new ObjectArrayList<>(packetSize / 10);
|
||||
}
|
||||
|
||||
currentUnloadList.add(new MapChunk(mapChunkX, mapChunkZ, null));
|
||||
packetSize -= 10;
|
||||
iterator.remove();
|
||||
if (packetSize < 10) {
|
||||
packetSize = 2621427;
|
||||
if (allUnloadLists == null) {
|
||||
allUnloadLists = new ObjectArrayList<>(this.loaded.size() / (packetSize / 10));
|
||||
}
|
||||
|
||||
allUnloadLists.add(currentUnloadList);
|
||||
currentUnloadList = new ObjectArrayList<>(packetSize / 10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (allUnloadLists != null) {
|
||||
for (List<MapChunk> unloadList : allUnloadLists) {
|
||||
this.writeUpdatePacket(unloadList);
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(currentUnloadList);
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
} catch (NullPointerException e) {
|
||||
System.out.println("[HyFix] WARNING: Iterator corruption in WorldMapTracker.unloadImages() - recovered gracefully (Issue #16)");
|
||||
}
|
||||
}
|
||||
|
||||
private void processPendingReloadChunks(@Nonnull World world) {
|
||||
List<MapChunk> chunksToSend = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
if (!this.pendingReloadChunks.isEmpty()) {
|
||||
int imageSize = MathUtil.fastFloor(32.0F * world.getWorldMapManager().getWorldMapSettings().getImageScale());
|
||||
int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
|
||||
int packetSize = 2621427;
|
||||
LongIterator iterator = this.pendingReloadChunks.iterator();
|
||||
|
||||
while (iterator.hasNext()) {
|
||||
long chunkCoordinates = iterator.nextLong();
|
||||
CompletableFuture<MapImage> future = this.pendingReloadFutures.get(chunkCoordinates);
|
||||
if (future == null) {
|
||||
future = world.getWorldMapManager().getImageAsync(chunkCoordinates);
|
||||
this.pendingReloadFutures.put(chunkCoordinates, future);
|
||||
}
|
||||
|
||||
if (future.isDone()) {
|
||||
iterator.remove();
|
||||
this.pendingReloadFutures.remove(chunkCoordinates);
|
||||
if (chunksToSend == null) {
|
||||
chunksToSend = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
|
||||
int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
chunksToSend.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
|
||||
this.loaded.add(chunkCoordinates);
|
||||
packetSize -= fullMapChunkSize;
|
||||
if (packetSize < fullMapChunkSize) {
|
||||
this.writeUpdatePacket(chunksToSend);
|
||||
chunksToSend = new ObjectArrayList<>(2621440 - 13 / fullMapChunkSize);
|
||||
packetSize = 2621427;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(chunksToSend);
|
||||
return;
|
||||
}
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
private int loadImages(@Nonnull World world, int chunkViewRadius, int playerChunkX, int playerChunkZ, int maxGeneration) {
|
||||
List<MapChunk> currentLoadList = null;
|
||||
List<List<MapChunk>> allLoadLists = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
int packetSize = 2621427;
|
||||
int imageSize = MathUtil.fastFloor(32.0F * world.getWorldMapManager().getWorldMapSettings().getImageScale());
|
||||
int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
|
||||
boolean areAllLoaded = true;
|
||||
this.spiralIterator.init(playerChunkX, playerChunkZ, this.sentViewRadius, chunkViewRadius);
|
||||
|
||||
while (maxGeneration > 0 && this.spiralIterator.hasNext()) {
|
||||
long chunkCoordinates = this.spiralIterator.next();
|
||||
if (!this.loaded.contains(chunkCoordinates)) {
|
||||
areAllLoaded = false;
|
||||
CompletableFuture<MapImage> future = world.getWorldMapManager().getImageAsync(chunkCoordinates);
|
||||
if (!future.isDone()) {
|
||||
maxGeneration--;
|
||||
} else if (this.loaded.add(chunkCoordinates)) {
|
||||
if (currentLoadList == null) {
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
|
||||
int mapChunkX = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int mapChunkZ = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
currentLoadList.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
|
||||
packetSize -= fullMapChunkSize;
|
||||
if (packetSize < fullMapChunkSize) {
|
||||
packetSize = 2621427;
|
||||
if (allLoadLists == null) {
|
||||
allLoadLists = new ObjectArrayList<>();
|
||||
}
|
||||
|
||||
allLoadLists.add(currentLoadList);
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
}
|
||||
} else if (areAllLoaded) {
|
||||
this.sentViewRadius = this.spiralIterator.getCompletedRadius();
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllLoaded) {
|
||||
this.sentViewRadius = this.spiralIterator.getCompletedRadius();
|
||||
}
|
||||
|
||||
if (allLoadLists != null) {
|
||||
for (List<MapChunk> unloadList : allLoadLists) {
|
||||
this.writeUpdatePacket(unloadList);
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(currentLoadList);
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
return maxGeneration;
|
||||
}
|
||||
|
||||
private int loadWorldMap(@Nonnull World world, @Nonnull Box2D worldMapArea, int maxGeneration) {
|
||||
List<MapChunk> currentLoadList = null;
|
||||
List<List<MapChunk>> allLoadLists = null;
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
int packetSize = 2621427;
|
||||
int imageSize = MathUtil.fastFloor(32.0F * world.getWorldMapManager().getWorldMapSettings().getImageScale());
|
||||
int fullMapChunkSize = 23 + 4 * imageSize * imageSize;
|
||||
|
||||
for (int mapChunkX = MathUtil.floor(worldMapArea.min.x); mapChunkX < MathUtil.ceil(worldMapArea.max.x) && maxGeneration > 0; mapChunkX++) {
|
||||
for (int mapChunkZ = MathUtil.floor(worldMapArea.min.y); mapChunkZ < MathUtil.ceil(worldMapArea.max.y) && maxGeneration > 0; mapChunkZ++) {
|
||||
long chunkCoordinates = ChunkUtil.indexChunk(mapChunkX, mapChunkZ);
|
||||
if (!this.loaded.contains(chunkCoordinates)) {
|
||||
CompletableFuture<MapImage> future = CompletableFutureUtil._catch(world.getWorldMapManager().getImageAsync(chunkCoordinates));
|
||||
if (!future.isDone()) {
|
||||
maxGeneration--;
|
||||
} else {
|
||||
if (currentLoadList == null) {
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
|
||||
currentLoadList.add(new MapChunk(mapChunkX, mapChunkZ, future.getNow(null)));
|
||||
this.loaded.add(chunkCoordinates);
|
||||
packetSize -= fullMapChunkSize;
|
||||
if (packetSize < fullMapChunkSize) {
|
||||
packetSize = 2621427;
|
||||
if (allLoadLists == null) {
|
||||
allLoadLists = new ObjectArrayList<>(Math.max(packetSize / fullMapChunkSize, 1));
|
||||
}
|
||||
|
||||
allLoadLists.add(currentLoadList);
|
||||
currentLoadList = new ObjectArrayList<>(packetSize / fullMapChunkSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
if (allLoadLists != null) {
|
||||
for (List<MapChunk> unloadList : allLoadLists) {
|
||||
this.writeUpdatePacket(unloadList);
|
||||
}
|
||||
}
|
||||
|
||||
this.writeUpdatePacket(currentLoadList);
|
||||
return maxGeneration;
|
||||
}
|
||||
|
||||
private void writeUpdatePacket(@Nullable List<MapChunk> list) {
|
||||
if (list != null) {
|
||||
UpdateWorldMap packet = new UpdateWorldMap(list.toArray(MapChunk[]::new), null, null);
|
||||
LOGGER.at(Level.FINE).log("Sending world map update to %s - %d chunks", this.player.getUuid(), list.size());
|
||||
this.player.getPlayerConnection().write(packet);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Map<String, MapMarker> getSentMarkers() {
|
||||
return this.markerTracker.getSentMarkers();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Player getPlayer() {
|
||||
return this.player;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public TransformComponent getTransformComponent() {
|
||||
return this.transformComponent;
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
this.loaded.clear();
|
||||
this.sentViewRadius = 0;
|
||||
this.markerTracker.getSentMarkers().clear();
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
this.player.getPlayerConnection().write(new ClearWorldMap());
|
||||
}
|
||||
|
||||
public void clearChunks(@Nonnull LongSet chunkIndices) {
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
chunkIndices.forEach(index -> {
|
||||
this.loaded.remove(index);
|
||||
this.pendingReloadChunks.add(index);
|
||||
this.pendingReloadFutures.remove(index);
|
||||
});
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
|
||||
this.updateTimer = 0.0F;
|
||||
}
|
||||
|
||||
public void sendSettings(@Nonnull World world) {
|
||||
UpdateWorldMapSettings worldMapSettingsPacket = new UpdateWorldMapSettings(world.getWorldMapManager().getWorldMapSettings().getSettingsPacket());
|
||||
world.execute(() -> {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Ref<EntityStore> ref = this.player.getReference();
|
||||
if (ref != null) {
|
||||
Player playerComponent = store.getComponent(ref, Player.getComponentType());
|
||||
|
||||
assert playerComponent != null;
|
||||
|
||||
PlayerRef playerRefComponent = store.getComponent(ref, PlayerRef.getComponentType());
|
||||
|
||||
assert playerRefComponent != null;
|
||||
|
||||
worldMapSettingsPacket.allowTeleportToCoordinates = this.isAllowTeleportToCoordinates();
|
||||
worldMapSettingsPacket.allowTeleportToMarkers = this.isAllowTeleportToMarkers();
|
||||
playerRefComponent.getPacketHandler().write(worldMapSettingsPacket);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private boolean hasDiscoveredZone(@Nonnull String zoneName) {
|
||||
return this.player.getPlayerConfigData().getDiscoveredZones().contains(zoneName);
|
||||
}
|
||||
|
||||
public boolean discoverZone(@Nonnull World world, @Nonnull String zoneName) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (!discoveredZones.contains(zoneName)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.add(zoneName);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean undiscoverZone(@Nonnull World world, @Nonnull String zoneName) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (discoveredZones.contains(zoneName)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.remove(zoneName);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean discoverZones(@Nonnull World world, @Nonnull Set<String> zoneNames) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (!discoveredZones.containsAll(zoneNames)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.addAll(zoneNames);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean undiscoverZones(@Nonnull World world, @Nonnull Set<String> zoneNames) {
|
||||
Set<String> discoveredZones = this.player.getPlayerConfigData().getDiscoveredZones();
|
||||
if (discoveredZones.containsAll(zoneNames)) {
|
||||
Set<String> var4 = new HashSet<>(discoveredZones);
|
||||
var4.removeAll(zoneNames);
|
||||
this.player.getPlayerConfigData().setDiscoveredZones(var4);
|
||||
this.sendSettings(world);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isAllowTeleportToCoordinates() {
|
||||
return this.player.hasPermission("hytale.world_map.teleport.coordinate");
|
||||
}
|
||||
|
||||
public boolean isAllowTeleportToMarkers() {
|
||||
return this.player.hasPermission("hytale.world_map.teleport.marker");
|
||||
}
|
||||
|
||||
public void setPlayerMapFilter(Predicate<PlayerRef> playerMapFilter) {
|
||||
this.markerTracker.setPlayerMapFilter(playerMapFilter);
|
||||
}
|
||||
|
||||
public void setClientHasWorldMapVisible(boolean visible) {
|
||||
this.clientHasWorldMapVisible = visible;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Integer getViewRadiusOverride() {
|
||||
return this.viewRadiusOverride;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getCurrentBiomeName() {
|
||||
return this.currentBiomeName;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public WorldMapTracker.ZoneDiscoveryInfo getCurrentZone() {
|
||||
return this.currentZone;
|
||||
}
|
||||
|
||||
public void setViewRadiusOverride(@Nullable Integer viewRadiusOverride) {
|
||||
this.viewRadiusOverride = viewRadiusOverride;
|
||||
this.clear();
|
||||
}
|
||||
|
||||
public int getEffectiveViewRadius(@Nonnull World world) {
|
||||
return this.viewRadiusOverride != null
|
||||
? this.viewRadiusOverride
|
||||
: world.getWorldMapManager().getWorldMapSettings().getViewRadius(this.player.getViewRadius());
|
||||
}
|
||||
|
||||
public boolean shouldBeVisible(int chunkViewRadius, long chunkCoordinates) {
|
||||
if (this.player != null && this.transformComponent != null) {
|
||||
Vector3d position = this.transformComponent.getPosition();
|
||||
int chunkX = MathUtil.floor(position.getX()) >> 5;
|
||||
int chunkZ = MathUtil.floor(position.getZ()) >> 5;
|
||||
int x = ChunkUtil.xOfChunkIndex(chunkCoordinates);
|
||||
int z = ChunkUtil.zOfChunkIndex(chunkCoordinates);
|
||||
return shouldBeVisible(chunkViewRadius, chunkX, chunkZ, x, z);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void copyFrom(@Nonnull WorldMapTracker worldMapTracker) {
|
||||
this.loadedLock.writeLock().lock();
|
||||
|
||||
try {
|
||||
worldMapTracker.loadedLock.readLock().lock();
|
||||
|
||||
try {
|
||||
this.loaded.addAll(worldMapTracker.loaded);
|
||||
this.markerTracker.copyFrom(worldMapTracker.markerTracker);
|
||||
} finally {
|
||||
worldMapTracker.loadedLock.readLock().unlock();
|
||||
}
|
||||
} finally {
|
||||
this.loadedLock.writeLock().unlock();
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean shouldBeVisible(int chunkViewRadius, int chunkX, int chunkZ, int x, int z) {
|
||||
int xDiff = Math.abs(x - chunkX);
|
||||
int zDiff = Math.abs(z - chunkZ);
|
||||
int distanceSq = xDiff * xDiff + zDiff * zDiff;
|
||||
return distanceSq <= chunkViewRadius * chunkViewRadius;
|
||||
}
|
||||
|
||||
public record ZoneDiscoveryInfo(
|
||||
@Nonnull String zoneName,
|
||||
@Nonnull String regionName,
|
||||
boolean display,
|
||||
@Nullable String discoverySoundEventId,
|
||||
@Nullable String icon,
|
||||
boolean major,
|
||||
float duration,
|
||||
float fadeInDuration,
|
||||
float fadeOutDuration
|
||||
) {
|
||||
@Nonnull
|
||||
public WorldMapTracker.ZoneDiscoveryInfo clone() {
|
||||
return new WorldMapTracker.ZoneDiscoveryInfo(
|
||||
this.zoneName,
|
||||
this.regionName,
|
||||
this.display,
|
||||
this.discoverySoundEventId,
|
||||
this.icon,
|
||||
this.major,
|
||||
this.duration,
|
||||
this.fadeInDuration,
|
||||
this.fadeOutDuration
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ChunkStore> {
|
||||
public static final BuilderCodec<BlockComponentChunk> CODEC = BuilderCodec.builder(BlockComponentChunk.class, BlockComponentChunk::new)
|
||||
.addField(
|
||||
new KeyedCodec<>("BlockComponents", new Int2ObjectMapCodec<>(new StoredCodec<>(ChunkStore.HOLDER_CODEC_KEY), Int2ObjectOpenHashMap::new)),
|
||||
(entityChunk, map) -> {
|
||||
entityChunk.entityHolders.clear();
|
||||
entityChunk.entityHolders.putAll(map);
|
||||
},
|
||||
entityChunk -> {
|
||||
if (entityChunk.entityReferences.isEmpty()) {
|
||||
return entityChunk.entityHolders;
|
||||
} else {
|
||||
Int2ObjectMap<Holder<ChunkStore>> map = new Int2ObjectOpenHashMap<>(entityChunk.entityHolders.size() + entityChunk.entityReferences.size());
|
||||
map.putAll(entityChunk.entityHolders);
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entry : entityChunk.entityReferences.int2ObjectEntrySet()) {
|
||||
Ref<ChunkStore> reference = entry.getValue();
|
||||
Store<ChunkStore> store = reference.getStore();
|
||||
if (store.getArchetype(reference).hasSerializableComponents(store.getRegistry().getData())) {
|
||||
map.put(entry.getIntKey(), store.copySerializableEntity(reference));
|
||||
}
|
||||
}
|
||||
|
||||
return map;
|
||||
}
|
||||
}
|
||||
)
|
||||
.build();
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Holder<ChunkStore>> entityHolders;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Ref<ChunkStore>> entityReferences;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Holder<ChunkStore>> entityHoldersUnmodifiable;
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Ref<ChunkStore>> entityReferencesUnmodifiable;
|
||||
private boolean needsSaving;
|
||||
|
||||
public static ComponentType<ChunkStore, BlockComponentChunk> getComponentType() {
|
||||
return LegacyModule.get().getBlockComponentChunkComponentType();
|
||||
}
|
||||
|
||||
public BlockComponentChunk() {
|
||||
this.entityHolders = new Int2ObjectOpenHashMap<>();
|
||||
this.entityReferences = new Int2ObjectOpenHashMap<>();
|
||||
this.entityHoldersUnmodifiable = Int2ObjectMaps.unmodifiable(this.entityHolders);
|
||||
this.entityReferencesUnmodifiable = Int2ObjectMaps.unmodifiable(this.entityReferences);
|
||||
}
|
||||
|
||||
public BlockComponentChunk(@Nonnull Int2ObjectMap<Holder<ChunkStore>> entityHolders, @Nonnull Int2ObjectMap<Ref<ChunkStore>> entityReferences) {
|
||||
this.entityHolders = entityHolders;
|
||||
this.entityReferences = entityReferences;
|
||||
this.entityHoldersUnmodifiable = Int2ObjectMaps.unmodifiable(entityHolders);
|
||||
this.entityReferencesUnmodifiable = Int2ObjectMaps.unmodifiable(entityReferences);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<ChunkStore> clone() {
|
||||
Int2ObjectOpenHashMap<Holder<ChunkStore>> entityHoldersClone = new Int2ObjectOpenHashMap<>(this.entityHolders.size() + this.entityReferences.size());
|
||||
|
||||
for (Entry<Holder<ChunkStore>> entry : this.entityHolders.int2ObjectEntrySet()) {
|
||||
entityHoldersClone.put(entry.getIntKey(), entry.getValue().clone());
|
||||
}
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entry : this.entityReferences.int2ObjectEntrySet()) {
|
||||
Ref<ChunkStore> reference = entry.getValue();
|
||||
entityHoldersClone.put(entry.getIntKey(), reference.getStore().copyEntity(reference));
|
||||
}
|
||||
|
||||
return new BlockComponentChunk(entityHoldersClone, new Int2ObjectOpenHashMap<>());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<ChunkStore> cloneSerializable() {
|
||||
ComponentRegistry.Data<ChunkStore> data = ChunkStore.REGISTRY.getData();
|
||||
Int2ObjectOpenHashMap<Holder<ChunkStore>> entityHoldersClone = new Int2ObjectOpenHashMap<>(this.entityHolders.size() + this.entityReferences.size());
|
||||
|
||||
for (Entry<Holder<ChunkStore>> entry : this.entityHolders.int2ObjectEntrySet()) {
|
||||
Holder<ChunkStore> holder = entry.getValue();
|
||||
if (holder.getArchetype().hasSerializableComponents(data)) {
|
||||
entityHoldersClone.put(entry.getIntKey(), holder.cloneSerializable(data));
|
||||
}
|
||||
}
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entryx : this.entityReferences.int2ObjectEntrySet()) {
|
||||
Ref<ChunkStore> reference = entryx.getValue();
|
||||
Store<ChunkStore> store = reference.getStore();
|
||||
if (store.getArchetype(reference).hasSerializableComponents(data)) {
|
||||
entityHoldersClone.put(entryx.getIntKey(), store.copySerializableEntity(reference));
|
||||
}
|
||||
}
|
||||
|
||||
return new BlockComponentChunk(entityHoldersClone, new Int2ObjectOpenHashMap<>());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Int2ObjectMap<Holder<ChunkStore>> getEntityHolders() {
|
||||
return this.entityHoldersUnmodifiable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Holder<ChunkStore> getEntityHolder(int index) {
|
||||
return this.entityHolders.get(index);
|
||||
}
|
||||
|
||||
public void addEntityHolder(int index, @Nonnull Holder<ChunkStore> holder) {
|
||||
if (this.entityReferences.containsKey(index)) {
|
||||
throw new IllegalArgumentException("Duplicate block components at: " + index);
|
||||
} else if (this.entityHolders.putIfAbsent(index, Objects.requireNonNull(holder)) != null) {
|
||||
throw new IllegalArgumentException("Duplicate block components (entity holder) at: " + index);
|
||||
} else {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
}
|
||||
|
||||
public void storeEntityHolder(int index, @Nonnull Holder<ChunkStore> holder) {
|
||||
if (this.entityHolders.putIfAbsent(index, Objects.requireNonNull(holder)) != null) {
|
||||
throw new IllegalArgumentException("Duplicate block components (entity holder) at: " + index);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Holder<ChunkStore> removeEntityHolder(int index) {
|
||||
Holder<ChunkStore> reference = this.entityHolders.remove(index);
|
||||
if (reference != null) {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
|
||||
return reference;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Int2ObjectMap<Ref<ChunkStore>> getEntityReferences() {
|
||||
return this.entityReferencesUnmodifiable;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getEntityReference(int index) {
|
||||
return this.entityReferences.get(index);
|
||||
}
|
||||
|
||||
public void addEntityReference(int index, @Nonnull Ref<ChunkStore> reference) {
|
||||
reference.validate();
|
||||
// HyFix #8: Handle duplicate block components gracefully instead of throwing
|
||||
if (this.entityHolders.containsKey(index)) {
|
||||
System.out.println("[HyFix] WARNING: Duplicate block component detected at index " + index + " - ignoring (teleporter fix)");
|
||||
return;
|
||||
} else if (this.entityReferences.putIfAbsent(index, Objects.requireNonNull(reference)) != null) {
|
||||
System.out.println("[HyFix] WARNING: Duplicate block component (entity reference) detected at index " + index + " - ignoring (teleporter fix)");
|
||||
return;
|
||||
} else {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
}
|
||||
|
||||
public void loadEntityReference(int index, @Nonnull Ref<ChunkStore> reference) {
|
||||
reference.validate();
|
||||
if (this.entityHolders.containsKey(index)) {
|
||||
throw new IllegalArgumentException("Duplicate block components at: " + index);
|
||||
} else if (this.entityReferences.putIfAbsent(index, Objects.requireNonNull(reference)) != null) {
|
||||
throw new IllegalArgumentException("Duplicate block components (entity reference) at: " + index);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeEntityReference(int index, Ref<ChunkStore> reference) {
|
||||
if (this.entityReferences.remove(index, reference)) {
|
||||
this.markNeedsSaving();
|
||||
}
|
||||
}
|
||||
|
||||
public void unloadEntityReference(int index, Ref<ChunkStore> reference) {
|
||||
this.entityReferences.remove(index, reference);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Int2ObjectMap<Holder<ChunkStore>> takeEntityHolders() {
|
||||
if (this.entityHolders.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
Int2ObjectOpenHashMap<Holder<ChunkStore>> holders = new Int2ObjectOpenHashMap<>(this.entityHolders);
|
||||
this.entityHolders.clear();
|
||||
return holders;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Int2ObjectMap<Ref<ChunkStore>> takeEntityReferences() {
|
||||
if (this.entityReferences.isEmpty()) {
|
||||
return null;
|
||||
} else {
|
||||
Int2ObjectOpenHashMap<Ref<ChunkStore>> holders = new Int2ObjectOpenHashMap<>(this.entityReferences);
|
||||
this.entityReferences.clear();
|
||||
return holders;
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T extends Component<ChunkStore>> T getComponent(int index, @Nonnull ComponentType<ChunkStore, T> componentType) {
|
||||
Ref<ChunkStore> reference = this.entityReferences.get(index);
|
||||
if (reference != null) {
|
||||
return reference.getStore().getComponent(reference, componentType);
|
||||
} else {
|
||||
Holder<ChunkStore> holder = this.entityHolders.get(index);
|
||||
return holder != null ? holder.getComponent(componentType) : null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasComponents(int index) {
|
||||
return this.entityReferences.containsKey(index) || this.entityHolders.containsKey(index);
|
||||
}
|
||||
|
||||
public boolean getNeedsSaving() {
|
||||
return this.needsSaving;
|
||||
}
|
||||
|
||||
public void markNeedsSaving() {
|
||||
this.needsSaving = true;
|
||||
}
|
||||
|
||||
public boolean consumeNeedsSaving() {
|
||||
boolean out = this.needsSaving;
|
||||
this.needsSaving = false;
|
||||
return out;
|
||||
}
|
||||
|
||||
public static class BlockComponentChunkLoadingSystem extends RefChangeSystem<ChunkStore, NonTicking<ChunkStore>> {
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
private final Archetype<ChunkStore> archetype = Archetype.of(WorldChunk.getComponentType(), BlockComponentChunk.getComponentType());
|
||||
|
||||
public BlockComponentChunkLoadingSystem() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return this.archetype;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ComponentType<ChunkStore, NonTicking<ChunkStore>> componentType() {
|
||||
return ChunkStore.REGISTRY.getNonTickingComponentType();
|
||||
}
|
||||
|
||||
public void onComponentAdded(
|
||||
@Nonnull Ref<ChunkStore> ref,
|
||||
@Nonnull NonTicking<ChunkStore> component,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
BlockComponentChunk blockComponentChunk = store.getComponent(ref, BlockComponentChunk.getComponentType());
|
||||
Int2ObjectMap<Ref<ChunkStore>> entityReferences = blockComponentChunk.takeEntityReferences();
|
||||
if (entityReferences != null) {
|
||||
int size = entityReferences.size();
|
||||
int[] indexes = new int[size];
|
||||
Ref<ChunkStore>[] references = new Ref[size];
|
||||
int j = 0;
|
||||
|
||||
for (Entry<Ref<ChunkStore>> entry : entityReferences.int2ObjectEntrySet()) {
|
||||
indexes[j] = entry.getIntKey();
|
||||
references[j] = entry.getValue();
|
||||
j++;
|
||||
}
|
||||
|
||||
ComponentRegistry.Data<ChunkStore> data = ChunkStore.REGISTRY.getData();
|
||||
|
||||
for (int i = 0; i < size; i++) {
|
||||
if (store.getArchetype(references[i]).hasSerializableComponents(data)) {
|
||||
Holder<ChunkStore> holder = ChunkStore.REGISTRY.newHolder();
|
||||
commandBuffer.removeEntity(references[i], holder, RemoveReason.UNLOAD);
|
||||
blockComponentChunk.storeEntityHolder(indexes[i], holder);
|
||||
} else {
|
||||
commandBuffer.removeEntity(references[i], RemoveReason.UNLOAD);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void onComponentSet(
|
||||
@Nonnull Ref<ChunkStore> ref,
|
||||
NonTicking<ChunkStore> oldComponent,
|
||||
@Nonnull NonTicking<ChunkStore> newComponent,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
}
|
||||
|
||||
public void onComponentRemoved(
|
||||
@Nonnull Ref<ChunkStore> ref,
|
||||
@Nonnull NonTicking<ChunkStore> component,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
@Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
WorldChunk chunk = store.getComponent(ref, WorldChunk.getComponentType());
|
||||
BlockComponentChunk blockComponentChunk = store.getComponent(ref, BlockComponentChunk.getComponentType());
|
||||
Int2ObjectMap<Holder<ChunkStore>> entityHolders = blockComponentChunk.takeEntityHolders();
|
||||
if (entityHolders != null) {
|
||||
int holderCount = entityHolders.size();
|
||||
int[] indexes = new int[holderCount];
|
||||
Holder<ChunkStore>[] holders = new Holder[holderCount];
|
||||
int j = 0;
|
||||
|
||||
for (Entry<Holder<ChunkStore>> entry : entityHolders.int2ObjectEntrySet()) {
|
||||
indexes[j] = entry.getIntKey();
|
||||
holders[j] = entry.getValue();
|
||||
j++;
|
||||
}
|
||||
|
||||
for (int i = holderCount - 1; i >= 0; i--) {
|
||||
Holder<ChunkStore> holder = holders[i];
|
||||
if (holder.getArchetype().isEmpty()) {
|
||||
LOGGER.at(Level.SEVERE).log("Empty archetype entity holder: %s (#%d)", holder, i);
|
||||
holders[i] = holders[--holderCount];
|
||||
holders[holderCount] = holder;
|
||||
chunk.markNeedsSaving();
|
||||
} else {
|
||||
int index = indexes[i];
|
||||
int x = ChunkUtil.xFromBlockInColumn(index);
|
||||
int y = ChunkUtil.yFromBlockInColumn(index);
|
||||
int z = ChunkUtil.zFromBlockInColumn(index);
|
||||
holder.putComponent(BlockModule.BlockStateInfo.getComponentType(), new BlockModule.BlockStateInfo(index, ref));
|
||||
BlockState state = BlockState.getBlockState(holder);
|
||||
if (state != null) {
|
||||
state.setPosition(chunk, new Vector3i(x, y, z));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
commandBuffer.addEntities(holders, AddReason.LOAD);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class LoadBlockComponentPacketSystem extends ChunkStore.LoadPacketDataQuerySystem {
|
||||
private final ComponentType<ChunkStore, BlockComponentChunk> componentType;
|
||||
|
||||
public LoadBlockComponentPacketSystem(ComponentType<ChunkStore, BlockComponentChunk> blockComponentChunkComponentType) {
|
||||
this.componentType = blockComponentChunkComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return this.componentType;
|
||||
}
|
||||
|
||||
public void fetch(
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
CommandBuffer<ChunkStore> commandBuffer,
|
||||
PlayerRef player,
|
||||
@Nonnull List<Packet> results
|
||||
) {
|
||||
BlockComponentChunk component = archetypeChunk.getComponent(index, this.componentType);
|
||||
ObjectCollection<Ref<ChunkStore>> references = component.entityReferences.values();
|
||||
Store<ChunkStore> componentStore = store.getExternalData().getWorld().getChunkStore().getStore();
|
||||
componentStore.fetch(references, ChunkStore.LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE, player, results);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UnloadBlockComponentPacketSystem extends ChunkStore.UnloadPacketDataQuerySystem {
|
||||
private final ComponentType<ChunkStore, BlockComponentChunk> componentType;
|
||||
|
||||
public UnloadBlockComponentPacketSystem(ComponentType<ChunkStore, BlockComponentChunk> blockComponentChunkComponentType) {
|
||||
this.componentType = blockComponentChunkComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Query<ChunkStore> getQuery() {
|
||||
return this.componentType;
|
||||
}
|
||||
|
||||
public void fetch(
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<ChunkStore> archetypeChunk,
|
||||
@Nonnull Store<ChunkStore> store,
|
||||
CommandBuffer<ChunkStore> commandBuffer,
|
||||
PlayerRef player,
|
||||
@Nonnull List<Packet> results
|
||||
) {
|
||||
BlockComponentChunk component = archetypeChunk.getComponent(index, this.componentType);
|
||||
ObjectCollection<Ref<ChunkStore>> references = component.entityReferences.values();
|
||||
Store<ChunkStore> componentStore = store.getExternalData().getWorld().getChunkStore().getStore();
|
||||
componentStore.fetch(references, ChunkStore.UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE, player, results);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Vector3i> set = ConcurrentHashMap.newKeySet();
|
||||
private final ObjectArrayFIFOQueue<Vector3i> queue = new ObjectArrayFIFOQueue<>();
|
||||
private LightCalculation lightCalculation;
|
||||
|
||||
// HyFix: Periodic chunk lighting invalidation to prevent memory leaks
|
||||
private static final long PERIODIC_INVALIDATION_INTERVAL_MS = 30000; // 30 seconds
|
||||
private long lastPeriodicInvalidation = 0;
|
||||
|
||||
public ChunkLightingManager(@Nonnull World world) {
|
||||
this.logger = HytaleLogger.get("World|" + world.getName() + "|L");
|
||||
this.thread = new Thread(this, "ChunkLighting - " + world.getName());
|
||||
this.thread.setDaemon(true);
|
||||
this.world = world;
|
||||
this.lightCalculation = new FloodLightCalculation(this);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
protected HytaleLogger getLogger() {
|
||||
return this.logger;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public World getWorld() {
|
||||
return this.world;
|
||||
}
|
||||
|
||||
public void setLightCalculation(LightCalculation lightCalculation) {
|
||||
this.lightCalculation = lightCalculation;
|
||||
}
|
||||
|
||||
public LightCalculation getLightCalculation() {
|
||||
return this.lightCalculation;
|
||||
}
|
||||
|
||||
public void start() {
|
||||
this.thread.start();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
int lastSize = 0;
|
||||
int count = 0;
|
||||
this.lastPeriodicInvalidation = System.currentTimeMillis();
|
||||
|
||||
while (!this.thread.isInterrupted()) {
|
||||
this.semaphore.drainPermits();
|
||||
Vector3i pos;
|
||||
synchronized (this.queue) {
|
||||
pos = this.queue.isEmpty() ? null : this.queue.dequeue();
|
||||
}
|
||||
|
||||
if (pos != null) {
|
||||
this.process(pos);
|
||||
}
|
||||
|
||||
// HyFix: Periodic chunk lighting invalidation to prevent memory leaks and stale lighting
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - this.lastPeriodicInvalidation >= PERIODIC_INVALIDATION_INTERVAL_MS) {
|
||||
this.lastPeriodicInvalidation = now;
|
||||
try {
|
||||
this.invalidateLoadedChunks();
|
||||
} catch (Exception e) {
|
||||
// Silently handle errors - may occur during world transitions
|
||||
}
|
||||
}
|
||||
|
||||
Thread.yield();
|
||||
int currentSize;
|
||||
synchronized (this.queue) {
|
||||
currentSize = this.queue.size();
|
||||
}
|
||||
|
||||
if (currentSize != lastSize) {
|
||||
count = 0;
|
||||
lastSize = currentSize;
|
||||
} else if (count <= currentSize) {
|
||||
count++;
|
||||
} else {
|
||||
this.semaphore.acquire();
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException var9) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
private void process(Vector3i chunkPosition) {
|
||||
try {
|
||||
switch (this.lightCalculation.calculateLight(chunkPosition)) {
|
||||
case NOT_LOADED:
|
||||
case WAITING_FOR_NEIGHBOUR:
|
||||
case DONE:
|
||||
this.set.remove(chunkPosition);
|
||||
break;
|
||||
case INVALIDATED:
|
||||
synchronized (this.queue) {
|
||||
this.queue.enqueue(chunkPosition);
|
||||
}
|
||||
}
|
||||
} catch (Exception var5) {
|
||||
this.logger.at(Level.WARNING).withCause(var5).log("Failed to calculate lighting for: %s", chunkPosition);
|
||||
this.set.remove(chunkPosition);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean interrupt() {
|
||||
if (this.thread.isAlive()) {
|
||||
this.thread.interrupt();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public void stop() {
|
||||
try {
|
||||
int i = 0;
|
||||
|
||||
while (this.thread.isAlive()) {
|
||||
this.thread.interrupt();
|
||||
this.thread.join(this.world.getTickStepNanos() / 1000000);
|
||||
i += this.world.getTickStepNanos() / 1000000;
|
||||
if (i > 5000) {
|
||||
StringBuilder sb = new StringBuilder();
|
||||
|
||||
for (StackTraceElement traceElement : this.thread.getStackTrace()) {
|
||||
sb.append("\tat ").append(traceElement).append('\n');
|
||||
}
|
||||
|
||||
HytaleLogger.getLogger().at(Level.SEVERE).log("Forcing ChunkLighting Thread %s to stop:\n%s", this.thread, sb.toString());
|
||||
this.thread.stop();
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (InterruptedException var7) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
public void init(WorldChunk worldChunk) {
|
||||
this.lightCalculation.init(worldChunk);
|
||||
}
|
||||
|
||||
public void addToQueue(Vector3i chunkPosition) {
|
||||
if (this.set.add(chunkPosition)) {
|
||||
synchronized (this.queue) {
|
||||
this.queue.enqueue(chunkPosition);
|
||||
}
|
||||
|
||||
this.semaphore.release(1);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isQueued(int chunkX, int chunkZ) {
|
||||
Vector3i chunkPos = new Vector3i(chunkX, 0, chunkZ);
|
||||
|
||||
for (int chunkY = 0; chunkY < 10; chunkY++) {
|
||||
chunkPos.setY(chunkY);
|
||||
if (this.isQueued(chunkPos)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean isQueued(Vector3i chunkPosition) {
|
||||
return this.set.contains(chunkPosition);
|
||||
}
|
||||
|
||||
public int getQueueSize() {
|
||||
synchronized (this.queue) {
|
||||
return this.queue.size();
|
||||
}
|
||||
}
|
||||
|
||||
public boolean invalidateLightAtBlock(WorldChunk worldChunk, int blockX, int blockY, int blockZ, BlockType blockType, int oldHeight, int newHeight) {
|
||||
return this.lightCalculation.invalidateLightAtBlock(worldChunk, blockX, blockY, blockZ, blockType, oldHeight, newHeight);
|
||||
}
|
||||
|
||||
public boolean invalidateLightInChunk(WorldChunk worldChunk) {
|
||||
return this.lightCalculation.invalidateLightInChunkSections(worldChunk, 0, 10);
|
||||
}
|
||||
|
||||
public boolean invalidateLightInChunkSection(WorldChunk worldChunk, int sectionIndex) {
|
||||
return this.lightCalculation.invalidateLightInChunkSections(worldChunk, sectionIndex, sectionIndex + 1);
|
||||
}
|
||||
|
||||
public boolean invalidateLightInChunkSections(WorldChunk worldChunk, int sectionIndexFrom, int sectionIndexTo) {
|
||||
return this.lightCalculation.invalidateLightInChunkSections(worldChunk, sectionIndexFrom, sectionIndexTo);
|
||||
}
|
||||
|
||||
public void invalidateLoadedChunks() {
|
||||
this.world.getChunkStore().getStore().forEachEntityParallel(WorldChunk.getComponentType(), (index, archetypeChunk, storeCommandBuffer) -> {
|
||||
WorldChunk chunk = archetypeChunk.getComponent(index, WorldChunk.getComponentType());
|
||||
|
||||
for (int y = 0; y < 10; y++) {
|
||||
BlockSection section = chunk.getBlockChunk().getSectionAtIndex(y);
|
||||
section.invalidateLocalLight();
|
||||
if (BlockChunk.SEND_LOCAL_LIGHTING_DATA || BlockChunk.SEND_GLOBAL_LIGHTING_DATA) {
|
||||
chunk.getBlockChunk().invalidateChunkSection(y);
|
||||
}
|
||||
}
|
||||
});
|
||||
this.world.getChunkStore().getChunkIndexes().forEach(index -> {
|
||||
int x = ChunkUtil.xOfChunkIndex(index);
|
||||
int z = ChunkUtil.zOfChunkIndex(index);
|
||||
|
||||
for (int y = 0; y < 10; y++) {
|
||||
this.addToQueue(new Vector3i(x, y, z));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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<ChunkStore> {
|
||||
public static final BuilderCodec<RespawnBlock> 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<ChunkStore, RespawnBlock> 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<ChunkStore> clone() {
|
||||
return new RespawnBlock(this.ownerUUID);
|
||||
}
|
||||
|
||||
public static class OnRemove extends RefSystem<ChunkStore> {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final ComponentType<ChunkStore, RespawnBlock> COMPONENT_TYPE_RESPAWN_BLOCK = RespawnBlock.getComponentType();
|
||||
public static final ComponentType<ChunkStore, BlockModule.BlockStateInfo> COMPONENT_TYPE_BLOCK_STATE_INFO = BlockModule.BlockStateInfo.getComponentType();
|
||||
@Nonnull
|
||||
public static final Query<ChunkStore> QUERY = Query.and(COMPONENT_TYPE_RESPAWN_BLOCK, COMPONENT_TYPE_BLOCK_STATE_INFO);
|
||||
|
||||
public OnRemove() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<ChunkStore> ref, @Nonnull AddReason reason, @Nonnull Store<ChunkStore> store, @Nonnull CommandBuffer<ChunkStore> commandBuffer
|
||||
) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<ChunkStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<ChunkStore> store, @Nonnull CommandBuffer<ChunkStore> 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<ChunkStore> 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<ChunkStore> getQuery() {
|
||||
return QUERY;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<ChunkStore> METRICS_REGISTRY = new MetricsRegistry<ChunkStore>()
|
||||
.register("Store", ChunkStore::getStore, Store.METRICS_REGISTRY)
|
||||
.register("ChunkLoader", MetricProvider.maybe(ChunkStore::getLoader))
|
||||
.register("ChunkSaver", MetricProvider.maybe(ChunkStore::getSaver))
|
||||
.register("WorldGen", MetricProvider.maybe(ChunkStore::getGenerator))
|
||||
.register("TotalGeneratedChunkCount", chunkComponentStore -> (long)chunkComponentStore.totalGeneratedChunksCount.get(), Codec.LONG)
|
||||
.register("TotalLoadedChunkCount", chunkComponentStore -> (long)chunkComponentStore.totalLoadedChunksCount.get(), Codec.LONG);
|
||||
public static final long MAX_FAILURE_BACKOFF_NANOS = TimeUnit.SECONDS.toNanos(10L);
|
||||
public static final long FAILURE_BACKOFF_NANOS = TimeUnit.MILLISECONDS.toNanos(1L);
|
||||
public static final ComponentRegistry<ChunkStore> REGISTRY = new ComponentRegistry<>();
|
||||
public static final CodecKey<Holder<ChunkStore>> HOLDER_CODEC_KEY = new CodecKey<>("ChunkHolder");
|
||||
@Nonnull
|
||||
public static final SystemType<ChunkStore, ChunkStore.LoadPacketDataQuerySystem> LOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType(
|
||||
ChunkStore.LoadPacketDataQuerySystem.class
|
||||
);
|
||||
@Nonnull
|
||||
public static final SystemType<ChunkStore, ChunkStore.LoadFuturePacketDataQuerySystem> LOAD_FUTURE_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType(
|
||||
ChunkStore.LoadFuturePacketDataQuerySystem.class
|
||||
);
|
||||
@Nonnull
|
||||
public static final SystemType<ChunkStore, ChunkStore.UnloadPacketDataQuerySystem> UNLOAD_PACKETS_DATA_QUERY_SYSTEM_TYPE = REGISTRY.registerSystemType(
|
||||
ChunkStore.UnloadPacketDataQuerySystem.class
|
||||
);
|
||||
@Nonnull
|
||||
public static final ResourceType<ChunkStore, ChunkUnloadingSystem.Data> UNLOAD_RESOURCE = REGISTRY.registerResource(
|
||||
ChunkUnloadingSystem.Data.class, ChunkUnloadingSystem.Data::new
|
||||
);
|
||||
@Nonnull
|
||||
public static final ResourceType<ChunkStore, ChunkSavingSystems.Data> SAVE_RESOURCE = REGISTRY.registerResource(
|
||||
ChunkSavingSystems.Data.class, ChunkSavingSystems.Data::new
|
||||
);
|
||||
public static final SystemGroup<ChunkStore> INIT_GROUP = REGISTRY.registerSystemGroup();
|
||||
@Nonnull
|
||||
private final World world;
|
||||
@Nonnull
|
||||
private final Long2ObjectConcurrentHashMap<ChunkStore.ChunkLoadState> chunks = new Long2ObjectConcurrentHashMap<>(true, ChunkUtil.NOT_FOUND);
|
||||
private Store<ChunkStore> store;
|
||||
@Nullable
|
||||
private IChunkLoader loader;
|
||||
@Nullable
|
||||
private IChunkSaver saver;
|
||||
@Nullable
|
||||
private IWorldGen generator;
|
||||
@Nonnull
|
||||
private CompletableFuture<Void> generatorLoaded = new CompletableFuture<>();
|
||||
private final StampedLock generatorLock = new StampedLock();
|
||||
private final AtomicInteger totalGeneratedChunksCount = new AtomicInteger();
|
||||
private final AtomicInteger totalLoadedChunksCount = new AtomicInteger();
|
||||
|
||||
public ChunkStore(@Nonnull World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public World getWorld() {
|
||||
return this.world;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Store<ChunkStore> getStore() {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IChunkLoader getLoader() {
|
||||
return this.loader;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IChunkSaver getSaver() {
|
||||
return this.saver;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public IWorldGen getGenerator() {
|
||||
long readStamp = this.generatorLock.readLock();
|
||||
|
||||
IWorldGen var3;
|
||||
try {
|
||||
var3 = this.generator;
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStamp);
|
||||
}
|
||||
|
||||
return var3;
|
||||
}
|
||||
|
||||
public void shutdownGenerator() {
|
||||
this.setGenerator(null);
|
||||
}
|
||||
|
||||
public void setGenerator(@Nullable IWorldGen generator) {
|
||||
long writeStamp = this.generatorLock.writeLock();
|
||||
|
||||
try {
|
||||
if (this.generator != null) {
|
||||
this.generator.shutdown();
|
||||
}
|
||||
|
||||
this.totalGeneratedChunksCount.set(0);
|
||||
this.generator = generator;
|
||||
if (generator != null) {
|
||||
this.generatorLoaded.complete(null);
|
||||
this.generatorLoaded = new CompletableFuture<>();
|
||||
}
|
||||
} finally {
|
||||
this.generatorLock.unlockWrite(writeStamp);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public LongSet getChunkIndexes() {
|
||||
return LongSets.unmodifiable(this.chunks.keySet());
|
||||
}
|
||||
|
||||
public int getLoadedChunksCount() {
|
||||
return this.chunks.size();
|
||||
}
|
||||
|
||||
public int getTotalGeneratedChunksCount() {
|
||||
return this.totalGeneratedChunksCount.get();
|
||||
}
|
||||
|
||||
public int getTotalLoadedChunksCount() {
|
||||
return this.totalLoadedChunksCount.get();
|
||||
}
|
||||
|
||||
public void start(@Nonnull IResourceStorage resourceStorage) {
|
||||
this.store = REGISTRY.addStore(this, resourceStorage, store -> this.store = store);
|
||||
}
|
||||
|
||||
public void waitForLoadingChunks() {
|
||||
long start = System.nanoTime();
|
||||
|
||||
boolean hasLoadingChunks;
|
||||
do {
|
||||
this.world.consumeTaskQueue();
|
||||
Thread.yield();
|
||||
hasLoadingChunks = false;
|
||||
|
||||
for (Entry<ChunkStore.ChunkLoadState> entry : this.chunks.long2ObjectEntrySet()) {
|
||||
ChunkStore.ChunkLoadState chunkState = entry.getValue();
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
try {
|
||||
CompletableFuture<Ref<ChunkStore>> future = chunkState.future;
|
||||
if (future != null && !future.isDone()) {
|
||||
hasLoadingChunks = true;
|
||||
break;
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
}
|
||||
} while (hasLoadingChunks && System.nanoTime() - start <= 5000000000L);
|
||||
|
||||
this.world.consumeTaskQueue();
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.store.shutdown();
|
||||
this.chunks.clear();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Ref<ChunkStore> add(@Nonnull Holder<ChunkStore> holder) {
|
||||
this.world.debugAssertInTickingThread();
|
||||
WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(worldChunkComponent.getIndex());
|
||||
if (chunkState == null) {
|
||||
throw new IllegalStateException("Expected the ChunkLoadState to exist!");
|
||||
} else {
|
||||
Ref<ChunkStore> oldReference = null;
|
||||
long stamp = chunkState.lock.writeLock();
|
||||
|
||||
try {
|
||||
if (chunkState.future == null) {
|
||||
throw new IllegalStateException("Expected the ChunkLoadState to have a future!");
|
||||
}
|
||||
|
||||
if (chunkState.reference != null) {
|
||||
oldReference = chunkState.reference;
|
||||
chunkState.reference = null;
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
}
|
||||
|
||||
if (oldReference != null) {
|
||||
WorldChunk oldWorldChunkComponent = this.store.getComponent(oldReference, WorldChunk.getComponentType());
|
||||
|
||||
assert oldWorldChunkComponent != null;
|
||||
|
||||
oldWorldChunkComponent.setFlag(ChunkFlag.TICKING, false);
|
||||
this.store.removeEntity(oldReference, RemoveReason.REMOVE);
|
||||
this.world.getNotificationHandler().updateChunk(worldChunkComponent.getIndex());
|
||||
}
|
||||
|
||||
oldReference = this.store.addEntity(holder, AddReason.SPAWN);
|
||||
if (oldReference == null) {
|
||||
throw new UnsupportedOperationException("Unable to add the chunk to the world!");
|
||||
} else {
|
||||
worldChunkComponent.setReference(oldReference);
|
||||
stamp = chunkState.lock.writeLock();
|
||||
|
||||
Ref var17;
|
||||
try {
|
||||
chunkState.reference = oldReference;
|
||||
chunkState.flags = 0;
|
||||
chunkState.future = null;
|
||||
chunkState.throwable = null;
|
||||
chunkState.failedWhen = 0L;
|
||||
chunkState.failedCounter = 0;
|
||||
var17 = oldReference;
|
||||
} finally {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
}
|
||||
|
||||
return var17;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void remove(@Nonnull Ref<ChunkStore> reference, @Nonnull RemoveReason reason) {
|
||||
this.world.debugAssertInTickingThread();
|
||||
WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
long index = worldChunkComponent.getIndex();
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(index);
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
try {
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, false);
|
||||
this.store.removeEntity(reference, reason);
|
||||
if (chunkState.future != null) {
|
||||
chunkState.reference = null;
|
||||
} else {
|
||||
this.chunks.remove(index, chunkState);
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getChunkReference(long index) {
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(index);
|
||||
if (chunkState == null) {
|
||||
return null;
|
||||
} else {
|
||||
long stamp = chunkState.lock.tryOptimisticRead();
|
||||
Ref<ChunkStore> reference = chunkState.reference;
|
||||
if (chunkState.lock.validate(stamp)) {
|
||||
return reference;
|
||||
} else {
|
||||
stamp = chunkState.lock.readLock();
|
||||
|
||||
Ref var7;
|
||||
try {
|
||||
var7 = chunkState.reference;
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
|
||||
return var7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getChunkSectionReference(int x, int y, int z) {
|
||||
Ref<ChunkStore> ref = this.getChunkReference(ChunkUtil.indexChunk(x, z));
|
||||
if (ref == null) {
|
||||
return null;
|
||||
} else {
|
||||
ChunkColumn chunkColumnComponent = this.store.getComponent(ref, ChunkColumn.getComponentType());
|
||||
return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<ChunkStore> getChunkSectionReference(@Nonnull ComponentAccessor<ChunkStore> commandBuffer, int x, int y, int z) {
|
||||
Ref<ChunkStore> ref = this.getChunkReference(ChunkUtil.indexChunk(x, z));
|
||||
if (ref == null) {
|
||||
return null;
|
||||
} else {
|
||||
ChunkColumn chunkColumnComponent = commandBuffer.getComponent(ref, ChunkColumn.getComponentType());
|
||||
return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y);
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Ref<ChunkStore>> getChunkSectionReferenceAsync(int x, int y, int z) {
|
||||
return y >= 0 && y < 10 ? this.getChunkReferenceAsync(ChunkUtil.indexChunk(x, z)).thenApplyAsync(ref -> {
|
||||
if (ref != null && ref.isValid()) {
|
||||
Store<ChunkStore> store = ref.getStore();
|
||||
ChunkColumn chunkColumnComponent = store.getComponent((Ref<ChunkStore>)ref, ChunkColumn.getComponentType());
|
||||
return chunkColumnComponent == null ? null : chunkColumnComponent.getSection(y);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}, this.store.getExternalData().getWorld()) : CompletableFuture.failedFuture(new IndexOutOfBoundsException("Invalid y: " + y));
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public <T extends Component<ChunkStore>> T getChunkComponent(long index, @Nonnull ComponentType<ChunkStore, T> componentType) {
|
||||
Ref<ChunkStore> reference = this.getChunkReference(index);
|
||||
return reference != null && reference.isValid() ? this.store.getComponent(reference, componentType) : null;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Ref<ChunkStore>> getChunkReferenceAsync(long index) {
|
||||
return this.getChunkReferenceAsync(index, 0);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public CompletableFuture<Ref<ChunkStore>> getChunkReferenceAsync(long index, int flags) {
|
||||
if (this.store.isShutdown()) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
} else {
|
||||
ChunkStore.ChunkLoadState chunkState;
|
||||
if ((flags & 3) == 3) {
|
||||
chunkState = this.chunks.get(index);
|
||||
if (chunkState == null) {
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
try {
|
||||
if ((flags & 4) == 0 || (chunkState.flags & 4) != 0) {
|
||||
if (chunkState.reference != null) {
|
||||
return CompletableFuture.completedFuture(chunkState.reference);
|
||||
}
|
||||
|
||||
if (chunkState.future != null) {
|
||||
return chunkState.future;
|
||||
}
|
||||
|
||||
return CompletableFuture.completedFuture(null);
|
||||
}
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
} else {
|
||||
chunkState = this.chunks.computeIfAbsent(index, l -> new ChunkStore.ChunkLoadState());
|
||||
}
|
||||
|
||||
long stamp = chunkState.lock.writeLock();
|
||||
if (chunkState.future == null && chunkState.reference != null && (flags & 8) == 0) {
|
||||
Ref<ChunkStore> reference = chunkState.reference;
|
||||
if ((flags & 4) == 0) {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
return CompletableFuture.completedFuture(reference);
|
||||
} else if (this.world.isInThread() && (flags & -2147483648) == 0) {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, true);
|
||||
return CompletableFuture.completedFuture(reference);
|
||||
} else {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
return CompletableFuture.supplyAsync(() -> {
|
||||
WorldChunk worldChunkComponent = this.store.getComponent(reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, true);
|
||||
return reference;
|
||||
}, this.world);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (chunkState.throwable != null) {
|
||||
long nanosSince = System.nanoTime() - chunkState.failedWhen;
|
||||
int count = chunkState.failedCounter;
|
||||
if (nanosSince < Math.min(MAX_FAILURE_BACKOFF_NANOS, count * count * FAILURE_BACKOFF_NANOS)) {
|
||||
return CompletableFuture.failedFuture(new RuntimeException("Chunk failure backoff", chunkState.throwable));
|
||||
}
|
||||
|
||||
chunkState.throwable = null;
|
||||
chunkState.failedWhen = 0L;
|
||||
}
|
||||
|
||||
boolean isNew = chunkState.future == null;
|
||||
if (isNew) {
|
||||
chunkState.flags = flags;
|
||||
}
|
||||
|
||||
int x = ChunkUtil.xOfChunkIndex(index);
|
||||
int z = ChunkUtil.zOfChunkIndex(index);
|
||||
int seed = (int)this.world.getWorldConfig().getSeed();
|
||||
if ((isNew || (chunkState.flags & 1) != 0) && (flags & 1) == 0) {
|
||||
if (chunkState.future == null) {
|
||||
chunkState.future = this.loader.loadHolder(x, z).thenApplyAsync(holder -> {
|
||||
if (holder != null && !this.store.isShutdown()) {
|
||||
this.totalLoadedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, (Holder<ChunkStore>)holder, false);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).exceptionallyCompose(throwable -> {
|
||||
// Corruption detected during load - recover by regenerating
|
||||
return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable);
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world);
|
||||
} else {
|
||||
chunkState.flags &= -2;
|
||||
chunkState.future = chunkState.future
|
||||
.thenCompose(
|
||||
reference -> reference != null
|
||||
? CompletableFuture.completedFuture((Ref<ChunkStore>)reference)
|
||||
: this.loader.loadHolder(x, z).thenApplyAsync(holder -> {
|
||||
if (holder != null && !this.store.isShutdown()) {
|
||||
this.totalLoadedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, (Holder<ChunkStore>)holder, false);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).exceptionallyCompose(throwable -> {
|
||||
// Corruption detected during load - recover by regenerating
|
||||
return this.handleCorruptionAndRegenerate(index, x, z, seed, throwable);
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ((isNew || (chunkState.flags & 2) != 0) && (flags & 2) == 0) {
|
||||
if (chunkState.future == null) {
|
||||
long readStamp = this.generatorLock.readLock();
|
||||
|
||||
CompletableFuture<GeneratedChunk> future;
|
||||
try {
|
||||
if (this.generator == null) {
|
||||
future = this.generatorLoaded
|
||||
.thenCompose(aVoid -> this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null));
|
||||
} else {
|
||||
future = this.generator.generate(seed, index, x, z, (flags & 16) != 0 ? this::isChunkStillNeeded : null);
|
||||
}
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStamp);
|
||||
}
|
||||
|
||||
chunkState.future = future.<Holder<ChunkStore>>thenApplyAsync(generatedChunk -> {
|
||||
if (generatedChunk != null && !this.store.isShutdown()) {
|
||||
this.totalGeneratedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> {
|
||||
LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z);
|
||||
chunkState.fail(throwable);
|
||||
throw SneakyThrow.sneakyThrow(throwable);
|
||||
});
|
||||
} else {
|
||||
chunkState.flags &= -3;
|
||||
chunkState.future = chunkState.future.thenCompose(reference -> {
|
||||
if (reference != null) {
|
||||
return CompletableFuture.completedFuture((Ref<ChunkStore>)reference);
|
||||
} else {
|
||||
long readStampx = this.generatorLock.readLock();
|
||||
|
||||
CompletableFuture<GeneratedChunk> future;
|
||||
try {
|
||||
if (this.generator == null) {
|
||||
future = this.generatorLoaded.thenCompose(aVoid -> this.generator.generate(seed, index, x, z, null));
|
||||
} else {
|
||||
future = this.generator.generate(seed, index, x, z, null);
|
||||
}
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStampx);
|
||||
}
|
||||
|
||||
return future.<Holder<ChunkStore>>thenApplyAsync(generatedChunk -> {
|
||||
if (generatedChunk != null && !this.store.isShutdown()) {
|
||||
this.totalGeneratedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}).thenApplyAsync(this::postLoadChunk, this.world).exceptionally(throwable -> {
|
||||
LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to generate chunk! %s, %s", x, z);
|
||||
chunkState.fail(throwable);
|
||||
throw SneakyThrow.sneakyThrow(throwable);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if ((isNew || (chunkState.flags & 4) == 0) && (flags & 4) != 0) {
|
||||
chunkState.flags |= 4;
|
||||
if (chunkState.future != null) {
|
||||
chunkState.future = chunkState.future.<Ref<ChunkStore>>thenApplyAsync(reference -> {
|
||||
if (reference != null) {
|
||||
WorldChunk worldChunkComponent = this.store.getComponent((Ref<ChunkStore>)reference, WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.TICKING, true);
|
||||
}
|
||||
|
||||
return reference;
|
||||
}, this.world).exceptionally(throwable -> {
|
||||
LOGGER.at(Level.SEVERE).withCause(throwable).log("Failed to set chunk ticking! %s, %s", x, z);
|
||||
chunkState.fail(throwable);
|
||||
throw SneakyThrow.sneakyThrow(throwable);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return chunkState.future != null ? chunkState.future : CompletableFuture.completedFuture(null);
|
||||
} finally {
|
||||
chunkState.lock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isChunkStillNeeded(long index) {
|
||||
for (PlayerRef playerRef : this.world.getPlayerRefs()) {
|
||||
if (playerRef.getChunkTracker().shouldBeVisible(index)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles corruption recovery by logging the corruption, removing the corrupted chunk data,
|
||||
* and triggering regeneration.
|
||||
*/
|
||||
@Nonnull
|
||||
private CompletableFuture<Holder<ChunkStore>> handleCorruptionAndRegenerate(
|
||||
long index, int x, int z, int seed, Throwable corruptionCause) {
|
||||
|
||||
// Log corruption to file
|
||||
CorruptedChunkLogger.logCorruption(this.world.getName(), x, z, corruptionCause);
|
||||
|
||||
LOGGER.at(Level.WARNING).log(
|
||||
"Corrupted chunk detected at (%d, %d) in world '%s' - removing and regenerating. See logs/corrupted_chunks.log for details.",
|
||||
x, z, this.world.getName()
|
||||
);
|
||||
|
||||
// Remove the corrupted chunk data
|
||||
CompletableFuture<Void> 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<GeneratedChunk> genFuture;
|
||||
if (this.generator == null) {
|
||||
genFuture = this.generatorLoaded.thenCompose(
|
||||
aVoid -> this.generator.generate(seed, index, x, z, null)
|
||||
);
|
||||
} else {
|
||||
genFuture = this.generator.generate(seed, index, x, z, null);
|
||||
}
|
||||
return genFuture.thenApplyAsync(generatedChunk -> {
|
||||
if (generatedChunk != null && !this.store.isShutdown()) {
|
||||
this.totalGeneratedChunksCount.getAndIncrement();
|
||||
return this.preLoadChunkAsync(index, generatedChunk.toHolder(this.world), true);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
});
|
||||
} finally {
|
||||
this.generatorLock.unlockRead(readStamp);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public boolean isChunkOnBackoff(long index, long maxFailureBackoffNanos) {
|
||||
ChunkStore.ChunkLoadState chunkState = this.chunks.get(index);
|
||||
if (chunkState == null) {
|
||||
return false;
|
||||
} else {
|
||||
long stamp = chunkState.lock.readLock();
|
||||
|
||||
boolean nanosSince;
|
||||
try {
|
||||
if (chunkState.throwable != null) {
|
||||
long nanosSincex = System.nanoTime() - chunkState.failedWhen;
|
||||
int count = chunkState.failedCounter;
|
||||
return nanosSincex < Math.min(maxFailureBackoffNanos, count * count * FAILURE_BACKOFF_NANOS);
|
||||
}
|
||||
|
||||
nanosSince = false;
|
||||
} finally {
|
||||
chunkState.lock.unlockRead(stamp);
|
||||
}
|
||||
|
||||
return nanosSince;
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Holder<ChunkStore> preLoadChunkAsync(long index, @Nonnull Holder<ChunkStore> holder, boolean newlyGenerated) {
|
||||
WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType());
|
||||
if (worldChunkComponent == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Holder missing WorldChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index))
|
||||
);
|
||||
} else if (worldChunkComponent.getIndex() != index) {
|
||||
throw new IllegalStateException(
|
||||
String.format(
|
||||
"Incorrect chunk index! Got (%d, %d) expected (%d, %d)",
|
||||
worldChunkComponent.getX(),
|
||||
worldChunkComponent.getZ(),
|
||||
ChunkUtil.xOfChunkIndex(index),
|
||||
ChunkUtil.zOfChunkIndex(index)
|
||||
)
|
||||
);
|
||||
} else {
|
||||
BlockChunk blockChunk = holder.getComponent(BlockChunk.getComponentType());
|
||||
if (blockChunk == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("Holder missing BlockChunk component! (%d, %d)", ChunkUtil.xOfChunkIndex(index), ChunkUtil.zOfChunkIndex(index))
|
||||
);
|
||||
} else {
|
||||
blockChunk.loadFromHolder(holder);
|
||||
worldChunkComponent.setFlag(ChunkFlag.NEWLY_GENERATED, newlyGenerated);
|
||||
worldChunkComponent.setLightingUpdatesEnabled(false);
|
||||
if (newlyGenerated && this.world.getWorldConfig().shouldSaveNewChunks()) {
|
||||
worldChunkComponent.markNeedsSaving();
|
||||
}
|
||||
|
||||
try {
|
||||
long start = System.nanoTime();
|
||||
IEventDispatcher<ChunkPreLoadProcessEvent, ChunkPreLoadProcessEvent> dispatcher = HytaleServer.get()
|
||||
.getEventBus()
|
||||
.dispatchFor(ChunkPreLoadProcessEvent.class, this.world.getName());
|
||||
if (dispatcher.hasListener()) {
|
||||
ChunkPreLoadProcessEvent event = dispatcher.dispatch(new ChunkPreLoadProcessEvent(holder, worldChunkComponent, newlyGenerated, start));
|
||||
if (!event.didLog()) {
|
||||
long end = System.nanoTime();
|
||||
long diff = end - start;
|
||||
if (diff > this.world.getTickStepNanos()) {
|
||||
LOGGER.at(Level.SEVERE)
|
||||
.log(
|
||||
"Took too long to pre-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s",
|
||||
FormatUtil.nanosToString(diff),
|
||||
this.world.consumeGCHasRun(),
|
||||
worldChunkComponent
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
worldChunkComponent.setLightingUpdatesEnabled(true);
|
||||
}
|
||||
|
||||
return holder;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private Ref<ChunkStore> postLoadChunk(@Nullable Holder<ChunkStore> holder) {
|
||||
this.world.debugAssertInTickingThread();
|
||||
if (holder != null && !this.store.isShutdown()) {
|
||||
long start = System.nanoTime();
|
||||
WorldChunk worldChunkComponent = holder.getComponent(WorldChunk.getComponentType());
|
||||
|
||||
assert worldChunkComponent != null;
|
||||
|
||||
worldChunkComponent.setFlag(ChunkFlag.START_INIT, true);
|
||||
if (worldChunkComponent.is(ChunkFlag.TICKING)) {
|
||||
holder.tryRemoveComponent(REGISTRY.getNonTickingComponentType());
|
||||
} else {
|
||||
holder.ensureComponent(REGISTRY.getNonTickingComponentType());
|
||||
}
|
||||
|
||||
Ref<ChunkStore> reference = this.add(holder);
|
||||
worldChunkComponent.initFlags();
|
||||
this.world.getChunkLighting().init(worldChunkComponent);
|
||||
long end = System.nanoTime();
|
||||
long diff = end - start;
|
||||
if (diff > this.world.getTickStepNanos()) {
|
||||
LOGGER.at(Level.SEVERE)
|
||||
.log(
|
||||
"Took too long to post-load process chunk: %s > TICK_STEP, Has GC Run: %s, %s",
|
||||
FormatUtil.nanosToString(diff),
|
||||
this.world.consumeGCHasRun(),
|
||||
worldChunkComponent
|
||||
);
|
||||
}
|
||||
|
||||
return reference;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static {
|
||||
CodecStore.STATIC.putCodecSupplier(HOLDER_CODEC_KEY, REGISTRY::getEntityCodec);
|
||||
REGISTRY.registerSystem(new ChunkStore.ChunkLoaderSaverSetupSystem());
|
||||
REGISTRY.registerSystem(new ChunkUnloadingSystem());
|
||||
REGISTRY.registerSystem(new ChunkSavingSystems.WorldRemoved());
|
||||
REGISTRY.registerSystem(new ChunkSavingSystems.Ticking());
|
||||
}
|
||||
|
||||
private static class ChunkLoadState {
|
||||
private final StampedLock lock = new StampedLock();
|
||||
private int flags = 0;
|
||||
@Nullable
|
||||
private CompletableFuture<Ref<ChunkStore>> future;
|
||||
@Nullable
|
||||
private Ref<ChunkStore> reference;
|
||||
@Nullable
|
||||
private Throwable throwable;
|
||||
private long failedWhen;
|
||||
private int failedCounter;
|
||||
|
||||
private ChunkLoadState() {
|
||||
}
|
||||
|
||||
private void fail(Throwable throwable) {
|
||||
long stamp = this.lock.writeLock();
|
||||
|
||||
try {
|
||||
this.flags = 0;
|
||||
this.future = null;
|
||||
this.throwable = throwable;
|
||||
this.failedWhen = System.nanoTime();
|
||||
this.failedCounter++;
|
||||
} finally {
|
||||
this.lock.unlockWrite(stamp);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility class for logging corrupted chunk information to logs/corrupted_chunks.log
|
||||
*/
|
||||
private static class CorruptedChunkLogger {
|
||||
private static final Path LOG_PATH = Paths.get("logs", "corrupted_chunks.log");
|
||||
private static final DateTimeFormatter FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")
|
||||
.withZone(ZoneId.systemDefault());
|
||||
private static final Object WRITE_LOCK = new Object();
|
||||
|
||||
static void logCorruption(String worldName, int chunkX, int chunkZ, Throwable cause) {
|
||||
try {
|
||||
synchronized (WRITE_LOCK) {
|
||||
Files.createDirectories(LOG_PATH.getParent());
|
||||
try (BufferedWriter writer = Files.newBufferedWriter(LOG_PATH,
|
||||
StandardOpenOption.CREATE, StandardOpenOption.APPEND)) {
|
||||
String timestamp = FORMATTER.format(Instant.now());
|
||||
writer.write(String.format("[%s] CORRUPTED CHUNK DETECTED - Regenerating%n", timestamp));
|
||||
writer.write(String.format(" World: %s%n", worldName));
|
||||
writer.write(String.format(" Chunk: (%d, %d)%n", chunkX, chunkZ));
|
||||
writer.write(String.format(" Block coords: (%d, %d) to (%d, %d)%n",
|
||||
chunkX * 16, chunkZ * 16, chunkX * 16 + 15, chunkZ * 16 + 15));
|
||||
writer.write(String.format(" Cause: %s%n", cause.getClass().getSimpleName()));
|
||||
writer.write(String.format(" Message: %s%n", cause.getMessage()));
|
||||
|
||||
// Write stack trace
|
||||
StringWriter sw = new StringWriter();
|
||||
cause.printStackTrace(new PrintWriter(sw));
|
||||
String stackTrace = sw.toString();
|
||||
// Indent stack trace
|
||||
for (String line : stackTrace.split("\n")) {
|
||||
writer.write(String.format(" %s%n", line));
|
||||
}
|
||||
writer.write(String.format("---%n%n"));
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.at(Level.WARNING).withCause(e).log("Failed to write to corrupted_chunks.log");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class ChunkLoaderSaverSetupSystem extends StoreSystem<ChunkStore> {
|
||||
public ChunkLoaderSaverSetupSystem() {
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public SystemGroup<ChunkStore> getGroup() {
|
||||
return ChunkStore.INIT_GROUP;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemAddedToStore(@Nonnull Store<ChunkStore> store) {
|
||||
ChunkStore data = store.getExternalData();
|
||||
World world = data.getWorld();
|
||||
IChunkStorageProvider chunkStorageProvider = world.getWorldConfig().getChunkStorageProvider();
|
||||
|
||||
try {
|
||||
data.loader = chunkStorageProvider.getLoader(store);
|
||||
data.saver = chunkStorageProvider.getSaver(store);
|
||||
} catch (IOException var6) {
|
||||
throw SneakyThrow.sneakyThrow(var6);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSystemRemovedFromStore(@Nonnull Store<ChunkStore> store) {
|
||||
ChunkStore data = store.getExternalData();
|
||||
|
||||
try {
|
||||
if (data.loader != null) {
|
||||
IChunkLoader oldLoader = data.loader;
|
||||
data.loader = null;
|
||||
oldLoader.close();
|
||||
}
|
||||
|
||||
if (data.saver != null) {
|
||||
IChunkSaver oldSaver = data.saver;
|
||||
data.saver = null;
|
||||
oldSaver.close();
|
||||
}
|
||||
} catch (IOException var4) {
|
||||
ChunkStore.LOGGER.at(Level.SEVERE).withCause(var4).log("Failed to close storage!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class LoadFuturePacketDataQuerySystem extends EntityDataSystem<ChunkStore, PlayerRef, CompletableFuture<Packet>> {
|
||||
public LoadFuturePacketDataQuerySystem() {
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class LoadPacketDataQuerySystem extends EntityDataSystem<ChunkStore, PlayerRef, Packet> {
|
||||
public LoadPacketDataQuerySystem() {
|
||||
}
|
||||
}
|
||||
|
||||
public abstract static class UnloadPacketDataQuerySystem extends EntityDataSystem<ChunkStore, PlayerRef, Packet> {
|
||||
public UnloadPacketDataQuerySystem() {
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EntityStore> METRICS_REGISTRY = new MetricsRegistry<EntityStore>()
|
||||
.register("Store", EntityStore::getStore, Store.METRICS_REGISTRY);
|
||||
@Nonnull
|
||||
public static final ComponentRegistry<EntityStore> REGISTRY = new ComponentRegistry<>();
|
||||
@Nonnull
|
||||
public static final CodecKey<Holder<EntityStore>> HOLDER_CODEC_KEY = new CodecKey<>("EntityHolder");
|
||||
@Nonnull
|
||||
public static final SystemGroup<EntityStore> SEND_PACKET_GROUP = REGISTRY.registerSystemGroup();
|
||||
@Nonnull
|
||||
private final AtomicInteger networkIdCounter = new AtomicInteger(1);
|
||||
@Nonnull
|
||||
private final World world;
|
||||
private Store<EntityStore> store;
|
||||
@Nonnull
|
||||
private final Map<UUID, Ref<EntityStore>> entitiesByUuid = new ConcurrentHashMap<>();
|
||||
@Nonnull
|
||||
private final Int2ObjectMap<Ref<EntityStore>> networkIdToRef = new Int2ObjectOpenHashMap<>();
|
||||
|
||||
public EntityStore(@Nonnull World world) {
|
||||
this.world = world;
|
||||
}
|
||||
|
||||
public void start(@Nonnull IResourceStorage resourceStorage) {
|
||||
this.store = REGISTRY.addStore(this, resourceStorage, store -> this.store = store);
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.store.shutdown();
|
||||
this.entitiesByUuid.clear();
|
||||
}
|
||||
|
||||
public Store<EntityStore> getStore() {
|
||||
return this.store;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<EntityStore> getRefFromUUID(@Nonnull UUID uuid) {
|
||||
return this.entitiesByUuid.get(uuid);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Ref<EntityStore> getRefFromNetworkId(int networkId) {
|
||||
return this.networkIdToRef.get(networkId);
|
||||
}
|
||||
|
||||
public int takeNextNetworkId() {
|
||||
return this.networkIdCounter.getAndIncrement();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public World getWorld() {
|
||||
return this.world;
|
||||
}
|
||||
|
||||
static {
|
||||
CodecStore.STATIC.putCodecSupplier(HOLDER_CODEC_KEY, REGISTRY::getEntityCodec);
|
||||
}
|
||||
|
||||
public static class NetworkIdSystem extends RefSystem<EntityStore> {
|
||||
public NetworkIdSystem() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return NetworkId.getComponentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityStore entityStore = store.getExternalData();
|
||||
NetworkId networkIdComponent = commandBuffer.getComponent(ref, NetworkId.getComponentType());
|
||||
|
||||
assert networkIdComponent != null;
|
||||
|
||||
int networkId = networkIdComponent.getId();
|
||||
if (entityStore.networkIdToRef.putIfAbsent(networkId, ref) != null) {
|
||||
networkId = entityStore.takeNextNetworkId();
|
||||
commandBuffer.putComponent(ref, NetworkId.getComponentType(), new NetworkId(networkId));
|
||||
entityStore.networkIdToRef.put(networkId, ref);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
EntityStore entityStore = store.getExternalData();
|
||||
NetworkId networkIdComponent = commandBuffer.getComponent(ref, NetworkId.getComponentType());
|
||||
|
||||
assert networkIdComponent != null;
|
||||
|
||||
entityStore.networkIdToRef.remove(networkIdComponent.getId(), ref);
|
||||
}
|
||||
}
|
||||
|
||||
public static class UUIDSystem extends RefSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
public UUIDSystem() {
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return UUIDComponent.getComponentType();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
UUIDComponent uuidComponent = commandBuffer.getComponent(ref, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
Ref<EntityStore> currentRef = store.getExternalData().entitiesByUuid.putIfAbsent(uuidComponent.getUuid(), ref);
|
||||
if (currentRef != null) {
|
||||
LOGGER.at(Level.WARNING).log("Removing duplicate entity with UUID: %s", uuidComponent.getUuid());
|
||||
commandBuffer.removeEntity(ref, RemoveReason.REMOVE);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
UUIDComponent uuidComponent = commandBuffer.getComponent(ref, UUIDComponent.getComponentType());
|
||||
|
||||
// HyFix: Null check for uuidComponent and uuid to prevent NPE on entity remove
|
||||
// Entities can be removed before UUID component is fully initialized
|
||||
if (uuidComponent == null) {
|
||||
LOGGER.at(Level.WARNING).log("Null UUIDComponent during entity remove - skipping UUID cleanup");
|
||||
return;
|
||||
}
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
if (uuid == null) {
|
||||
LOGGER.at(Level.WARNING).log("Null UUID in UUIDComponent during entity remove - skipping UUID cleanup");
|
||||
return;
|
||||
}
|
||||
|
||||
store.getExternalData().entitiesByUuid.remove(uuid, ref);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<Void> 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<Void> 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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
1169
src/com/hypixel/hytale/server/npc/role/Role.java
Normal file
1169
src/com/hypixel/hytale/server/npc/role/Role.java
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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<Role> registration : positionCache.getExternalRegistrations()) {
|
||||
registration.accept(role);
|
||||
}
|
||||
|
||||
positionCache.finalizeConfiguration();
|
||||
}
|
||||
|
||||
public static class OnFlockJoinSystem extends RefChangeSystem<EntityStore, FlockMembership> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, FlockMembership> flockMembershipComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, StateEvaluator> stateEvaluatorComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public OnFlockJoinSystem(
|
||||
@Nonnull ComponentType<EntityStore, NPCEntity> npcComponentType, @Nonnull ComponentType<EntityStore, FlockMembership> flockMembershipComponentType
|
||||
) {
|
||||
this.flockMembershipComponentType = flockMembershipComponentType;
|
||||
this.npcComponentType = npcComponentType;
|
||||
this.stateEvaluatorComponentType = StateEvaluator.getComponentType();
|
||||
this.query = Archetype.of(npcComponentType, flockMembershipComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public ComponentType<EntityStore, FlockMembership> componentType() {
|
||||
return this.flockMembershipComponentType;
|
||||
}
|
||||
|
||||
public void onComponentAdded(
|
||||
@Nonnull Ref<EntityStore> ref,
|
||||
@Nonnull FlockMembership component,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> 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<EntityStore> ref,
|
||||
FlockMembership oldComponent,
|
||||
@Nonnull FlockMembership newComponent,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> 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<EntityStore> ref,
|
||||
@Nonnull FlockMembership component,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
public static class RoleActivateSystem extends HolderSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, StateEvaluator> stateEvaluatorComponentType;
|
||||
|
||||
public RoleActivateSystem(
|
||||
@Nonnull ComponentType<EntityStore, NPCEntity> npcComponentType, @Nonnull ComponentType<EntityStore, StateEvaluator> stateEvaluatorComponentType
|
||||
) {
|
||||
this.npcComponentType = npcComponentType;
|
||||
this.stateEvaluatorComponentType = stateEvaluatorComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdd(@Nonnull Holder<EntityStore> holder, @Nonnull AddReason reason, @Nonnull Store<EntityStore> 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<EntityStore> holder, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store) {
|
||||
NPCEntity npcComponent = holder.getComponent(this.npcComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
npcComponent.getRole().getPositionCache().reset(false);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.npcComponentType;
|
||||
}
|
||||
}
|
||||
|
||||
public static class UpdateSystem extends SteppableTickingSystem {
|
||||
@Nonnull
|
||||
private static final ThreadLocal<BucketItemPool<Ref<EntityStore>>> BUCKET_POOL_THREAD_LOCAL = ThreadLocal.withInitial(BucketItemPool::new);
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, ModelComponent> modelComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, TransformComponent> transformComponentType;
|
||||
@Nonnull
|
||||
private final ResourceType<EntityStore, SpatialResource<Ref<EntityStore>, EntityStore>> playerSpatialResource;
|
||||
@Nonnull
|
||||
private final ResourceType<EntityStore, SpatialResource<Ref<EntityStore>, EntityStore>> npcSpatialResource;
|
||||
@Nonnull
|
||||
private final ResourceType<EntityStore, SpatialResource<Ref<EntityStore>, EntityStore>> itemSpatialResource;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, PlayerSpatialSystem.class, OrderPriority.CLOSEST),
|
||||
new SystemDependency<>(Order.BEFORE, RoleSystems.PreBehaviourSupportTickSystem.class)
|
||||
);
|
||||
|
||||
public UpdateSystem(
|
||||
@Nonnull ComponentType<EntityStore, NPCEntity> npcComponentType,
|
||||
@Nonnull ResourceType<EntityStore, SpatialResource<Ref<EntityStore>, 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<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void steppedTick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
Ref<EntityStore> 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<Ref<EntityStore>, 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<Ref<EntityStore>, 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<Ref<EntityStore>, EntityStore> spatialResource = store.getResource(this.itemSpatialResource);
|
||||
List<Ref<EntityStore>> list = positionCache.getDroppedItemList();
|
||||
list.clear();
|
||||
spatialResource.getSpatialStructure().ordered(position, (int) maxDroppedItemDistance + 1, list);
|
||||
}
|
||||
|
||||
double maxSpawnMarkerDistance = positionCache.getMaxSpawnMarkerDistance();
|
||||
if (maxSpawnMarkerDistance > 0.0) {
|
||||
SpatialResource<Ref<EntityStore>, EntityStore> spatialResource = store.getResource(SpawningPlugin.get().getSpawnMarkerSpatialResource());
|
||||
List<Ref<EntityStore>> list = positionCache.getSpawnMarkerList();
|
||||
list.clear();
|
||||
spatialResource.getSpatialStructure().collect(position, (int) maxSpawnMarkerDistance + 1, list);
|
||||
}
|
||||
|
||||
int maxSpawnBeaconDistance = positionCache.getMaxSpawnBeaconDistance();
|
||||
if (maxSpawnBeaconDistance > 0) {
|
||||
SpatialResource<Ref<EntityStore>, EntityStore> spatialResource = store.getResource(SpawningPlugin.get().getManualSpawnBeaconSpatialResource());
|
||||
List<Ref<EntityStore>> list = positionCache.getSpawnBeaconList();
|
||||
list.clear();
|
||||
spatialResource.getSpatialStructure().ordered(position, maxSpawnBeaconDistance + 1, list);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void addEntities(
|
||||
@Nonnull Ref<EntityStore> self,
|
||||
@Nonnull Vector3d position,
|
||||
@Nonnull EntityList entityList,
|
||||
@Nonnull SpatialResource<Ref<EntityStore>, EntityStore> spatialResource,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
List<Ref<EntityStore>> results = SpatialResource.getThreadLocalReferenceList();
|
||||
spatialResource.getSpatialStructure().collect(position, entityList.getSearchRadius(), results);
|
||||
|
||||
for (Ref<EntityStore> result : results) {
|
||||
if (result.isValid() && !result.equals(self)) {
|
||||
entityList.add(result, position, commandBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
406
src/com/hypixel/hytale/server/npc/systems/RoleSystems.java
Normal file
406
src/com/hypixel/hytale/server/npc/systems/RoleSystems.java
Normal file
@@ -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<List<Ref<EntityStore>>> ENTITY_LIST = ThreadLocal.withInitial(ArrayList::new);
|
||||
|
||||
public RoleSystems() {
|
||||
}
|
||||
|
||||
public static class BehaviourTickSystem extends TickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, StepComponent> stepComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, Frozen> frozenComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NewSpawnComponent> newSpawnComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, TransformComponent> transformComponentType;
|
||||
|
||||
public BehaviourTickSystem(
|
||||
@Nonnull ComponentType<EntityStore, NPCEntity> npcComponentType, @Nonnull ComponentType<EntityStore, StepComponent> 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<EntityStore> store) {
|
||||
List<Ref<EntityStore>> 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<EntityStore> 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<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, TransformComponent> transformComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, SteeringSystem.class), new SystemDependency<>(Order.BEFORE, TransformSystems.EntityTrackerUpdate.class)
|
||||
);
|
||||
|
||||
public PostBehaviourSupportTickSystem(@Nonnull ComponentType<EntityStore, NPCEntity> npcComponentType) {
|
||||
this.npcComponentType = npcComponentType;
|
||||
this.transformComponentType = TransformComponent.getComponentType();
|
||||
this.query = Query.and(npcComponentType, this.transformComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void steppedTick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
Ref<EntityStore> 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<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, Player> playerComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, DeathComponent> deathComponentType;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
|
||||
public PreBehaviourSupportTickSystem(@Nonnull ComponentType<EntityStore, NPCEntity> 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<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.npcComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void steppedTick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
Role role = npcComponent.getRole();
|
||||
MarkedEntitySupport markedEntitySupport = role.getMarkedEntitySupport();
|
||||
Ref<EntityStore>[] entityTargets = markedEntitySupport.getEntityTargets();
|
||||
|
||||
for (int i = 0; i < entityTargets.length; i++) {
|
||||
Ref<EntityStore> 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<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, ModelComponent> modelComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, BoundingBox> boundingBoxComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
|
||||
public RoleActivateSystem(@Nonnull ComponentType<EntityStore, NPCEntity> 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<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdd(@Nonnull Holder<EntityStore> holder, @Nonnull AddReason reason, @Nonnull Store<EntityStore> 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<EntityStore> holder, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> 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<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
|
||||
public RoleDebugSystem(@Nonnull ComponentType<EntityStore, NPCEntity> npcComponentType, @Nonnull Set<Dependency<EntityStore>> dependencies) {
|
||||
this.npcComponentType = npcComponentType;
|
||||
this.dependencies = dependencies;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.npcComponentType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void steppedTick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, LegacySpawnBeaconEntity> legacySpawnBeaconComponent;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public BeaconAddRemoveSystem(
|
||||
@Nonnull ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType,
|
||||
@Nonnull ComponentType<EntityStore, LegacySpawnBeaconEntity> legacySpawnBeaconComponent
|
||||
) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.legacySpawnBeaconComponent = legacySpawnBeaconComponent;
|
||||
this.npcComponentType = NPCEntity.getComponentType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case LOAD:
|
||||
SpawnBeaconReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
Ref<EntityStore> markerReference = spawnReferenceComponent.getReference().getEntity(store);
|
||||
if (markerReference == null) {
|
||||
return;
|
||||
} else {
|
||||
LegacySpawnBeaconEntity legacySpawnBeaconComponent = store.getComponent(markerReference, this.legacySpawnBeaconComponent);
|
||||
|
||||
assert legacySpawnBeaconComponent != null;
|
||||
|
||||
NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
spawnReferenceComponent.getReference().setEntity(markerReference, store);
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
BeaconSpawnController spawnController = legacySpawnBeaconComponent.getSpawnController();
|
||||
|
||||
// HyFix: Add null check for spawnController before calling hasSlots()
|
||||
if (spawnController == null) {
|
||||
System.out.println("[HyFix] WARNING: null spawnController in BeaconAddRemoveSystem - despawning NPC (missing beacon type?)");
|
||||
npcComponent.setToDespawn();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!spawnController.hasSlots()) {
|
||||
npcComponent.setToDespawn();
|
||||
return;
|
||||
} else {
|
||||
spawnController.notifySpawnedEntityExists(markerReference, commandBuffer);
|
||||
}
|
||||
}
|
||||
case SPAWN:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case REMOVE:
|
||||
SpawnBeaconReference spawnReference = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
if (spawnReference == null) {
|
||||
return;
|
||||
} else {
|
||||
Ref<EntityStore> spawnBeaconRef = spawnReference.getReference().getEntity(store);
|
||||
if (spawnBeaconRef == null) {
|
||||
return;
|
||||
} else {
|
||||
LegacySpawnBeaconEntity legacySpawnBeaconComponent = store.getComponent(spawnBeaconRef, this.legacySpawnBeaconComponent);
|
||||
if (legacySpawnBeaconComponent == null) {
|
||||
return;
|
||||
} else {
|
||||
legacySpawnBeaconComponent.getSpawnController().notifyNPCRemoval(ref, store);
|
||||
}
|
||||
}
|
||||
}
|
||||
case UNLOAD:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class MarkerAddRemoveSystem extends RefSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerEntity> spawnMarkerEntityComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, WorldGenId> worldGenIdComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, UUIDComponent> uuidComponentComponentType;
|
||||
@Nonnull
|
||||
private final ResourceType<EntityStore, WorldTimeResource> worldTimeResourceResourceType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public MarkerAddRemoveSystem(
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType,
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerEntity> spawnMarkerEntityComponentType
|
||||
) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.spawnMarkerEntityComponentType = spawnMarkerEntityComponentType;
|
||||
this.npcComponentType = NPCEntity.getComponentType();
|
||||
this.worldGenIdComponentType = WorldGenId.getComponentType();
|
||||
this.uuidComponentComponentType = UUIDComponent.getComponentType();
|
||||
this.worldTimeResourceResourceType = WorldTimeResource.getResourceType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcComponentType, this.uuidComponentComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityAdded(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull AddReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case LOAD:
|
||||
SpawnMarkerReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
Ref<EntityStore> markerReference = spawnReferenceComponent.getReference().getEntity(store);
|
||||
if (markerReference == null) {
|
||||
return;
|
||||
} else {
|
||||
SpawnMarkerEntity markerTypeComponent = store.getComponent(markerReference, this.spawnMarkerEntityComponentType);
|
||||
|
||||
assert markerTypeComponent != null;
|
||||
|
||||
NPCEntity npcComponent = store.getComponent(ref, this.npcComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
spawnReferenceComponent.getReference().setEntity(markerReference, store);
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
markerTypeComponent.refreshTimeout();
|
||||
WorldGenId worldGenIdComponent = commandBuffer.getComponent(markerReference, this.worldGenIdComponentType);
|
||||
int worldGenId = worldGenIdComponent != null ? worldGenIdComponent.getWorldGenId() : 0;
|
||||
commandBuffer.putComponent(markerReference, WorldGenId.getComponentType(), new WorldGenId(worldGenId));
|
||||
HytaleLogger.Api context = SpawningPlugin.get().getLogger().at(Level.FINE);
|
||||
if (context.isEnabled()) {
|
||||
UUIDComponent uuidComponent = commandBuffer.getComponent(markerReference, this.uuidComponentComponentType);
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
context.log("%s synced up with marker %s", npcComponent.getRoleName(), uuid);
|
||||
}
|
||||
}
|
||||
case SPAWN:
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEntityRemove(
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull RemoveReason reason, @Nonnull Store<EntityStore> store, @Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
switch (reason) {
|
||||
case REMOVE:
|
||||
SpawnMarkerReference spawnReferenceComponent = store.getComponent(ref, this.spawnReferenceComponentType);
|
||||
if (spawnReferenceComponent == null) {
|
||||
return;
|
||||
} else {
|
||||
Ref<EntityStore> spawnMarkerRef = spawnReferenceComponent.getReference().getEntity(store);
|
||||
if (spawnMarkerRef == null) {
|
||||
return;
|
||||
} else {
|
||||
SpawnMarkerEntity spawnMarkerComponent = store.getComponent(spawnMarkerRef, this.spawnMarkerEntityComponentType);
|
||||
|
||||
assert spawnMarkerComponent != null;
|
||||
|
||||
UUIDComponent uuidComponent = store.getComponent(ref, this.uuidComponentComponentType);
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
int spawnCount = spawnMarkerComponent.decrementAndGetSpawnCount();
|
||||
SpawnMarker cachedMarker = spawnMarkerComponent.getCachedMarker();
|
||||
if (spawnCount > 0 && cachedMarker.getDeactivationDistance() > 0.0) {
|
||||
InvalidatablePersistentRef[] newReferences = new InvalidatablePersistentRef[spawnCount];
|
||||
int pos = 0;
|
||||
InvalidatablePersistentRef[] npcReferences = spawnMarkerComponent.getNpcReferences();
|
||||
|
||||
for (InvalidatablePersistentRef npcRef : npcReferences) {
|
||||
if (!npcRef.getUuid().equals(uuid)) {
|
||||
newReferences[pos++] = npcRef;
|
||||
}
|
||||
}
|
||||
|
||||
spawnMarkerComponent.setNpcReferences(newReferences);
|
||||
}
|
||||
|
||||
if (spawnCount <= 0 && !cachedMarker.isRealtimeRespawn()) {
|
||||
Instant instant = store.getResource(this.worldTimeResourceResourceType).getGameTime();
|
||||
Duration gameTimeRespawn = spawnMarkerComponent.pollGameTimeRespawn();
|
||||
if (gameTimeRespawn != null) {
|
||||
instant = instant.plus(gameTimeRespawn);
|
||||
}
|
||||
|
||||
spawnMarkerComponent.setSpawnAfter(instant);
|
||||
spawnMarkerComponent.setNpcReferences(null);
|
||||
StoredFlock storedFlock = spawnMarkerComponent.getStoredFlock();
|
||||
if (storedFlock != null) {
|
||||
storedFlock.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
case UNLOAD:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TickingSpawnBeaconSystem extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, NPCPreTickSystem.class), new SystemDependency<>(Order.BEFORE, DeathSystems.CorpseRemoval.class)
|
||||
);
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcEntityComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public TickingSpawnBeaconSystem(@Nonnull ComponentType<EntityStore, SpawnBeaconReference> spawnReferenceComponentType) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.npcEntityComponentType = NPCEntity.getComponentType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcEntityComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcEntityComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
if (!npcComponent.isDespawning() && !npcComponent.isPlayingDespawnAnim()) {
|
||||
SpawnBeaconReference spawnReferenceComponent = archetypeChunk.getComponent(index, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
if (spawnReferenceComponent.tickMarkerLostTimeoutCounter(dt)) {
|
||||
Ref<EntityStore> spawnBeaconRef = spawnReferenceComponent.getReference().getEntity(commandBuffer);
|
||||
if (spawnBeaconRef != null) {
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
} else if (npcComponent.getRole().getStateSupport().isInBusyState()) {
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
} else {
|
||||
npcComponent.setToDespawn();
|
||||
HytaleLogger.Api context = SpawningPlugin.get().getLogger().at(Level.WARNING);
|
||||
if (context.isEnabled()) {
|
||||
context.log("NPCEntity despawning due to lost marker: %s", archetypeChunk.getReferenceTo(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class TickingSpawnMarkerSystem extends EntityTickingSystem<EntityStore> {
|
||||
@Nonnull
|
||||
private static final Set<Dependency<EntityStore>> DEPENDENCIES = Set.of(
|
||||
new SystemDependency<>(Order.AFTER, NPCPreTickSystem.class), new SystemDependency<>(Order.BEFORE, DeathSystems.CorpseRemoval.class)
|
||||
);
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, SpawnMarkerEntity> markerTypeComponentType;
|
||||
@Nonnull
|
||||
private final ComponentType<EntityStore, NPCEntity> npcEntityComponentType;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public TickingSpawnMarkerSystem(
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerReference> spawnReferenceComponentType,
|
||||
@Nonnull ComponentType<EntityStore, SpawnMarkerEntity> markerTypeComponentType
|
||||
) {
|
||||
this.spawnReferenceComponentType = spawnReferenceComponentType;
|
||||
this.markerTypeComponentType = markerTypeComponentType;
|
||||
this.npcEntityComponentType = NPCEntity.getComponentType();
|
||||
this.query = Archetype.of(spawnReferenceComponentType, this.npcEntityComponentType);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Set<Dependency<EntityStore>> getDependencies() {
|
||||
return DEPENDENCIES;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return EntityTickingSystem.maybeUseParallel(archetypeChunkSize, taskCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer
|
||||
) {
|
||||
NPCEntity npcComponent = archetypeChunk.getComponent(index, this.npcEntityComponentType);
|
||||
|
||||
assert npcComponent != null;
|
||||
|
||||
if (!npcComponent.isDespawning() && !npcComponent.isPlayingDespawnAnim()) {
|
||||
SpawnMarkerReference spawnReferenceComponent = archetypeChunk.getComponent(index, this.spawnReferenceComponentType);
|
||||
|
||||
assert spawnReferenceComponent != null;
|
||||
|
||||
if (spawnReferenceComponent.tickMarkerLostTimeoutCounter(dt)) {
|
||||
Ref<EntityStore> spawnMarkerRef = spawnReferenceComponent.getReference().getEntity(commandBuffer);
|
||||
if (spawnMarkerRef != null) {
|
||||
SpawnMarkerEntity spawnMarkerComponent = commandBuffer.getComponent(spawnMarkerRef, this.markerTypeComponentType);
|
||||
|
||||
assert spawnMarkerComponent != null;
|
||||
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
spawnMarkerComponent.refreshTimeout();
|
||||
} else if (npcComponent.getRole().getStateSupport().isInBusyState()) {
|
||||
spawnReferenceComponent.refreshTimeoutCounter();
|
||||
} else {
|
||||
npcComponent.setToDespawn();
|
||||
HytaleLogger.Api context = SpawningPlugin.get().getLogger().at(Level.WARNING);
|
||||
if (context.isEnabled()) {
|
||||
context.log("NPCEntity despawning due to lost marker: %s", archetypeChunk.getReferenceTo(index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
128
src/com/hypixel/hytale/server/npc/systems/SteeringSystem.java
Normal file
128
src/com/hypixel/hytale/server/npc/systems/SteeringSystem.java
Normal file
@@ -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<EntityStore, NPCEntity> npcEntityComponent;
|
||||
@Nonnull
|
||||
private final Set<Dependency<EntityStore>> dependencies;
|
||||
@Nonnull
|
||||
private final Query<EntityStore> query;
|
||||
|
||||
public SteeringSystem(@Nonnull ComponentType<EntityStore, NPCEntity> 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<Dependency<EntityStore>> getDependencies() {
|
||||
return this.dependencies;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isParallel(int archetypeChunkSize, int taskCount) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Query<EntityStore> getQuery() {
|
||||
return this.query;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void steppedTick(
|
||||
float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> archetypeChunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> 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<EntityStore> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
91
src/com/hypixel/hytale/server/npc/systems/TickSkipState.java
Normal file
91
src/com/hypixel/hytale/server/npc/systems/TickSkipState.java
Normal file
@@ -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, SpatialResource<Ref<EntityStore>, 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<EntityStore> 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<EntityStore> store) {
|
||||
SpatialResource<Ref<EntityStore>, EntityStore> spatial = store.getResource(PLAYER_SPATIAL);
|
||||
if (spatial == null) {
|
||||
return 0f;
|
||||
}
|
||||
Ref<EntityStore> 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);
|
||||
}
|
||||
}
|
||||
@@ -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<NPCBeaconSpawnJob> {
|
||||
@Nonnull
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
public static final int MAX_ATTEMPTS_PER_TICK = 5;
|
||||
public static final double ROUNDING_BREAK_POINT = 0.25;
|
||||
@Nonnull
|
||||
private final Ref<EntityStore> ownerRef;
|
||||
private final List<Ref<EntityStore>> spawnedEntities = new ObjectArrayList<>();
|
||||
private final List<PlayerRef> playersInRegion = new ObjectArrayList<>();
|
||||
private int nextPlayerIndex = 0;
|
||||
private final Object2IntMap<UUID> entitiesPerPlayer = new Object2IntOpenHashMap<>();
|
||||
private final Object2DoubleMap<Ref<EntityStore>> entityTimeoutCounter = new Object2DoubleOpenHashMap<>();
|
||||
private final IntSet unspawnableRoles = new IntOpenHashSet();
|
||||
private final Comparator<PlayerRef> threatComparator = Comparator.comparingInt(playerRef -> this.entitiesPerPlayer.getOrDefault(playerRef.getUuid(), 0));
|
||||
private int baseMaxTotalSpawns;
|
||||
private int currentScaledMaxTotalSpawns;
|
||||
private int[] baseMaxConcurrentSpawns;
|
||||
private int currentScaledMaxConcurrentSpawns;
|
||||
private int spawnsThisRound;
|
||||
private int remainingSpawns;
|
||||
private boolean roundStart = true;
|
||||
private double beaconRadiusSquared;
|
||||
private double spawnRadiusSquared;
|
||||
private double despawnNPCAfterTimeout;
|
||||
private Duration despawnBeaconAfterTimeout;
|
||||
private boolean despawnNPCsIfIdle;
|
||||
|
||||
public BeaconSpawnController(@Nonnull World world, @Nonnull Ref<EntityStore> ownerRef) {
|
||||
super(world);
|
||||
this.ownerRef = ownerRef;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getMaxActiveJobs() {
|
||||
return Math.min(this.remainingSpawns, this.baseMaxActiveJobs);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public NPCBeaconSpawnJob createRandomSpawnJob(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
LegacySpawnBeaconEntity legacySpawnBeaconComponent = componentAccessor.getComponent(this.ownerRef, LegacySpawnBeaconEntity.getComponentType());
|
||||
|
||||
assert legacySpawnBeaconComponent != null;
|
||||
|
||||
BeaconSpawnWrapper wrapper = legacySpawnBeaconComponent.getSpawnWrapper();
|
||||
RoleSpawnParameters spawn = wrapper.pickRole(ThreadLocalRandom.current());
|
||||
|
||||
// HyFix: Null check for spawn parameter - prevents NPE when beacon has misconfigured spawn types
|
||||
if (spawn == null) {
|
||||
System.out.println("[HyFix] WARNING: null spawn from pickRole() - returning null (missing spawn config in beacon?)");
|
||||
return null;
|
||||
} else {
|
||||
String spawnId = spawn.getId();
|
||||
int roleIndex = NPCPlugin.get().getIndex(spawnId);
|
||||
if (roleIndex >= 0 && !this.unspawnableRoles.contains(roleIndex)) {
|
||||
NPCBeaconSpawnJob job = null;
|
||||
int predictedTotal = this.spawnedEntities.size() + this.activeJobs.size();
|
||||
if (this.activeJobs.size() < this.getMaxActiveJobs()
|
||||
&& this.nextPlayerIndex < this.playersInRegion.size()
|
||||
&& predictedTotal < this.currentScaledMaxTotalSpawns) {
|
||||
job = this.idleJobs.isEmpty() ? new NPCBeaconSpawnJob() : this.idleJobs.pop();
|
||||
job.beginProbing(this.playersInRegion.get(this.nextPlayerIndex++), this.currentScaledMaxConcurrentSpawns, roleIndex, spawn.getFlockDefinition());
|
||||
this.activeJobs.add(job);
|
||||
if (this.nextPlayerIndex >= this.playersInRegion.size()) {
|
||||
this.nextPlayerIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return job;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void initialise(@Nonnull BeaconSpawnWrapper spawnWrapper) {
|
||||
BeaconNPCSpawn spawn = spawnWrapper.getSpawn();
|
||||
this.baseMaxTotalSpawns = spawn.getMaxSpawnedNpcs();
|
||||
this.baseMaxConcurrentSpawns = spawn.getConcurrentSpawnsRange();
|
||||
double beaconRadius = spawn.getBeaconRadius();
|
||||
this.beaconRadiusSquared = beaconRadius * beaconRadius;
|
||||
double spawnRadius = spawn.getSpawnRadius();
|
||||
this.spawnRadiusSquared = spawnRadius * spawnRadius;
|
||||
this.despawnNPCAfterTimeout = spawn.getNpcIdleDespawnTimeSeconds();
|
||||
this.despawnBeaconAfterTimeout = spawn.getBeaconVacantDespawnTime();
|
||||
this.despawnNPCsIfIdle = spawn.getNpcSpawnState() != null;
|
||||
}
|
||||
|
||||
public int getSpawnsThisRound() {
|
||||
return this.spawnsThisRound;
|
||||
}
|
||||
|
||||
public void setRemainingSpawns(int remainingSpawns) {
|
||||
this.remainingSpawns = remainingSpawns;
|
||||
}
|
||||
|
||||
public void addRoundSpawn() {
|
||||
this.spawnsThisRound++;
|
||||
this.remainingSpawns--;
|
||||
}
|
||||
|
||||
public boolean isRoundStart() {
|
||||
return this.roundStart;
|
||||
}
|
||||
|
||||
public void setRoundStart(boolean roundStart) {
|
||||
this.roundStart = roundStart;
|
||||
}
|
||||
|
||||
public Ref<EntityStore> getOwnerRef() {
|
||||
return this.ownerRef;
|
||||
}
|
||||
|
||||
public int[] getBaseMaxConcurrentSpawns() {
|
||||
return this.baseMaxConcurrentSpawns;
|
||||
}
|
||||
|
||||
public List<PlayerRef> getPlayersInRegion() {
|
||||
return this.playersInRegion;
|
||||
}
|
||||
|
||||
public int getCurrentScaledMaxConcurrentSpawns() {
|
||||
return this.currentScaledMaxConcurrentSpawns;
|
||||
}
|
||||
|
||||
public void setCurrentScaledMaxConcurrentSpawns(int currentScaledMaxConcurrentSpawns) {
|
||||
this.currentScaledMaxConcurrentSpawns = currentScaledMaxConcurrentSpawns;
|
||||
}
|
||||
|
||||
public Duration getDespawnBeaconAfterTimeout() {
|
||||
return this.despawnBeaconAfterTimeout;
|
||||
}
|
||||
|
||||
public double getSpawnRadiusSquared() {
|
||||
return this.spawnRadiusSquared;
|
||||
}
|
||||
|
||||
public double getBeaconRadiusSquared() {
|
||||
return this.beaconRadiusSquared;
|
||||
}
|
||||
|
||||
public int getBaseMaxTotalSpawns() {
|
||||
return this.baseMaxTotalSpawns;
|
||||
}
|
||||
|
||||
public void setCurrentScaledMaxTotalSpawns(int currentScaledMaxTotalSpawns) {
|
||||
this.currentScaledMaxTotalSpawns = currentScaledMaxTotalSpawns;
|
||||
}
|
||||
|
||||
public List<Ref<EntityStore>> getSpawnedEntities() {
|
||||
return this.spawnedEntities;
|
||||
}
|
||||
|
||||
public void setNextPlayerIndex(int nextPlayerIndex) {
|
||||
this.nextPlayerIndex = nextPlayerIndex;
|
||||
}
|
||||
|
||||
public Object2DoubleMap<Ref<EntityStore>> getEntityTimeoutCounter() {
|
||||
return this.entityTimeoutCounter;
|
||||
}
|
||||
|
||||
public Object2IntMap<UUID> getEntitiesPerPlayer() {
|
||||
return this.entitiesPerPlayer;
|
||||
}
|
||||
|
||||
public boolean isDespawnNPCsIfIdle() {
|
||||
return this.despawnNPCsIfIdle;
|
||||
}
|
||||
|
||||
public double getDespawnNPCAfterTimeout() {
|
||||
return this.despawnNPCAfterTimeout;
|
||||
}
|
||||
|
||||
public Comparator<PlayerRef> getThreatComparator() {
|
||||
return this.threatComparator;
|
||||
}
|
||||
|
||||
public void notifySpawnedEntityExists(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
this.spawnedEntities.add(ref);
|
||||
HytaleLogger.Api context = LOGGER.at(Level.FINE);
|
||||
if (context.isEnabled()) {
|
||||
UUIDComponent ownerUuidComponent = componentAccessor.getComponent(this.ownerRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert ownerUuidComponent != null;
|
||||
|
||||
context.log("Registering NPC with reference %s with Spawn Beacon %s", ref, ownerUuidComponent.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
public void onJobFinished(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
if (++this.spawnsThisRound >= this.currentScaledMaxConcurrentSpawns) {
|
||||
this.onAllConcurrentSpawned(componentAccessor);
|
||||
}
|
||||
}
|
||||
|
||||
public void notifyNPCRemoval(@Nonnull Ref<EntityStore> ref, @Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
this.spawnedEntities.remove(ref);
|
||||
this.entityTimeoutCounter.removeDouble(ref);
|
||||
if (this.spawnedEntities.size() == this.currentScaledMaxTotalSpawns - 1) {
|
||||
LegacySpawnBeaconEntity.prepareNextSpawnTimer(this.ownerRef, componentAccessor);
|
||||
}
|
||||
|
||||
HytaleLogger.Api context = LOGGER.at(Level.FINE);
|
||||
if (context.isEnabled()) {
|
||||
UUIDComponent ownerUuidComponent = componentAccessor.getComponent(this.ownerRef, UUIDComponent.getComponentType());
|
||||
|
||||
assert ownerUuidComponent != null;
|
||||
|
||||
context.log("Removing NPC with reference %s from Spawn Beacon %s", ref, ownerUuidComponent.getUuid());
|
||||
}
|
||||
}
|
||||
|
||||
public boolean hasSlots() {
|
||||
return this.spawnedEntities.size() < this.currentScaledMaxTotalSpawns;
|
||||
}
|
||||
|
||||
public void markNPCUnspawnable(int roleIndex) {
|
||||
this.unspawnableRoles.add(roleIndex);
|
||||
}
|
||||
|
||||
public void clearUnspawnableNPCs() {
|
||||
this.unspawnableRoles.clear();
|
||||
}
|
||||
|
||||
public void onAllConcurrentSpawned(@Nonnull ComponentAccessor<EntityStore> componentAccessor) {
|
||||
this.spawnsThisRound = 0;
|
||||
this.remainingSpawns = 0;
|
||||
LegacySpawnBeaconEntity.prepareNextSpawnTimer(this.ownerRef, componentAccessor);
|
||||
this.roundStart = true;
|
||||
}
|
||||
}
|
||||
@@ -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<EntityStore> {
|
||||
private static final double SPAWN_LOST_TIMEOUT = 35.0;
|
||||
@Nonnull
|
||||
private static final InvalidatablePersistentRef[] EMPTY_REFERENCES = new InvalidatablePersistentRef[0];
|
||||
public static final ArrayCodec<InvalidatablePersistentRef> NPC_REFERENCES_CODEC = new ArrayCodec<>(
|
||||
InvalidatablePersistentRef.CODEC, InvalidatablePersistentRef[]::new
|
||||
);
|
||||
@Nonnull
|
||||
public static final BuilderCodec<SpawnMarkerEntity> CODEC = BuilderCodec.builder(SpawnMarkerEntity.class, SpawnMarkerEntity::new)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnMarker", Codec.STRING),
|
||||
(spawnMarkerEntity, s) -> spawnMarkerEntity.spawnMarkerId = s,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.spawnMarkerId
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("RespawnTime", Codec.DOUBLE),
|
||||
(spawnMarkerEntity, d) -> spawnMarkerEntity.respawnCounter = d,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.respawnCounter
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnCount", Codec.INTEGER),
|
||||
(spawnMarkerEntity, i) -> spawnMarkerEntity.spawnCount = i,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.spawnCount
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("GameTimeRespawn", Codec.DURATION),
|
||||
(spawnMarkerEntity, duration) -> spawnMarkerEntity.gameTimeRespawn = duration,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.gameTimeRespawn
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnAfter", Codec.INSTANT),
|
||||
(spawnMarkerEntity, instant) -> spawnMarkerEntity.spawnAfter = instant,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.spawnAfter
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("NPCReferences", NPC_REFERENCES_CODEC),
|
||||
(spawnMarkerEntity, array) -> spawnMarkerEntity.npcReferences = array,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.npcReferences
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("PersistedFlock", StoredFlock.CODEC),
|
||||
(spawnMarkerEntity, o) -> spawnMarkerEntity.storedFlock = o,
|
||||
spawnMarkerEntity -> spawnMarkerEntity.storedFlock
|
||||
)
|
||||
.addField(
|
||||
new KeyedCodec<>("SpawnPosition", Vector3d.CODEC),
|
||||
(spawnMarkerEntity, v) -> spawnMarkerEntity.spawnPosition.assign(v),
|
||||
spawnMarkerEntity -> spawnMarkerEntity.storedFlock == null ? null : spawnMarkerEntity.spawnPosition
|
||||
)
|
||||
.build();
|
||||
private static final int MAX_FAILED_SPAWNS = 5;
|
||||
private String spawnMarkerId;
|
||||
private SpawnMarker cachedMarker;
|
||||
private double respawnCounter;
|
||||
@Nullable
|
||||
private Duration gameTimeRespawn;
|
||||
@Nullable
|
||||
private Instant spawnAfter;
|
||||
private int spawnCount;
|
||||
@Nullable
|
||||
private Set<UUID> suppressedBy;
|
||||
private int failedSpawns;
|
||||
@Nonnull
|
||||
private final SpawningContext context;
|
||||
private final Vector3d spawnPosition = new Vector3d();
|
||||
private InvalidatablePersistentRef[] npcReferences;
|
||||
@Nullable
|
||||
private StoredFlock storedFlock;
|
||||
@Nullable
|
||||
private List<Pair<Ref<EntityStore>, NPCEntity>> tempStorageList;
|
||||
private double timeToDeactivation;
|
||||
private boolean despawnStarted;
|
||||
private double spawnLostTimeoutCounter;
|
||||
|
||||
public static ComponentType<EntityStore, SpawnMarkerEntity> getComponentType() {
|
||||
return SpawningPlugin.get().getSpawnMarkerComponentType();
|
||||
}
|
||||
|
||||
public SpawnMarkerEntity() {
|
||||
this.context = new SpawningContext();
|
||||
this.npcReferences = EMPTY_REFERENCES;
|
||||
}
|
||||
|
||||
public SpawnMarker getCachedMarker() {
|
||||
return this.cachedMarker;
|
||||
}
|
||||
|
||||
public void setCachedMarker(@Nonnull SpawnMarker marker) {
|
||||
this.cachedMarker = marker;
|
||||
}
|
||||
|
||||
public int getSpawnCount() {
|
||||
return this.spawnCount;
|
||||
}
|
||||
|
||||
public void setSpawnCount(int spawnCount) {
|
||||
this.spawnCount = spawnCount;
|
||||
}
|
||||
|
||||
public void setRespawnCounter(double respawnCounter) {
|
||||
this.respawnCounter = respawnCounter;
|
||||
}
|
||||
|
||||
public void setSpawnAfter(@Nullable Instant spawnAfter) {
|
||||
this.spawnAfter = spawnAfter;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Instant getSpawnAfter() {
|
||||
return this.spawnAfter;
|
||||
}
|
||||
|
||||
public void setGameTimeRespawn(@Nullable Duration gameTimeRespawn) {
|
||||
this.gameTimeRespawn = gameTimeRespawn;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Duration pollGameTimeRespawn() {
|
||||
Duration ret = this.gameTimeRespawn;
|
||||
this.gameTimeRespawn = null;
|
||||
return ret;
|
||||
}
|
||||
|
||||
public boolean tickRespawnTimer(float dt) {
|
||||
return (this.respawnCounter -= dt) <= 0.0;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public Set<UUID> getSuppressedBy() {
|
||||
return this.suppressedBy;
|
||||
}
|
||||
|
||||
public void setStoredFlock(@Nonnull StoredFlock storedFlock) {
|
||||
this.storedFlock = storedFlock;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public StoredFlock getStoredFlock() {
|
||||
return this.storedFlock;
|
||||
}
|
||||
|
||||
public double getTimeToDeactivation() {
|
||||
return this.timeToDeactivation;
|
||||
}
|
||||
|
||||
public void setTimeToDeactivation(double timeToDeactivation) {
|
||||
this.timeToDeactivation = timeToDeactivation;
|
||||
}
|
||||
|
||||
public boolean tickTimeToDeactivation(float dt) {
|
||||
return (this.timeToDeactivation -= dt) <= 0.0;
|
||||
}
|
||||
|
||||
public boolean tickSpawnLostTimeout(float dt) {
|
||||
return (this.spawnLostTimeoutCounter -= dt) <= 0.0;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Vector3d getSpawnPosition() {
|
||||
return this.spawnPosition;
|
||||
}
|
||||
|
||||
public InvalidatablePersistentRef[] getNpcReferences() {
|
||||
return this.npcReferences;
|
||||
}
|
||||
|
||||
public void setNpcReferences(@Nullable InvalidatablePersistentRef[] npcReferences) {
|
||||
this.npcReferences = npcReferences != null ? npcReferences : EMPTY_REFERENCES;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public List<Pair<Ref<EntityStore>, NPCEntity>> getTempStorageList() {
|
||||
return this.tempStorageList;
|
||||
}
|
||||
|
||||
public void setTempStorageList(@Nonnull List<Pair<Ref<EntityStore>, NPCEntity>> tempStorageList) {
|
||||
this.tempStorageList = tempStorageList;
|
||||
}
|
||||
|
||||
public boolean isDespawnStarted() {
|
||||
return this.despawnStarted;
|
||||
}
|
||||
|
||||
public void setDespawnStarted(boolean despawnStarted) {
|
||||
this.despawnStarted = despawnStarted;
|
||||
}
|
||||
|
||||
public void refreshTimeout() {
|
||||
this.spawnLostTimeoutCounter = 35.0;
|
||||
}
|
||||
|
||||
public boolean spawnNPC(@Nonnull Ref<EntityStore> ref, @Nonnull SpawnMarker marker, @Nonnull Store<EntityStore> store) {
|
||||
SpawnMarker.SpawnConfiguration spawn = marker.getWeightedConfigurations().get(ThreadLocalRandom.current());
|
||||
if (spawn == null) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s has no spawn configuration to spawn", ref);
|
||||
this.refreshTimeout();
|
||||
return false;
|
||||
} else {
|
||||
boolean realtime = marker.isRealtimeRespawn();
|
||||
if (realtime) {
|
||||
this.respawnCounter = spawn.getRealtimeRespawnTime();
|
||||
} else {
|
||||
this.spawnAfter = null;
|
||||
this.gameTimeRespawn = spawn.getSpawnAfterGameTime();
|
||||
}
|
||||
|
||||
UUIDComponent uuidComponent = store.getComponent(ref, UUIDComponent.getComponentType());
|
||||
|
||||
assert uuidComponent != null;
|
||||
|
||||
UUID uuid = uuidComponent.getUuid();
|
||||
String roleName = spawn.getNpc();
|
||||
if (roleName != null && !roleName.isEmpty()) {
|
||||
NPCPlugin npcModule = NPCPlugin.get();
|
||||
int roleIndex = npcModule.getIndex(roleName);
|
||||
TransformComponent transformComponent = store.getComponent(ref, TransformComponent.getComponentType());
|
||||
|
||||
assert transformComponent != null;
|
||||
|
||||
Vector3d position = transformComponent.getPosition();
|
||||
BuilderInfo builderInfo = npcModule.getRoleBuilderInfo(roleIndex);
|
||||
if (builderInfo == null) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s attempted to spawn non-existent NPC role '%s'", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.NONEXISTENT_ROLE);
|
||||
return false;
|
||||
} else {
|
||||
Builder<?> role = builderInfo.isValid() ? builderInfo.getBuilder() : null;
|
||||
if (role == null) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s attempted to spawn invalid NPC role '%s'", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.INVALID_ROLE);
|
||||
return false;
|
||||
} else if (!role.isSpawnable()) {
|
||||
SpawningPlugin.get().getLogger().at(Level.SEVERE).log("Marker %s attempted to spawn a non-spawnable (abstract) role '%s'", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.INVALID_ROLE);
|
||||
return false;
|
||||
} else if (!this.context.setSpawnable((ISpawnableWithModel) role)) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.SEVERE)
|
||||
.log("Marker %s failed to spawn NPC role '%s' due to failed role validation", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.FAILED_ROLE_VALIDATION);
|
||||
return false;
|
||||
} else {
|
||||
ObjectList<Ref<EntityStore>> results = SpatialResource.getThreadLocalReferenceList();
|
||||
SpatialResource<Ref<EntityStore>, EntityStore> spatialResource = store.getResource(EntityModule.get().getPlayerSpatialResourceType());
|
||||
spatialResource.getSpatialStructure().collect(position, marker.getExclusionRadius(), results);
|
||||
boolean hasPlayersInRange = !results.isEmpty();
|
||||
if (hasPlayersInRange) {
|
||||
this.refreshTimeout();
|
||||
return false;
|
||||
} else {
|
||||
World world = store.getExternalData().getWorld();
|
||||
if (!this.context.set(world, position.x, position.y, position.z)) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s attempted to spawn NPC '%s' at %s but could not fit", uuid, roleName, position);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.NO_ROOM);
|
||||
return false;
|
||||
} else {
|
||||
SpawnTestResult testResult = this.context.canSpawn(true, false);
|
||||
if (testResult != SpawnTestResult.TEST_OK) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s attempted to spawn NPC '%s' at %s but could not fit: %s", uuid, roleName, position, testResult);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.NO_ROOM);
|
||||
return false;
|
||||
} else {
|
||||
this.spawnPosition.assign(this.context.xSpawn, this.context.ySpawn, this.context.zSpawn);
|
||||
if (this.spawnPosition.distanceSquaredTo(position) > marker.getMaxDropHeightSquared()) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s attempted to spawn NPC '%s' but was offset too far from the ground at %s", uuid, roleName, position);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.TOO_HIGH);
|
||||
return false;
|
||||
} else {
|
||||
TriConsumer<NPCEntity, Ref<EntityStore>, Store<EntityStore>> postSpawn = (_entity, _ref, _store) -> {
|
||||
SpawnMarkerReference spawnMarkerReference = _store.ensureAndGetComponent(_ref, SpawnMarkerReference.getComponentType());
|
||||
spawnMarkerReference.getReference().setEntity(ref, _store);
|
||||
spawnMarkerReference.refreshTimeoutCounter();
|
||||
WorldGenId worldGenIdComponent = _store.getComponent(ref, WorldGenId.getComponentType());
|
||||
int worldGenId = worldGenIdComponent != null ? worldGenIdComponent.getWorldGenId() : 0;
|
||||
_store.putComponent(_ref, WorldGenId.getComponentType(), new WorldGenId(worldGenId));
|
||||
};
|
||||
Vector3f rotation = transformComponent.getRotation();
|
||||
Pair<Ref<EntityStore>, NPCEntity> npcPair = npcModule.spawnEntity(store, roleIndex, this.spawnPosition, rotation, null, postSpawn);
|
||||
if (npcPair == null) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.SEVERE)
|
||||
.log("Marker %s failed to spawn NPC role '%s' due to an internal error", uuid, roleName);
|
||||
this.fail(ref, uuid, roleName, position, store, SpawnMarkerEntity.FailReason.INVALID_ROLE);
|
||||
return false;
|
||||
} else {
|
||||
Ref<EntityStore> npcRef = npcPair.first();
|
||||
NPCEntity npcComponent = npcPair.second();
|
||||
Ref<EntityStore> flockReference = FlockPlugin.trySpawnFlock(
|
||||
npcRef, npcComponent, store, roleIndex, this.spawnPosition, rotation, spawn.getFlockDefinition(), postSpawn
|
||||
);
|
||||
EntityGroup group = flockReference == null ? null : store.getComponent(flockReference, EntityGroup.getComponentType());
|
||||
this.spawnCount = group != null ? group.size() : 1;
|
||||
if (this.storedFlock != null) {
|
||||
this.despawnStarted = false;
|
||||
this.npcReferences = new InvalidatablePersistentRef[this.spawnCount];
|
||||
if (group != null) {
|
||||
group.forEachMember((index, member, referenceArray) -> {
|
||||
InvalidatablePersistentRef referencex = new InvalidatablePersistentRef();
|
||||
referencex.setEntity(member, store);
|
||||
referenceArray[index] = referencex;
|
||||
}, this.npcReferences);
|
||||
} else {
|
||||
InvalidatablePersistentRef reference = new InvalidatablePersistentRef();
|
||||
reference.setEntity(npcRef, store);
|
||||
this.npcReferences[0] = reference;
|
||||
}
|
||||
|
||||
this.storedFlock.clear();
|
||||
}
|
||||
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log(
|
||||
"Marker %s spawned %s and set respawn to %s",
|
||||
uuid,
|
||||
npcComponent.getRoleName(),
|
||||
realtime ? this.respawnCounter : this.gameTimeRespawn
|
||||
);
|
||||
this.refreshTimeout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.FINE)
|
||||
.log("Marker %s performed noop spawn and set repawn to %s", uuid, realtime ? this.respawnCounter : this.gameTimeRespawn);
|
||||
this.refreshTimeout();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void fail(
|
||||
@Nonnull Ref<EntityStore> self,
|
||||
@Nonnull UUID uuid,
|
||||
@Nonnull String role,
|
||||
@Nonnull Vector3d position,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull SpawnMarkerEntity.FailReason reason
|
||||
) {
|
||||
if (++this.failedSpawns >= 5) {
|
||||
SpawningPlugin.get()
|
||||
.getLogger()
|
||||
.at(Level.WARNING)
|
||||
.log("Marker %s at %s removed due to repeated spawning fails of %s with reason: %s", uuid, position, role, reason);
|
||||
store.removeEntity(self, RemoveReason.REMOVE);
|
||||
} else {
|
||||
this.refreshTimeout();
|
||||
}
|
||||
}
|
||||
|
||||
public void setSpawnMarker(@Nonnull SpawnMarker marker) {
|
||||
this.spawnMarkerId = marker.getId();
|
||||
this.cachedMarker = marker;
|
||||
if (this.cachedMarker.getDeactivationDistance() > 0.0) {
|
||||
this.storedFlock = new StoredFlock();
|
||||
this.tempStorageList = new ObjectArrayList<>();
|
||||
} else {
|
||||
this.storedFlock = null;
|
||||
this.tempStorageList = null;
|
||||
}
|
||||
}
|
||||
|
||||
public int decrementAndGetSpawnCount() {
|
||||
return --this.spawnCount;
|
||||
}
|
||||
|
||||
public String getSpawnMarkerId() {
|
||||
return this.spawnMarkerId;
|
||||
}
|
||||
|
||||
public boolean isManualTrigger() {
|
||||
return this.cachedMarker.isManualTrigger();
|
||||
}
|
||||
|
||||
public boolean trigger(@Nonnull Ref<EntityStore> markerRef, @Nonnull Store<EntityStore> store) {
|
||||
return this.cachedMarker.isManualTrigger() && this.spawnCount <= 0 ? this.spawnNPC(markerRef, this.cachedMarker, store) : false;
|
||||
}
|
||||
|
||||
public void suppress(@Nonnull UUID suppressor) {
|
||||
if (this.suppressedBy == null) {
|
||||
this.suppressedBy = new HashSet<>();
|
||||
}
|
||||
|
||||
this.suppressedBy.add(suppressor);
|
||||
}
|
||||
|
||||
public void releaseSuppression(@Nonnull UUID suppressor) {
|
||||
if (this.suppressedBy != null) {
|
||||
this.suppressedBy.remove(suppressor);
|
||||
}
|
||||
}
|
||||
|
||||
public void clearAllSuppressions() {
|
||||
if (this.suppressedBy != null) {
|
||||
this.suppressedBy.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public Component<EntityStore> clone() {
|
||||
SpawnMarkerEntity spawnMarker = new SpawnMarkerEntity();
|
||||
spawnMarker.spawnMarkerId = this.spawnMarkerId;
|
||||
spawnMarker.cachedMarker = this.cachedMarker;
|
||||
spawnMarker.respawnCounter = this.respawnCounter;
|
||||
spawnMarker.gameTimeRespawn = this.gameTimeRespawn;
|
||||
spawnMarker.spawnAfter = this.spawnAfter;
|
||||
spawnMarker.spawnCount = this.spawnCount;
|
||||
spawnMarker.suppressedBy = this.suppressedBy != null ? new HashSet<>(this.suppressedBy) : null;
|
||||
spawnMarker.failedSpawns = this.failedSpawns;
|
||||
spawnMarker.spawnPosition.assign(this.spawnPosition);
|
||||
spawnMarker.npcReferences = this.npcReferences;
|
||||
spawnMarker.storedFlock = this.storedFlock != null ? this.storedFlock.clone() : null;
|
||||
spawnMarker.timeToDeactivation = this.timeToDeactivation;
|
||||
spawnMarker.despawnStarted = this.despawnStarted;
|
||||
spawnMarker.spawnLostTimeoutCounter = this.spawnLostTimeoutCounter;
|
||||
return spawnMarker;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
@Override
|
||||
public String toString() {
|
||||
return "SpawnMarkerEntity{spawnMarkerId='"
|
||||
+ this.spawnMarkerId
|
||||
+ "', cachedMarker="
|
||||
+ this.cachedMarker
|
||||
+ ", respawnCounter="
|
||||
+ this.respawnCounter
|
||||
+ ", gameTimeRespawn="
|
||||
+ this.gameTimeRespawn
|
||||
+ ", spawnAfter="
|
||||
+ this.spawnAfter
|
||||
+ ", spawnCount="
|
||||
+ this.spawnCount
|
||||
+ ", spawnLostTimeoutCounter="
|
||||
+ this.spawnLostTimeoutCounter
|
||||
+ ", failedSpawns="
|
||||
+ this.failedSpawns
|
||||
+ ", context="
|
||||
+ this.context
|
||||
+ ", spawnPosition="
|
||||
+ this.spawnPosition
|
||||
+ ", storedFlock="
|
||||
+ this.storedFlock
|
||||
+ "} "
|
||||
+ super.toString();
|
||||
}
|
||||
|
||||
public static Model getModel(@Nonnull SpawnMarker marker) {
|
||||
String modelName = marker.getModel();
|
||||
ModelAsset modelAsset = null;
|
||||
if (modelName != null && !modelName.isEmpty()) {
|
||||
modelAsset = ModelAsset.getAssetMap().getAsset(modelName);
|
||||
}
|
||||
|
||||
Model model;
|
||||
if (modelAsset == null) {
|
||||
model = SpawningPlugin.get().getSpawnMarkerModel();
|
||||
} else {
|
||||
model = Model.createUnitScaleModel(modelAsset);
|
||||
}
|
||||
|
||||
return model;
|
||||
}
|
||||
|
||||
private static enum FailReason {
|
||||
INVALID_ROLE,
|
||||
NONEXISTENT_ROLE,
|
||||
FAILED_ROLE_VALIDATION,
|
||||
NO_ROOM,
|
||||
TOO_HIGH;
|
||||
|
||||
private FailReason() {
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user