feat: implemented ScriptService

This commit is contained in:
Andrey Terentev 2025-04-06 13:38:49 +07:00
parent 65a784a1ef
commit 7ebcad5fa7
8 changed files with 308 additions and 4 deletions

View File

@ -19,12 +19,11 @@ import ru.dragonestia.msb3.api.event.NPCClickEvent;
import ru.dragonestia.msb3.api.item.ItemUtil; import ru.dragonestia.msb3.api.item.ItemUtil;
import ru.dragonestia.msb3.api.player.MsbPlayer; import ru.dragonestia.msb3.api.player.MsbPlayer;
import ru.dragonestia.msb3.api.player.PlayerContextManager; import ru.dragonestia.msb3.api.player.PlayerContextManager;
import ru.dragonestia.msb3.api.player.defaults.DebugParamsContext; import ru.dragonestia.msb3.api.player.defaults.*;
import ru.dragonestia.msb3.api.player.defaults.KeyedBossBarContext;
import ru.dragonestia.msb3.api.player.defaults.NavigatorContext;
import ru.dragonestia.msb3.api.player.defaults.TalksContext;
import ru.dragonestia.msb3.api.resource.DialogueResources; import ru.dragonestia.msb3.api.resource.DialogueResources;
import ru.dragonestia.msb3.api.resource.MonologueResources; import ru.dragonestia.msb3.api.resource.MonologueResources;
import ru.dragonestia.msb3.api.script.ScriptRegistry;
import ru.dragonestia.msb3.api.script.defaults.HelloWorldScript;
import ru.dragonestia.msb3.api.skin.SkinStorage; import ru.dragonestia.msb3.api.skin.SkinStorage;
import ru.dragonestia.msb3.api.ui.BlackScreen; import ru.dragonestia.msb3.api.ui.BlackScreen;
import ru.dragonestia.msb3.api.ui.bossbar.KeyedBossBars; import ru.dragonestia.msb3.api.ui.bossbar.KeyedBossBars;
@ -103,6 +102,7 @@ public final class ServerBootstrap {
initDefaultDialogActionsAndConditions(); initDefaultDialogActionsAndConditions();
NPCClickEvent.init(); NPCClickEvent.init();
DebugCommands.init(); DebugCommands.init();
initDefaultScripts();
} }
private void compileResourcePack() { private void compileResourcePack() {
@ -193,6 +193,7 @@ public final class ServerBootstrap {
PlayerContextManager.registerContext(NavigatorContext.class, NavigatorContext::new); PlayerContextManager.registerContext(NavigatorContext.class, NavigatorContext::new);
PlayerContextManager.registerContext(TalksContext.class, TalksContext::new); PlayerContextManager.registerContext(TalksContext.class, TalksContext::new);
PlayerContextManager.registerContext(DebugParamsContext.class, DebugParamsContext::new); PlayerContextManager.registerContext(DebugParamsContext.class, DebugParamsContext::new);
PlayerContextManager.registerContext(PlayerScriptContext.class, PlayerScriptContext::new);
} }
private void initDefaultSkins() { private void initDefaultSkins() {
@ -206,4 +207,8 @@ public final class ServerBootstrap {
DialogRegistry.registerConditionHandler("always", new AlwaysDialogConditionHandler()); DialogRegistry.registerConditionHandler("always", new AlwaysDialogConditionHandler());
DialogRegistry.registerConditionHandler("never", new NeverDialogConditionHandler()); DialogRegistry.registerConditionHandler("never", new NeverDialogConditionHandler());
} }
private void initDefaultScripts() {
ScriptRegistry.register(HelloWorldScript::new);
}
} }

View File

@ -31,6 +31,7 @@ public class DebugCommands {
registerGameModeCommand(); registerGameModeCommand();
DebugRendererCommand.register(); DebugRendererCommand.register();
DebugAICommand.register(); DebugAICommand.register();
ScriptCommand.register();
log.info("Registered debug commands"); log.info("Registered debug commands");
} }

View File

