Kaynağa Gözat

Fix several bugs in multiplay game (#118)

* Update fixedUpdate

* Fix positions

* Update

* Fix bugs that keep alive will kick player and we don't see reason

* Add packet compression

* fix bugs when open chest or furnace windows

* Lower cpu usage

* Fix player remove
sunnycase 8 yıl önce
ebeveyn
işleme
656de4cdd8
50 değiştirilmiş dosya ile 601 ekleme ve 187 silme
  1. 17 14
      src/MineCase.Client.Scripts/Network/ClientSession.cs
  2. 8 4
      src/MineCase.Core/World/Position.cs
  3. 1 0
      src/MineCase.Gateway/MineCase.Gateway.csproj
  4. 34 15
      src/MineCase.Gateway/Network/ClientSession.cs
  5. 1 0
      src/MineCase.Protocol/MineCase.Protocol.csproj
  6. 31 0
      src/MineCase.Protocol/Protocol/Login/SetCompression.cs
  7. 2 0
      src/MineCase.Protocol/Protocol/Packet.cs
  8. 29 18
      src/MineCase.Protocol/Protocol/PacketCompress.cs
  9. 4 0
      src/MineCase.Protocol/Protocol/Play/PlayerListItem.cs
  10. 6 0
      src/MineCase.Protocol/Protocol/Protocol.cs
  11. 4 1
      src/MineCase.Server.Grains/Components/AddressByPartitionKeyComponent.cs
  12. 14 14
      src/MineCase.Server.Grains/Components/FixedUpdateComponent.cs
  13. 6 13
      src/MineCase.Server.Grains/Game/BlockEntities/Components/ChestComponent.cs
  14. 1 3
      src/MineCase.Server.Grains/Game/BlockEntities/Components/FurnaceComponent.cs
  15. 10 16
      src/MineCase.Server.Grains/Game/Entities/Components/ChunkLoaderComponent.cs
  16. 4 2
      src/MineCase.Server.Grains/Game/Entities/Components/CollectorComponent.cs
  17. 1 1
      src/MineCase.Server.Grains/Game/Entities/Components/EntityAiComponent.cs
  18. 5 0
      src/MineCase.Server.Grains/Game/Entities/Components/EntityLifeTimeComponent.cs
  19. 10 22
      src/MineCase.Server.Grains/Game/Entities/Components/KeepAliveComponent.cs
  20. 7 2
      src/MineCase.Server.Grains/Game/Entities/Components/PlayerDiscoveryComponent.cs
  21. 9 2
      src/MineCase.Server.Grains/Game/Entities/Components/PlayerListComponent.cs
  22. 16 1
      src/MineCase.Server.Grains/Game/Entities/Components/ViewDistanceComponent.cs
  23. 2 2
      src/MineCase.Server.Grains/Game/Entities/Components/WindowManagerComponent.cs
  24. 0 1
      src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs
  25. 15 9
      src/MineCase.Server.Grains/Game/GameSession.cs
  26. 2 0
      src/MineCase.Server.Grains/Game/Windows/WindowGrain.cs
  27. 16 0
      src/MineCase.Server.Grains/MineCase.Server.Grains.csproj
  28. 11 2
      src/MineCase.Server.Grains/Network/ClientboundPacketSinkGrain.cs
  29. 11 0
      src/MineCase.Server.Grains/Network/Login/LoginFlowGrain.cs
  30. 14 0
      src/MineCase.Server.Grains/Network/Play/ClientPlayPacketGenerator.cs
  31. 4 2
      src/MineCase.Server.Grains/Network/Play/ClientboundPacketComponent.cs
  32. 4 2
      src/MineCase.Server.Grains/Network/Play/ForwardToPlayerPacketSink.cs
  33. 8 6
      src/MineCase.Server.Grains/User/UserChunkLoaderGrain.cs
  34. 24 4
      src/MineCase.Server.Grains/User/UserGrain.cs
  35. 1 1
      src/MineCase.Server.Grains/World/ChunkColumnGrain.cs
  36. 1 1
      src/MineCase.Server.Grains/World/ChunkTrackingHub.cs
  37. 6 6
      src/MineCase.Server.Grains/World/CollectableFinder.cs
  38. 46 20
      src/MineCase.Server.Grains/World/TickEmitterGrain.cs
  39. 43 0
      src/MineCase.Server.Grains/World/WorldPartitionGrain.cs
  40. 7 1
      src/MineCase.Server.Interfaces/Game/Entities/EntityMessages.cs
  41. 1 0
      src/MineCase.Server.Interfaces/Game/IGameSession.cs
  42. 2 0
      src/MineCase.Server.Interfaces/Network/IClientboundPacketObserver.cs
  43. 2 0
      src/MineCase.Server.Interfaces/Network/IClientboundPacketSink.cs
  44. 3 0
      src/MineCase.Server.Interfaces/User/IUser.cs
  45. 1 1
      src/MineCase.Server.Interfaces/User/IUserChunkLoader.cs
  46. 2 0
      src/MineCase.Server.Interfaces/World/ITickEmitter.cs
  47. 4 0
      src/MineCase.Server.Interfaces/World/IWorldPartition.cs
  48. 1 1
      src/MineCase.Server/server.json
  49. 4 0
      tests/UnitTest/MineCase.UnitTest.csproj
  50. 146 0
      tests/UnitTest/PositionTest.cs

+ 17 - 14
src/MineCase.Client.Scripts/Network/ClientSession.cs

@@ -70,20 +70,23 @@ namespace MineCase.Client.Network
 
         private async Task SendOutcomingPacket(UncompressedPacket packet)
         {
-            // Close
-            if (packet == null)
-            {
-                _tcpClient.Client.Shutdown(SocketShutdown.Send);
-                _outcomingPacketDispatcher.Complete();
-            }
-            else if (_useCompression)
-            {
-                var newPacket = PacketCompress.Compress(ref packet);
-                await newPacket.SerializeAsync(_remoteStream);
-            }
-            else
+            using (var bufferScope = _bufferPool.CreateScope())
             {
-                await packet.SerializeAsync(_remoteStream);
+                // Close
+                if (packet == null)
+                {
+                    _tcpClient.Client.Shutdown(SocketShutdown.Send);
+                    _outcomingPacketDispatcher.Complete();
+                }
+                else if (_useCompression)
+                {
+                    var newPacket = PacketCompress.Compress(packet, bufferScope, 0);
+                    await newPacket.SerializeAsync(_remoteStream);
+                }
+                else
+                {
+                    await packet.SerializeAsync(_remoteStream);
+                }
             }
         }
 
@@ -97,7 +100,7 @@ namespace MineCase.Client.Network
                     if (_useCompression)
                     {
                         var compressedPacket = await CompressedPacket.DeserializeAsync(_remoteStream, null);
-                        packet = PacketCompress.Decompress(ref compressedPacket);
+                        packet = PacketCompress.Decompress(compressedPacket, bufferScope, 0, packet);
                     }
                     else
                     {

+ 8 - 4
src/MineCase.Core/World/Position.cs

@@ -49,8 +49,8 @@ namespace MineCase.World
         {
             int chunkPosX = X / ChunkConstants.BlockEdgeWidthInSection;
             int chunkPosZ = Z / ChunkConstants.BlockEdgeWidthInSection;
-            if (X < 0) chunkPosX -= 1;
-            if (Z < 0) chunkPosZ -= 1;
+            if (X < 0) chunkPosX--;
+            if (Z < 0) chunkPosZ--;
             return new ChunkWorldPos(chunkPosX, chunkPosZ);
         }
 
@@ -206,7 +206,11 @@ namespace MineCase.World
 
         public BlockWorldPos ToBlockWorldPos()
         {
-            return new BlockWorldPos(X * 16, 0, Z * 16);
+            var x = X * 16;
+            if (x < 0) x++;
+            var z = Z * 16;
+            if (z < 0) z++;
+            return new BlockWorldPos(x, 0, z);
         }
 
         public override bool Equals(object obj)
@@ -337,7 +341,7 @@ namespace MineCase.World
 
         public EntityWorldPos ToEntityWorldPos(ChunkWorldPos pos)
         {
-            return new EntityWorldPos(X + pos.X, Y, Z + pos.Z);
+            return new EntityWorldPos(pos.X * 16 + X, Y, pos.Z * 16 + Z);
         }
 
         public static EntityChunkPos Add(EntityChunkPos pos, float x, float y, float z)

+ 1 - 0
src/MineCase.Gateway/MineCase.Gateway.csproj

@@ -36,6 +36,7 @@
     <PackageReference Include="Microsoft.Extensions.ObjectPool" Version="2.0.0" />
     <PackageReference Include="Microsoft.Orleans.Client" Version="2.0.0-beta1" />
     <PackageReference Include="Polly" Version="5.5.0" />
+    <PackageReference Include="sharpcompress" Version="0.19.2" />
   </ItemGroup>
 
   <ItemGroup>

+ 34 - 15
src/MineCase.Gateway/Network/ClientSession.cs

@@ -20,13 +20,17 @@ namespace MineCase.Gateway.Network
         private readonly TcpClient _tcpClient;
         private Stream _remoteStream;
         private readonly IGrainFactory _grainFactory;
-        private bool _useCompression = false;
+        private volatile bool _useCompression = false;
         private readonly Guid _sessionId;
         private readonly OutcomingPacketObserver _outcomingPacketObserver;
-        private readonly ActionBlock<UncompressedPacket> _outcomingPacketDispatcher;
+        private IClientboundPacketObserver _clientboundPacketObserverRef;
+        private readonly ActionBlock<object> _outcomingPacketDispatcher;
         private readonly ObjectPool<UncompressedPacket> _uncompressedPacketObjectPool;
         private readonly IBufferPool<byte> _bufferPool;
 
+        private readonly object _useCompressionPacket = new object();
+        private uint _compressThreshold;
+
         public ClientSession(TcpClient tcpClient, IGrainFactory grainFactory, IBufferPool<byte> bufferPool, ObjectPool<UncompressedPacket> uncompressedPacketObjectPool)
         {
             _sessionId = Guid.NewGuid();
@@ -35,15 +39,15 @@ namespace MineCase.Gateway.Network
             _bufferPool = bufferPool;
             _uncompressedPacketObjectPool = uncompressedPacketObjectPool;
             _outcomingPacketObserver = new OutcomingPacketObserver(this);
-            _outcomingPacketDispatcher = new ActionBlock<UncompressedPacket>(SendOutcomingPacket);
+            _outcomingPacketDispatcher = new ActionBlock<object>(SendOutcomingPacket);
         }
 
         public async Task Startup(CancellationToken cancellationToken)
         {
             using (_remoteStream = _tcpClient.GetStream())
             {
-                var observerRef = await _grainFactory.CreateObjectReference<IClientboundPacketObserver>(_outcomingPacketObserver);
-                await _grainFactory.GetGrain<IClientboundPacketSink>(_sessionId).Subscribe(observerRef);
+                _clientboundPacketObserverRef = await _grainFactory.CreateObjectReference<IClientboundPacketObserver>(_outcomingPacketObserver);
+                await _grainFactory.GetGrain<IClientboundPacketSink>(_sessionId).Subscribe(_clientboundPacketObserverRef);
                 try
                 {
                     while (!cancellationToken.IsCancellationRequested &&
@@ -77,7 +81,7 @@ namespace MineCase.Gateway.Network
                     if (_useCompression)
                     {
                         var compressedPacket = await CompressedPacket.DeserializeAsync(_remoteStream, null);
-                        packet = PacketCompress.Decompress(ref compressedPacket);
+                        packet = PacketCompress.Decompress(compressedPacket, bufferScope, _compressThreshold, packet);
                     }
                     else
                     {
@@ -92,22 +96,31 @@ namespace MineCase.Gateway.Network
             }
         }
 
-        private async Task SendOutcomingPacket(UncompressedPacket packet)
+        private async Task SendOutcomingPacket(object packetOrCommand)
         {
-            // Close
-            if(packet == null)
+            if (packetOrCommand == null)
             {
                 _tcpClient.Client.Shutdown(SocketShutdown.Send);
                 _outcomingPacketDispatcher.Complete();
             }
-            else if (_useCompression)
+            else if (packetOrCommand == _useCompressionPacket)
             {
-                var newPacket = PacketCompress.Compress(ref packet);
-                await newPacket.SerializeAsync(_remoteStream);
+                _useCompression = true;
             }
-            else
+            else if (packetOrCommand is UncompressedPacket packet)
             {
-                await packet.SerializeAsync(_remoteStream);
+                using (var bufferScope = _bufferPool.CreateScope())
+                {
+                    if (_useCompression)
+                    {
+                        var newPacket = PacketCompress.Compress(packet, bufferScope, _compressThreshold);
+                        await newPacket.SerializeAsync(_remoteStream);
+                    }
+                    else
+                    {
+                        await packet.SerializeAsync(_remoteStream);
+                    }
+                }
             }
         }
 
@@ -117,7 +130,7 @@ namespace MineCase.Gateway.Network
             await router.SendPacket(packet);
         }
 
-        private async void DispatchOutcomingPacket(UncompressedPacket packet)
+        private async void DispatchOutcomingPacket(object packet)
         {
             try
             {
@@ -148,6 +161,12 @@ namespace MineCase.Gateway.Network
             {
                 _session.DispatchOutcomingPacket(packet);
             }
+
+            public void UseCompression(uint threshold)
+            {
+                _session._compressThreshold = threshold;
+                _session.DispatchOutcomingPacket(_session._useCompressionPacket);
+            }
         }
 
         #region IDisposable Support

+ 1 - 0
src/MineCase.Protocol/MineCase.Protocol.csproj

@@ -24,6 +24,7 @@
     <PackageReference Include="System.Numerics.Vectors" Version="4.4.0" />
     <PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="4.4.0" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004" PrivateAssets="All" />
+    <PackageReference Include="sharpcompress" Version="0.19.2"/>
   </ItemGroup>
 
   <ItemGroup Condition="'$(TargetFramework)' == 'net46'">

+ 31 - 0
src/MineCase.Protocol/Protocol/Login/SetCompression.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MineCase.Serialization;
+
+namespace MineCase.Protocol.Login
+{
+#if !NET46
+    [Orleans.Concurrency.Immutable]
+#endif
+    [Packet(0x03)]
+    public sealed class SetCompression : ISerializablePacket
+    {
+        [SerializeAs(DataType.VarInt)]
+        public uint Threshold;
+
+        public static SetCompression Deserialize(ref SpanReader br)
+        {
+            return new SetCompression
+            {
+                Threshold = br.ReadAsVarInt(out _),
+            };
+        }
+
+        public void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsVarInt(Threshold, out _);
+        }
+    }
+}

+ 2 - 0
src/MineCase.Protocol/Protocol/Packet.cs

@@ -44,6 +44,7 @@ namespace MineCase.Protocol
             using (var br = new BinaryReader(stream, Encoding.UTF8, true))
             {
                 packet.Length = br.ReadAsVarInt(out _);
+                Protocol.ValidatePacketLength(packet.Length);
                 packet.PacketId = br.ReadAsVarInt(out packetIdLen);
             }
 
@@ -88,6 +89,7 @@ namespace MineCase.Protocol
             using (var br = new BinaryReader(stream, Encoding.UTF8, true))
             {
                 packet.PacketLength = br.ReadAsVarInt(out _);
+                Protocol.ValidatePacketLength(packet.PacketLength);
                 packet.DataLength = br.ReadAsVarInt(out dataLengthLen);
             }
 

+ 29 - 18
src/MineCase.Protocol/Protocol/PacketCompress.cs

@@ -1,46 +1,57 @@
 using System;
 using System.Collections.Generic;
 using System.IO;
-using System.IO.Compression;
 using System.Text;
+using MineCase.Buffers;
 using MineCase.Serialization;
+using SharpCompress.Compressors;
+using SharpCompress.Compressors.Deflate;
 
 namespace MineCase.Protocol
 {
     public static class PacketCompress
     {
-        public static UncompressedPacket Decompress(ref CompressedPacket packet)
+        public static UncompressedPacket Decompress(CompressedPacket packet, IBufferPoolScope<byte> bufferPool, uint threshold, UncompressedPacket targetPacket = null)
         {
-            throw new NotImplementedException();
-            /*
-            var targetPacket = default(UncompressedPacket);
-            using (var br = new BinaryReader(new DeflateStream(new MemoryStream(packet.CompressedData), CompressionMode.Decompress)))
+            if (packet.DataLength != 0 && packet.DataLength < threshold)
+                throw new InvalidDataException("Uncompressed data length is lower than threshold.");
+            bool useCompression = packet.DataLength != 0;
+            var dataLength = useCompression ? packet.DataLength : (uint)packet.CompressedData.Length;
+
+            targetPacket = targetPacket ?? new UncompressedPacket();
+            using (var stream = new MemoryStream(packet.CompressedData))
+            using (var br = new BinaryReader(useCompression ? (Stream)new ZlibStream(stream, CompressionMode.Decompress, true) : stream))
             {
                 targetPacket.PacketId = br.ReadAsVarInt(out var packetIdLen);
-                targetPacket.Data = br.ReadBytes((int)packet.DataLength - packetIdLen);
+
+                targetPacket.Data = bufferPool.Rent((int)(dataLength - packetIdLen));
+                br.Read(targetPacket.Data.Array, targetPacket.Data.Offset, targetPacket.Data.Count);
             }
 
             targetPacket.Length = packet.DataLength;
-            return targetPacket;*/
+            return targetPacket;
         }
 
-        public static CompressedPacket Compress(ref UncompressedPacket packet)
+        public static CompressedPacket Compress(UncompressedPacket packet, IBufferPoolScope<byte> bufferPool, uint threshold)
         {
-            throw new NotImplementedException();
-            /*
-            var targetPacket = default(CompressedPacket);
+            var targetPacket = new CompressedPacket();
             using (var stream = new MemoryStream())
-            using (var bw = new BinaryWriter(new DeflateStream(stream, CompressionMode.Compress)))
             {
-                bw.WriteAsVarInt(packet.PacketId, out var packetIdLen);
-                bw.Write(packet.Data);
-                bw.Flush();
+                var dataLength = packet.PacketId.SizeOfVarInt() + (uint)packet.Data.Count;
+                bool useCompression = dataLength >= threshold;
+                targetPacket.DataLength = useCompression ? dataLength : 0;
+
+                using (var bw = new BinaryWriter(useCompression ? (Stream)new ZlibStream(stream, CompressionMode.Compress, true) : stream))
+                {
+                    bw.WriteAsVarInt(packet.PacketId, out _);
+                    bw.Write(packet.Data.Array, packet.Data.Offset, packet.Data.Count);
+                    bw.Flush();
+                }
 
-                targetPacket.DataLength = packetIdLen + (uint)packet.Data.Length;
                 targetPacket.CompressedData = stream.ToArray();
             }
 
-            return targetPacket;*/
+            return targetPacket;
         }
     }
 }

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

@@ -75,4 +75,8 @@ namespace MineCase.Protocol.Play
                 throw new NotImplementedException();
         }
     }
+
+    public sealed class PlayerListItemRemovePlayerAction : PlayerListItemAction
+    {
+    }
 }

+ 6 - 0
src/MineCase.Protocol/Protocol/Protocol.cs

@@ -7,5 +7,11 @@ namespace MineCase.Protocol
     public static class Protocol
     {
         public const uint Version = 335;
+
+        public static void ValidatePacketLength(uint length)
+        {
+            if (length > 16 * 1024)
+                throw new ArgumentOutOfRangeException("Packet is too large.");
+        }
     }
 }

