瀏覽代碼

进入游戏世界

SunnyCase 8 年之前
父節點
當前提交
b2c1b0175d

+ 74 - 0
src/MineCase.Protocol/Protocol/Play/PlayerListItem.cs

@@ -0,0 +1,74 @@
+using MineCase.Serialization;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace MineCase.Protocol.Play
+{
+    [Packet(0x2D)]
+    public sealed class PlayerListItem<TAction> : ISerializablePacket where TAction : PlayerListItemAction
+    {
+        [SerializeAs(DataType.VarInt)]
+        public uint Action;
+
+        [SerializeAs(DataType.VarInt)]
+        public uint NumberOfPlayers;
+
+        [SerializeAs(DataType.Array)]
+        public TAction[] Players;
+
+        public void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsVarInt(Action, out _);
+            bw.WriteAsVarInt(NumberOfPlayers, out _);
+            if (NumberOfPlayers != 0)
+                bw.WriteAsArray(Players);
+        }
+    }
+
+    public abstract class PlayerListItemAction : ISerializablePacket
+    {
+        [SerializeAs(DataType.UUID)]
+        public Guid UUID;
+
+        public virtual void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsUUID(UUID);
+        }
+    }
+
+    public sealed class PlayerListItemAddPlayerAction : PlayerListItemAction
+    {
+        [SerializeAs(DataType.String)]
+        public string Name;
+
+        [SerializeAs(DataType.VarInt)]
+        public uint NumberOfProperties;
+
+        [SerializeAs(DataType.VarInt)]
+        public uint GameMode;
+
+        [SerializeAs(DataType.VarInt)]
+        public uint Ping;
+
+        [SerializeAs(DataType.Boolean)]
+        public bool HasDisplayName;
+
+        [SerializeAs(DataType.Chat)]
+        public string DisplayName;
+
+        public override void Serialize(BinaryWriter bw)
+        {
+            base.Serialize(bw);
+
+            bw.WriteAsString(Name);
+            bw.WriteAsVarInt(NumberOfProperties, out _);
+            bw.WriteAsVarInt(GameMode, out _);
+            bw.WriteAsVarInt(Ping, out _);
+            bw.WriteAsBoolean(HasDisplayName);
+            if (HasDisplayName)
+                throw new NotImplementedException();
+        }
+    }
+}

+ 44 - 0
src/MineCase.Protocol/Protocol/Play/PositionAndLook.cs

@@ -0,0 +1,44 @@
+using MineCase.Serialization;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.IO;
+
+namespace MineCase.Protocol.Play
+{
+    [Packet(0x2E)]
+    public sealed class PositionAndLook : ISerializablePacket
+    {
+        [SerializeAs(DataType.Double)]
+        public double X;
+
+        [SerializeAs(DataType.Double)]
+        public double Y;
+
+        [SerializeAs(DataType.Double)]
+        public double Z;
+
+        [SerializeAs(DataType.Float)]
+        public float Yaw;
+
+        [SerializeAs(DataType.Float)]
+        public float Pitch;
+
+        [SerializeAs(DataType.Byte)]
+        public byte Flags;
+
+        [SerializeAs(DataType.VarInt)]
+        public uint TeleportId;
+
+        public void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsDouble(X);
+            bw.WriteAsDouble(Y);
+            bw.WriteAsDouble(Z);
+            bw.WriteAsFloat(Yaw);
+            bw.WriteAsFloat(Pitch);
+            bw.WriteAsByte(Flags);
+            bw.WriteAsVarInt(TeleportId, out _);
+        }
+    }
+}

+ 23 - 0
src/MineCase.Protocol/Protocol/Play/TeleportConfirm.cs

@@ -0,0 +1,23 @@
+using MineCase.Serialization;
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+
+namespace MineCase.Protocol.Play
+{
+    [Packet(0x00)]
+    public sealed class TeleportConfirm
+    {
+        [SerializeAs(DataType.VarInt)]
+        public uint TeleportId;
+
+        public static TeleportConfirm Deserialize(BinaryReader br)
+        {
+            return new TeleportConfirm
+            {
+                TeleportId = br.ReadAsVarInt(out _)
+            };
+        }
+    }
+}

+ 11 - 0
src/MineCase.Protocol/Serialization/BinaryWriterExtensions.cs

@@ -62,6 +62,17 @@ namespace MineCase.Serialization
             bw.Write(uintValue.ToBigEndian());
         }
 
