GiveCommand.java 21.1 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
@Command(label = "give", usage = "give <itemId|avatarId|\"all\"|\"weapons\"|\"mats\"|\"avatars\"> [lv<level>] [r<refinement>] [x<amount>] | give <artifactId> [lv<level>] [x<amount>] [mainPropId] [<appendPropId>[,<times>]]...", aliases = {
27
        "g", "item", "giveitem"}, permission = "player.give", permissionTargeted = "player.give.others", description = "commands.give.description")
KingRainbow44's avatar
KingRainbow44 committed
28
public final class GiveCommand implements CommandHandler {
29
30
31
32
    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))");
33

34
    private static int matchIntOrNeg(Pattern pattern, String arg) {
35
36
37
38
39
40
        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
41

42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
    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
62

63
64
65
66
67
    private static GiveItemParameters parseArgs(Player sender, List<String> args) throws IllegalArgumentException {
        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
68
69
70
            String arg = args.get(i).toLowerCase();
            boolean deleteArg = false;
            int argNum;
71
            // Note that a single argument can actually match all of these, e.g. "lv90r5x100"
72
            if ((argNum = matchIntOrNeg(lvlRegex, arg)) != -1) {
73
                param.lvl = argNum;
74
75
76
                deleteArg = true;
            }
            if ((argNum = matchIntOrNeg(refineRegex, arg)) != -1) {
77
78
79
80
81
                param.refinement = argNum;
                deleteArg = true;
            }
            if ((argNum = matchIntOrNeg(constellationRegex, arg)) != -1) {
                param.constellation = argNum;
82
83
84
                deleteArg = true;
            }
            if ((argNum = matchIntOrNeg(amountRegex, arg)) != -1) {
85
                param.amount = argNum;
86
87
88
89
90
91
92
                deleteArg = true;
            }
            if (deleteArg) {
                args.remove(i);
            }
        }

93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
        // At this point, first remaining argument MUST be itemId/avatarId
        if (args.size() < 1) {
            CommandHandler.sendTranslatedMessage(sender, "commands.give.usage");  // Reachable if someone does `/give lv90` or similar
            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
115
                try {
116
117
                    param.id = Integer.parseInt(id);
                } catch (NumberFormatException e) {
KingRainbow44's avatar
KingRainbow44 committed
118
                    // TODO: Parse from item name using GM Handbook.
119
120
                    CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.itemId");
                    throw e;
KingRainbow44's avatar
KingRainbow44 committed
121
                }
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
                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
137
138
        }

139
140
141
142
143
144
145
146
147
148
        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");
149
        } else {
150
151
152
153
154
            // Suitable for Avatars and Weapons
            if (param.lvl < 1) param.lvl = 1;
            if (param.lvl > 90) param.lvl = 90;
        }

155
156
157
158
159
160
161
162
163
164
165
166
        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 {
                CommandHandler.sendTranslatedMessage(sender, "commands.give.usage");
                throw new IllegalArgumentException();
167
168
169
170
171
172
173
174
175
176
            }
        }

        return param;
    }

    @Override
    public void execute(Player sender, Player targetPlayer, List<String> args) {
        if (args.size() < 1) { // *No args*
            CommandHandler.sendTranslatedMessage(sender, "commands.give.usage");
KingRainbow44's avatar
KingRainbow44 committed
177
178
            return;
        }
179
180
181
182
183
184
185
        try {
            GiveItemParameters param = parseArgs(sender, args);

            switch (param.giveAllType) {
                case ALL:
                    giveAll(targetPlayer, param);
                    CommandHandler.sendTranslatedMessage(sender, "commands.give.giveall_success");
186
                    return;
187
188
189
190
191
192
193
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
                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()));
                    //CommandHandler.sendTranslatedMessage(sender, "commands.giveArtifact.success", Integer.toString(param.id), 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) {
232
233
                return;
        }
234
    }
KingRainbow44's avatar
KingRainbow44 committed
235

236
237
238
239
240
241
242
243
244
    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
245

246
247
248
249
250
251
252
253
254
        // 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);
255
        }
256
257
258
259
260
261
262
263
264
265
266
267

        // Main character needs skill depot manually added.
        if (avatar.getAvatarId() == GameConstants.MAIN_CHARACTER_MALE) {
            avatar.setSkillDepotData(GameData.getAvatarSkillDepotDataMap().get(504));
        }
        else if(avatar.getAvatarId() == GameConstants.MAIN_CHARACTER_FEMALE) {
            avatar.setSkillDepotData(GameData.getAvatarSkillDepotDataMap().get(704));
        }

        avatar.recalcStats();

        return avatar;
KingRainbow44's avatar
KingRainbow44 committed
268
269
    }

270
271
272
273
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
    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
302
            }
303
            items.add(item);
KingRainbow44's avatar
KingRainbow44 committed
304
        }
305
        return items;
KingRainbow44's avatar
KingRainbow44 committed
306
    }
307
308
309
310
311
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
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
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
486
487
488
489

    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;
    }

	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) {
            // 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);
		}
	}

    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
        """);
    
    private static final SparseSet illegalRelicIds = new SparseSet("""
        20001, 23300-23340, 23383-23385, 78310-78554, 99310-99554
        """);
    
    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
490
}