+ 4 - 1
src/MineCase.Server.Grains/Components/AddressByPartitionKeyComponent.cs

@@ -43,6 +43,8 @@ namespace MineCase.Server.Components
             UpdateKey();
         }
 
+        private ChunkWorldPos? _oldChunkWorldPos;
+
         private void UpdateKey()
         {
             if (AttachedObject.TryGetWorld(out var world))
@@ -53,8 +55,9 @@ namespace MineCase.Server.Components
                 else if (AttachedObject.TryGetLocalValue(BlockWorldPositionComponent.BlockWorldPositionProperty, out var blockPos))
                     chunkWorldPos = blockPos.ToChunkWorldPos();
 
-                if (chunkWorldPos.HasValue)
+                if (chunkWorldPos.HasValue && _oldChunkWorldPos != chunkWorldPos)
                 {
+                    _oldChunkWorldPos = chunkWorldPos;
                     var key = world.MakeAddressByPartitionKey(chunkWorldPos.Value);
                     AttachedObject.SetLocalValue(AddressByPartitionKeyProperty, key);
                 }

+ 14 - 14
src/MineCase.Server.Grains/Components/FixedUpdateComponent.cs

@@ -17,7 +17,7 @@ namespace MineCase.Server.Components
         private long _worldAge;
         private long _actualAge;
         private TimeSpan _lastUpdate;
-        private static readonly long _updateTick = TimeSpan.FromMilliseconds(50).Ticks;
+        private static readonly long _updateMs = 50;
 
         public event AsyncEventHandler<GameTickArgs> Tick;
 
@@ -33,27 +33,27 @@ namespace MineCase.Server.Components
             _actualAge = 0;
             _stopwatch = new Stopwatch();
             _stopwatch.Start();
-            _tickTimer = AttachedObject.RegisterTimer(OnTick, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(5));
+            _tickTimer = AttachedObject.RegisterTimer(OnTick, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(1));
         }
 
         private async Task OnTick(object arg)
         {
-            var expectedAge = _stopwatch.ElapsedTicks / _updateTick;
-            if (_stopwatch.ElapsedTicks % _updateTick > 0) expectedAge++;
-            var e = new GameTickArgs { DeltaTime = TimeSpan.FromMilliseconds(50) };
+            var expectedAge = (_stopwatch.ElapsedMilliseconds + _updateMs - 1) / _updateMs;
             var updateTimes = expectedAge - _actualAge;
-            var now = _stopwatch.Elapsed;
-            for (int i = 0; i < updateTimes; i++)
-            {
-                e.WorldAge = _worldAge;
-                e.TimeOfDay = _worldAge % 24000;
-                await Tick.InvokeSerial(this, e);
-                _worldAge++;
-                _actualAge++;
-            }
 
             if (updateTimes > 0)
             {
+                var e = new GameTickArgs { DeltaTime = TimeSpan.FromMilliseconds(50) };
+                for (int i = 0; i < updateTimes; i++)
+                {
+                    e.WorldAge = _worldAge;
+                    e.TimeOfDay = _worldAge % 24000;
+                    await Tick.InvokeSerial(this, e);
+                    _worldAge++;
+                    _actualAge++;
+                }
+
+                var now = _stopwatch.Elapsed;
                 var deltaTime = now - _lastUpdate;
                 _lastUpdate = now;
             }

+ 6 - 13
src/MineCase.Server.Grains/Game/BlockEntities/Components/ChestComponent.cs

@@ -15,10 +15,10 @@ namespace MineCase.Server.Game.BlockEntities.Components
     internal class ChestComponent : Component<BlockEntityGrain>, IHandle<NeighborEntityChanged>, IHandle<DestroyBlockEntity>, IHandle<UseBy>
     {
         public static readonly DependencyProperty<IBlockEntity> NeighborEntityProperty =
-            DependencyProperty.Register("NeighborEntity", typeof(ChestComponent), new PropertyMetadata<IBlockEntity>(null, OnNeighborEntityChanged));
+            DependencyProperty.Register(nameof(NeighborEntity), typeof(ChestComponent), new PropertyMetadata<IBlockEntity>(null, OnNeighborEntityChanged));
 
         public static readonly DependencyProperty<IChestWindow> ChestWindowProperty =
-            DependencyProperty.Register<IChestWindow>("ChestWindow", typeof(ChestComponent));
+            DependencyProperty.Register<IChestWindow>(nameof(ChestWindow), typeof(ChestComponent));
 
         public IBlockEntity NeighborEntity => AttachedObject.GetValue(NeighborEntityProperty);
 
@@ -67,16 +67,14 @@ namespace MineCase.Server.Game.BlockEntities.Components
         async Task IHandle<UseBy>.Handle(UseBy message)
         {
             var masterEntity = await FindMasterEntity(NeighborEntity);
-            if (masterEntity.GetPrimaryKey() == AttachedObject.GetPrimaryKey())
+            if (object.Equals(masterEntity, AttachedObject.AsReference<IBlockEntity>()))
             {
                 if (ChestWindow == null)
-                {
                     AttachedObject.SetLocalValue(ChestWindowProperty, GrainFactory.GetGrain<IChestWindow>(Guid.NewGuid()));
-                    await ChestWindow.SetEntities((NeighborEntity == null ?
-                        new[] { AttachedObject.AsReference<IDependencyObject>() } :
-                        new[] { AttachedObject.AsReference<IDependencyObject>(), NeighborEntity }).AsImmutable());
-                }
 
+                await ChestWindow.SetEntities((NeighborEntity == null ?
+                    new[] { AttachedObject.AsReference<IDependencyObject>() } :
+                    new[] { AttachedObject.AsReference<IDependencyObject>(), NeighborEntity }).AsImmutable());
                 await message.Entity.Tell(new OpenWindow { Window = ChestWindow });
             }
             else
@@ -98,10 +96,5 @@ namespace MineCase.Server.Game.BlockEntities.Components
                     orderby e.position.X, e.position.Z
                     select e.entity).First();
         }
-
-        private void MarkDirty()
-        {
-            AttachedObject.ValueStorage.IsDirty = true;
-        }
     }
 }

