CommandMap.java 13 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;
4
import emu.grasscutter.database.DatabaseHelper;
Melledy's avatar
Melledy committed
5
import emu.grasscutter.game.player.Player;
6
7
8
import it.unimi.dsi.fastutil.objects.Object2IntMap;
import it.unimi.dsi.fastutil.objects.Object2IntOpenHashMap;

KingRainbow44's avatar
KingRainbow44 committed
9
10
import org.reflections.Reflections;

Kawaa's avatar
Kawaa committed
11
import java.net.IDN;
KingRainbow44's avatar
KingRainbow44 committed
12
13
import java.util.*;

Kawaa's avatar
Kawaa committed
14
15
16
import static emu.grasscutter.config.Configuration.ACCOUNT;
import static emu.grasscutter.config.Configuration.SERVER;

Jaida Wu's avatar
Jaida Wu committed
17
@SuppressWarnings({"UnusedReturnValue", "unused"})
KingRainbow44's avatar
KingRainbow44 committed
18
public final class CommandMap {
19
20
21
    private final Map<String, CommandHandler> commands = new TreeMap<>();
    private final Map<String, CommandHandler> aliases = new TreeMap<>();
    private final Map<String, Command> annotations = new TreeMap<>();
22
23
    private final Object2IntMap<String> targetPlayerIds = new Object2IntOpenHashMap<>();
    private static final int INVALID_UID = Integer.MIN_VALUE;
AnimeGitB's avatar
AnimeGitB committed
24
    private static final String consoleId = "console";
4Benj_'s avatar
4Benj_ committed
25

Jaida Wu's avatar
Jaida Wu committed
26
27
28
29
30
31
32
33
    public CommandMap() {
        this(false);
    }

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

KingRainbow44's avatar
KingRainbow44 committed
34
    public static CommandMap getInstance() {
35
        return Grasscutter.getCommandMap();
KingRainbow44's avatar
KingRainbow44 committed
36
37
38
39
    }

    /**
     * Register a command handler.
Jaida Wu's avatar
Jaida Wu committed
40
41
     *
     * @param label   The command label.
KingRainbow44's avatar
KingRainbow44 committed
42
43
44
45
     * @param command The command handler.
     * @return Instance chaining.
     */
    public CommandMap registerCommand(String label, CommandHandler command) {
46
        Grasscutter.getLogger().debug("Registered command: " + label);
47
        label = label.toLowerCase();
Jaida Wu's avatar
Jaida Wu committed
48

KingRainbow44's avatar
KingRainbow44 committed
49
        // Get command data.
50
        Command annotation = command.getClass().getAnnotation(Command.class);
51
        this.annotations.put(label, annotation);
KingRainbow44's avatar
KingRainbow44 committed
52
        this.commands.put(label, command);
Jaida Wu's avatar
Jaida Wu committed
53

KingRainbow44's avatar
KingRainbow44 committed
54
        // Register aliases.
Jaida Wu's avatar
Jaida Wu committed
55
        if (annotation.aliases().length > 0) {
KingRainbow44's avatar
KingRainbow44 committed
56
            for (String alias : annotation.aliases()) {
57
                this.aliases.put(alias, command);
58
                this.annotations.put(alias, annotation);
KingRainbow44's avatar
KingRainbow44 committed
59
            }
Jaida Wu's avatar
Jaida Wu committed
60
61
        }
        return this;
KingRainbow44's avatar
KingRainbow44 committed
62
63
64
65
    }

