GiveCommand.java 20.8 KB
Newer Older
KingRainbow44's avatar
KingRainbow44 committed
1
2
package emu.grasscutter.command.commands;

3
import emu.grasscutter.GameConstants;
KingRainbow44's avatar
KingRainbow44 committed
4
5
import emu.grasscutter.command.Command;
import emu.grasscutter.command.CommandHandler;
6
import emu.grasscutter.data.GameData;
7
8
import emu.grasscutter.data.GameDepot;
import emu.grasscutter.data.excels.AvatarData;
Melledy's avatar
Melledy committed
9
import emu.grasscutter.data.excels.ItemData;
10
11
12
import emu.grasscutter.data.excels.ReliquaryAffixData;
import emu.grasscutter.data.excels.ReliquaryMainPropData;
import emu.grasscutter.game.avatar.Avatar;
13
import emu.grasscutter.game.inventory.GameItem;
14
import emu.grasscutter.game.inventory.ItemType;
Melledy's avatar
Melledy committed
15
import emu.grasscutter.game.player.Player;
KingRainbow44's avatar
KingRainbow44 committed
16
import emu.grasscutter.game.props.ActionReason;
17
18
import emu.grasscutter.game.props.FightProperty;
import emu.grasscutter.utils.SparseSet;
KingRainbow44's avatar
KingRainbow44 committed
19

20
import java.util.ArrayList;
KingRainbow44's avatar
KingRainbow44 committed
21
import java.util.List;
22

23
import java.util.regex.Matcher;
24
import java.util.regex.Pattern;
KingRainbow44's avatar
KingRainbow44 committed
25

26
27
28
29
30
31
32
33
34
@Command(
    label = "give",
    aliases = {"g", "item", "giveitem"},
    usage = {
        "(<itemId>|<avatarId>|all|weapons|mats|avatars) [lv<level>] [r<refinement>] [x<amount>] [c<constellation>]",
        "<artifactId> [lv<level>] [x<amount>] [<mainPropId>] [<appendPropId>[,<times>]]..."},
    permission = "player.give",
    permissionTargeted = "player.give.others",
    threading = true)
KingRainbow44's avatar
KingRainbow44 committed
35
public final class GiveCommand implements CommandHandler {
36
37
38
39
    private static Pattern lvlRegex = Pattern.compile("l(?:vl?)?(\\d+)");  // Java doesn't have raw string literals :(
    private static Pattern refineRegex = Pattern.compile("r(\\d+)");
    private static Pattern constellationRegex = Pattern.compile("c(\\d+)");
    private static Pattern amountRegex = Pattern.compile("((?<=x)\\d+|\\d+(?=x)(?!x\\d))");
40

41
    private static int matchIntOrNeg(Pattern pattern, String arg) {
42
43
44
45
46
47
        Matcher match = pattern.matcher(arg);
        if (match.find()) {
            return Integer.parseInt(match.group(1));  // This should be exception-safe as only \d+ can be passed to it (i.e. non-empty string of pure digits)
        }
        return -1;
    }
KingRainbow44's avatar
KingRainbow44 committed
48

49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
    private static enum GiveAllType {
        NONE,
        ALL,
        WEAPONS,
        MATS,
        AVATARS
    }