@ -0,0 +1,105 @@
package ru.dragonestia.msb3.api.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.minestom.server.MinecraftServer;
import net.minestom.server.command.CommandSender;
import net.minestom.server.command.builder.Command;
import net.minestom.server.command.builder.CommandContext;
import net.minestom.server.command.builder.arguments.ArgumentString;
import net.minestom.server.command.builder.arguments.ArgumentType;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.script.Script;
import ru.dragonestia.msb3.api.script.ScriptRegistry;
import ru.dragonestia.msb3.api.script.ScriptService;
import java.util.Collection;
public class ScriptCommand extends Command {
private final ArgumentString argScriptId = ArgumentType.String("scriptId");
public ScriptCommand() {
super("script");
setDefaultExecutor(this::defaultExecutor);
addSyntax(this::executeStartScript, ArgumentType.Literal("start"), argScriptId);
addSyntax(this::executeStopScript, ArgumentType.Literal("stop"), argScriptId);
addSyntax(this::executeListActiveScripts, ArgumentType.Literal("active"));
addSyntax(this::executeListRegisteredScripts, ArgumentType.Literal("registered"));
}
public static void register() {
MinecraftServer.getCommandManager().register(new ScriptCommand());
}
private void defaultExecutor(CommandSender sender, CommandContext ctx) {
var player = (Player) sender;
player.sendMessage("Script commands:");
player.sendMessage("/script start <scriptId> - Start script");
player.sendMessage("/script stop <scriptId> - Stop script");
player.sendMessage("/script active - List active scripts");
player.sendMessage("/script registered - List registered scripts");
}
private void executeStartScript(CommandSender sender, CommandContext ctx) {
var player = (Player) sender;
var scriptService = ScriptService.ofPlayer(player);
var scriptId = ctx.get(argScriptId);
var script = ScriptRegistry.findAndCreateScript(scriptId);
if (script.isEmpty()) {
player.sendMessage(Component.text("Script '%s' is not registered".formatted(scriptId), NamedTextColor.RED));
return;
}
player.sendMessage(Component.text("Staring script '%s'...".formatted(scriptId), NamedTextColor.YELLOW));
scriptService.start(script.get());
}
private void executeStopScript(CommandSender sender, CommandContext ctx) {
var player = (Player) sender;
var scriptService = ScriptService.ofPlayer(player);
var scriptId = ctx.get(argScriptId);
var script = scriptService.findScript(scriptId);
if (script.isEmpty()) {
player.sendMessage(Component.text("Active script '%s' not found".formatted(scriptId), NamedTextColor.RED));
return;
}
player.sendMessage(Component.text("Stopping script '%s'...".formatted(scriptId), NamedTextColor.YELLOW));
scriptService.stop(script.get());
}
private void executeListActiveScripts(CommandSender sender, CommandContext ctx) {
var player = (Player) sender;
var scriptService = ScriptService.ofPlayer(player);
var scripts = scriptService.getActiveScripts().stream().map(Script::getId).toList();
player.sendMessage(Component.text("Active scripts(%s): ".formatted(scripts.size()))
.append(listScriptIds(scripts)));
}
private void executeListRegisteredScripts(CommandSender sender, CommandContext ctx) {
var player = (Player) sender;
var scripts = ScriptRegistry.listRegisteredScripts();
player.sendMessage(Component.text("Registered scripts(%s): ".formatted(scripts.size()))
.append(listScriptIds(scripts)));
}
private Component listScriptIds(Collection<String> scriptIds) {
return Component.join(
JoinConfiguration.builder().separator(Component.text(", ")).build(),
scriptIds.stream().map(id -> Component.text(id, NamedTextColor.GOLD)
.hoverEvent(HoverEvent.showText(Component.text("Click to copy")))
.clickEvent(ClickEvent.clickEvent(ClickEvent.Action.COPY_TO_CLIPBOARD, id)))
.toList()
);
}
}

View File