+        public static void WriteAsDouble(this BinaryWriter bw, double value)
+        {
+            var ulongValue = Unsafe.As<double, ulong>(ref value);
+            bw.Write(ulongValue.ToBigEndian());
+        }
+
+        public static void WriteAsUUID(this BinaryWriter bw, Guid value)
+        {
+            bw.Write(value.ToByteArray());
+        }
+
         public static void WriteAsArray<T>(this BinaryWriter bw, IReadOnlyList<T> array) where T : ISerializablePacket
         {
             foreach (var item in array)

+ 65 - 1
src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs

@@ -6,13 +6,19 @@ using System.Text;
 using System.Threading.Tasks;
 using MineCase.Server.User;
 using MineCase.Server.Network;
+using System.Linq;
+using Orleans;
+using System.Numerics;
 
 namespace MineCase.Server.Game.Entities
 {
     class PlayerGrain : EntityGrain, IPlayer
     {
+        private IUser _user;
         private ClientPlayPacketGenerator _generator;
+        private uint _ping;
 
+        private string _name;
         private IInventoryWindow _inventory;
 
         public Task<IInventoryWindow> GetInventory() => Task.FromResult(_inventory);
@@ -22,6 +28,11 @@ namespace MineCase.Server.Game.Entities
         public const uint MaxFood = 20;
         private uint _currentExp, _levelMaxExp, _totalExp;
         private uint _level;
+        private uint _teleportId;
+        private bool _teleportConfirmed;
+
+        private Vector3 _position;
+        private float _pitch, _yaw;
 
         public override Task OnActivateAsync()
         {
@@ -36,9 +47,11 @@ namespace MineCase.Server.Game.Entities
             await _generator.WindowItems(0, slots);
         }
 
-        public Task SetClientSink(IClientboundPacketSink sink)
+        public Task BindToUser(IUser user, IClientboundPacketSink sink)
         {
             _generator = new ClientPlayPacketGenerator(sink);
+            _user = user;
+            _health = MaxHealth;
             return Task.CompletedTask;
         }
 
@@ -51,5 +64,56 @@ namespace MineCase.Server.Game.Entities
         {
             await _generator.SetExperience((float)_currentExp / _levelMaxExp, _level, _totalExp);
         }
+
+        public Task<string> GetName() => Task.FromResult(_name);
+
+        public Task SetName(string name)
+        {
+            _name = name;
+            return Task.CompletedTask;
+        }
+
+        public async Task SendPlayerListAddPlayer(IReadOnlyList<IPlayer> player)
+        {
+            var desc = await Task.WhenAll(from p in player
+                                          select p.GetDescription());
+            await _generator.PlayerListItemAddPlayer(desc);
+        }
+
+        public Task<PlayerDescription> GetDescription()
+        {
+            return Task.FromResult(new PlayerDescription
+            {
+                UUID = _user.GetPrimaryKey(),
+                Name = _name,
+                GameMode = new GameMode { ModeClass = GameMode.Class.Survival },
+                Ping = _ping
+            });
+        }
+
+        public async Task NotifyLoggedIn()
+        {
+            await SendWholeInventory();
+            await SendExperience();
+            await SendPlayerListAddPlayer(new[] { this });
+        }
+
+        public async Task SendPositionAndLook()
+        {
+            await _generator.PositionAndLook(_position.X, _position.Y, _position.Z, _yaw, _pitch, 0, _teleportId);
+            _teleportConfirmed = false;
+        }
+
+        public Task SetPing(uint ping)
+        {
+            _pitch = ping;
+            return Task.CompletedTask;
+        }
+
+        public Task OnTeleportConfirm(uint teleportId)
+        {
+            _teleportConfirmed = true;
+            return Task.CompletedTask;
+        }
     }
 }

+ 19 - 4
src/MineCase.Server.Grains/Game/GameSession.cs

@@ -6,17 +6,23 @@ using System.Text;
 using System.Threading.Tasks;
 using MineCase.Server.User;
 using MineCase.Server.Network.Play;
+using System.Linq;
 
 namespace MineCase.Server.Game
 {
     class GameSession : Grain, IGameSession
     {
         private IWorld _world;
-        private readonly Dictionary<IUser, PlayerContext> _players = new Dictionary<IUser, PlayerContext>();
+        private readonly Dictionary<IUser, UserContext> _users = new Dictionary<IUser, UserContext>();
+
+        private IDisposable _gameTick;
+        private DateTime _lastGameTickTime;
 
         public override async Task OnActivateAsync()
         {
             _world = await GrainFactory.GetGrain<IWorldAccessor>(0).GetWorld(this.GetPrimaryKeyString());
+            _lastGameTickTime = DateTime.UtcNow;
+            _gameTick = RegisterTimer(OnGameTick, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(50));
         }
 
         public async Task JoinGame(IUser user)
@@ -24,7 +30,7 @@ namespace MineCase.Server.Game
             var sink = await user.GetClientPacketSink();
             var generator = new ClientPlayPacketGenerator(sink);
 
-            _players[user] = new PlayerContext
+            _users[user] = new UserContext
             {
                 Generator = generator
             };
@@ -37,11 +43,20 @@ namespace MineCase.Server.Game
 
         public Task LeaveGame(IUser player)
         {
-            _players.Remove(player);
+            _users.Remove(player);
             return Task.CompletedTask;
         }
 
-        class PlayerContext
+        private async Task OnGameTick(object state)
+        {
+            var now = DateTime.UtcNow;
+            var deltaTime = now - _lastGameTickTime;
+            _lastGameTickTime = now;
+
+            await Task.WhenAll(_users.Keys.Select(o => o.OnGameTick(deltaTime)));
+        }
+
+        class UserContext
         {
             public ClientPlayPacketGenerator Generator { get; set; }
         }

+ 7 - 7
src/MineCase.Server.Grains/Network/Login/LoginFlowGrain.cs

@@ -19,16 +19,16 @@ namespace MineCase.Server.Network.Login
                 throw new NotImplementedException();
             else
             {
-                var uuid = await GrainFactory.GetGrain<INonAuthenticatedUser>(packet.Name).GetUUID();
+                var user = await GrainFactory.GetGrain<INonAuthenticatedUser>(packet.Name).GetUser();
+                var uuid = user.GetPrimaryKey();
                 await SendLoginSuccess(packet.Name, uuid);
+                
+                await user.SetClientPacketSink(GrainFactory.GetGrain<IClientboundPacketSink>(this.GetPrimaryKey()));
+                await GrainFactory.GetGrain<IPacketRouter>(this.GetPrimaryKey()).BindToUser(user);
 
-                var player = GrainFactory.GetGrain<IUser>(uuid);
-                await player.SetClientPacketSink(GrainFactory.GetGrain<IClientboundPacketSink>(this.GetPrimaryKey()));
-                await GrainFactory.GetGrain<IPacketRouter>(this.GetPrimaryKey()).BindToUser(player);
-
-                var world = await player.GetWorld();
+                var world = await user.GetWorld();
                 var game = GrainFactory.GetGrain<IGameSession>(world.GetPrimaryKeyString());
-                await game.JoinGame(player);
+                await game.JoinGame(user);
             }
         }
 

+ 10 - 0
src/MineCase.Server.Grains/Network/PacketRouterGrain.Play.cs

@@ -20,6 +20,10 @@ namespace MineCase.Server.Network
                 object innerPacket;
                 switch (packet.PacketId)
                 {
+                    // Teleport Confirm
+                    case 0x00:
+                        innerPacket = TeleportConfirm.Deserialize(br);
+                        break;
                     // Client Settings
                     case 0x05:
                         innerPacket = ClientSettings.Deserialize(br);
@@ -41,6 +45,12 @@ namespace MineCase.Server.Network
             }
         }
 
+        private async Task DispatchPacket(TeleportConfirm packet)
+        {
+            var player = await _user.GetPlayer();
+            player.OnTeleportConfirm(packet.TeleportId).Ignore();
+        }
+
         private Task DispatchPacket(ClientSettings packet)
         {
             return Task.CompletedTask;

+ 50 - 1
src/MineCase.Server.Grains/Network/Play/ClientPlayPacketGenerator.cs

@@ -5,6 +5,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
+using MineCase.Server.Game.Entities;
 
 namespace MineCase.Server.Network.Play
 {
@@ -22,7 +23,7 @@ namespace MineCase.Server.Network.Play
             return Sink.SendPacket(new JoinGame
             {
                 EID = (int)eid,
-                GameMode = (byte)(((uint)gameMode.ModeClass) | (gameMode.IsHardcore ? 0b100u : 0u)),
+                GameMode = ToByte(gameMode),
                 Dimension = (int)dimension,
                 Difficulty = (byte)difficulty,
                 LevelType = levelType,
@@ -68,6 +69,25 @@ namespace MineCase.Server.Network.Play
             });
         }
 
+        public Task PlayerListItemAddPlayer(IReadOnlyList<PlayerDescription> desc)
+        {
+            return Sink.SendPacket(new PlayerListItem<PlayerListItemAddPlayerAction>
+            {
+                Action = 0,
+                NumberOfPlayers = (uint)desc.Count,
+                Players = (from d in desc
+                           select new PlayerListItemAddPlayerAction
+                           {
+                               UUID = d.UUID,
+                               Name = d.Name,
+                               NumberOfProperties = 0,
+                               GameMode = ToByte(d.GameMode),
+                               Ping = d.Ping,
+                               HasDisplayName = false
+                           }).ToArray()
+            });
+        }
+
         public Task WindowItems(byte windowId, IReadOnlyList<Game.Slot> slots)
         {
             return Sink.SendPacket(new WindowItems
@@ -78,6 +98,20 @@ namespace MineCase.Server.Network.Play
             });
         }
 
+        public Task PositionAndLook(double x, double y, double z, float yaw, float pitch, RelativeFlags relative, uint teleportId)
+        {
+            return Sink.SendPacket(new PositionAndLook
+            {
+                X = x,
+                Y = y,
+                Z = z,
+                Yaw = yaw,
+                Pitch = pitch,
+                Flags = (byte)relative,
+                TeleportId = teleportId
+            });
+        }
+
         private Protocol.Play.Slot TransformSlotData(Game.Slot o)
         {
             return new Protocol.Play.Slot
@@ -85,5 +119,20 @@ namespace MineCase.Server.Network.Play
                 BlockId = -1
             };
         }