    /**
     * Removes a registered command handler.
Jaida Wu's avatar
Jaida Wu committed
66
     *
KingRainbow44's avatar
KingRainbow44 committed
67
68
69
70
     * @param label The command label.
     * @return Instance chaining.
     */
    public CommandMap unregisterCommand(String label) {
71
        Grasscutter.getLogger().debug("Unregistered command: " + label);
72

73
        CommandHandler handler = this.commands.get(label);
Jaida Wu's avatar
Jaida Wu committed
74
75
        if (handler == null) return this;

76
        Command annotation = handler.getClass().getAnnotation(Command.class);
77
        this.annotations.remove(label);
KingRainbow44's avatar
KingRainbow44 committed
78
        this.commands.remove(label);
Jaida Wu's avatar
Jaida Wu committed
79

KingRainbow44's avatar
KingRainbow44 committed
80
        // Unregister aliases.
Jaida Wu's avatar
Jaida Wu committed
81
        if (annotation.aliases().length > 0) {
KingRainbow44's avatar
KingRainbow44 committed
82
            for (String alias : annotation.aliases()) {
83
                this.aliases.remove(alias);
84
                this.annotations.remove(alias);
KingRainbow44's avatar
KingRainbow44 committed
85
86
            }
        }
Jaida Wu's avatar
Jaida Wu committed
87

88
        return this;
KingRainbow44's avatar
KingRainbow44 committed
89
90
    }

91
92
93
    public List<Command> getAnnotationsAsList() {
        return new LinkedList<>(this.annotations.values());
    }
94

AnimeGitB's avatar
AnimeGitB committed
95
    public Map<String, Command> getAnnotations() {
96
97
98
        return new LinkedHashMap<>(this.annotations);
    }

KingRainbow44's avatar
KingRainbow44 committed
99
100
    /**
     * Returns a list of all registered commands.
Jaida Wu's avatar
Jaida Wu committed
101
     *
KingRainbow44's avatar
KingRainbow44 committed
102
103
     * @return All command handlers as a list.
     */
104
    public List<CommandHandler> getHandlersAsList() {
KingRainbow44's avatar
KingRainbow44 committed
105
106
107
        return new LinkedList<>(this.commands.values());
    }

AnimeGitB's avatar
AnimeGitB committed
108
109
    public Map<String, CommandHandler> getHandlers() {
        return this.commands;
Jaida Wu's avatar
Jaida Wu committed
110
    }
111

KingRainbow44's avatar
KingRainbow44 committed
112
113
    /**
     * Returns a handler by label/alias.
Jaida Wu's avatar
Jaida Wu committed
114
     *
KingRainbow44's avatar
KingRainbow44 committed
115
116
117
118
     * @param label The command label.
     * @return The command handler.
     */
    public CommandHandler getHandler(String label) {
Luke H-W's avatar
Luke H-W committed
119
120
121
122
123
124
        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
125
126
    }

127
128
129
130
131
132
133
134
135
136
137
138
139
140
    private static int getUidFromString(String input) {
        try {
            return Integer.parseInt(input);
        } catch (NumberFormatException ignored) {
            var account = DatabaseHelper.getAccountByName(input);
            if (account == null) return INVALID_UID;
            var player = DatabaseHelper.getPlayerByAccount(account);
            if (player == null) return INVALID_UID;
            // We will be immediately fetching the player again after this,
            // but offline vs online Player safety is more important than saving a lookup
            return player.getUid();
        }
    }

141
142
143
144
145
146
147
148
149
150
151
    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;
                }
152
153
                int uid = getUidFromString(arg);
                if (uid == INVALID_UID) {
Luke H-W's avatar
Luke H-W committed
154
                    CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid");
155
156
                    throw new IllegalArgumentException();
                }
157
158
159
160
161
162
                targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid, true);
                if (targetPlayer == null) {
                    CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
                    throw new IllegalArgumentException();
                }
                return targetPlayer;
163
164
165
166
167
168
169
170
171
172
173
            }
        }

        // 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)) {
174
            targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.getInt(playerId), true);
175
176
177
178
179
180
181
182
183
184
185
186
187
188
            // 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.
189
            targetPlayerIds.removeInt(playerId);
Kawaa's avatar
Kawaa committed
190
191
            CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target");
            return true;
192
        }
github-actions's avatar
github-actions committed
193

194
        // Sets default targetPlayer to the UID provided.
195
196
        int uid = getUidFromString(targetUid);
        if (uid == INVALID_UID) {
Luke H-W's avatar
Luke H-W committed
197
            CommandHandler.sendTranslatedMessage(player, "commands.generic.invalid.uid");
198
199
            return false;
        }
200
201
202
203
204
205
206
207
208
209
210
        Player targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid, true);
        if (targetPlayer == null) {
            CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
            return false;
        }

        targetPlayerIds.put(playerId, uid);
        String target = uid + " (" + targetPlayer.getAccount().getUsername() + ")";
        CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", target);
        CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline() ? "commands.execution.set_target_online" : "commands.execution.set_target_offline", target);
        return true;
211
212
    }

KingRainbow44's avatar
KingRainbow44 committed
213
214
    /**
     * Invoke a command handler with the given arguments.
Jaida Wu's avatar
Jaida Wu committed
215
216
     *
     * @param player     The player invoking the command or null for the server console.
KingRainbow44's avatar
KingRainbow44 committed
217
218
     * @param rawMessage The messaged used to invoke the command.
     */
