Exemplo n.º 1
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, SetControlledUnit header)
 {
     if (header.UnitID != session.Player.ID)
     {
         log.WarnFormat("received {0} with incorrect unit id {1}. it should be {2}", ShardClientOpcode.SetControlledUnit, header.UnitID, session.Player.ID);
     }
 }
Exemplo n.º 2
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, QueryName header)
        {
            Character character = session.Server.Shard.GetCharacter(header.CharacterID);

            if (character == null)
            {
                // not online
                character = new CharacterDao().GetCharacterByID(session.Server.ShardDB, header.CharacterID);
            }

            if (character == null)
            {
                log.WarnFormat("received {0} for non-existing object id {1}", ShardClientOpcode.QueryName, header.CharacterID);
            }
            else
            {
                using (ByteBuffer response = new ByteBuffer())
                {
                    response.Append(character.ID);
                    response.Append(character.Name);
                    response.Append((byte)0); // used to identify shard name in cross-shard bg
                    response.Append((int)character.Race);
                    response.Append((int)character.Sex);
                    response.Append((int)character.Class);

                    session.Send(ShardServerOpcode.QueryName, response);
                }
            }
        }
Exemplo n.º 3
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
        {
            using (ByteBuffer response = new ByteBuffer())
            {
                response.Append(NoUnreadMail); // mystery float
                response.Append(0);            // count of mails (to show on map?)

                session.Send(ShardServerOpcode.QueryNextMailTime, response);
            }
        }
Exemplo n.º 4
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
        {
            using (ByteBuffer response = new ByteBuffer())
            {
                response.Append(0);
                response.Append((byte)6); // ?

                session.Send(ShardServerOpcode.MeetingStoneSetQueue, response);
            }
        }
Exemplo n.º 5
0
 protected override Task OnInitialize()
 {
     if (self == null)
     {
         self = GrainFactory.CreateObjectReference <IShardSessionObserver>(this).Result;
         return(ShardSession.Connect(self, shardName));
     }
     else
     {
         throw new InvalidOperationException("cannot initialize more than once");
     }
 }
Exemplo n.º 6
0
        private async Task HandleQueryName(QueryNameRequest request)
        {
            try
            {
                var response = await ShardSession.QueryName(request);

                ForwardPacket(response);
            }
            catch (ObjectDoesNotExistException ex)
            {
                // TODO: logging
                Console.Error.WriteLine(ex.Message);
            }
        }
        private async Task HandleCharacterList(CharacterListRequest request)
        {
            var response      = new CharacterListResponse();
            var characterList = await ShardSession.GetCharacterList();

            // TODO: this equipment list obviously needs revisited once items are implemented
            var equipmentCount          = Enum.GetValues(typeof(EquipmentSlot)).Length;
            var equipmentDisplayIds     = new int[equipmentCount];
            var equipmentInventoryTypes = new byte[equipmentCount];

            foreach (var character in characterList)
            {
                response.AddCharacter(character.Id, character.Name, character.Race, character.Class, character.Sex, character.Skin, character.Face, character.HairStyle,
                                      character.HairColor, character.FacialHair, character.Level, character.ZoneId, character.MapId, character.Position, 0, 0, true, 0, 0, 0, 0, 0, equipmentDisplayIds, equipmentInventoryTypes);
            }

            ForwardPacket(response);
        }
        private async Task HandleAuthSession(AuthSessionRequest request)
        {
            try
            {
                // this handler is a little whacky. it can't just send its own response to the client, because the client
                //  expects the response to be encrypted with the session key. so we have a bit of call-and-response here
                //  to manage this
                var sessionKey = await ShardSession.Authenticate(request);

                packetCipher.Initialize(sessionKey);
                await ShardSession.Handshake(request);
            }
            catch (SessionException ex)
            {
                // TODO: logging
                Console.WriteLine(ex.Message);
                authenticationFailed = true;
            }
        }
        private async Task HandleCharacterCreate(CharacterCreateRequest request)
        {
            try
            {
                await ShardSession.CreateCharacter(request);

                ForwardPacket(new CharacterCreateResponse()
                {
                    Response = CharacterCreateResponseCode.Success
                });
            }
            catch (CharacterAlreadyExistsException)
            {
                ForwardPacket(new CharacterCreateResponse()
                {
                    Response = CharacterCreateResponseCode.NameTaken
                });
            }
        }