+
+        public static byte ToByte(GameMode gameMode)
+        {
+            return (byte)(((uint)gameMode.ModeClass) | (gameMode.IsHardcore ? 0b100u : 0u));
+        }
+    }
+
+    [Flags]
+    public enum RelativeFlags : byte
+    {
+        X = 0x1,
+        Y = 0x2,
+        Z = 0x4,
+        Yaw = 0x8,
+        Pitch = 0x10
     }
 }

+ 7 - 0
src/MineCase.Server.Grains/User/NonAuthenticatedUserGrain.cs

@@ -20,5 +20,12 @@ namespace MineCase.Server.User
         {
             return Task.FromResult(_uuid);
         }
+
+        public async Task<IUser> GetUser()
+        {
+            var user = GrainFactory.GetGrain<IUser>(_uuid);
+            await user.SetName(this.GetPrimaryKeyString());
+            return user;
+        }
     }
 }

+ 65 - 6
src/MineCase.Server.Grains/User/UserGrain.cs

@@ -14,6 +14,7 @@ namespace MineCase.Server.User
 {
     class UserGrain : Grain, IUser
     {
+        private string _name;
         private string _worldId;
         private IWorld _world;
         private IClientboundPacketSink _sink;
@@ -24,6 +25,8 @@ namespace MineCase.Server.User
         private readonly Random _keepAliveIdRand = new Random();
         private const int ClientKeepInterval = 6;
         private bool _isOnline = false;
+        private DateTime _keepAliveRequestTime, _keepAliveResponseTime;
+        private UserState _state;
 
         private IPlayer _player;
 
@@ -62,8 +65,14 @@ namespace MineCase.Server.User
         {
             var playerEid = await _world.NewEntityId();
             _player = GrainFactory.GetGrain<IPlayer>(_world.MakeEntityKey(playerEid));
-            await _player.SetClientSink(_sink);
+            await _player.SetName(_name);
+            await _player.BindToUser(this, _sink);
             await _world.AttachEntity(_player);
+
+            _state = UserState.JoinedGame;
+            _keepAliveWaiters = new HashSet<uint>();
+            _sendKeepAliveTimer = RegisterTimer(OnSendKeepAliveRequests, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
+            //_worldTimeSyncTimer = RegisterTimer(OnSyncWorldTime, null, TimeSpan.Zero, )
         }
 
         private async Task SendTimeUpdate()
@@ -88,6 +97,7 @@ namespace MineCase.Server.User
             {
                 var id = (uint)_keepAliveIdRand.Next();
                 _keepAliveWaiters.Add(id);
+                _keepAliveRequestTime = DateTime.UtcNow;
                 await _generator.KeepAlive(id);
             }
         }
@@ -95,6 +105,8 @@ namespace MineCase.Server.User
         public Task KeepAlive(uint keepAliveId)
         {
             _keepAliveWaiters.Remove(keepAliveId);
+            if (_keepAliveWaiters.Count == 0)
+                _keepAliveResponseTime = DateTime.UtcNow;
             return Task.CompletedTask;
         }
 
@@ -117,13 +129,60 @@ namespace MineCase.Server.User
         {
             _isOnline = true;
             _keepAliveWaiters = new HashSet<uint>();
-            
+
             await SendTimeUpdate();
-            await _player.SendWholeInventory();
-            await _player.SendExperience();
+            await _player.NotifyLoggedIn();
+            _state = UserState.DownloadingWorld;
+        }
 
-            _sendKeepAliveTimer = RegisterTimer(OnSendKeepAliveRequests, null, TimeSpan.Zero, TimeSpan.FromSeconds(5));
-            //_worldTimeSyncTimer = RegisterTimer(OnSyncWorldTime, null, TimeSpan.Zero, )
+        public Task SetName(string name)
+        {
+            _name = name;
+            return Task.CompletedTask;
+        }
+
+        public Task<uint> GetPing()
+        {
+            uint ping;
+            var diff = DateTime.UtcNow - _keepAliveRequestTime;
+            if (diff.Ticks < 0)
+                ping = int.MaxValue;
+            else
+                ping = (uint)diff.TotalMilliseconds;
+            return Task.FromResult(ping);
+        }
+
+        public async Task OnGameTick(TimeSpan deltaTime)
+        {
+            await _player.SetPing(await GetPing());
+            if(_state == UserState.DownloadingWorld)
+            {
+                await _player.SendPositionAndLook();
+                _state = UserState.Playing;
+            }
+
+            if(_state >= UserState.JoinedGame && _state < UserState.Destroying)
+            {
+                for (int i = 0; i < 4; i++)
+                {
+                    if (!await StreamNextChunk())
+                        break;
+                }
+            }
+        }
+
+        private async Task<bool> StreamNextChunk()
+        {
+            return true;
+        }
+
+        enum UserState : uint
+        {
+            None,
+            JoinedGame,
+            DownloadingWorld,
+            Playing,
+            Destroying
         }
     }
 }