    private static class GiveItemParameters {
        public int id;
        public int lvl = 0;
        public int amount = 1;
        public int refinement = 1;
        public int constellation = -1;
        public int mainPropId = -1;
        public List<Integer> appendPropIdList;
        public ItemData data;
        public AvatarData avatarData;
        public GiveAllType giveAllType = GiveAllType.NONE;
    };
KingRainbow44's avatar
KingRainbow44 committed
69

70
    private GiveItemParameters parseArgs(Player sender, List<String> args) throws IllegalArgumentException {
71
72
73
74
        GiveItemParameters param = new GiveItemParameters();

        // Extract any tagged arguments (e.g. "lv90", "x100", "r5")
        for (int i = args.size() - 1; i >= 0; i--) {  // Reverse iteration as we are deleting elements
75
76
77
            String arg = args.get(i).toLowerCase();
            boolean deleteArg = false;
            int argNum;
78
            // Note that a single argument can actually match all of these, e.g. "lv90r5x100"
79
            if ((argNum = matchIntOrNeg(lvlRegex, arg)) != -1) {
80
                param.lvl = argNum;
81
82
83
                deleteArg = true;
            }
            if ((argNum = matchIntOrNeg(refineRegex, arg)) != -1) {
84
85
86
87
88
                param.refinement = argNum;
                deleteArg = true;
            }
            if ((argNum = matchIntOrNeg(constellationRegex, arg)) != -1) {
                param.constellation = argNum;
89
90
91
                deleteArg = true;
            }
            if ((argNum = matchIntOrNeg(amountRegex, arg)) != -1) {
92
                param.amount = argNum;
93
94
95
96
97
98
99
                deleteArg = true;
            }
            if (deleteArg) {
                args.remove(i);
            }
        }

100
101
        // At this point, first remaining argument MUST be itemId/avatarId
        if (args.size() < 1) {
102
            sendUsageMessage(sender);  // Reachable if someone does `/give lv90` or similar
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
            throw new IllegalArgumentException();
        }
        String id = args.remove(0);
        boolean isRelic = false;

        switch (id) {
            case "all":
                param.giveAllType = GiveAllType.ALL;
                break;
            case "weapons":
                param.giveAllType = GiveAllType.WEAPONS;
                break;
            case "mats":
                param.giveAllType = GiveAllType.MATS;
                break;
            case "avatars":
                param.giveAllType = GiveAllType.AVATARS;
                break;
            default:
KingRainbow44's avatar
KingRainbow44 committed
122
                try {
123
124
                    param.id = Integer.parseInt(id);
                } catch (NumberFormatException e) {
KingRainbow44's avatar
KingRainbow44 committed
125
                    // TODO: Parse from item name using GM Handbook.
126
127
                    CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.itemId");
                    throw e;
KingRainbow44's avatar
KingRainbow44 committed
128
                }
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
                param.data = GameData.getItemDataMap().get(param.id);
                if ((param.id > 10_000_000) && (param.id < 12_000_000))
                    param.avatarData = GameData.getAvatarDataMap().get(param.id);
                else if ((param.id > 1000) && (param.id < 1100))
                    param.avatarData = GameData.getAvatarDataMap().get(param.id - 1000 + 10_000_000);
                isRelic = ((param.data != null) && (param.data.getItemType() == ItemType.ITEM_RELIQUARY));

                if (!isRelic && !args.isEmpty() && (param.amount == 1)) {  // A concession for the people that truly hate [x<amount>].
                    try {
                        param.amount = Integer.parseInt(args.remove(0));
                    } catch (NumberFormatException e) {
                        CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.amount");
                        throw e;
                    }
                }
KingRainbow44's avatar
KingRainbow44 committed
144
145
        }

146
147
148
149
150
151
152
153
154
155
        if (param.amount < 1) param.amount = 1;
        if (param.refinement < 1) param.refinement = 1;
        if (param.refinement > 5) param.refinement = 5;
        if (isRelic) {
            // Input 0-20 to match game, instead of 1-21 which is the real level
            if (param.lvl < 0) param.lvl = 0;
            if (param.lvl > 20) param.lvl = 20;
            param.lvl += 1;
            if (illegalRelicIds.contains(param.id))
                CommandHandler.sendTranslatedMessage(sender, "commands.give.illegal_relic");
156
        } else {
157
158
159
160
161
            // Suitable for Avatars and Weapons
            if (param.lvl < 1) param.lvl = 1;
            if (param.lvl > 90) param.lvl = 90;
        }

162
163
164
165
166
167
168
169
170
171
        if (!args.isEmpty()) {
            if (isRelic) {
                try {
                    parseRelicArgs(param, args);
                } catch (IllegalArgumentException e) {
                    CommandHandler.sendTranslatedMessage(sender, "commands.execution.argument_error");
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.usage_relic");
                    throw e;
                }
            } else {
172
                sendUsageMessage(sender);
173
                throw new IllegalArgumentException();
174
175
176
177
178
179
180
181
182
            }
        }

        return param;
    }

