mirror of
https://github.com/Grasscutters/Grasscutter.git
synced 2025-05-08 04:55:52 +08:00
497 lines
21 KiB
Java
497 lines
21 KiB
Java
package emu.grasscutter.command.commands;
|
|
|
|
import emu.grasscutter.GameConstants;
|
|
import emu.grasscutter.command.Command;
|
|
import emu.grasscutter.command.CommandHandler;
|
|
import emu.grasscutter.data.GameData;
|
|
import emu.grasscutter.data.GameDepot;
|
|
import emu.grasscutter.data.excels.AvatarData;
|
|
import emu.grasscutter.data.excels.ItemData;
|
|
import emu.grasscutter.data.excels.ReliquaryAffixData;
|
|
import emu.grasscutter.data.excels.ReliquaryMainPropData;
|
|
import emu.grasscutter.game.avatar.Avatar;
|
|
import emu.grasscutter.game.inventory.GameItem;
|
|
import emu.grasscutter.game.inventory.ItemType;
|
|
import emu.grasscutter.game.player.Player;
|
|
import emu.grasscutter.game.props.ActionReason;
|
|
import emu.grasscutter.game.props.FightProperty;
|
|
import emu.grasscutter.utils.SparseSet;
|
|
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
|
|
import java.util.regex.Matcher;
|
|
import java.util.regex.Pattern;
|
|
|
|
@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)
|
|
public final class GiveCommand implements CommandHandler {
|
|
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))");
|
|
|
|
private static int matchIntOrNeg(Pattern pattern, String arg) {
|
|
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;
|
|
}
|
|
|
|
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;
|
|
};
|
|
|
|
private 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
|
|
String arg = args.get(i).toLowerCase();
|
|
boolean deleteArg = false;
|
|
int argNum;
|
|
// Note that a single argument can actually match all of these, e.g. "lv90r5x100"
|
|
if ((argNum = matchIntOrNeg(lvlRegex, arg)) != -1) {
|
|
param.lvl = argNum;
|
|
deleteArg = true;
|
|
}
|
|
if ((argNum = matchIntOrNeg(refineRegex, arg)) != -1) {
|
|
param.refinement = argNum;
|
|
deleteArg = true;
|
|
}
|
|
if ((argNum = matchIntOrNeg(constellationRegex, arg)) != -1) {
|
|
param.constellation = argNum;
|
|
deleteArg = true;
|
|
}
|
|
if ((argNum = matchIntOrNeg(amountRegex, arg)) != -1) {
|
|
param.amount = argNum;
|
|
deleteArg = true;
|
|
}
|
|
if (deleteArg) {
|
|
args.remove(i);
|
|
}
|
|
}
|
|
|
|
// At this point, first remaining argument MUST be itemId/avatarId
|
|
if (args.size() < 1) {
|
|
sendUsageMessage(sender); // 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:
|
|
try {
|
|
param.id = Integer.parseInt(id);
|
|
} catch (NumberFormatException e) {
|
|
// TODO: Parse from item name using GM Handbook.
|
|
CommandHandler.sendTranslatedMessage(sender, "commands.generic.invalid.itemId");
|
|
throw e;
|
|
}
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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");
|
|
} else {
|
|
// Suitable for Avatars and Weapons
|
|
if (param.lvl < 1) param.lvl = 1;
|
|
if (param.lvl > 90) param.lvl = 90;
|
|
}
|
|
|
|
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 {
|
|
sendUsageMessage(sender);
|
|
throw new IllegalArgumentException();
|
|
}
|
|
}
|
|
|
|
return param;
|
|
}
|
|
|
|
@Override
|
|
public void execute(Player sender, Player targetPlayer, List<String> args) {
|
|
if (args.size() < 1) { // *No args*
|
|
sendUsageMessage(sender);
|
|
return;
|
|
}
|
|
try {
|
|
GiveItemParameters param = parseArgs(sender, args);
|
|
|
|
switch (param.giveAllType) {
|
|
case ALL:
|
|
giveAll(targetPlayer, param);
|
|
CommandHandler.sendTranslatedMessage(sender, "commands.give.giveall_success");
|
|
return;
|
|
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) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
private static Avatar makeAvatar(GiveItemParameters param) {
|
|
return makeAvatar(param.avatarData, param.lvl, Avatar.getMinPromoteLevel(param.lvl), param.constellation);
|
|
}
|
|
|
|
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);
|
|
|
|
// 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);
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
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
|
|
}
|
|
items.add(item);
|
|
}
|
|
return items;
|
|
}
|
|
|
|
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
|
|
""");
|
|
}
|