From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 From: wangxyper Date: Sun, 15 Jan 2023 09:56:15 +0800 Subject: [PATCH] Hearse: Base codes Original license: MIT Original project: https://github.com/NaturalCodeClub/Hearse diff --git a/src/main/java/co/earthme/hearse/Hearse.java b/src/main/java/co/earthme/hearse/Hearse.java new file mode 100644 index 0000000000000000000000000000000000000000..692fef51b2f15dd1ddc28773a381b9da3b42725e --- /dev/null +++ b/src/main/java/co/earthme/hearse/Hearse.java @@ -0,0 +1,27 @@ +package co.earthme.hearse; + +import co.earthme.hearse.commands.EntityCountCommand; +import co.earthme.hearse.commands.WorkerCommand; +import co.earthme.hearse.server.ServerEntityTickHook; +import co.earthme.hearse.workers.WorkerThreadPoolManager; +import net.minecraft.server.MinecraftServer; + +public class Hearse { + private static final WorkerThreadPoolManager workerManager = new WorkerThreadPoolManager(); + + public static void initAll(){ + HearseConfig.init(); + ServerEntityTickHook.init(); + MinecraftServer.getServer().server.getCommandMap().register("workers","hearse",new WorkerCommand()); + MinecraftServer.getServer().server.getCommandMap().register("entitycount","hearse",new EntityCountCommand()); + } + + public static void onServerStop(){ + HearseConfig.save(); + workerManager.shutdownAllNow(); + } + + public static WorkerThreadPoolManager getWorkerManager() { + return workerManager; + } +} diff --git a/src/main/java/co/earthme/hearse/HearseConfig.java b/src/main/java/co/earthme/hearse/HearseConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..73b5e76660b5162a7a0b327ddc7dcc3295b86699 --- /dev/null +++ b/src/main/java/co/earthme/hearse/HearseConfig.java @@ -0,0 +1,49 @@ +package co.earthme.hearse; + +import org.bukkit.configuration.InvalidConfigurationException; +import org.bukkit.configuration.file.YamlConfiguration; +import java.io.File; +import java.io.IOException; + +public class HearseConfig { + private static final YamlConfiguration configEntry = new YamlConfiguration(); + private static final File CONFIG_FILE = new File("hearse.yml"); + + public static void init(){ + try { + configEntry.load(CONFIG_FILE); + }catch (IOException ignored){ + } catch (InvalidConfigurationException e) { + e.printStackTrace(); + } + configEntry.options().copyDefaults(true); + } + + public static void save(){ + try { + configEntry.save(CONFIG_FILE); + } catch (IOException e) { + e.printStackTrace(); + } + } + + public static int getInt(String key,int def){ + configEntry.addDefault(key,def); + return configEntry.getInt(key); + } + + public static long getLong(String key,int def){ + configEntry.addDefault(key,def); + return configEntry.getLong(key); + } + + public static String getString(String key,String def){ + configEntry.addDefault(key,def); + return configEntry.getString(key); + } + + public static boolean getBoolean(String key,boolean def){ + configEntry.addDefault(key,def); + return configEntry.getBoolean(key); + } +} diff --git a/src/main/java/co/earthme/hearse/commands/EntityCountCommand.java b/src/main/java/co/earthme/hearse/commands/EntityCountCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..de759c808040058062078130b527e78215216ebb --- /dev/null +++ b/src/main/java/co/earthme/hearse/commands/EntityCountCommand.java @@ -0,0 +1,36 @@ +package co.earthme.hearse.commands; + +import com.google.common.collect.Maps; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import java.util.Map; + +public class EntityCountCommand extends Command { + public EntityCountCommand() { + super("entitycount"); + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + final Map counts = Maps.newHashMap(); + for (ServerLevel level : MinecraftServer.getServer().getAllLevels()){ + for (Entity entity : level.entityTickList.entities){ + final String name = entity.getType().getName(); + if (!counts.containsKey(name)){ + counts.put(name,0); + } + counts.replace(name,counts.get(name)+1); + } + } + sender.sendMessage("Exists entity Counts:"); + for (Map.Entry entry : counts.entrySet()){ + sender.sendMessage(ChatColor.BLUE+String.format("%s:%s",entry.getKey(),entry.getValue())); + } + return true; + } +} diff --git a/src/main/java/co/earthme/hearse/commands/WorkerCommand.java b/src/main/java/co/earthme/hearse/commands/WorkerCommand.java new file mode 100644 index 0000000000000000000000000000000000000000..1a4a6869a7278beadd97af006f4b5fae578b83ed --- /dev/null +++ b/src/main/java/co/earthme/hearse/commands/WorkerCommand.java @@ -0,0 +1,72 @@ +package co.earthme.hearse.commands; + +import co.earthme.hearse.Hearse; +import co.earthme.hearse.concurrent.WorkerThreadPoolExecutor; +import org.bukkit.ChatColor; +import org.bukkit.command.Command; +import org.bukkit.command.CommandSender; +import org.jetbrains.annotations.NotNull; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +public class WorkerCommand extends Command { + public WorkerCommand() { + super("workers"); + this.setPermission("hearse.commands.workers"); + this.setDescription("You can see or edit the server workers by using this command"); + this.setUsage("/workers "); + } + + @Override + public @NotNull List tabComplete(@NotNull CommandSender sender, @NotNull String alias, @NotNull String[] args) throws IllegalArgumentException { + final List ret = new ArrayList<>(); + if (args.length == 1){ + ret.add("status"); + ret.add("setThreadCount"); + ret.add("forceStop"); + } + if (args.length == 2){ + for (Map.Entry entry : Hearse.getWorkerManager().getManagedWorkers().entrySet()){ + ret.add(entry.getKey()); + } + } + return ret; + } + + @Override + public boolean execute(@NotNull CommandSender sender, @NotNull String commandLabel, @NotNull String[] args) { + if (args.length >= 2){ + final String action = args[0]; + final String workerName = args[1]; + final WorkerThreadPoolExecutor searchedWorker = Hearse.getWorkerManager().getTargetWorker(workerName); + if (searchedWorker == null){ + sender.sendMessage(ChatColor.RED+"Target worker not found!"); + return true; + } + switch (action){ + case "status": + sender.sendMessage(ChatColor.GREEN+"Worker: "+workerName+" Status:"+ searchedWorker); + break; + case "setThreadCount": + if (args.length == 3){ + try { + searchedWorker.setCorePoolSize(Integer.parseInt(args[2])); + sender.sendMessage(ChatColor.GREEN+"Finished!"); + }catch (NumberFormatException e){ + sender.sendMessage(ChatColor.RED+"Please supply an integer!"); + } + }else{ + sender.sendMessage(ChatColor.RED+"Please supply an integer!"); + } + break; + case "forceStop": + searchedWorker.shutdownNow(); + sender.sendMessage(ChatColor.YELLOW+"Worker "+workerName+" has been stopped!"); + break; + } + return true; + } + return false; + } +} diff --git a/src/main/java/co/earthme/hearse/concurrent/WorkerThread.java b/src/main/java/co/earthme/hearse/concurrent/WorkerThread.java new file mode 100644 index 0000000000000000000000000000000000000000..421d4926ac674b5eb12d9613ceb6d20185ea557d --- /dev/null +++ b/src/main/java/co/earthme/hearse/concurrent/WorkerThread.java @@ -0,0 +1,18 @@ +package co.earthme.hearse.concurrent; + +import io.papermc.paper.util.TickThread; + +public class WorkerThread extends TickThread { + + public WorkerThread(String name) { + super(name); + } + + public WorkerThread(Runnable run, String name) { + super(run, name); + } + + public static boolean isWorker(){ + return Thread.currentThread() instanceof WorkerThread; + } +} diff --git a/src/main/java/co/earthme/hearse/concurrent/WorkerThreadFactory.java b/src/main/java/co/earthme/hearse/concurrent/WorkerThreadFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..e65b1eba68003a9f7ce5080d07a521817831ff48 --- /dev/null +++ b/src/main/java/co/earthme/hearse/concurrent/WorkerThreadFactory.java @@ -0,0 +1,5 @@ +package co.earthme.hearse.concurrent; + +public interface WorkerThreadFactory { + WorkerThread getNewThread(Runnable task); +} diff --git a/src/main/java/co/earthme/hearse/concurrent/WorkerThreadPoolExecutor.java b/src/main/java/co/earthme/hearse/concurrent/WorkerThreadPoolExecutor.java new file mode 100644 index 0000000000000000000000000000000000000000..7e010bf23c9fc26284212a4388172f5d7d5a4b99 --- /dev/null +++ b/src/main/java/co/earthme/hearse/concurrent/WorkerThreadPoolExecutor.java @@ -0,0 +1,76 @@ +package co.earthme.hearse.concurrent; + +import org.jetbrains.annotations.NotNull; + +import java.util.Queue; +import java.util.concurrent.*; +import java.util.concurrent.locks.LockSupport; + +public class WorkerThreadPoolExecutor extends ThreadPoolExecutor { + private final Queue taskEntries = new ConcurrentLinkedQueue<>(); + + public WorkerThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, @NotNull TimeUnit unit, @NotNull BlockingQueue workQueue, @NotNull WorkerThreadFactory workerThreadFactory) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, workerThreadFactory::getNewThread); + } + + public WorkerThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, @NotNull TimeUnit unit, @NotNull BlockingQueue workQueue, @NotNull WorkerThreadFactory workerThreadFactory, @NotNull RejectedExecutionHandler handler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, workerThreadFactory::getNewThread, handler); + } + + public int getCurrentNotProcessingTasks(){ + return this.getQueue().size(); + } + + public void clearAllTasks(){ + this.getQueue().clear(); + } + + public void executeWithSubTask(Runnable mainTask,Runnable subTask){ + final TaskEntry wrapped = new TaskEntry(subTask,mainTask); + this.taskEntries.offer(wrapped); + this.execute(wrapped); + } + + public void runAllSubTasks(){ + TaskEntry task; + while ((task = this.taskEntries.poll())!=null){ + while (!task.allRunned()){ + LockSupport.parkNanos(this,10000000); + } + } + } + + private static class TaskEntry implements Runnable{ + private final Runnable mainTask; + private final Runnable subTask; + private volatile boolean mainTaskFinished = false; + + public TaskEntry(Runnable subTask,Runnable mainTask){ + this.subTask = subTask; + this.mainTask = mainTask; + } + + public boolean allRunned(){ + if (!this.mainTaskFinished){ + return false; + } + try { + this.subTask.run(); + }catch (Exception e){ + e.printStackTrace(); + } + return true; + } + + @Override + public void run() { + try { + this.mainTask.run(); + }catch(Exception e){ + e.printStackTrace(); + }finally { + this.mainTaskFinished = true; + } + } + } +} diff --git a/src/main/java/co/earthme/hearse/concurrent/threadfactory/DefaultWorkerFactory.java b/src/main/java/co/earthme/hearse/concurrent/threadfactory/DefaultWorkerFactory.java new file mode 100644 index 0000000000000000000000000000000000000000..03a29509821a17faac2dc8ab810a2693b03bfbc6 --- /dev/null +++ b/src/main/java/co/earthme/hearse/concurrent/threadfactory/DefaultWorkerFactory.java @@ -0,0 +1,42 @@ +package co.earthme.hearse.concurrent.threadfactory; + +import co.earthme.hearse.concurrent.WorkerThread; +import co.earthme.hearse.concurrent.WorkerThreadFactory; +import it.unimi.dsi.fastutil.objects.ObjectArrayList; +import it.unimi.dsi.fastutil.objects.ObjectLists; +import net.minecraft.server.MinecraftServer; + +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +public class DefaultWorkerFactory implements WorkerThreadFactory { + private static final AtomicInteger poolId = new AtomicInteger(); + private final AtomicInteger threadId = new AtomicInteger(); + private final String bound; + private final List createdThreads = ObjectLists.synchronize(new ObjectArrayList<>()); + + public DefaultWorkerFactory(String bound){ + poolId.getAndIncrement(); + this.bound = bound; + } + + public List getCreatedThreads() { + return this.createdThreads; + } + + @Override + public WorkerThread getNewThread(Runnable task) { + final WorkerThread workerThread = new WorkerThread(()->{ + try { + task.run(); + }finally { + this.createdThreads.remove(Thread.currentThread()); + } + },"pool-"+poolId.get()+"-worker-"+threadId.getAndIncrement()+"-bound-"+this.bound); + this.createdThreads.add(workerThread); + workerThread.setDaemon(true); + workerThread.setPriority(Thread.NORM_PRIORITY - 2); + workerThread.setContextClassLoader(MinecraftServer.class.getClassLoader()); + return workerThread; + } +} diff --git a/src/main/java/co/earthme/hearse/server/ServerEntityTickHook.java b/src/main/java/co/earthme/hearse/server/ServerEntityTickHook.java new file mode 100644 index 0000000000000000000000000000000000000000..c0e7a9cf79ddf00827daba0aa9c7a32fa76b0c7c --- /dev/null +++ b/src/main/java/co/earthme/hearse/server/ServerEntityTickHook.java @@ -0,0 +1,102 @@ +package co.earthme.hearse.server; + +import co.earthme.hearse.Hearse; +import co.earthme.hearse.HearseConfig; +import co.earthme.hearse.concurrent.WorkerThreadFactory; +import co.earthme.hearse.concurrent.WorkerThreadPoolExecutor; +import co.earthme.hearse.concurrent.threadfactory.DefaultWorkerFactory; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.player.Player; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class ServerEntityTickHook { + private static final Logger logger = LogManager.getLogger(); + private static volatile boolean firstTick = false; + private static final WorkerThreadFactory defFactory = new DefaultWorkerFactory("entity"); + private static final AtomicInteger threadId = new AtomicInteger(); + private static WorkerThreadPoolExecutor worker; + private static boolean asyncEntityEnabled; + + public static void executeAsyncTask(Runnable task){ + if (!asyncEntityEnabled){ + throw new RejectedExecutionException(); + } + worker.execute(task); + } + + public static void init(){ + boolean asyncEntityEnabled1 = HearseConfig.getBoolean("optimizations.enable-async-entity",true); + final int workerCount = HearseConfig.getInt("workers.async-entity-worker-count",Runtime.getRuntime().availableProcessors()); + if (asyncEntityEnabled1){ + worker = new WorkerThreadPoolExecutor( + workerCount, + workerCount, + 0L, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + defFactory + ); + Hearse.getWorkerManager().addWorker("entity",worker); + } + asyncEntityEnabled = asyncEntityEnabled1; + } + + public static void executeAsyncTaskWithMainThreadCallback(Runnable task,Runnable callBack){ + if (!asyncEntityEnabled){ + throw new IllegalStateException(); + } + worker.executeWithSubTask(task,callBack); + } + + public static void callTickStart(){ + if (!firstTick){ + firstTick = true; + return; + } + if (!asyncEntityEnabled){ + return; + } + worker.runAllSubTasks(); + } + + public static void callAsyncEntityTick(Entity entity, ServerLevel level){ + MinecraftServer.getServer().executeMidTickTasks(); + Runnable task = ()->{ + entity.activatedPriorityReset = false; + if (!entity.isRemoved()) { + entity.checkDespawn(); + Entity entity1 = entity.getVehicle(); + if (entity1 != null) { + if (!entity1.isRemoved() && entity1.hasPassenger(entity)) { + return; + } + entity.stopRiding(); + } + try { + level.tickNonPassenger(entity); + } catch (Throwable throwable) { + if (throwable instanceof ThreadDeath) throw throwable; + level.getCraftServer().getPluginManager().callEvent(new com.destroystokyo.paper.event.server.ServerExceptionEvent(new com.destroystokyo.paper.exception.ServerInternalException(throwable.getMessage(), throwable))); + throwable.printStackTrace(); + } + } + }; + if (!asyncEntityEnabled){ + task.run(); + return; + } + try { + worker.execute(task); + }catch (RejectedExecutionException e){ + logger.warn("Worker rejected our task.Falling back to sync entity updating"); + asyncEntityEnabled = false; + } + } +} diff --git a/src/main/java/co/earthme/hearse/server/ServerLevelTickHook.java b/src/main/java/co/earthme/hearse/server/ServerLevelTickHook.java new file mode 100644 index 0000000000000000000000000000000000000000..8085eb700d8e5c20ebb5bfeceb78198c6e973019 --- /dev/null +++ b/src/main/java/co/earthme/hearse/server/ServerLevelTickHook.java @@ -0,0 +1,78 @@ +package co.earthme.hearse.server; + +import co.earthme.hearse.Hearse; +import co.earthme.hearse.HearseConfig; +import co.earthme.hearse.concurrent.WorkerThread; +import co.earthme.hearse.concurrent.WorkerThreadPoolExecutor; +import co.earthme.hearse.concurrent.threadfactory.DefaultWorkerFactory; +import net.minecraft.CrashReport; +import net.minecraft.ReportedException; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.level.ServerLevel; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.LockSupport; +import java.util.function.BooleanSupplier; + +public class ServerLevelTickHook { + private static final DefaultWorkerFactory workerFactory = new DefaultWorkerFactory("world"); + private static WorkerThreadPoolExecutor worker; + private static boolean enabledParaWorld; + private static volatile boolean inited = false; + private static final AtomicInteger activeTaskCount = new AtomicInteger(); + private static final Logger logger = LogManager.getLogger(); + + public static void initWorker(){ + enabledParaWorld = HearseConfig.getBoolean("optimizations.enableparallelworldtick",true); + if (enabledParaWorld){ + worker = new WorkerThreadPoolExecutor( + MinecraftServer.getServer().levels.size(), + MinecraftServer.getServer().levels.size(), + Long.MAX_VALUE, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + workerFactory + ); + worker.allowCoreThreadTimeOut(true); + worker.prestartAllCoreThreads(); + Hearse.getWorkerManager().addWorker("world",worker); + for (Thread worker : workerFactory.getCreatedThreads()){ + logger.warn("World worker name:{}.This can help you to slove the lag problems when you using parallel world ticking",worker.getName()); + } + } + inited = true; + } + + public static boolean isInited(){ + return inited; + } + + public static void callWorldTick(ServerLevel worldserver, BooleanSupplier shouldKeepTicking){ + activeTaskCount.getAndIncrement(); + worker.execute(()->{ + try { + try { + worldserver.tick(shouldKeepTicking); + for (final io.papermc.paper.chunk.SingleThreadChunkRegionManager regionManager : worldserver.getChunkSource().chunkMap.regionManagers) { + regionManager.recalculateRegions(); + } + } catch (Throwable throwable) { + throwable.printStackTrace(); + } + worldserver.explosionDensityCache.clear(); + }finally { + activeTaskCount.getAndDecrement(); + } + }); + } + + public static void awaitWorldTicKTasks(){ + while (activeTaskCount.get() > 0){ + LockSupport.parkNanos("Await world ticking",1000000); + } + } +} diff --git a/src/main/java/co/earthme/hearse/workers/WorkerThreadPoolManager.java b/src/main/java/co/earthme/hearse/workers/WorkerThreadPoolManager.java new file mode 100644 index 0000000000000000000000000000000000000000..527dba288e1988773fd5a89f076f92084034f421 --- /dev/null +++ b/src/main/java/co/earthme/hearse/workers/WorkerThreadPoolManager.java @@ -0,0 +1,68 @@ +package co.earthme.hearse.workers; + +import co.earthme.hearse.concurrent.WorkerThreadPoolExecutor; +import com.google.common.collect.Maps; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +public class WorkerThreadPoolManager { + private final Map managedWorkers = Maps.newConcurrentMap(); + + public void addWorker(String bound,WorkerThreadPoolExecutor worker){ + this.managedWorkers.put(bound,worker); + } + + public void shutdownAll() throws InterruptedException { + for (WorkerThreadPoolExecutor worker : this.managedWorkers.values()){ + if (!worker.isShutdown()){ + worker.getQueue().clear(); //Clear the tasks.We don't need wait them + worker.shutdown(); + while (worker.awaitTermination(100, TimeUnit.MILLISECONDS)) {} + } + } + } + + @Deprecated + public Map getManagedWorkers() { + return Maps.newHashMap(this.managedWorkers); + } + + @Deprecated + public WorkerThreadPoolExecutor getTargetWorker(String bound){ + return this.managedWorkers.get(bound); + } + + public Map> shutdownAllNow(){ + final Map> ret = Maps.newHashMap(); + for (Map.Entry entry : this.managedWorkers.entrySet()){ + final String workerName = entry.getKey(); + final WorkerThreadPoolExecutor worker = entry.getValue(); + if (!worker.isShutdown()){ + try { + final List taskNotRunned = worker.shutdownNow(); + ret.put(workerName,taskNotRunned); + }catch (Exception e){ + e.printStackTrace(); + } + } + } + return ret; + } + + public void shutdownAll(long singleWorkerAwaitTimeOutCount) throws InterruptedException { + long counter = singleWorkerAwaitTimeOutCount; + for (WorkerThreadPoolExecutor worker : this.managedWorkers.values()){ + if (!worker.isShutdown()){ + worker.shutdown(); + while (worker.awaitTermination(1, TimeUnit.MILLISECONDS)) { + if (counter == 0){ + break; + } + counter--; + } + counter = singleWorkerAwaitTimeOutCount; + } + } + } +} diff --git a/src/main/java/net/minecraft/world/entity/EntityType.java b/src/main/java/net/minecraft/world/entity/EntityType.java index f7e8b6e1872a397c96afc938754726b0d4e493b4..2448673ee847fe3bc05f1269737aae5b43ae8291 100644 --- a/src/main/java/net/minecraft/world/entity/EntityType.java +++ b/src/main/java/net/minecraft/world/entity/EntityType.java @@ -308,6 +308,16 @@ public class EntityType implements FeatureElement, EntityTypeT return (EntityType) Registry.register(BuiltInRegistries.ENTITY_TYPE, id, (EntityType) type.build(id)); // CraftBukkit - decompile error } + // Purpur start + public static EntityType getFromBukkitType(org.bukkit.entity.EntityType bukkitType) { + return getFromKey(new ResourceLocation(bukkitType.getKey().toString())); + } + + public static EntityType getFromKey(ResourceLocation location) { + return BuiltInRegistries.ENTITY_TYPE.get(location); + } + // Purpur end + public static ResourceLocation getKey(EntityType type) { return BuiltInRegistries.ENTITY_TYPE.getKey(type); } @@ -522,6 +532,16 @@ public class EntityType implements FeatureElement, EntityTypeT return this.category; } + // Purpur start + public String getName() { + return BuiltInRegistries.ENTITY_TYPE.getKey(this).getPath(); + } + + public String getTranslatedName() { + return getDescription().getString(); + } + // Purpur end + public String getDescriptionId() { if (this.descriptionId == null) { this.descriptionId = Util.makeDescriptionId("entity", BuiltInRegistries.ENTITY_TYPE.getKey(this));