    @Override
    public void execute(Player sender, Player targetPlayer, List<String> args) {
        if (args.size() < 1) { // *No args*
183
            sendUsageMessage(sender);
KingRainbow44's avatar
KingRainbow44 committed
184
185
            return;
        }
186
187
188
189
190
191
192
        try {
            GiveItemParameters param = parseArgs(sender, args);

            switch (param.giveAllType) {
                case ALL:
                    giveAll(targetPlayer, param);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.giveall_success");
193
                    return;
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
                case WEAPONS:
                    giveAllWeapons(targetPlayer, param);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.giveall_success");
                    return;
                case MATS:
                    giveAllMats(targetPlayer, param);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.giveall_success");
                    return;
                case AVATARS:
                    giveAllAvatars(targetPlayer, param);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.giveall_success");
                    return;
                case NONE:
                    break;
            }

            // Check if this is an avatar
            if (param.avatarData != null) {
                Avatar avatar = makeAvatar(param);
                targetPlayer.addAvatar(avatar);
                CommandHandler.sendTranslatedMessage(sender, "commands.give.given_avatar", Integer.toString(param.id), Integer.toString(param.lvl), Integer.toString(targetPlayer.getUid()));
                return;
            }
            // If it's not an avatar, it needs to be a valid item
            if (param.data == null) {
                CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.itemId");
                return;
            }

            switch (param.data.getItemType()) {
                case ITEM_WEAPON:
                    targetPlayer.getInventory().addItems(makeUnstackableItems(param), ActionReason.SubfieldDrop);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.given_with_level_and_refinement", Integer.toString(param.id), Integer.toString(param.lvl), Integer.toString(param.refinement), Integer.toString(param.amount), Integer.toString(targetPlayer.getUid()));
                    return;
                case ITEM_RELIQUARY:
                    targetPlayer.getInventory().addItems(makeArtifacts(param), ActionReason.SubfieldDrop);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.given_level", Integer.toString(param.id), Integer.toString(param.lvl), Integer.toString(param.amount), Integer.toString(targetPlayer.getUid()));
                    return;
                default:
                    targetPlayer.getInventory().addItem(new GameItem(param.data, param.amount), ActionReason.SubfieldDrop);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.given", Integer.toString(param.amount), Integer.toString(param.id), Integer.toString(targetPlayer.getUid()));
                    return;
            }
        } catch (IllegalArgumentException ignored) {
238
239
                return;
        }
240
    }
KingRainbow44's avatar
KingRainbow44 committed
241

242
243
244
245
246
247
248
249
250
    private static Avatar makeAvatar(GiveItemParameters param) {
        return makeAvatar(param.avatarData, param.lvl, Avatar.getMinPromoteLevel(param.lvl), 0);
    }

    private static Avatar makeAvatar(AvatarData avatarData, int level, int promoteLevel, int constellation) {
        // Calculate ascension level.
        Avatar avatar = new Avatar(avatarData);
        avatar.setLevel(level);
        avatar.setPromoteLevel(promoteLevel);
KingRainbow44's avatar
KingRainbow44 committed
251

252
253
254
255
256
257
258
259
260
        // Add constellations.
        int talentBase = switch (avatar.getAvatarId()) {
            case 10000005 -> 70;
            case 10000006 -> 40;
            default -> (avatar.getAvatarId() - 10000000) * 10;
        };

        for (int i = 1; i <= constellation; i++) {
            avatar.getTalentIdList().add(talentBase + i);
261
        }
262
263
264
265
266

        // Main character needs skill depot manually added.
        if (avatar.getAvatarId() == GameConstants.MAIN_CHARACTER_MALE) {
            avatar.setSkillDepotData(GameData.getAvatarSkillDepotDataMap().get(504));
        }
github-actions's avatar
github-actions committed
267
        else if (avatar.getAvatarId() == GameConstants.MAIN_CHARACTER_FEMALE) {
268
269
270
271
272
273
            avatar.setSkillDepotData(GameData.getAvatarSkillDepotDataMap().get(704));
        }

        avatar.recalcStats();

        return avatar;
KingRainbow44's avatar
KingRainbow44 committed
274
275
    }

276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
    private static void giveAllAvatars(Player player, GiveItemParameters param) {
        int promoteLevel = Avatar.getMinPromoteLevel(param.lvl);
        if (param.constellation < 0) {
            param.constellation = 6;
        }
        for (AvatarData avatarData : GameData.getAvatarDataMap().values()) {
            // Exclude test avatars
            int id = avatarData.getId();
            if (id < 10000002 || id >= 11000000) continue;

            // Don't try to add each avatar to the current team
            player.addAvatar(makeAvatar(avatarData, param.lvl, promoteLevel, param.constellation), false);
        }
    }

