Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/luponix/olmod
Browse files Browse the repository at this point in the history
  • Loading branch information
luponix committed Jul 30, 2023
2 parents 1c8eaa3 + 8557acf commit 9c920f4
Show file tree
Hide file tree
Showing 5 changed files with 241 additions and 27 deletions.
1 change: 1 addition & 0 deletions GameMod/GameMod.csproj
Expand Up @@ -206,6 +206,7 @@
<Compile Include="MPTeamsEnemyArrow.cs" />
<Compile Include="MPTweaks.cs" />
<Compile Include="MPUnlockAll.cs" />
<Compile Include="MPValidatePlayerNames.cs" />
<Compile Include="MPWeaponBehavior.cs" />
<Compile Include="MPWeaponCycling.cs" />
<Compile Include="MusicCustom.cs" />
Expand Down
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
103 changes: 103 additions & 0 deletions GameMod/MPValidatePlayerNames.cs
@@ -0,0 +1,103 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using HarmonyLib;
using Overload;

namespace GameMod {
/// <summary>
/// Prevents players to use invalid / empty names
/// - spaces are allowed, but not at the beginning and end, and not exclusively
/// - other sorts of whitespaces are converted to space
/// - Non-ASCII characters are still allowed
/// - Only Uppercase characters are allowed, convert to uppercase in any case
/// - forbidden characters (as per IsValidPilotNameChar) and characters not
/// in the font are replaced by a $ sign
/// - At least one non-forbidden character must be present
/// - maximum length is 17 characters, longer names are cut
/// </summary>
[HarmonyPatch(typeof(Overload.Server), "ResolvePotentialNameCollision")]
class MPValidatePlayerNames {
// Helper function: get the player name without the "<X>" number suffix, and the number
// The suffix can be one or two digits.
private static string GetBaseName(string name, out int number)
{
number = 0;
if (String.IsNullOrEmpty(name)) {
return "";
}
if (name.EndsWith(">")) {
int idxStart = name.LastIndexOf(" <");
if (idxStart > 0) {
int len = name.Length - idxStart - 3;
if (len >= 1 && len <= 2) {
string potentialNumber = name.Substring(idxStart+2, len);
if (int.TryParse(potentialNumber, NumberStyles.Number, CultureInfo.InvariantCulture, out number)) {
if (number > 0) {
name = name.Substring(0, idxStart);
} else {
number = 0;
}
}
}
}
}
return name;
}

// completely replace ResolvePotentialNameCollision() by a fixed and improved version
public static bool Prefix(PlayerLobbyData pld) {
if (pld != null) {
// Step 1: validate name
bool nameValid = false;
if (!String.IsNullOrEmpty(pld.m_name)) {
pld.m_name = pld.m_name.ToUpper(); // only uppercase
pld.m_name = pld.m_name.Trim(); // replace any leading or trailing whitespaces
if (pld.m_name.Length > 17) { // at most 17 characters
pld.m_name = pld.m_name.Substring(0,17);
}
for (int i=0; i<pld.m_name.Length; i++) {
Char ch = pld.m_name[i];
if ((Char.IsWhiteSpace(ch) || Char.IsSeparator(ch)) && ch != ' ') {
// all whitespace and separator charachters which aren't space are converted to space
pld.m_name = pld.m_name.Replace(ch, ' ');
} else if (FontInfo.IsInCharset((int)ch) && PilotManager.IsValidPilotNameChar(ch)) {
nameValid = true;
} else {
// replace invalid characters by '$'
pld.m_name = pld.m_name.Replace(ch, '$');
}
}
}
if (!nameValid) {
pld.m_name = "<INVALID NAME>";
}

// Step 2: resolve any name collisions
// Picks the lowest free number for this name, beginning from 2
ulong collisionSet = 0;
foreach (KeyValuePair<int, PlayerLobbyData> player in NetworkMatch.m_players) {
PlayerLobbyData otherPlayerValue = player.Value;
if (pld != otherPlayerValue) {
int number;
string baseName = GetBaseName(otherPlayerValue.m_name, out number);
if (baseName == pld.m_name) {
// same base name as we, mark as used
collisionSet = collisionSet | (((ulong)1)<<(number&63));
}
}
}
if (collisionSet != 0) {
int idx;
for (idx=2; idx<64; idx++) {
if ( (collisionSet & (((ulong)1)<<idx)) == 0) {
break;
}
}
pld.m_name = String.Format("{0} <{1}>", pld.m_name, idx);
}
}
return false; // skip the original in every case
}
}
}
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 9c920f4

Please sign in to comment.