@ -0,0 +1,86 @@
package ru.dragonestia.msb3.api.player.defaults;
import lombok.extern.log4j.Log4j2;
import ru.dragonestia.msb3.api.debug.DebugMessage;
import ru.dragonestia.msb3.api.player.MsbPlayer;
import ru.dragonestia.msb3.api.player.PlayerContext;
import ru.dragonestia.msb3.api.script.Script;
import ru.dragonestia.msb3.api.script.ScriptService;
import ru.dragonestia.msb3.api.util.UncheckedRunnable;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
@Log4j2
public class PlayerScriptContext extends PlayerContext implements ScriptService {
private final Map<String, Script> scripts = new ConcurrentHashMap<>();
public PlayerScriptContext(MsbPlayer player) {
super(player);
}
@Override
public void init() {}
@Override
public void dispose() {
synchronized (this) {
for (var script : scripts.values()) {
UncheckedRunnable.runIgnoreException(() -> script.end(getPlayer()));
}
scripts.clear();
}
}
@Override
public void start(Script script) {
synchronized (this) {
var opt = findScript(script.getId());
if (opt.isPresent()) {
DebugMessage.send(getPlayer(), "Script '%s' already started".formatted(script.getId()));
return;
}
try {
script.start(getPlayer());
DebugMessage.send(getPlayer(), "Script '%s' started!".formatted(script.getId()));
} catch (Exception ex) {
DebugMessage.sendError(getPlayer(), "Script '%s' started with error %s: %s".formatted(script.getId(), ex.getClass().getSimpleName(), ex.getMessage()));
log.error(ex.getMessage(), ex);
}
scripts.put(script.getId(), script);
}
}
@Override
public void stop(Script script) {
synchronized (this) {
if (!scripts.containsKey(script.getId())) {
DebugMessage.send(getPlayer(), "Script '%s' not started".formatted(script.getId()));
return;
}
scripts.remove(script.getId());
try {
script.end(getPlayer());
DebugMessage.send(getPlayer(), "Script '%s' stopped".formatted(script.getId()));
} catch (Exception ex) {
DebugMessage.sendError(getPlayer(), "Script '%s' stopped with error %s: %s".formatted(script.getId(), ex.getClass().getSimpleName(), ex.getMessage()));
log.error(ex.getMessage(), ex);
}
}
}
@Override
public Collection<Script> getActiveScripts() {
return scripts.values();
}
@Override
public Optional<Script> findScript(String id) {
return Optional.ofNullable(scripts.get(id));
}
}

View File

@ -0,0 +1,33 @@
package ru.dragonestia.msb3.api.script;
import lombok.Getter;
import ru.dragonestia.msb3.api.player.MsbPlayer;
@Getter
public abstract class Script {
private final String id;
public Script(String id) {
this.id = id;
}
public abstract void start(MsbPlayer player);
public abstract void end(MsbPlayer player);
@Override
public final int hashCode() {
return id.hashCode();
}
@Override
public final boolean equals(Object obj) {
if (obj == this) return true;
if (obj == null) return false;
if (obj instanceof Script other) {
return id.equals(other.id);
}
return false;
}
}

View File

@ -0,0 +1,30 @@
package ru.dragonestia.msb3.api.script;
import lombok.experimental.UtilityClass;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Supplier;
@UtilityClass
public class ScriptRegistry {
private final Map<String, Supplier<Script>> scripts = new ConcurrentHashMap<>();
public void register(Supplier<Script> scriptSupplier) {
var prev = scripts.put(scriptSupplier.get().getId(), scriptSupplier);
if (prev != null) {
throw new IllegalStateException("Script already registered: " + scriptSupplier.get().getId());
}
}
public Optional<Script> findAndCreateScript(String id) {
return Optional.ofNullable(scripts.get(id)).map(Supplier::get);
}
public Collection<String> listRegisteredScripts() {
return scripts.keySet();
}
}

View File

@ -0,0 +1,23 @@
package ru.dragonestia.msb3.api.script;
import net.minestom.server.entity.Player;
import ru.dragonestia.msb3.api.player.PlayerContext;
import ru.dragonestia.msb3.api.player.defaults.PlayerScriptContext;
import java.util.Collection;
import java.util.Optional;
public interface ScriptService {
void start(Script script);
void stop(Script script);
Collection<Script> getActiveScripts();
Optional<Script> findScript(String id);
static ScriptService ofPlayer(Player player) {
return PlayerContext.of(player, PlayerScriptContext.class);
}
}

View File

@ -0,0 +1,21 @@
package ru.dragonestia.msb3.api.script.defaults;
import ru.dragonestia.msb3.api.player.MsbPlayer;
import ru.dragonestia.msb3.api.script.Script;
public class HelloWorldScript extends Script {
public HelloWorldScript() {
super("hello_world");
}
@Override
public void start(MsbPlayer player) {
player.sendMessage("Hello World!");
}
@Override
public void end(MsbPlayer player) {
player.sendMessage("Goodbye my dear friend!");
}
}