+ 1 - 3
src/MineCase.Server.Grains/Game/BlockEntities/Components/FurnaceComponent.cs

@@ -240,11 +240,9 @@ namespace MineCase.Server.Game.BlockEntities.Components
         async Task IHandle<UseBy>.Handle(UseBy message)
         {
             if (FurnaceWindow == null)
-            {
                 AttachedObject.SetLocalValue(FurnaceWindowProperty, GrainFactory.GetGrain<IFurnaceWindow>(Guid.NewGuid()));
-                await FurnaceWindow.SetEntity(AttachedObject);
-            }
 
+            await FurnaceWindow.SetEntity(AttachedObject);
             await message.Entity.Tell(new OpenWindow { Window = FurnaceWindow });
         }
 

+ 10 - 16
src/MineCase.Server.Grains/Game/Entities/Components/ChunkLoaderComponent.cs

@@ -10,10 +10,11 @@ using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class ChunkLoaderComponent : Component<PlayerGrain>, IHandle<BeginLogin>, IHandle<PlayerLoggedIn>, IHandle<BindToUser>
+    internal class ChunkLoaderComponent : Component<UserGrain>, IHandle<BeginLogin>, IHandle<PlayerLoggedIn>, IHandle<BindToUser>, IHandle<GameTick>
     {
         private IUserChunkLoader _chunkLoader;
         private bool _loaded;
+        private IPlayer _player;
 
         public ChunkLoaderComponent(string name = "chunkLoader")
             : base(name)
@@ -24,31 +25,18 @@ namespace MineCase.Server.Game.Entities.Components
         {
             _loaded = false;
             _chunkLoader = GrainFactory.GetGrain<IUserChunkLoader>(AttachedObject.GetPrimaryKey());
-            AttachedObject.RegisterPropertyChangedHandler(ViewDistanceComponent.ViewDistanceProperty, OnViewDistanceChanged);
-            AttachedObject.GetComponent<GameTickComponent>().Tick += OnGameTick;
-        }
-
-        private Task OnGameTick(object sender, GameTickArgs e)
-        {
-            if (_loaded)
-                return _chunkLoader.OnGameTick(e.WorldAge);
-            return Task.CompletedTask;
-        }
-
-        private void OnViewDistanceChanged(object sender, PropertyChangedEventArgs<byte> e)
-        {
-            AttachedObject.QueueOperation(() => _chunkLoader.SetViewDistance(e.NewValue));
         }
 
         async Task IHandle<PlayerLoggedIn>.Handle(PlayerLoggedIn message)
         {
             _loaded = false;
-            await _chunkLoader.JoinGame(AttachedObject.GetWorld(), AttachedObject);
+            await _chunkLoader.JoinGame(await AttachedObject.GetWorld(), _player);
             _loaded = true;
         }
 
         async Task IHandle<BindToUser>.Handle(BindToUser message)
         {
+            _player = GrainFactory.GetGrain<IPlayer>(message.User.GetPrimaryKey());
             await _chunkLoader.SetClientPacketSink(await message.User.GetClientPacketSink());
         }
 
@@ -57,5 +45,11 @@ namespace MineCase.Server.Game.Entities.Components
             _loaded = false;
             return Task.CompletedTask;
         }
