Commit 0ae3c3d7 authored by Luke H-W's avatar Luke H-W Committed by GitHub
Browse files

Allow commands to target offline players (#1022)



* Add targetRequirement annotation for Command

* Added MTL lines for other langs

* Fix TargetRequirement enum scoping

* Adjust commands to targetRequirement system

* Add translation message sugar to prevent future messages from being translated for wrong player

* Temporarily disable offline targeting on /permission and /clear

* Preliminary README cleanup

* Readme commands cleanup

* Clean up command table in README, including column shuffle
Co-authored-by: default avatarAnimeGitB <AnimeGitB@bigblueball.in>
parent fa0344d1
......@@ -106,43 +106,50 @@ You may want to use this command (`java -jar grasscutter.jar -gachamap`) to gene
There is a dummy user named "Server" in every player's friends list that you can message to use commands. Commands also work in other chat rooms, such as private/team chats. to run commands ingame, you need to add prefix `/` or `!` such as `/pos`
| Commands | Usage | Permission node | Availability | description | Alias |
| -------------- | ------------------------------------------------- | ------------------------- |--------------| ------------------------------------------------------------ | ----------------------------------------------- |
| account | account <create\|delete> \<username> [UID] | | Server only | Creates an account with the specified username and the in-game UID for that account. The UID will be auto generated if not set. | |
| broadcast | broadcast \<message> | server.broadcast | Both side | Sends a message to all the players. | b |
| coop | coop \<playerId> \<target playerId> | server.coop | Both side | Forces someone to join the world of others. | |
| changescene | changescene \<scene id> | player.changescene | Client only | Switch scenes by scene ID. | scene |
| clear | clear <all\|wp\| art\ |mat> [UID] | player.clearinv | Client only | Deletes all unequipped and unlocked level 0 artifacts(art)/weapons(wp)/material(all) or all, including 5-star rarity ones from your inventory. | clear |
| drop | drop <itemID\|itemName> [amount] | server.drop | Client only | Drops an item around you. | `d` `dropitem` |
| enterdungeon | enterdungeon \<dungeon id> | player.enterdungeon | Client only | Enter a dungeon by dungeon ID | |
| give | give [player] <itemId\|itemName> [amount] [level] [finement] | player.give | Both side | Gives item(s) to you or the specified player. (finement option only weapon.) | `g` `item` `giveitem` |
| givechar | givechar \<uid> \<avatarId> | player.givechar | Both side | Gives the player a specified character. | givec |
| giveart | giveart [player] \<artifactId> \<mainPropId> [\<appendPropId>[,\<times>]]... [level] | player.giveart | Both side | Gives the player a specified artifact. | gart |
| giveall | giveall [uid] [amount] | player.giveall | Both side | Gives all items. | givea |
| godmode | godmode [uid] | player.godmode | Client only | Prevents you from taking damage. | |
| heal | heal | player.heal | Client only | Heals all characters in your current team. | h |
| help | help [command] | | Both side | Sends the help message or shows information about a specified command. | |
| join | join [avatarIds] | player.join | Client only | Force let avatars to join into your current team. Such as `join 10000020 10000021`. | |
| kick | kick \<player> | server.kick | Both side | Kicks the specified player from the server. (WIP) | k |
| killall | killall [playerUid] [sceneId] | server.killall | Both side | Kills all entities in the current scene or specified scene of the corresponding player. | |
| list | list | | Both side | Lists online players. | |
| permission | permission <add\|remove> \<UID> \<permission> | permission | Both side | Grants or removes a permission for a user. | |
| position | position | | Client only | Sends your current coordinates. | pos |
| reload | reload | server.reload | Both side | Reloads the server config | |
| remove | remove [avatarIndexInYourTeams] | player.remove | Client only | Force remove avatar in your current team. Index start from 1.Such as `remove 1 2`. | |
| resetconst | resetconst [all] | player.resetconstellation | Client only | Resets the constellation level on your currently selected character, will need to relog after using the command to see any changes. | resetconstellation |
| restart | | | Both side | Restarts the current session | |
| say | say \<player> \<message> | server.sendmessage | Both side | Sends a message to a player as the server | `sendservmsg` `sendservermessage` `sendmessage` |
| setfetterlevel | setfetterlevel \<level> | player.setfetterlevel | Client only | Sets the friendship level for your currently selected character | setfetterlvl |
| setstats | setstats \<stat> \<value> | player.setstats | Client only | Sets a stat for your currently selected character | stats |
| setworldlevel | setworldlevel \<level> | player.setworldlevel | Client only | Sets your world level (Relog to see proper effects) | setworldlvl |
| spawn | spawn \<entityId> [amount] [level(monster only)] | server.spawn | Client only | Spawns some entities around you | |
| stop | stop | server.stop | Both side | Stops the server | |
| talent | talent \<talentID> \<value> | player.settalent | Client only | Sets talent level for your currently selected character | |
| teleport | teleport [@playerUid] \<x> \<y> \<z> [sceneId] | player.teleport | Both side | Change the player's position. | tp |
| tpall | | player.tpall | Client only | Teleports all players in your world to your position | |
| unlocktower | | player.tower | Client only | Unlock the all floors of abyss | ut |
| weather | weather \<weatherID> \<climateID> | player.weather | Client only | Changes the weather | w |
### Targeting
1. For commands that target a Player, you can specify a target UID with `@UID` as an argument in any position to the command.
2. If you message a valid command at another player (instead of at the "Server" virtual player), they will be the target for that command if you didn't set one above.
3. If none of the above, it will default to a persistent target player you previously set using the command `/target <UID>`.
4. If none of the above, you will be the target of the command. If you are entering the command from the Server console, **it will not work!**
Note that performing commands on other players will usually require different a permission to the base permission node. e.g. `player.give` becomes `player.give.others` if used on another player.
| Commands | Description | Alias | Targeting | Usage | Permission node |
| -------------- | ------------------------------------------------------------------------------------------------- | ------------------ | ------------- | --------------------------------------------------------------------------- | ------------------------- |
| account | Creates an account with the specified username, and the in-game UID if specified. | | Server only | account \<create\|delete> \<username> [UID] | |
| broadcast | Sends a message to all the players. | b | None | broadcast \<message> | server.broadcast |
| coop | Forces someone to join the world of others. | | Online Player | coop [host UID (default self)] | server.coop |
| changescene | Switch scenes by scene ID. | scene | Online Player | changescene \<scene id> | player.changescene |
| clear | Deletes all unequipped and unlocked lvl0 artifacts(art)/weapons(wp)/material(mat) from inventory. | | Online Player | clear \<all\|wp\|art\|mat> | player.clearinv |
| drop | Drops an item around you. | d dropitem | Online Player | drop \<itemID\|itemName> [amount] | server.drop |
| enterdungeon | Enter a dungeon by dungeon ID. | | Online Player | enterdungeon \<dungeon id> | player.enterdungeon |
| give | Gives item(s) to you or the specified player. | g item giveitem | Online Player | give \<itemId\|itemName> [amount] [level] [refinement] | player.give |
| giveall | Gives all items. | givea | Online Player | giveall [amount] | player.giveall |
| giveart | Gives the player a specified artifact. | gart | Online Player | giveart \<artifactId> \<mainPropId> [\<appendPropId>[,\<times>]]... [level] | player.giveart |
| givechar | Gives the player a specified character. | givec | Online Player | givechar \<avatarId> | player.givechar |
| godmode | Prevents you from taking damage. | | Online Player | godmode | player.godmode |
| heal | Heals all characters in your current team. | h | Online Player | heal | player.heal |
| help | Sends the help message or shows information about a specified command. | | None | help [command] | |
| kick | Kicks the specified player from the server. | k | Online Player | kick | server.kick |
| killall | Kills all entities in the current scene or specified scene of the corresponding player. | | Online Player | killall [sceneId] | server.killall |
| list | Lists online players. | | None | list | |
| permission | Grants or removes a permission for a user. | | Online Player | permission \<add\|remove> \<permission> | permission |
| position | Sends your current coordinates. | pos | Online Player | position | |
| reload | Reloads the server config. | | None | reload | server.reload |
| resetconst | Resets currently selected (or all) character(s) to C0. Relog to see proper effects. | resetconstellation | Online Player | resetconst [all] | player.resetconstellation |
| restart | Restarts the current session. | | None | restart | |
| sendmessage | Sends a message to a player as the server. | say | Online Player | say \<message> | server.sendmessage |
| setfetterlevel | Sets the friendship level for your currently selected character. | setfetterlvl | Online Player | setfetterlevel \<level> | player.setfetterlevel |
| setstats | Sets a stat for your currently selected character. | stats | Online Player | setstats \<stat> \<value> | player.setstats |
| setworldlevel | Sets your world level. Relog to see proper effects. | setworldlvl | Online Player | setworldlevel \<level> | player.setworldlevel |
| spawn | Spawns some entities around you. | | Online Player | spawn \<entityId> [amount] [level(monster only)] | server.spawn |
| stop | Stops the server. | | None | stop | server.stop |
| talent | Sets talent level for your currently selected character | | Online Player | talent \<talentID> \<value> | player.settalent |
| team | Add, remove, or swap avatars in your current team. Index start from 1. | | Online Player | team \<add\|remove\|set> [avatarId,...] [index|first|last|index-index,...] | player.team |
| teleport | Change the player's position. | tp | Online Player | teleport \<x> \<y> \<z> [sceneId] | player.teleport |
| tpall | Teleports all players in your world to your position. | | Online Player | tpall | player.tpall |
| unlocktower | Unlock the all floors of abyss. | ut | Online Player | ut | player.tower |
| weather | Changes the weather. | w | Online Player | weather \<weatherID> \<climateID> | player.weather |
### Bonus
......
......@@ -17,5 +17,13 @@ public @interface Command {
String permissionTargeted() default "";
public enum TargetRequirement {
NONE, // targetPlayer is not required
OFFLINE, // targetPlayer must be offline
PLAYER, // targetPlayer can be online or offline
ONLINE // targetPlayer must be online
}
TargetRequirement targetRequirement() default TargetRequirement.ONLINE;
boolean threading() default false;
}
......@@ -4,6 +4,7 @@ import emu.grasscutter.Grasscutter;
import emu.grasscutter.game.player.Player;
import emu.grasscutter.server.event.game.CommandResponseEvent;
import emu.grasscutter.server.event.types.ServerEvent;
import static emu.grasscutter.utils.Language.translate;
import java.util.List;
......@@ -25,6 +26,10 @@ public interface CommandHandler {
event.call();
}
static void sendTranslatedMessage(Player player, String messageKey, Object... args) {
sendMessage(player, translate(player, messageKey, args));
}
/**
* Called when a player/console invokes a command.
* @param sender The player/console that invoked the command.
......
......@@ -8,8 +8,6 @@ import org.reflections.Reflections;
import java.util.*;
import static emu.grasscutter.utils.Language.translate;
@SuppressWarnings({"UnusedReturnValue", "unused"})
public final class CommandMap {
private final Map<String, CommandHandler> commands = new HashMap<>();
......@@ -117,7 +115,7 @@ public final class CommandMap {
public void invoke(Player player, Player targetPlayer, String rawMessage) {
rawMessage = rawMessage.trim();
if (rawMessage.length() == 0) {
CommandHandler.sendMessage(player, translate("commands.generic.not_specified"));
CommandHandler.sendTranslatedMessage(player, "commands.generic.not_specified");
return;
}
......@@ -144,19 +142,20 @@ public final class CommandMap {
if (targetUidStr != null) {
if (targetUidStr.equals("")) { // Clears the default targetPlayer.
targetPlayerIds.remove(playerId);
CommandHandler.sendMessage(player, translate("commands.execution.clear_target"));
CommandHandler.sendTranslatedMessage(player, "commands.execution.clear_target");
} else { // Sets default targetPlayer to the UID provided.
try {
int uid = Integer.parseInt(targetUidStr);
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid);
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid, true);
if (targetPlayer == null) {
CommandHandler.sendMessage(player, translate("commands.execution.player_exist_offline_error"));
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
} else {
targetPlayerIds.put(playerId, uid);
CommandHandler.sendMessage(player, translate("commands.execution.set_target", targetUidStr));
CommandHandler.sendTranslatedMessage(player, "commands.execution.set_target", targetUidStr);
CommandHandler.sendTranslatedMessage(player, targetPlayer.isOnline()? "commands.execution.set_target_online" : "commands.execution.set_target_offline", targetUidStr);
}
} catch (NumberFormatException e) {
CommandHandler.sendMessage(player, translate("commands.execution.uid_error"));
CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error");
}
}
return;
......@@ -165,7 +164,7 @@ public final class CommandMap {
// Get command handler.
CommandHandler handler = this.commands.get(label);
if (handler == null) {
CommandHandler.sendMessage(player, translate("commands.generic.unknown_command", label));
CommandHandler.sendTranslatedMessage(player, "commands.generic.unknown_command", label);
return;
}
......@@ -176,14 +175,14 @@ public final class CommandMap {
arg = args.remove(i).substring(1);
try {
int uid = Integer.parseInt(arg);
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid);
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(uid, true);
if (targetPlayer == null) {
CommandHandler.sendMessage(player, translate("commands.execution.player_exist_offline_error"));
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
return;
}
break;
} catch (NumberFormatException e) {
CommandHandler.sendMessage(player, translate("commands.execution.uid_error"));
CommandHandler.sendTranslatedMessage(player, "commands.execution.uid_error");
return;
}
}
......@@ -192,9 +191,9 @@ public final class CommandMap {
// If there's still no targetPlayer at this point, use previously-set target
if (targetPlayer == null) {
if (targetPlayerIds.containsKey(playerId)) {
targetPlayer = Grasscutter.getGameServer().getPlayerByUid(targetPlayerIds.get(playerId)); // We check every time in case the target goes offline after being targeted
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.sendMessage(player, translate("commands.execution.player_exist_offline_error"));
CommandHandler.sendTranslatedMessage(player, "commands.execution.player_exist_error");
return;
}
} else {
......@@ -210,12 +209,29 @@ public final class CommandMap {
Account account = player.getAccount();
if (player != targetPlayer) { // Additional permission required for targeting another player
if (!permissionNodeTargeted.isEmpty() && !account.hasPermission(permissionNodeTargeted)) {
CommandHandler.sendMessage(player, translate("commands.generic.permission_error"));
CommandHandler.sendTranslatedMessage(player, "commands.generic.permission_error");
return;
}
}
if (!permissionNode.isEmpty() && !account.hasPermission(permissionNode)) {
CommandHandler.sendMessage(player, translate("commands.generic.permission_error"));
CommandHandler.sendTranslatedMessage(player, "commands.generic.permission_error");
return;
}
}
// Check if command has unfulfilled constraints on targetPlayer
Command.TargetRequirement targetRequirement = this.annotations.get(label).targetRequirement();
if (targetRequirement != Command.TargetRequirement.NONE) {
if (targetPlayer == null) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target");
return;
}
if ((targetRequirement == Command.TargetRequirement.ONLINE) && !targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_online");
return;
}
if ((targetRequirement == Command.TargetRequirement.OFFLINE) && targetPlayer.isOnline()) {
CommandHandler.sendTranslatedMessage(player, "commands.execution.need_target_offline");
return;
}
}
......
......@@ -11,7 +11,7 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "account", usage = "account <create|delete> <username> [uid]", description = "commands.account.description")
@Command(label = "account", usage = "account <create|delete> <username> [uid]", description = "commands.account.description", targetRequirement = Command.TargetRequirement.NONE)
public final class AccountCommand implements CommandHandler {
@Override
......
......@@ -9,7 +9,7 @@ import java.util.List;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "broadcast", usage = "broadcast <message>", aliases = {"b"}, permission = "server.broadcast", description = "commands.broadcast.description")
@Command(label = "broadcast", usage = "broadcast <message>", aliases = {"b"}, permission = "server.broadcast", description = "commands.broadcast.description", targetRequirement = Command.TargetRequirement.NONE)
public final class BroadcastCommand implements CommandHandler {
@Override
......
......@@ -13,11 +13,6 @@ public final class ChangeSceneCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
if (args.size() != 1) {
CommandHandler.sendMessage(sender, translate(sender, "commands.changescene.usage"));
return;
......
......@@ -20,10 +20,6 @@ public final class ClearCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
if (args.size() < 1) {
CommandHandler.sendMessage(sender, translate(sender, "commands.clear.command_usage"));
return;
......
......@@ -14,11 +14,6 @@ public final class CoopCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
Player host = sender;
switch (args.size()) {
case 0: // Summon target to self
......
......@@ -18,11 +18,6 @@ public final class DropCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(null, translate(sender, "commands.execution.need_target"));
return;
}
int item = 0;
int amount = 1;
......
......@@ -13,11 +13,6 @@ public final class EnterDungeonCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(null, translate(sender, "commands.execution.need_target"));
return;
}
if (args.size() < 1) {
CommandHandler.sendMessage(sender, translate(sender, "commands.enter_dungeon.usage"));
return;
......
......@@ -20,10 +20,6 @@ public final class GiveAllCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
int amount = 99999;
switch (args.size()) {
......
......@@ -113,11 +113,7 @@ public final class GiveArtifactCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
// Sanity checks
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
// Sanity check
if (args.size() < 2) {
CommandHandler.sendMessage(sender, translate(sender, "commands.giveArtifact.usage"));
return;
......
......@@ -17,11 +17,6 @@ public final class GiveCharCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
int avatarId;
int level = 1;
......
......@@ -33,10 +33,6 @@ public final class GiveCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
int item;
int lvl = 1;
int amount = 1;
......
......@@ -13,11 +13,6 @@ public final class GodModeCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
boolean enabled = !targetPlayer.inGodmode();
if (args.size() == 1) {
switch (args.get(0).toLowerCase()) {
......
......@@ -16,11 +16,6 @@ public final class HealCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
targetPlayer.getTeamManager().getActiveTeam().forEach(entity -> {
boolean isAlive = entity.isAlive();
entity.setFightProperty(
......
......@@ -10,7 +10,7 @@ import java.util.*;
import static emu.grasscutter.utils.Language.translate;
@Command(label = "help", usage = "help [command]", description = "commands.help.description")
@Command(label = "help", usage = "help [command]", description = "commands.help.description", targetRequirement = Command.TargetRequirement.NONE)
public final class HelpCommand implements CommandHandler {
@Override
......
......@@ -13,11 +13,6 @@ public final class KickCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
if (sender != null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.kick.player_kick_player",
Integer.toString(sender.getAccount().getPlayerUid()), sender.getAccount().getUsername(),
......
......@@ -17,11 +17,6 @@ public final class KillAllCommand implements CommandHandler {
@Override
public void execute(Player sender, Player targetPlayer, List<String> args) {
if (targetPlayer == null) {
CommandHandler.sendMessage(sender, translate(sender, "commands.execution.need_target"));
return;
}
Scene scene = targetPlayer.getScene();
try {
switch (args.size()) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment