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 {
Jaida Wu's avatar
Jaida Wu committed
11
    private final Map<String, CommandHandler> commands = new HashMap<>();
12
    private final Map<String, CommandHandler> aliases = new HashMap<>();
Jaida Wu's avatar
Jaida Wu committed
13
    private final Map<String, Command> annotations = new HashMap<>();
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);
Jaida Wu's avatar
Jaida Wu committed
38

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

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

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

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

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

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

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

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

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

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

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

KingRainbow44's avatar
KingRainbow44 committed
102
103
    /**
     * Returns a handler by label/alias.
Jaida Wu's avatar
Jaida Wu committed
104
     *
KingRainbow44's avatar
KingRainbow44 committed
105
106
107
108
109
110
111
     * @param label The command label.
     * @return The command handler.
     */
    public CommandHandler getHandler(String label) {
        return this.commands.get(label);
    }

112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
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
171
172
173
174
175
176
177
178
179
180
181
182
183
184
    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) {
                    CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error");
                    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;
        }
        
        // 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) {
            CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error");
            return false;
        }
    }

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

KingRainbow44's avatar
KingRainbow44 committed
198
199
        // Parse message.
        String[] split = rawMessage.split(" ");
200
        List<String> args = new LinkedList<>(Arrays.asList(split));
KingRainbow44's avatar
KingRainbow44 committed
201
        String label = args.remove(0);
AnimeGitB's avatar
AnimeGitB committed
202
        String playerId = (player == null) ? consoleId : player.getAccount().getId();
203

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

KingRainbow44's avatar
KingRainbow44 committed
222
223
        // Get command handler.
        CommandHandler handler = this.commands.get(label);
224
225
226
227
228
        if(handler == null)
            // Try to get the handler by alias.
            handler = this.aliases.get(label);

        // Check if the handler is still null.
Jaida Wu's avatar
Jaida Wu committed
229
        if (handler == null) {
230
            CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
Jaida Wu's avatar
Jaida Wu committed
231
            return;
KingRainbow44's avatar
KingRainbow44 committed
232
        }
Jaida Wu's avatar
Jaida Wu committed
233

234
235
236
        // Get the command's annotation.
        Command annotation = this.annotations.get(label);

237
238
239
240
241
        // Resolve targetPlayer
        try{
            targetPlayer = getTargetPlayer(playerId, player, targetPlayer, args);
        } catch (IllegalArgumentException e) {
            return;
AnimeGitB's avatar
AnimeGitB committed
242
243
        }

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

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

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

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

268
269
270
271
        // 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
272
        // Invoke execute method for handler.
273
274
        Runnable runnable = () -> handlerF.execute(player, targetPlayerF, args);
        if (annotation.threading()) {
275
276
            new Thread(runnable).start();
        } else {
BaiSugar's avatar
BaiSugar committed
277
278
            runnable.run();
        }
KingRainbow44's avatar
KingRainbow44 committed
279
280
281
282
283
284
285
    }

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

KingRainbow44's avatar
KingRainbow44 committed
288
289
        classes.forEach(annotated -> {
            try {
290
                Command cmdData = annotated.getAnnotation(Command.class);
291
                Object object = annotated.getDeclaredConstructor().newInstance();
KingRainbow44's avatar
KingRainbow44 committed
292
293
                if (object instanceof CommandHandler)
                    this.registerCommand(cmdData.label(), (CommandHandler) object);
294
295
296
297
                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
298
299
300
        });
    }
}