CommandMap.java 11.5 KB
Newer Older
Jaida Wu's avatar
Jaida Wu committed
1
package emu.grasscutter.command;
KingRainbow44's avatar
KingRainbow44 committed
2
3

import emu.grasscutter.Grasscutter;
Melledy's avatar
Melledy committed
4
import emu.grasscutter.game.player.Player;
KingRainbow44's avatar
KingRainbow44 committed
5
6
7
8
import org.reflections.Reflections;

import java.util.*;

Jaida Wu's avatar
Jaida Wu committed
9
@SuppressWarnings({"UnusedReturnValue", "unused"})
KingRainbow44's avatar
KingRainbow44 committed
10
public final class CommandMap {
11
12
13
    private final Map<String, CommandHandler> commands = new TreeMap<>();
    private final Map<String, CommandHandler> aliases = new TreeMap<>();
    private final Map<String, Command> annotations = new TreeMap<>();
AnimeGitB's avatar
AnimeGitB committed
14
    private final Map<String, Integer> targetPlayerIds = new HashMap<>();
AnimeGitB's avatar
AnimeGitB committed
15
    private static final String consoleId = "console";
4Benj_'s avatar
4Benj_ committed
16

Jaida Wu's avatar
Jaida Wu committed
17
18
19
20
21
22
23
24
    public CommandMap() {
        this(false);
    }

    public CommandMap(boolean scan) {
        if (scan) this.scan();
    }

KingRainbow44's avatar
KingRainbow44 committed
25
26
27
28
29
30
    public static CommandMap getInstance() {
        return Grasscutter.getGameServer().getCommandMap();
    }

    /**
     * Register a command handler.
Jaida Wu's avatar
Jaida Wu committed
31
32
     *
     * @param label   The command label.
KingRainbow44's avatar
KingRainbow44 committed
33
34
35
36
     * @param command The command handler.
     * @return Instance chaining.
     */
    public CommandMap registerCommand(String label, CommandHandler command) {
37
        Grasscutter.getLogger().debug("Registered command: " + label);
38
        label = label.toLowerCase();
Jaida Wu's avatar
Jaida Wu committed
39

KingRainbow44's avatar
KingRainbow44 committed
40
        // Get command data.
41
        Command annotation = command.getClass().getAnnotation(Command.class);
42
        this.annotations.put(label, annotation);
KingRainbow44's avatar
KingRainbow44 committed
43
        this.commands.put(label, command);
Jaida Wu's avatar
Jaida Wu committed
44

KingRainbow44's avatar
KingRainbow44 committed
45
        // Register aliases.
Jaida Wu's avatar
Jaida Wu committed
46
        if (annotation.aliases().length > 0) {
KingRainbow44's avatar
KingRainbow44 committed
47
            for (String alias : annotation.aliases()) {
48
                this.aliases.put(alias, command);
49
                this.annotations.put(alias, annotation);
KingRainbow44's avatar
KingRainbow44 committed
50
            }
Jaida Wu's avatar
Jaida Wu committed
51
52
        }
        return this;
KingRainbow44's avatar
KingRainbow44 committed
53
54
55
56
    }

    /**
     * Removes a registered command handler.
Jaida Wu's avatar
Jaida Wu committed
57
     *
KingRainbow44's avatar
KingRainbow44 committed
58
59
60
61
     * @param label The command label.
     * @return Instance chaining.
     */
    public CommandMap unregisterCommand(String label) {
62
        Grasscutter.getLogger().debug("Unregistered command: " + label);
63

64
        CommandHandler handler = this.commands.get(label);
Jaida Wu's avatar
Jaida Wu committed
65
66
        if (handler == null) return this;

67
        Command annotation = handler.getClass().getAnnotation(Command.class);
68
        this.annotations.remove(label);
KingRainbow44's avatar
KingRainbow44 committed
69
        this.commands.remove(label);
Jaida Wu's avatar
Jaida Wu committed
70

KingRainbow44's avatar
KingRainbow44 committed
71
        // Unregister aliases.
Jaida Wu's avatar
Jaida Wu committed
72
        if (annotation.aliases().length > 0) {
KingRainbow44's avatar
KingRainbow44 committed
73
            for (String alias : annotation.aliases()) {
74
                this.aliases.remove(alias);
75
                this.annotations.remove(alias);
KingRainbow44's avatar
KingRainbow44 committed
76
77
            }
        }
Jaida Wu's avatar
Jaida Wu committed
78

79
        return this;
KingRainbow44's avatar
KingRainbow44 committed
80
81
    }

82
83
84
    public List<Command> getAnnotationsAsList() {
        return new LinkedList<>(this.annotations.values());
    }
85
86
87
88
89

