init
This commit is contained in:
+30
@@ -0,0 +1,30 @@
|
||||
# Build output
|
||||
target/
|
||||
out/
|
||||
build/
|
||||
*.class
|
||||
*.jar
|
||||
dependency-reduced-pom.xml
|
||||
|
||||
# IDE
|
||||
.idea/
|
||||
*.iml
|
||||
*.iws
|
||||
*.ipr
|
||||
.vscode/
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Local config / secrets
|
||||
config.json.local
|
||||
*.env
|
||||
.env
|
||||
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>net.kewwbec</groupId>
|
||||
<artifactId>corenpc</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>CoreNpc</name>
|
||||
<description>Reusable NPC service: handler-routed interactions + admin /npc editor for KweebecNet plugins</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<hytale.server.jar>${user.home}/AppData/Roaming/Hytale/install/release/package/game/latest/Server/HytaleServer.jar</hytale.server.jar>
|
||||
<networkcore.jar>${project.basedir}/../Core/target/NetworkCore-0.1.0.jar</networkcore.jar>
|
||||
</properties>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.hypixel.hytale</groupId>
|
||||
<artifactId>hytale-server</artifactId>
|
||||
<version>local</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${hytale.server.jar}</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>net.kewwbec</groupId>
|
||||
<artifactId>networkcore</artifactId>
|
||||
<version>0.1.0</version>
|
||||
<scope>system</scope>
|
||||
<systemPath>${networkcore.jar}</systemPath>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.google.code.findbugs</groupId>
|
||||
<artifactId>jsr305</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<finalName>${project.artifactId}-${project.version}</finalName>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-shade-plugin</artifactId>
|
||||
<version>3.5.1</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<phase>package</phase>
|
||||
<goals>
|
||||
<goal>shade</goal>
|
||||
</goals>
|
||||
<configuration>
|
||||
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||
<minimizeJar>false</minimizeJar>
|
||||
<artifactSet>
|
||||
<excludes>
|
||||
<exclude>com.hypixel.hytale:*</exclude>
|
||||
<exclude>net.kewwbec:networkcore</exclude>
|
||||
<exclude>com.google.code.findbugs:*</exclude>
|
||||
</excludes>
|
||||
</artifactSet>
|
||||
<filters>
|
||||
<filter>
|
||||
<artifact>*:*</artifact>
|
||||
<excludes>
|
||||
<exclude>META-INF/*.SF</exclude>
|
||||
<exclude>META-INF/*.DSA</exclude>
|
||||
<exclude>META-INF/*.RSA</exclude>
|
||||
<exclude>module-info.class</exclude>
|
||||
</excludes>
|
||||
</filter>
|
||||
</filters>
|
||||
</configuration>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,10 @@
|
||||
package net.kewwbec.corenpc;
|
||||
|
||||
public final class CoreNpcItems {
|
||||
|
||||
/** Held admin tool whose right-click action tags the focused entity instead of freeze/clone/remove. */
|
||||
public static final String TAG_WAND = "CoreNpc_Tag_Wand";
|
||||
|
||||
private CoreNpcItems() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package net.kewwbec.corenpc;
|
||||
|
||||
public final class CoreNpcPermissions {
|
||||
|
||||
public static final String ADMIN = "corenpc.admin";
|
||||
|
||||
private CoreNpcPermissions() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
package net.kewwbec.corenpc;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerDisconnectEvent;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerInteractEvent;
|
||||
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.plugin.JavaPlugin;
|
||||
import com.hypixel.hytale.server.core.plugin.JavaPluginInit;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import net.kewwbec.corenpc.api.Npcs;
|
||||
import net.kewwbec.corenpc.command.NpcCommand;
|
||||
import net.kewwbec.corenpc.command.NpcSelectionStore;
|
||||
import net.kewwbec.corenpc.command.TagModeStore;
|
||||
import net.kewwbec.corenpc.handler.RunCommandHandler;
|
||||
import net.kewwbec.corenpc.impl.CoreNpcUseInteraction;
|
||||
import net.kewwbec.corenpc.impl.EnsureInteractionsSystem;
|
||||
import net.kewwbec.corenpc.impl.InteractionRouter;
|
||||
import net.kewwbec.corenpc.impl.NpcMarkerComponent;
|
||||
import net.kewwbec.corenpc.impl.NpcsImpl;
|
||||
import net.kewwbec.corenpc.impl.TagNpcInteraction;
|
||||
import net.kewwbec.networkcore.NetworkCore;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class CoreNpcPlugin extends JavaPlugin {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private static volatile CoreNpcPlugin instance;
|
||||
|
||||
private NpcsImpl npcs;
|
||||
private InteractionRouter router;
|
||||
private NpcSelectionStore selectionStore;
|
||||
private TagModeStore tagModeStore;
|
||||
|
||||
public CoreNpcPlugin(@Nonnull JavaPluginInit init) {
|
||||
super(init);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static CoreNpcPlugin getInstance() {
|
||||
CoreNpcPlugin i = instance;
|
||||
if (i == null) throw new IllegalStateException("CoreNpc not initialized");
|
||||
return i;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setup() {
|
||||
instance = this;
|
||||
NpcMarkerComponent.register(getEntityStoreRegistry());
|
||||
getCodecRegistry(Interaction.CODEC).register(
|
||||
TagNpcInteraction.INTERACTION_ID, TagNpcInteraction.class, TagNpcInteraction.CODEC);
|
||||
getCodecRegistry(Interaction.CODEC).register(
|
||||
CoreNpcUseInteraction.CODEC_ID, CoreNpcUseInteraction.class, CoreNpcUseInteraction.CODEC);
|
||||
registerUseInteractionAssets();
|
||||
this.npcs = new NpcsImpl();
|
||||
this.selectionStore = new NpcSelectionStore();
|
||||
this.tagModeStore = new TagModeStore();
|
||||
this.router = new InteractionRouter(npcs, tagModeStore);
|
||||
LOGGER.at(Level.INFO).log("CoreNpc setup complete (marker + tag/use interactions registered)");
|
||||
}
|
||||
|
||||
private void registerUseInteractionAssets() {
|
||||
try {
|
||||
Interaction.getAssetStore().loadAssets(
|
||||
"ThirdParty:CoreNpc",
|
||||
List.of(new CoreNpcUseInteraction(CoreNpcUseInteraction.ASSET_ID)));
|
||||
RootInteraction.getAssetStore().loadAssets(
|
||||
"ThirdParty:CoreNpc",
|
||||
List.of(new RootInteraction(CoreNpcUseInteraction.ASSET_ID, CoreNpcUseInteraction.ASSET_ID)));
|
||||
} catch (Exception e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e))
|
||||
.log("Failed to load Use interaction assets");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void start() {
|
||||
NetworkCore core = NetworkCore.getInstance();
|
||||
core.registerService(Npcs.class, npcs);
|
||||
npcs.registerHandler(RunCommandHandler.HANDLER_ID, new RunCommandHandler());
|
||||
|
||||
getEventRegistry().registerGlobal(PlayerInteractEvent.class, router::handle);
|
||||
getEventRegistry().registerGlobal(PlayerDisconnectEvent.class, this::onDisconnect);
|
||||
getCommandRegistry().registerCommand(new NpcCommand(npcs, selectionStore, tagModeStore));
|
||||
getEntityStoreRegistry().registerSystem(new EnsureInteractionsSystem());
|
||||
LOGGER.at(Level.INFO).log("CoreNpc started (service + /cnpc command + runcmd handler + ensure-interactions)");
|
||||
}
|
||||
|
||||
private void onDisconnect(@Nonnull PlayerDisconnectEvent event) {
|
||||
PlayerRef ref = event.getPlayerRef();
|
||||
if (ref == null) return;
|
||||
if (selectionStore != null) selectionStore.clear(ref.getUuid());
|
||||
if (tagModeStore != null) tagModeStore.clear(ref.getUuid());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void shutdown() {
|
||||
npcs = null;
|
||||
router = null;
|
||||
selectionStore = null;
|
||||
tagModeStore = null;
|
||||
instance = null;
|
||||
LOGGER.at(Level.INFO).log("CoreNpc shutdown complete");
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public Npcs getNpcs() {
|
||||
Npcs n = this.npcs;
|
||||
if (n == null) throw new IllegalStateException("Npcs service not yet wired");
|
||||
return n;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public NpcsImpl getNpcsImpl() {
|
||||
NpcsImpl n = this.npcs;
|
||||
if (n == null) throw new IllegalStateException("Npcs not yet wired");
|
||||
return n;
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public TagModeStore getTagModeStore() {
|
||||
TagModeStore t = this.tagModeStore;
|
||||
if (t == null) throw new IllegalStateException("TagModeStore not yet wired");
|
||||
return t;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package net.kewwbec.corenpc.api;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Read-only view of a CoreNpc-managed NPC. Backed by the {@code NpcMarkerComponent}
|
||||
* on the underlying entity, refreshed on every getter call so changes made elsewhere
|
||||
* (e.g. by the {@code /npc} command) are visible immediately.
|
||||
*
|
||||
* <p>Mutations are admin-only and flow through the {@code /npc} command — there is
|
||||
* deliberately no public setter API on this handle.</p>
|
||||
*/
|
||||
public interface NpcHandle {
|
||||
|
||||
@Nonnull String id();
|
||||
@Nonnull String handlerId();
|
||||
@Nonnull String namePlate();
|
||||
boolean namePlateVisible();
|
||||
@Nonnull String roleName();
|
||||
@Nonnull Map<String, String> args();
|
||||
|
||||
@Nonnull String worldName();
|
||||
@Nonnull Vector3d position();
|
||||
float yaw();
|
||||
|
||||
/** Reference to the backing ECS entity; {@code null} if the world is unloaded. */
|
||||
@Nullable
|
||||
Ref<EntityStore> entityRef();
|
||||
|
||||
/** True when the backing entity is currently present in a loaded world. */
|
||||
boolean isSpawned();
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package net.kewwbec.corenpc.api;
|
||||
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Plugin-supplied callback that runs when a player interacts with an NPC routed to this
|
||||
* handler id. The id is stored on the NPC's persisted definition and looked up at click time.
|
||||
*
|
||||
* <p><b>Threading:</b> {@link #onInteract} runs on the world thread that owns the NPC's
|
||||
* entity store. World/entity mutations are safe; HTTP, DB I/O, or other blocking work
|
||||
* must be moved to a background executor.</p>
|
||||
*
|
||||
* <p>Register handlers from your plugin's {@code start()}:
|
||||
* <pre>{@code
|
||||
* Npcs npcs = NetworkCore.getInstance().findService(Npcs.class);
|
||||
* npcs.registerHandler("queue:bridge_duels", (player, npc, args) -> {
|
||||
* queueService.join(player, args.getOrDefault("gameId", "bridge_duels"));
|
||||
* });
|
||||
* }</pre>
|
||||
*/
|
||||
@FunctionalInterface
|
||||
public interface NpcInteractionHandler {
|
||||
|
||||
/**
|
||||
* @param player the interacting player
|
||||
* @param npc the NPC that was clicked
|
||||
* @param args handler arguments stored on the NPC's definition (immutable view)
|
||||
*/
|
||||
void onInteract(@Nonnull PlayerRef player, @Nonnull NpcHandle npc, @Nonnull Map<String, String> args);
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package net.kewwbec.corenpc.api;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collection;
|
||||
|
||||
/**
|
||||
* Service for interactive NPCs. Registered by {@code CoreNpc} and resolved through
|
||||
* {@code NetworkCore.getInstance().findService(Npcs.class)}.
|
||||
*
|
||||
* <p>NPCs are existing entities (Trork, Mannequin, animal, whatever) that an admin
|
||||
* tagged via {@code /npc tag <handlerId>}. The marker component carries the handler
|
||||
* id + args and rides Hytale's world save automatically. Consumers register handlers;
|
||||
* they don't spawn or place NPCs.</p>
|
||||
*/
|
||||
public interface Npcs {
|
||||
|
||||
/**
|
||||
* Register a callback that fires when a player interacts with any NPC whose
|
||||
* marker {@code handlerId} equals {@code handlerId}. Idempotent for the same id;
|
||||
* a re-register replaces the previous handler.
|
||||
*/
|
||||
void registerHandler(@Nonnull String handlerId, @Nonnull NpcInteractionHandler handler);
|
||||
|
||||
/**
|
||||
* Remove a previously registered handler. NPCs routed to it no-op (with a warning)
|
||||
* until re-registered.
|
||||
*/
|
||||
void unregisterHandler(@Nonnull String handlerId);
|
||||
|
||||
/** Despawn the entity behind {@code handle}. Use {@code /npc untag} for marker-only removal. */
|
||||
void despawn(@Nonnull NpcHandle handle);
|
||||
|
||||
/**
|
||||
* Look up by id across every loaded world. <b>Only safe to call from a
|
||||
* non-world thread</b> (e.g. an async task) because Hytale asserts that
|
||||
* world stores are accessed from their own ticking thread. From inside a
|
||||
* command or event handler, use {@link #byIdInWorld(String, World)}.
|
||||
*/
|
||||
@Nullable
|
||||
NpcHandle byId(@Nonnull String id);
|
||||
|
||||
/**
|
||||
* Look up by id within a single world's store. Safe to call from that
|
||||
* world's tick thread (e.g. inside a command's {@code execute}).
|
||||
*/
|
||||
@Nullable
|
||||
NpcHandle byIdInWorld(@Nonnull String id, @Nonnull com.hypixel.hytale.server.core.universe.world.World world);
|
||||
|
||||
/** All NPCs whose backing entity is in the given world. */
|
||||
@Nonnull
|
||||
Collection<NpcHandle> inWorld(@Nonnull String worldName);
|
||||
|
||||
/** Every NPC the service knows about across all loaded worlds. */
|
||||
@Nonnull
|
||||
Collection<NpcHandle> all();
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package net.kewwbec.corenpc.command;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
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 net.kewwbec.corenpc.CoreNpcPermissions;
|
||||
import net.kewwbec.corenpc.api.NpcHandle;
|
||||
import net.kewwbec.corenpc.impl.NpcsImpl;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Sub-collection for {@code /npc arg set|unset|list}: per-NPC handler arguments.
|
||||
*/
|
||||
public final class NpcArgSub extends AbstractCommandCollection {
|
||||
|
||||
public NpcArgSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("arg", "Edit the selected NPC's handler args");
|
||||
addSubCommand(new SetSub(npcs, selection));
|
||||
addSubCommand(new UnsetSub(npcs, selection));
|
||||
addSubCommand(new ListSub(npcs, selection));
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
public static final class SetSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<String> keyArg;
|
||||
private final RequiredArg<String> valueArg;
|
||||
|
||||
public SetSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("set", "Set an arg on the selected NPC");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.keyArg = withRequiredArg("key", "Arg key", ArgTypes.STRING);
|
||||
this.valueArg = withRequiredArg("value", "Arg value", ArgTypes.GREEDY_STRING);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = NpcCommand.requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
String k = ctx.get(keyArg);
|
||||
String v = ctx.get(valueArg);
|
||||
npcs.setArg(h, k, v);
|
||||
ctx.sendMessage(Message.raw("Set " + k + " = " + v + " on " + h.id() + ".").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class UnsetSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<String> keyArg;
|
||||
|
||||
public UnsetSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("unset", "Remove an arg from the selected NPC");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.keyArg = withRequiredArg("key", "Arg key", ArgTypes.STRING);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = NpcCommand.requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
String k = ctx.get(keyArg);
|
||||
npcs.removeArg(h, k);
|
||||
ctx.sendMessage(Message.raw("Removed " + k + " from " + h.id() + ".").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
public static final class ListSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public ListSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("list", "List args on the selected NPC");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = NpcCommand.requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
Map<String, String> args = h.args();
|
||||
if (args.isEmpty()) {
|
||||
ctx.sendMessage(Message.raw("(no args)").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
ctx.sendMessage(Message.raw("--- args for " + h.id() + " ---").color(Color.GRAY));
|
||||
for (Map.Entry<String, String> e : args.entrySet()) {
|
||||
ctx.sendMessage(Message.raw(" " + e.getKey() + " = " + e.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,674 @@
|
||||
package net.kewwbec.corenpc.command;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.math.vector.Transform;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandContext;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.system.OptionalArg;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.system.RequiredArg;
|
||||
import com.hypixel.hytale.server.core.command.system.arguments.types.ArgTypes;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractCommandCollection;
|
||||
import com.hypixel.hytale.server.core.command.system.basecommands.AbstractPlayerCommand;
|
||||
import com.hypixel.hytale.server.core.entity.entities.Player;
|
||||
import com.hypixel.hytale.server.core.inventory.ItemStack;
|
||||
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 net.kewwbec.corenpc.CoreNpcItems;
|
||||
import net.kewwbec.corenpc.CoreNpcPermissions;
|
||||
import net.kewwbec.corenpc.api.NpcHandle;
|
||||
import net.kewwbec.corenpc.impl.NpcsImpl;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.awt.Color;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public final class NpcCommand extends AbstractCommandCollection {
|
||||
|
||||
public NpcCommand(@Nonnull NpcsImpl npcs,
|
||||
@Nonnull NpcSelectionStore selection,
|
||||
@Nonnull TagModeStore tagMode) {
|
||||
super("cnpc", "CoreNpc admin: tag entities as interactive NPCs and edit them");
|
||||
addSubCommand(new TagSub(tagMode));
|
||||
addSubCommand(new TagIdSub(npcs, selection));
|
||||
addSubCommand(new CancelSub(tagMode));
|
||||
addSubCommand(new UntagSub(npcs, selection));
|
||||
addSubCommand(new DeleteSub(npcs, selection));
|
||||
addSubCommand(new GetToolSub());
|
||||
addSubCommand(new SelectSub(npcs, selection));
|
||||
addSubCommand(new DeselectSub(selection));
|
||||
addSubCommand(new InfoSub(npcs, selection));
|
||||
addSubCommand(new TpHereSub(npcs, selection));
|
||||
addSubCommand(new TpToSub(npcs, selection));
|
||||
addSubCommand(new MoveSub(npcs, selection));
|
||||
addSubCommand(new MoveToSub(npcs, selection));
|
||||
addSubCommand(new RotateSub(npcs, selection));
|
||||
addSubCommand(new FaceSub(npcs, selection));
|
||||
addSubCommand(new NameSub(npcs, selection));
|
||||
addSubCommand(new HandlerSub(npcs, selection));
|
||||
addSubCommand(new NpcArgSub(npcs, selection));
|
||||
addSubCommand(new ListSub(npcs));
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
// ---- helpers -------------------------------------------------------------
|
||||
|
||||
@Nullable
|
||||
static NpcHandle requireSelected(@Nonnull NpcsImpl npcs,
|
||||
@Nonnull NpcSelectionStore selection,
|
||||
@Nonnull CommandContext ctx,
|
||||
@Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
String id = selection.get(sender.getUuid());
|
||||
if (id == null) {
|
||||
ctx.sendMessage(Message.raw("No NPC selected. Use /cnpc select <id> first.").color(Color.YELLOW));
|
||||
return null;
|
||||
}
|
||||
NpcHandle handle = npcs.byIdInWorld(id, world);
|
||||
if (handle == null) {
|
||||
ctx.sendMessage(Message.raw("Selected NPC '" + id + "' is not in this world.").color(Color.YELLOW));
|
||||
return null;
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
static void requireSameWorld(@Nonnull CommandContext ctx,
|
||||
@Nonnull World current,
|
||||
@Nonnull NpcHandle target,
|
||||
@Nonnull Runnable action) {
|
||||
String currentName = current.getName();
|
||||
if (currentName == null || !currentName.equals(target.worldName())) {
|
||||
ctx.sendMessage(Message.raw("That NPC is in world '" + target.worldName() +
|
||||
"'. Cross-world is not supported yet.").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
action.run();
|
||||
}
|
||||
|
||||
// ---- sub: tag ------------------------------------------------------------
|
||||
|
||||
public static final class TagSub extends AbstractPlayerCommand {
|
||||
private final TagModeStore tagMode;
|
||||
private final RequiredArg<String> handlerArg;
|
||||
private final OptionalArg<List<String>> kvArgs;
|
||||
|
||||
public TagSub(@Nonnull TagModeStore tagMode) {
|
||||
super("tag", "Arm tag mode: next right-click on any entity tags it as a CoreNpc");
|
||||
this.tagMode = tagMode;
|
||||
this.handlerArg = withRequiredArg("handlerId", "Handler id to route clicks to", ArgTypes.STRING);
|
||||
this.kvArgs = withListOptionalArg("args", "Optional key=value pairs", ArgTypes.STRING);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
String handlerId = ctx.get(handlerArg);
|
||||
java.util.Map<String, String> args = new LinkedHashMap<>();
|
||||
if (ctx.provided(kvArgs)) {
|
||||
for (String pair : ctx.get(kvArgs)) {
|
||||
int eq = pair.indexOf('=');
|
||||
if (eq <= 0) {
|
||||
ctx.sendMessage(Message.raw("Skipping malformed arg '" + pair + "', expected key=value")
|
||||
.color(Color.YELLOW));
|
||||
continue;
|
||||
}
|
||||
args.put(pair.substring(0, eq), pair.substring(eq + 1));
|
||||
}
|
||||
}
|
||||
tagMode.set(sender.getUuid(), new TagModeStore.PendingTag(handlerId, args));
|
||||
ctx.sendMessage(Message.raw("Tag mode armed (handler=" + handlerId +
|
||||
"). Right-click an entity to tag it. /npc cancel to abort.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: tagid (by entity network id, no right-click needed) ------------
|
||||
|
||||
public static final class TagIdSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<String> handlerArg;
|
||||
private final RequiredArg<Integer> entityIdArg;
|
||||
private final OptionalArg<List<String>> kvArgs;
|
||||
|
||||
public TagIdSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("tagid", "Tag an entity by its network id (use the F-key entity tool to find it)");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.handlerArg = withRequiredArg("handlerId", "Handler id to route clicks to", ArgTypes.STRING);
|
||||
this.entityIdArg = withRequiredArg("entityId", "Entity network id", ArgTypes.INTEGER);
|
||||
this.kvArgs = withListOptionalArg("args", "Optional key=value pairs", ArgTypes.STRING);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
int networkId = ctx.get(entityIdArg);
|
||||
Ref<EntityStore> targetRef = world.getEntityStore().getRefFromNetworkId(networkId);
|
||||
if (targetRef == null || !targetRef.isValid()) {
|
||||
ctx.sendMessage(Message.raw("No entity with network id " + networkId + " in this world.")
|
||||
.color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
|
||||
java.util.Map<String, String> args = new LinkedHashMap<>();
|
||||
if (ctx.provided(kvArgs)) {
|
||||
for (String pair : ctx.get(kvArgs)) {
|
||||
int eq = pair.indexOf('=');
|
||||
if (eq <= 0) {
|
||||
ctx.sendMessage(Message.raw("Skipping malformed arg '" + pair + "', expected key=value")
|
||||
.color(Color.YELLOW));
|
||||
continue;
|
||||
}
|
||||
args.put(pair.substring(0, eq), pair.substring(eq + 1));
|
||||
}
|
||||
}
|
||||
|
||||
String handlerId = ctx.get(handlerArg);
|
||||
try {
|
||||
NpcHandle handle = npcs.tag(targetRef, null, handlerId, args);
|
||||
selection.set(sender.getUuid(), handle.id());
|
||||
ctx.sendMessage(Message.raw("Tagged entity " + networkId + " as " + handle.id() +
|
||||
" (handler=" + handlerId + "), selected.").color(Color.GREEN));
|
||||
} catch (RuntimeException e) {
|
||||
ctx.sendMessage(Message.raw("Tag failed: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: gettool (give the Tag Wand) ------------------------------------
|
||||
|
||||
public static final class GetToolSub extends AbstractPlayerCommand {
|
||||
|
||||
public GetToolSub() {
|
||||
super("gettool", "Receive a Tag Wand: right-click an entity to tag it with the runcmd handler");
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
Player player = store.getComponent(ref, Player.getComponentType());
|
||||
if (player == null) {
|
||||
ctx.sendMessage(Message.raw("Could not resolve your player entity.").color(Color.RED));
|
||||
return;
|
||||
}
|
||||
player.getInventory().getHotbar().addItemStack(new ItemStack(CoreNpcItems.TAG_WAND, 1));
|
||||
ctx.sendMessage(Message.raw("Tag Wand added to your hotbar. Right-click an entity to tag " +
|
||||
"with runcmd (then /cnpc arg set cmd <command>).").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: cancel ---------------------------------------------------------
|
||||
|
||||
public static final class CancelSub extends AbstractPlayerCommand {
|
||||
private final TagModeStore tagMode;
|
||||
|
||||
public CancelSub(@Nonnull TagModeStore tagMode) {
|
||||
super("cancel", "Cancel any pending tag mode");
|
||||
this.tagMode = tagMode;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
tagMode.clear(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("Tag mode cancelled.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: untag (marker-only removal) ------------------------------------
|
||||
|
||||
public static final class UntagSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public UntagSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("untag", "Remove the marker from the selected NPC; the entity stays in world");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
String id = h.id();
|
||||
npcs.untag(h);
|
||||
selection.clear(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("Untagged NPC " + id + ". Entity still in world.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: delete (full entity removal) ----------------------------------
|
||||
|
||||
public static final class DeleteSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public DeleteSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("delete", "Delete the selected NPC");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle handle = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (handle == null) return;
|
||||
String id = handle.id();
|
||||
npcs.despawn(handle);
|
||||
selection.clear(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("Deleted NPC " + id + ".").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: select ---------------------------------------------------------
|
||||
|
||||
public static final class SelectSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<String> idArg;
|
||||
|
||||
public SelectSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("select", "Select an NPC by id");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.idArg = withRequiredArg("id", "NPC id", ArgTypes.STRING);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
String id = ctx.get(idArg);
|
||||
NpcHandle handle = npcs.byIdInWorld(id, world);
|
||||
if (handle == null) {
|
||||
ctx.sendMessage(Message.raw("No NPC with id '" + id + "' in this world.").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
selection.set(sender.getUuid(), id);
|
||||
ctx.sendMessage(Message.raw("Selected " + id + " (handler=" + handle.handlerId() +
|
||||
", world=" + handle.worldName() + ")").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: deselect -------------------------------------------------------
|
||||
|
||||
public static final class DeselectSub extends AbstractPlayerCommand {
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public DeselectSub(@Nonnull NpcSelectionStore selection) {
|
||||
super("deselect", "Clear your NPC selection");
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
selection.clear(sender.getUuid());
|
||||
ctx.sendMessage(Message.raw("Selection cleared.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: info -----------------------------------------------------------
|
||||
|
||||
public static final class InfoSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public InfoSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("info", "Dump the selected NPC's fields");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
Vector3d p = h.position();
|
||||
ctx.sendMessage(Message.raw("--- NPC " + h.id() + " ---").color(Color.GRAY));
|
||||
ctx.sendMessage(Message.raw("handler: " + h.handlerId()));
|
||||
ctx.sendMessage(Message.raw("role: " + h.roleName()));
|
||||
ctx.sendMessage(Message.raw("world: " + h.worldName()));
|
||||
ctx.sendMessage(Message.raw("pos: " + fmt(p.x) + " " + fmt(p.y) + " " + fmt(p.z) +
|
||||
" yaw=" + fmt(h.yaw())));
|
||||
ctx.sendMessage(Message.raw("name: '" + h.namePlate() + "' (visible=" + h.namePlateVisible() + ")"));
|
||||
Map<String, String> args = h.args();
|
||||
if (args.isEmpty()) {
|
||||
ctx.sendMessage(Message.raw("args: (none)"));
|
||||
} else {
|
||||
ctx.sendMessage(Message.raw("args:"));
|
||||
for (Map.Entry<String, String> e : args.entrySet()) {
|
||||
ctx.sendMessage(Message.raw(" " + e.getKey() + " = " + e.getValue()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static String fmt(double v) { return String.format("%.2f", v); }
|
||||
}
|
||||
|
||||
// ---- sub: tphere ---------------------------------------------------------
|
||||
|
||||
public static final class TpHereSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public TpHereSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("tphere", "Teleport the selected NPC to your position");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
requireSameWorld(ctx, world, h, () -> {
|
||||
Vector3d pos = sender.getTransform().getPosition();
|
||||
npcs.teleport(h, pos.x, pos.y, pos.z);
|
||||
ctx.sendMessage(Message.raw("NPC " + h.id() + " teleported to you.").color(Color.GREEN));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: tpto -----------------------------------------------------------
|
||||
|
||||
public static final class TpToSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public TpToSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("tpto", "Teleport yourself to the selected NPC");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
requireSameWorld(ctx, world, h, () -> {
|
||||
Vector3d pos = h.position();
|
||||
Transform t = new Transform(pos, sender.getTransform().getRotation());
|
||||
sender.updatePosition(world, t, sender.getHeadRotation());
|
||||
ctx.sendMessage(Message.raw("Teleported to " + h.id() + ".").color(Color.GREEN));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: move (relative) ------------------------------------------------
|
||||
|
||||
public static final class MoveSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<Float> dxArg;
|
||||
private final RequiredArg<Float> dyArg;
|
||||
private final RequiredArg<Float> dzArg;
|
||||
|
||||
public MoveSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("move", "Nudge the selected NPC by (dx dy dz)");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.dxArg = withRequiredArg("dx", "delta x", ArgTypes.FLOAT);
|
||||
this.dyArg = withRequiredArg("dy", "delta y", ArgTypes.FLOAT);
|
||||
this.dzArg = withRequiredArg("dz", "delta z", ArgTypes.FLOAT);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
Vector3d p = h.position();
|
||||
npcs.teleport(h, p.x + ctx.get(dxArg), p.y + ctx.get(dyArg), p.z + ctx.get(dzArg));
|
||||
ctx.sendMessage(Message.raw("NPC " + h.id() + " moved.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: moveto (absolute) ----------------------------------------------
|
||||
|
||||
public static final class MoveToSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<Double> xArg;
|
||||
private final RequiredArg<Double> yArg;
|
||||
private final RequiredArg<Double> zArg;
|
||||
|
||||
public MoveToSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("moveto", "Place the selected NPC at (x y z)");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.xArg = withRequiredArg("x", "x", ArgTypes.DOUBLE);
|
||||
this.yArg = withRequiredArg("y", "y", ArgTypes.DOUBLE);
|
||||
this.zArg = withRequiredArg("z", "z", ArgTypes.DOUBLE);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
npcs.teleport(h, ctx.get(xArg), ctx.get(yArg), ctx.get(zArg));
|
||||
ctx.sendMessage(Message.raw("NPC " + h.id() + " placed.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: rotate ---------------------------------------------------------
|
||||
|
||||
public static final class RotateSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<Float> yawArg;
|
||||
|
||||
public RotateSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("rotate", "Set the selected NPC's yaw (degrees)");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.yawArg = withRequiredArg("yaw", "yaw in degrees", ArgTypes.FLOAT);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
npcs.rotate(h, ctx.get(yawArg));
|
||||
ctx.sendMessage(Message.raw("NPC " + h.id() + " yaw set.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: face -----------------------------------------------------------
|
||||
|
||||
public static final class FaceSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
|
||||
public FaceSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("face", "Rotate the selected NPC to face you");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
requireSameWorld(ctx, world, h, () -> {
|
||||
Vector3d npcPos = h.position();
|
||||
Vector3d playerPos = sender.getTransform().getPosition();
|
||||
double dx = playerPos.x - npcPos.x;
|
||||
double dz = playerPos.z - npcPos.z;
|
||||
float yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
|
||||
npcs.rotate(h, yaw);
|
||||
ctx.sendMessage(Message.raw("NPC " + h.id() + " now faces you.").color(Color.GREEN));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: name -----------------------------------------------------------
|
||||
|
||||
public static final class NameSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<String> textArg;
|
||||
|
||||
public NameSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("name", "Set the selected NPC's name plate text");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.textArg = withRequiredArg("text", "Name plate text (supports inline color codes)",
|
||||
ArgTypes.GREEDY_STRING);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
String text = ctx.get(textArg);
|
||||
npcs.setNamePlate(h, text);
|
||||
ctx.sendMessage(Message.raw("NPC " + h.id() + " name set.").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: handler --------------------------------------------------------
|
||||
|
||||
public static final class HandlerSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final NpcSelectionStore selection;
|
||||
private final RequiredArg<String> handlerArg;
|
||||
|
||||
public HandlerSub(@Nonnull NpcsImpl npcs, @Nonnull NpcSelectionStore selection) {
|
||||
super("handler", "Re-route the selected NPC to a different handler id");
|
||||
this.npcs = npcs;
|
||||
this.selection = selection;
|
||||
this.handlerArg = withRequiredArg("handlerId", "Handler id", ArgTypes.STRING);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
NpcHandle h = requireSelected(npcs, selection, ctx, sender, world);
|
||||
if (h == null) return;
|
||||
String handlerId = ctx.get(handlerArg);
|
||||
npcs.setHandlerId(h, handlerId);
|
||||
ctx.sendMessage(Message.raw("NPC " + h.id() + " handler set to " + handlerId + ".").color(Color.GREEN));
|
||||
}
|
||||
}
|
||||
|
||||
// ---- sub: list -----------------------------------------------------------
|
||||
|
||||
public static final class ListSub extends AbstractPlayerCommand {
|
||||
private final NpcsImpl npcs;
|
||||
private final OptionalArg<World> worldArg;
|
||||
|
||||
public ListSub(@Nonnull NpcsImpl npcs) {
|
||||
super("list", "List NPCs (optionally filtered by world)");
|
||||
this.npcs = npcs;
|
||||
this.worldArg = withOptionalArg("world", "World name", ArgTypes.WORLD);
|
||||
requirePermission(CoreNpcPermissions.ADMIN);
|
||||
}
|
||||
|
||||
@Override protected boolean canGeneratePermission() { return false; }
|
||||
|
||||
@Override
|
||||
protected void execute(@Nonnull CommandContext ctx, @Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref, @Nonnull PlayerRef sender,
|
||||
@Nonnull World world) {
|
||||
Collection<NpcHandle> all = ctx.provided(worldArg)
|
||||
? npcs.inWorld(ctx.get(worldArg).getName())
|
||||
: npcs.inWorld(world.getName());
|
||||
if (all.isEmpty()) {
|
||||
ctx.sendMessage(Message.raw("No NPCs found.").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
ctx.sendMessage(Message.raw("--- NPCs (" + all.size() + ") ---").color(Color.GRAY));
|
||||
for (NpcHandle h : all) {
|
||||
Vector3d p = h.position();
|
||||
ctx.sendMessage(Message.raw(String.format("%s handler=%s world=%s (%.1f %.1f %.1f)",
|
||||
h.id(), h.handlerId(), h.worldName(), p.x, p.y, p.z)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package net.kewwbec.corenpc.command;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* In-memory mapping of admin player UUID -> currently selected NPC id.
|
||||
* Selection is per-session: nothing is persisted across server restarts.
|
||||
*/
|
||||
public final class NpcSelectionStore {
|
||||
|
||||
private final ConcurrentMap<UUID, String> selectionByAdmin = new ConcurrentHashMap<>();
|
||||
|
||||
@Nullable
|
||||
public String get(@Nonnull UUID admin) {
|
||||
return selectionByAdmin.get(admin);
|
||||
}
|
||||
|
||||
public void set(@Nonnull UUID admin, @Nonnull String npcId) {
|
||||
selectionByAdmin.put(admin, npcId);
|
||||
}
|
||||
|
||||
public void clear(@Nonnull UUID admin) {
|
||||
selectionByAdmin.remove(admin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package net.kewwbec.corenpc.command;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
|
||||
/**
|
||||
* In-memory state for admins who have run {@code /npc tag <handler>} and are waiting
|
||||
* to right-click an entity to attach the marker.
|
||||
*/
|
||||
public final class TagModeStore {
|
||||
|
||||
public static final class PendingTag {
|
||||
@Nonnull public final String handlerId;
|
||||
@Nonnull public final Map<String, String> args;
|
||||
|
||||
public PendingTag(@Nonnull String handlerId, @Nonnull Map<String, String> args) {
|
||||
this.handlerId = handlerId;
|
||||
this.args = Collections.unmodifiableMap(new LinkedHashMap<>(args));
|
||||
}
|
||||
}
|
||||
|
||||
private final ConcurrentMap<UUID, PendingTag> byAdmin = new ConcurrentHashMap<>();
|
||||
|
||||
@Nullable
|
||||
public PendingTag get(@Nonnull UUID admin) {
|
||||
return byAdmin.get(admin);
|
||||
}
|
||||
|
||||
public void set(@Nonnull UUID admin, @Nonnull PendingTag pending) {
|
||||
byAdmin.put(admin, pending);
|
||||
}
|
||||
|
||||
public void clear(@Nonnull UUID admin) {
|
||||
byAdmin.remove(admin);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package net.kewwbec.corenpc.handler;
|
||||
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.command.system.CommandManager;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import net.kewwbec.corenpc.api.NpcHandle;
|
||||
import net.kewwbec.corenpc.api.NpcInteractionHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Built-in handler: runs a slash command as the clicking player. The command string
|
||||
* is read from the NPC's {@code cmd} arg. Leading slashes are stripped.
|
||||
*
|
||||
* <p>Usage from an admin:
|
||||
* <pre>
|
||||
* /npc tag runcmd cmd=help
|
||||
* right-click entity
|
||||
* (player clicks NPC → command "help" runs as that player)
|
||||
*
|
||||
* # For commands with spaces, set after tagging:
|
||||
* /npc tag runcmd
|
||||
* right-click entity
|
||||
* /npc arg set cmd queue bridge_duels
|
||||
* </pre>
|
||||
*/
|
||||
public final class RunCommandHandler implements NpcInteractionHandler {
|
||||
|
||||
public static final String HANDLER_ID = "runcmd";
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
@Override
|
||||
public void onInteract(@Nonnull PlayerRef player,
|
||||
@Nonnull NpcHandle npc,
|
||||
@Nonnull Map<String, String> args) {
|
||||
String cmd = args.get("cmd");
|
||||
if (cmd == null || cmd.isBlank()) {
|
||||
player.sendMessage(Message.raw("This NPC has no command configured.").color(Color.YELLOW));
|
||||
LOGGER.at(Level.WARNING).log("runcmd NPC %s is missing the 'cmd' arg", npc.id());
|
||||
return;
|
||||
}
|
||||
if (cmd.startsWith("/")) cmd = cmd.substring(1);
|
||||
CommandManager.get().handleCommand(player, cmd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.kewwbec.corenpc.impl;
|
||||
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.protocol.InteractionState;
|
||||
import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.server.core.entity.InteractionContext;
|
||||
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.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.corenpc.CoreNpcPlugin;
|
||||
import net.kewwbec.corenpc.api.NpcHandle;
|
||||
import net.kewwbec.corenpc.api.NpcInteractionHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.Collections;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Fires when a player presses F (or otherwise triggers {@code InteractionType.Use})
|
||||
* on an entity carrying our {@link NpcMarkerComponent}. The wiring is the entity's
|
||||
* {@code Interactions} component, set during {@link NpcsImpl#tag} to route Use to
|
||||
* this interaction's asset id.
|
||||
*
|
||||
* <p>Mirrors the pattern used by HylogramsFramework's HologramUseInteraction — no
|
||||
* role/InteractionInstruction surgery needed; works on any entity we tag.</p>
|
||||
*/
|
||||
public final class CoreNpcUseInteraction extends SimpleInstantInteraction {
|
||||
|
||||
/** Asset id stored on tagged entities' {@code Interactions} component. */
|
||||
public static final String ASSET_ID = "*CoreNpcUse";
|
||||
|
||||
/** Codec-registration name (without the {@code *} prefix the runtime uses for synthetic ids). */
|
||||
public static final String CODEC_ID = "CoreNpcUse";
|
||||
|
||||
@Nonnull
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
public static final BuilderCodec<CoreNpcUseInteraction> CODEC = ((BuilderCodec.Builder) BuilderCodec
|
||||
.builder(CoreNpcUseInteraction.class, CoreNpcUseInteraction::new, SimpleInstantInteraction.CODEC)
|
||||
.documentation("Routes a Use action on a CoreNpc-tagged entity to its registered handler."))
|
||||
.build();
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
public CoreNpcUseInteraction(@Nonnull String id) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
protected CoreNpcUseInteraction() {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void firstRun(@Nonnull InteractionType type,
|
||||
@Nonnull InteractionContext context,
|
||||
@Nonnull CooldownHandler cooldown) {
|
||||
CommandBuffer<EntityStore> cb = context.getCommandBuffer();
|
||||
if (cb == null) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
Ref<EntityStore> sourceRef = context.getEntity();
|
||||
if (sourceRef == null || !sourceRef.isValid()) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
return;
|
||||
}
|
||||
PlayerRef player = cb.getComponent(sourceRef, PlayerRef.getComponentType());
|
||||
if (player == null) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
Ref<EntityStore> targetRef = context.getTargetEntity();
|
||||
if (targetRef == null || !targetRef.isValid()) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
NpcMarkerComponent marker = cb.getComponent(targetRef, NpcMarkerComponent.getComponentType());
|
||||
if (marker == null) {
|
||||
context.getState().state = InteractionState.Failed;
|
||||
return;
|
||||
}
|
||||
|
||||
String handlerId = marker.getHandlerId();
|
||||
if (handlerId == null || handlerId.isBlank()) return;
|
||||
|
||||
NpcsImpl npcs = CoreNpcPlugin.getInstance().getNpcsImpl();
|
||||
NpcInteractionHandler handler = npcs.getHandler(handlerId);
|
||||
if (handler == null) {
|
||||
LOGGER.at(Level.WARNING).log(
|
||||
"NPC %s routed to unknown handler '%s' — ignoring use from %s",
|
||||
marker.getId(), handlerId, player.getUsername());
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
World world = cb.getExternalData() != null ? cb.getExternalData().getWorld() : null;
|
||||
NpcHandle handle = world != null ? npcs.byIdInWorld(marker.getId(), world) : null;
|
||||
handler.onInteract(player, handle, Collections.unmodifiableMap(marker.getArgs()));
|
||||
} catch (Throwable t) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.SEVERE).withCause(t))
|
||||
.log("NPC handler '%s' threw on use from %s", handlerId, player.getUsername());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package net.kewwbec.corenpc.impl;
|
||||
|
||||
import com.hypixel.hytale.component.ArchetypeChunk;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.component.query.Query;
|
||||
import com.hypixel.hytale.component.system.tick.EntityTickingSystem;
|
||||
import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.Interactable;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.Interactions;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
|
||||
/**
|
||||
* Re-applies our {@code Use} interaction mapping to every entity carrying a
|
||||
* {@link NpcMarkerComponent}. NPC entities derive their {@code Interactions}
|
||||
* component from their role on load, which wipes the mapping we set at tag time —
|
||||
* this system idempotently restores it each tick so F-key dispatch survives
|
||||
* server restart.
|
||||
*
|
||||
* <p>Pure component mutation when the {@code Interactions} component is present
|
||||
* (cheap, no structural change). When the component is missing entirely, the
|
||||
* structural change is queued via {@link Store#ensureComponent} on the next
|
||||
* world-thread pass — but in practice NPC entities always have one.</p>
|
||||
*/
|
||||
public final class EnsureInteractionsSystem extends EntityTickingSystem<EntityStore> {
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static final Query<EntityStore> QUERY =
|
||||
(Query<EntityStore>) (Query) NpcMarkerComponent.getComponentType();
|
||||
|
||||
public EnsureInteractionsSystem() {
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public Query<EntityStore> getQuery() {
|
||||
return QUERY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void tick(float dt,
|
||||
int index,
|
||||
@Nonnull ArchetypeChunk<EntityStore> chunk,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull CommandBuffer<EntityStore> commandBuffer) {
|
||||
Interactions interactions = chunk.getComponent(index, Interactions.getComponentType());
|
||||
if (interactions == null) {
|
||||
Ref<EntityStore> ref = chunk.getReferenceTo(index);
|
||||
if (ref == null || !ref.isValid()) return;
|
||||
store.ensureComponent(ref, Interactable.getComponentType());
|
||||
store.ensureComponent(ref, Interactions.getComponentType());
|
||||
Interactions installed = store.getComponent(ref, Interactions.getComponentType());
|
||||
if (installed != null) {
|
||||
installed.setInteractionId(InteractionType.Use, CoreNpcUseInteraction.ASSET_ID);
|
||||
}
|
||||
return;
|
||||
}
|
||||
String currentUse = interactions.getInteractionId(InteractionType.Use);
|
||||
if (!CoreNpcUseInteraction.ASSET_ID.equals(currentUse)) {
|
||||
interactions.setInteractionId(InteractionType.Use, CoreNpcUseInteraction.ASSET_ID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package net.kewwbec.corenpc.impl;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.event.events.player.PlayerInteractEvent;
|
||||
import com.hypixel.hytale.server.core.universe.PlayerRef;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.corenpc.CoreNpcPermissions;
|
||||
import net.kewwbec.corenpc.api.NpcHandle;
|
||||
import net.kewwbec.corenpc.api.NpcInteractionHandler;
|
||||
import net.kewwbec.corenpc.command.TagModeStore;
|
||||
import net.kewwbec.corenpc.command.TagModeStore.PendingTag;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.util.Collections;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class InteractionRouter {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final NpcsImpl npcs;
|
||||
private final TagModeStore tagMode;
|
||||
|
||||
public InteractionRouter(@Nonnull NpcsImpl npcs, @Nonnull TagModeStore tagMode) {
|
||||
this.npcs = npcs;
|
||||
this.tagMode = tagMode;
|
||||
}
|
||||
|
||||
public void handle(@Nonnull PlayerInteractEvent event) {
|
||||
if (event.isCancelled()) return;
|
||||
InteractionType type = event.getActionType();
|
||||
if (type != InteractionType.Use && type != InteractionType.Secondary) return;
|
||||
|
||||
Ref<EntityStore> targetRef = event.getTargetRef();
|
||||
if (targetRef == null || !targetRef.isValid()) return;
|
||||
|
||||
Store<EntityStore> store = targetRef.getStore();
|
||||
if (store == null) return;
|
||||
|
||||
PlayerRef player = resolvePlayer(event);
|
||||
if (player == null) return;
|
||||
|
||||
PendingTag pending = tagMode.get(player.getUuid());
|
||||
if (pending != null && player.hasPermission(CoreNpcPermissions.ADMIN)) {
|
||||
handleTag(event, store, targetRef, player, pending);
|
||||
return;
|
||||
}
|
||||
|
||||
NpcMarkerComponent marker = store.getComponent(targetRef, NpcMarkerComponent.getComponentType());
|
||||
if (marker == null) return;
|
||||
|
||||
String handlerId = marker.getHandlerId();
|
||||
if (handlerId == null || handlerId.isBlank()) return;
|
||||
|
||||
NpcInteractionHandler handler = npcs.getHandler(handlerId);
|
||||
if (handler == null) {
|
||||
LOGGER.at(Level.WARNING).log(
|
||||
"NPC %s routed to unknown handler '%s' — ignoring click from %s",
|
||||
marker.getId(), handlerId, player.getUsername());
|
||||
event.setCancelled(true);
|
||||
return;
|
||||
}
|
||||
|
||||
event.setCancelled(true);
|
||||
try {
|
||||
handler.onInteract(player, npcs.resolve(marker.getId()),
|
||||
Collections.unmodifiableMap(marker.getArgs()));
|
||||
} catch (Throwable t) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.SEVERE).withCause(t))
|
||||
.log("NPC handler '%s' threw on click from %s", handlerId, player.getUsername());
|
||||
}
|
||||
}
|
||||
|
||||
private void handleTag(@Nonnull PlayerInteractEvent event,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> targetRef,
|
||||
@Nonnull PlayerRef admin,
|
||||
@Nonnull PendingTag pending) {
|
||||
event.setCancelled(true);
|
||||
|
||||
NpcMarkerComponent existing = store.getComponent(targetRef, NpcMarkerComponent.getComponentType());
|
||||
if (existing != null) {
|
||||
admin.sendMessage(Message.raw("Entity is already a CoreNpc (" + existing.getId() +
|
||||
"). Use /npc untag first if you want to retag.").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
NpcHandle handle = npcs.tag(targetRef, null, pending.handlerId, pending.args);
|
||||
tagMode.clear(admin.getUuid());
|
||||
admin.sendMessage(Message.raw("Tagged entity as " + handle.id() +
|
||||
" (handler=" + pending.handlerId + ").").color(Color.GREEN));
|
||||
} catch (RuntimeException e) {
|
||||
admin.sendMessage(Message.raw("Tag failed: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
}
|
||||
|
||||
private static PlayerRef resolvePlayer(@Nonnull PlayerInteractEvent event) {
|
||||
Ref<EntityStore> ref = event.getPlayerRef();
|
||||
if (ref == null || !ref.isValid()) return null;
|
||||
Store<EntityStore> store = ref.getStore();
|
||||
if (store == null) return null;
|
||||
return store.getComponent(ref, PlayerRef.getComponentType());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package net.kewwbec.corenpc.impl;
|
||||
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.component.Store;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.corenpc.api.NpcHandle;
|
||||
import org.joml.Vector3d;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
final class NpcHandleImpl implements NpcHandle {
|
||||
|
||||
private final String worldName;
|
||||
private final Store<EntityStore> store;
|
||||
private volatile Ref<EntityStore> ref;
|
||||
|
||||
NpcHandleImpl(@Nonnull String worldName,
|
||||
@Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> ref) {
|
||||
this.worldName = worldName;
|
||||
this.store = store;
|
||||
this.ref = ref;
|
||||
}
|
||||
|
||||
void invalidate() {
|
||||
this.ref = null;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
NpcMarkerComponent marker() {
|
||||
Ref<EntityStore> r = this.ref;
|
||||
if (r == null || !r.isValid()) return null;
|
||||
return store.getComponent(r, NpcMarkerComponent.getComponentType());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
TransformComponent transform() {
|
||||
Ref<EntityStore> r = this.ref;
|
||||
if (r == null || !r.isValid()) return null;
|
||||
return store.getComponent(r, TransformComponent.getComponentType());
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
Store<EntityStore> store() {
|
||||
return store;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public String id() {
|
||||
NpcMarkerComponent m = marker();
|
||||
return m != null ? m.getId() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public String handlerId() {
|
||||
NpcMarkerComponent m = marker();
|
||||
return m != null ? m.getHandlerId() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public String namePlate() {
|
||||
NpcMarkerComponent m = marker();
|
||||
return m != null ? m.getNamePlate() : "";
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean namePlateVisible() {
|
||||
NpcMarkerComponent m = marker();
|
||||
return m != null && m.isNamePlateVisible();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public String roleName() {
|
||||
NpcMarkerComponent m = marker();
|
||||
return m != null ? m.getRoleName() : "Static_NPC";
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public Map<String, String> args() {
|
||||
NpcMarkerComponent m = marker();
|
||||
if (m == null) return Collections.emptyMap();
|
||||
return Collections.unmodifiableMap(new LinkedHashMap<>(m.getArgs()));
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public String worldName() {
|
||||
return worldName;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public Vector3d position() {
|
||||
TransformComponent t = transform();
|
||||
return t != null ? new Vector3d(t.getPosition()) : new Vector3d();
|
||||
}
|
||||
|
||||
@Override
|
||||
public float yaw() {
|
||||
TransformComponent t = transform();
|
||||
return t != null ? t.getRotation().yaw() : 0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public Ref<EntityStore> entityRef() {
|
||||
Ref<EntityStore> r = this.ref;
|
||||
return (r != null && r.isValid()) ? r : null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSpawned() {
|
||||
Ref<EntityStore> r = this.ref;
|
||||
return r != null && r.isValid();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
package net.kewwbec.corenpc.impl;
|
||||
|
||||
import com.hypixel.hytale.codec.KeyedCodec;
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.codec.codecs.map.MapCodec;
|
||||
import com.hypixel.hytale.codec.codecs.simple.BooleanCodec;
|
||||
import com.hypixel.hytale.codec.codecs.simple.StringCodec;
|
||||
import com.hypixel.hytale.component.Component;
|
||||
import com.hypixel.hytale.component.ComponentRegistryProxy;
|
||||
import com.hypixel.hytale.component.ComponentType;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Persisted ECS component tagging an entity as a CoreNpc-managed NPC. Carries the
|
||||
* routing data read by InteractionRouter at click time. Position/yaw come from
|
||||
* TransformComponent — not duplicated here to avoid drift.
|
||||
*/
|
||||
public final class NpcMarkerComponent implements Component<EntityStore> {
|
||||
|
||||
private static final StringCodec STRING_CODEC = new StringCodec();
|
||||
private static final BooleanCodec BOOL_CODEC = new BooleanCodec();
|
||||
|
||||
public static final BuilderCodec<NpcMarkerComponent> CODEC = BuilderCodec
|
||||
.builder(NpcMarkerComponent.class, NpcMarkerComponent::new)
|
||||
.addField(new KeyedCodec<>("Id", STRING_CODEC),
|
||||
NpcMarkerComponent::setId, NpcMarkerComponent::getId)
|
||||
.addField(new KeyedCodec<>("HandlerId", STRING_CODEC),
|
||||
NpcMarkerComponent::setHandlerId, NpcMarkerComponent::getHandlerId)
|
||||
.addField(new KeyedCodec<>("NamePlate", STRING_CODEC),
|
||||
NpcMarkerComponent::setNamePlate, NpcMarkerComponent::getNamePlate)
|
||||
.addField(new KeyedCodec<>("NamePlateVisible", BOOL_CODEC),
|
||||
NpcMarkerComponent::setNamePlateVisibleBoxed, NpcMarkerComponent::getNamePlateVisibleBoxed)
|
||||
.addField(new KeyedCodec<>("RoleName", STRING_CODEC),
|
||||
NpcMarkerComponent::setRoleName, NpcMarkerComponent::getRoleName)
|
||||
.addField(new KeyedCodec<>("Args", MapCodec.STRING_HASH_MAP_CODEC),
|
||||
NpcMarkerComponent::setArgs, NpcMarkerComponent::getArgs)
|
||||
.build();
|
||||
|
||||
private static volatile ComponentType<EntityStore, NpcMarkerComponent> TYPE;
|
||||
|
||||
public static void register(@Nonnull ComponentRegistryProxy<EntityStore> registry) {
|
||||
if (TYPE != null) return;
|
||||
TYPE = registry.registerComponent(NpcMarkerComponent.class, "kewwbec.corenpc.NpcMarker", CODEC);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
public static ComponentType<EntityStore, NpcMarkerComponent> getComponentType() {
|
||||
ComponentType<EntityStore, NpcMarkerComponent> t = TYPE;
|
||||
if (t == null) throw new IllegalStateException("NpcMarkerComponent has not been registered yet");
|
||||
return t;
|
||||
}
|
||||
|
||||
private String id = "";
|
||||
private String handlerId = "";
|
||||
private String namePlate = "";
|
||||
private boolean namePlateVisible = true;
|
||||
private String roleName = "Static_NPC";
|
||||
private Map<String, String> args = new LinkedHashMap<>();
|
||||
|
||||
public NpcMarkerComponent() {
|
||||
}
|
||||
|
||||
public String getId() { return id; }
|
||||
public void setId(String id) { this.id = Objects.requireNonNullElse(id, ""); }
|
||||
|
||||
public String getHandlerId() { return handlerId; }
|
||||
public void setHandlerId(String handlerId) { this.handlerId = Objects.requireNonNullElse(handlerId, ""); }
|
||||
|
||||
public String getNamePlate() { return namePlate; }
|
||||
public void setNamePlate(String namePlate) { this.namePlate = Objects.requireNonNullElse(namePlate, ""); }
|
||||
|
||||
public boolean isNamePlateVisible() { return namePlateVisible; }
|
||||
public void setNamePlateVisible(boolean v) { this.namePlateVisible = v; }
|
||||
Boolean getNamePlateVisibleBoxed() { return namePlateVisible; }
|
||||
void setNamePlateVisibleBoxed(Boolean v) { this.namePlateVisible = (v != null) ? v : true; }
|
||||
|
||||
public String getRoleName() { return roleName; }
|
||||
public void setRoleName(String roleName) { this.roleName = Objects.requireNonNullElse(roleName, "Static_NPC"); }
|
||||
|
||||
public Map<String, String> getArgs() { return args; }
|
||||
public void setArgs(Map<String, String> args) {
|
||||
this.args = (args == null) ? new LinkedHashMap<>() : new LinkedHashMap<>(args);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Component<EntityStore> clone() {
|
||||
NpcMarkerComponent c = new NpcMarkerComponent();
|
||||
c.id = this.id;
|
||||
c.handlerId = this.handlerId;
|
||||
c.namePlate = this.namePlate;
|
||||
c.namePlateVisible = this.namePlateVisible;
|
||||
c.roleName = this.roleName;
|
||||
c.args = new LinkedHashMap<>(this.args);
|
||||
return c;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,322 @@
|
||||
package net.kewwbec.corenpc.impl;
|
||||
|
||||
import com.hypixel.hytale.component.AddReason;
|
||||
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.query.Query;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.math.vector.Rotation3f;
|
||||
import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.Interactable;
|
||||
import com.hypixel.hytale.server.core.modules.entity.component.TransformComponent;
|
||||
import com.hypixel.hytale.server.core.modules.interaction.Interactions;
|
||||
import com.hypixel.hytale.server.npc.entities.NPCEntity;
|
||||
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 net.kewwbec.corenpc.api.NpcHandle;
|
||||
import net.kewwbec.corenpc.api.NpcInteractionHandler;
|
||||
import net.kewwbec.corenpc.api.Npcs;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentMap;
|
||||
import java.util.logging.Level;
|
||||
|
||||
public final class NpcsImpl implements Npcs {
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
private final ConcurrentMap<String, NpcInteractionHandler> handlers = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void registerHandler(@Nonnull String handlerId, @Nonnull NpcInteractionHandler handler) {
|
||||
handlers.put(handlerId, handler);
|
||||
LOGGER.at(Level.INFO).log("NPC handler registered: %s", handlerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterHandler(@Nonnull String handlerId) {
|
||||
if (handlers.remove(handlerId) != null) {
|
||||
LOGGER.at(Level.INFO).log("NPC handler unregistered: %s", handlerId);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public NpcInteractionHandler getHandler(@Nonnull String handlerId) {
|
||||
return handlers.get(handlerId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach a marker to an already-spawned entity. Used by tag-mode in InteractionRouter.
|
||||
*
|
||||
* @param targetRef entity to tag (must not already carry a marker)
|
||||
* @param id marker id; auto-generated if null/blank
|
||||
* @param handlerId handler this NPC routes to
|
||||
* @param args per-NPC handler payload (copied)
|
||||
* @return new handle backed by the modified entity
|
||||
*/
|
||||
@Nonnull
|
||||
public NpcHandle tag(@Nonnull Ref<EntityStore> targetRef,
|
||||
@Nullable String id,
|
||||
@Nonnull String handlerId,
|
||||
@Nonnull Map<String, String> args) {
|
||||
Store<EntityStore> store = targetRef.getStore();
|
||||
if (store == null) throw new IllegalStateException("entity has no store");
|
||||
|
||||
if (store.getComponent(targetRef, NpcMarkerComponent.getComponentType()) != null) {
|
||||
throw new IllegalStateException("entity is already tagged as a CoreNpc");
|
||||
}
|
||||
|
||||
String finalId = (id == null || id.isBlank()) ? generateId() : id;
|
||||
if (byIdInStore(store, finalId) != null) {
|
||||
throw new IllegalStateException("NPC id already in use: " + finalId);
|
||||
}
|
||||
|
||||
NpcMarkerComponent marker = new NpcMarkerComponent();
|
||||
marker.setId(finalId);
|
||||
marker.setHandlerId(handlerId);
|
||||
marker.setArgs(new LinkedHashMap<>(args));
|
||||
|
||||
Ref<EntityStore> newRef = attachMarker(store, targetRef, marker);
|
||||
NPCEntity npcEntity = store.getComponent(newRef, NPCEntity.getComponentType());
|
||||
if (npcEntity != null) npcEntity.markNeedsSave();
|
||||
String worldName = resolveWorldName(store);
|
||||
LOGGER.at(Level.INFO).log("Tagged entity as NPC %s (handler=%s, world=%s)",
|
||||
finalId, handlerId, worldName);
|
||||
return new NpcHandleImpl(worldName, store, newRef);
|
||||
}
|
||||
|
||||
/** Strip the marker, leaving the entity intact. */
|
||||
public void untag(@Nonnull NpcHandle handle) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
Ref<EntityStore> ref = impl.entityRef();
|
||||
if (ref == null) return;
|
||||
Store<EntityStore> store = impl.store();
|
||||
Holder<EntityStore> holder = store.removeEntity(ref, RemoveReason.REMOVE);
|
||||
holder.removeComponent(NpcMarkerComponent.getComponentType());
|
||||
store.addEntity(holder, AddReason.SPAWN);
|
||||
impl.invalidate();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private Ref<EntityStore> attachMarker(@Nonnull Store<EntityStore> store,
|
||||
@Nonnull Ref<EntityStore> originalRef,
|
||||
@Nonnull NpcMarkerComponent marker) {
|
||||
Holder<EntityStore> holder = store.removeEntity(originalRef, RemoveReason.REMOVE);
|
||||
holder.addComponent(NpcMarkerComponent.getComponentType(), marker);
|
||||
if (holder.getComponent(Interactable.getComponentType()) == null) {
|
||||
holder.addComponent(Interactable.getComponentType(), Interactable.INSTANCE);
|
||||
}
|
||||
Interactions interactions = holder.getComponent(Interactions.getComponentType());
|
||||
if (interactions == null) {
|
||||
interactions = new Interactions();
|
||||
holder.addComponent(Interactions.getComponentType(), interactions);
|
||||
}
|
||||
interactions.setInteractionId(InteractionType.Use, CoreNpcUseInteraction.ASSET_ID);
|
||||
return store.addEntity(holder, AddReason.SPAWN);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void despawn(@Nonnull NpcHandle handle) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
Ref<EntityStore> ref = impl.entityRef();
|
||||
if (ref == null) return;
|
||||
impl.store().removeEntity(ref, RemoveReason.REMOVE);
|
||||
impl.invalidate();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public NpcHandle byId(@Nonnull String id) {
|
||||
for (World world : universeOrEmpty()) {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Ref<EntityStore> ref = byIdInStore(store, id);
|
||||
if (ref != null) {
|
||||
return new NpcHandleImpl(world.getName(), store, ref);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public NpcHandle byIdInWorld(@Nonnull String id, @Nonnull World world) {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
Ref<EntityStore> ref = byIdInStore(store, id);
|
||||
if (ref == null) return null;
|
||||
return new NpcHandleImpl(world.getName(), store, ref);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public Collection<NpcHandle> inWorld(@Nonnull String worldName) {
|
||||
World world = findWorld(worldName);
|
||||
if (world == null) return Collections.emptyList();
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
List<NpcHandle> out = new ArrayList<>();
|
||||
scanMarkers(store, (ref, marker) -> out.add(new NpcHandleImpl(worldName, store, ref)));
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nonnull
|
||||
public Collection<NpcHandle> all() {
|
||||
List<NpcHandle> out = new ArrayList<>();
|
||||
for (World world : universeOrEmpty()) {
|
||||
Store<EntityStore> store = world.getEntityStore().getStore();
|
||||
String wn = world.getName();
|
||||
scanMarkers(store, (ref, marker) -> out.add(new NpcHandleImpl(wn, store, ref)));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// ---- mutation helpers consumed by the /npc command -----------------------
|
||||
|
||||
@Nullable
|
||||
NpcHandleImpl resolve(@Nonnull String id) {
|
||||
NpcHandle h = byId(id);
|
||||
return (h instanceof NpcHandleImpl impl) ? impl : null;
|
||||
}
|
||||
|
||||
public void teleport(@Nonnull NpcHandle handle, double x, double y, double z) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
TransformComponent t = impl.transform();
|
||||
if (t == null) return;
|
||||
t.teleportPosition(new org.joml.Vector3d(x, y, z));
|
||||
markNeedsSave(impl);
|
||||
}
|
||||
|
||||
public void rotate(@Nonnull NpcHandle handle, float yaw) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
TransformComponent t = impl.transform();
|
||||
if (t == null) return;
|
||||
Rotation3f current = t.getRotation();
|
||||
t.teleportRotation(new Rotation3f(current.pitch(), yaw, current.roll()));
|
||||
markNeedsSave(impl);
|
||||
}
|
||||
|
||||
public void setNamePlate(@Nonnull NpcHandle handle, @Nonnull String text) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
NpcMarkerComponent m = impl.marker();
|
||||
if (m == null) return;
|
||||
m.setNamePlate(text);
|
||||
markNeedsSave(impl);
|
||||
}
|
||||
|
||||
public void setNamePlateVisible(@Nonnull NpcHandle handle, boolean visible) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
NpcMarkerComponent m = impl.marker();
|
||||
if (m == null) return;
|
||||
m.setNamePlateVisible(visible);
|
||||
markNeedsSave(impl);
|
||||
}
|
||||
|
||||
public void setHandlerId(@Nonnull NpcHandle handle, @Nonnull String handlerId) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
NpcMarkerComponent m = impl.marker();
|
||||
if (m == null) return;
|
||||
m.setHandlerId(handlerId);
|
||||
markNeedsSave(impl);
|
||||
}
|
||||
|
||||
public void setArg(@Nonnull NpcHandle handle, @Nonnull String key, @Nonnull String value) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
NpcMarkerComponent m = impl.marker();
|
||||
if (m == null) return;
|
||||
m.getArgs().put(key, value);
|
||||
markNeedsSave(impl);
|
||||
}
|
||||
|
||||
public void removeArg(@Nonnull NpcHandle handle, @Nonnull String key) {
|
||||
NpcHandleImpl impl = expect(handle);
|
||||
NpcMarkerComponent m = impl.marker();
|
||||
if (m == null) return;
|
||||
m.getArgs().remove(key);
|
||||
markNeedsSave(impl);
|
||||
}
|
||||
|
||||
private static void markNeedsSave(@Nonnull NpcHandleImpl handle) {
|
||||
Ref<EntityStore> ref = handle.entityRef();
|
||||
if (ref == null || !ref.isValid()) return;
|
||||
NPCEntity npcEntity = handle.store().getComponent(ref, NPCEntity.getComponentType());
|
||||
if (npcEntity != null) npcEntity.markNeedsSave();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static NpcHandleImpl expect(@Nonnull NpcHandle handle) {
|
||||
if (handle instanceof NpcHandleImpl impl) return impl;
|
||||
throw new IllegalArgumentException("foreign NpcHandle implementation");
|
||||
}
|
||||
|
||||
// ---- scanning ------------------------------------------------------------
|
||||
|
||||
private interface MarkerVisitor {
|
||||
void visit(@Nonnull Ref<EntityStore> ref, @Nonnull NpcMarkerComponent marker);
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
private static final Query<EntityStore> MARKER_QUERY =
|
||||
(Query<EntityStore>) (Query) NpcMarkerComponent.getComponentType();
|
||||
|
||||
private static void scanMarkers(@Nonnull Store<EntityStore> store, @Nonnull MarkerVisitor visitor) {
|
||||
store.forEachChunk(MARKER_QUERY, (chunk, cb) -> {
|
||||
int n = chunk.size();
|
||||
for (int i = 0; i < n; i++) {
|
||||
NpcMarkerComponent m = chunk.getComponent(i, NpcMarkerComponent.getComponentType());
|
||||
if (m != null) visitor.visit(chunk.getReferenceTo(i), m);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private static Ref<EntityStore> byIdInStore(@Nonnull Store<EntityStore> store, @Nonnull String id) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Ref<EntityStore>[] match = (Ref<EntityStore>[]) new Ref[1];
|
||||
scanMarkers(store, (ref, marker) -> {
|
||||
if (id.equals(marker.getId()) && match[0] == null) match[0] = ref;
|
||||
});
|
||||
return match[0];
|
||||
}
|
||||
|
||||
// ---- world resolution ----------------------------------------------------
|
||||
|
||||
@Nullable
|
||||
private static World findWorld(@Nonnull String worldName) {
|
||||
Universe universe = Universe.get();
|
||||
if (universe == null) return null;
|
||||
return universe.getWorld(worldName);
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static Collection<World> universeOrEmpty() {
|
||||
Universe universe = Universe.get();
|
||||
return (universe == null) ? Collections.emptyList() : universe.getWorlds().values();
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String resolveWorldName(@Nonnull Store<EntityStore> store) {
|
||||
for (World w : universeOrEmpty()) {
|
||||
if (w.getEntityStore().getStore() == store) {
|
||||
String n = w.getName();
|
||||
return (n != null) ? n : "";
|
||||
}
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
@Nonnull
|
||||
private static String generateId() {
|
||||
return "npc_" + UUID.randomUUID().toString().substring(0, 8);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
package net.kewwbec.corenpc.impl;
|
||||
|
||||
import com.hypixel.hytale.codec.builder.BuilderCodec;
|
||||
import com.hypixel.hytale.component.CommandBuffer;
|
||||
import com.hypixel.hytale.component.Ref;
|
||||
import com.hypixel.hytale.logger.HytaleLogger;
|
||||
import com.hypixel.hytale.protocol.InteractionType;
|
||||
import com.hypixel.hytale.server.core.Message;
|
||||
import com.hypixel.hytale.server.core.entity.InteractionContext;
|
||||
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.World;
|
||||
import com.hypixel.hytale.server.core.universe.world.storage.EntityStore;
|
||||
import net.kewwbec.corenpc.CoreNpcPermissions;
|
||||
import net.kewwbec.corenpc.CoreNpcPlugin;
|
||||
import net.kewwbec.corenpc.api.NpcHandle;
|
||||
import net.kewwbec.corenpc.command.TagModeStore;
|
||||
import net.kewwbec.corenpc.command.TagModeStore.PendingTag;
|
||||
import net.kewwbec.corenpc.handler.RunCommandHandler;
|
||||
|
||||
import javax.annotation.Nonnull;
|
||||
import java.awt.Color;
|
||||
import java.util.Collections;
|
||||
import java.util.UUID;
|
||||
import java.util.logging.Level;
|
||||
|
||||
/**
|
||||
* Custom item interaction triggered by the Tag Wand's right-click. Registered in
|
||||
* {@link CoreNpcPlugin#setup()} as {@code CoreNpc_Tag_Interaction}.
|
||||
*
|
||||
* <p>{@code firstRun} runs inside the world's tick — structural ECS changes
|
||||
* (removeEntity/addEntity) are forbidden there. We capture the inputs and defer
|
||||
* the actual tag operation via {@code world.execute(...)}, which runs once the
|
||||
* current tick releases its write lock.</p>
|
||||
*/
|
||||
public final class TagNpcInteraction extends SimpleInstantInteraction {
|
||||
|
||||
public static final String INTERACTION_ID = "CoreNpc_Tag_Interaction";
|
||||
|
||||
public static final BuilderCodec<TagNpcInteraction> CODEC = BuilderCodec.builder(
|
||||
TagNpcInteraction.class, TagNpcInteraction::new, SimpleInstantInteraction.CODEC
|
||||
).build();
|
||||
|
||||
private static final HytaleLogger LOGGER = HytaleLogger.forEnclosingClass();
|
||||
|
||||
public TagNpcInteraction() {
|
||||
super();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void firstRun(@Nonnull InteractionType type,
|
||||
@Nonnull InteractionContext ctx,
|
||||
@Nonnull CooldownHandler cooldown) {
|
||||
Ref<EntityStore> targetRef = ctx.getTargetEntity();
|
||||
if (targetRef == null || !targetRef.isValid()) return;
|
||||
|
||||
CommandBuffer<EntityStore> cb = ctx.getCommandBuffer();
|
||||
if (cb == null) return;
|
||||
|
||||
Ref<EntityStore> playerRef = ctx.getEntity();
|
||||
if (playerRef == null || !playerRef.isValid()) return;
|
||||
PlayerRef player = cb.getComponent(playerRef, PlayerRef.getComponentType());
|
||||
if (player == null) return;
|
||||
if (!player.hasPermission(CoreNpcPermissions.ADMIN)) return;
|
||||
|
||||
NpcMarkerComponent existing = cb.getComponent(targetRef, NpcMarkerComponent.getComponentType());
|
||||
if (existing != null) {
|
||||
player.sendMessage(Message.raw("Already tagged as " + existing.getId() +
|
||||
". /cnpc untag first if you want to retag.").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
|
||||
World world = cb.getExternalData().getWorld();
|
||||
if (world == null) return;
|
||||
|
||||
CoreNpcPlugin plugin = CoreNpcPlugin.getInstance();
|
||||
NpcsImpl npcs = plugin.getNpcsImpl();
|
||||
TagModeStore tagMode = plugin.getTagModeStore();
|
||||
|
||||
UUID playerUuid = player.getUuid();
|
||||
PendingTag pending = tagMode.get(playerUuid);
|
||||
final PendingTag finalPending = (pending != null)
|
||||
? pending
|
||||
: new PendingTag(RunCommandHandler.HANDLER_ID, Collections.emptyMap());
|
||||
|
||||
final Ref<EntityStore> finalTarget = targetRef;
|
||||
final PlayerRef finalPlayer = player;
|
||||
|
||||
world.execute(() -> {
|
||||
if (!finalTarget.isValid()) {
|
||||
finalPlayer.sendMessage(Message.raw("Target entity is no longer valid.").color(Color.YELLOW));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
NpcHandle handle = npcs.tag(finalTarget, null, finalPending.handlerId, finalPending.args);
|
||||
tagMode.clear(playerUuid);
|
||||
finalPlayer.sendMessage(Message.raw("Tagged entity as " + handle.id() +
|
||||
" (handler=" + finalPending.handlerId + ").").color(Color.GREEN));
|
||||
} catch (RuntimeException e) {
|
||||
((HytaleLogger.Api) LOGGER.at(Level.WARNING).withCause(e))
|
||||
.log("Tag failed for %s", finalPlayer.getUsername());
|
||||
finalPlayer.sendMessage(Message.raw("Tag failed: " + e.getMessage()).color(Color.RED));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"$Comment": "Held admin tool. Right-click on an entity runs our custom CoreNpc_Tag_Interaction, which attaches a marker via NpcsImpl.tag. /cnpc gettool grants one.",
|
||||
"Id": "CoreNpc_Tag_Wand",
|
||||
"TranslationProperties": {
|
||||
"Name": "CoreNpc Tag Wand",
|
||||
"Description": "Right-click an entity to tag it as a CoreNpc"
|
||||
},
|
||||
"Icon": "Icons/Items/EditorTools/Entity.png",
|
||||
"Model": "Items/Tools/Brush/Brush.blockymodel",
|
||||
"Texture": "Items/Tools/Brush/Brush_Texture.png",
|
||||
"Quality": "Tool",
|
||||
"MaxStack": 1,
|
||||
"PlayerAnimationsId": "Item",
|
||||
"Categories": [
|
||||
"Tool.BuilderTool"
|
||||
],
|
||||
"SubCategory": "BasicTools",
|
||||
"InteractionConfig": {
|
||||
"DebugOutlines": true,
|
||||
"UseDistance": {
|
||||
"Adventure": 128,
|
||||
"Creative": 128
|
||||
},
|
||||
"AllEntities": true
|
||||
},
|
||||
"Interactions": {
|
||||
"Secondary": {
|
||||
"Interactions": [
|
||||
{ "Type": "CoreNpc_Tag_Interaction" }
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"Group": "kewwbec",
|
||||
"Name": "CoreNpc",
|
||||
"Version": "0.1.0",
|
||||
"Description": "Reusable NPC service: tag any builder-tools-spawned entity as an interactive NPC with handler-routed clicks",
|
||||
"IncludesAssetPack": true,
|
||||
"Authors": [
|
||||
{"Name": "kewwbec"}
|
||||
],
|
||||
"ServerVersion": "*",
|
||||
"Dependencies": {
|
||||
"kewwbec:NetworkCore": "*"
|
||||
},
|
||||
"OptionalDependencies": {},
|
||||
"DisabledByDefault": false,
|
||||
"Main": "net.kewwbec.corenpc.CoreNpcPlugin"
|
||||
}
|
||||
Reference in New Issue
Block a user