using Barotrauma.Extensions; using Barotrauma.IO; using System; using System.Collections.Generic; using System.Linq; using System.Xml.Linq; using Barotrauma.Steam; 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); 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 (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.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( NetworkMember.ClientLogName(c) + $" changed {netProperties[key].Name}" + $" to {netProperties[key].Value}", 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); //the byte indicates the direction we're changing the value, subtract one to get negative values from a byte TraitorDangerLevel = TraitorDangerLevel + incMsg.ReadByte() - 1; 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("port", Port); if (QueryPort != 0) { doc.Root.SetAttributeValue("queryport", QueryPort); } doc.Root.SetAttributeValue("password", password ?? ""); doc.Root.SetAttributeValue("enableupnp", EnableUPnP); doc.Root.SetAttributeValue("autorestart", autoRestart); 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()); doc.Root.SetAttributeValue("DisabledMonsters", string.Join(",", MonsterEnabled.Where(kvp => !kvp.Value).Select(kvp => kvp.Key.Value))); 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; } if (string.IsNullOrEmpty(doc.Root.GetAttributeString("language", ""))) { Language = ServerLanguageOptions.PickLanguage(GameSettings.CurrentConfig.Language); } AutoRestart = doc.Root.GetAttributeBool("autorestart", false); AllowSubVoting = SubSelectionMode == SelectionMode.Vote; AllowModeVoting = ModeSelectionMode == SelectionMode.Vote; GameMain.NetLobbyScreen.SetTraitorProbability(traitorProbability); 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", "4352-4607", //Hangul Jamo "44032-55215", //Hangul Syllables "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(); var disabledMonsters = doc.Root.GetAttributeIdentifierArray("DisabledMonsters", Array.Empty()); foreach (var disabledMonster in disabledMonsters) { if (MonsterEnabled.ContainsKey(disabledMonster)) { MonsterEnabled[disabledMonster] = false; } } 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; } } else { string presetName = clientElement.GetAttributeString("preset", ""); PermissionPreset preset = PermissionPreset.List.Find(p => p.DisplayName == 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)); if (matchingPreset != null && matchingPreset.Identifier == "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.DisplayName)); 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); } } } }