+
+        async Task IHandle<GameTick>.Handle(GameTick message)
+        {
+            if (_loaded)
+                await _chunkLoader.OnGameTick(message.Args, await _player.GetPosition());
+        }
     }
 }

+ 4 - 2
src/MineCase.Server.Grains/Game/Entities/Components/CollectorComponent.cs

@@ -5,6 +5,7 @@ using System.Text;
 using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.Components;
+using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
@@ -17,8 +18,9 @@ namespace MineCase.Server.Game.Entities.Components
 
         Task IHandle<CollisionWith>.Handle(CollisionWith message)
         {
-            return Task.WhenAll(from e in message.Entities
-                                select e.Tell(new CollectBy { Entity = AttachedObject }));
+            foreach (var e in message.Entities)
+                e.InvokeOneWay(g => g.Tell(new CollectBy { Entity = AttachedObject }));
+            return Task.CompletedTask;
         }
     }
 }

+ 1 - 1
src/MineCase.Server.Grains/Game/Entities/Components/EntityAiComponent.cs

@@ -40,7 +40,7 @@ namespace MineCase.Server.Game.Entities.Components
 
         protected override void OnAttached()
         {
-            Register();
+            // Register();
             AttachedObject.SetLocalValue(EntityAiComponent.CreatureStateProperty, CreatureState.Stop);
             CreateAi(MobType);
             AttachedObject.RegisterPropertyChangedHandler(MobTypeComponent.MobTypeProperty, OnMobTypeChanged);

+ 5 - 0
src/MineCase.Server.Grains/Game/Entities/Components/EntityLifeTimeComponent.cs

@@ -29,6 +29,11 @@ namespace MineCase.Server.Game.Entities.Components
         async Task IHandle<DestroyEntity>.Handle(DestroyEntity message)
         {
             await AttachedObject.Tell(Disable.Default);
+            AttachedObject.QueueOperation(() =>
+            {
+                AttachedObject.Destroy();
+                return Task.CompletedTask;
+            });
         }
     }
 }

+ 10 - 22
src/MineCase.Server.Grains/Game/Entities/Components/KeepAliveComponent.cs

@@ -12,26 +12,12 @@ namespace MineCase.Server.Game.Entities.Components
     internal class KeepAliveComponent : Component, IHandle<BeginLogin>, IHandle<PlayerLoggedIn>, IHandle<KickPlayer>
     {
         private uint _keepAliveId = 0;
-        public readonly HashSet<uint> _keepAliveWaiters = new HashSet<uint>();
-        private DateTime _keepAliveRequestTime;
-        private DateTime _keepAliveResponseTime;
+        public readonly Dictionary<uint, DateTime> _keepAliveWaiters = new Dictionary<uint, DateTime>();
         private bool _isOnline = false;
 
         private const int ClientKeepInterval = 6;
 
-        public uint Ping
-        {
-            get
-            {
-                uint ping;
-                var diff = DateTime.UtcNow - _keepAliveRequestTime;
-                if (diff.Ticks < 0)
-                    ping = int.MaxValue;
-                else
-                    ping = (uint)diff.TotalMilliseconds;
-                return ping;
-            }
-        }
+        public uint Ping { get; private set; }
 
         public KeepAliveComponent(string name = "keepAlive")
             : base(name)
@@ -40,9 +26,12 @@ namespace MineCase.Server.Game.Entities.Components
 
         public Task ReceiveResponse(uint keepAliveId)
         {
-            _keepAliveWaiters.Remove(keepAliveId);
-            if (_keepAliveWaiters.Count == 0)
-                _keepAliveResponseTime = DateTime.UtcNow;
+            if (_keepAliveWaiters.TryGetValue(keepAliveId, out var sendTime))
+            {
+                _keepAliveWaiters.Remove(keepAliveId);
+                Ping = (uint)(DateTime.UtcNow - sendTime).TotalMilliseconds;
+            }
+
             return Task.CompletedTask;
         }
 
@@ -53,11 +42,10 @@ namespace MineCase.Server.Game.Entities.Components
                 _isOnline = false;
                 await AttachedObject.Tell(new KickPlayer());
             }
-            else if (e.WorldAge % 20 == 0)
+            else if (_isOnline && e.WorldAge % 20 == 0)
             {
                 var id = _keepAliveId++;
-                _keepAliveWaiters.Add(id);
-                _keepAliveRequestTime = DateTime.UtcNow;
+                _keepAliveWaiters.Add(id, DateTime.UtcNow);
                 await AttachedObject.GetComponent<ClientboundPacketComponent>().GetGenerator().KeepAlive(id);
             }
         }

+ 7 - 2
src/MineCase.Server.Grains/Game/Entities/Components/PlayerDiscoveryComponent.cs

@@ -39,8 +39,13 @@ namespace MineCase.Server.Game.Entities.Components
 
         Task IHandle<DestroyEntity>.Handle(DestroyEntity message)
         {
-            return AttachedObject.GetComponent<ChunkEventBroadcastComponent>().GetGenerator()
-                .DestroyEntities(new[] { AttachedObject.EntityId });
+            if (AttachedObject.EntityId != 0)
+            {
+                return AttachedObject.GetComponent<ChunkEventBroadcastComponent>().GetGenerator()
+                    .DestroyEntities(new[] { AttachedObject.EntityId });
+            }
+
+            return Task.CompletedTask;
         }
     }
 }

+ 9 - 2
src/MineCase.Server.Grains/Game/Entities/Components/PlayerListComponent.cs

@@ -10,7 +10,7 @@ using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class PlayerListComponent : Component<PlayerGrain>, IHandle<PlayerLoggedIn>, IHandle<PlayerListUpdate>, IHandle<AskPlayerDescription, PlayerDescription>
+    internal class PlayerListComponent : Component<PlayerGrain>, IHandle<PlayerLoggedIn>, IHandle<PlayerListAdd>, IHandle<AskPlayerDescription, PlayerDescription>, IHandle<PlayerListRemove>
     {
         public PlayerListComponent(string name = "playerList")
             : base(name)
@@ -22,7 +22,7 @@ namespace MineCase.Server.Game.Entities.Components
             await SendPlayerListAddPlayer(new[] { AttachedObject });
         }
 
-        async Task IHandle<PlayerListUpdate>.Handle(PlayerListUpdate message)
+        async Task IHandle<PlayerListAdd>.Handle(PlayerListAdd message)
         {
             await SendPlayerListAddPlayer(message.Players);
         }
@@ -38,6 +38,13 @@ namespace MineCase.Server.Game.Entities.Components
             });
         }
 
