diff --git a/src/deprecated/java/emu/grasscutter/commands/PlayerCommands.java b/src/deprecated/java/emu/grasscutter/commands/PlayerCommands.java new file mode 100644 index 000000000..2e8be354d --- /dev/null +++ b/src/deprecated/java/emu/grasscutter/commands/PlayerCommands.java @@ -0,0 +1,307 @@ +package emu.grasscutter.commands; + +import java.lang.reflect.Modifier; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; + +import emu.grasscutter.data.GenshinData; +import emu.grasscutter.data.def.ItemData; +import emu.grasscutter.data.def.MonsterData; +import emu.grasscutter.game.GenshinPlayer; +import emu.grasscutter.game.avatar.GenshinAvatar; +import emu.grasscutter.game.entity.EntityAvatar; +import emu.grasscutter.game.entity.EntityItem; +import emu.grasscutter.game.entity.EntityMonster; +import emu.grasscutter.game.entity.GenshinEntity; +import emu.grasscutter.game.inventory.GenshinItem; +import emu.grasscutter.game.inventory.ItemType; +import emu.grasscutter.game.props.ActionReason; +import emu.grasscutter.game.props.FightProperty; +import emu.grasscutter.server.packet.send.PacketEntityFightPropUpdateNotify; +import emu.grasscutter.server.packet.send.PacketItemAddHintNotify; +import emu.grasscutter.utils.Position; + +public class PlayerCommands { + private static HashMap list = new HashMap<>(); + + static { + try { + // Look for classes + for (Class cls : PlayerCommands.class.getDeclaredClasses()) { + // Get non abstract classes + if (!Modifier.isAbstract(cls.getModifiers())) { + Command commandAnnotation = cls.getAnnotation(Command.class); + PlayerCommand command = (PlayerCommand) cls.newInstance(); + + if (commandAnnotation != null) { + command.setLevel(commandAnnotation.gmLevel()); + for (String alias : commandAnnotation.aliases()) { + if (alias.length() == 0) { + continue; + } + + String commandName = "!" + alias; + list.put(commandName, command); + commandName = "/" + alias; + list.put(commandName, command); + } + } + + String commandName = "!" + cls.getSimpleName().toLowerCase(); + list.put(commandName, command); + commandName = "/" + cls.getSimpleName().toLowerCase(); + list.put(commandName, command); + } + + } + } catch (Exception e) { + + } + } + + public static void handle(GenshinPlayer player, String msg) { + String[] split = msg.split(" "); + + // End if invalid + if (split.length == 0) { + return; + } + + // + String first = split[0].toLowerCase(); + PlayerCommand c = PlayerCommands.list.get(first); + + if (c != null) { + // Level check + if (player.getGmLevel() < c.getLevel()) { + return; + } + // Execute + int len = Math.min(first.length() + 1, msg.length()); + c.execute(player, msg.substring(len)); + } + } + + public static abstract class PlayerCommand { + // GM level required to use this command + private int level; + protected int getLevel() { return this.level; } + protected void setLevel(int minLevel) { this.level = minLevel; } + + // Main + public abstract void execute(GenshinPlayer player, String raw); + } + + // ================ Commands ================ + + @Command(aliases = {"g", "item", "additem"}, helpText = "/give [item id] [count] - Gives {count} amount of {item id}") + public static class Give extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + String[] split = raw.split(" "); + int itemId = 0, count = 1; + + try { + itemId = Integer.parseInt(split[0]); + } catch (Exception e) { + itemId = 0; + } + + try { + count = Math.max(Math.min(Integer.parseInt(split[1]), Integer.MAX_VALUE), 1); + } catch (Exception e) { + count = 1; + } + + // Give + ItemData itemData = GenshinData.getItemDataMap().get(itemId); + GenshinItem item; + + if (itemData == null) { + player.dropMessage("Error: Item data not found"); + return; + } + + if (itemData.isEquip()) { + List items = new LinkedList<>(); + for (int i = 0; i < count; i++) { + item = new GenshinItem(itemData); + items.add(item); + } + player.getInventory().addItems(items); + player.sendPacket(new PacketItemAddHintNotify(items, ActionReason.SubfieldDrop)); + } else { + item = new GenshinItem(itemData, count); + player.getInventory().addItem(item); + player.sendPacket(new PacketItemAddHintNotify(item, ActionReason.SubfieldDrop)); + } + } + } + + @Command(aliases = {"d"}, helpText = "/drop [item id] [count] - Drops {count} amount of {item id}") + public static class Drop extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + String[] split = raw.split(" "); + int itemId = 0, count = 1; + + try { + itemId = Integer.parseInt(split[0]); + } catch (Exception e) { + itemId = 0; + } + + try { + count = Math.max(Math.min(Integer.parseInt(split[1]), Integer.MAX_VALUE), 1); + } catch (Exception e) { + count = 1; + } + + // Give + ItemData itemData = GenshinData.getItemDataMap().get(itemId); + + if (itemData == null) { + player.dropMessage("Error: Item data not found"); + return; + } + + if (itemData.isEquip()) { + float range = (5f + (.1f * count)); + for (int i = 0; i < count; i++) { + Position pos = player.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); + EntityItem entity = new EntityItem(player.getWorld(), player, itemData, pos, 1); + player.getWorld().addEntity(entity); + } + } else { + EntityItem entity = new EntityItem(player.getWorld(), player, itemData, player.getPos().clone().addY(3f), count); + player.getWorld().addEntity(entity); + } + } + } + + @Command(helpText = "/spawn [monster id] [count] - Creates {count} amount of {item id}") + public static class Spawn extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + String[] split = raw.split(" "); + int monsterId = 0, count = 1, level = 1; + + try { + monsterId = Integer.parseInt(split[0]); + } catch (Exception e) { + monsterId = 0; + } + + try { + level = Math.max(Math.min(Integer.parseInt(split[1]), 200), 1); + } catch (Exception e) { + level = 1; + } + + try { + count = Math.max(Math.min(Integer.parseInt(split[2]), 1000), 1); + } catch (Exception e) { + count = 1; + } + + // Give + MonsterData monsterData = GenshinData.getMonsterDataMap().get(monsterId); + + if (monsterData == null) { + player.dropMessage("Error: Monster data not found"); + return; + } + + float range = (5f + (.1f * count)); + for (int i = 0; i < count; i++) { + Position pos = player.getPos().clone().addX((float) (Math.random() * range) - (range / 2)).addY(3f).addZ((float) (Math.random() * range) - (range / 2)); + EntityMonster entity = new EntityMonster(player.getWorld(), monsterData, pos, level); + player.getWorld().addEntity(entity); + } + } + } + + @Command(helpText = "/killall") + public static class KillAll extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + List toRemove = new LinkedList<>(); + for (GenshinEntity entity : player.getWorld().getEntities().values()) { + if (entity instanceof EntityMonster) { + toRemove.add(entity); + } + } + toRemove.forEach(e -> player.getWorld().killEntity(e, 0)); + } + } + + @Command(helpText = "/resetconst - Resets all constellations for the currently active character") + public static class ResetConst extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + EntityAvatar entity = player.getTeamManager().getCurrentAvatarEntity(); + + if (entity == null) { + return; + } + + GenshinAvatar avatar = entity.getAvatar(); + + avatar.getTalentIdList().clear(); + avatar.setCoreProudSkillLevel(0); + avatar.recalcStats(); + avatar.save(); + + player.dropMessage("Constellations for " + entity.getAvatar().getAvatarData().getName() + " have been reset. Please relogin to see changes."); + } + } + + @Command(helpText = "/godmode - Prevents you from taking damage") + public static class Godmode extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + player.setGodmode(!player.hasGodmode()); + player.dropMessage("Godmode is now " + (player.hasGodmode() ? "ON" : "OFF")); + } + } + + @Command(helpText = "/sethp [hp]") + public static class Sethp extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + String[] split = raw.split(" "); + int hp = 0; + + try { + hp = Math.max(Integer.parseInt(split[0]), 1); + } catch (Exception e) { + hp = 1; + } + + EntityAvatar entity = player.getTeamManager().getCurrentAvatarEntity(); + + if (entity == null) { + return; + } + + entity.setFightProperty(FightProperty.FIGHT_PROP_CUR_HP, hp); + entity.getWorld().broadcastPacket(new PacketEntityFightPropUpdateNotify(entity, FightProperty.FIGHT_PROP_CUR_HP)); + } + } + + @Command(aliases = {"clearart"}, helpText = "/clearartifacts") + public static class ClearArtifacts extends PlayerCommand { + @Override + public void execute(GenshinPlayer player, String raw) { + List toRemove = new LinkedList<>(); + for (GenshinItem item : player.getInventory().getItems().values()) { + if (item.getItemType() == ItemType.ITEM_RELIQUARY && item.getLevel() == 1 && item.getExp() == 0 && !item.isLocked() && !item.isEquipped()) { + toRemove.add(item); + } + } + + player.getInventory().removeItems(toRemove); + } + } +} diff --git a/src/main/java/emu/grasscutter/commands/ServerCommands.java b/src/deprecated/java/emu/grasscutter/commands/ServerCommands.java similarity index 100% rename from src/main/java/emu/grasscutter/commands/ServerCommands.java rename to src/deprecated/java/emu/grasscutter/commands/ServerCommands.java diff --git a/src/main/java/emu/grasscutter/Grasscutter.java b/src/main/java/emu/grasscutter/Grasscutter.java index f0fa2cc89..2c247f8bb 100644 --- a/src/main/java/emu/grasscutter/Grasscutter.java +++ b/src/main/java/emu/grasscutter/Grasscutter.java @@ -7,7 +7,9 @@ import java.io.FileWriter; import java.io.InputStreamReader; import java.net.InetSocketAddress; +import emu.grasscutter.commands.CommandMap; import emu.grasscutter.utils.Utils; +import org.reflections.Reflections; import org.slf4j.LoggerFactory; import com.google.gson.Gson; @@ -23,10 +25,6 @@ import emu.grasscutter.tools.Tools; import emu.grasscutter.utils.Crypto; public final class Grasscutter { - static { - System.setProperty("logback.configurationFile", "src/main/resources/logback.xml"); - } - private static final Logger log = (Logger) LoggerFactory.getLogger(Grasscutter.class); private static Config config; @@ -37,8 +35,13 @@ public final class Grasscutter { private static DispatchServer dispatchServer; private static GameServer gameServer; + public static final Reflections reflector = new Reflections(); + static { - // Load configuration. + // Declare logback configuration. + System.setProperty("logback.configurationFile", "src/main/resources/logback.xml"); + + // Load server configuration. Grasscutter.loadConfig(); // Check server structure. Utils.startupCheck(); @@ -100,7 +103,7 @@ public final class Grasscutter { String input; try (BufferedReader br = new BufferedReader(new InputStreamReader(System.in))) { while ((input = br.readLine()) != null) { - ServerCommands.handle(input); + CommandMap.getInstance().invoke(null, input); } } catch (Exception e) { Grasscutter.getLogger().error("An error occurred.", e); diff --git a/src/main/java/emu/grasscutter/commands/Command.java b/src/main/java/emu/grasscutter/commands/Command.java index aa88826dd..7147e6b6b 100644 --- a/src/main/java/emu/grasscutter/commands/Command.java +++ b/src/main/java/emu/grasscutter/commands/Command.java @@ -5,9 +5,11 @@ import java.lang.annotation.RetentionPolicy; @Retention(RetentionPolicy.RUNTIME) public @interface Command { + String label() default ""; + String[] aliases() default ""; int gmLevel() default 1; - String helpText() default ""; + String usage() default ""; } diff --git a/src/main/java/emu/grasscutter/commands/CommandHandler.java b/src/main/java/emu/grasscutter/commands/CommandHandler.java new file mode 100644 index 000000000..a1f58a539 --- /dev/null +++ b/src/main/java/emu/grasscutter/commands/CommandHandler.java @@ -0,0 +1,28 @@ +package emu.grasscutter.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.GenshinPlayer; + +import java.util.List; + +public interface CommandHandler { + /* Invoked on player execution. */ + void execute(GenshinPlayer player, List args); + /* Invoked on server execution. */ + void execute(List args); + + /* + * Utilities. + */ + + /** + * Send a message to the target. + * @param player The player to send the message to, or null for the server console. + * @param message The message to send. + */ + static void sendMessage(GenshinPlayer player, String message) { + if(player == null) { + Grasscutter.getLogger().info(message); + } else player.dropMessage(message); + } +} diff --git a/src/main/java/emu/grasscutter/commands/CommandMap.java b/src/main/java/emu/grasscutter/commands/CommandMap.java new file mode 100644 index 000000000..b139e2266 --- /dev/null +++ b/src/main/java/emu/grasscutter/commands/CommandMap.java @@ -0,0 +1,87 @@ +package emu.grasscutter.commands; + +import emu.grasscutter.Grasscutter; +import emu.grasscutter.game.GenshinPlayer; +import org.reflections.Reflections; + +import java.util.*; + +@SuppressWarnings("UnusedReturnValue") +public final class CommandMap { + public static CommandMap getInstance() { + return Grasscutter.getGameServer().getCommandMap(); + } + + private final Map commands = new HashMap<>(); + + /** + * Register a command handler. + * @param label The command label. + * @param command The command handler. + * @return Instance chaining. + */ + public CommandMap registerCommand(String label, CommandHandler command) { + this.commands.put(label, command); return this; + } + + /** + * Removes a registered command handler. + * @param label The command label. + * @return Instance chaining. + */ + public CommandMap unregisterCommand(String label) { + this.commands.remove(label); return this; + } + + /** + * Invoke a command handler with the given arguments. + * @param player The player invoking the command or null for the server console. + * @param rawMessage The messaged used to invoke the command. + */ + public void invoke(GenshinPlayer player, String rawMessage) { + // Remove prefix if present. + if(!Character.isLetter(rawMessage.charAt(0))) + rawMessage = rawMessage.substring(1); + + // Parse message. + String[] split = rawMessage.split(" "); + List args = Arrays.asList(split); + String label = args.remove(0); + + // Get command handler. + CommandHandler handler = this.commands.get(label); + if(handler == null) { + CommandHandler.sendMessage(player, "Unknown command: " + label); return; + } + + // Invoke execute method for handler. + if(player == null) + handler.execute(args); + else handler.execute(player, args); + } + + public CommandMap() { + this(false); + } + + public CommandMap(boolean scan) { + if(scan) this.scan(); + } + + /** + * Scans for all classes annotated with {@link Command} and registers them. + */ + private void scan() { + Reflections reflector = Grasscutter.reflector; + Set classes = reflector.getTypesAnnotatedWith(Command.class); + classes.forEach(annotated -> { + try { + Class cls = annotated.getClass(); + Command cmdData = cls.getAnnotation(Command.class); + Object object = cls.getDeclaredConstructors()[0].newInstance(); + if (object instanceof CommandHandler) + this.registerCommand(cmdData.label(), (CommandHandler) object); + } catch (Exception ignored) { } + }); + } +} diff --git a/src/main/java/emu/grasscutter/game/managers/ChatManager.java b/src/main/java/emu/grasscutter/game/managers/ChatManager.java index d2d1acac4..bd5a8dcc1 100644 --- a/src/main/java/emu/grasscutter/game/managers/ChatManager.java +++ b/src/main/java/emu/grasscutter/game/managers/ChatManager.java @@ -1,6 +1,6 @@ package emu.grasscutter.game.managers; -import emu.grasscutter.Grasscutter; +import emu.grasscutter.commands.CommandMap; import emu.grasscutter.commands.PlayerCommands; import emu.grasscutter.game.GenshinPlayer; import emu.grasscutter.net.packet.GenshinPacket; @@ -26,8 +26,8 @@ public class ChatManager { } // Check if command - if (message.charAt(0) == '!' || message.charAt(0) == '/') { - PlayerCommands.handle(player, message); + if (message.charAt(0) == '!') { + CommandMap.getInstance().invoke(player, message); return; } @@ -68,7 +68,7 @@ public class ChatManager { // Check if command if (message.charAt(0) == '!') { - PlayerCommands.handle(player, message); + CommandMap.getInstance().invoke(player, message); return; } diff --git a/src/main/java/emu/grasscutter/server/game/GameServer.java b/src/main/java/emu/grasscutter/server/game/GameServer.java index 1baf3253d..263c0cc72 100644 --- a/src/main/java/emu/grasscutter/server/game/GameServer.java +++ b/src/main/java/emu/grasscutter/server/game/GameServer.java @@ -11,6 +11,7 @@ import java.util.concurrent.ConcurrentHashMap; import emu.grasscutter.GenshinConstants; import emu.grasscutter.Grasscutter; +import emu.grasscutter.commands.CommandMap; import emu.grasscutter.database.DatabaseHelper; import emu.grasscutter.game.GenshinPlayer; import emu.grasscutter.game.dungeons.DungeonManager; @@ -23,11 +24,10 @@ import emu.grasscutter.net.packet.PacketHandler; import emu.grasscutter.net.proto.SocialDetailOuterClass.SocialDetail; import emu.grasscutter.netty.MihoyoKcpServer; -public class GameServer extends MihoyoKcpServer { +public final class GameServer extends MihoyoKcpServer { private final InetSocketAddress address; private final GameServerPacketHandler packetHandler; - private final Timer gameLoop; - + private final Map players; private final ChatManager chatManager; @@ -36,9 +36,11 @@ public class GameServer extends MihoyoKcpServer { private final ShopManager shopManager; private final MultiplayerManager multiplayerManager; private final DungeonManager dungeonManager; + private final CommandMap commandMap; public GameServer(InetSocketAddress address) { super(address); + this.setServerInitializer(new GameServerInitializer(this)); this.address = address; this.packetHandler = new GameServerPacketHandler(PacketHandler.class); @@ -50,22 +52,22 @@ public class GameServer extends MihoyoKcpServer { this.shopManager = new ShopManager(this); this.multiplayerManager = new MultiplayerManager(this); this.dungeonManager = new DungeonManager(this); + this.commandMap = new CommandMap(true); - // Ticker - this.gameLoop = new Timer(); - this.gameLoop.scheduleAtFixedRate(new TimerTask() { + // Schedule game loop. + Timer gameLoop = new Timer(); + gameLoop.scheduleAtFixedRate(new TimerTask() { @Override public void run() { try { onTick(); } catch (Exception e) { - // TODO Auto-generated catch block - e.printStackTrace(); + Grasscutter.getLogger().error("An error occurred during game update.", e); } } }, new Date(), 1000L); - // Shutdown hook + // Hook into shutdown event. Runtime.getRuntime().addShutdownHook(new Thread(this::onServerShutdown)); } @@ -101,6 +103,10 @@ public class GameServer extends MihoyoKcpServer { return dungeonManager; } + public CommandMap getCommandMap() { + return this.commandMap; + } + public void registerPlayer(GenshinPlayer player) { getPlayers().put(player.getId(), player); }