Skip to content

Commit

Permalink
Merge pull request #307 from derhass/for_roncli/chat_command_player_s…
Browse files Browse the repository at this point in the history
…election

Improve chat commands, especially player selection and tracker update on /EXTEND.
  • Loading branch information
roncli committed May 17, 2023
2 parents 8051c70 + 36e825e commit 252e61a
Show file tree
Hide file tree
Showing 3 changed files with 137 additions and 27 deletions.
145 changes: 122 additions & 23 deletions GameMod/MPChatCommands.cs
Expand Up @@ -131,6 +131,7 @@ public enum Command {
Say,
Test,
SwitchTeam,
ListPlayers,
}

// properties:
Expand Down Expand Up @@ -243,14 +244,16 @@ public static bool IsEnabled()
} else if (cmdName == "EX" || cmdName == "EXTEND") {
cmd = Command.Extend;
needAuth = true;
} else if (cmdName == "STATUS") {
} else if (cmdName == "I" || cmdName == "INFO" || cmdName == "STATUS") {
cmd = Command.Status;
} else if (cmdName == "SAY" || cmdName == "CHAT") {
cmd = Command.Say;
} else if (cmdName == "T" || cmdName == "TEST") {
cmd = Command.Test;
} else if (cmdName == "ST" || cmdName == "SWITCHTEAM") {
cmd = Command.SwitchTeam;
} else if (cmdName == "LP" || cmdName == "LISTPLAYERS") {
cmd = Command.ListPlayers;
}
}

Expand Down Expand Up @@ -343,6 +346,9 @@ public static bool IsEnabled()
case Command.SwitchTeam:
result = DoSwitchTeam();
break;
case Command.ListPlayers:
result = DoListPlayers();
break;
default:
Debug.LogFormat("CHATCMD {0}: {1} {2} was not handled by server", cmd, cmdName, arg);
result = true; // treat it as normal chat message
Expand Down Expand Up @@ -663,8 +669,8 @@ public bool DoStartExtend(bool start)
matchTimeOutInfo.SetValue(hai, offset);
matchStartInfo.SetValue(hai, now);

ReturnTo(String.Format("manual {0} request by {1}: {2} seconds",op,senderEntry.name,offset.TotalSeconds));
//ReturnToSender(String.Format("Server's timeout is now {0}",startTime));
ReturnTo(String.Format("manual {0} request by {1}: {2} seconds",op,senderEntry.name,seconds));
ServerLobbyStatus.SendToTracker(); // update the tracker
} else {
Debug.LogFormat("{0} request via chat command ignored: no HostActiveMatchInfo",op);
ReturnToSender(String.Format("{0} rejected: no HostActiveMatchInfo",op));
Expand Down Expand Up @@ -763,6 +769,45 @@ public bool DoSwitchTeam()
return false;
}

// Execute LISTPLAYERS command
public bool DoListPlayers()
{
int argIdLow = -1;
int argIdHigh = -1;
int cnt = 0;

if (!String.IsNullOrEmpty(arg)) {
string[] parts = arg.Split(' ');
if (parts.Length > 0) {
if (!int.TryParse(parts[0], NumberStyles.Number, CultureInfo.InvariantCulture, out argIdLow)) {
argIdLow = -1;
}
}
if (parts.Length > 1) {
if (!int.TryParse(parts[1], NumberStyles.Number, CultureInfo.InvariantCulture, out argIdHigh)) {
argIdHigh = -1;
}
}
if (argIdHigh < 0) {
argIdHigh = argIdLow;
}
}

for (int i = 0; i < NetworkServer.connections.Count; i++) {
if (argIdLow < 0 || (i >= argIdLow && i <= argIdHigh)) {
string name = FindPlayerNameForConnection(i, inLobby);
if (!String.IsNullOrEmpty(name)) {
ReturnToSender(String.Format("{0}: {1}",i,name));
cnt++;
}
}
}
if (cnt < 1) {
ReturnToSender("LISTPLAYERS: no players found");
}
return false;
}

// trim down the fields in candidate so that it doesn't alias authenticated
// Return values is the same as MPBanEntry.trim
private int TrimBanCandidate(ref MPBanEntry candidate, MPBanEntry authenticated) {
Expand Down Expand Up @@ -997,6 +1042,44 @@ public int MatchPlayerName(string name, string pattern)
return 0;
}

// helper class for parsing player selection data from a user-specified argument string
private class PlayerSelector {
public string pattern = null;
public string namePattern = null;
public int connectionId = -1;
public bool valid = false;

public PlayerSelector(string thePattern) {
valid = false;
if (!String.IsNullOrEmpty(thePattern)) {
pattern = thePattern.ToUpper().Trim();
int idx = pattern.IndexOf("C:");
if (idx == 0) {
idx += 2;
} else {
idx = pattern.IndexOf("CONN:");
if (idx == 0) {
idx += 5;
}
}
if (idx >= 0) {
if (idx < pattern.Length) {
string num = pattern.Substring(idx);
if (!int.TryParse(num, NumberStyles.Number, CultureInfo.InvariantCulture, out connectionId)) {
connectionId = -1;
}
if (connectionId >= 0) {
valid = true;
}
}
} else {
namePattern = pattern;
valid = !String.IsNullOrEmpty(namePattern);
}
}
}
}