+        Task IHandle<PlayerListRemove>.Handle(PlayerListRemove message)
+        {
+            return AttachedObject.GetComponent<ChunkEventBroadcastComponent>().GetGenerator()
+                .PlayerListItemRemovePlayer((from p in message.Players
+                                             select p.GetPrimaryKey()).ToList());
+        }
+
         private async Task SendPlayerListAddPlayer(IReadOnlyList<IPlayer> players)
         {
             var desc = await Task.WhenAll(from p in players

+ 16 - 1
src/MineCase.Server.Grains/Game/Entities/Components/ViewDistanceComponent.cs

@@ -2,17 +2,32 @@
 using System.Collections.Generic;
 using System.Text;
 using MineCase.Engine;
+using MineCase.Server.User;
+using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class ViewDistanceComponent : Component
+    internal class ViewDistanceComponent : Component<PlayerGrain>
     {
         public static readonly DependencyProperty<byte> ViewDistanceProperty =
             DependencyProperty.Register("ViewDistance", typeof(ViewDistanceComponent), new PropertyMetadata<byte>(10));
 
+        private IUserChunkLoader _chunkLoader;
+
         public ViewDistanceComponent(string name = "viewDistance")
             : base(name)
         {
         }
+
+        protected override void OnAttached()
+        {
+            _chunkLoader = GrainFactory.GetGrain<IUserChunkLoader>(AttachedObject.GetPrimaryKey());
+            AttachedObject.RegisterPropertyChangedHandler(ViewDistanceComponent.ViewDistanceProperty, OnViewDistanceChanged);
+        }
+
+        private void OnViewDistanceChanged(object sender, PropertyChangedEventArgs<byte> e)
+        {
+            AttachedObject.QueueOperation(() => _chunkLoader.SetViewDistance(e.NewValue));
+        }
     }
 }

+ 2 - 2
src/MineCase.Server.Grains/Game/Entities/Components/WindowManagerComponent.cs

@@ -45,7 +45,7 @@ namespace MineCase.Server.Game.Entities.Components
         public async Task OpenWindow(IWindow window)
         {
             var id = (from w in _windows
-                      where w.Value.Window.GetPrimaryKey() == window.GetPrimaryKey()
+                      where object.Equals(w.Value.Window, window)
                       select (byte?)w.Key).FirstOrDefault();
             if (id == null)
             {
@@ -73,7 +73,7 @@ namespace MineCase.Server.Game.Entities.Components
             OpenWindow(message.Window);
 
         Task<byte> IHandle<AskWindowId, byte>.Handle(AskWindowId message) =>
-            Task.FromResult(_windows.First(o => o.Value.Window.GetPrimaryKey() == message.Window.GetPrimaryKey()).Key);
+            Task.FromResult(_windows.First(o => object.Equals(o.Value.Window, message.Window)).Key);
 
         private class WindowContext
         {

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

@@ -32,7 +32,6 @@ namespace MineCase.Server.Game.Entities
             SetComponent(new ActiveWorldPartitionComponent());
             SetComponent(new BlockPlacementComponent());
             SetComponent(new ClientboundPacketComponent());
-            SetComponent(new ChunkLoaderComponent());
             SetComponent(new DiggingComponent());
             SetComponent(new DiscoveryRegisterComponent());
             SetComponent(new DraggedSlotComponent());

+ 15 - 9
src/MineCase.Server.Grains/Game/GameSession.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Collections.Immutable;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
@@ -8,6 +9,7 @@ using Microsoft.Extensions.Logging;
 using MineCase.Engine;
 using MineCase.Protocol.Play;
 using MineCase.Server.Components;
+using MineCase.Server.Game.Entities;
 using MineCase.Server.Network.Play;
 using MineCase.Server.Persistence.Components;
 using MineCase.Server.Settings;
@@ -23,7 +25,6 @@ namespace MineCase.Server.Game
     internal class GameSession : DependencyObject, IGameSession
     {
         private IWorld _world;
-        private IChunkSender _chunkSender;
         private FixedUpdateComponent _fixedUpdate;
         private readonly Dictionary<IUser, UserContext> _users = new Dictionary<IUser, UserContext>();
 
@@ -34,7 +35,6 @@ namespace MineCase.Server.Game
             await base.OnActivateAsync();
             _logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger<GameSession>();
             _world = await GrainFactory.GetGrain<IWorldAccessor>(0).GetWorld(this.GetPrimaryKeyString());
-            _chunkSender = GrainFactory.GetGrain<IChunkSender>(this.GetPrimaryKeyString());
             await _fixedUpdate.Start(_world);
         }
 
@@ -51,7 +51,7 @@ namespace MineCase.Server.Game
         {
             await _world.OnGameTick(e);
             await Task.WhenAll(from u in _users.Keys
-                         select u.OnGameTick(e));
+                               select u.OnGameTick(e));
         }
 
         public async Task JoinGame(IUser user)
@@ -75,21 +75,27 @@ namespace MineCase.Server.Game
                 LevelTypes.Default,
                 false);
             await user.NotifyLoggedIn();
-            await UpdatePlayerList();
+            await SendWholePlayersList(user);
         }
 
-        public Task LeaveGame(IUser player)
+        public Task LeaveGame(IUser user)
         {
-            _users.Remove(player);
-            return UpdatePlayerList();
+            _users.Remove(user);
+            return BroadcastRemovePlayerFromList(user);
         }
 
-        public async Task UpdatePlayerList()
+        private async Task BroadcastRemovePlayerFromList(IUser user)
+        {
+            var players = new List<IPlayer> { await user.GetPlayer() };
+            await Task.WhenAll(from u in _users.Keys select u.RemovePlayerList(players));
+        }
+
+        public async Task SendWholePlayersList(IUser user)
         {
             var list = await Task.WhenAll(from p in _users.Keys
                                           select p.GetPlayer());
 
-            await Task.WhenAll(from p in _users.Keys select p.UpdatePlayerList(list));
+            await user.UpdatePlayerList(list);
         }
 
         public async Task SendChatMessage(IUser sender, string message)

+ 2 - 0
src/MineCase.Server.Grains/Game/Windows/WindowGrain.cs

@@ -9,6 +9,7 @@ using MineCase.Server.Game.Entities.Components;
 using MineCase.Server.Game.Windows.SlotAreas;
 using MineCase.Server.Network;
 using MineCase.Server.Network.Play;
+using MineCase.Server.Persistence;
 using MineCase.Server.World;
 using Orleans;
 using Orleans.Concurrency;
@@ -201,6 +202,7 @@ namespace MineCase.Server.Game.Windows
 
             await Task.WhenAll(from p in _players select SendCloseWindow(p));
             await Task.WhenAll(from p in _players select Close(p));
+            DeactivateOnIdle();
         }
 
         public Task BroadcastSlotChanged(int slotIndex, Slot item)

+ 16 - 0
src/MineCase.Server.Grains/MineCase.Server.Grains.csproj

@@ -11,6 +11,22 @@
     <DebugSymbols>true</DebugSymbols>
   </PropertyGroup>
 
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='TravisCI|AnyCPU'">
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+
+  <PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Appveyor|AnyCPU'">
+    <LangVersion>latest</LangVersion>
+  </PropertyGroup>
+
   <ItemGroup>
     <Compile Remove="GameSystems\**" />
     <EmbeddedResource Remove="GameSystems\**" />

+ 11 - 2
src/MineCase.Server.Grains/Network/ClientboundPacketSinkGrain.cs

@@ -11,7 +11,6 @@ using Orleans.Concurrency;
 
 namespace MineCase.Server.Network
 {
-    [Reentrant]
     internal class ClientboundPacketSinkGrain : Grain, IClientboundPacketSink
     {
         private ObserverSubscriptionManager<IClientboundPacketObserver> _subsManager;
@@ -55,15 +54,25 @@ namespace MineCase.Server.Network
                 PacketId = packetId,
                 Data = new ArraySegment<byte>(data.Value)
             };
-            _subsManager.Notify(n => n.ReceivePacket(packet));
+            if (_subsManager.Count == 0)
+                DeactivateOnIdle();
+            else
+                _subsManager.Notify(n => n.ReceivePacket(packet));
             return Task.CompletedTask;
         }
 
         public Task Close()
         {
             _subsManager.Notify(n => n.OnClosed());
+            _subsManager.Clear();
             DeactivateOnIdle();
             return Task.CompletedTask;
         }
+
+        public Task NotifyUseCompression(uint threshold)
+        {
+            _subsManager.Notify(n => n.UseCompression(threshold));
+            return Task.CompletedTask;
+        }
     }
 }

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

@@ -16,6 +16,8 @@ namespace MineCase.Server.Network.Login
     {
         private bool _useAuthentication = false;
 
+        private const uint CompressPacketThreshold = 256;
+
         public async Task DispatchPacket(LoginStart packet)
         {
             if (_useAuthentication)
@@ -43,6 +45,8 @@ namespace MineCase.Server.Network.Login
                 }
                 else
                 {
+                    await SendSetCompression();
+
                     var uuid = user.GetPrimaryKey();
                     await SendLoginSuccess(packet.Name, uuid);
 
@@ -57,6 +61,13 @@ namespace MineCase.Server.Network.Login
             }
         }
 
+        private async Task SendSetCompression()
+        {
+            var sink = GrainFactory.GetGrain<IClientboundPacketSink>(this.GetPrimaryKey());
+            await sink.SendPacket(new SetCompression { Threshold = CompressPacketThreshold });
+            await sink.NotifyUseCompression(CompressPacketThreshold);
+        }
+
         private async Task SendLoginSuccess(string userName, Guid uuid)
         {
             var sink = GrainFactory.GetGrain<IClientboundPacketSink>(this.GetPrimaryKey());

+ 14 - 0
src/MineCase.Server.Grains/Network/Play/ClientPlayPacketGenerator.cs

@@ -381,6 +381,20 @@ namespace MineCase.Server.Network.Play
             });
         }
 
