using Barotrauma.Extensions; using Barotrauma.IO; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; namespace Barotrauma.Networking { partial class ServerSettings { partial class NetPropertyData { private object lastSyncedValue; public UInt16 LastUpdateID { get; private set; } public void SyncValue() { if (!PropEquals(lastSyncedValue, Value)) { LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID; lastSyncedValue = Value; } } public void ForceUpdate() { LastUpdateID = GameMain.NetLobbyScreen.LastUpdateID++; } } public static readonly string ClientPermissionsFile = "Data" + Path.DirectorySeparatorChar + "clientpermissions.xml"; public static readonly char SubmarineSeparatorChar = '|'; public readonly Dictionary LastUpdateIdForFlag = ((NetFlags[])Enum.GetValues(typeof(NetFlags))) .Select(f => (f, (ushort)1)) .ToDictionary(); public void UpdateFlag(NetFlags flag) => LastUpdateIdForFlag[flag] = (UInt16)(GameMain.NetLobbyScreen.LastUpdateID + 1); public NetFlags UnsentFlags() => LastUpdateIdForFlag.Keys .Where(k => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[k], GameMain.NetLobbyScreen.LastUpdateID)) .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); private bool IsFlagRequired(Client c, NetFlags flag) => NetIdUtils.IdMoreRecent(LastUpdateIdForFlag[flag], c.LastRecvLobbyUpdate); public NetFlags GetRequiredFlags(Client c) => LastUpdateIdForFlag.Keys .Where(k => IsFlagRequired(c, k)) .Aggregate(NetFlags.None, (f1, f2) => f1 | f2); partial void InitProjSpecific() { LoadSettings(); LoadClientPermissions(); } public void ForcePropertyUpdate() { UpdateFlag(NetFlags.Properties); foreach (NetPropertyData property in netProperties.Values) { property.ForceUpdate(); } } private void WriteNetProperties(IWriteMessage outMsg, Client c) { foreach (UInt32 key in netProperties.Keys) { var property = netProperties[key]; property.SyncValue(); if (NetIdUtils.IdMoreRecent(property.LastUpdateID, c.LastRecvLobbyUpdate)) { outMsg.WriteUInt32(key); netProperties[key].Write(outMsg); } } outMsg.WriteUInt32((UInt32)0); } public void ServerAdminWrite(IWriteMessage outMsg, Client c) { c.LastSentServerSettingsUpdate = LastUpdateIdForFlag[NetFlags.Properties]; WriteNetProperties(outMsg, c); WriteMonsterEnabled(outMsg); BanList.ServerAdminWrite(outMsg, c); } public void ServerWrite(IWriteMessage outMsg, Client c) { NetFlags requiredFlags = GetRequiredFlags(c); outMsg.WriteByte((byte)requiredFlags); if (requiredFlags.HasFlag(NetFlags.Name)) { outMsg.WriteString(ServerName); } if (requiredFlags.HasFlag(NetFlags.Message)) { outMsg.WriteString(ServerMessageText); } outMsg.WriteByte((byte)PlayStyle); outMsg.WriteByte((byte)MaxPlayers); outMsg.WriteBoolean(HasPassword); outMsg.WriteBoolean(IsPublic); outMsg.WriteBoolean(AllowFileTransfers); outMsg.WritePadBits(); outMsg.WriteRangedInteger(TickRate, 1, 60); if (requiredFlags.HasFlag(NetFlags.Properties)) { WriteExtraCargo(outMsg); } if (requiredFlags.HasFlag(NetFlags.HiddenSubs)) { WriteHiddenSubs(outMsg); } if (c.HasPermission(Networking.ClientPermissions.ManageSettings) && NetIdUtils.IdMoreRecent( newID: LastUpdateIdForFlag[NetFlags.Properties], oldID: c.LastRecvServerSettingsUpdate)) { outMsg.WriteBoolean(true); outMsg.WritePadBits(); ServerAdminWrite(outMsg, c); } else { outMsg.WriteBoolean(false); outMsg.WritePadBits(); } } public void ServerRead(IReadMessage incMsg, Client c) { if (!c.HasPermission(Networking.ClientPermissions.ManageSettings)) return; NetFlags flags = (NetFlags)incMsg.ReadByte(); bool changed = false; if (flags.HasFlag(NetFlags.Name)) { string serverName = incMsg.ReadString(); if (ServerName != serverName) { changed = true; } ServerName = serverName; } if (flags.HasFlag(NetFlags.Message)) { string serverMessageText = incMsg.ReadString(); if (ServerMessageText != serverMessageText) { changed = true; } ServerMessageText = serverMessageText; } if (flags.HasFlag(NetFlags.Properties)) { bool propertiesChanged = ReadExtraCargo(incMsg); UInt32 count = incMsg.ReadUInt32(); for (int i = 0; i < count; i++) { UInt32 key = incMsg.ReadUInt32(); if (netProperties.ContainsKey(key)) { object prevValue = netProperties[key].Value; netProperties[key].Read(incMsg); if (!netProperties[key].PropEquals(prevValue, netProperties[key])) { GameServer.Log(GameServer.ClientLogName(c) + " changed " + netProperties[key].Name + " to " + netProperties[key].Value.ToString(), ServerLog.MessageType.ServerMessage); } propertiesChanged = true; } else { UInt32 size = incMsg.ReadVariableUInt32(); incMsg.BitPosition += (int)(8 * size); } } bool changedMonsterSettings = incMsg.ReadBoolean(); incMsg.ReadPadBits(); propertiesChanged |= changedMonsterSettings; if (changedMonsterSettings) { ReadMonsterEnabled(incMsg); } propertiesChanged |= BanList.ServerAdminRead(incMsg, c); if (propertiesChanged) { UpdateFlag(NetFlags.Properties); } changed |= propertiesChanged; } if (flags.HasFlag(NetFlags.HiddenSubs)) { ReadHiddenSubs(incMsg); changed |= true; UpdateFlag(NetFlags.HiddenSubs); } if (flags.HasFlag(NetFlags.Misc)) { int orBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; int andBits = incMsg.ReadRangedInteger(0, (int)Barotrauma.MissionType.All) & (int)Barotrauma.MissionType.All; GameMain.NetLobbyScreen.MissionType = (MissionType)(((int)GameMain.NetLobbyScreen.MissionType | orBits) & andBits); int traitorSetting = (int)TraitorsEnabled + incMsg.ReadByte() - 1; if (traitorSetting < 0) { traitorSetting = 2; } if (traitorSetting > 2) { traitorSetting = 0; } TraitorsEnabled = (YesNoMaybe)traitorSetting; int botCount = BotCount + incMsg.ReadByte() - 1; if (botCount < 0) { botCount = MaxBotCount; } if (botCount > MaxBotCount) { botCount = 0; } BotCount = botCount; int botSpawnMode = (int)BotSpawnMode + incMsg.ReadByte() - 1; if (botSpawnMode < 0) { botSpawnMode = 1; } if (botSpawnMode > 1) { botSpawnMode = 0; } BotSpawnMode = (BotSpawnMode)botSpawnMode; float levelDifficulty = incMsg.ReadSingle(); if (levelDifficulty >= 0.0f) { SelectedLevelDifficulty = levelDifficulty; } bool changedUseRespawnShuttle = incMsg.ReadBoolean(); bool useRespawnShuttle = incMsg.ReadBoolean(); if (changedUseRespawnShuttle) { UseRespawnShuttle = useRespawnShuttle; } bool changedAutoRestart = incMsg.ReadBoolean(); bool autoRestart = incMsg.ReadBoolean(); if (changedAutoRestart) { AutoRestart = autoRestart; } changed |= true; UpdateFlag(NetFlags.Misc); } if (flags.HasFlag(NetFlags.LevelSeed)) { GameMain.NetLobbyScreen.LevelSeed = incMsg.ReadString(); changed |= true; UpdateFlag(NetFlags.LevelSeed); } if (changed) { if (KarmaPreset == "custom") { GameMain.NetworkMember?.KarmaManager?.SaveCustomPreset(); GameMain.NetworkMember?.KarmaManager?.Save(); } SaveSettings(); GameMain.NetLobbyScreen.LastUpdateID++; } } public void SaveSettings() { XDocument doc = new XDocument(new XElement("serversettings")); doc.Root.SetAttributeValue("name", ServerName); doc.Root.SetAttributeValue("public", IsPublic); doc.Root.SetAttributeValue("port", Port); #if USE_STEAM doc.Root.SetAttributeValue("queryport", QueryPort); #endif doc.Root.SetAttributeValue("password", password ?? ""); doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); doc.Root.SetAttributeValue("LevelDifficulty", ((int)selectedLevelDifficulty).ToString()); doc.Root.SetAttributeValue("ServerMessage", ServerMessageText); doc.Root.SetAttributeValue("HiddenSubs", string.Join(",", HiddenSubs)); doc.Root.SetAttributeValue("AllowedRandomMissionTypes", string.Join(",", AllowedRandomMissionTypes)); doc.Root.SetAttributeValue("AllowedClientNameChars", string.Join(",", AllowedClientNameChars.Select(c => $"{c.Start}-{c.End}"))); SerializableProperty.SerializeProperties(this, doc.Root, true); doc.Root.Add(CampaignSettings.Save()); System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings { Indent = true, NewLineOnAttributes = true }; using (var writer = XmlWriter.Create(SettingsFile, settings)) { doc.SaveSafe(writer); } if (KarmaPreset == "custom") { GameMain.Server?.KarmaManager?.SaveCustomPreset(); } GameMain.Server?.KarmaManager?.Save(); } private void LoadSettings() { XDocument doc = null; if (File.Exists(SettingsFile)) { doc = XMLExtensions.TryLoadXml(SettingsFile); } if (doc == null) { doc = new XDocument(new XElement("serversettings")); } SerializableProperties = SerializableProperty.DeserializeProperties(this, doc.Root); if (string.IsNullOrEmpty(doc.Root.GetAttributeString("losmode", ""))) { LosMode = GameSettings.CurrentConfig.Graphics.LosMode; } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); AllowSubVoting = SubSelectionMode == SelectionMode.Vote; AllowModeVoting = ModeSelectionMode == SelectionMode.Vote; selectedLevelDifficulty = doc.Root.GetAttributeFloat("LevelDifficulty", 20.0f); GameMain.NetLobbyScreen.SetLevelDifficulty(selectedLevelDifficulty); GameMain.NetLobbyScreen.SetTraitorsEnabled(traitorsEnabled); HiddenSubs.UnionWith(doc.Root.GetAttributeStringArray("HiddenSubs", Array.Empty())); if (HiddenSubs.Any()) { UpdateFlag(NetFlags.HiddenSubs); } SelectedSubmarine = SelectNonHiddenSubmarine(SelectedSubmarine); string[] defaultAllowedClientNameChars = new string[] { "32-33", "38-46", "48-57", "65-90", "91", "93", "95-122", "192-255", "384-591", "1024-1279", "19968-21327","21329-40959","13312-19903","131072-173791","173824-178207","178208-183983","63744-64255","194560-195103" //CJK }; string[] allowedClientNameCharsStr = doc.Root.GetAttributeStringArray("AllowedClientNameChars", defaultAllowedClientNameChars); if (doc.Root.GetAttributeString("AllowedClientNameChars", "") == "65-90,97-122,48-59") { allowedClientNameCharsStr = defaultAllowedClientNameChars; } foreach (string allowedClientNameCharRange in allowedClientNameCharsStr) { string[] splitRange = allowedClientNameCharRange.Split('-'); if (splitRange.Length == 0 || splitRange.Length > 2) { DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names."); continue; } int min = -1; if (!int.TryParse(splitRange[0], out min)) { DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names."); continue; } int max = min; if (splitRange.Length == 2) { if (!int.TryParse(splitRange[1], out max)) { DebugConsole.ThrowError("Error in server settings - " + allowedClientNameCharRange + " is not a valid range for characters allowed in client names."); continue; } } if (min > max) { //swap min and max (min, max) = (max, min); } if (min > -1 && max > -1) { AllowedClientNameChars.Add(new Range(min, max)); } } AllowedRandomMissionTypes = new List(); string[] allowedMissionTypeNames = doc.Root.GetAttributeStringArray( "AllowedRandomMissionTypes", Enum.GetValues(typeof(MissionType)).Cast().Select(m => m.ToString()).ToArray()); foreach (string missionTypeName in allowedMissionTypeNames) { if (Enum.TryParse(missionTypeName, out MissionType missionType)) { if (missionType == Barotrauma.MissionType.None) { continue; } if (MissionPrefab.HiddenMissionClasses.Contains(missionType)) { continue; } AllowedRandomMissionTypes.Add(missionType); } } ServerName = doc.Root.GetAttributeString("name", ""); if (ServerName.Length > NetConfig.ServerNameMaxLength) { ServerName = ServerName.Substring(0, NetConfig.ServerNameMaxLength); } ServerMessageText = doc.Root.GetAttributeString("ServerMessage", ""); GameMain.NetLobbyScreen.SelectedModeIdentifier = GameModeIdentifier; //handle Random as the mission type, which is no longer a valid setting //MissionType.All offers equivalent functionality if (MissionType == "Random") { MissionType = "All"; } GameMain.NetLobbyScreen.MissionTypeName = MissionType; GameMain.NetLobbyScreen.SetBotSpawnMode(BotSpawnMode); GameMain.NetLobbyScreen.SetBotCount(BotCount); MonsterEnabled ??= CharacterPrefab.Prefabs.Select(p => (p.Identifier, true)).ToDictionary(); foreach (XElement element in doc.Root.Elements()) { if (element.Name.ToIdentifier() == nameof(Barotrauma.CampaignSettings)) { CampaignSettings = new CampaignSettings(element); } } } public string SelectNonHiddenSubmarine(string current = null) { current ??= GameMain.NetLobbyScreen.SelectedSub.Name; if (HiddenSubs.Contains(current)) { var candidates = GameMain.NetLobbyScreen.GetSubList().Where(s => !HiddenSubs.Contains(s.Name)).ToArray(); if (candidates.Any()) { GameMain.NetLobbyScreen.SelectedSub = candidates.GetRandom(Rand.RandSync.Unsynced); return GameMain.NetLobbyScreen.SelectedSub.Name; } else { HiddenSubs.Remove(current); return current; } } return current; } public void LoadClientPermissions() { ClientPermissions.Clear(); if (!File.Exists(ClientPermissionsFile)) { return; } XDocument doc = XMLExtensions.TryLoadXml(ClientPermissionsFile); if (doc == null) { return; } foreach (XElement clientElement in doc.Root.Elements()) { string clientName = clientElement.GetAttributeString("name", ""); string addressStr = clientElement.GetAttributeString("address", null) ?? clientElement.GetAttributeString("endpoint", null) ?? clientElement.GetAttributeString("ip", ""); string accountIdStr = clientElement.GetAttributeString("accountid", null) ?? clientElement.GetAttributeString("steamid", ""); if (string.IsNullOrWhiteSpace(clientName)) { DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have a name."); continue; } if (string.IsNullOrWhiteSpace(addressStr) && string.IsNullOrWhiteSpace(accountIdStr)) { DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - all clients must have an endpoint or a Steam ID."); continue; } ClientPermissions permissions = Networking.ClientPermissions.None; HashSet permittedCommands = new HashSet(); if (clientElement.Attribute("preset") == null) { string permissionsStr = clientElement.GetAttributeString("permissions", ""); if (permissionsStr.Equals("all", StringComparison.OrdinalIgnoreCase)) { foreach (ClientPermissions permission in Enum.GetValues(typeof(ClientPermissions))) { permissions |= permission; } } else if (!Enum.TryParse(permissionsStr, out permissions)) { DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + permissionsStr + "\" is not a valid client permission."); continue; } if (permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) { foreach (XElement commandElement in clientElement.Elements()) { if (!commandElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = commandElement.GetAttributeString("name", ""); DebugConsole.Command command = DebugConsole.FindCommand(commandName); if (command == null) { command = new DebugConsole.Command(commandName, "", (_) => {}, null, true); } permittedCommands.Add(command); } } } else { string presetName = clientElement.GetAttributeString("preset", ""); PermissionPreset preset = PermissionPreset.List.Find(p => p.Name == presetName); if (preset == null) { DebugConsole.ThrowError("Failed to restore saved permissions to the client \"" + clientName + "\". Permission preset \"" + presetName + "\" not found."); return; } else { permissions = preset.Permissions; permittedCommands = preset.PermittedCommands.ToHashSet(); } } if (permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) { foreach (XElement commandElement in clientElement.Elements()) { if (!commandElement.Name.ToString().Equals("command", StringComparison.OrdinalIgnoreCase)) { continue; } string commandName = commandElement.GetAttributeString("name", ""); DebugConsole.Command command = DebugConsole.FindCommand(commandName); if (command == null) { DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + commandName + "\" is not a valid console command."); continue; } permittedCommands.Add(command); } } if (!string.IsNullOrEmpty(accountIdStr)) { if (AccountId.Parse(accountIdStr).TryUnwrap(out var accountId)) { ClientPermissions.Add(new SavedClientPermission(clientName, accountId, permissions, permittedCommands)); } else { DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + accountIdStr + "\" is not a valid account ID."); } } else { if (Address.Parse(addressStr).TryUnwrap(out var address)) { ClientPermissions.Add(new SavedClientPermission(clientName, address, permissions, permittedCommands)); } else { DebugConsole.ThrowError("Error in " + ClientPermissionsFile + " - \"" + addressStr + "\" is not a valid endpoint."); } } } } public void SaveClientPermissions() { GameServer.Log("Saving client permissions", ServerLog.MessageType.ServerMessage); XDocument doc = new XDocument(new XElement("ClientPermissions")); foreach (SavedClientPermission clientPermission in ClientPermissions) { var matchingPreset = PermissionPreset.List.Find(p => p.MatchesPermissions(clientPermission.Permissions, clientPermission.PermittedCommands)); #warning TODO: this is broken because of localization if (matchingPreset != null && matchingPreset.Name == "None") { continue; } XElement clientElement = new XElement("Client", new XAttribute("name", clientPermission.Name)); clientElement.Add(clientPermission.AddressOrAccountId.TryGet(out AccountId accountId) ? new XAttribute("accountid", accountId.StringRepresentation) : new XAttribute("address", ((Address)clientPermission.AddressOrAccountId).StringRepresentation)); clientElement.Add(matchingPreset == null ? new XAttribute("permissions", clientPermission.Permissions.ToString()) : new XAttribute("preset", matchingPreset.Name)); if (clientPermission.Permissions.HasFlag(Networking.ClientPermissions.ConsoleCommands)) { foreach (DebugConsole.Command command in clientPermission.PermittedCommands) { clientElement.Add(new XElement("command", new XAttribute("name", command.names[0]))); } } doc.Root.Add(clientElement); } try { System.Xml.XmlWriterSettings settings = new System.Xml.XmlWriterSettings(); settings.Indent = true; settings.NewLineOnAttributes = true; using (var writer = XmlWriter.Create(ClientPermissionsFile, settings)) { doc.SaveSafe(writer); } } catch (Exception e) { DebugConsole.ThrowError("Saving client permissions to " + ClientPermissionsFile + " failed", e); } } } }