    public HashMap<String, Command> getAnnotations() {
        return new LinkedHashMap<>(this.annotations);
    }

KingRainbow44's avatar
KingRainbow44 committed
90
91
    /**
     * Returns a list of all registered commands.
Jaida Wu's avatar
Jaida Wu committed
92
     *
KingRainbow44's avatar
KingRainbow44 committed
93
94
     * @return All command handlers as a list.
     */
95
    public List<CommandHandler> getHandlersAsList() {
KingRainbow44's avatar
KingRainbow44 committed
96
97
98
        return new LinkedList<>(this.commands.values());
    }

Jaida Wu's avatar
Jaida Wu committed
99
100
101
    public HashMap<String, CommandHandler> getHandlers() {
        return new LinkedHashMap<>(this.commands);
    }
102

KingRainbow44's avatar
KingRainbow44 committed
103
104
    /**
     * Returns a handler by label/alias.
Jaida Wu's avatar
Jaida Wu committed
105
     *
KingRainbow44's avatar
KingRainbow44 committed
106
107
108
109
     * @param label The command label.
     * @return The command handler.
     */
    public CommandHandler getHandler(String label) {
Luke H-W's avatar
Luke H-W committed
110
111
112
113
114
115
        CommandHandler handler = this.commands.get(label);
        if (handler == null) {
            // Try getting by alias
            handler = this.aliases.get(label);
        }
        return handler;
KingRainbow44's avatar
KingRainbow44 committed
116
117
    }

118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
    private Player getTargetPlayer(String playerId, Player player, Player targetPlayer, List<String> args) {
        // Top priority: If any @UID argument is present, override targetPlayer with it.
        for (int i = 0; i < args.size(); i++) {
            String arg = args.get(i);
            if (arg.startsWith("@")) {
                arg = args.remove(i).substring(1);
                if (arg.equals("")) {
                    // This is a special case to target nothing, distinct from failing to assign a target.
                    // This is specifically to allow in-game players to run a command without targeting themselves or anyone else.
                    return null;
                }
                try {
                    int uid = Integer.parseInt(arg);
                    targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid, true);
                    if (targetPlayer == null) {
                        CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
                        throw new IllegalArgumentException();
                    }
                    return targetPlayer;
                } catch (NumberFormatException e) {
Luke H-W's avatar
Luke H-W committed
138
                    CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid");
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
                    throw new IllegalArgumentException();
                }
            }
        }

        // Next priority: If we invoked with a target, use that.
        // By default, this only happens when you message another player in-game with a command.
        if (targetPlayer != null) {
            return targetPlayer;
        }

        // Next priority: Use previously-set target. (see /target [[@]UID])
        if (targetPlayerIds.containsKey(playerId)) {
            targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.get(playerId), true);
            // We check every time in case the target is deleted after being targeted
            if (targetPlayer == null) {
                CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
                throw new IllegalArgumentException();
            }
            return targetPlayer;
        }

        // Lowest priority: Target the player invoking the command. In the case of the console, this will return null.
        return player;
    }

    private boolean setPlayerTarget(String playerId, Player player, String targetUid) {
        if (targetUid.equals("")) { // Clears the default targetPlayer.
                targetPlayerIds.remove(playerId);
                CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target");
                return true;
        }
github-actions's avatar
github-actions committed
171

172
173
174
175
176
177
178
179
180
181
182
183
184
185
        // Sets default targetPlayer to the UID provided.
        try {
            int uid = Integer.parseInt(targetUid);
            Player targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid, true);
            if (targetPlayer == null) {
                CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
                return false;
            }

            targetPlayerIds.put(playerId, uid);
            CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", targetUid);
            CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline()? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUid);
            return true;
        } catch (NumberFormatException e) {
Luke H-W's avatar
Luke H-W committed
186
            CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid");
187
188
189
190
            return false;
        }
    }

KingRainbow44's avatar
KingRainbow44 committed
191
192
    /**
     * Invoke a command handler with the given arguments.
Jaida Wu's avatar
Jaida Wu committed
193
194
     *
     * @param player     The player invoking the command or null for the server console.
KingRainbow44's avatar
KingRainbow44 committed
195
196
     * @param rawMessage The messaged used to invoke the command.
     */