+        public Task PlayerListItemRemovePlayer(IReadOnlyList<Guid> desc)
+        {
+            return SendPacket(new PlayerListItem<PlayerListItemRemovePlayerAction>
+            {
+                Action = 4,
+                NumberOfPlayers = (uint)desc.Count,
+                Players = (from d in desc
+                           select new PlayerListItemRemovePlayerAction
+                           {
+                               UUID = d
+                           }).ToArray()
+            });
+        }
+
         public Task WindowItems(byte windowId, IReadOnlyList<Slot> slots)
         {
             return SendPacket(new WindowItems

+ 4 - 2
src/MineCase.Server.Grains/Network/Play/ClientboundPacketComponent.cs

@@ -28,7 +28,7 @@ namespace MineCase.Server.Network.Play
         public async Task Kick()
         {
             await AttachedObject.Tell(Disable.Default);
-            if (_user.GetPlayer() == AttachedObject.AsReference<IPlayer>())
+            if (object.Equals(await _user.GetPlayer(), AttachedObject.AsReference<IPlayer>()))
                 await _user.Kick();
         }
 
@@ -45,7 +45,9 @@ namespace MineCase.Server.Network.Play
 
         Task IHandle<PacketForwardToPlayer>.Handle(PacketForwardToPlayer message)
         {
-            return _sink.SendPacket(message.PacketId, message.Data.AsImmutable());
+            if (_sink != null)
+                return _sink.SendPacket(message.PacketId, message.Data.AsImmutable());
+            return Task.CompletedTask;
         }
     }
 }

+ 4 - 2
src/MineCase.Server.Grains/Network/Play/ForwardToPlayerPacketSink.cs

@@ -4,6 +4,7 @@ using System.Text;
 using System.Threading.Tasks;
 using MineCase.Protocol;
 using MineCase.Server.Game.Entities;
+using Orleans;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.Network.Play
@@ -27,11 +28,12 @@ namespace MineCase.Server.Network.Play
 
         public Task SendPacket(uint packetId, Immutable<byte[]> data)
         {
-            return _player.Tell(new PacketForwardToPlayer
+            _player.InvokeOneWay(e => e.Tell(new PacketForwardToPlayer
             {
                 PacketId = packetId,
                 Data = data.Value
-            });
+            }));
+            return Task.CompletedTask;
         }
     }
 }

+ 8 - 6
src/MineCase.Server.Grains/User/UserChunkLoaderGrain.cs

@@ -35,21 +35,23 @@ namespace MineCase.Server.User
             return Task.CompletedTask;
         }
 
-        public async Task OnGameTick(long worldAge)
+        public async Task OnGameTick(GameTickArgs e, EntityWorldPos playerPosition)
         {
-            for (int i = 0; i < 4; i++)
+            if (e.WorldAge % 10 == 0)
             {
-                if (await StreamNextChunk()) break;
+                for (int i = 0; i < 4 && _sendingChunks.Count <= 4; i++)
+                {
+                    if (await StreamNextChunk(playerPosition.ToChunkWorldPos())) break;
+                }
             }
 
             // unload per 5 seconds
-            if (worldAge % 100 == 0)
+            if (e.WorldAge % 100 == 0)
                 await UnloadOutOfRangeChunks();
         }
 
-        private async Task<bool> StreamNextChunk()
+        private async Task<bool> StreamNextChunk(ChunkWorldPos currentChunk)
         {
-            var currentChunk = (await _player.GetPosition()).ToChunkWorldPos();
             if (_lastStreamedChunk.HasValue && _lastStreamedChunk.Value == currentChunk) return true;
 
             for (int d = 0; d <= _viewDistance; d++)

+ 24 - 4
src/MineCase.Server.Grains/User/UserGrain.cs

@@ -65,6 +65,11 @@ namespace MineCase.Server.User
             }
         }
 
+        protected override void InitializeComponents()
+        {
+            SetComponent(new ChunkLoaderComponent());
+        }
+
         public Task<IClientboundPacketSink> GetClientPacketSink()
         {
             return Task.FromResult(_sink);
@@ -90,29 +95,33 @@ namespace MineCase.Server.User
         public async Task JoinGame()
         {
             _player = GrainFactory.GetGrain<IPlayer>(this.GetPrimaryKey());
-            await _player.Tell(Disable.Default);
+            await _player.Tell(DestroyEntity.Default);
+            await Tell(BeginLogin.Default);
             await _player.Tell(BeginLogin.Default);
+            await Tell(new BindToUser { User = this.AsReference<IUser>() });
             await _player.Tell(new BindToUser { User = this.AsReference<IUser>() });
 
             _userState = UserState.JoinedGame;
 
             // 设置出生点
             var world = State.World;
-            var spawnPosition = await world.GetSpawnPosition();
             await _player.Tell(new SpawnEntity
             {
                 World = world,
                 EntityId = await world.NewEntityId(),
-                Position = spawnPosition
+                Position = State.SpawnPosition ?? await world.GetSpawnPosition()
             });
         }
 
         public async Task Kick()
         {
+            State.SpawnPosition = await _player.GetPosition();
+            MarkDirty();
             await _player.Tell(DestroyEntity.Default);
             var game = await GetGameSession();
             await game.LeaveGame(this);
             await _sink.Close();
+            _sink = null;
             DeactivateOnIdle();
         }
 
@@ -120,13 +129,14 @@ namespace MineCase.Server.User
 
         public async Task NotifyLoggedIn()
         {
+            await Tell(PlayerLoggedIn.Default);
             await _player.Tell(PlayerLoggedIn.Default);
             _userState = UserState.DownloadingWorld;
         }
 
         public Task UpdatePlayerList(IReadOnlyList<IPlayer> players)
         {
-            return _player.Tell(new PlayerListUpdate { Players = players });
+            return _player.Tell(new PlayerListAdd { Players = players });
         }
 
         public Task SendChatMessage(Chat jsonData, byte position)
@@ -170,10 +180,13 @@ namespace MineCase.Server.User
             if (_userState == UserState.DownloadingWorld)
             {
                 _userState = UserState.Playing;
+                await _generator.TimeUpdate(e.WorldAge, e.TimeOfDay);
             }
 
             if (e.WorldAge % 20 == 0)
                 await _generator.TimeUpdate(e.WorldAge, e.TimeOfDay);
+
+            await Tell(new GameTick { Args = e });
         }
 
         public Task SetPacketRouter(IPacketRouter packetRouter)
@@ -214,6 +227,11 @@ namespace MineCase.Server.User
             ValueStorage.IsDirty = true;
         }
 