    private static List<GameItem> makeUnstackableItems(GiveItemParameters param) {
        int promoteLevel = GameItem.getMinPromoteLevel(param.lvl);
        int totalExp = 0;
        if (param.data.getItemType() == ItemType.ITEM_WEAPON) {
            int rankLevel = param.data.getRankLevel();
            for (int i = 1; i < param.lvl; i++)
                totalExp += GameData.getWeaponExpRequired(rankLevel, i);
        }

        List<GameItem> items = new ArrayList<>(param.amount);
        for (int i = 0; i < param.amount; i++) {
            GameItem item = new GameItem(param.data);
            item.setLevel(param.lvl);
            if (item.getItemType() == ItemType.ITEM_WEAPON) {
                item.setPromoteLevel(promoteLevel);
                item.setTotalExp(totalExp);
                item.setRefinement(param.refinement - 1);  // Actual refinement data is 0..4 not 1..5
memetrollsXD's avatar
memetrollsXD committed
308
            }
309
            items.add(item);
KingRainbow44's avatar
KingRainbow44 committed
310
        }
311
        return items;
KingRainbow44's avatar
KingRainbow44 committed
312
    }
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361

    private static List<GameItem> makeArtifacts(GiveItemParameters param) {
        param.lvl = Math.min(param.lvl, param.data.getMaxLevel());
        int rank = param.data.getRankLevel();
        int totalExp = 0;
        for (int i = 1; i < param.lvl; i++)
            totalExp += GameData.getRelicExpRequired(rank, i);

        List<GameItem> items = new ArrayList<>(param.amount);
        for (int i = 0; i < param.amount; i++) {
            // Create item for the artifact.
            GameItem item = new GameItem(param.data);
            item.setLevel(param.lvl);
            item.setTotalExp(totalExp);
            int numAffixes = param.data.getAppendPropNum() + (param.lvl-1)/4;
            if (param.mainPropId > 0)  // Keep random mainProp if we didn't specify one
                item.setMainPropId(param.mainPropId);
            if (param.appendPropIdList != null) {
                item.getAppendPropIdList().clear();
                item.getAppendPropIdList().addAll(param.appendPropIdList);
            }
            // If we didn't include enough substats, top them up to the appropriate level at random
            item.addAppendProps(numAffixes - item.getAppendPropIdList().size());
            items.add(item);
        }
        return items;
    }

    private static int getArtifactMainProp(ItemData itemData, FightProperty prop) throws IllegalArgumentException {
        if (prop != FightProperty.FIGHT_PROP_NONE)
            for (ReliquaryMainPropData data : GameDepot.getRelicMainPropList(itemData.getMainPropDepotId()))
                if (data.getWeight() > 0 && data.getFightProp() == prop)
                    return data.getId();
        throw new IllegalArgumentException();
    }

    private static List<Integer> getArtifactAffixes(ItemData itemData, FightProperty prop) throws IllegalArgumentException {
        if (prop == FightProperty.FIGHT_PROP_NONE) {
            throw new IllegalArgumentException();
        }
        List<Integer> affixes = new ArrayList<>();
        for (ReliquaryAffixData data : GameDepot.getRelicAffixList(itemData.getAppendPropDepotId())) {
            if (data.getWeight() > 0 && data.getFightProp() == prop) {
                affixes.add(data.getId());
            }
        }
        return affixes;
    }

github-actions's avatar
github-actions committed
362
363
364
365
366
    private static int getAppendPropId(String substatText, ItemData itemData) throws IllegalArgumentException {
        // If the given substat text is an integer, we just use that as the append prop ID.
        try {
            return Integer.parseInt(substatText);
        } catch (NumberFormatException ignored) {
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
            // If the argument was not an integer, we try to determine
            // the append prop ID from the given text + artifact information.
            // A substat string has the format `substat_tier`, with the
            // `_tier` part being optional, defaulting to the maximum.
            String[] substatArgs = substatText.split("_");
            String substatType = substatArgs[0];

            int substatTier = 4;
            if (substatArgs.length > 1) {
                substatTier = Integer.parseInt(substatArgs[1]);
            }

            List<Integer> substats = getArtifactAffixes(itemData, FightProperty.getPropByShortName(substatType));

            if (substats.isEmpty()) {
                throw new IllegalArgumentException();
            }

            substatTier -= 1;  // 1-indexed to 0-indexed
            substatTier = Math.min(Math.max(0, substatTier), substats.size() - 1);
            return substats.get(substatTier);
github-actions's avatar
github-actions committed
388
389
        }
    }
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485