// Find the best match for a player
// Search the active players in game
// May return null if no match can be found
Expand All @@ -1007,16 +1090,24 @@ public int MatchPlayerName(string name, string pattern)

int bestScore = -1000000000;
Player bestPlayer = null;
pattern = pattern.ToUpper();

foreach (var p in Overload.NetworkManager.m_Players) {
int score = MatchPlayerName(p.m_mp_name.ToUpper(), pattern);
if (score > 0) {
return p;
}
if (score < 0 && score > bestScore) {
bestScore = score;
bestPlayer = p;
PlayerSelector s = new PlayerSelector(pattern);
if (s.valid) {
foreach (var p in Overload.NetworkManager.m_Players) {
int score = -1000000000;
if (s.connectionId >= 0) {
if (p.connectionToClient.connectionId == s.connectionId) {
score = 1;
}
} else if (!String.IsNullOrEmpty(s.namePattern)) {
score = MatchPlayerName(p.m_mp_name.ToUpper(), s.namePattern);
}
if (score > 0) {
return p;
}
if (score < 0 && score > bestScore) {
bestScore = score;
bestPlayer = p;
}
}
}
if (bestPlayer == null) {
Expand All @@ -1035,16 +1126,24 @@ public int MatchPlayerName(string name, string pattern)

int bestScore = -1000000000;
PlayerLobbyData bestPlayer = null;
pattern = pattern.ToUpper();

foreach (KeyValuePair<int, PlayerLobbyData> p in NetworkMatch.m_players) {
int score = MatchPlayerName(p.Value.m_name.ToUpper(), pattern);
if (score > 0) {
return p.Value;
}
if (score < 0 && score > bestScore) {
bestScore = score;
bestPlayer = p.Value;
PlayerSelector s = new PlayerSelector(pattern);
if (s.valid) {
foreach (KeyValuePair<int, PlayerLobbyData> p in NetworkMatch.m_players) {
int score = -1000000000;
if (s.connectionId >= 0) {
if (p.Value.m_id == s.connectionId) {
score = 1;
}
} else if (!String.IsNullOrEmpty(s.namePattern)) {
score = MatchPlayerName(p.Value.m_name.ToUpper(), s.namePattern);
}
if (score > 0) {
return p.Value;
}
if (score < 0 && score > bestScore) {
bestScore = score;
bestPlayer = p.Value;
}
}
}
if (bestPlayer == null) {
Expand Down
7 changes: 6 additions & 1 deletion GameMod/ServerTrackerPost.cs
Expand Up @@ -43,7 +43,7 @@ public static IEnumerator PingRoutine()
[HarmonyPatch(typeof(Server), "SendPlayersInLobbyToAllClients")]
class ServerLobbyStatus
{
public static void Postfix()
public static void SendToTracker()
{
MatchState state = NetworkMatch.GetMatchState();
if (state != MatchState.LOBBY && state != MatchState.LOBBY_LOADING_SCENE && state != MatchState.LOBBY_LOAD_COUNTDOWN)
Expand All @@ -55,5 +55,10 @@ public static void Postfix()

ServerStatLog.TrackerPostStats(obj);
}

public static void Postfix()
{
SendToTracker();
}
}
}
12 changes: 9 additions & 3 deletions ServerCommands.md
Expand Up @@ -20,14 +20,20 @@ see [the section on privilege management below](#privilege-management) for detai
* `/GIVEPERM <player>`: grant a player the chat command permissions.
* `/REVOKEPERM [<player>]`: revoke chat command permissions from a player or all players.
* `/AUTH password`: a server operator can also start the server with the commandline argument `-chatCommandPassword serverPassword`. Any Player knowing this password can get to authenticated state with this command. If no `serverPassword` is set, this command always fails. Note that the password check is **not** case-sensitive.
* `/STATUS`: short info about chat command and ban status. No permission required for this command.
* `/STATUS` or `/INFO`: short info about chat command and ban status. No permission required for this command.
* `/SAY`: send a message to all players which are not blocked for chat
* `/TEST <player>`: Test player name selection. No permission required for this command.
* `/SWITCHTEAM [<player>]`: Switch the team a player is in. If used without an argument, it affects the player sending this command. Switching teams for players other than yourself requires chat command permissions.
* `/LISTPLAYERS [connectionId1 [connectionId2]]`: List players by their connection ID. If no arguments are given, all players are listed. If a single argument is given, it is treated as a connection ID and only
the player on that ID is queried. If two arguments are given (separated by a single space character), they are treated as a range of connection IDs. Since the chat history shows at most 8 entries, you can split it into multiple queries that way.

### Player Name Matching
### Player Selection and Name Matching

Player names are matches the `<player>` pattern as follows:
The argument `<player>` may either be a string pattern to match for a player name, or a connection ID, when the prefix `CONN:` or `C:` is given (like `CONN:2`, use `/LISTPLAYERS` command to get the IDs).
You can always use the `/TEST` command to find out which player a specific `<player>` argument would select. The selection by connection ID is useful when there are player names with characters which
can't be typed in your current language.

Player names are matched to the `<player>` pattern as follows:
* If the name matches completely, that player is selected
* If only one player name contains the pattern, that player is selected.
* If several player names contain the pattern:
Expand Down

0 comments on commit 252e61a

Please sign in to comment.