+        public Task RemovePlayerList(List<IPlayer> players)
+        {
+            return _player.Tell(new PlayerListRemove { Players = players });
+        }
+
         internal class StateHolder
         {
             public string Name { get; set; }
@@ -224,6 +242,8 @@ namespace MineCase.Server.User
 
             public GameMode GameMode { get; set; }
 
+            public EntityWorldPos? SpawnPosition { get; set; }
+
             public StateHolder()
             {
             }

+ 1 - 1
src/MineCase.Server.Grains/World/ChunkColumnGrain.cs

@@ -83,7 +83,7 @@ namespace MineCase.Server.World
                     // 删除旧的 BlockEntity
                     if (state.BlockEntities.TryGetValue(chunkPos, out var entity))
                     {
-                        if (newEntity != null && entity.GetPrimaryKeyString() == newEntity.GetPrimaryKeyString())
+                        if (object.Equals(entity, newEntity))
                             replaceOld = false;
 
                         if (replaceOld)

+ 1 - 1
src/MineCase.Server.Grains/World/ChunkTrackingHub.cs

@@ -20,7 +20,7 @@ namespace MineCase.Server.World
     internal class ChunkTrackingHub : Grain, IChunkTrackingHub
     {
         private readonly IPacketPackager _packetPackager;
-        private Dictionary<IPlayer, IPacketSink> _trackingPlayers = new Dictionary<IPlayer, IPacketSink>();
+        private readonly Dictionary<IPlayer, IPacketSink> _trackingPlayers = new Dictionary<IPlayer, IPacketSink>();
         private BroadcastPacketSink _broadcastPacketSink;
 
         public ChunkTrackingHub(IPacketPackager packetPackager)

+ 6 - 6
src/MineCase.Server.Grains/World/CollectableFinder.cs

@@ -70,13 +70,13 @@ namespace MineCase.Server.World
 
         private async Task Collision(IDependencyObject entity, Shape colliderShape)
         {
-            var result = (await Task.WhenAll(from finder in _neighborFinders
-                                             where colliderShape.CollideWith(finder.box)
-                                             select finder.finder.CollisionInChunk(colliderShape)))
-                                             .SelectMany(o => o).ToList();
+            var result = new HashSet<IDependencyObject>((await Task.WhenAll(from finder in _neighborFinders
+                                                                            where colliderShape.CollideWith(finder.box)
+                                                                            select finder.finder.CollisionInChunk(colliderShape)))
+                                                                            .SelectMany(o => o));
             result.Remove(entity);
-            if (result.Any())
-                await entity.Tell(new CollisionWith { Entities = result });
+            if (result.Count != 0)
+                entity.InvokeOneWay(e => e.Tell(new CollisionWith { Entities = result }));
         }
 
         public async Task SpawnPickup(Vector3 position, Immutable<Slot[]> slots)

+ 46 - 20
src/MineCase.Server.Grains/World/TickEmitterGrain.cs

@@ -6,6 +6,7 @@ using System.Text;
 using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.Components;
+using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Persistence;
 using MineCase.Server.Persistence.Components;
@@ -17,47 +18,72 @@ using Orleans.Streams;
 namespace MineCase.Server.World
 {
     [Reentrant]
+    [PersistTableName("tickEmitter")]
     internal class TickEmitterGrain : AddressByPartitionGrain, ITickEmitter
     {
-        private ImmutableHashSet<IDependencyObject> _tickables = ImmutableHashSet<IDependencyObject>.Empty;
+        private IWorldPartition _worldPartition;
 
-        private FixedUpdateComponent _fixedUpdate;
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
 
-        protected override void InitializeComponents()
+        protected override void InitializePreLoadComponent()
         {
-            SetComponent(new PeriodicSaveStateComponent(TimeSpan.FromMinutes(1)));
-
-            _fixedUpdate = new FixedUpdateComponent();
-            _fixedUpdate.Tick += OnFixedUpdate;
-            SetComponent(_fixedUpdate);
+            SetComponent(new StateComponent<StateHolder>());
+            _worldPartition = GrainFactory.GetPartitionGrain<IWorldPartition>(this);
         }
 
-        private Task OnFixedUpdate(object sender, GameTickArgs e)
+        protected override void InitializeComponents()
         {
-            var msg = new GameTick { Args = e };
-            return Task.WhenAll(from t in _tickables select t.Tell(msg));
+            SetComponent(new PeriodicSaveStateComponent(TimeSpan.FromMinutes(1)));
         }
 
-        public async Task Subscribe(IDependencyObject observer)
+        public Task Subscribe(IDependencyObject observer)
         {
-            bool active = _tickables.IsEmpty;
-            _tickables = _tickables.Add(observer);
-
-            if (active)
+            bool active = State.Subscription.Count == 0;
+            if (State.Subscription.Add(observer))
             {
-                await _fixedUpdate.Start(World);
+                MarkDirty();
+                if (active)
+                    return _worldPartition.SubscribeTickEmitter(this);
             }
+
+            return Task.CompletedTask;
         }
 
         public Task Unsubscribe(IDependencyObject observer)
         {
-            _tickables = _tickables.Remove(observer);
-            if (_tickables.IsEmpty)
+            if (State.Subscription.Remove(observer))
             {
-                _fixedUpdate.Stop();
+                MarkDirty();
+                if (State.Subscription.Count == 0)
+                    return _worldPartition.UnsubscribeTickEmitter(this);
             }
 
             return Task.CompletedTask;
         }
+
+        public Task OnGameTick(GameTickArgs e)
+        {
+            var msg = new GameTick { Args = e };
+            return Task.WhenAll(from t in State.Subscription select t.Tell(msg));
+        }
+
+        private void MarkDirty()
+        {
+            ValueStorage.IsDirty = true;
+        }
+
+        internal class StateHolder
+        {
+            public HashSet<IDependencyObject> Subscription { get; set; }
+
+            public StateHolder()
+            {
+            }
+
+            public StateHolder(InitializeStateMark mark)
+            {
+                Subscription = new HashSet<IDependencyObject>();
+            }
+        }
     }
 }

+ 43 - 0
src/MineCase.Server.Grains/World/WorldPartitionGrain.cs

@@ -21,21 +21,33 @@ namespace MineCase.Server.World
     internal class WorldPartitionGrain : AddressByPartitionGrain, IWorldPartition
     {
         private HashSet<IPlayer> _players = new HashSet<IPlayer>();
+        private ITickEmitter _tickEmitter;
+        private FixedUpdateComponent _fixedUpdate;
 
         private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
 
         protected override void InitializePreLoadComponent()
         {
             SetComponent(new StateComponent<StateHolder>());
+            _tickEmitter = GrainFactory.GetPartitionGrain<ITickEmitter>(this);
         }
 
         protected override void InitializeComponents()
         {
             SetComponent(new PeriodicSaveStateComponent(TimeSpan.FromMinutes(1)));
+            _fixedUpdate = new FixedUpdateComponent();
+            _fixedUpdate.Tick += OnFixedUpdate;
+            SetComponent(_fixedUpdate);
+        }
+
+        private Task OnFixedUpdate(object sender, GameTickArgs e)
+        {
+            return _tickEmitter.OnGameTick(e);
         }
 
         public Task Enter(IPlayer player)
         {
+            bool active = _players.Count == 0;
             if (_players.Add(player))
             {
                 var message = new DiscoveredByPlayer { Player = player };
@@ -44,6 +56,9 @@ namespace MineCase.Server.World
                     if (entity.Equals(player)) continue;
                     entity.InvokeOneWay(g => g.Tell(message));
                 }
+
+                if (active && State.IsTickEmitterActive)
+                    return _fixedUpdate.Start(World);
             }
 
             return Task.CompletedTask;
@@ -55,6 +70,7 @@ namespace MineCase.Server.World
             {
                 if (_players.Count == 0)
                 {
+                    _fixedUpdate.Stop();
                     DeactivateOnIdle();
                 }
             }
@@ -85,10 +101,37 @@ namespace MineCase.Server.World
             ValueStorage.IsDirty = true;
         }
 
+        public Task SubscribeTickEmitter(ITickEmitter tickEmitter)
+        {
+            if (!State.IsTickEmitterActive)
+            {
+                State.IsTickEmitterActive = true;
+                MarkDirty();
+                if (_players.Count != 0)
+                    return _fixedUpdate.Start(World);
+            }
+
+            return Task.CompletedTask;
+        }
+
+        public Task UnsubscribeTickEmitter(ITickEmitter tickEmitter)
+        {
+            if (State.IsTickEmitterActive)
+            {
+                State.IsTickEmitterActive = false;
+                MarkDirty();
+                _fixedUpdate.Stop();
+            }
+
+            return Task.CompletedTask;
+        }
+
         internal class StateHolder
         {
             public HashSet<IEntity> DiscoveryEntities { get; set; }
 
+            public bool IsTickEmitterActive { get; set; }
+
             public StateHolder()
             {
             }

+ 7 - 1
src/MineCase.Server.Interfaces/Game/Entities/EntityMessages.cs

@@ -17,7 +17,13 @@ namespace MineCase.Server.Game.Entities.Components
     }
 
     [Immutable]
-    public sealed class PlayerListUpdate : IEntityMessage
+    public sealed class PlayerListAdd : IEntityMessage
+    {
+        public IReadOnlyList<IPlayer> Players { get; set; }
+    }
+
+    [Immutable]
+    public sealed class PlayerListRemove : IEntityMessage
     {
         public IReadOnlyList<IPlayer> Players { get; set; }
     }

+ 1 - 0
src/MineCase.Server.Interfaces/Game/IGameSession.cs

@@ -5,6 +5,7 @@ using System.Threading.Tasks;
 
 using MineCase.Protocol.Play;
 using MineCase.Server.User;
+using MineCase.Server.World;
 using Orleans;
 
 namespace MineCase.Server.Game

+ 2 - 0
src/MineCase.Server.Interfaces/Network/IClientboundPacketObserver.cs

@@ -10,6 +10,8 @@ namespace MineCase.Server.Network
     {
         void ReceivePacket(UncompressedPacket packet);
 
+        void UseCompression(uint threshold);
+
         void OnClosed();
     }
 }

+ 2 - 0
src/MineCase.Server.Interfaces/Network/IClientboundPacketSink.cs

@@ -14,6 +14,8 @@ namespace MineCase.Server.Network
 
         Task UnSubscribe(IClientboundPacketObserver observer);
 
+        Task NotifyUseCompression(uint threshold);
+
         Task Close();
     }
 }

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

@@ -43,6 +43,9 @@ namespace MineCase.Server.User
         [OneWay]
         Task UpdatePlayerList(IReadOnlyList<IPlayer> players);
 
+        [OneWay]
+        Task RemovePlayerList(List<IPlayer> players);
+
         Task SendChatMessage(Chat jsonData, Byte position);
 
         [OneWay]

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

@@ -17,7 +17,7 @@ namespace MineCase.Server.User
 
         Task JoinGame(IWorld world, IPlayer player);
 
-        Task OnGameTick(long worldAge);
+        Task OnGameTick(GameTickArgs e, EntityWorldPos playerPosition);
 
         [OneWay]
         Task OnChunkSent(ChunkWorldPos chunkPos);

+ 2 - 0
src/MineCase.Server.Interfaces/World/ITickEmitter.cs

@@ -15,5 +15,7 @@ namespace MineCase.Server.World
         Task Subscribe(IDependencyObject observer);
 
         Task Unsubscribe(IDependencyObject observer);
+
+        Task OnGameTick(GameTickArgs e);
     }
 }

+ 4 - 0
src/MineCase.Server.Interfaces/World/IWorldPartition.cs

@@ -17,5 +17,9 @@ namespace MineCase.Server.World
         Task SubscribeDiscovery(IEntity entity);
 
         Task UnsubscribeDiscovery(IEntity entity);
+
+        Task SubscribeTickEmitter(ITickEmitter tickEmitter);
+
+        Task UnsubscribeTickEmitter(ITickEmitter tickEmitter);
     }
 }

+ 1 - 1
src/MineCase.Server/server.json

