From 0ee058910159db2404631876e3460339716de2c1 Mon Sep 17 00:00:00 2001 From: MrOkiDoki <0mrokidoki@gmail.com> Date: Sun, 20 Aug 2023 14:53:28 +0300 Subject: [PATCH] Callbacks for session changes. --- BattleBitAPI/Common/Extentions/Extentions.cs | 6 + .../Networking/NetworkCommuncation.cs | 1 + BattleBitAPI/Server/GameServer.cs | 39 ++- BattleBitAPI/Server/Player.cs | 84 +++-- BattleBitAPI/Server/ServerListener.cs | 293 +++++++++++++----- 5 files changed, 305 insertions(+), 118 deletions(-) diff --git a/BattleBitAPI/Common/Extentions/Extentions.cs b/BattleBitAPI/Common/Extentions/Extentions.cs index 3ef94dd..ae5b7ec 100644 --- a/BattleBitAPI/Common/Extentions/Extentions.cs +++ b/BattleBitAPI/Common/Extentions/Extentions.cs @@ -25,6 +25,12 @@ namespace BattleBitAPI.Common.Extentions #endif } + public static void Replace(this Dictionary dic, TKey key, TValue value) + { + dic.Remove(key); + dic.Add(key, value); + } + public static void SafeClose(this TcpClient client) { try { client.Close(); } catch { } diff --git a/BattleBitAPI/Networking/NetworkCommuncation.cs b/BattleBitAPI/Networking/NetworkCommuncation.cs index 69f4304..f30cbe0 100644 --- a/BattleBitAPI/Networking/NetworkCommuncation.cs +++ b/BattleBitAPI/Networking/NetworkCommuncation.cs @@ -40,5 +40,6 @@ OnPlayerGivenUp = 70, OnPlayerRevivedAnother = 71, OnSquadPointsChanged = 72, + NotifyNewRoundID = 73, } } diff --git a/BattleBitAPI/Server/GameServer.cs b/BattleBitAPI/Server/GameServer.cs index e9c9916..ff45b59 100644 --- a/BattleBitAPI/Server/GameServer.cs +++ b/BattleBitAPI/Server/GameServer.cs @@ -17,7 +17,6 @@ namespace BattleBitAPI.Server public IPAddress GameIP => mInternal.GameIP; public int GamePort => mInternal.GamePort; - public TcpClient Socket => mInternal.Socket; public bool IsPasswordProtected => mInternal.IsPasswordProtected; public string ServerName => mInternal.ServerName; public string Gamemode => mInternal.Gamemode; @@ -29,6 +28,8 @@ namespace BattleBitAPI.Server public int MaxPlayerCount => mInternal.MaxPlayerCount; public string LoadingScreenText => mInternal.LoadingScreenText; public string ServerRulesText => mInternal.ServerRulesText; + public uint RoundIndex => mInternal.RoundIndex; + public long SessionID => mInternal.SessionID; public ServerSettings ServerSettings => mInternal.ServerSettings; public MapRotation MapRotation => mInternal.MapRotation; public GamemodeRotation GamemodeRotation => mInternal.GamemodeRotation; @@ -158,7 +159,7 @@ namespace BattleBitAPI.Server try { //Are we still connected on socket level? - if (!Socket.Connected) + if (mInternal.Socket == null || !mInternal.Socket.Connected) { mClose("Connection was terminated."); return; @@ -171,10 +172,10 @@ namespace BattleBitAPI.Server return; } - var networkStream = Socket.GetStream(); + var networkStream = mInternal.Socket.GetStream(); //Read network packages. - while (Socket.Available > 0) + while (mInternal.Socket.Available > 0) { this.mInternal.mLastPackageReceived = Extentions.TickCount; @@ -405,10 +406,6 @@ namespace BattleBitAPI.Server public virtual async Task OnTick() { - } - public virtual async Task OnReconnected() - { - } public virtual async Task OnDisconnected() { @@ -500,6 +497,10 @@ namespace BattleBitAPI.Server public virtual async Task OnRoundEnded() { + } + public virtual async Task OnSessionChanged(long oldSessionID, long newSessionID) + { + } // ---- Functions ---- @@ -553,10 +554,18 @@ namespace BattleBitAPI.Server { ExecuteCommand("endgame"); } - public void SayToChat(string msg) + public void SayToAllChat(string msg) { ExecuteCommand("say " + msg); } + public void SayToChat(string msg, ulong steamID) + { + ExecuteCommand("sayto " + steamID + " " + msg); + } + public void SayToChat(string msg, Player player) + { + SayToChat(msg, player.SteamID); + } public void StopServer() { @@ -897,7 +906,7 @@ namespace BattleBitAPI.Server } // ---- Static ---- - public static void SetInstance(GameServer server, Internal @internal) + internal static void SetInstance(GameServer server, Internal @internal) { server.mInternal = @internal; } @@ -908,6 +917,7 @@ namespace BattleBitAPI.Server // ---- Variables ---- public ulong ServerHash; public bool IsConnected; + public bool HasActiveConnectionSession; public IPAddress GameIP; public int GamePort; public TcpClient Socket; @@ -924,6 +934,8 @@ namespace BattleBitAPI.Server public int MaxPlayerCount; public string LoadingScreenText; public string ServerRulesText; + public uint RoundIndex; + public long SessionID; public ServerSettings ServerSettings; public MapRotation MapRotation; public GamemodeRotation GamemodeRotation; @@ -944,6 +956,7 @@ namespace BattleBitAPI.Server public long mLastPackageReceived; public long mLastPackageSent; public bool mWantsToCloseConnection; + public long mPreviousSessionID; public StringBuilder mBuilder; public Queue<(ulong steamID, PlayerModifications.mPlayerModifications)> mChangedModifications; @@ -1290,7 +1303,9 @@ namespace BattleBitAPI.Server int inQueuePlayers, int maxPlayers, string loadingScreenText, - string serverRulesText + string serverRulesText, + uint roundIndex, + long sessionID ) { this.ServerHash = ((ulong)port << 32) | (ulong)iP.ToUInt(); @@ -1311,6 +1326,8 @@ namespace BattleBitAPI.Server this.MaxPlayerCount = maxPlayers; this.LoadingScreenText = loadingScreenText; this.ServerRulesText = serverRulesText; + this.RoundIndex = roundIndex; + this.SessionID = sessionID; this.ServerSettings.Reset(); this._RoomSettings.Reset(); diff --git a/BattleBitAPI/Server/Player.cs b/BattleBitAPI/Server/Player.cs index 4bea9c5..880548f 100644 --- a/BattleBitAPI/Server/Player.cs +++ b/BattleBitAPI/Server/Player.cs @@ -58,7 +58,7 @@ namespace BattleBitAPI KickFromSquad(); else { - if(value.Team != this.Team) + if (value.Team != this.Team) ChangeTeam(value.Team); JoinSquad(value.Name); } @@ -66,6 +66,8 @@ namespace BattleBitAPI } public bool InSquad => mInternal.SquadName != Squads.NoSquad; public int PingMs => mInternal.PingMs; + public long CurrentSessionID => mInternal.SessionID; + public bool IsConnected => mInternal.SessionID != 0; public float HP { @@ -156,56 +158,78 @@ namespace BattleBitAPI public virtual async Task OnDisconnected() { + } + public virtual async Task OnSessionChanged(long oldSessionID, long newSessionID) + { + } // ---- Functions ---- public void Kick(string reason = "") { - GameServer.Kick(this, reason); + if (IsConnected) + GameServer.Kick(this, reason); } public void Kill() { - GameServer.Kill(this); + if (IsConnected) + GameServer.Kill(this); } public void ChangeTeam() { - GameServer.ChangeTeam(this); + if (IsConnected) + GameServer.ChangeTeam(this); } public void ChangeTeam(Team team) { - GameServer.ChangeTeam(this, team); + if (IsConnected) + GameServer.ChangeTeam(this, team); } public void KickFromSquad() { - GameServer.KickFromSquad(this); + if (IsConnected) + GameServer.KickFromSquad(this); } public void JoinSquad(Squads targetSquad) { - GameServer.JoinSquad(this, targetSquad); + if (IsConnected) + GameServer.JoinSquad(this, targetSquad); } public void DisbandTheSquad() { - GameServer.DisbandPlayerCurrentSquad(this); + if (IsConnected) + GameServer.DisbandPlayerCurrentSquad(this); } public void PromoteToSquadLeader() { - GameServer.PromoteSquadLeader(this); + if (IsConnected) + GameServer.PromoteSquadLeader(this); } public void WarnPlayer(string msg) { - GameServer.WarnPlayer(this, msg); + if (IsConnected) + GameServer.WarnPlayer(this, msg); } public void Message(string msg) { - GameServer.MessageToPlayer(this, msg); + if (IsConnected) + GameServer.MessageToPlayer(this, msg); } + public void SayToChat(string msg) + { + if (IsConnected) + GameServer.SayToChat(msg, this); + } + public void Message(string msg, float fadeoutTime) { - GameServer.MessageToPlayer(this, msg, fadeoutTime); + if (IsConnected) + GameServer.MessageToPlayer(this, msg, fadeoutTime); } public void SetNewRole(GameRole role) { - GameServer.SetRoleTo(this, role); + if (IsConnected) + GameServer.SetRoleTo(this, role); } public void Teleport(Vector3 target) { @@ -213,47 +237,57 @@ namespace BattleBitAPI } public void SpawnPlayer(PlayerLoadout loadout, PlayerWearings wearings, Vector3 position, Vector3 lookDirection, PlayerStand stand, float spawnProtection) { - GameServer.SpawnPlayer(this, loadout, wearings, position, lookDirection, stand, spawnProtection); + if (IsConnected) + GameServer.SpawnPlayer(this, loadout, wearings, position, lookDirection, stand, spawnProtection); } public void SetHP(float newHP) { - GameServer.SetHP(this, newHP); + if (IsConnected) + GameServer.SetHP(this, newHP); } public void GiveDamage(float damage) { - GameServer.GiveDamage(this, damage); + if (IsConnected) + GameServer.GiveDamage(this, damage); } public void Heal(float hp) { - GameServer.Heal(this, hp); + if (IsConnected) + GameServer.Heal(this, hp); } public void SetPrimaryWeapon(WeaponItem item, int extraMagazines, bool clear = false) { - GameServer.SetPrimaryWeapon(this, item, extraMagazines, clear); + if (IsConnected) + GameServer.SetPrimaryWeapon(this, item, extraMagazines, clear); } public void SetSecondaryWeapon(WeaponItem item, int extraMagazines, bool clear = false) { - GameServer.SetSecondaryWeapon(this, item, extraMagazines, clear); + if (IsConnected) + GameServer.SetSecondaryWeapon(this, item, extraMagazines, clear); } public void SetFirstAidGadget(string item, int extra, bool clear = false) { - GameServer.SetFirstAid(this, item, extra, clear); + if (IsConnected) + GameServer.SetFirstAid(this, item, extra, clear); } public void SetLightGadget(string item, int extra, bool clear = false) { - GameServer.SetLightGadget(this, item, extra, clear); + if (IsConnected) + GameServer.SetLightGadget(this, item, extra, clear); } public void SetHeavyGadget(string item, int extra, bool clear = false) { - GameServer.SetHeavyGadget(this, item, extra, clear); + if (IsConnected) + GameServer.SetHeavyGadget(this, item, extra, clear); } public void SetThrowable(string item, int extra, bool clear = false) { - GameServer.SetThrowable(this, item, extra, clear); + if (IsConnected) + GameServer.SetThrowable(this, item, extra, clear); } // ---- Static ---- - public static void SetInstance(TPlayer player, Player.Internal @internal) + internal static void SetInstance(TPlayer player, Player.Internal @internal) { player.mInternal = @internal; } @@ -275,6 +309,8 @@ namespace BattleBitAPI public Team Team; public Squads SquadName; public int PingMs = 999; + public long PreviousSessionID = 0; + public long SessionID = 0; public bool IsAlive; public float HP; diff --git a/BattleBitAPI/Server/ServerListener.cs b/BattleBitAPI/Server/ServerListener.cs index c7ad39d..b75c3b3 100644 --- a/BattleBitAPI/Server/ServerListener.cs +++ b/BattleBitAPI/Server/ServerListener.cs @@ -1,10 +1,11 @@ using System.Net; using System.Net.Sockets; using System.Numerics; -using System.Runtime.CompilerServices; +using System.Resources; using BattleBitAPI.Common; using BattleBitAPI.Common.Extentions; using BattleBitAPI.Networking; +using BattleBitAPI.Pooling; namespace BattleBitAPI.Server { @@ -55,15 +56,6 @@ namespace BattleBitAPI.Server /// public Func, Task> OnGameServerConnected { get; set; } - /// - /// Fired when a game server reconnects. (When game server connects while a socket is already open) - /// - /// - /// - /// GameServer: Game server that is reconnecting.
- ///
- public Func, Task> OnGameServerReconnected { get; set; } - /// /// Fired when a game server disconnects. Check (GameServer.TerminationReason) to see the reason. /// @@ -97,12 +89,14 @@ namespace BattleBitAPI.Server private TcpListener mSocket; private Dictionary.Internal resources)> mActiveConnections; private mInstances mInstanceDatabase; + private ItemPooling> mGameServerPool; // --- Construction --- public ServerListener() { this.mActiveConnections = new Dictionary.Internal)>(16); this.mInstanceDatabase = new mInstances(); + this.mGameServerPool = new ItemPooling>(64); } // --- Starting --- @@ -160,10 +154,12 @@ namespace BattleBitAPI.Server { var ip = (client.Client.RemoteEndPoint as IPEndPoint).Address; + //Is this IP allowed? bool allow = true; if (OnGameServerConnecting != null) allow = await OnGameServerConnecting(ip); + //Close connection if it was not allowed. if (!allow) { //Connection is not allowed from this IP. @@ -171,11 +167,13 @@ namespace BattleBitAPI.Server return; } - TGameServer server = null; - GameServer.Internal resources; + //Read port,token,version + string token; + string version; + int gamePort; try { - using (CancellationTokenSource source = new CancellationTokenSource(Const.HailConnectTimeout)) + using (var source = new CancellationTokenSource(2000)) { using (var readStream = Common.Serialization.Stream.Get()) { @@ -186,13 +184,13 @@ namespace BattleBitAPI.Server readStream.Reset(); if (!await networkStream.TryRead(readStream, 1, source.Token)) throw new Exception("Unable to read the package type"); + NetworkCommuncation type = (NetworkCommuncation)readStream.ReadInt8(); if (type != NetworkCommuncation.Hail) throw new Exception("Incoming package wasn't hail."); } //Read the server token - string token; { readStream.Reset(); if (!await networkStream.TryRead(readStream, 2, source.Token)) @@ -210,7 +208,6 @@ namespace BattleBitAPI.Server } //Read the server version - string version; { readStream.Reset(); if (!await networkStream.TryRead(readStream, 2, source.Token)) @@ -227,23 +224,59 @@ namespace BattleBitAPI.Server version = readStream.ReadString(stringSize); } - if (version != Const.Version) - throw new Exception("Incoming server's version `" + version + "` does not match with current API version `" + Const.Version + "`"); - //Read port - int gamePort; { readStream.Reset(); if (!await networkStream.TryRead(readStream, 2, source.Token)) throw new Exception("Unable to read the Port"); gamePort = readStream.ReadUInt16(); } + } + } + } + catch { client.SafeClose(); return; } - if (OnValidateGameServerToken != null) - allow = await OnValidateGameServerToken(ip, (ushort)gamePort, token); + var hash = ((ulong)gamePort << 32) | (ulong)ip.ToUInt(); + TGameServer server = null; + GameServer.Internal resources = null; + try + { + //Does versions match? + if (version != Const.Version) + throw new Exception("Incoming server's version `" + version + "` does not match with current API version `" + Const.Version + "`"); - if (!allow) - throw new Exception("Token was not valid!"); + //Is valid token? + if (OnValidateGameServerToken != null) + { + if (!await OnValidateGameServerToken(ip, (ushort)gamePort, token)) + throw new Exception("Token was not valid!"); + } + + //Are there any connections with same IP and port? + { + bool sessionExist = false; + (TGameServer server, GameServer.Internal resources) oldSession; + + //Any sessions with this IP:Port? + lock (this.mActiveConnections) + sessionExist = this.mActiveConnections.TryGetValue(hash, out oldSession); + + if (sessionExist) + { + //Close old session. + oldSession.server.CloseConnection("Reconnecting."); + + //Wait until session is fully closed. + while (oldSession.resources.HasActiveConnectionSession) + await Task.Delay(1); + } + } + + using (var source = new CancellationTokenSource(Const.HailConnectTimeout)) + { + using (var readStream = Common.Serialization.Stream.Get()) + { + var networkStream = client.GetStream(); //Read is server protected bool isPasswordProtected; @@ -403,7 +436,28 @@ namespace BattleBitAPI.Server } } - var hash = ((ulong)gamePort << 32) | (ulong)ip.ToUInt(); + //Round index + uint roundIndex; + { + readStream.Reset(); + if (!await networkStream.TryRead(readStream, 4, source.Token)) + throw new Exception("Unable to read the Server Round Index"); + roundIndex = readStream.ReadUInt32(); + } + + //Round index + long sessionID; + { + readStream.Reset(); + if (!await networkStream.TryRead(readStream, 8, source.Token)) + throw new Exception("Unable to read the Server Round ID"); + sessionID = readStream.ReadInt64(); + } + + + + + server = this.mInstanceDatabase.GetServerInstance(hash, out resources, this.OnCreatingGameServerInstance, ip, (ushort)gamePort); resources.Set( this.mExecutePackage, @@ -421,7 +475,9 @@ namespace BattleBitAPI.Server queuePlayers, maxPlayers, loadingScreenText, - serverRulesText + serverRulesText, + roundIndex, + sessionID ); //Room settings @@ -583,7 +639,6 @@ namespace BattleBitAPI.Server playerInternal.SteamID = steamid; playerInternal.Name = username; playerInternal.IP = new IPAddress(ipHash); - playerInternal.GameServer = (GameServer)server; playerInternal.Team = team; playerInternal.SquadName = squad; playerInternal.Role = role; @@ -604,6 +659,9 @@ namespace BattleBitAPI.Server playerInternal._Modifications.Read(readStream); } + playerInternal.GameServer = (GameServer)server; + playerInternal.SessionID = server.SessionID; + resources.AddPlayer(player); } @@ -669,37 +727,6 @@ namespace BattleBitAPI.Server return; } - bool connectionExist = false; - - //Track the connection - lock (this.mActiveConnections) - { - //An old connection exist with same IP + Port? - if (connectionExist = this.mActiveConnections.TryGetValue(server.ServerHash, out var oldServer)) - { - oldServer.resources.ReconnectFlag = true; - this.mActiveConnections.Remove(server.ServerHash); - } - - this.mActiveConnections.Add(server.ServerHash, (server, resources)); - } - - //Call the callback. - if (!connectionExist) - { - //New connection! - server.OnConnected(); - if (this.OnGameServerConnected != null) - this.OnGameServerConnected(server); - } - else - { - //Reconnection - server.OnReconnected(); - if (this.OnGameServerReconnected != null) - this.OnGameServerReconnected(server); - } - //Set the buffer sizes. client.ReceiveBufferSize = Const.MaxNetworkPackageSize; client.SendBufferSize = Const.MaxNetworkPackageSize; @@ -709,39 +736,77 @@ namespace BattleBitAPI.Server } private async Task mHandleGameServer(TGameServer server, GameServer.Internal @internal) { - bool isTicking = false; - - using (server) + @internal.HasActiveConnectionSession = true; { - async Task mTickAsync() + // ---- Connected ---- { - isTicking = true; - await server.OnTick(); - isTicking = false; + lock (this.mActiveConnections) + this.mActiveConnections.Replace(server.ServerHash, (server, @internal)); + + server.OnConnected(); + if (this.OnGameServerConnected != null) + this.OnGameServerConnected(server); } - while (server.IsConnected) + //Update sessions { - if (!isTicking) - mTickAsync(); + if (@internal.mPreviousSessionID != @internal.SessionID) + { + var oldSession = @internal.mPreviousSessionID; + @internal.mPreviousSessionID = @internal.SessionID; - await server.Tick(); - await Task.Delay(10); + if (oldSession != 0) + server.OnSessionChanged(oldSession, @internal.SessionID); + } + + foreach (var item in @internal.Players) + { + var @player_internal = mInstanceDatabase.GetPlayerInternals(item.Key); + if (@player_internal.PreviousSessionID != @player_internal.SessionID) + { + var previousID = @player_internal.PreviousSessionID; + @player_internal.PreviousSessionID = @player_internal.SessionID; + + if (previousID != 0) + item.Value.OnSessionChanged(previousID, @player_internal.SessionID); + } + } } - if (!server.ReconnectFlag) + // ---- Ticking ---- + using (server) { + var isTicking = false; + async Task mTickAsync() + { + isTicking = true; + await server.OnTick(); + isTicking = false; + } + + while (server.IsConnected) + { + if (!isTicking) + mTickAsync(); + + await server.Tick(); + await Task.Delay(10); + } + } + + // ---- Disconnected ---- + { + mCleanup(server, @internal); + + lock (this.mActiveConnections) + this.mActiveConnections.Remove(server.ServerHash); + server.OnDisconnected(); - if (this.OnGameServerDisconnected != null) this.OnGameServerDisconnected(server); } } - - //Remove from list. - if (!server.ReconnectFlag) - lock (this.mActiveConnections) - this.mActiveConnections.Remove(server.ServerHash); + @internal.HasActiveConnectionSession = false; } // --- Logic Executing --- @@ -766,8 +831,6 @@ namespace BattleBitAPI.Server playerInternal.SteamID = steamID; playerInternal.Name = username; playerInternal.IP = new IPAddress(ip); - playerInternal.GameServer = (GameServer)server; - playerInternal.Team = team; playerInternal.SquadName = squad; playerInternal.Role = role; @@ -775,9 +838,21 @@ namespace BattleBitAPI.Server //Start from default. playerInternal._Modifications.Reset(); + playerInternal.GameServer = (GameServer)server; + playerInternal.SessionID = server.SessionID; + resources.AddPlayer(player); player.OnConnected(); server.OnPlayerConnected(player); + + if (playerInternal.PreviousSessionID != playerInternal.SessionID) + { + var previousID = playerInternal.PreviousSessionID; + playerInternal.PreviousSessionID = playerInternal.SessionID; + + if (previousID != 0) + player.OnSessionChanged(previousID, playerInternal.SessionID); + } } } break; @@ -816,6 +891,9 @@ namespace BattleBitAPI.Server server.OnPlayerLeftSquad((TPlayer)player, msquad); } + @internal.SessionID = 0; + @internal.GameServer = null; + player.OnDisconnected(); server.OnPlayerDisconnected((TPlayer)player); } @@ -1330,10 +1408,55 @@ namespace BattleBitAPI.Server } break; } + case NetworkCommuncation.NotifyNewRoundID: + { + if (stream.CanRead(4 + 8)) + { + resources.RoundIndex = stream.ReadUInt32(); + resources.SessionID = stream.ReadInt64(); + + if (resources.mPreviousSessionID != resources.SessionID) + { + var oldSession = resources.mPreviousSessionID; + resources.mPreviousSessionID = resources.SessionID; + + if (oldSession != 0) + server.OnSessionChanged(oldSession, resources.SessionID); + } + + foreach (var item in resources.Players) + { + var @player_internal = mInstanceDatabase.GetPlayerInternals(item.Key); + @player_internal.SessionID = resources.SessionID; + + if (@player_internal.PreviousSessionID != @player_internal.SessionID) + { + var previousID = @player_internal.PreviousSessionID; + @player_internal.PreviousSessionID = @player_internal.SessionID; + + if (previousID != 0) + item.Value.OnSessionChanged(previousID, @player_internal.SessionID); + } + } + } + break; + } } } // --- Private --- + private void mCleanup(GameServer server, GameServer.Internal @internal) + { + lock (@internal.Players) + { + foreach (var item in @internal.Players) + { + var @player_internal = mInstanceDatabase.GetPlayerInternals(item.Key); + @player_internal.SessionID = 0; + @player_internal.GameServer = null; + } + } + } private Player.Internal mGetPlayerInternals(ulong steamID) { return mInstanceDatabase.GetPlayerInternals(steamID); @@ -1344,13 +1467,17 @@ namespace BattleBitAPI.Server { get { - var list = new List(mActiveConnections.Count); - lock (mActiveConnections) + using (var list = this.mGameServerPool.Get()) { - foreach (var item in mActiveConnections.Values) - list.Add(item.server); + //Get a copy + lock (mActiveConnections) + foreach (var item in mActiveConnections.Values) + list.ListItems.Add(item.server); + + //Iterate + for (int i = 0; i < list.ListItems.Count; i++) + yield return (TGameServer)list.ListItems[i]; } - return list; } } public bool TryGetGameServer(IPAddress ip, ushort port, out TGameServer server)