Exemplo n.º 10
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, MovementHeader header)
        {
            ObjectID?       transportId       = null;
            OrientedVector3?transportPosition = null;
            float?          pitch             = null;
            uint            fallTime          = 0;
            float?          jumpVelocity      = null;
            float?          jumpSinAngle      = null;
            float?          jumpCosAngle      = null;
            float?          jumpXySpeed       = null;

            using (BinaryReader reader = new BinaryReader(new MemoryStream(request.Packet, false), Encoding.UTF8, false))
            {
                reader.BaseStream.Seek(Marshal.SizeOf <MovementHeader>(), SeekOrigin.Begin);

                if ((header.Flags & MovementFlags.OnTransport) != 0)
                {
                    transportId       = new ObjectID(reader.ReadUInt64());
                    transportPosition = new OrientedVector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
                }

                if ((header.Flags & MovementFlags.ModeSwimming) != 0)
                {
                    pitch = reader.ReadSingle();
                }

                fallTime = reader.ReadUInt32();

                if ((header.Flags & MovementFlags.ModeFalling) != 0)
                {
                    jumpVelocity = reader.ReadSingle();
                    jumpSinAngle = reader.ReadSingle();
                    jumpCosAngle = reader.ReadSingle();
                    jumpXySpeed  = reader.ReadSingle();
                }
            }

            // TODO: implement falling, swimming, transport
            // TODO: verify that client isn't sending bogus data
            session.Player.Control.MovementFlags = header.Flags & ~(MovementFlags.ModeFalling | MovementFlags.ModeSwimming | MovementFlags.OnTransport);
            session.Player.Control.Position      = header.Position;
            session.Player.Control.MovementTime  = header.Time;
        }
Exemplo n.º 11
0
 private Task HandleMoveJump(MoveJumpRequest request)
 {
     return(ShardSession.Jump(request));
 }
Exemplo n.º 12
0
 private Task HandleGenericMovement(MovementInPacket request)
 {
     return(ShardSession.Move(request));
 }
Exemplo n.º 13
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
 {
     session.SendQueryTimePacket();
 }
 private Task HandlePlayerLogoutRequest(PlayerLogoutRequest request)
 {
     return(ShardSession.Logout());
 }
Exemplo n.º 15
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
 {
     // well, it's a no-op. what did you expect?
 }