@@ -3,7 +3,7 @@
 {
     //server
     "motd": "A Minecase Server",
-    "server-ip": "127.0.0.1",
+    "server-ip": "0.0.0.0",
     "server-port": 25565,
     "max-tick-time": 60000,
     "online-mode": false,

+ 4 - 0
tests/UnitTest/MineCase.UnitTest.csproj

@@ -31,4 +31,8 @@
     <ProjectReference Include="..\..\src\MineCase.Server.Interfaces\MineCase.Server.Interfaces.csproj" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Compile Include="..\..\src\MineCase.Protocol\Serialization\BinaryWriterExtensions.cs" Link="BinaryWriterExtensions.cs" />
+  </ItemGroup>
+
 </Project>

+ 146 - 0
tests/UnitTest/PositionTest.cs

@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MineCase.Serialization;
+using MineCase.World;
+using Xunit;
+
+namespace MineCase.UnitTest
+{
+    public class PositionTest
+    {
+        private readonly BlockWorldPos _bwPos1 = new BlockWorldPos(0, 0, 0);
+        private readonly BlockWorldPos _bwPos2 = new BlockWorldPos(1, 1, 1);
+        private readonly BlockWorldPos _bwPos3 = new BlockWorldPos(17, 17, 17);
+        private readonly BlockWorldPos _bwPos4 = new BlockWorldPos(-1, 1, -1);
+        private readonly BlockWorldPos _bwPos5 = new BlockWorldPos(-17, 17, -17);
+
+        private readonly BlockChunkPos _bcPos1 = new BlockChunkPos(0, 0, 0);
+        private readonly BlockChunkPos _bcPos2 = new BlockChunkPos(1, 1, 1);
+        private readonly BlockChunkPos _bcPos3 = new BlockChunkPos(1, 17, 1);
+        private readonly BlockChunkPos _bcPos4 = new BlockChunkPos(15, 1, 15);
+        private readonly BlockChunkPos _bcPos5 = new BlockChunkPos(15, 17, 15);
+
+        private readonly ChunkWorldPos _cwPos1 = new ChunkWorldPos(0, 0);
+        private readonly ChunkWorldPos _cwPos2 = new ChunkWorldPos(0, 0);
+        private readonly ChunkWorldPos _cwPos3 = new ChunkWorldPos(1, 1);
+        private readonly ChunkWorldPos _cwPos4 = new ChunkWorldPos(-1, -1);
+        private readonly ChunkWorldPos _cwPos5 = new ChunkWorldPos(-2, -2);
+
+        private readonly BlockWorldPos _cbwPos1 = new BlockWorldPos(0, 0, 0);
+        private readonly BlockWorldPos _cbwPos2 = new BlockWorldPos(0, 0, 0);
+        private readonly BlockWorldPos _cbwPos3 = new BlockWorldPos(16, 0, 16);
+        private readonly BlockWorldPos _cbwPos4 = new BlockWorldPos(-15, 0, -15);
+        private readonly BlockWorldPos _cbwPos5 = new BlockWorldPos(-31, 0, -31);
+
+        private readonly EntityWorldPos _ewPos1 = new EntityWorldPos(0, 0, 0);
+        private readonly EntityWorldPos _ewPos2 = new EntityWorldPos(1.5f, 1.5f, 1.5f);
+        private readonly EntityWorldPos _ewPos3 = new EntityWorldPos(17.5f, 17.5f, 17.5f);
+        private readonly EntityWorldPos _ewPos4 = new EntityWorldPos(-0.5f, 1.5f, -0.5f);
+        private readonly EntityWorldPos _ewPos5 = new EntityWorldPos(-16.5f, 17.5f, -16.5f);
+
+        private readonly EntityChunkPos _ecPos1 = new EntityChunkPos(0, 0, 0);
+        private readonly EntityChunkPos _ecPos2 = new EntityChunkPos(1.5f, 1.5f, 1.5f);
+        private readonly EntityChunkPos _ecPos3 = new EntityChunkPos(1.5f, 17.5f, 1.5f);
+        private readonly EntityChunkPos _ecPos4 = new EntityChunkPos(15.5f, 1.5f, 15.5f);
+        private readonly EntityChunkPos _ecPos5 = new EntityChunkPos(15.5f, 17.5f, 15.5f);
+
+        [Fact]
+        public void TestBlockWorldPos()
+        {
+            Assert.Equal(_bcPos1, _bwPos1.ToBlockChunkPos());
+            Assert.Equal(_bcPos2, _bwPos2.ToBlockChunkPos());
+            Assert.Equal(_bcPos3, _bwPos3.ToBlockChunkPos());
+            Assert.Equal(_bcPos4, _bwPos4.ToBlockChunkPos());
+            Assert.Equal(_bcPos5, _bwPos5.ToBlockChunkPos());
+
+            Assert.Equal(_cwPos1, _bwPos1.ToChunkWorldPos());
+            Assert.Equal(_cwPos2, _bwPos2.ToChunkWorldPos());
+            Assert.Equal(_cwPos3, _bwPos3.ToChunkWorldPos());
+            Assert.Equal(_cwPos4, _bwPos4.ToChunkWorldPos());
+            Assert.Equal(_cwPos5, _bwPos5.ToChunkWorldPos());
+        }
+
+        [Fact]
+        public void TestBlockChunkPos()
+        {
+            Assert.Equal(_bwPos1, _bcPos1.ToBlockWorldPos(_cwPos1));
+            Assert.Equal(_bwPos2, _bcPos2.ToBlockWorldPos(_cwPos2));
+            Assert.Equal(_bwPos3, _bcPos3.ToBlockWorldPos(_cwPos3));
+            Assert.Equal(_bwPos4, _bcPos4.ToBlockWorldPos(_cwPos4));
+            Assert.Equal(_bwPos5, _bcPos5.ToBlockWorldPos(_cwPos5));
+        }
+
+        [Fact]
+        public void TestChunkWorldPos()
+        {
+            Assert.Equal(_cbwPos1, _cwPos1.ToBlockWorldPos());
+            Assert.Equal(_cbwPos2, _cwPos2.ToBlockWorldPos());
+            Assert.Equal(_cbwPos3, _cwPos3.ToBlockWorldPos());
+            Assert.Equal(_cbwPos4, _cwPos4.ToBlockWorldPos());
+            Assert.Equal(_cbwPos5, _cwPos5.ToBlockWorldPos());
+        }
+
+        [Fact]
+        public void TestEntityWorldPos_ToChunkWorldPos()
+        {
+            Assert.Equal(_cwPos1, _ewPos1.ToChunkWorldPos());
+            Assert.Equal(_cwPos2, _ewPos2.ToChunkWorldPos());
+            Assert.Equal(_cwPos3, _ewPos3.ToChunkWorldPos());
+            Assert.Equal(_cwPos4, _ewPos4.ToChunkWorldPos());
+            Assert.Equal(_cwPos5, _ewPos5.ToChunkWorldPos());
+        }
+
+        [Fact]
+        public void TestEntityWorldPos_ToBlockWorldPos()
+        {
+            Assert.Equal(_bwPos1, _ewPos1.ToBlockWorldPos());
+            Assert.Equal(_bwPos2, _ewPos2.ToBlockWorldPos());
+            Assert.Equal(_bwPos3, _ewPos3.ToBlockWorldPos());
+            Assert.Equal(_bwPos4, _ewPos4.ToBlockWorldPos());
+            Assert.Equal(_bwPos5, _ewPos5.ToBlockWorldPos());
+        }
+
+        [Fact]
+        public void TestEntityWorldPos_ToEntityChunkPos()
+        {
+            Assert.Equal(_ecPos1, _ewPos1.ToEntityChunkPos());
+            Assert.Equal(_ecPos2, _ewPos2.ToEntityChunkPos());
+            Assert.Equal(_ecPos3, _ewPos3.ToEntityChunkPos());
+            Assert.Equal(_ecPos4, _ewPos4.ToEntityChunkPos());
+            Assert.Equal(_ecPos5, _ewPos5.ToEntityChunkPos());
+        }
+
+        [Fact]
+        public void TestEntityChunkPos_ToEntityWorldPos()
+        {
+            Assert.Equal(_ewPos1, _ecPos1.ToEntityWorldPos(_cwPos1));
+            Assert.Equal(_ewPos2, _ecPos2.ToEntityWorldPos(_cwPos2));
+            Assert.Equal(_ewPos3, _ecPos3.ToEntityWorldPos(_cwPos3));
+            Assert.Equal(_ewPos4, _ecPos4.ToEntityWorldPos(_cwPos4));
+            Assert.Equal(_ewPos5, _ecPos5.ToEntityWorldPos(_cwPos5));
+        }
+
+        [Fact]
+        public void TestPositionReadWrites()
+        {
+            TestPositionReadWrite(_bwPos1);
+            TestPositionReadWrite(_bwPos2);
+            TestPositionReadWrite(_bwPos3);
+            TestPositionReadWrite(_bwPos4);
+            TestPositionReadWrite(_bwPos5);
+        }
+
+        private void TestPositionReadWrite(Position position)
+        {
+            using (var mem = new MemoryStream())
+            using (var bw = new BinaryWriter(mem, Encoding.UTF8, true))
+            {
+                bw.WriteAsPosition(position);
+                var br = new SpanReader(mem.ToArray());
+                Assert.Equal(position, br.ReadAsPosition());
+            }
+        }
+    }
+}