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