Exemplo n.º 16
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
        {
            int    clientBuild = BitConverter.ToInt32(request.Packet, 6);
            int    identityLength;
            string identity   = Strings.FromNullTerminated(request.Packet, 14, Encoding.UTF8, out identityLength);
            int    clientSeed = BitConverter.ToInt32(request.Packet, 14 + identityLength + 1); // + 1 for null terminator

            byte[] clientDigest = new byte[20];
            Array.Copy(request.Packet, 14 + identityLength + 1 + 4, clientDigest, 0, 20);

            log.DebugFormat("read {0} packet. client build = {1}, identity = {2}, client seed = {3:X}, packet size = {4}", header.Opcode, clientBuild, identity, clientSeed, request.Size);

            int    accountId  = -1;
            string sessionKey = null;

            if (!session.Server.AuthDB.ExecuteQuery("select name, session_key, id from account where name = ? and enabled = true", identity, result =>
            {
                if (result.Read())
                {
                    identity = result.GetString(0);
                    sessionKey = result.GetString(1);
                    accountId = result.GetInt32(2);
                    return(true);
                }
                return(false);
            }))
            {
                // no identity match
                log.DebugFormat("account {0} not found. sending {1} response", identity, AuthResponse.UnknownAccount);
                session.Send(ShardServerOpcode.AuthResponse, (byte)AuthResponse.UnknownAccount);
                return;
            }

            BigInteger sessionKeyInt = BigInteger.Parse(sessionKey, NumberStyles.AllowHexSpecifier);

            // verify client/server identity and session key match
            using (Digester sha1 = new Digester(new SHA1CryptoServiceProvider()))
            {
                byte[] serverDigest = sha1.CalculateDigest(new byte[][]
                {
                    Encoding.UTF8.GetBytes(identity),
                    new byte[4],
                    BitConverter.GetBytes(clientSeed),
                    BitConverter.GetBytes(session.Seed),
                    Arrays.Left(sessionKeyInt.ToByteArray(), 40)
                });

                log.DebugFormat("client digest = {0}", Strings.HexOf(clientDigest));
                log.DebugFormat("server digest = {0}", Strings.HexOf(serverDigest));

                if (Arrays.AreEqual(clientDigest, serverDigest))
                {
                    log.InfoFormat("client {0} successfully authenticated from {1}", identity, session.RemoteEndPoint.Address.ToString());

                    session.AccountID = accountId;
                    session.Status    = SessionStatus.Authenticated;
                    session.InitializeCipher(sessionKeyInt);

                    using (ByteBuffer authPacket = new ByteBuffer())
                    {
                        authPacket.Append((byte)AuthResponse.Success);
                        authPacket.Append(0);
                        authPacket.Append((byte)0);
                        authPacket.Append(0);

                        session.Send(ShardServerOpcode.AuthResponse, authPacket);
                    }

                    using (ByteBuffer addonPacket = BuildAddonPacket(new ArraySegment <byte>(request.Packet, 14 + identityLength + 1 + 4 + 20, request.Size - (14 + identityLength + 1 + 4 + 20))))
                        session.Send(ShardServerOpcode.AddonInfo, addonPacket);
                }
                else
                {
                    // digest mismatch
                    log.DebugFormat("client digest did not match server digest for account {0}. sending {1} response", identity, AuthResponse.Failed);
                    session.Send(ShardServerOpcode.AuthResponse, (byte)AuthResponse.Failed);
                    return;
                }
            }
        }
Exemplo n.º 17
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
 {
 }
Exemplo n.º 18
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, MoveTimeSkipped header)
 {
     log.DebugFormat("received {0} packet. unit id = {1}, time skipped = {2}", ShardClientOpcode.MoveTimeSkipped, header.UnitID, header.TimeSkipped);
 }
Exemplo n.º 19
0
 private Task HandleTargetSet(TargetSetRequest request)
 {
     return(ShardSession.SetTarget(request.TargetId));
 }
Exemplo n.º 20
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, ZoneUpdate header)
 {
     log.DebugFormat("received {0} command. new zone id = {1}", Name, header.ZoneID);
 }