    private static void parseRelicArgs(GiveItemParameters param, List<String> args) throws IllegalArgumentException {
        // Get the main stat from the arguments.
        // If the given argument is an integer, we use that.
        // If not, we check if the argument string is in the main prop map.
        String mainPropIdString = args.remove(0);

        try {
            param.mainPropId = Integer.parseInt(mainPropIdString);
        } catch (NumberFormatException ignored) {
            // This can in turn throw an exception which we don't want to catch here.
            param.mainPropId = getArtifactMainProp(param.data, FightProperty.getPropByShortName(mainPropIdString));
        }

        // Get substats.
        param.appendPropIdList = new ArrayList<>();
        // Every remaining argument is a substat.
        for (String prop : args) {
            // The substat syntax permits specifying a number of rolls for the given
            // substat. Split the string into stat and number if that is the case here.
            String[] arr = prop.split(",");
            prop = arr[0];
            int n = 1;
            if (arr.length > 1) {
                n = Math.min(Integer.parseInt(arr[1]), 200);
            }

            // Determine the substat ID.
            int appendPropId = getAppendPropId(prop, param.data);

            // Add the current substat.
            for (int i = 0; i < n; i++) {
                param.appendPropIdList.add(appendPropId);
            }
        };
    }

    private static void addItemsChunked(Player player, List<GameItem> items, int packetSize) {
        // Send the items in multiple packets
        int lastIdx = items.size() - 1;
        for (int i = 0; i <= lastIdx; i += packetSize) {
            player.getInventory().addItems(items.subList(i, Math.min(i + packetSize, lastIdx)));
        }
    }

    private static void giveAllMats(Player player, GiveItemParameters param) {
        List<GameItem> itemList = new ArrayList<>();
        for (ItemData itemdata : GameData.getItemDataMap().values()) {
            int id = itemdata.getId();
            if (id < 100_000) continue;  // Nothing meaningful below this
            if (illegalItemIds.contains(id)) continue;
            if (itemdata.isEquip()) continue;

            GameItem item = new GameItem(itemdata);
            item.setCount(param.amount);
            itemList.add(item);
        }

        addItemsChunked(player, itemList, 100);
    }

    private static void giveAllWeapons(Player player, GiveItemParameters param) {
        int promoteLevel = GameItem.getMinPromoteLevel(param.lvl);
        int quantity = Math.min(param.amount, 5);
        int refinement = param.refinement - 1;

        List<GameItem> itemList = new ArrayList<>();
        for (ItemData itemdata : GameData.getItemDataMap().values()) {
            int id = itemdata.getId();
            if (id < 11100 || id > 16000) continue;  // All extant weapons are within this range
            if (illegalWeaponIds.contains(id)) continue;
            if (!itemdata.isEquip()) continue;
            if (itemdata.getItemType() != ItemType.ITEM_WEAPON) continue;

            for (int i = 0; i < quantity; i++) {
                GameItem item = new GameItem(itemdata);
                item.setLevel(param.lvl);
                item.setPromoteLevel(promoteLevel);
                item.setRefinement(refinement);
                itemList.add(item);
            }
        }

        addItemsChunked(player, itemList, 100);
    }

    private static void giveAll(Player player, GiveItemParameters param) {
        giveAllAvatars(player, param);
        giveAllMats(player, param);
        giveAllWeapons(player, param);
    }

    private static final SparseSet illegalWeaponIds = new SparseSet("""
        10000-10008, 11411, 11506-11508, 12505, 12506, 12508, 12509,
        13503, 13506, 14411, 14503, 14505, 14508, 15504-15506
        """);
github-actions's avatar
github-actions committed
486

487
488
489
    private static final SparseSet illegalRelicIds = new SparseSet("""
        20001, 23300-23340, 23383-23385, 78310-78554, 99310-99554
        """);
github-actions's avatar
github-actions committed
490

491
492
493
494
495
    private static final SparseSet illegalItemIds = new SparseSet("""
        100086, 100087, 100100-101000, 101106-101110, 101306, 101500-104000,
        105001, 105004, 106000-107000, 107011, 108000, 109000-110000,
        115000-130000, 200200-200899, 220050, 220054
        """);
KingRainbow44's avatar
KingRainbow44 committed
496
}