AnimeGitB's avatar
AnimeGitB committed
197
    public void invoke(Player player, Player targetPlayer, String rawMessage) {
198
        rawMessage = rawMessage.trim();
BaiSugar's avatar
BaiSugar committed
199
        if (rawMessage.length() == 0) {
200
            CommandHandler.sendTranslatedMessage(player, "commands.generic.not_specified");
BaiSugar's avatar
BaiSugar committed
201
            return;
202
        }
Jaida Wu's avatar
Jaida Wu committed
203

KingRainbow44's avatar
KingRainbow44 committed
204
205
        // Parse message.
        String[] split = rawMessage.split(" ");
206
        List<String> args = new LinkedList<>(Arrays.asList(split));
207
        String label = args.remove(0).toLowerCase();
AnimeGitB's avatar
AnimeGitB committed
208
        String playerId = (player == null) ? consoleId : player.getAccount().getId();
209

210
211
        // Check for special cases - currently only target command.
        if (label.startsWith("@")) { // @[UID]
212
213
            this.setPlayerTarget(playerId, player, label.substring(1));
            return;
214
        } else if (label.equalsIgnoreCase("target")) { // target [[@]UID]
AnimeGitB's avatar
AnimeGitB committed
215
            if (args.size() > 0) {
216
                String targetUidStr = args.get(0);
AnimeGitB's avatar
AnimeGitB committed
217
218
219
                if (targetUidStr.startsWith("@")) {
                    targetUidStr = targetUidStr.substring(1);
                }
220
221
                this.setPlayerTarget(playerId, player, targetUidStr);
            return;
AnimeGitB's avatar
AnimeGitB committed
222
            } else {
223
224
                this.setPlayerTarget(playerId, player, "");
                return;
AnimeGitB's avatar
AnimeGitB committed
225
            }
AnimeGitB's avatar
AnimeGitB committed
226
        }
AnimeGitB's avatar
AnimeGitB committed
227

KingRainbow44's avatar
KingRainbow44 committed
228
        // Get command handler.
Luke H-W's avatar
Luke H-W committed
229
        CommandHandler handler = this.getHandler(label);
230

Luke H-W's avatar
Luke H-W committed
231
        // Check if the handler is null.
Jaida Wu's avatar
Jaida Wu committed
232
        if (handler == null) {
233
            CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
Jaida Wu's avatar
Jaida Wu committed
234
            return;
KingRainbow44's avatar
KingRainbow44 committed
235
        }
Jaida Wu's avatar
Jaida Wu committed
236

237
238
239
        // Get the command's annotation.
        Command annotation = this.annotations.get(label);

240
        // Resolve targetPlayer
github-actions's avatar
github-actions committed
241
        try {
242
243
244
            targetPlayer = getTargetPlayer(playerId, player, targetPlayer, args);
        } catch (IllegalArgumentException e) {
            return;
AnimeGitB's avatar
AnimeGitB committed
245
246
        }

4Benj_'s avatar
4Benj_ committed
247
        // Check for permissions.
248
        if (!Grasscutter.getPermissionHandler().checkPermission(player, targetPlayer, annotation.permission(), this.annotations.get(label).permissionTargeted())) {
4Benj_'s avatar
4Benj_ committed
249
            return;
250
251
252
        }

        // Check if command has unfulfilled constraints on targetPlayer
253
        Command.TargetRequirement targetRequirement = annotation.targetRequirement();
254
255
        if (targetRequirement != Command.TargetRequirement.NONE) {
            if (targetPlayer == null) {
256
                CommandHandler.sendTranslatedMessage(null, "commands.execution.need_target");
257
258
                return;
            }
259

260
261
262
263
            if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) {
                CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online");
                return;
            }
264

265
266
            if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) {
                CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline");
Jaida Wu's avatar
Jaida Wu committed
267
                return;
268
269
            }
        }
Jaida Wu's avatar
Jaida Wu committed
270

271
272
273
274
        // Copy player and handler to final properties.
        final Player targetPlayerF = targetPlayer; // Is there a better way to do this?
        final CommandHandler handlerF = handler; // Is there a better way to do this?

KingRainbow44's avatar
KingRainbow44 committed
275
        // Invoke execute method for handler.
276
277
        Runnable runnable = () -> handlerF.execute(player, targetPlayerF, args);
        if (annotation.threading()) {
278
279
            new Thread(runnable).start();
        } else {
BaiSugar's avatar
BaiSugar committed
280
281
            runnable.run();
        }
KingRainbow44's avatar
KingRainbow44 committed
282
283
284
285
286
287
288
    }

    /**
     * Scans for all classes annotated with {@link Command} and registers them.
     */
    private void scan() {
        Reflections reflector = Grasscutter.reflector;
289
        Set<Class<?>> classes = reflector.getTypesAnnotatedWith(Command.class);
290

KingRainbow44's avatar
KingRainbow44 committed
291
292
        classes.forEach(annotated -> {
            try {
293
                Command cmdData = annotated.getAnnotation(Command.class);
294
                Object object = annotated.getDeclaredConstructor().newInstance();
KingRainbow44's avatar
KingRainbow44 committed
295
296
                if (object instanceof CommandHandler)
                    this.registerCommand(cmdData.label(), (CommandHandler) object);
297
298
299
300
                else Grasscutter.getLogger().error("Class " + annotated.getName() + " is not a CommandHandler!");
            } catch (Exception exception) {
                Grasscutter.getLogger().error("Failed to register command handler for " + annotated.getSimpleName(), exception);
            }
KingRainbow44's avatar
KingRainbow44 committed
301
302
303
        });
    }
}