Exemplo n.º 21
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
        {
            int count = session.Server.ShardDB.ExecuteQuery("select count(0) from `character` where account_id = ?", session.AccountID, result =>
            {
                if (result.Read())
                {
                    return(result.GetInt32(0));
                }
                else
                {
                    log.Error("selecting count on character table returned no result row");
                    return(0);
                }
            });

            if (count > byte.MaxValue)
            {
                log.WarnFormat("character count [{0}] for account id [{1}] is larger than maximum [{2}] and is being limited", count, session.AccountID, byte.MaxValue);
                count = byte.MaxValue;
            }

            using (ByteBuffer response = new ByteBuffer())
            {
                response.Append((byte)count);

                //                                          0   1     2     3      4    5     6     7           8           9            10     11      12          13          14          15            16
                session.Server.ShardDB.ExecuteQuery("select id, name, race, class, sex, skin, face, hair_style, hair_color, facial_hair, level, map_id, position_x, position_y, position_z, player_flags, zone_id from `character` where account_id = ? limit ?", new object[] { session.AccountID, byte.MaxValue }, result =>
                {
                    while (result.Read())
                    {
                        response.Append(new ObjectID(result.GetInt32(0), ObjectID.Type.Player)); // id
                        response.Append(result.GetString(1));                                    // name
                        response.Append(result.GetByte(2));                                      // race
                        response.Append(result.GetByte(3));                                      // class
                        response.Append(result.GetByte(4));                                      // sex
                        response.Append(result.GetByte(5));                                      // skin
                        response.Append(result.GetByte(6));                                      // face
                        response.Append(result.GetByte(7));                                      // hair style
                        response.Append(result.GetByte(8));                                      // hair color
                        response.Append(result.GetByte(9));                                      // facial hair
                        response.Append(result.GetByte(10));                                     // level
                        response.Append(result.GetInt32(16));                                    // zone
                        response.Append(result.GetInt32(11));                                    // map id
                        response.Append(result.GetFloat(12));                                    // x
                        response.Append(result.GetFloat(13));                                    // y
                        response.Append(result.GetFloat(14));                                    // z
                        response.Append(0);                                                      // guild id
                        response.Append(result.GetInt32(15));                                    // player flags
                        response.Append((byte)1);                                                // first login
                        response.Append(0);                                                      // pet display id
                        response.Append(0);                                                      // pet level
                        response.Append(0);                                                      // pet family

                        for (int i = 0; i < Enum.GetValues(typeof(EquipmentSlot)).Length; ++i)
                        {
                            response.Append(0);       // display info id
                            response.Append((byte)0); // inventory type
                        }

                        response.Append(0);       // first bag display info id
                        response.Append((byte)0); // first bag inventory type
                    }

                    return(true);
                });

                session.Send(ShardServerOpcode.CharacterList, response);
            }
        }
Exemplo n.º 22
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
        {
            session.SendQueryTimePacket();

            session.Send(ShardServerOpcode.SupportTicketQuery, BitConverter.GetBytes((int)SupportTicketStatus.NotExists));
        }