+ 10 - 2
src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs

@@ -10,10 +10,18 @@ namespace MineCase.Server.Game.Entities
 {
     public interface IPlayer : IEntity
     {
-        Task SetClientSink(IClientboundPacketSink sink);
-
+        Task<string> GetName();
+        Task SetName(string name);
+        Task BindToUser(IUser user, IClientboundPacketSink sink);
+        Task SetPing(uint ping);
+        
+        Task<PlayerDescription> GetDescription();
         Task<IInventoryWindow> GetInventory();
         Task SendWholeInventory();
         Task SendExperience();
+        Task SendPlayerListAddPlayer(IReadOnlyList<IPlayer> player);
+        Task NotifyLoggedIn();
+        Task SendPositionAndLook();
+        Task OnTeleportConfirm(uint teleportId);
     }
 }

+ 15 - 0
src/MineCase.Server.Interfaces/Game/Entities/PlayerDescription.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Server.Game.Entities
+{
+    public sealed class PlayerDescription
+    {
+        public Guid UUID { get; set; }
+        public string Name { get; set; }
+        public GameMode GameMode { get; set; }
+        public uint Ping { get; set; }
+        public string DisplayName { get; set; }
+    }
+}

+ 1 - 1
src/MineCase.Server.Interfaces/User/INonAuthenticatedPlayer.cs

@@ -8,6 +8,6 @@ namespace MineCase.Server.User
 {
     public interface INonAuthenticatedUser : IGrainWithStringKey
     {
-        Task<Guid> GetUUID();
+        Task<IUser> GetUser();
     }
 }

+ 4 - 0
src/MineCase.Server.Interfaces/User/IUser.cs

@@ -13,6 +13,7 @@ namespace MineCase.Server.User
 {
     public interface IUser : IGrainWithGuidKey
     {
+        Task SetName(string name);
         Task<IWorld> GetWorld();
         Task<IGameSession> GetGameSession();
         Task<IPlayer> GetPlayer();
@@ -22,5 +23,8 @@ namespace MineCase.Server.User
         Task JoinGame();
         Task NotifyLoggedIn();
         Task KeepAlive(uint keepAliveId);
+
+        Task<uint> GetPing();
+        Task OnGameTick(TimeSpan deltaTime);
     }
 }