diff --git a/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java b/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java index 45e86c9..3b084b5 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/boot/ServerBootstrap.java @@ -19,12 +19,11 @@ import ru.dragonestia.msb3.api.event.NPCClickEvent; import ru.dragonestia.msb3.api.item.ItemUtil; import ru.dragonestia.msb3.api.player.MsbPlayer; import ru.dragonestia.msb3.api.player.PlayerContextManager; -import ru.dragonestia.msb3.api.player.defaults.DebugParamsContext; -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.player.defaults.*; import ru.dragonestia.msb3.api.resource.DialogueResources; 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.ui.BlackScreen; import ru.dragonestia.msb3.api.ui.bossbar.KeyedBossBars; @@ -103,6 +102,7 @@ public final class ServerBootstrap { initDefaultDialogActionsAndConditions(); NPCClickEvent.init(); DebugCommands.init(); + initDefaultScripts(); } private void compileResourcePack() { @@ -193,6 +193,7 @@ public final class ServerBootstrap { PlayerContextManager.registerContext(NavigatorContext.class, NavigatorContext::new); PlayerContextManager.registerContext(TalksContext.class, TalksContext::new); PlayerContextManager.registerContext(DebugParamsContext.class, DebugParamsContext::new); + PlayerContextManager.registerContext(PlayerScriptContext.class, PlayerScriptContext::new); } private void initDefaultSkins() { @@ -206,4 +207,8 @@ public final class ServerBootstrap { DialogRegistry.registerConditionHandler("always", new AlwaysDialogConditionHandler()); DialogRegistry.registerConditionHandler("never", new NeverDialogConditionHandler()); } + + private void initDefaultScripts() { + ScriptRegistry.register(HelloWorldScript::new); + } } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java index 0c68f11..80c9852 100644 --- a/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/DebugCommands.java @@ -31,6 +31,7 @@ public class DebugCommands { registerGameModeCommand(); DebugRendererCommand.register(); DebugAICommand.register(); + ScriptCommand.register(); log.info("Registered debug commands"); } diff --git a/api/src/main/java/ru/dragonestia/msb3/api/command/ScriptCommand.java b/api/src/main/java/ru/dragonestia/msb3/api/command/ScriptCommand.java new file mode 100644 index 0000000..ff0c5b4 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/command/ScriptCommand.java @@ -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 - Start script"); + player.sendMessage("/script stop - 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 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() + ); + } +} diff --git a/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/PlayerScriptContext.java b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/PlayerScriptContext.java new file mode 100644 index 0000000..cc608b7 --- /dev/null +++ b/api/src/main/java/ru/dragonestia/msb3/api/player/defaults/PlayerScriptContext.java @@ -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 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