Exemplo n.º 23
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
        {
            int offset = Marshal.SizeOf <ClientPacketHeader>();

            string name       = Strings.FromNullTerminated(request.Packet, ref offset);
            byte   race       = request.Packet[offset++];
            byte   @class     = request.Packet[offset++];
            byte   sex        = request.Packet[offset++];
            byte   skin       = request.Packet[offset++];
            byte   face       = request.Packet[offset++];
            byte   hairStyle  = request.Packet[offset++];
            byte   hairColor  = request.Packet[offset++];
            byte   facialHair = request.Packet[offset++];
            byte   outfitId   = request.Packet[offset++];

            Race raceEnum;

            if (!Enum.TryParse(race.ToString(), out raceEnum))
            {
                log.WarnFormat("client attempted to create character with invalid race [{0}]", race);
                session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.Failed);
                return;
            }

            Class classEnum;

            if (!Enum.TryParse(@class.ToString(), out classEnum))
            {
                log.WarnFormat("client attempted to create character with invalid class [{0}]", @class);
                session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.Failed);
                return;
            }

            if (sex != (byte)Sex.Male && sex != (byte)Sex.Female)
            {
                log.WarnFormat("client attempted to create character with invalid sex [{0}]", sex);
                session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.Failed);
                return;
            }

            CharacterTemplate template;

            try
            {
                template = session.Server.World.CharacterTemplates[raceEnum][classEnum];
            }
            catch (KeyNotFoundException)
            {
                // no template for race/class combination
                log.WarnFormat("cannot create character with race {0} and class {1} because that combination is not valid", raceEnum, classEnum);
                session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.Failed);
                return;
            }

            if (session.Server.ShardDB.ExecuteQuery("select id from `character` where name = ?", name, result =>
            {
                return(result.Read());
            }))
            {
                // character already exists
                log.DebugFormat("cannot create character [{0}] because that name already exists", name);
                session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.NameTaken);
                return;
            }

            int updated = session.Server.ShardDB.ExecuteNonQuery("insert into `character` (account_id, name, race, class, sex, skin, face, hair_style, hair_color, facial_hair, position_x, position_y, position_z, orientation, map_id, zone_id) values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
                                                                 session.AccountID, name, race, @class, sex, skin, face, hairStyle, hairColor, facialHair, template.PositionX, template.PositionY, template.PositionZ, template.Orientation, template.MapID, template.ZoneID);

            if (updated != 1)
            {
                log.ErrorFormat("expected 1 row updated when inserting new character but got {0}", updated);
                session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.Failed);
                return;
            }

            int characterId = session.Server.ShardDB.ExecuteQuery("select id from `character` where account_id = ? and name = ?", new object[] { session.AccountID, name }, result =>
            {
                if (result.Read())
                {
                    return(result.GetInt32(0));
                }
                else
                {
                    return(-1);
                }
            });

            if (characterId < 0)
            {
                log.ErrorFormat("unable to locate recently inserted character with account id [{0}] and name '{1}'", session.AccountID, name);
                session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.Failed);
                return;
            }

            foreach (var spellId in template.SpellIDs)
            {
                if (session.Server.ShardDB.ExecuteNonQuery("insert into character_spell (character_id, spell_id) values (?, ?)", characterId, spellId) != 1)
                {
                    log.Error("insert into character_spell did not affect 1 row");
                }
            }

            for (int i = 0; i < template.ActionButtons.Count; ++i)
            {
                if (session.Server.ShardDB.ExecuteNonQuery("insert into character_action_button (character_id, button, action, type) values (?, ?, ?, ?)", characterId, i, template.ActionButtons[i].Action, template.ActionButtons[i].ButtonType) != 1)
                {
                    log.Error("insert into character_action_button did not affect 1 row");
                }
            }

            // success case
            log.InfoFormat("account [{0}] successfully created new {1} {2} named '{3}'", session.AccountID, raceEnum, classEnum, name);
            session.Send(ShardServerOpcode.CharacterCreate, (byte)CharacterCreateResponse.Success);
        }
Exemplo n.º 24
0
 private Task HandleChatMessage(ChatMessageRequest request)
 {
     return(ShardSession.SendChatMessage(request));
 }