AnimeGitB's avatar
AnimeGitB committed
219
    public void invoke(Player player, Player targetPlayer, String rawMessage) {
Kawaa's avatar
Kawaa committed
220
221
222
223
224
225
226
227
        // The console outputs in-game command. [{Account Username} (Player UID: {Player Uid})]
        if (SERVER.logCommands) {
            if (player != null) {
                Grasscutter.getLogger().info("Command used by [" + player.getAccount().getUsername() + " (Player UID: " + player.getUid() + ")]: " + rawMessage);
            } else {
                Grasscutter.getLogger().info("Command used by server console: " + rawMessage);
            }
        }
github-actions's avatar
github-actions committed
228

229
        rawMessage = rawMessage.trim();
BaiSugar's avatar
BaiSugar committed
230
        if (rawMessage.length() == 0) {
231
            CommandHandler.sendTranslatedMessage(player, "commands.generic.not_specified");
BaiSugar's avatar
BaiSugar committed
232
            return;
233
        }
Jaida Wu's avatar
Jaida Wu committed
234

KingRainbow44's avatar
KingRainbow44 committed
235
236
        // Parse message.
        String[] split = rawMessage.split(" ");
237
        List<String> args = new LinkedList<>(Arrays.asList(split));
238
        String label = args.remove(0).toLowerCase();
AnimeGitB's avatar
AnimeGitB committed
239
        String playerId = (player == null) ? consoleId : player.getAccount().getId();
240

241
242
        // Check for special cases - currently only target command.
        if (label.startsWith("@")) { // @[UID]
243
244
            this.setPlayerTarget(playerId, player, label.substring(1));
            return;
245
        } else if (label.equalsIgnoreCase("target")) { // target [[@]UID]
AnimeGitB's avatar
AnimeGitB committed
246
            if (args.size() > 0) {
247
                String targetUidStr = args.get(0);
AnimeGitB's avatar
AnimeGitB committed
248
249
250
                if (targetUidStr.startsWith("@")) {
                    targetUidStr = targetUidStr.substring(1);
                }
251
                this.setPlayerTarget(playerId, player, targetUidStr);
Kawaa's avatar
Kawaa committed
252
                return;
AnimeGitB's avatar
AnimeGitB committed
253
            } else {
254
255
                this.setPlayerTarget(playerId, player, "");
                return;
AnimeGitB's avatar
AnimeGitB committed
256
            }
AnimeGitB's avatar
AnimeGitB committed
257
        }
AnimeGitB's avatar
AnimeGitB committed
258

KingRainbow44's avatar
KingRainbow44 committed
259
        // Get command handler.
Luke H-W's avatar
Luke H-W committed
260
        CommandHandler handler = this.getHandler(label);
261

Luke H-W's avatar
Luke H-W committed
262
        // Check if the handler is null.
Jaida Wu's avatar
Jaida Wu committed
263
        if (handler == null) {
264
            CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
Jaida Wu's avatar
Jaida Wu committed
265
            return;
KingRainbow44's avatar
KingRainbow44 committed
266
        }
Jaida Wu's avatar
Jaida Wu committed
267

268
269
270
        // Get the command's annotation.
        Command annotation = this.annotations.get(label);

271
        // Resolve targetPlayer
github-actions's avatar
github-actions committed
272
        try {
273
274
275
            targetPlayer = getTargetPlayer(playerId, player, targetPlayer, args);
        } catch (IllegalArgumentException e) {
            return;
AnimeGitB's avatar
AnimeGitB committed
276
277
        }

4Benj_'s avatar
4Benj_ committed
278
        // Check for permissions.
279
        if (!Grasscutter.getPermissionHandler().checkPermission(player, targetPlayer, annotation.permission(), this.annotations.get(label).permissionTargeted())) {
4Benj_'s avatar
4Benj_ committed
280
            return;
281
282
283
        }

        // Check if command has unfulfilled constraints on targetPlayer
284
        Command.TargetRequirement targetRequirement = annotation.targetRequirement();
285
286
        if (targetRequirement != Command.TargetRequirement.NONE) {
            if (targetPlayer == null) {
287
288
                handler.sendUsageMessage(player);
                CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target");
289
290
                return;
            }
291

292
            if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) {
293
                handler.sendUsageMessage(player);
294
295
296
                CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online");
                return;
            }
297

298
            if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) {
299
                handler.sendUsageMessage(player);
300
                CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline");
Jaida Wu's avatar
Jaida Wu committed
301
                return;
302
303
            }
        }
Jaida Wu's avatar
Jaida Wu committed
304

305
306
307
308
        // 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
309
        // Invoke execute method for handler.
310
311
        Runnable runnable = () -> handlerF.execute(player, targetPlayerF, args);
        if (annotation.threading()) {
312
313
            new Thread(runnable).start();
        } else {
BaiSugar's avatar
BaiSugar committed
314
315
            runnable.run();
        }
KingRainbow44's avatar
KingRainbow44 committed
316
317
318
319
320
321
322
    }

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

KingRainbow44's avatar
KingRainbow44 committed
325
326
        classes.forEach(annotated -> {
            try {
327
                Command cmdData = annotated.getAnnotation(Command.class);
328
                Object object = annotated.getDeclaredConstructor().newInstance();
KingRainbow44's avatar
KingRainbow44 committed
329
330
                if (object instanceof CommandHandler)
                    this.registerCommand(cmdData.label(), (CommandHandler) object);
331
332
333
334
                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
335
336
337
        });
    }
}