Exemplo n.º 25
0
        public override void ExecuteCommand(ShardSession session, ShardRequest request, PlayerLogin header)
        {
            Character player = new CharacterDao().GetCharacterByID(session.Server.ShardDB, header.CharacterID);

            log.DebugFormat("character '{0}' loaded successfully", player.Name);

            // initialize the character if it has never logged in before now
            if (player.FirstLogin)
            {
                log.DebugFormat("character '{0}' first login, initializing", player.Name);
                player.Initialize(session.Server.World);
            }

            // set session status to ingame and add the player to the Shard
            session.Player = player;
            session.Status = SessionStatus.Ingame;
            session.Server.Shard.AddCharacter(player);

            // send logon player response packet
            using (ByteBuffer packet = new ByteBuffer())
            {
                packet.Append(player.MapID);
                packet.Append(player.Position);

                session.Send(ShardServerOpcode.LoginVerifyWorld, packet);
            }

            // not sure what this does
            session.Send(ShardServerOpcode.AccountDataTimes, new byte[128]);

            // not sure what this does, either
            session.Send(ShardServerOpcode.LoginSetRestStart, BitConverter.GetBytes(0));

            // update bind point
            using (ByteBuffer packet = new ByteBuffer())
            {
                // TODO: implement bind point other than initial world position
                CharacterTemplate ct = session.Server.World.CharacterTemplates[player.Race][player.Class];

                packet.Append(ct.PositionX); // x
                packet.Append(ct.PositionY); // y
                packet.Append(ct.PositionZ); // z
                packet.Append(ct.MapID);     // map id
                packet.Append(ct.ZoneID);    // zone id

                session.Send(ShardServerOpcode.BindPointUpdate, packet);
            }

            // set tutorial state
            using (ByteBuffer buffer = new ByteBuffer())
            {
                for (int i = 0; i < 8; ++i)
                {
                    buffer.Append(-1);
                }

                session.Send(ShardServerOpcode.LoginTutorialFlags, buffer);
            }

            // set initial spells
            using (ByteBuffer packet = new ByteBuffer())
            {
                packet.Append((byte)0);
                packet.Append((ushort)player.Spells.Where(spell => spell.Enabled).Count());

                foreach (Character.Spell spell in player.Spells.Where(spell => spell.Enabled))
                {
                    packet.Append((ushort)spell.SpellID);
                    packet.Append((short)0);
                }

                packet.Append((ushort)0); // spell cooldown count

                session.Send(ShardServerOpcode.LoginInitializeSpells, packet);
            }

            // set initial action buttons
            using (ByteBuffer packet = new ByteBuffer())
            {
                for (int i = 0; i < Character.MaxActionButtons; ++i)
                {
                    if (i < player.ActionButtons.Count)
                    {
                        packet.Append(player.ActionButtons[i]);
                    }
                    else
                    {
                        packet.Append(0);
                    }
                }

                session.Send(ShardServerOpcode.LoginInitializeActionButtons, packet);
            }

            // set initial faction standing

            /*using (ByteBuffer packet = new ByteBuffer())
             * {
             *  packet.Append(FactionCount);
             *
             *  for(int i = 0; i < FactionCount; ++i)
             *  {
             *      packet.Append((byte)0); // faction flags
             *      packet.Append(0); // faction standing
             *  }
             *
             *  session.Send(ShardServerOpcode.LoginInitializeFactions, packet);
             * }*/

            // set initial time and speed
            using (ByteBuffer packet = new ByteBuffer())
            {
                packet.Append(DateTimes.GetBitfield(DateTime.Now));
                packet.Append(GameSpeed);

                session.Send(ShardServerOpcode.LoginSetTimeAndSpeed, packet);
            }

            // trigger cinematic if this is the character's first login
            if (player.FirstLogin)
            {
                RaceDefinition rd;
                if (session.Server.World.RaceDefinitions.TryGetValue(player.Race, out rd))
                {
                    session.Send(ShardServerOpcode.TriggerCinematic, BitConverter.GetBytes(rd.FirstLoginCinematicID));
                }
                else
                {
                    log.WarnFormat("cannot send first login cinematic for undefined race '{0}'", player.Race);
                }
            }

            // send friend list and ignore list (empty for now)
            session.Send(ShardServerOpcode.FriendList, (byte)0);
            session.Send(ShardServerOpcode.IgnoreList, (byte)0);

            // initialize world state
            using (ByteBuffer packet = new ByteBuffer())
            {
                packet.Append(player.MapID);
                packet.Append(player.ZoneID);

                packet.Append((ushort)0); // special map int64s (for battleground, outdoor pvp areas)

                session.Send(ShardServerOpcode.InitializeWorldState, packet);
            }
        }
Exemplo n.º 26
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
 {
     // no battlefields yet, so nothing to report
 }
Exemplo n.º 27
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, Ping header)
 {
     log.DebugFormat("client sends ping with latency = {0}", header.Latency);
     session.Send(ShardServerOpcode.Pong, BitConverter.GetBytes(header.Cookie));
 }
 private Task HandlePlayerLogin(PlayerLoginRequest request)
 {
     return(ShardSession.Login(request.CharacterId));
 }
Exemplo n.º 29
0
 public override void ExecuteCommand(ShardSession session, ShardRequest request, ClientPacketHeader header)
 {
     session.Send(ShardServerOpcode.QueryRaidInfo, BitConverter.GetBytes(0)); // number of saved instance ids
 }
Exemplo n.º 30
0
 protected override Task OnSessionDisconnected(ClientDisconnectedEventArgs e)
 {
     return(ShardSession.Disconnect(self));
 }