Ver código fonte

Add world persistence (#94)

* Add mongodb config

* Start adding serialization

* Add persistence grain infrastructure

* Pass basic serialize test

* Update

* Update

* Update

* improve performance

* Update

* Fix dead lock in ClearOperationQueue

* Update sln

* Fix filename

* Fix bugs
sunnycase 8 anos atrás
pai
commit
65bd758ca1
100 arquivos alterados com 2600 adições e 409 exclusões
  1. 1 1
      .travis.yml
  2. 5 0
      build/docker/linux/docker-compose.yml
  3. 5 0
      build/docker/win/docker-compose.yml
  4. 98 0
      src/Common/Engine/Data/DependencyValueStorage.Serialize.cs
  5. 26 2
      src/Common/Engine/Data/DependencyValueStorage.cs
  6. 5 0
      src/Common/Engine/Data/IDependencyValueStorage.cs
  7. 5 0
      src/Common/Engine/Data/IEffectiveValue.cs
  8. 7 0
      src/Common/Engine/Data/LocalDependencyValueProvider.cs
  9. 12 42
      src/Common/Engine/DependencyObject.cs
  10. 46 1
      src/Common/Engine/DependencyProperty.cs
  11. 28 0
      src/Common/Engine/DependencyPropertyHelper.cs
  12. 17 0
      src/MineCase.Client.Engine/DependencyObject.Client.cs
  13. 1 0
      src/MineCase.Client.Engine/MineCase.Client.Engine.csproj
  14. 31 1
      src/MineCase.Core/Graphics/Cuboid.cs
  15. 64 2
      src/MineCase.Core/Graphics/Point.cs
  16. 33 1
      src/MineCase.Core/Graphics/Size.cs
  17. 15 0
      src/MineCase.Core/World/GameTickArgs.cs
  18. 119 0
      src/MineCase.Engine/DependencyObject.Server.cs
  19. 3 0
      src/MineCase.Engine/MineCase.Server.Engine.csproj
  20. 86 0
      src/MineCase.Engine/Serialization/DependencyObjectSerializer.cs
  21. 22 0
      src/MineCase.Engine/Serialization/EntityMessages.cs
  22. 18 0
      src/MineCase.Serialization/MineCase.Serialization.csproj
  23. 298 0
      src/MineCase.Serialization/Serializers/BlockChunkPosSerializer.cs
  24. 68 0
      src/MineCase.Serialization/Serializers/ChunkColumnCompactStorageSerializer.cs
  25. 117 0
      src/MineCase.Serialization/Serializers/ChunkSectionCompactStorageSerializer.cs
  26. 65 0
      src/MineCase.Serialization/Serializers/GrainRerferenceSerializer.cs
  27. 59 0
      src/MineCase.Serialization/Serializers/Point3DSerializer.cs
  28. 40 0
      src/MineCase.Serialization/Serializers/Serializers.cs
  29. 59 0
      src/MineCase.Serialization/Serializers/SizeSerializer.cs
  30. 58 0
      src/MineCase.Serialization/Serializers/SlotSerializer.cs
  31. 3 3
      src/MineCase.Server.Grains/Components/EntityLookComponent.cs
  32. 1 1
      src/MineCase.Server.Grains/Components/EntityOnGroundComponent.cs
  33. 30 10
      src/MineCase.Server.Grains/Components/GameTickComponent.cs
  34. 31 0
      src/MineCase.Server.Grains/Components/IsEnabledComponent.cs
  35. 8 1
      src/MineCase.Server.Grains/Game/BlockEntities/BlockEntityGrain.cs
  36. 6 0
      src/MineCase.Server.Grains/Game/BlockEntities/Components/BlockEntityLiftTimeComponent.cs
  37. 5 0
      src/MineCase.Server.Grains/Game/BlockEntities/Components/ChestComponent.cs
  38. 90 49
      src/MineCase.Server.Grains/Game/BlockEntities/Components/FurnaceComponent.cs
  39. 2 0
      src/MineCase.Server.Grains/Game/ChunkSenderGrain.cs
  40. 1 0
      src/MineCase.Server.Grains/Game/ChunkSenderJobWorker.cs
  41. 16 3
      src/MineCase.Server.Grains/Game/Entities/Components/ChunkLoaderComponent.cs
  42. 40 11
      src/MineCase.Server.Grains/Game/Entities/Components/ColliderComponent.cs
  43. 32 6
      src/MineCase.Server.Grains/Game/Entities/Components/DiscoveryRegisterComponent.cs
  44. 31 21
      src/MineCase.Server.Grains/Game/Entities/Components/EntityAiComponent.cs
  45. 7 6
      src/MineCase.Server.Grains/Game/Entities/Components/EntityDiscoveryComponentBase.cs
  46. 12 3
      src/MineCase.Server.Grains/Game/Entities/Components/KeepAliveComponent.cs
  47. 2 2
      src/MineCase.Server.Grains/Game/Entities/Components/MobSpawnerComponent.cs
  48. 7 2
      src/MineCase.Server.Grains/Game/Entities/Components/PlayerDiscoveryComponent.cs
  49. 9 1
      src/MineCase.Server.Grains/Game/Entities/Components/SlotContainerComponent.cs
  50. 7 0
      src/MineCase.Server.Grains/Game/Entities/Components/SyncMobStateComponent.cs
  51. 14 0
      src/MineCase.Server.Grains/Game/Entities/Components/SyncPlayerStateComponent.cs
  52. 8 1
      src/MineCase.Server.Grains/Game/Entities/EntityGrain.cs
  53. 2 0
      src/MineCase.Server.Grains/Game/Entities/MobGrain.cs
  54. 2 0
      src/MineCase.Server.Grains/Game/Entities/MonsterGrain.cs
  55. 2 0
      src/MineCase.Server.Grains/Game/Entities/PassiveMobGrain.cs
  56. 13 26
      src/MineCase.Server.Grains/Game/GameSession.cs
  57. 4 1
      src/MineCase.Server.Grains/GrainsAssemblyExtensions.cs
  58. 2 0
      src/MineCase.Server.Grains/MineCase.Server.Grains.csproj
  59. 1 0
      src/MineCase.Server.Grains/Network/ClientboundPacketSinkGrain.cs
  60. 2 0
      src/MineCase.Server.Grains/Network/Login/LoginFlowGrain.cs
  61. 10 2
      src/MineCase.Server.Grains/Network/Play/ClientboundPacketComponent.cs
  62. 15 6
      src/MineCase.Server.Grains/Network/Play/ServerboundPacketComponent.cs
  63. 33 0
      src/MineCase.Server.Grains/Persistence/AppDbContext.cs
  64. 41 0
      src/MineCase.Server.Grains/Persistence/Components/AutoSaveStateComponent.cs
  65. 45 0
      src/MineCase.Server.Grains/Persistence/Components/StateComponent.cs
  66. 60 0
      src/MineCase.Server.Grains/Persistence/PersistableDependencyObject.cs
  67. 15 0
      src/MineCase.Server.Grains/Persistence/PersistenceModule.cs
  68. 14 0
      src/MineCase.Server.Grains/Settings/PersistenceOptions.cs
  69. 26 10
      src/MineCase.Server.Grains/User/NonAuthenticatedUserGrain.cs
  70. 22 8
      src/MineCase.Server.Grains/User/UserChunkLoaderGrain.cs
  71. 75 31
      src/MineCase.Server.Grains/User/UserGrain.cs
  72. 16 0
      src/MineCase.Server.Grains/User/UserLifecycle.cs
  73. 1 1
      src/MineCase.Server.Grains/World/AddressByPartitionGrain.cs
  74. 78 40
      src/MineCase.Server.Grains/World/ChunkColumnGrain.cs
  75. 13 9
      src/MineCase.Server.Grains/World/ChunkTrackingHub.cs
  76. 54 21
      src/MineCase.Server.Grains/World/CollectableFinder.cs
  77. 46 12
      src/MineCase.Server.Grains/World/TickEmitterGrain.cs
  78. 2 0
      src/MineCase.Server.Grains/World/WorldAccessorGrain.cs
  79. 49 18
      src/MineCase.Server.Grains/World/WorldGrain.cs
  80. 71 24
      src/MineCase.Server.Grains/World/WorldPartitionGrain.cs
  81. 8 3
      src/MineCase.Server.Interfaces/Components/EntityMessages.cs
  82. 6 0
      src/MineCase.Server.Interfaces/Game/Entities/EntityMessages.cs
  83. 0 4
      src/MineCase.Server.Interfaces/Game/IGameSession.cs
  84. 8 1
      src/MineCase.Server.Interfaces/User/IUser.cs
  85. 11 0
      src/MineCase.Server.Interfaces/User/IUserLifeCycle.cs
  86. 2 0
      src/MineCase.Server.Interfaces/World/IChunkColumn.cs
  87. 3 0
      src/MineCase.Server.Interfaces/World/IChunkTrackingHub.cs
  88. 3 9
      src/MineCase.Server.Interfaces/World/ICollectableFinder.cs
  89. 2 2
      src/MineCase.Server.Interfaces/World/ITickEmitter.cs
  90. 2 1
      src/MineCase.Server.Interfaces/World/IWorld.cs
  91. 2 2
      src/MineCase.Server.Interfaces/World/IWorldPartition.cs
  92. 3 1
      src/MineCase.Server/AppBootstrapper.cs
  93. 1 0
      src/MineCase.Server/Dockerfile
  94. 8 0
      src/MineCase.Server/MineCase.Server.csproj
  95. 21 3
      src/MineCase.Server/Program.cs
  96. 5 0
      src/MineCase.Server/config.docker.json
  97. 5 0
      src/MineCase.Server/config.json
  98. 11 0
      src/MineCase.sln
  99. 7 1
      tests/MineCase.Tests.sln
  100. 5 3
      tests/UnitTest/MineCase.UnitTest.csproj

+ 1 - 1
.travis.yml

@@ -20,7 +20,7 @@ script:
  - docker tag minecase.gateway sunnycase/minecase.gateway:ci-latest
  - docker tag minecase.server sunnycase/minecase.server:ci-latest
  - >
-   if [ "$TRAVIS_BRANCH" == "master" ]; then
+   if [ "$TRAVIS_BRANCH" == "master" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ]; then
      docker login -u="$DOCKER_USER" -p="$DOCKER_PASS"
      docker push sunnycase/minecase.gateway
      docker push sunnycase/minecase.server

+ 5 - 0
build/docker/linux/docker-compose.yml

@@ -1,8 +1,13 @@
 version: '3'
 
 services:
+  minecase.persistdb:
+    image: "mongo:3.4.10"
+
   minecase.server:
     image: "sunnycase/minecase.server:ci-latest"
+    depends_on:
+      - minecase.persistdb
 
   minecase.gateway:
     image: "sunnycase/minecase.gateway:ci-latest"

+ 5 - 0
build/docker/win/docker-compose.yml

@@ -1,8 +1,13 @@
 version: '3'
 
 services:
+  minecase.persistdb:
+    image: "mongo:3.4.10"
+
   minecase.server:
     image: "sunnycase/minecase.server:ci-latest-nanoserver"
+    depends_on:
+      - minecase.persistdb
 
   minecase.gateway:
     image: "sunnycase/minecase.gateway:ci-latest-nanoserver"

+ 98 - 0
src/Common/Engine/Data/DependencyValueStorage.Serialize.cs

@@ -0,0 +1,98 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+using NorminalType = System.Collections.Generic.Dictionary<MineCase.Engine.DependencyProperty, System.Collections.Generic.SortedList<float, MineCase.Engine.Data.IEffectiveValue>>;
+
+namespace MineCase.Engine.Data
+{
+    /// <summary>
+    /// 依赖值存储 - 序列化
+    /// </summary>
+    internal partial class DependencyValueStorage
+    {
+        internal class DependencyValueStorageSerializer : ClassSerializerBase<DependencyValueStorage>
+        {
+            protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, DependencyValueStorage value)
+            {
+                var writer = context.Writer;
+                writer.WriteStartDocument();
+
+                foreach (var keyPair in value._dict)
+                {
+                    writer.WriteName(PropertyToString(keyPair.Key));
+                    SerializeValues(context, keyPair.Key, keyPair.Value);
+                }
+
+                writer.WriteEndDocument();
+            }
+
+            private void SerializeValues(BsonSerializationContext context, DependencyProperty key, SortedList<float, IEffectiveValue> values)
+            {
+                var writer = context.Writer;
+                writer.WriteStartDocument();
+                foreach (var valuePair in values)
+                {
+                    writer.WriteName(valuePair.Key.ToString());
+                    var value = key.Helper.GetValue(valuePair.Value);
+                    var serializer = BsonSerializer.LookupSerializer(key.PropertyType);
+                    serializer.Serialize(context, value);
+                }
+
+                writer.WriteEndDocument();
+            }
+
+            protected override DependencyValueStorage DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+            {
+                var dict = new Dictionary<DependencyProperty, SortedList<float, IEffectiveValue>>();
+                var reader = context.Reader;
+                reader.ReadStartDocument();
+
+                while (reader.ReadBsonType() != BsonType.EndOfDocument)
+                    DeserializeValues(context, dict);
+
+                reader.ReadEndDocument();
+                return new DependencyValueStorage(dict);
+            }
+
+            private void DeserializeValues(BsonDeserializationContext context, NorminalType dict)
+            {
+                var reader = context.Reader;
+                var key = ParsePropertyFromString(reader.ReadName());
+                dict.Add(key, DeserializeValues(key, context));
+            }
+
+            private SortedList<float, IEffectiveValue> DeserializeValues(DependencyProperty key, BsonDeserializationContext context)
+            {
+                var list = new SortedList<float, IEffectiveValue>();
+                var serializer = BsonSerializer.LookupSerializer(key.PropertyType);
+                var reader = context.Reader;
+                reader.ReadStartDocument();
+
+                while (reader.ReadBsonType() != BsonType.EndOfDocument)
+                {
+                    var priority = float.Parse(reader.ReadName());
+                    var value = serializer.Deserialize(context);
+                    list.Add(priority, key.Helper.FromValue(value));
+                }
+
+                reader.ReadEndDocument();
+                return list;
+            }
+
+            private static string PropertyToString(DependencyProperty key) =>
+                $"{DependencyProperty.OwnerTypeToString(key.OwnerType)}:{key.Name}";
+
+            private static DependencyProperty ParsePropertyFromString(string name)
+            {
+                var lastPoint = name.LastIndexOf(':');
+                var ownerType = DependencyProperty.StringToOwnerType(name.Substring(0, lastPoint));
+                return DependencyProperty.FromName(name.Substring(lastPoint + 1), ownerType);
+            }
+        }
+    }
+}

+ 26 - 2
src/Common/Engine/Data/DependencyValueStorage.cs

@@ -5,9 +5,12 @@ using System.Threading.Tasks;
 
 namespace MineCase.Engine.Data
 {
-    internal class DependencyValueStorage : IDependencyValueStorage
+    /// <summary>
+    /// 依赖值存储 - CRUD
+    /// </summary>
+    internal partial class DependencyValueStorage : IDependencyValueStorage
     {
-        private readonly Dictionary<DependencyProperty, SortedList<float, IEffectiveValue>> _dict = new Dictionary<DependencyProperty, SortedList<float, IEffectiveValue>>();
+        private readonly Dictionary<DependencyProperty, SortedList<float, IEffectiveValue>> _dict;
 
         public IEnumerable<DependencyProperty> Keys
         {
@@ -22,6 +25,8 @@ namespace MineCase.Engine.Data
             }
         }
 
+        public bool IsDirty { get; set; }
+
         public event
 #if ECS_SERVER
         AsyncEventHandler<CurrentValueChangedEventArgs>
@@ -32,6 +37,21 @@ namespace MineCase.Engine.Data
 
         public DependencyValueStorage()
         {
+            _dict = new Dictionary<DependencyProperty, SortedList<float, IEffectiveValue>>();
+        }
+
+        public DependencyValueStorage(Dictionary<DependencyProperty, SortedList<float, IEffectiveValue>> values)
+        {
+            _dict = values;
+
+            foreach (var keyPair in values)
+            {
+                foreach (var valuePairs in keyPair.Value)
+                {
+                    var priority = valuePairs.Value.Provider.Priority;
+                    valuePairs.Value.ValueChanged = (s, e) => OnEffectiveValueChanged(priority, keyPair.Key, e.OldValue, e.NewValue);
+                }
+            }
         }
 
         public
@@ -86,6 +106,7 @@ namespace MineCase.Engine.Data
                 result = newValue;
             }
 
+            IsDirty = true;
             return result;
         }
 
@@ -136,6 +157,7 @@ namespace MineCase.Engine.Data
 #endif
             OnCurrentValueChanged(DependencyProperty key, bool hasOldValue, object oldValue, bool hasNewValue, object newValue)
         {
+            IsDirty = true;
 #if ECS_SERVER
             return
 #endif
@@ -150,6 +172,7 @@ namespace MineCase.Engine.Data
 #endif
             OnEffectiveValueCleared(int index, DependencyProperty key, object oldValue)
         {
+            IsDirty = true;
             if (index == 0)
             {
                 bool hasNewValue = false;
@@ -180,6 +203,7 @@ namespace MineCase.Engine.Data
 #endif
             OnEffectiveValueChanged(float priority, DependencyProperty key, object oldValue, object newValue)
         {
+            IsDirty = true;
             SortedList<float, IEffectiveValue> list;
             if (_dict.TryGetValue(key, out list) && list.IndexOfKey(priority) == 0)
             {

+ 5 - 0
src/Common/Engine/Data/IDependencyValueStorage.cs

@@ -10,6 +10,11 @@ namespace MineCase.Engine.Data
     /// </summary>
     public interface IDependencyValueStorage
     {
+        /// <summary>
+        /// 获取或设置是否有脏数据
+        /// </summary>
+        bool IsDirty { get; set; }
+
         /// <summary>
         /// 当前值变更事件
         /// </summary>

+ 5 - 0
src/Common/Engine/Data/IEffectiveValue.cs

@@ -10,6 +10,11 @@ namespace MineCase.Engine.Data
     /// </summary>
     public interface IEffectiveValue
     {
+        /// <summary>
+        /// 获取提供程序
+        /// </summary>
+        IDependencyValueProvider Provider { get; }
+
         /// <summary>
         /// 获取值改变处理器
         /// </summary>

+ 7 - 0
src/Common/Engine/Data/LocalDependencyValueProvider.cs

@@ -87,6 +87,11 @@ namespace MineCase.Engine.Data
             storage.TryRemove(this, property, out eValue);
         }
 
+        internal static IEffectiveValue<T> FromValue<T>(T value)
+        {
+            return new LocalEffectiveValue<T>(value);
+        }
+
         internal class LocalEffectiveValue<T> : IEffectiveValue<T>
         {
             /// <inheritdoc/>
@@ -106,6 +111,8 @@ namespace MineCase.Engine.Data
             /// <inheritdoc/>
             public T Value => _value;
 
+            IDependencyValueProvider IEffectiveValue.Provider => LocalDependencyValueProvider.Current;
+
             public LocalEffectiveValue(T value)
             {
                 _value = value;

+ 12 - 42
src/Common/Engine/DependencyObject.cs

@@ -35,44 +35,11 @@ namespace MineCase.Engine
         public DependencyObject()
         {
             _realType = this.GetType();
-            _valueStorage.CurrentValueChanged += ValueStorage_CurrentValueChanged;
-        }
-
-        private void LoadState()
-        {
             _components = new Dictionary<string, IComponentIntern>();
             _indexes = new Dictionary<IComponentIntern, int>();
             _messageHandlers = new MultiValueDictionary<Type, IComponentIntern>();
         }
 
-#if ECS_SERVER
-        public override async Task OnActivateAsync()
-        {
-            LoadState();
-            await InitializeComponents();
-        }
-
-        protected virtual Task InitializeComponents()
-        {
-            return Task.CompletedTask;
-        }
-#else
-        /// <inheritdoc/>
-        protected override void Awake()
-        {
-            base.Awake();
-            LoadState();
-            InitializeComponents();
-        }
-
-        /// <summary>
-        /// 初始化组件
-        /// </summary>
-        protected virtual void InitializeComponents()
-        {
-        }
-#endif
-
         /// <summary>
         /// 获取组件
         /// </summary>
@@ -168,9 +135,12 @@ namespace MineCase.Engine
 
         internal readonly Type _realType;
 
-        private readonly DependencyValueStorage _valueStorage = new DependencyValueStorage();
+        private DependencyValueStorage _valueStorage;
 
-        internal IDependencyValueStorage ValueStorage => _valueStorage;
+        /// <summary>
+        /// 获取值存储
+        /// </summary>
+        public IDependencyValueStorage ValueStorage => _valueStorage;
 
         private readonly ConcurrentDictionary<DependencyProperty, Delegate> _propertyChangedHandlers = new ConcurrentDictionary<DependencyProperty, Delegate>();
         private readonly ConcurrentDictionary<DependencyProperty, Delegate> _propertyChangedHandlersGen = new ConcurrentDictionary<DependencyProperty, Delegate>();
@@ -553,6 +523,10 @@ namespace MineCase.Engine
                     invoker(handler, message);
                 }
             }
+
+#if ECS_SERVER
+            await ClearOperationQueue();
+#endif
         }
 
         /// <inheritdoc />
@@ -602,18 +576,14 @@ namespace MineCase.Engine
             await
 #endif
                     invoker(handler, message);
+#if ECS_SERVER
+                    await ClearOperationQueue();
+#endif
                     return new AskResult<TResponse> { Succeeded = true, Response = response };
                 }
             }
 
             return AskResult<TResponse>.Failed;
         }
-
-#if ECS_SERVER
-        public virtual void Destroy()
-        {
-            DeactivateOnIdle();
-        }
-#endif
     }
 }

+ 46 - 1
src/Common/Engine/DependencyProperty.cs

@@ -34,6 +34,12 @@ namespace MineCase.Engine
     {
         private static int _nextAvailableGlobalId = 0;
         private static readonly ConcurrentDictionary<FromNameKey, DependencyProperty> _fromNameMaps = new ConcurrentDictionary<FromNameKey, DependencyProperty>();
+        private static readonly ConcurrentDictionary<string, Type> _ownerTypes = new ConcurrentDictionary<string, Type>();
+
+        /// <summary>
+        /// 所有者类型加载器
+        /// </summary>
+        public static Func<string, Type> OwnerTypeLoader { get; set; } = t => Type.GetType(t);
 
         /// <summary>
         /// 获取名称
@@ -62,6 +68,8 @@ namespace MineCase.Engine
 
         internal DependencyPropertyFlags Flags { get; }
 
+        internal abstract IDependencyPropertyHelper Helper { get; }
+
         private readonly int _globalId;
 
         internal DependencyProperty(string name, Type ownerType, DependencyPropertyFlags flags)
@@ -147,12 +155,45 @@ namespace MineCase.Engine
             while (property == null && ownerType != null)
             {
                 if (!_fromNameMaps.TryGetValue(new FromNameKey(name, ownerType), out property))
-                    ownerType = ownerType.GetType().BaseType;
+                    ownerType = ownerType.BaseType;
             }
 
             return property != null ? property : throw new InvalidOperationException($"Property {ownerType.Name}.{name} not found.");
         }
 
+        internal static string OwnerTypeToString(Type type)
+        {
+            var str = EscapeOwnerTypeString(type);
+            if (!_ownerTypes.ContainsKey(str))
+                throw new InvalidOperationException($"OwnerType: {type.Name} is not registered.");
+            return str;
+        }
+
+        internal static Type StringToOwnerType(string str)
+        {
+            if (!_ownerTypes.TryGetValue(str, out var type))
+            {
+                var ownerType = UnescapeOwnerTypeString(str);
+                System.Runtime.CompilerServices.RuntimeHelpers.RunClassConstructor(ownerType.TypeHandle);
+            }
+
+            if (!_ownerTypes.TryGetValue(str, out type))
+                throw new ArgumentException($"OwnerType: {str} is not registered.");
+            return type;
+        }
+
+        private static string EscapeOwnerTypeString(Type type)
+        {
+            var str = type.FullName;
+            return type.ToString().Replace('.', ':');
+        }
+
+        private static Type UnescapeOwnerTypeString(string str)
+        {
+            str = str.Replace(':', '.');
+            return OwnerTypeLoader(str);
+        }
+
         /// <summary>
         /// 添加从名称获取
         /// </summary>
@@ -162,6 +203,8 @@ namespace MineCase.Engine
         {
             if (!_fromNameMaps.TryAdd(new FromNameKey(name, ownerType), this))
                 throw new ArgumentException($"Property {ownerType.Name}.{name} is already registered.");
+            else
+                _ownerTypes.TryAdd(EscapeOwnerTypeString(ownerType), ownerType);
         }
 
         /// <summary>
@@ -222,6 +265,8 @@ namespace MineCase.Engine
         /// <inheritdoc/>
         public override Type PropertyType => typeof(T);
 
+        internal override IDependencyPropertyHelper Helper { get; } = new DependencyPropertyHelper<T>();
+
         internal DependencyProperty(string name, Type ownerType, DependencyPropertyFlags flags, PropertyMetadata<T> metadata)
             : base(name, ownerType, flags)
         {

+ 28 - 0
src/Common/Engine/DependencyPropertyHelper.cs

@@ -0,0 +1,28 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Engine.Data;
+
+namespace MineCase.Engine
+{
+    internal interface IDependencyPropertyHelper
+    {
+        object GetValue(IEffectiveValue effectiveValue);
+
+        IEffectiveValue FromValue(object value);
+    }
+
+    internal sealed class DependencyPropertyHelper<T> : IDependencyPropertyHelper
+    {
+        public IEffectiveValue FromValue(object value)
+        {
+            return LocalDependencyValueProvider.FromValue<T>((T)value);
+        }
+
+        public object GetValue(IEffectiveValue effectiveValue)
+        {
+            return ((IEffectiveValue<T>)effectiveValue).Value;
+        }
+    }
+}

+ 17 - 0
src/MineCase.Client.Engine/DependencyObject.Client.cs

@@ -8,6 +8,23 @@ namespace MineCase.Engine
 {
     public partial class DependencyObject
     {
+        /// <inheritdoc/>
+        protected override void Awake()
+        {
+            base.Awake();
+
+            _valueStorage = new Data.DependencyValueStorage();
+            _valueStorage.CurrentValueChanged += ValueStorage_CurrentValueChanged;
+            InitializeComponents();
+        }
+
+        /// <summary>
+        /// 初始化组件
+        /// </summary>
+        protected virtual void InitializeComponents()
+        {
+        }
+
         private void Update()
         {
             Tell(Messages.Update.Default);

+ 1 - 0
src/MineCase.Client.Engine/MineCase.Client.Engine.csproj

@@ -44,6 +44,7 @@
   <ItemGroup>
     <Compile Include="..\Common\Engine\Component.cs" Link="Component.cs" />
     <Compile Include="..\Common\Engine\Data\DependencyValueStorage.cs" Link="Data\DependencyValueStorage.cs" />
+    <Compile Include="..\Common\Engine\DependencyPropertyHelper.cs" Link="DependencyPropertyHelper.cs" />
     <Compile Include="..\Common\Engine\Data\IDependencyValueProvider.cs" Link="Data\IDependencyValueProvider.cs" />
     <Compile Include="..\Common\Engine\Data\IDependencyValueStorage.cs" Link="Data\IDependencyValueStorage.cs" />
     <Compile Include="..\Common\Engine\Data\IEffectiveValue.cs" Link="Data\IEffectiveValue.cs" />

+ 31 - 1
src/MineCase.Core/Graphics/Cuboid.cs

@@ -5,7 +5,7 @@ using System.Text;
 namespace MineCase.Graphics
 {
     [Serializable]
-    public class Cuboid : Shape
+    public class Cuboid : Shape, IEquatable<Cuboid>
     {
         public override ShapeType Type => ShapeType.Cuboid;
 
@@ -53,5 +53,35 @@ namespace MineCase.Graphics
 
             return false;
         }
+
+        public override bool Equals(object obj)
+        {
+            return Equals(obj as Cuboid);
+        }
+
+        public bool Equals(Cuboid other)
+        {
+            return other != null &&
+                   EqualityComparer<Point3d>.Default.Equals(Point, other.Point) &&
+                   EqualityComparer<Size>.Default.Equals(Size, other.Size);
+        }
+
+        public override int GetHashCode()
+        {
+            var hashCode = 1392910933;
+            hashCode = hashCode * -1521134295 + EqualityComparer<Point3d>.Default.GetHashCode(Point);
+            hashCode = hashCode * -1521134295 + EqualityComparer<Size>.Default.GetHashCode(Size);
+            return hashCode;
+        }
+
+        public static bool operator ==(Cuboid cuboid1, Cuboid cuboid2)
+        {
+            return EqualityComparer<Cuboid>.Default.Equals(cuboid1, cuboid2);
+        }
+
+        public static bool operator !=(Cuboid cuboid1, Cuboid cuboid2)
+        {
+            return !(cuboid1 == cuboid2);
+        }
     }
 }

+ 64 - 2
src/MineCase.Core/Graphics/Point.cs

@@ -6,7 +6,7 @@ using System.Text;
 namespace MineCase.Graphics
 {
     [Serializable]
-    public struct Point2d
+    public struct Point2d : IEquatable<Point2d>
     {
         /// <summary>
         /// Gets or sets the positon of X-axis in minecraft world.
@@ -51,6 +51,26 @@ namespace MineCase.Graphics
             return X * X + Z * Z;
         }
 
+        public override bool Equals(object obj)
+        {
+            return obj is Point2d && Equals((Point2d)obj);
+        }
+
+        public bool Equals(Point2d other)
+        {
+            return X == other.X &&
+                   Z == other.Z;
+        }
+
+        public override int GetHashCode()
+        {
+            var hashCode = 1911744652;
+            hashCode = hashCode * -1521134295 + base.GetHashCode();
+            hashCode = hashCode * -1521134295 + X.GetHashCode();
+            hashCode = hashCode * -1521134295 + Z.GetHashCode();
+            return hashCode;
+        }
+
         public static Point2d operator + (Point2d p1, Point2d p2)
         {
             return new Point2d(p1.X + p2.X, p1.Z + p2.Z);
@@ -65,10 +85,20 @@ namespace MineCase.Graphics
         {
             return new Point2d(p1.X - p2.X, p1.Z - p2.Z);
         }
+
+        public static bool operator ==(Point2d d1, Point2d d2)
+        {
+            return d1.Equals(d2);
+        }
+
+        public static bool operator !=(Point2d d1, Point2d d2)
+        {
+            return !(d1 == d2);
+        }
     }
 
     [Serializable]
-    public struct Point3d
+    public struct Point3d : IEquatable<Point3d>
     {
         /// <summary>
         /// Gets or sets the positon of X-axis in minecraft world.
@@ -105,5 +135,37 @@ namespace MineCase.Graphics
             Z = vector.Z;
             Y = vector.Y;
         }
+
+        public override bool Equals(object obj)
+        {
+            return obj is Point3d && Equals((Point3d)obj);
+        }
+
+        public bool Equals(Point3d other)
+        {
+            return X == other.X &&
+                   Z == other.Z &&
+                   Y == other.Y;
+        }
+
+        public override int GetHashCode()
+        {
+            var hashCode = 900060928;
+            hashCode = hashCode * -1521134295 + base.GetHashCode();
+            hashCode = hashCode * -1521134295 + X.GetHashCode();
+            hashCode = hashCode * -1521134295 + Z.GetHashCode();
+            hashCode = hashCode * -1521134295 + Y.GetHashCode();
+            return hashCode;
+        }
+
+        public static bool operator ==(Point3d d1, Point3d d2)
+        {
+            return d1.Equals(d2);
+        }
+
+        public static bool operator !=(Point3d d1, Point3d d2)
+        {
+            return !(d1 == d2);
+        }
     }
 }

+ 33 - 1
src/MineCase.Core/Graphics/Size.cs

@@ -7,7 +7,7 @@ using System.Text;
 namespace MineCase.Graphics
 {
     [Serializable]
-    public struct Size
+    public struct Size : IEquatable<Size>
     {
         /// <summary>
         /// Gets or sets the size of X-axis.
@@ -50,5 +50,37 @@ namespace MineCase.Graphics
         {
             return Length * Width * Height;
         }
+
+        public override bool Equals(object obj)
+        {
+            return obj is Size && Equals((Size)obj);
+        }
+
+        public bool Equals(Size other)
+        {
+            return Length == other.Length &&
+                   Width == other.Width &&
+                   Height == other.Height;
+        }
+
+        public override int GetHashCode()
+        {
+            var hashCode = -1791230722;
+            hashCode = hashCode * -1521134295 + base.GetHashCode();
+            hashCode = hashCode * -1521134295 + Length.GetHashCode();
+            hashCode = hashCode * -1521134295 + Width.GetHashCode();
+            hashCode = hashCode * -1521134295 + Height.GetHashCode();
+            return hashCode;
+        }
+
+        public static bool operator ==(Size size1, Size size2)
+        {
+            return size1.Equals(size2);
+        }
+
+        public static bool operator !=(Size size1, Size size2)
+        {
+            return !(size1 == size2);
+        }
     }
 }

+ 15 - 0
src/MineCase.Core/World/GameTickArgs.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.World
+{
+    public sealed class GameTickArgs
+    {
+        public TimeSpan DeltaTime { get; set; }
+
+        public long WorldAge { get; set; }
+
+        public long TimeOfDay { get; set; }
+    }
+}

+ 119 - 0
src/MineCase.Engine/DependencyObject.Server.cs

@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging;
+using MineCase.Engine.Serialization;
+
+namespace MineCase.Engine
+{
+    public partial class DependencyObject
+    {
+        private bool _isDestroyed = false;
+        private readonly Queue<Func<Task>> _operationQueue = new Queue<Func<Task>>();
+
+        protected ILogger Logger { get; private set; }
+
+        public override async Task OnActivateAsync()
+        {
+            Logger = ServiceProvider.GetRequiredService<ILoggerFactory>().CreateLogger(GetType());
+            await InitializePreLoadComponent();
+            await ReadStateAsync();
+            await InitializeComponents();
+        }
+
+        public override async Task OnDeactivateAsync()
+        {
+            await WriteStateAsync();
+            await base.OnDeactivateAsync();
+        }
+
+        public void Destroy()
+        {
+            _isDestroyed = true;
+            DeactivateOnIdle();
+        }
+
+        protected virtual Task InitializeComponents()
+        {
+            return Task.CompletedTask;
+        }
+
+        protected virtual Task InitializePreLoadComponent()
+        {
+            return Task.CompletedTask;
+        }
+
+        public async Task ReadStateAsync()
+        {
+            var state = await DeserializeStateAsync();
+            if (state == null || state.ValueStorage == null)
+            {
+                _valueStorage = new Data.DependencyValueStorage();
+            }
+            else
+            {
+                _valueStorage = (Data.DependencyValueStorage)state.ValueStorage;
+            }
+
+            _valueStorage.CurrentValueChanged += ValueStorage_CurrentValueChanged;
+            await Tell(AfterReadState.Default);
+        }
+
+        public async Task WriteStateAsync()
+        {
+            try
+            {
+                if (_isDestroyed)
+                {
+                    await ClearStateAsync();
+                }
+                else
+                {
+                    await Tell(BeforeWriteState.Default);
+                    var state = new DependencyObjectState
+                    {
+                        GrainKeyString = GrainReference.ToKeyString(),
+                        ValueStorage = _valueStorage
+                    };
+
+                    await SerializeStateAsync(state);
+                    ValueStorage.IsDirty = false;
+                }
+            }
+            catch (Exception ex)
+            {
+                Logger.LogError(ex, ex.Message);
+            }
+        }
+
+        protected virtual Task<DependencyObjectState> DeserializeStateAsync()
+        {
+            return Task.FromResult<DependencyObjectState>(null);
+        }
+
+        protected virtual Task SerializeStateAsync(DependencyObjectState state)
+        {
+            return Task.CompletedTask;
+        }
+
+        protected virtual Task ClearStateAsync()
+        {
+            return Task.CompletedTask;
+        }
+
+        public void QueueOperation(Func<Task> operation)
+        {
+            _operationQueue.Enqueue(operation);
+        }
+
+        public async Task ClearOperationQueue()
+        {
+            while (_operationQueue.Count != 0)
+            {
+                await _operationQueue.Dequeue()();
+            }
+        }
+    }
+}

+ 3 - 0
src/MineCase.Engine/MineCase.Server.Engine.csproj

@@ -19,6 +19,7 @@
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004" PrivateAssets="All" />
     <PackageReference Include="System.Collections.Generic.MultiValueDictionary" Version="0.1.0-e170912-3" />
     <PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="2.0.0" />
+    <PackageReference Include="MongoDB.Bson" Version="2.4.4" />
   </ItemGroup>
 
   <ItemGroup>
@@ -29,6 +30,7 @@
     <Compile Include="..\Common\Engine\AsyncEventHandler.cs" Link="AsyncEventHandler.cs" />
     <Compile Include="..\Common\Engine\Component.cs" Link="Component.cs" />
     <Compile Include="..\Common\Engine\Data\DependencyValueStorage.cs" Link="Data\DependencyValueStorage.cs" />
+    <Compile Include="..\Common\Engine\Data\DependencyValueStorage.Serialize.cs" Link="Data\DependencyValueStorage.Serialize.cs" />
     <Compile Include="..\Common\Engine\Data\IDependencyValueProvider.cs" Link="Data\IDependencyValueProvider.cs" />
     <Compile Include="..\Common\Engine\Data\IDependencyValueStorage.cs" Link="Data\IDependencyValueStorage.cs" />
     <Compile Include="..\Common\Engine\Data\IEffectiveValue.cs" Link="Data\IEffectiveValue.cs" />
@@ -36,6 +38,7 @@
     <Compile Include="..\Common\Engine\Data\LocalDependencyValueProvider.cs" Link="Data\LocalDependencyValueProvider.cs" />
     <Compile Include="..\Common\Engine\DependencyObject.cs" Link="DependencyObject.cs" />
     <Compile Include="..\Common\Engine\DependencyProperty.cs" Link="DependencyProperty.cs" />
+    <Compile Include="..\Common\Engine\DependencyPropertyHelper.cs" Link="DependencyPropertyHelper.cs" />
     <Compile Include="..\Common\Engine\EngineAssemblyExtensions.cs" Link="EngineAssemblyExtensions.cs" />
     <Compile Include="..\Common\Engine\IDependencyObject.cs" Link="IDependencyObject.cs" />
     <Compile Include="..\Common\Engine\IEntityMessage.cs" Link="IEntityMessage.cs" />

+ 86 - 0
src/MineCase.Engine/Serialization/DependencyObjectSerializer.cs

@@ -0,0 +1,86 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Engine.Data;
+using MongoDB.Bson;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Attributes;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace MineCase.Engine.Serialization
+{
+    public class DependencyObjectState
+    {
+        [BsonId]
+        public string GrainKeyString { get; set; }
+
+        public IDependencyValueStorage ValueStorage { get; set; }
+    }
+
+    /// <summary>
+    /// DependencyObject 状态序列化器
+    /// </summary>
+    public class DependencyObjectStateSerializer : ClassSerializerBase<DependencyObjectState>, IBsonDocumentSerializer
+    {
+        private readonly IBsonSerializer<DependencyValueStorage> _valueStorageSerializer = new DependencyValueStorage.DependencyValueStorageSerializer();
+
+        protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, DependencyObjectState value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName("_id");
+            writer.WriteString(value.GrainKeyString);
+
+            writer.WriteName("ValueStorage");
+            _valueStorageSerializer.Serialize(context, value.ValueStorage);
+
+            writer.WriteEndDocument();
+        }
+
+        protected override DependencyObjectState DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var state = new DependencyObjectState();
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            while (reader.ReadBsonType() != BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case "_id":
+                        state.GrainKeyString = reader.ReadString();
+                        break;
+                    case "ValueStorage":
+                        state.ValueStorage = _valueStorageSerializer.Deserialize(context);
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return state;
+        }
+
+        public bool TryGetMemberSerializationInfo(string memberName, out BsonSerializationInfo serializationInfo)
+        {
+            switch (memberName)
+            {
+                case "GrainKeyString":
+                    serializationInfo = new BsonSerializationInfo("_id", new StringSerializer(), typeof(string));
+                    return true;
+                case "ValueStorage":
+                    serializationInfo = new BsonSerializationInfo("ValueStorage", _valueStorageSerializer, typeof(DependencyValueStorage));
+                    return true;
+                default:
+                    serializationInfo = null;
+                    return false;
+            }
+        }
+    }
+}

+ 22 - 0
src/MineCase.Engine/Serialization/EntityMessages.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Engine.Serialization
+{
+    /// <summary>
+    /// 加载状态后
+    /// </summary>
+    public sealed class AfterReadState : IEntityMessage
+    {
+        public static readonly AfterReadState Default = new AfterReadState();
+    }
+
+    /// <summary>
+    /// 写入状态前
+    /// </summary>
+    public sealed class BeforeWriteState : IEntityMessage
+    {
+        public static readonly BeforeWriteState Default = new BeforeWriteState();
+    }
+}

+ 18 - 0
src/MineCase.Serialization/MineCase.Serialization.csproj

@@ -0,0 +1,18 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFramework>netstandard2.0</TargetFramework>
+    <Configurations>Debug;Release;Appveyor;TravisCI</Configurations>
+  </PropertyGroup>
+  
+  <ItemGroup>
+    <PackageReference Include="MongoDB.Bson" Version="2.4.4" />
+    <PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0-beta1" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <ProjectReference Include="..\MineCase.Core\MineCase.Core.csproj" />
+    <ProjectReference Include="..\MineCase.Engine\MineCase.Server.Engine.csproj" />
+  </ItemGroup>
+
+</Project>

+ 298 - 0
src/MineCase.Serialization/Serializers/BlockChunkPosSerializer.cs

@@ -0,0 +1,298 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MineCase.World;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace MineCase.Serialization.Serializers
+{
+    public class BlockChunkPosSerializer : StructSerializerBase<BlockChunkPos>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, BlockChunkPos value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(BlockChunkPos.X));
+            writer.WriteInt32(value.X);
+
+            writer.WriteName(nameof(BlockChunkPos.Y));
+            writer.WriteInt32(value.Y);
+
+            writer.WriteName(nameof(BlockChunkPos.Z));
+            writer.WriteInt32(value.Z);
+
+            writer.WriteEndDocument();
+        }
+
+        public override BlockChunkPos Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(BlockChunkPos);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(BlockChunkPos.X):
+                        value.X = reader.ReadInt32();
+                        break;
+                    case nameof(BlockChunkPos.Y):
+                        value.Y = reader.ReadInt32();
+                        break;
+                    case nameof(BlockChunkPos.Z):
+                        value.Z = reader.ReadInt32();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+
+    public class BlockWorldPosSerializer : StructSerializerBase<BlockWorldPos>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, BlockWorldPos value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(BlockWorldPos.X));
+            writer.WriteInt32(value.X);
+
+            writer.WriteName(nameof(BlockWorldPos.Y));
+            writer.WriteInt32(value.Y);
+
+            writer.WriteName(nameof(BlockWorldPos.Z));
+            writer.WriteInt32(value.Z);
+
+            writer.WriteEndDocument();
+        }
+
+        public override BlockWorldPos Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(BlockWorldPos);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(BlockWorldPos.X):
+                        value.X = reader.ReadInt32();
+                        break;
+                    case nameof(BlockWorldPos.Y):
+                        value.Y = reader.ReadInt32();
+                        break;
+                    case nameof(BlockWorldPos.Z):
+                        value.Z = reader.ReadInt32();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+
+    public class BlockSectionPosSerializer : StructSerializerBase<BlockSectionPos>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, BlockSectionPos value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(BlockSectionPos.X));
+            writer.WriteInt32(value.X);
+
+            writer.WriteName(nameof(BlockSectionPos.Y));
+            writer.WriteInt32(value.Y);
+
+            writer.WriteName(nameof(BlockSectionPos.Z));
+            writer.WriteInt32(value.Z);
+
+            writer.WriteEndDocument();
+        }
+
+        public override BlockSectionPos Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(BlockSectionPos);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(BlockSectionPos.X):
+                        value.X = reader.ReadInt32();
+                        break;
+                    case nameof(BlockSectionPos.Y):
+                        value.Y = reader.ReadInt32();
+                        break;
+                    case nameof(BlockSectionPos.Z):
+                        value.Z = reader.ReadInt32();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+
+    public class ChunkWorldPosSerializer : StructSerializerBase<ChunkWorldPos>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, ChunkWorldPos value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(ChunkWorldPos.X));
+            writer.WriteInt32(value.X);
+
+            writer.WriteName(nameof(ChunkWorldPos.Z));
+            writer.WriteInt32(value.Z);
+
+            writer.WriteEndDocument();
+        }
+
+        public override ChunkWorldPos Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(ChunkWorldPos);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(ChunkWorldPos.X):
+                        value.X = reader.ReadInt32();
+                        break;
+                    case nameof(ChunkWorldPos.Z):
+                        value.Z = reader.ReadInt32();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+
+    public class EntityWorldPosSerializer : StructSerializerBase<EntityWorldPos>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, EntityWorldPos value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(EntityWorldPos.X));
+            writer.WriteDouble(value.X);
+
+            writer.WriteName(nameof(EntityWorldPos.Y));
+            writer.WriteDouble(value.Y);
+
+            writer.WriteName(nameof(EntityWorldPos.Z));
+            writer.WriteDouble(value.Z);
+
+            writer.WriteEndDocument();
+        }
+
+        public override EntityWorldPos Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(EntityWorldPos);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(EntityWorldPos.X):
+                        value.X = (float)reader.ReadDouble();
+                        break;
+                    case nameof(EntityWorldPos.Y):
+                        value.Y = (float)reader.ReadDouble();
+                        break;
+                    case nameof(EntityWorldPos.Z):
+                        value.Z = (float)reader.ReadDouble();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+
+    public class EntityChunkPosSerializer : StructSerializerBase<EntityChunkPos>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, EntityChunkPos value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(EntityChunkPos.X));
+            writer.WriteDouble(value.X);
+
+            writer.WriteName(nameof(EntityChunkPos.Y));
+            writer.WriteDouble(value.Y);
+
+            writer.WriteName(nameof(EntityChunkPos.Z));
+            writer.WriteDouble(value.Z);
+
+            writer.WriteEndDocument();
+        }
+
+        public override EntityChunkPos Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(EntityChunkPos);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(EntityChunkPos.X):
+                        value.X = (float)reader.ReadDouble();
+                        break;
+                    case nameof(EntityChunkPos.Y):
+                        value.Y = (float)reader.ReadDouble();
+                        break;
+                    case nameof(EntityChunkPos.Z):
+                        value.Z = (float)reader.ReadDouble();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+}

+ 68 - 0
src/MineCase.Serialization/Serializers/ChunkColumnCompactStorageSerializer.cs

@@ -0,0 +1,68 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MineCase.World;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace MineCase.Serialization.Serializers
+{
+    public class ChunkColumnCompactStorageSerializer : SealedClassSerializerBase<ChunkColumnCompactStorage>
+    {
+        private readonly IBsonSerializer<ChunkSectionCompactStorage> _serializer = new ChunkSectionCompactStorageSerializer();
+
+        protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, ChunkColumnCompactStorage value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            if (value.Biomes != null)
+            {
+                writer.WriteName(nameof(ChunkColumnCompactStorage.Biomes));
+                writer.WriteBytes(value.Biomes);
+            }
+
+            writer.WriteName(nameof(ChunkColumnCompactStorage.Sections));
+            writer.WriteStartArray();
+            foreach (var section in value.Sections)
+                _serializer.Serialize(context, section);
+            writer.WriteEndArray();
+
+            writer.WriteEndDocument();
+        }
+
+        protected override ChunkColumnCompactStorage DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var biomes = default(byte[]);
+            var sections = new ChunkSectionCompactStorage[ChunkConstants.SectionsPerChunk];
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(ChunkColumnCompactStorage.Biomes):
+                        biomes = reader.ReadBytes();
+                        break;
+                    case nameof(ChunkColumnCompactStorage.Sections):
+                        reader.ReadStartArray();
+                        for (int i = 0; i < sections.Length; i++)
+                            sections[i] = _serializer.Deserialize(context);
+                        reader.ReadEndArray();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            var value = new ChunkColumnCompactStorage(biomes);
+            for (int i = 0; i < sections.Length; i++)
+                value.Sections[i] = sections[i];
+            return value;
+        }
+    }
+}

+ 117 - 0
src/MineCase.Serialization/Serializers/ChunkSectionCompactStorageSerializer.cs

@@ -0,0 +1,117 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MineCase.World;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace MineCase.Serialization.Serializers
+{
+    public class ChunkSectionCompactStorageSerializer : SealedClassSerializerBase<ChunkSectionCompactStorage>
+    {
+        private readonly IBsonSerializer<ChunkSectionCompactStorage.NibbleArray> _nibbleSerializer = new NibbleArraySerializer();
+        private readonly IBsonSerializer<ChunkSectionCompactStorage.DataArray> _dataSerializer = new DataArraySerializer();
+
+        protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, ChunkSectionCompactStorage value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(ChunkSectionCompactStorage.Data));
+            _dataSerializer.Serialize(context, value.Data);
+
+            writer.WriteName(nameof(ChunkSectionCompactStorage.BlockLight));
+            _nibbleSerializer.Serialize(context, value.BlockLight);
+
+            if (value.SkyLight != null)
+            {
+                writer.WriteName(nameof(ChunkSectionCompactStorage.SkyLight));
+                _nibbleSerializer.Serialize(context, value.SkyLight);
+            }
+
+            writer.WriteEndDocument();
+        }
+
+        protected override ChunkSectionCompactStorage DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var data = default(ChunkSectionCompactStorage.DataArray);
+            var blockLight = default(ChunkSectionCompactStorage.NibbleArray);
+            var skyLight = default(ChunkSectionCompactStorage.NibbleArray);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(ChunkSectionCompactStorage.Data):
+                        data = _dataSerializer.Deserialize(context);
+                        break;
+                    case nameof(ChunkSectionCompactStorage.BlockLight):
+                        blockLight = _nibbleSerializer.Deserialize(context);
+                        break;
+                    case nameof(ChunkSectionCompactStorage.SkyLight):
+                        skyLight = _nibbleSerializer.Deserialize(context);
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return new ChunkSectionCompactStorage(data, blockLight, skyLight);
+        }
+
+        private class DataArraySerializer : SealedClassSerializerBase<ChunkSectionCompactStorage.DataArray>
+        {
+            protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, ChunkSectionCompactStorage.DataArray value)
+            {
+                var writer = context.Writer;
+
+                var data = new byte[value.Storage.Length * sizeof(ulong)];
+                using (var bw = new BinaryWriter(new MemoryStream(data, true)))
+                {
+                    foreach (var item in value.Storage)
+                        bw.Write(item);
+                }
+
+                writer.WriteBytes(data);
+            }
+
+            protected override ChunkSectionCompactStorage.DataArray DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+            {
+                var reader = context.Reader;
+
+                var data = reader.ReadBytes();
+                var storage = new ulong[data.Length / sizeof(ulong)];
+                using (var br = new BinaryReader(new MemoryStream(data, false)))
+                {
+                    for (int i = 0; i < storage.Length; i++)
+                        storage[i] = br.ReadUInt64();
+                }
+
+                return new ChunkSectionCompactStorage.DataArray(storage);
+            }
+        }
+
+        private class NibbleArraySerializer : SealedClassSerializerBase<ChunkSectionCompactStorage.NibbleArray>
+        {
+            protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, ChunkSectionCompactStorage.NibbleArray value)
+            {
+                var writer = context.Writer;
+
+                writer.WriteBytes(value.Storage);
+            }
+
+            protected override ChunkSectionCompactStorage.NibbleArray DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+            {
+                var reader = context.Reader;
+
+                return new ChunkSectionCompactStorage.NibbleArray(reader.ReadBytes());
+            }
+        }
+    }
+}

+ 65 - 0
src/MineCase.Serialization/Serializers/GrainRerferenceSerializer.cs

@@ -0,0 +1,65 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Extensions.DependencyInjection;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+using Orleans;
+using Orleans.Runtime;
+
+namespace MineCase.Serialization.Serializers
+{
+    public class GrainRerferenceSerializer<TInterface> : SealedClassSerializerBase<TInterface>
+        where TInterface : class, IAddressable
+    {
+        private IGrainReferenceConverter _grainReferenceConverter;
+        private IGrainFactory _grainFactory;
+
+        public GrainRerferenceSerializer(IServiceProvider serviceProvider)
+        {
+            _grainReferenceConverter = serviceProvider.GetRequiredService<IGrainReferenceConverter>();
+            _grainFactory = serviceProvider.GetRequiredService<IGrainFactory>();
+        }
+
+        protected override void SerializeValue(BsonSerializationContext context, BsonSerializationArgs args, TInterface value)
+        {
+            var refer = (GrainReference)(object)value;
+            var key = refer.ToKeyString();
+            context.Writer.WriteString(key);
+        }
+
+        protected override TInterface DeserializeValue(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var key = context.Reader.ReadString();
+            var refer = _grainReferenceConverter.GetGrainFromKeyString(key);
+            if (refer != null)
+            {
+                refer.BindGrainReference(_grainFactory);
+                return refer.AsReference<TInterface>();
+            }
+
+            return null;
+        }
+    }
+
+    public class GrainRerferenceSerializerProvider : IBsonSerializationProvider
+    {
+        private readonly ConcurrentDictionary<Type, IBsonSerializer> _bsonSerializers = new ConcurrentDictionary<Type, IBsonSerializer>();
+        private readonly Type _referType = typeof(IAddressable);
+        private readonly IServiceProvider _serviceProvider;
+        private readonly Type _serializerTypeGen = typeof(GrainRerferenceSerializer<>);
+
+        public GrainRerferenceSerializerProvider(IServiceProvider serviceProvider)
+        {
+            _serviceProvider = serviceProvider;
+        }
+
+        public IBsonSerializer GetSerializer(Type type)
+        {
+            if (_referType.IsAssignableFrom(type))
+                return _bsonSerializers.GetOrAdd(type, t => (IBsonSerializer)Activator.CreateInstance(_serializerTypeGen.MakeGenericType(t), _serviceProvider));
+            return null;
+        }
+    }
+}

+ 59 - 0
src/MineCase.Serialization/Serializers/Point3DSerializer.cs

@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MineCase.Graphics;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace MineCase.Serialization.Serializers
+{
+    public class Point3dSerializer : StructSerializerBase<Point3d>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Point3d value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(Point3d.X));
+            writer.WriteDouble(value.X);
+
+            writer.WriteName(nameof(Point3d.Y));
+            writer.WriteDouble(value.Y);
+
+            writer.WriteName(nameof(Point3d.Z));
+            writer.WriteDouble(value.Z);
+
+            writer.WriteEndDocument();
+        }
+
+        public override Point3d Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(Point3d);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(Point3d.X):
+                        value.X = (float)reader.ReadDouble();
+                        break;
+                    case nameof(Point3d.Y):
+                        value.Y = (float)reader.ReadDouble();
+                        break;
+                    case nameof(Point3d.Z):
+                        value.Z = (float)reader.ReadDouble();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+}

+ 40 - 0
src/MineCase.Serialization/Serializers/Serializers.cs

@@ -0,0 +1,40 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MineCase.Engine.Serialization;
+using MineCase.Graphics;
+using MongoDB.Bson.Serialization;
+
+namespace MineCase.Serialization.Serializers
+{
+    public static class Serializers
+    {
+        public static void RegisterAll(IServiceProvider serviceProvider)
+        {
+            BsonSerializer.RegisterSerializationProvider(new GrainRerferenceSerializerProvider(serviceProvider));
+        }
+
+        public static void RegisterAll()
+        {
+            BsonClassMap.RegisterClassMap<Shape>(c =>
+            {
+                c.SetDiscriminatorIsRequired(true);
+                c.SetIsRootClass(true);
+                c.AddKnownType(typeof(Cuboid));
+            });
+
+            BsonSerializer.RegisterSerializer(new SlotSerializer());
+            BsonSerializer.RegisterSerializer(new DependencyObjectStateSerializer());
+            BsonSerializer.RegisterSerializer(new BlockChunkPosSerializer());
+            BsonSerializer.RegisterSerializer(new BlockWorldPosSerializer());
+            BsonSerializer.RegisterSerializer(new ChunkWorldPosSerializer());
+            BsonSerializer.RegisterSerializer(new BlockSectionPosSerializer());
+            BsonSerializer.RegisterSerializer(new EntityWorldPosSerializer());
+            BsonSerializer.RegisterSerializer(new EntityChunkPosSerializer());
+            BsonSerializer.RegisterSerializer(new Point3dSerializer());
+            BsonSerializer.RegisterSerializer(new SizeSerializer());
+            BsonSerializer.RegisterSerializer(new ChunkColumnCompactStorageSerializer());
+            BsonSerializer.RegisterSerializer(new ChunkSectionCompactStorageSerializer());
+        }
+    }
+}

+ 59 - 0
src/MineCase.Serialization/Serializers/SizeSerializer.cs

@@ -0,0 +1,59 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MineCase.Graphics;
+using MongoDB.Bson.IO;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.Serialization.Serializers;
+
+namespace MineCase.Serialization.Serializers
+{
+    public class SizeSerializer : StructSerializerBase<Size>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Size value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(Size.Width));
+            writer.WriteDouble(value.Width);
+
+            writer.WriteName(nameof(Size.Height));
+            writer.WriteDouble(value.Height);
+
+            writer.WriteName(nameof(Size.Length));
+            writer.WriteDouble(value.Length);
+
+            writer.WriteEndDocument();
+        }
+
+        public override Size Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var value = default(Size);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(Size.Width):
+                        value.Width = (float)reader.ReadDouble();
+                        break;
+                    case nameof(Size.Height):
+                        value.Height = (float)reader.ReadDouble();
+                        break;
+                    case nameof(Size.Length):
+                        value.Length = (float)reader.ReadDouble();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return value;
+        }
+    }
+}

+ 58 - 0
src/MineCase.Serialization/Serializers/SlotSerializer.cs

@@ -0,0 +1,58 @@
+using MongoDB.Bson.Serialization.Serializers;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using MongoDB.Bson.Serialization;
+using MongoDB.Bson.IO;
+
+namespace MineCase.Serialization.Serializers
+{
+    public class SlotSerializer : StructSerializerBase<Slot>
+    {
+        public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, Slot value)
+        {
+            var writer = context.Writer;
+            writer.WriteStartDocument();
+
+            writer.WriteName(nameof(Slot.BlockId));
+            writer.WriteInt32(value.BlockId);
+
+            writer.WriteName(nameof(Slot.ItemCount));
+            writer.WriteInt32(value.ItemCount);
+
+            writer.WriteName(nameof(Slot.ItemDamage));
+            writer.WriteInt32(value.ItemDamage);
+
+            writer.WriteEndDocument();
+        }
+
+        public override Slot Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
+        {
+            var reader = context.Reader;
+            reader.ReadStartDocument();
+
+            var slot = default(Slot);
+            while (reader.ReadBsonType() != MongoDB.Bson.BsonType.EndOfDocument)
+            {
+                var name = reader.ReadName();
+                switch (name)
+                {
+                    case nameof(Slot.BlockId):
+                        slot.BlockId = (short)reader.ReadInt32();
+                        break;
+                    case nameof(Slot.ItemCount):
+                        slot.ItemCount = (byte)reader.ReadInt32();
+                        break;
+                    case nameof(Slot.ItemDamage):
+                        slot.ItemDamage = (short)reader.ReadInt32();
+                        break;
+                    default:
+                        break;
+                }
+            }
+
+            reader.ReadEndDocument();
+            return slot;
+        }
+    }
+}

+ 3 - 3
src/MineCase.Server.Grains/Components/EntityLookComponent.cs

@@ -10,13 +10,13 @@ namespace MineCase.Server.Components
     internal class EntityLookComponent : Component
     {
         public static readonly DependencyProperty<float> PitchProperty =
-            DependencyProperty.Register<float>("Pitch", typeof(EntityWorldPositionComponent));
+            DependencyProperty.Register<float>("Pitch", typeof(EntityLookComponent));
 
         public static readonly DependencyProperty<float> YawProperty =
-            DependencyProperty.Register<float>("Yaw", typeof(EntityWorldPositionComponent));
+            DependencyProperty.Register<float>("Yaw", typeof(EntityLookComponent));
 
         public static readonly DependencyProperty<float> HeadYawProperty =
-            DependencyProperty.Register<float>("HeadYaw", typeof(EntityWorldPositionComponent));
+            DependencyProperty.Register<float>("HeadYaw", typeof(EntityLookComponent));
 
         public float Pitch => AttachedObject.GetValue(PitchProperty);
 

+ 1 - 1
src/MineCase.Server.Grains/Components/EntityOnGroundComponent.cs

@@ -9,7 +9,7 @@ namespace MineCase.Server.Components
     internal class EntityOnGroundComponent : Component
     {
         public static readonly DependencyProperty<bool> IsOnGroundProperty =
-            DependencyProperty.Register<bool>("IsOnGround", typeof(EntityWorldPositionComponent));
+            DependencyProperty.Register<bool>("IsOnGround", typeof(EntityOnGroundComponent));
 
         public bool IsOnGround => AttachedObject.GetValue(IsOnGroundProperty);
 

+ 30 - 10
src/MineCase.Server.Grains/Components/GameTickComponent.cs

@@ -4,14 +4,15 @@ using System.Text;
 using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.World;
+using MineCase.World;
 using Orleans;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.Components
 {
-    internal class GameTickComponent : Component, IHandle<GameTick>, IHandle<Disable>
+    internal class GameTickComponent : Component, IHandle<GameTick>
     {
-        public event AsyncEventHandler<(TimeSpan deltaTime, long worldAge)> Tick;
+        public event AsyncEventHandler<GameTickArgs> Tick;
 
         public GameTickComponent(string name = "gameTick")
             : base(name)
@@ -22,35 +23,54 @@ namespace MineCase.Server.Components
         {
             AttachedObject.GetComponent<AddressByPartitionKeyComponent>()
                 .KeyChanged += OnAddressByPartitionKeyChanged;
+            AttachedObject.RegisterPropertyChangedHandler(IsEnabledComponent.IsEnabledProperty, OnIsEnabledChanged);
+            AttachedObject.QueueOperation(TrySubscribe);
             return base.OnAttached();
         }
 
-        protected override Task OnDetached()
+        protected override async Task OnDetached()
         {
             AttachedObject.GetComponent<AddressByPartitionKeyComponent>()
                 .KeyChanged -= OnAddressByPartitionKeyChanged;
-            return base.OnDetached();
+            await TryUnsubscribe();
         }
 
         private async Task OnAddressByPartitionKeyChanged(object sender, (string oldKey, string newKey) e)
         {
             if (!string.IsNullOrEmpty(e.oldKey))
                 await GrainFactory.GetGrain<ITickEmitter>(e.oldKey).Unsubscribe(AttachedObject);
-            if (!string.IsNullOrEmpty(e.newKey))
-                await GrainFactory.GetGrain<ITickEmitter>(e.newKey).Subscribe(AttachedObject);
+            await TrySubscribe();
         }
 
-        public void OnGameTick(TimeSpan deltaTime, long worldAge)
+        private Task OnIsEnabledChanged(object sender, PropertyChangedEventArgs<bool> e)
         {
-            Tick.InvokeSerial(this, (deltaTime, worldAge)).Ignore();
+            if (e.NewValue)
+                return TrySubscribe();
+            else
+                return TryUnsubscribe();
+        }
+
+        public Task OnGameTick(GameTickArgs e)
+        {
+            return Tick.InvokeSerial(this, e);
         }
 
         Task IHandle<GameTick>.Handle(GameTick message)
         {
-            return Tick.InvokeSerial(this, (message.DeltaTime, message.WorldAge));
+            return OnGameTick(message.Args);
+        }
+
+        private async Task TrySubscribe()
+        {
+            if (AttachedObject.GetValue(IsEnabledComponent.IsEnabledProperty))
+            {
+                var key = AttachedObject.GetAddressByPartitionKey();
+                if (!string.IsNullOrEmpty(key))
+                    await GrainFactory.GetGrain<ITickEmitter>(key).Subscribe(AttachedObject);
+            }
         }
 
-        async Task IHandle<Disable>.Handle(Disable message)
+        private async Task TryUnsubscribe()
         {
             var key = AttachedObject.GetAddressByPartitionKey();
             if (!string.IsNullOrEmpty(key))

+ 31 - 0
src/MineCase.Server.Grains/Components/IsEnabledComponent.cs

@@ -0,0 +1,31 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Engine;
+
+namespace MineCase.Server.Components
+{
+    internal class IsEnabledComponent : Component, IHandle<Disable>, IHandle<Enable>
+    {
+        public static readonly DependencyProperty<bool> IsEnabledProperty =
+            DependencyProperty.Register<bool>(nameof(IsEnabled), typeof(IsEnabledComponent));
+
+        public bool IsEnabled => AttachedObject.GetValue(IsEnabledProperty);
+
+        public IsEnabledComponent(string name = "isEnabled")
+            : base(name)
+        {
+        }
+
+        Task IHandle<Enable>.Handle(Enable message)
+        {
+            return AttachedObject.SetLocalValue(IsEnabledProperty, true);
+        }
+
+        Task IHandle<Disable>.Handle(Disable message)
+        {
+            return AttachedObject.SetLocalValue(IsEnabledProperty, false);
+        }
+    }
+}

+ 8 - 1
src/MineCase.Server.Grains/Game/BlockEntities/BlockEntityGrain.cs

@@ -5,13 +5,18 @@ using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.Game.BlockEntities.Components;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
 using MineCase.Server.World;
 using MineCase.World;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game.BlockEntities
 {
-    internal abstract class BlockEntityGrain : DependencyObject, IBlockEntity
+    [PersistTableName("blockEntity")]
+    [Reentrant]
+    internal abstract class BlockEntityGrain : PersistableDependencyObject, IBlockEntity
     {
         public IWorld World => GetValue(WorldComponent.WorldProperty);
 
@@ -19,12 +24,14 @@ namespace MineCase.Server.Game.BlockEntities
 
         protected override async Task InitializeComponents()
         {
+            await SetComponent(new IsEnabledComponent());
             await SetComponent(new WorldComponent());
             await SetComponent(new BlockWorldPositionComponent());
             await SetComponent(new AddressByPartitionKeyComponent());
             await SetComponent(new ChunkEventBroadcastComponent());
             await SetComponent(new GameTickComponent());
             await SetComponent(new BlockEntityLiftTimeComponent());
+            await SetComponent(new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute));
         }
 
         Task<IWorld> IBlockEntity.GetWorld() =>

+ 6 - 0
src/MineCase.Server.Grains/Game/BlockEntities/Components/BlockEntityLiftTimeComponent.cs

@@ -18,6 +18,12 @@ namespace MineCase.Server.Game.BlockEntities.Components
         {
             await AttachedObject.GetComponent<WorldComponent>().SetWorld(message.World);
             await AttachedObject.GetComponent<BlockWorldPositionComponent>().SetBlockWorldPosition(message.Position);
+            AttachedObject.QueueOperation(async () =>
+            {
+                await AttachedObject.Tell(Enable.Default);
+                if (AttachedObject.ValueStorage.IsDirty)
+                    await AttachedObject.WriteStateAsync();
+            });
         }
 
         Task IHandle<DestroyBlockEntity>.Handle(DestroyBlockEntity message)

+ 5 - 0
src/MineCase.Server.Grains/Game/BlockEntities/Components/ChestComponent.cs

@@ -89,5 +89,10 @@ namespace MineCase.Server.Game.BlockEntities.Components
                     orderby e.position.X, e.position.Z
                     select e.entity).First();
         }
+
+        private void MarkDirty()
+        {
+            AttachedObject.ValueStorage.IsDirty = true;
+        }
     }
 }

+ 90 - 49
src/MineCase.Server.Grains/Game/BlockEntities/Components/FurnaceComponent.cs

@@ -8,22 +8,37 @@ using MineCase.Server.Components;
 using MineCase.Server.Game.Entities.Components;
 using MineCase.Server.Game.Windows;
 using MineCase.Server.World;
+using MineCase.World;
 
 namespace MineCase.Server.Game.BlockEntities.Components
 {
     internal class FurnaceComponent : Component<BlockEntityGrain>, IHandle<SetSlot>, IHandle<SpawnBlockEntity>, IHandle<DestroyBlockEntity>, IHandle<UseBy>
     {
-        private bool _isCooking;
-        private FurnaceRecipe _currentRecipe;
-        private FurnaceFuel _currentFuel;
-        private int _fuelLeft;
-        private int _maxFuelTime;
-        private int _cookProgress;
-        private int _maxProgress;
+        public class FurnaceState
+        {
+            public bool IsCooking;
+
+            public FurnaceRecipe CurrentRecipe;
+
+            public FurnaceFuel CurrentFuel;
+
+            public int FuelLeft;
+
+            public int MaxFuelTime;
+
+            public int CookProgress;
+
+            public int MaxProgress;
+        }
+
+        public static readonly DependencyProperty<FurnaceState> StateProperty =
+            DependencyProperty.Register<FurnaceState>(nameof(State), typeof(FurnaceComponent));
 
         public static readonly DependencyProperty<IFurnaceWindow> FurnaceWindowProperty =
             DependencyProperty.Register<IFurnaceWindow>("FurnaceWindow", typeof(FurnaceComponent));
 
+        public FurnaceState State => AttachedObject.GetValue(StateProperty);
+
         public IFurnaceWindow FurnaceWindow => AttachedObject.GetValue(FurnaceWindowProperty);
 
         public FurnaceComponent(string name = "furnace")
@@ -33,45 +48,53 @@ namespace MineCase.Server.Game.BlockEntities.Components
 
         protected override Task OnAttached()
         {
-            _currentRecipe = null;
-            _isCooking = false;
-            _fuelLeft = 0;
-            _maxFuelTime = 0;
-            _cookProgress = 0;
-            _maxFuelTime = 200;
+            if (State == null)
+            {
+                AttachedObject.SetLocalValue(StateProperty, new FurnaceState
+                {
+                    MaxFuelTime = 200
+                });
+            }
+
             Register();
             return base.OnAttached();
         }
 
-        private bool CanCook() => _currentRecipe != null && (_fuelLeft > 0 || _currentFuel != null);
+        private bool CanCook()
+        {
+            var state = State;
+            return state.CurrentRecipe != null && (state.FuelLeft > 0 || state.CurrentFuel != null);
+        }
 
-        private async Task OnGameTick(object sender, (TimeSpan deltaTime, long worldAge) e)
+        private async Task OnGameTick(object sender, GameTickArgs e)
         {
+            var state = State;
             if (CanCook())
             {
-                if (!_isCooking)
+                if (!state.IsCooking)
                     await StartCooking();
-                if (_cookProgress == 0)
+                if (state.CookProgress == 0)
                     await TakeIngredient();
-                if (_fuelLeft == 0)
+                if (state.FuelLeft == 0)
                     await TakeFuel();
 
-                if (_currentRecipe != null)
+                if (state.CurrentRecipe != null)
                 {
-                    _cookProgress++;
-                    _fuelLeft--;
+                    state.CookProgress++;
+                    state.FuelLeft--;
+                    MarkDirty();
 
-                    if (FurnaceWindow != null && e.worldAge % 10 == 0)
+                    if (FurnaceWindow != null && e.WorldAge % 10 == 0)
                     {
-                        await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.ProgressArrow, (short)_cookProgress);
-                        await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.FireIcon, (short)_fuelLeft);
+                        await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.ProgressArrow, (short)state.CookProgress);
+                        await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.FireIcon, (short)state.FuelLeft);
                     }
                 }
 
-                if (_cookProgress == _maxProgress)
+                if (state.CookProgress == state.MaxProgress)
                     await Produce();
             }
-            else if (_isCooking)
+            else if (state.IsCooking)
             {
                 await StopCooking();
             }
@@ -79,11 +102,12 @@ namespace MineCase.Server.Game.BlockEntities.Components
 
         async Task IHandle<SetSlot>.Handle(SetSlot message)
         {
-            if (_currentRecipe == null && message.Index == 0)
+            var state = State;
+            if (state.CurrentRecipe == null && message.Index == 0)
                 await UpdateRecipe();
             if (message.Index == 1)
                 await UpdateFuel();
-            if (!_isCooking && CanCook())
+            if (!state.IsCooking && CanCook())
                 Register();
         }
 
@@ -95,7 +119,8 @@ namespace MineCase.Server.Game.BlockEntities.Components
 
         private async Task UpdateFuel()
         {
-            _currentFuel = await GrainFactory.GetGrain<IFurnaceRecipes>(0).FindFuel(GetSlot(1));
+            State.CurrentFuel = await GrainFactory.GetGrain<IFurnaceRecipes>(0).FindFuel(GetSlot(1));
+            MarkDirty();
         }
 
         private async Task UpdateRecipe()
@@ -105,39 +130,44 @@ namespace MineCase.Server.Game.BlockEntities.Components
             {
                 if (GetSlot(2).IsEmpty || GetSlot(2).CanStack(recipe.Output))
                 {
-                    _currentRecipe = recipe;
+                    State.CurrentRecipe = recipe;
                     return;
                 }
             }
 
-            _currentRecipe = null;
+            State.CurrentRecipe = null;
+            MarkDirty();
         }
 
         private async Task Produce()
         {
+            var state = State;
             if (GetSlot(2).IsEmpty)
-                await SetSlot(2, _currentRecipe.Output);
+                await SetSlot(2, state.CurrentRecipe.Output);
             else
-                await SetSlot(2, GetSlot(2).AddItemCount(_currentRecipe.Output.ItemCount));
-            _cookProgress = 0;
+                await SetSlot(2, GetSlot(2).AddItemCount(state.CurrentRecipe.Output.ItemCount));
+            state.CookProgress = 0;
+            MarkDirty();
             if (FurnaceWindow != null)
             {
                 await FurnaceWindow.BroadcastSlotChanged(2, GetSlot(2));
-                await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.ProgressArrow, (short)_cookProgress);
+                await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.ProgressArrow, (short)state.CookProgress);
             }
         }
 
         private async Task TakeFuel()
         {
-            var slot = GetSlot(1).AddItemCount(-_currentFuel.Slot.ItemCount);
+            var state = State;
+            var slot = GetSlot(1).AddItemCount(-state.CurrentFuel.Slot.ItemCount);
             slot.MakeEmptyIfZero();
             await SetSlot(1, slot);
-            _maxFuelTime = _fuelLeft = _currentFuel.Time;
+            state.MaxFuelTime = state.FuelLeft = state.CurrentFuel.Time;
+            MarkDirty();
             if (FurnaceWindow != null)
             {
                 await FurnaceWindow.BroadcastSlotChanged(1, slot);
-                await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.MaximumFuelBurnTime, (short)_maxFuelTime);
-                await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.FireIcon, (short)_fuelLeft);
+                await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.MaximumFuelBurnTime, (short)state.MaxFuelTime);
+                await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.FireIcon, (short)state.FuelLeft);
             }
 
             await UpdateFuel();
@@ -145,33 +175,37 @@ namespace MineCase.Server.Game.BlockEntities.Components
 
         private async Task TakeIngredient()
         {
+            var state = State;
             await UpdateRecipe();
-            if (_currentRecipe != null)
+            if (state.CurrentRecipe != null)
             {
-                var slot = GetSlot(0).AddItemCount(-_currentRecipe.Input.ItemCount);
+                var slot = GetSlot(0).AddItemCount(-state.CurrentRecipe.Input.ItemCount);
                 slot.MakeEmptyIfZero();
                 await SetSlot(0, slot);
-                _cookProgress = 0;
-                _maxProgress = _currentRecipe.Time;
+                state.CookProgress = 0;
+                state.MaxProgress = state.CurrentRecipe.Time;
+                MarkDirty();
                 if (FurnaceWindow != null)
                 {
                     await FurnaceWindow.BroadcastSlotChanged(0, slot);
-                    await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.ProgressArrow, (short)_cookProgress);
-                    await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.MaximumProgress, (short)_maxProgress);
+                    await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.ProgressArrow, (short)state.CookProgress);
+                    await FurnaceWindow.SetProperty(FurnaceWindowPropertyType.MaximumProgress, (short)state.MaxProgress);
                 }
             }
         }
 
         private async Task StartCooking()
         {
-            _isCooking = true;
+            State.IsCooking = true;
+            MarkDirty();
             var meta = (await AttachedObject.World.GetBlockState(GrainFactory, AttachedObject.Position)).MetaValue;
             await AttachedObject.World.SetBlockState(GrainFactory, AttachedObject.Position, new BlockState { Id = (uint)BlockId.BurningFurnace, MetaValue = meta });
         }
 
         private async Task StopCooking()
         {
-            _isCooking = false;
+            State.IsCooking = false;
+            MarkDirty();
             var meta = (await AttachedObject.World.GetBlockState(GrainFactory, AttachedObject.Position)).MetaValue;
             await AttachedObject.World.SetBlockState(GrainFactory, AttachedObject.Position, new BlockState { Id = (uint)BlockId.Furnace, MetaValue = meta });
             Unregister();
@@ -191,13 +225,15 @@ namespace MineCase.Server.Game.BlockEntities.Components
 
         async Task IHandle<SpawnBlockEntity>.Handle(SpawnBlockEntity message)
         {
-            _isCooking = (await AttachedObject.World.GetBlockState(GrainFactory, AttachedObject.Position))
+            State.IsCooking = (await AttachedObject.World.GetBlockState(GrainFactory, AttachedObject.Position))
                 .IsSameId(BlockStates.BurningFurnace());
+            MarkDirty();
         }
 
         Task IHandle<DestroyBlockEntity>.Handle(DestroyBlockEntity message)
         {
-            _isCooking = false;
+            State.IsCooking = false;
+            MarkDirty();
             Unregister();
             return Task.CompletedTask;
         }
@@ -212,5 +248,10 @@ namespace MineCase.Server.Game.BlockEntities.Components
 
             await message.Entity.Tell(new OpenWindow { Window = FurnaceWindow });
         }
+
+        private void MarkDirty()
+        {
+            AttachedObject.ValueStorage.IsDirty = true;
+        }
     }
 }

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

@@ -7,9 +7,11 @@ using MineCase.Server.User;
 using MineCase.Server.World;
 using MineCase.World;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game
 {
+    [Reentrant]
     internal class ChunkSenderGrain : Grain, IChunkSender
     {
         private Guid _jobWorkerId;

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

@@ -31,6 +31,7 @@ namespace MineCase.Server.Game
     }
 
     [ImplicitStreamSubscription(StreamProviders.Namespaces.ChunkSender)]
+    [Reentrant]
     internal class ChunkSenderJobWorker : Grain, IChunkSenderJobWorker
     {
         private readonly IPacketPackager _packetPackager;

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

@@ -5,13 +5,15 @@ using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.User;
+using MineCase.World;
 using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class ChunkLoaderComponent : Component<PlayerGrain>, IHandle<PlayerLoggedIn>, IHandle<BindToUser>
+    internal class ChunkLoaderComponent : Component<PlayerGrain>, IHandle<BeginLogin>, IHandle<PlayerLoggedIn>, IHandle<BindToUser>
     {
         private IUserChunkLoader _chunkLoader;
+        private bool _loaded;
 
         public ChunkLoaderComponent(string name = "chunkLoader")
             : base(name)
@@ -20,15 +22,18 @@ namespace MineCase.Server.Game.Entities.Components
 
         protected override Task OnAttached()
         {
+            _loaded = false;
             _chunkLoader = GrainFactory.GetGrain<IUserChunkLoader>(AttachedObject.GetPrimaryKey());
             AttachedObject.RegisterPropertyChangedHandler(ViewDistanceComponent.ViewDistanceProperty, OnViewDistanceChanged);
             AttachedObject.GetComponent<GameTickComponent>().Tick += OnGameTick;
             return base.OnAttached();
         }
 
-        private Task OnGameTick(object sender, (TimeSpan deltaTime, long worldAge) e)
+        private Task OnGameTick(object sender, GameTickArgs e)
         {
-            return _chunkLoader.OnGameTick(e.worldAge);
+            if (_loaded)
+                return _chunkLoader.OnGameTick(e.WorldAge);
+            return Task.CompletedTask;
         }
 
         private Task OnViewDistanceChanged(object sender, PropertyChangedEventArgs<byte> e)
@@ -38,12 +43,20 @@ namespace MineCase.Server.Game.Entities.Components
 
         async Task IHandle<PlayerLoggedIn>.Handle(PlayerLoggedIn message)
         {
+            _loaded = false;
             await _chunkLoader.JoinGame(AttachedObject.GetWorld(), AttachedObject);
+            _loaded = true;
         }
 
         async Task IHandle<BindToUser>.Handle(BindToUser message)
         {
             await _chunkLoader.SetClientPacketSink(await message.User.GetClientPacketSink());
         }
+
+        Task IHandle<BeginLogin>.Handle(BeginLogin message)
+        {
+            _loaded = false;
+            return Task.CompletedTask;
+        }
     }
 }

+ 40 - 11
src/MineCase.Server.Grains/Game/Entities/Components/ColliderComponent.cs

@@ -10,7 +10,7 @@ using MineCase.World;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class ColliderComponent : Component<EntityGrain>, IHandle<Disable>
+    internal class ColliderComponent : Component<EntityGrain>
     {
         public static readonly DependencyProperty<Shape> ColliderShapeProperty =
             DependencyProperty.Register<Shape>("ColliderShape", typeof(ColliderComponent), new PropertyMetadata<Shape>(null, OnColliderShapeChanged));
@@ -24,17 +24,34 @@ namespace MineCase.Server.Game.Entities.Components
 
         protected override Task OnAttached()
         {
-            AttachedObject.RegisterPropertyChangedHandler(AddressByPartitionKeyComponent.AddressByPartitionKeyProperty, OnAddressByPartitionKeyChanged);
+            AttachedObject.GetComponent<AddressByPartitionKeyComponent>()
+                .KeyChanged += AddressByPartitionKeyChanged;
+            AttachedObject.RegisterPropertyChangedHandler(IsEnabledComponent.IsEnabledProperty, OnIsEnabledChanged);
+            AttachedObject.QueueOperation(TrySubscribe);
             return base.OnAttached();
         }
 
-        private async Task OnAddressByPartitionKeyChanged(object sender, PropertyChangedEventArgs<string> e)
+        protected override async Task OnDetached()
+        {
+            AttachedObject.GetComponent<AddressByPartitionKeyComponent>()
+                .KeyChanged -= AddressByPartitionKeyChanged;
+            await TryUnsubscribe();
+        }
+
+        private async Task AddressByPartitionKeyChanged(object sender, (string oldKey, string newKey) e)
         {
             var shape = ColliderShape;
-            if (!string.IsNullOrEmpty(e.OldValue))
-                await GrainFactory.GetGrain<ICollectableFinder>(e.OldValue).UnregisterCollider(AttachedObject);
-            if (!string.IsNullOrEmpty(e.NewValue) && shape != null)
-                await GrainFactory.GetGrain<ICollectableFinder>(e.NewValue).RegisterCollider(AttachedObject, shape);
+            if (!string.IsNullOrEmpty(e.oldKey))
+                await GrainFactory.GetGrain<ICollectableFinder>(e.oldKey).UnregisterCollider(AttachedObject);
+            await TrySubscribe();
+        }
+
+        private Task OnIsEnabledChanged(object sender, PropertyChangedEventArgs<bool> e)
+        {
+            if (e.NewValue)
+                return TrySubscribe();
+            else
+                return TryUnsubscribe();
         }
 
         private async Task OnColliderShapeChanged(PropertyChangedEventArgs<Shape> e)
@@ -44,7 +61,7 @@ namespace MineCase.Server.Game.Entities.Components
             if (shape != null)
                 await GrainFactory.GetGrain<ICollectableFinder>(key).RegisterCollider(AttachedObject, shape);
             else
-                await GrainFactory.GetGrain<ICollectableFinder>(key).UnregisterCollider(AttachedObject);
+                await TrySubscribe();
         }
 
         private static Task OnColliderShapeChanged(object sender, PropertyChangedEventArgs<Shape> e)
@@ -56,10 +73,22 @@ namespace MineCase.Server.Game.Entities.Components
         public Task SetColliderShape(Shape value) =>
             AttachedObject.SetLocalValue(ColliderShapeProperty, value);
 
-        async Task IHandle<Disable>.Handle(Disable message)
+        private async Task TrySubscribe()
         {
-            var key = AttachedObject.GetValue(AddressByPartitionKeyComponent.AddressByPartitionKeyProperty);
-            await GrainFactory.GetGrain<ICollectableFinder>(key).UnregisterCollider(AttachedObject);
+            if (AttachedObject.GetValue(IsEnabledComponent.IsEnabledProperty))
+            {
+                var key = AttachedObject.GetAddressByPartitionKey();
+                var shape = ColliderShape;
+                if (!string.IsNullOrEmpty(key) && shape != null)
+                    await GrainFactory.GetGrain<ICollectableFinder>(key).RegisterCollider(AttachedObject, ColliderShape);
+            }
+        }
+
+        private async Task TryUnsubscribe()
+        {
+            var key = AttachedObject.GetAddressByPartitionKey();
+            if (!string.IsNullOrEmpty(key))
+                await GrainFactory.GetGrain<ICollectableFinder>(key).UnregisterCollider(AttachedObject);
         }
     }
 }

+ 32 - 6
src/MineCase.Server.Grains/Game/Entities/Components/DiscoveryRegisterComponent.cs

@@ -8,29 +8,55 @@ using MineCase.Server.World;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class DiscoveryRegisterComponent : Component<EntityGrain>, IHandle<Disable>
+    internal class DiscoveryRegisterComponent : Component<EntityGrain>
     {
         public DiscoveryRegisterComponent(string name = "discoveryRegister")
             : base(name)
         {
         }
 
-        protected override async Task OnAttached()
+        protected override Task OnAttached()
         {
-            await base.OnAttached();
             AttachedObject.GetComponent<AddressByPartitionKeyComponent>()
                 .KeyChanged += AddressByPartitionKeyChanged;
+            AttachedObject.RegisterPropertyChangedHandler(IsEnabledComponent.IsEnabledProperty, OnIsEnabledChanged);
+            AttachedObject.QueueOperation(TrySubscribe);
+            return base.OnAttached();
+        }
+
+        protected override async Task OnDetached()
+        {
+            AttachedObject.GetComponent<AddressByPartitionKeyComponent>()
+                .KeyChanged -= AddressByPartitionKeyChanged;
+            await TryUnsubscribe();
         }
 
         private async Task AddressByPartitionKeyChanged(object sender, (string oldKey, string newKey) e)
         {
             if (!string.IsNullOrEmpty(e.oldKey))
                 await GrainFactory.GetGrain<IWorldPartition>(e.oldKey).UnsubscribeDiscovery(AttachedObject);
-            if (!string.IsNullOrEmpty(e.newKey))
-                await GrainFactory.GetGrain<IWorldPartition>(e.newKey).SubscribeDiscovery(AttachedObject);
+            await TrySubscribe();
+        }
+
+        private Task OnIsEnabledChanged(object sender, PropertyChangedEventArgs<bool> e)
+        {
+            if (e.NewValue)
+                return TrySubscribe();
+            else
+                return TryUnsubscribe();
+        }
+
+        private async Task TrySubscribe()
+        {
+            if (AttachedObject.GetValue(IsEnabledComponent.IsEnabledProperty))
+            {
+                var key = AttachedObject.GetAddressByPartitionKey();
+                if (!string.IsNullOrEmpty(key))
+                    await GrainFactory.GetGrain<IWorldPartition>(key).SubscribeDiscovery(AttachedObject);
+            }
         }
 
-        async Task IHandle<Disable>.Handle(Disable message)
+        private async Task TryUnsubscribe()
         {
             var key = AttachedObject.GetAddressByPartitionKey();
             if (!string.IsNullOrEmpty(key))

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

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
 using MineCase.Algorithm;
 using MineCase.Algorithm.Game.Entity.Ai.MobAi;
 using MineCase.Engine;
@@ -19,19 +20,17 @@ using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class EntityAiComponent : Component<MobGrain>, IHandle<SpawnMob>
+    internal class EntityAiComponent : Component<MobGrain>
     {
-        public static readonly DependencyProperty<CreatureAi> AiTypeProperty =
-            DependencyProperty.Register<CreatureAi>(nameof(AiType), typeof(EntityAiComponent));
-
         public static readonly DependencyProperty<CreatureState> CreatureStateProperty =
             DependencyProperty.Register<CreatureState>(nameof(CreatureState), typeof(EntityAiComponent));
 
-        public CreatureAi AiType => AttachedObject.GetValue(AiTypeProperty);
+        public MobType MobType => AttachedObject.GetValue(MobTypeComponent.MobTypeProperty);
 
         public CreatureState CreatureState => AttachedObject.GetValue(CreatureStateProperty);
 
         private Random random;
+        private CreatureAi _ai;
 
         public EntityAiComponent(string name = "entityAi")
             : base(name)
@@ -43,6 +42,14 @@ namespace MineCase.Server.Game.Entities.Components
         {
             Register();
             await AttachedObject.SetLocalValue(EntityAiComponent.CreatureStateProperty, CreatureState.Stop);
+            CreateAi(MobType);
+            AttachedObject.RegisterPropertyChangedHandler(MobTypeComponent.MobTypeProperty, OnMobTypeChanged);
+        }
+
+        private Task OnMobTypeChanged(object sender, PropertyChangedEventArgs<MobType> e)
+        {
+            CreateAi(e.NewValue);
+            return Task.CompletedTask;
         }
 
         protected override Task OnDetached()
@@ -63,45 +70,47 @@ namespace MineCase.Server.Game.Entities.Components
                 .Tick -= OnGameTick;
         }
 
-        async Task IHandle<SpawnMob>.Handle(SpawnMob message)
+        private void CreateAi(MobType mobType)
         {
             Func<CreatureState> getter = () => AttachedObject.GetValue(CreatureStateProperty);
             Action<CreatureState> setter = v => AttachedObject.SetLocalValue(CreatureStateProperty, v).Wait();
             CreatureAi ai;
 
-            switch (message.MobType)
+            switch (mobType)
             {
-                case MobType.Chicken:
+                case Entities.MobType.Chicken:
                     ai = new AiChicken(getter, setter);
                     break;
-                case MobType.Cow:
+                case Entities.MobType.Cow:
                     ai = new AiCow(getter, setter);
                     break;
-                case MobType.Creeper:
+                case Entities.MobType.Creeper:
                     ai = new AiCreeper(getter, setter);
                     break;
-                case MobType.Pig:
+                case Entities.MobType.Pig:
                     ai = new AiPig(getter, setter);
                     break;
-                case MobType.Sheep:
+                case Entities.MobType.Sheep:
                     ai = new AiSheep(getter, setter);
                     break;
-                case MobType.Skeleton:
+                case Entities.MobType.Skeleton:
                     ai = new AiSkeleton(getter, setter);
                     break;
-                case MobType.Squid:
+                case Entities.MobType.Squid:
                     // TODO new ai for squid
                     ai = new AiChicken(getter, setter);
                     break;
-                case MobType.Zombie:
+                case Entities.MobType.Zombie:
                     ai = new AiZombie(getter, setter);
                     break;
                 default:
                     // TODO add more ai
-                    throw new NotImplementedException("AI of this mob has not been implemented.");
+                    Logger.LogWarning("AI of this mob has not been implemented: {0}.", mobType);
+                    ai = null;
+                    break;
             }
 
-            await AttachedObject.SetLocalValue(AiTypeProperty, ai);
+            _ai = ai;
         }
 
         private Task ActionStop()
@@ -230,7 +239,7 @@ namespace MineCase.Server.Game.Entities.Components
         private async Task GenerateEvent()
         {
             // get state
-            var state = AiType.State;
+            var state = _ai.State;
             var nextEvent = CreatureEvent.Nothing;
 
             // player approaching event
@@ -260,11 +269,12 @@ namespace MineCase.Server.Game.Entities.Components
                 nextEvent = CreatureEvent.Stop;
             }
 
-            await AiType.FireAsync(nextEvent);
+            await _ai.FireAsync(nextEvent);
         }
 
-        private async Task OnGameTick(object sender, (TimeSpan deltaTime, long worldAge) e)
+        private async Task OnGameTick(object sender, GameTickArgs e)
         {
+            if (_ai == null) return;
             /*
             if (e.worldAge % 16 == 0)
             {
@@ -290,7 +300,7 @@ namespace MineCase.Server.Game.Entities.Components
             await GenerateEvent();
 
             // get state
-            var newState = AiType.State;
+            var newState = _ai.State;
             switch (newState)
             {
                 case CreatureState.Attacking:

+ 7 - 6
src/MineCase.Server.Grains/Game/Entities/Components/EntityDiscoveryComponentBase.cs

@@ -7,6 +7,7 @@ using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.Network;
 using MineCase.Server.Network.Play;
+using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
@@ -18,11 +19,8 @@ namespace MineCase.Server.Game.Entities.Components
         {
         }
 
-        private TaskCompletionSource<object> _spawned;
-
         protected override Task OnAttached()
         {
-            _spawned = new TaskCompletionSource<object>();
             return base.OnAttached();
         }
 
@@ -31,13 +29,11 @@ namespace MineCase.Server.Game.Entities.Components
 
         async Task IHandle<DiscoveredByPlayer>.Handle(DiscoveredByPlayer message)
         {
-            await _spawned.Task;
             await SendSpawnPacket(GetPlayerPacketGenerator(message.Player));
         }
 
         async Task IHandle<BroadcastDiscovered>.Handle(BroadcastDiscovered message)
         {
-            await _spawned.Task;
             await SendSpawnPacket(AttachedObject.GetComponent<ChunkEventBroadcastComponent>().GetGenerator());
         }
 
@@ -45,7 +41,12 @@ namespace MineCase.Server.Game.Entities.Components
 
         protected void CompleteSpawn()
         {
-            _spawned.TrySetResult(null);
+            AttachedObject.QueueOperation(async () =>
+            {
+                await AttachedObject.Tell(Enable.Default);
+                if (AttachedObject.ValueStorage.IsDirty)
+                    await AttachedObject.WriteStateAsync();
+            });
         }
     }
 }

+ 12 - 3
src/MineCase.Server.Grains/Game/Entities/Components/KeepAliveComponent.cs

@@ -5,10 +5,11 @@ using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.Network.Play;
+using MineCase.World;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class KeepAliveComponent : Component, IHandle<PlayerLoggedIn>, IHandle<KickPlayer>
+    internal class KeepAliveComponent : Component, IHandle<BeginLogin>, IHandle<PlayerLoggedIn>, IHandle<KickPlayer>
     {
         private uint _keepAliveId = 0;
         public readonly HashSet<uint> _keepAliveWaiters = new HashSet<uint>();
@@ -45,14 +46,14 @@ namespace MineCase.Server.Game.Entities.Components
             return Task.CompletedTask;
         }
 
-        private async Task OnGameTick(object sender, (TimeSpan deltaTime, long worldAge) e)
+        private async Task OnGameTick(object sender, GameTickArgs e)
         {
             if (_isOnline && _keepAliveWaiters.Count >= ClientKeepInterval)
             {
                 _isOnline = false;
                 await AttachedObject.Tell(new KickPlayer());
             }
-            else if (e.worldAge % 20 == 0)
+            else if (e.WorldAge % 20 == 0)
             {
                 var id = _keepAliveId++;
                 _keepAliveWaiters.Add(id);
@@ -77,5 +78,13 @@ namespace MineCase.Server.Game.Entities.Components
             _isOnline = false;
             return Task.CompletedTask;
         }
+
+        Task IHandle<BeginLogin>.Handle(BeginLogin message)
+        {
+            AttachedObject.GetComponent<GameTickComponent>()
+                .Tick -= OnGameTick;
+            _isOnline = false;
+            return Task.CompletedTask;
+        }
     }
 }

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

@@ -56,9 +56,9 @@ namespace MineCase.Server.Game.Entities.Components
                 .Tick -= OnGameTick;
         }
 
-        private async Task OnGameTick(object sender, (TimeSpan deltaTime, long worldAge) e)
+        private async Task OnGameTick(object sender, GameTickArgs e)
         {
-            if (e.worldAge % 512 == 0 && e.worldAge > 9000 && e.worldAge < 18000)
+            if (e.WorldAge % 512 == 0 && e.TimeOfDay > 9000 && e.TimeOfDay < 18000)
             {
                 EntityWorldPos playerPosition = AttachedObject.GetValue(EntityWorldPositionComponent.EntityWorldPositionProperty);
                 int x = random.Next(9) - 4 + (int)playerPosition.X;

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

@@ -6,10 +6,11 @@ using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.Game.BlockEntities;
 using MineCase.Server.Network.Play;
+using MineCase.Server.World;
 
 namespace MineCase.Server.Game.Entities.Components
 {
-    internal class PlayerDiscoveryComponent : EntityDiscoveryComponentBase<PlayerGrain>, IHandle<SpawnEntity>
+    internal class PlayerDiscoveryComponent : EntityDiscoveryComponentBase<PlayerGrain>, IHandle<PlayerLoggedIn>
     {
         public PlayerDiscoveryComponent(string name = "playerDiscovery")
             : base(name)
@@ -21,9 +22,13 @@ namespace MineCase.Server.Game.Entities.Components
             return Task.CompletedTask;
         }
 
-        Task IHandle<SpawnEntity>.Handle(SpawnEntity message)
+        Task IHandle<PlayerLoggedIn>.Handle(PlayerLoggedIn message)
         {
             CompleteSpawn();
+            AttachedObject.QueueOperation(() =>
+            {
+                return GrainFactory.GetGrain<IWorldPartition>(AttachedObject.GetAddressByPartitionKey()).Enter(AttachedObject);
+            });
             return Task.CompletedTask;
         }
     }

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

@@ -24,7 +24,9 @@ namespace MineCase.Server.Game.Entities.Components
 
         protected override async Task OnAttached()
         {
-            await AttachedObject.SetLocalValue(SlotsProperty, Enumerable.Repeat(Slot.Empty, _slotsCount).ToArray());
+            var slots = AttachedObject.GetValue(SlotsProperty);
+            if (slots == null || slots.Length != _slotsCount)
+                await AttachedObject.SetLocalValue(SlotsProperty, Enumerable.Repeat(Slot.Empty, _slotsCount).ToArray());
             await base.OnAttached();
         }
 
@@ -37,6 +39,7 @@ namespace MineCase.Server.Game.Entities.Components
             if (old != slot)
             {
                 old = slot;
+                MarkDirty();
                 return SlotChanged.InvokeSerial(this, (index, slot));
             }
 
@@ -58,5 +61,10 @@ namespace MineCase.Server.Game.Entities.Components
 
         Task<Slot> IHandle<AskSlot, Slot>.Handle(AskSlot message) =>
             Task.FromResult(GetSlot(message.Index));
+
+        private void MarkDirty()
+        {
+            AttachedObject.ValueStorage.IsDirty = true;
+        }
     }
 }

+ 7 - 0
src/MineCase.Server.Grains/Game/Entities/Components/SyncMobStateComponent.cs

@@ -16,6 +16,13 @@ namespace MineCase.Server.Game.Entities.Components
         {
         }
 
+        protected override Task OnAttached()
+        {
+            if (AttachedObject.GetValue(IsEnabledComponent.IsEnabledProperty))
+                InstallPropertyChangedHandlers();
+            return base.OnAttached();
+        }
+
         Task IHandle<SpawnMob>.Handle(SpawnMob message)
         {
             InstallPropertyChangedHandlers();

+ 14 - 0
src/MineCase.Server.Grains/Game/Entities/Components/SyncPlayerStateComponent.cs

@@ -6,12 +6,16 @@ using MineCase.Engine;
 using MineCase.Graphics;
 using MineCase.Server.Components;
 using MineCase.Server.Network.Play;
+using MineCase.Server.User;
 using MineCase.World;
+using Orleans;
 
 namespace MineCase.Server.Game.Entities.Components
 {
     internal class SyncPlayerStateComponent : Component<PlayerGrain>, IHandle<PlayerLoggedIn>, IHandle<BindToUser>
     {
+        private IUser _user;
+
         public SyncPlayerStateComponent(string name = "syncPlayerState")
             : base(name)
         {
@@ -63,8 +67,18 @@ namespace MineCase.Server.Game.Entities.Components
 
         async Task IHandle<BindToUser>.Handle(BindToUser message)
         {
+            AttachedObject.GetComponent<SlotContainerComponent>().SlotChanged -= InventorySlotChanged;
+
+            _user = message.User;
             await AttachedObject.GetComponent<NameComponent>().SetName(await message.User.GetName());
             await AttachedObject.GetComponent<SlotContainerComponent>().SetSlots(await message.User.GetInventorySlots());
+
+            AttachedObject.GetComponent<SlotContainerComponent>().SlotChanged += InventorySlotChanged;
+        }
+
+        private Task InventorySlotChanged(object sender, (int index, Slot slot) e)
+        {
+            return _user.SetInventorySlot(e.index, e.slot);
         }
     }
 }

+ 8 - 1
src/MineCase.Server.Grains/Game/Entities/EntityGrain.cs

@@ -7,13 +7,18 @@ using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.Game.Entities.Components;
 using MineCase.Server.Network.Play;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
 using MineCase.Server.World;
 using MineCase.World;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game.Entities
 {
-    internal abstract class EntityGrain : DependencyObject, IEntity
+    [PersistTableName("entity")]
+    [Reentrant]
+    internal abstract class EntityGrain : PersistableDependencyObject, IEntity
     {
         public Guid UUID => this.GetPrimaryKey();
 
@@ -29,6 +34,7 @@ namespace MineCase.Server.Game.Entities
 
         protected override async Task InitializeComponents()
         {
+            await SetComponent(new IsEnabledComponent());
             await SetComponent(new EntityIdComponent());
             await SetComponent(new WorldComponent());
             await SetComponent(new EntityWorldPositionComponent());
@@ -37,6 +43,7 @@ namespace MineCase.Server.Game.Entities
             await SetComponent(new ChunkEventBroadcastComponent());
             await SetComponent(new GameTickComponent());
             await SetComponent(new ChunkAccessorComponent());
+            await SetComponent(new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute));
         }
 
         Task<uint> IEntity.GetEntityId() =>

+ 2 - 0
src/MineCase.Server.Grains/Game/Entities/MobGrain.cs

@@ -6,9 +6,11 @@ using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.Game.Entities.Components;
 using MineCase.Server.Network.Play;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game.Entities
 {
+    [Reentrant]
     internal class MobGrain : EntityGrain, IMob
     {
         protected override async Task InitializeComponents()

+ 2 - 0
src/MineCase.Server.Grains/Game/Entities/MonsterGrain.cs

@@ -8,9 +8,11 @@ using MineCase.Server.Game.Entities.Components;
 using MineCase.Server.World;
 using MineCase.Server.World.EntitySpawner;
 using MineCase.World;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game.Entities
 {
+    [Reentrant]
     internal class MonsterGrain : EntityGrain, IMonster
     {
         /*

+ 2 - 0
src/MineCase.Server.Grains/Game/Entities/PassiveMobGrain.cs

@@ -9,9 +9,11 @@ using MineCase.Server.Game.Entities.Components;
 using MineCase.Server.World;
 using MineCase.Server.World.EntitySpawner;
 using MineCase.World;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game.Entities
 {
+    [Reentrant]
     internal class PassiveMobGrain : EntityGrain, IPassiveMob
     {
         /*

+ 13 - 26
src/MineCase.Server.Grains/Game/GameSession.cs

@@ -9,10 +9,13 @@ using MineCase.Protocol.Play;
 using MineCase.Server.Network.Play;
 using MineCase.Server.User;
 using MineCase.Server.World;
+using MineCase.World;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game
 {
+    [Reentrant]
     internal class GameSession : Grain, IGameSession
     {
         private IWorld _world;
@@ -22,8 +25,6 @@ namespace MineCase.Server.Game
         private IDisposable _gameTick;
         private DateTime _lastGameTickTime;
 
-        private HashSet<ITickable> _tickables;
-
         private ILogger _logger;
 
         public override async Task OnActivateAsync()
@@ -32,8 +33,7 @@ namespace MineCase.Server.Game
             _world = await GrainFactory.GetGrain<IWorldAccessor>(0).GetWorld(this.GetPrimaryKeyString());
             _chunkSender = GrainFactory.GetGrain<IChunkSender>(this.GetPrimaryKeyString());
             _lastGameTickTime = DateTime.UtcNow;
-            _tickables = new HashSet<ITickable>();
-            _gameTick = RegisterTimer(OnGameTick, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(50));
+            _gameTick = RegisterTimer(OnGameTick, null, TimeSpan.Zero, TimeSpan.FromMilliseconds(5));
         }
 
         public async Task JoinGame(IUser user)
@@ -99,21 +99,20 @@ namespace MineCase.Server.Game
             {
                 var now = DateTime.UtcNow;
                 var deltaTime = now - _lastGameTickTime;
+
+                if (deltaTime.TotalMilliseconds < 30) return;
+
                 _lastGameTickTime = now;
 
                 var worldTime = await _world.GetTime();
+                var timeArgs = new GameTickArgs { DeltaTime = deltaTime, WorldAge = worldTime.WorldAge, TimeOfDay = worldTime.TimeOfDay };
+
+                await _world.OnGameTick(timeArgs);
+                Task.WhenAll(from u in _users.Keys
+                             select u.OnGameTick(timeArgs)).Ignore();
 
                 if (worldTime.WorldAge % 20 == 0)
-                {
-                    await Task.WhenAll(from u in _users.Values
-                                       select u.Generator.TimeUpdate(worldTime.WorldAge, worldTime.TimeOfDay));
-                }
-
-                await _world.OnGameTick(deltaTime);
-                await Task.WhenAll(from u in _users.Keys
-                                   select u.OnGameTick(deltaTime, worldTime.WorldAge));
-                await Task.WhenAll(from u in _tickables
-                                   select u.OnGameTick(deltaTime, worldTime.WorldAge));
+                    _logger.LogInformation($"Delta Game Tick: {deltaTime.TotalMilliseconds}ms.");
             }
             catch (Exception ex)
             {
@@ -140,18 +139,6 @@ namespace MineCase.Server.Game
             return Task.FromResult(jsonData);
         }
 
-        public Task Subscribe(ITickable tickable)
-        {
-            _tickables.Add(tickable);
-            return Task.CompletedTask;
-        }
-
-        public Task Unsubscribe(ITickable tickable)
-        {
-            _tickables.Remove(tickable);
-            return Task.CompletedTask;
-        }
-
         private class UserContext
         {
             public ClientPlayPacketGenerator Generator { get; set; }

+ 4 - 1
src/MineCase.Server.Grains/GrainsAssemblyExtensions.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Reflection;
 using System.Text;
 using Autofac;
+using MineCase.Engine;
 
 namespace MineCase.Server
 {
@@ -10,7 +11,9 @@ namespace MineCase.Server
     {
         public static ICollection<Assembly> AddGrains(this ICollection<Assembly> assemblies)
         {
-            assemblies.Add(typeof(GrainsAssemblyExtensions).Assembly);
+            var assembly = typeof(GrainsAssemblyExtensions).Assembly;
+            assemblies.Add(assembly);
+            DependencyProperty.OwnerTypeLoader = t => assembly.GetType(t, true);
             return assemblies;
         }
     }

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

@@ -26,6 +26,8 @@
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004" PrivateAssets="All" />
     <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
     <PackageReference Include="System.Numerics.Vectors" Version="4.4.0" />
+    <PackageReference Include="MongoDB.Driver" Version="2.4.4" />
+    <PackageReference Include="System.Threading.Tasks.Dataflow" Version="4.7.0" />
   </ItemGroup>
 
   <ItemGroup>

+ 1 - 0
src/MineCase.Server.Grains/Network/ClientboundPacketSinkGrain.cs

@@ -11,6 +11,7 @@ using Orleans.Concurrency;
 
 namespace MineCase.Server.Network
 {
+    [Reentrant]
     internal class ClientboundPacketSinkGrain : Grain, IClientboundPacketSink
     {
         private ObserverSubscriptionManager<IClientboundPacketObserver> _subsManager;

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

@@ -6,9 +6,11 @@ using MineCase.Protocol.Login;
 using MineCase.Server.Game;
 using MineCase.Server.User;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Network.Login
 {
+    [Reentrant]
     internal class LoginFlowGrain : Grain, ILoginFlow
     {
         private bool _useAuthentication = false;

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

@@ -3,7 +3,11 @@ using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
 using MineCase.Engine;
+using MineCase.Server.Components;
+using MineCase.Server.Game.Entities;
 using MineCase.Server.Game.Entities.Components;
+using MineCase.Server.User;
+using Orleans;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.Network.Play
@@ -11,6 +15,7 @@ namespace MineCase.Server.Network.Play
     internal class ClientboundPacketComponent : Component, IHandle<BindToUser>, IHandle<KickPlayer>, IHandle<PacketForwardToPlayer>
     {
         private IClientboundPacketSink _sink;
+        private IUser _user;
 
         public ClientboundPacketComponent(string name = "clientboundPacket")
             : base(name)
@@ -20,13 +25,16 @@ namespace MineCase.Server.Network.Play
         public ClientPlayPacketGenerator GetGenerator()
             => new ClientPlayPacketGenerator(_sink);
 
-        public Task Kick()
+        public async Task Kick()
         {
-            return _sink.Close();
+            await AttachedObject.Tell(Disable.Default);
+            if (_user.GetPlayer() == AttachedObject.AsReference<IPlayer>())
+                await _user.Kick();
         }
 
         async Task IHandle<BindToUser>.Handle(BindToUser message)
         {
+            _user = message.User;
             _sink = await message.User.GetClientPacketSink();
         }
 

+ 15 - 6
src/MineCase.Server.Grains/Network/Play/ServerboundPacketComponent.cs

@@ -1,9 +1,11 @@
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.IO;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
+using System.Threading.Tasks.Dataflow;
 using Microsoft.Extensions.Logging;
 using MineCase.Engine;
 using MineCase.Protocol;
@@ -19,11 +21,13 @@ namespace MineCase.Server.Network.Play
 {
     internal class ServerboundPacketComponent : Component<PlayerGrain>, IHandle<ServerboundPacketMessage>
     {
-        private readonly Queue<object> _deferredPacket = new Queue<object>();
+        private readonly ConcurrentQueue<object> _deferredPacket = new ConcurrentQueue<object>();
+        private readonly ActionBlock<UncompressedPacket> _receivePacket;
 
         public ServerboundPacketComponent(string name = "serverboundPacket")
             : base(name)
         {
+            _receivePacket = new ActionBlock<UncompressedPacket>((Action<UncompressedPacket>)OnReceivePacket);
         }
 
         protected override Task OnAttached()
@@ -40,18 +44,23 @@ namespace MineCase.Server.Network.Play
             return base.OnDetached();
         }
 
-        private async Task OnGameTick(object sender, (TimeSpan deltaTime, long worldAge) e)
+        private async Task OnGameTick(object sender, GameTickArgs e)
         {
-            while (_deferredPacket.Count != 0)
-                await DispatchPacket((dynamic)_deferredPacket.Dequeue());
+            while (_deferredPacket.TryDequeue(out var packet))
+                await DispatchPacket((dynamic)packet);
         }
 
         Task IHandle<ServerboundPacketMessage>.Handle(ServerboundPacketMessage message)
         {
-            var packet = DeserializePlayPacket(message.Packet);
+            _receivePacket.Post(message.Packet);
+            return Task.CompletedTask;
+        }
+
+        private void OnReceivePacket(UncompressedPacket rawPacket)
+        {
+            var packet = DeserializePlayPacket(rawPacket);
             if (packet != null)
                 _deferredPacket.Enqueue(packet);
-            return Task.CompletedTask;
         }
 
         private Task DispatchPacket(object packet)

+ 33 - 0
src/MineCase.Server.Grains/Persistence/AppDbContext.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Extensions.Options;
+using MineCase.Engine.Serialization;
+using MineCase.Server.Settings;
+using MongoDB.Driver;
+
+namespace MineCase.Server.Persistence
+{
+    internal class AppDbContext
+    {
+        public const string DatabaseName = "minecase";
+
+        private readonly IMongoDatabase _db;
+
+        public AppDbContext(IOptions<PersistenceOptions> options)
+        {
+            var client = new MongoClient(options.Value.ConnectionString);
+            _db = client.GetDatabase(DatabaseName);
+        }
+
+        public IMongoCollection<DependencyObjectState> GetEntityStateCollection(string name)
+        {
+            return _db.GetCollection<DependencyObjectState>(name);
+        }
+
+        public IMongoCollection<T> GetCollection<T>(string name)
+        {
+            return _db.GetCollection<T>(name);
+        }
+    }
+}

+ 41 - 0
src/MineCase.Server.Grains/Persistence/Components/AutoSaveStateComponent.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Engine;
+using MineCase.Server.Components;
+using MineCase.World;
+
+namespace MineCase.Server.Persistence.Components
+{
+    internal class AutoSaveStateComponent : Component
+    {
+        private readonly int _periodTime;
+
+        public const int PerMinute = 20 * 60;
+
+        public AutoSaveStateComponent(int periodTime, string name = "autoSaveState")
+            : base(name)
+        {
+            _periodTime = periodTime;
+        }
+
+        protected override Task OnAttached()
+        {
+            var tickComponent = AttachedObject.GetComponent<GameTickComponent>();
+            if (tickComponent != null)
+                tickComponent.Tick += OnGameTick;
+            return base.OnAttached();
+        }
+
+        public Task OnGameTick(object sender, GameTickArgs e)
+        {
+            if (AttachedObject.ValueStorage.IsDirty && (e.WorldAge % _periodTime == 0))
+            {
+                return AttachedObject.WriteStateAsync();
+            }
+
+            return Task.CompletedTask;
+        }
+    }
+}

+ 45 - 0
src/MineCase.Server.Grains/Persistence/Components/StateComponent.cs

@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Engine;
+using MineCase.Engine.Serialization;
+
+namespace MineCase.Server.Persistence.Components
+{
+    public sealed class InitializeStateMark
+    {
+        public static readonly InitializeStateMark Default = new InitializeStateMark();
+    }
+
+    public class StateComponent<T> : Component, IHandle<AfterReadState>, IHandle<BeforeWriteState>
+    {
+        public static readonly DependencyProperty<T> StateProperty =
+            DependencyProperty.Register<T>(nameof(State), typeof(StateComponent<T>));
+
+        public T State => AttachedObject.GetValue(StateProperty);
+
+        public event AsyncEventHandler<EventArgs> BeforeWriteState;
+
+        public event AsyncEventHandler<EventArgs> AfterReadState;
+
+        public StateComponent(string name = "state")
+            : base(name)
+        {
+        }
+
+        async Task IHandle<AfterReadState>.Handle(AfterReadState message)
+        {
+            // 如果为 null 需要初始化状态
+            if (State == null)
+                await AttachedObject.SetLocalValue(StateProperty, (T)Activator.CreateInstance(typeof(T), InitializeStateMark.Default));
+
+            await AfterReadState.InvokeSerial(this, EventArgs.Empty);
+        }
+
+        Task IHandle<BeforeWriteState>.Handle(BeforeWriteState message)
+        {
+            return BeforeWriteState.InvokeSerial(this, EventArgs.Empty);
+        }
+    }
+}

+ 60 - 0
src/MineCase.Server.Grains/Persistence/PersistableDependencyObject.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using MineCase.Engine;
+using MineCase.Engine.Serialization;
+using MongoDB.Driver;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Persistence
+{
+    [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
+    public sealed class PersistTableName : Attribute
+    {
+        public string TableName { get; }
+
+        public PersistTableName(string tableName)
+        {
+            TableName = tableName;
+        }
+    }
+
+    [Reentrant]
+    public abstract class PersistableDependencyObject : DependencyObject
+    {
+        protected override async Task SerializeStateAsync(DependencyObjectState state)
+        {
+            var coll = GetStateCollection();
+            var key = GrainReference.ToKeyString();
+            await coll.ReplaceOneAsync(o => o.GrainKeyString == key, state, new UpdateOptions { IsUpsert = true });
+        }
+
+        protected override async Task<DependencyObjectState> DeserializeStateAsync()
+        {
+            var coll = GetStateCollection();
+            var key = GrainReference.ToKeyString();
+            return await coll.Find(o => o.GrainKeyString == key).FirstOrDefaultAsync();
+        }
+
+        protected override async Task ClearStateAsync()
+        {
+            var coll = GetStateCollection();
+            var key = GrainReference.ToKeyString();
+            await coll.DeleteOneAsync(o => o.GrainKeyString == key);
+        }
+
+        private IMongoCollection<DependencyObjectState> GetStateCollection()
+        {
+            var db = ServiceProvider.GetRequiredService<AppDbContext>();
+            return db.GetEntityStateCollection(GetTablePrefix());
+        }
+
+        private string GetTablePrefix()
+        {
+            return GetType().GetCustomAttribute<PersistTableName>().TableName;
+        }
+    }
+}

+ 15 - 0
src/MineCase.Server.Grains/Persistence/PersistenceModule.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Autofac;
+
+namespace MineCase.Server.Persistence
+{
+    internal class PersistenceModule : Autofac.Module
+    {
+        protected override void Load(ContainerBuilder builder)
+        {
+            builder.RegisterType<AppDbContext>().SingleInstance();
+        }
+    }
+}

+ 14 - 0
src/MineCase.Server.Grains/Settings/PersistenceOptions.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Microsoft.Extensions.Options;
+
+namespace MineCase.Server.Settings
+{
+    public sealed class PersistenceOptions : IOptions<PersistenceOptions>
+    {
+        public string ConnectionString { get; set; }
+
+        PersistenceOptions IOptions<PersistenceOptions>.Value => this;
+    }
+}

+ 26 - 10
src/MineCase.Server.Grains/User/NonAuthenticatedUserGrain.cs

@@ -2,30 +2,46 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.User
 {
-    internal class NonAuthenticatedUserGrain : Grain, INonAuthenticatedUser
+    [PersistTableName("nonAuthenticatedUser")]
+    [Reentrant]
+    internal class NonAuthenticatedUserGrain : PersistableDependencyObject, INonAuthenticatedUser
     {
-        private Guid _uuid;
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
 
-        public override Task OnActivateAsync()
+        protected override async Task InitializePreLoadComponent()
         {
-            _uuid = Guid.NewGuid();
-            return base.OnActivateAsync();
+            await SetComponent(new StateComponent<StateHolder>());
         }
 
-        public Task<Guid> GetUUID()
-        {
-            return Task.FromResult(_uuid);
-        }
+        public Task<Guid> GetUUID() => Task.FromResult(State.UUID);
 
         public async Task<IUser> GetUser()
         {
-            var user = GrainFactory.GetGrain<IUser>(_uuid);
+            var user = GrainFactory.GetGrain<IUser>(State.UUID);
             await user.SetName(this.GetPrimaryKeyString());
+            await WriteStateAsync();
             return user;
         }
+
+        internal class StateHolder
+        {
+            public Guid UUID { get; set; }
+
+            public StateHolder()
+            {
+            }
+
+            public StateHolder(InitializeStateMark mark)
+            {
+                UUID = Guid.NewGuid();
+            }
+        }
     }
 }

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

@@ -9,9 +9,11 @@ using MineCase.Server.Network.Play;
 using MineCase.Server.World;
 using MineCase.World;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.User
 {
+    [Reentrant]
     internal class UserChunkLoaderGrain : Grain, IUserChunkLoader
     {
         private IPlayer _player;
@@ -25,12 +27,6 @@ namespace MineCase.Server.User
 
         private int _viewDistance = 10;
 
-        public override Task OnActivateAsync()
-        {
-            _player = GrainFactory.GetGrain<IPlayer>(this.GetPrimaryKey());
-            return Task.CompletedTask;
-        }
-
         public Task OnChunkSent(ChunkWorldPos chunkPos)
         {
             _sendingChunks.Remove(chunkPos);
@@ -119,14 +115,32 @@ namespace MineCase.Server.User
             return Task.CompletedTask;
         }
 
-        public Task JoinGame(IWorld world, IPlayer player)
+        public async Task JoinGame(IWorld world, IPlayer player)
         {
             _world = world;
             _player = player;
             _lastStreamedChunk = null;
+            if (_sendingChunks != null)
+            {
+                foreach (var chunkPos in _sendingChunks)
+                {
+                    await GrainFactory.GetPartitionGrain<IChunkTrackingHub>(_world, chunkPos).Unsubscribe(_player);
+                    await GrainFactory.GetPartitionGrain<IWorldPartition>(_world, chunkPos).Leave(_player);
+                }
+            }
+
             _sendingChunks = new HashSet<ChunkWorldPos>();
+
+            if (_sentChunks != null)
+            {
+                foreach (var chunkPos in _sentChunks)
+                {
+                    await GrainFactory.GetPartitionGrain<IChunkTrackingHub>(_world, chunkPos).Unsubscribe(_player);
+                    await GrainFactory.GetPartitionGrain<IWorldPartition>(_world, chunkPos).Leave(_player);
+                }
+            }
+
             _sentChunks = new HashSet<ChunkWorldPos>();
-            return Task.CompletedTask;
         }
 
         public Task SetViewDistance(int viewDistance)

+ 75 - 31
src/MineCase.Server.Grains/User/UserGrain.cs

@@ -5,12 +5,15 @@ using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
 using MineCase.Protocol;
+using MineCase.Server.Components;
 using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 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.Persistence.Components;
 using MineCase.Server.World;
 using MineCase.World;
 using Orleans;
@@ -18,37 +21,39 @@ using Orleans.Concurrency;
 
 namespace MineCase.Server.User
 {
+    [PersistTableName("user")]
     [Reentrant]
-    internal class UserGrain : Grain, IUser
+    internal class UserGrain : PersistableDependencyObject, IUser
     {
-        private string _name;
         private uint _protocolVersion;
-        private string _worldId;
-        private IWorld _world;
         private IClientboundPacketSink _sink;
         private IPacketRouter _packetRouter;
         private ClientPlayPacketGenerator _generator;
-        private UserState _state;
-
         private IPlayer _player;
+        private UserState _userState;
+
+        private AutoSaveStateComponent _autoSave;
 
-        private Slot[] _slots;
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
 
-        public override async Task OnActivateAsync()
+        protected override async Task InitializePreLoadComponent()
         {
-            if (string.IsNullOrEmpty(_worldId))
+            var stateComponent = new StateComponent<StateHolder>();
+            await SetComponent(stateComponent);
+            stateComponent.AfterReadState += StateComponent_AfterReadState;
+
+            _autoSave = new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute);
+            await SetComponent(_autoSave);
+        }
+
+        private async Task StateComponent_AfterReadState(object sender, EventArgs e)
+        {
+            if (State.World == null)
             {
                 var world = await GrainFactory.GetGrain<IWorldAccessor>(0).GetDefaultWorld();
-                _worldId = world.GetPrimaryKeyString();
-                _world = world;
+                State.World = world;
+                MarkDirty();
             }
-
-            _world = await GrainFactory.GetGrain<IWorldAccessor>(0).GetWorld(_worldId);
-            _slots = new[]
-            {
-                new Slot { BlockId = (short)BlockId.Furnace, ItemCount = 1 },
-                new Slot { BlockId = (short)BlockId.Wood, ItemCount = 8 }
-            }.Concat(Enumerable.Repeat(Slot.Empty, SlotArea.UserSlotsCount - 2)).ToArray();
         }
 
         public Task<IClientboundPacketSink> GetClientPacketSink()
@@ -62,7 +67,7 @@ namespace MineCase.Server.User
             return GrainFactory.GetGrain<IGameSession>(world.GetPrimaryKeyString());
         }
 
-        public Task<IWorld> GetWorld() => Task.FromResult(_world);
+        public Task<IWorld> GetWorld() => Task.FromResult(State.World);
 
         public Task SetClientPacketSink(IClientboundPacketSink sink)
         {
@@ -74,20 +79,23 @@ namespace MineCase.Server.User
         public async Task JoinGame()
         {
             _player = GrainFactory.GetGrain<IPlayer>(this.GetPrimaryKey());
+            await _player.Tell(Disable.Default);
+            await _player.Tell(BeginLogin.Default);
             await _player.Tell(new BindToUser { User = this.AsReference<IUser>() });
 
-            _state = UserState.JoinedGame;
+            _userState = UserState.JoinedGame;
 
             // 设置出生点
+            var world = State.World;
             await _player.Tell(new SpawnEntity
             {
-                World = _world,
-                EntityId = await _world.NewEntityId(),
+                World = world,
+                EntityId = await world.NewEntityId(),
                 Position = new EntityWorldPos(0, 200, 0)
             });
         }
 
-        private async Task KickPlayer()
+        public async Task Kick()
         {
             var game = await GetGameSession();
             await game.LeaveGame(this);
@@ -100,7 +108,7 @@ namespace MineCase.Server.User
         public async Task NotifyLoggedIn()
         {
             await _player.Tell(PlayerLoggedIn.Default);
-            _state = UserState.DownloadingWorld;
+            _userState = UserState.DownloadingWorld;
         }
 
         public Task SendChatMessage(Chat jsonData, byte position)
@@ -111,12 +119,13 @@ namespace MineCase.Server.User
 
         public Task<String> GetName()
         {
-            return Task.FromResult(_name);
+            return Task.FromResult(State.Name);
         }
 
         public Task SetName(string name)
         {
-            _name = name;
+            State.Name = name;
+            MarkDirty();
             return Task.CompletedTask;
         }
 
@@ -131,18 +140,19 @@ namespace MineCase.Server.User
             return Task.CompletedTask;
         }
 
-        public Task OnGameTick(TimeSpan deltaTime, long worldAge)
+        public async Task OnGameTick(GameTickArgs e)
         {
-            if (_state == UserState.DownloadingWorld)
+            if (_userState == UserState.DownloadingWorld)
             {
                 // await _player.SendPositionAndLook();
-                _state = UserState.Playing;
+                _userState = UserState.Playing;
             }
 
             /*
             if (_state >= UserState.JoinedGame && _state < UserState.Destroying)
                 await _chunkLoader.OnGameTick(worldAge);*/
-            return Task.CompletedTask;
+            await _generator.TimeUpdate(e.WorldAge, e.TimeOfDay);
+            await _autoSave.OnGameTick(this, e);
         }
 
         public Task SetPacketRouter(IPacketRouter packetRouter)
@@ -151,7 +161,7 @@ namespace MineCase.Server.User
             return Task.CompletedTask;
         }
 
-        public Task<Slot[]> GetInventorySlots() => Task.FromResult(_slots);
+        public Task<Slot[]> GetInventorySlots() => Task.FromResult(State.Slots);
 
         public Task ForwardPacket(UncompressedPacket packet)
         {
@@ -161,6 +171,13 @@ namespace MineCase.Server.User
             });
         }
 
+        public Task SetInventorySlot(int index, Slot slot)
+        {
+            State.Slots[index] = slot;
+            MarkDirty();
+            return Task.CompletedTask;
+        }
+
         private enum UserState : uint
         {
             None,
@@ -169,5 +186,32 @@ namespace MineCase.Server.User
             Playing,
             Destroying
         }
+
+        private void MarkDirty()
+        {
+            ValueStorage.IsDirty = true;
+        }
+
+        internal class StateHolder
+        {
+            public string Name { get; set; }
+
+            public IWorld World { get; set; }
+
+            public Slot[] Slots { get; set; }
+
+            public StateHolder()
+            {
+            }
+
+            public StateHolder(InitializeStateMark mark)
+            {
+                Slots = new[]
+                {
+                    new Slot { BlockId = (short)BlockId.Furnace, ItemCount = 1 },
+                    new Slot { BlockId = (short)BlockId.Wood, ItemCount = 8 }
+                }.Concat(Enumerable.Repeat(Slot.Empty, SlotArea.UserSlotsCount - 2)).ToArray();
+            }
+        }
     }
 }

+ 16 - 0
src/MineCase.Server.Grains/User/UserLifecycle.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Orleans;
+using Orleans.Runtime;
+
+namespace MineCase.Server.User
+{
+    internal class UserLifecycle : LifecycleObservable, IUserLifecycle
+    {
+        public UserLifecycle(Logger logger)
+            : base(logger)
+        {
+        }
+    }
+}

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

@@ -7,7 +7,7 @@ using Orleans;
 
 namespace MineCase.Server.World
 {
-    internal abstract class AddressByPartitionGrain : Grain, IAddressByPartition
+    internal abstract class AddressByPartitionGrain : Persistence.PersistableDependencyObject, IAddressByPartition
     {
         protected IWorld World { get; private set; }
 

+ 78 - 40
src/MineCase.Server.Grains/World/ChunkColumnGrain.cs

@@ -8,6 +8,8 @@ using MineCase.Server.Game.BlockEntities;
 using MineCase.Server.Game.Blocks;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Network.Play;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
 using MineCase.Server.Settings;
 using MineCase.Server.World.Generation;
 using MineCase.World;
@@ -18,41 +20,41 @@ using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [PersistTableName("chunkColumn")]
     [Reentrant]
-    internal class ChunkColumnGrain : Grain, IChunkColumn
+    internal class ChunkColumnGrain : AddressByPartitionGrain, IChunkColumn
     {
-        private IWorld _world;
-        private ChunkWorldPos _chunkPos;
+        private AutoSaveStateComponent _autoSave;
 
-        private bool _generated = false;
-        private ChunkColumnCompactStorage _state;
-        private Dictionary<BlockChunkPos, IBlockEntity> _blockEntities;
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
+
+        protected override async Task InitializePreLoadComponent()
+        {
+            await SetComponent(new StateComponent<StateHolder>());
+        }
+
+        protected override async Task InitializeComponents()
+        {
+            _autoSave = new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute * 5);
+            await SetComponent(_autoSave);
+        }
 
         public async Task<BlockState> GetBlockState(int x, int y, int z)
         {
             await EnsureChunkGenerated();
-            return _state[x, y, z];
+            return State.Storage[x, y, z];
         }
 
         public async Task<ChunkColumnCompactStorage> GetState()
         {
             await EnsureChunkGenerated();
-            return _state;
+            return State.Storage;
         }
 
         public async Task<BiomeId> GetBlockBiome(int x, int z)
         {
             await EnsureChunkGenerated();
-            return (BiomeId)_state.Biomes[(z * ChunkConstants.BlockEdgeWidthInSection) + x];
-        }
-
-        public override Task OnActivateAsync()
-        {
-            var keys = this.GetWorldAndChunkWorldPos();
-            _world = GrainFactory.GetGrain<IWorld>(keys.worldKey);
-            _chunkPos = keys.chunkWorldPos;
-            _blockEntities = new Dictionary<BlockChunkPos, IBlockEntity>();
-            return Task.CompletedTask;
+            return (BiomeId)State.Storage.Biomes[(z * ChunkConstants.BlockEdgeWidthInSection) + x];
         }
 
         public static readonly (int x, int z)[] CrossCoords = new[]
@@ -63,14 +65,15 @@ namespace MineCase.Server.World
         public async Task SetBlockState(int x, int y, int z, BlockState blockState)
         {
             await EnsureChunkGenerated();
-            var oldState = _state[x, y, z];
+            var state = State;
+            var oldState = state.Storage[x, y, z];
 
             if (oldState != blockState)
             {
-                _state[x, y, z] = blockState;
+                state.Storage[x, y, z] = blockState;
 
                 var chunkPos = new BlockChunkPos(x, y, z);
-                var blockWorldPos = chunkPos.ToBlockWorldPos(_chunkPos);
+                var blockWorldPos = chunkPos.ToBlockWorldPos(ChunkWorldPos);
                 await GetBroadcastGenerator().BlockChange(blockWorldPos, blockState);
 
                 if (oldState.Id != blockState.Id)
@@ -79,7 +82,7 @@ namespace MineCase.Server.World
                     var newEntity = BlockEntity.Create(GrainFactory, (BlockId)blockState.Id);
 
                     // 删除旧的 BlockEntity
-                    if (_blockEntities.TryGetValue(chunkPos, out var entity))
+                    if (state.BlockEntities.TryGetValue(chunkPos, out var entity))
                     {
                         if (newEntity != null && entity.GetPrimaryKeyString() == newEntity.GetPrimaryKeyString())
                             replaceOld = false;
@@ -87,15 +90,15 @@ namespace MineCase.Server.World
                         if (replaceOld)
                         {
                             await entity.Tell(DestroyBlockEntity.Default);
-                            _blockEntities.Remove(chunkPos);
+                            state.BlockEntities.Remove(chunkPos);
                         }
                     }
 
                     // 添加新的 BlockEntity
                     if (newEntity != null && replaceOld)
                     {
-                        _blockEntities.Add(chunkPos, newEntity);
-                        await newEntity.Tell(new SpawnBlockEntity { World = _world, Position = blockWorldPos });
+                        state.BlockEntities.Add(chunkPos, newEntity);
+                        await newEntity.Tell(new SpawnBlockEntity { World = World, Position = blockWorldPos });
                     }
                 }
 
@@ -107,29 +110,30 @@ namespace MineCase.Server.World
                    neighborPos.Z += crossCoord.z;
                    var chunk = neighborPos.ToChunkWorldPos();
                    var blockChunkPos = neighborPos.ToBlockChunkPos();
-                   return GrainFactory.GetPartitionGrain<IChunkColumn>(_world, chunk).OnBlockNeighborChanged(
+                   return GrainFactory.GetPartitionGrain<IChunkColumn>(World, chunk).OnBlockNeighborChanged(
                        blockChunkPos.X, blockChunkPos.Y, blockChunkPos.Z, blockWorldPos, oldState, blockState);
                }));
+                MarkDirty();
             }
         }
 
         private async Task EnsureChunkGenerated()
         {
-            if (!_generated)
+            if (!State.Generated)
             {
                 var serverSetting = GrainFactory.GetGrain<IServerSettings>(0);
                 string worldType = (await serverSetting.GetSettings()).LevelType;
                 if (worldType == "DEFAULT" || worldType == "default")
                 {
-                    var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(await _world.GetSeed());
+                    var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(await World.GetSeed());
                     GeneratorSettings settings = new GeneratorSettings
                     {
                     };
-                    _state = await generator.Generate(_world, _chunkPos.X, _chunkPos.Z, settings);
+                    State.Storage = await generator.Generate(World, ChunkWorldPos.X, ChunkWorldPos.Z, settings);
                 }
                 else if (worldType == "FLAT" || worldType == "flat")
                 {
-                    var generator = GrainFactory.GetGrain<IChunkGeneratorFlat>(await _world.GetSeed());
+                    var generator = GrainFactory.GetGrain<IChunkGeneratorFlat>(await World.GetSeed());
                     GeneratorSettings settings = new GeneratorSettings
                     {
                         FlatBlockId = new BlockState?[]
@@ -142,39 +146,73 @@ namespace MineCase.Server.World
                             BlockStates.Grass()
                         }
                     };
-                    _state = await generator.Generate(_world, _chunkPos.X, _chunkPos.Z, settings);
+                    State.Storage = await generator.Generate(World, ChunkWorldPos.X, ChunkWorldPos.Z, settings);
                 }
                 else
                 {
-                    var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(await _world.GetSeed());
+                    var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(await World.GetSeed());
                     GeneratorSettings settings = new GeneratorSettings
                     {
                     };
-                    _state = await generator.Generate(_world, _chunkPos.X, _chunkPos.Z, settings);
+                    State.Storage = await generator.Generate(World, ChunkWorldPos.X, ChunkWorldPos.Z, settings);
                 }
 
-                _generated = true;
+                State.Generated = true;
+                await WriteStateAsync();
             }
         }
 
         protected ClientPlayPacketGenerator GetBroadcastGenerator()
         {
-            return new ClientPlayPacketGenerator(GrainFactory.GetPartitionGrain<IChunkTrackingHub>(_world, _chunkPos));
+            return new ClientPlayPacketGenerator(GrainFactory.GetPartitionGrain<IChunkTrackingHub>(World, ChunkWorldPos));
         }
 
         public Task<IBlockEntity> GetBlockEntity(int x, int y, int z)
         {
-            if (_blockEntities.TryGetValue(new BlockChunkPos(x, y, z), out var entity))
+            if (State.BlockEntities.TryGetValue(new BlockChunkPos(x, y, z), out var entity))
                 return Task.FromResult(entity);
             return Task.FromResult<IBlockEntity>(null);
         }
 
         public Task OnBlockNeighborChanged(int x, int y, int z, BlockWorldPos neighborPosition, BlockState oldState, BlockState newState)
         {
-            var block = _state[x, y, z];
-            var blockHandler = BlockHandler.Create((BlockId)block.Id);
-            var selfPosition = new BlockChunkPos(x, y, z).ToBlockWorldPos(_chunkPos);
-            return blockHandler.OnNeighborChanged(selfPosition, neighborPosition, oldState, newState, GrainFactory, _world);
+            if (State.Generated)
+            {
+                var block = State.Storage[x, y, z];
+                var blockHandler = BlockHandler.Create((BlockId)block.Id);
+                var selfPosition = new BlockChunkPos(x, y, z).ToBlockWorldPos(ChunkWorldPos);
+                return blockHandler.OnNeighborChanged(selfPosition, neighborPosition, oldState, newState, GrainFactory, World);
+            }
+
+            return Task.CompletedTask;
+        }
+
+        public Task OnGameTick(GameTickArgs e)
+        {
+            return _autoSave.OnGameTick(this, e);
+        }
+
+        private void MarkDirty()
+        {
+            ValueStorage.IsDirty = true;
+        }
+
+        internal class StateHolder
+        {
+            public bool Generated { get; set; }
+
+            public ChunkColumnCompactStorage Storage { get; set; }
+
+            public Dictionary<BlockChunkPos, IBlockEntity> BlockEntities { get; set; }
+
+            public StateHolder()
+            {
+            }
+
+            public StateHolder(InitializeStateMark mark)
+            {
+                BlockEntities = new Dictionary<BlockChunkPos, IBlockEntity>();
+            }
         }
     }
 }

+ 13 - 9
src/MineCase.Server.Grains/World/ChunkTrackingHub.cs

@@ -8,7 +8,9 @@ using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Network;
 using MineCase.Server.Network.Play;
-using MineCase.Server.User;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
+using MineCase.World;
 using Orleans;
 using Orleans.Concurrency;
 
@@ -18,19 +20,13 @@ namespace MineCase.Server.World
     internal class ChunkTrackingHub : Grain, IChunkTrackingHub
     {
         private readonly IPacketPackager _packetPackager;
-        private Dictionary<IPlayer, IPacketSink> _trackingPlayers;
+        private Dictionary<IPlayer, IPacketSink> _trackingPlayers = new Dictionary<IPlayer, IPacketSink>();
         private BroadcastPacketSink _broadcastPacketSink;
 
         public ChunkTrackingHub(IPacketPackager packetPackager)
         {
             _packetPackager = packetPackager;
-        }
-
-        public override Task OnActivateAsync()
-        {
-            _trackingPlayers = new Dictionary<IPlayer, IPacketSink>();
-            _broadcastPacketSink = new BroadcastPacketSink(_trackingPlayers.Values, _packetPackager);
-            return base.OnActivateAsync();
+            _broadcastPacketSink = new BroadcastPacketSink(_trackingPlayers.Values, packetPackager);
         }
 
         public Task SendPacket(ISerializablePacket packet)
@@ -46,7 +42,10 @@ namespace MineCase.Server.World
         public Task Subscribe(IPlayer player)
         {
             if (!_trackingPlayers.ContainsKey(player))
+            {
                 _trackingPlayers.Add(player, new ForwardToPlayerPacketSink(player, _packetPackager));
+            }
+
             return Task.CompletedTask;
         }
 
@@ -60,5 +59,10 @@ namespace MineCase.Server.World
         {
             return Task.FromResult(_trackingPlayers.Keys.ToList());
         }
+
+        public Task OnGameTick(GameTickArgs e)
+        {
+            return Task.CompletedTask;
+        }
     }
 }

+ 54 - 21
src/MineCase.Server.Grains/World/CollectableFinder.cs

@@ -10,54 +10,63 @@ using MineCase.Server.Components;
 using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Game.Entities.Components;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
 using MineCase.World;
 using Orleans;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [PersistTableName("collectableFinder")]
     [Reentrant]
-    internal class CollectableFinder : Grain, ICollectableFinder
+    internal class CollectableFinder : AddressByPartitionGrain, ICollectableFinder
     {
-        private IWorld _world;
-
-        private List<(Cuboid box, ICollectableFinder finder)> _neighborFinders;
-
         public static readonly (int x, int z)[] CrossCoords = new[]
         {
             (0, 0), (-1, 0), (-1, 1), (0, 1), (1, 1), (1, 0), (1, -1), (0, -1), (-1, -1)
         };
 
-        /*
-        private List<ICollectable> _collectables;
-        */
-        public override Task OnActivateAsync()
+        private AutoSaveStateComponent _autoSave;
+        private List<(Cuboid box, ICollectableFinder finder)> _neighborFinders;
+
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
+
+        public override async Task OnActivateAsync()
         {
-            _world = GrainFactory.GetGrain<IWorld>(this.GetWorldAndChunkWorldPos().worldKey);
-            var selfPos = this.GetChunkWorldPos();
+            await base.OnActivateAsync();
             _neighborFinders = new List<(Cuboid box, ICollectableFinder finder)>();
             foreach (var crossCoord in CrossCoords)
             {
-                var newPos = new ChunkWorldPos(selfPos.X + crossCoord.x, selfPos.Z + crossCoord.z);
+                var newPos = new ChunkWorldPos(ChunkWorldPos.X + crossCoord.x, ChunkWorldPos.Z + crossCoord.z);
                 var shape = new Cuboid(new Point3d(newPos.X * 16, newPos.Z * 16, 0), new Size(16, 16, 256));
-                _neighborFinders.Add((shape, GrainFactory.GetPartitionGrain<ICollectableFinder>(_world, newPos)));
+                _neighborFinders.Add((shape, GrainFactory.GetPartitionGrain<ICollectableFinder>(World, newPos)));
             }
+        }
 
-            _colliders = new Dictionary<IDependencyObject, Shape>();
-            return base.OnActivateAsync();
+        protected override async Task InitializePreLoadComponent()
+        {
+            var state = new StateComponent<StateHolder>();
+            await SetComponent(state);
         }
 
-        private Dictionary<IDependencyObject, Shape> _colliders;
+        protected override async Task InitializeComponents()
+        {
+            _autoSave = new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute);
+            await SetComponent(_autoSave);
+        }
 
         public Task RegisterCollider(IDependencyObject entity, Shape colliderShape)
         {
-            _colliders[entity] = colliderShape;
+            State.Colliders[entity] = colliderShape;
+            MarkDirty();
             return Collision(entity, colliderShape);
         }
 
         public Task UnregisterCollider(IDependencyObject entity)
         {
-            _colliders.Remove(entity);
+            if (State.Colliders.Remove(entity))
+                MarkDirty();
             return Task.CompletedTask;
         }
 
@@ -79,8 +88,8 @@ namespace MineCase.Server.World
                 var pickup = GrainFactory.GetGrain<IPickup>(Guid.NewGuid());
                 await pickup.Tell(new SpawnEntity
                 {
-                    World = _world,
-                    EntityId = await _world.NewEntityId(),
+                    World = World,
+                    EntityId = await World.NewEntityId(),
                     Position = position
                 });
                 await pickup.Tell(new SetSlot { Slot = slot });
@@ -90,7 +99,7 @@ namespace MineCase.Server.World
         public Task<IReadOnlyCollection<IDependencyObject>> CollisionInChunk(Shape colliderShape)
         {
             List<IDependencyObject> result = null;
-            foreach (var collider in _colliders)
+            foreach (var collider in State.Colliders)
             {
                 if (collider.Value.CollideWith(colliderShape))
                 {
@@ -102,5 +111,29 @@ namespace MineCase.Server.World
 
             return Task.FromResult((IReadOnlyCollection<IDependencyObject>)result ?? Array.Empty<IDependencyObject>());
         }
+
+        private void MarkDirty()
+        {
+            ValueStorage.IsDirty = true;
+        }
+
+        public Task OnGameTick(GameTickArgs e)
+        {
+            return _autoSave.OnGameTick(this, e);
+        }
+
+        public class StateHolder
+        {
+            public Dictionary<IDependencyObject, Shape> Colliders { get; set; }
+
+            public StateHolder()
+            {
+            }
+
+            public StateHolder(InitializeStateMark mark)
+            {
+                Colliders = new Dictionary<IDependencyObject, Shape>();
+            }
+        }
     }
 }

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

@@ -1,42 +1,76 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.Components;
 using MineCase.Server.Game.Entities;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
+using MineCase.World;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
-    internal class TickEmitterGrain : Grain, ITickEmitter
+    [PersistTableName("tickEmitter")]
+    [Reentrant]
+    internal class TickEmitterGrain : PersistableDependencyObject, ITickEmitter
     {
-        private HashSet<IDependencyObject> _subscription;
+        private AutoSaveStateComponent _autoSave;
 
-        public override Task OnActivateAsync()
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
+
+        protected override async Task InitializePreLoadComponent()
         {
-            _subscription = new HashSet<IDependencyObject>();
-            return base.OnActivateAsync();
+            await SetComponent(new StateComponent<StateHolder>());
         }
 
-        public Task OnGameTick(TimeSpan deltaTime, long worldAge)
+        protected override async Task InitializeComponents()
         {
-            var message = new GameTick { DeltaTime = deltaTime, WorldAge = worldAge };
-            foreach (var entity in _subscription)
-                entity.InvokeOneWay(e => e.Tell(message));
-            return Task.CompletedTask;
+            _autoSave = new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute);
+            await SetComponent(_autoSave);
+        }
+
+        public async Task OnGameTick(GameTickArgs e)
+        {
+            var message = new GameTick { Args = e };
+            await Task.WhenAll(from en in State.Subscription select en.Tell(message));
+            await _autoSave.OnGameTick(this, e);
         }
 
         public Task Subscribe(IDependencyObject observer)
         {
-            _subscription.Add(observer);
+            if (State.Subscription.Add(observer))
+                MarkDirty();
             return Task.CompletedTask;
         }
 
         public Task Unsubscribe(IDependencyObject observer)
         {
-            _subscription.Remove(observer);
+            if (State.Subscription.Remove(observer))
+                MarkDirty();
             return Task.CompletedTask;
         }
+
+        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>();
+            }
+        }
     }
 }

+ 2 - 0
src/MineCase.Server.Grains/World/WorldAccessorGrain.cs

@@ -3,9 +3,11 @@ using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [Reentrant]
     internal class WorldAccessorGrain : Grain, IWorldAccessor
     {
         private const string _defaultWorldName = "defaultWorld";

+ 49 - 18
src/MineCase.Server.Grains/World/WorldGrain.cs

@@ -1,56 +1,67 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using MineCase.Server.Game;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
 using MineCase.Server.Settings;
-using MineCase.Server.World.Generation;
+using MineCase.World;
 using MineCase.World.Generation;
 using Orleans;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [PersistTableName("world")]
     [Reentrant]
-    internal class WorldGrain : Grain, IWorld
+    internal class WorldGrain : PersistableDependencyObject, IWorld
     {
-        private uint _nextAvailEId;
-        private long _worldAge;
         private GeneratorSettings _genSettings; // 生成设置
         private string _seed; // 世界种子
-        private HashSet<IWorldPartition> _activedPartitions;
+        private AutoSaveStateComponent _autoSave;
+        private readonly HashSet<IWorldPartition> _activedPartitions = new HashSet<IWorldPartition>();
 
-        public override async Task OnActivateAsync()
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
+
+        protected override async Task InitializePreLoadComponent()
         {
-            IServerSettings serverSettings = GrainFactory.GetGrain<IServerSettings>(0);
-            _nextAvailEId = 0;
+            await SetComponent(new StateComponent<StateHolder>());
+
+            var serverSettings = GrainFactory.GetGrain<IServerSettings>(0);
             _genSettings = new GeneratorSettings();
             await InitGeneratorSettings(_genSettings);
             _seed = (await serverSettings.GetSettings()).LevelSeed;
-            _activedPartitions = new HashSet<IWorldPartition>();
-            await base.OnActivateAsync();
+        }
+
+        protected override async Task InitializeComponents()
+        {
+            _autoSave = new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute);
+            await SetComponent(_autoSave);
         }
 
         public Task<WorldTime> GetTime()
         {
-            return Task.FromResult(new WorldTime { WorldAge = _worldAge, TimeOfDay = _worldAge % 24000 });
+            return Task.FromResult(new WorldTime { WorldAge = State.WorldAge, TimeOfDay = State.WorldAge % 24000 });
         }
 
         public Task<uint> NewEntityId()
         {
-            var id = _nextAvailEId++;
+            var id = State.NextAvailEId++;
+            MarkDirty();
             return Task.FromResult(id);
         }
 
-        public Task OnGameTick(TimeSpan deltaTime)
+        public async Task OnGameTick(GameTickArgs e)
         {
-            _worldAge++;
-            foreach (var partition in _activedPartitions)
-                partition.InvokeOneWay(p => p.OnGameTick(deltaTime, _worldAge));
-            return Task.CompletedTask;
+            State.WorldAge++;
+            MarkDirty();
+            await Task.WhenAll(from p in _activedPartitions select p.OnGameTick(e));
+            await _autoSave.OnGameTick(this, e);
         }
 
-        public Task<long> GetAge() => Task.FromResult(_worldAge);
+        public Task<long> GetAge() => Task.FromResult(State.WorldAge);
 
         public Task<int> GetSeed()
         {
@@ -87,5 +98,25 @@ namespace MineCase.Server.World
             _activedPartitions.Remove(worldPartition);
             return Task.CompletedTask;
         }
+
+        private void MarkDirty()
+        {
+            ValueStorage.IsDirty = true;
+        }
+
+        internal class StateHolder
+        {
+            public long WorldAge { get; set; }
+
+            public uint NextAvailEId { get; set; }
+
+            public StateHolder()
+            {
+            }
+
+            public StateHolder(InitializeStateMark mark)
+            {
+            }
+        }
     }
 }

+ 71 - 24
src/MineCase.Server.Grains/World/WorldPartitionGrain.cs

@@ -6,65 +6,112 @@ using System.Threading.Tasks;
 using MineCase.Server.Game.BlockEntities;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Game.Entities.Components;
+using MineCase.Server.Persistence;
+using MineCase.Server.Persistence.Components;
+using MineCase.World;
 using Orleans;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [PersistTableName("worldPartition")]
     [Reentrant]
     internal class WorldPartitionGrain : AddressByPartitionGrain, IWorldPartition
     {
         private ITickEmitter _tickEmitter;
-        private HashSet<IPlayer> _players;
-        private HashSet<IEntity> _discoveryEntities;
+        private ICollectableFinder _collectableFinder;
+        private IChunkTrackingHub _chunkTrackingHub;
+        private IChunkColumn _chunkColumn;
+        private AutoSaveStateComponent _autoSave;
+        private HashSet<IPlayer> _players = new HashSet<IPlayer>();
 
-        public override Task OnActivateAsync()
+        private StateHolder State => GetValue(StateComponent<StateHolder>.StateProperty);
+
+        protected override async Task InitializePreLoadComponent()
         {
+            await SetComponent(new StateComponent<StateHolder>());
+
             _tickEmitter = GrainFactory.GetPartitionGrain<ITickEmitter>(this);
-            _players = new HashSet<IPlayer>();
-            _discoveryEntities = new HashSet<IEntity>();
-            return base.OnActivateAsync();
+            _collectableFinder = GrainFactory.GetPartitionGrain<ICollectableFinder>(this);
+            _chunkTrackingHub = GrainFactory.GetPartitionGrain<IChunkTrackingHub>(this);
+            _chunkColumn = GrainFactory.GetPartitionGrain<IChunkColumn>(this);
         }
 
-        public async Task Enter(IPlayer player)
+        protected override async Task InitializeComponents()
         {
-            var active = _players.Count == 0;
-            _players.Add(player);
+            _autoSave = new AutoSaveStateComponent(AutoSaveStateComponent.PerMinute);
+            await SetComponent(_autoSave);
+        }
 
-            var message = new DiscoveredByPlayer { Player = player };
-            await Task.WhenAll(from e in _discoveryEntities
-                               select e.Tell(message));
+        public async Task Enter(IPlayer player)
+        {
+            bool active = _players.Count == 0;
+            if (_players.Add(player))
+            {
+                var message = new DiscoveredByPlayer { Player = player };
+                await Task.WhenAll(from e in State.DiscoveryEntities
+                                   select e.Tell(message));
 
-            if (active)
-                await World.ActivePartition(this);
+                if (active)
+                    await World.ActivePartition(this);
+            }
         }
 
         public async Task Leave(IPlayer player)
         {
-            _players.Remove(player);
-            if (_players.Count == 0)
+            if (_players.Remove(player))
             {
-                await World.DeactivePartition(this);
-                DeactivateOnIdle();
+                if (_players.Count == 0)
+                {
+                    await World.DeactivePartition(this);
+                    DeactivateOnIdle();
+                }
             }
         }
 
-        public Task OnGameTick(TimeSpan deltaTime, long worldAge)
+        public async Task OnGameTick(GameTickArgs e)
         {
-            _tickEmitter.InvokeOneWay(e => e.OnGameTick(deltaTime, worldAge));
-            return Task.CompletedTask;
+            await Task.WhenAll(
+                _tickEmitter.OnGameTick(e),
+                _collectableFinder.OnGameTick(e),
+                _chunkTrackingHub.OnGameTick(e),
+                _chunkColumn.OnGameTick(e));
+            await _autoSave.OnGameTick(this, e);
         }
 
         async Task IWorldPartition.SubscribeDiscovery(IEntity entity)
         {
-            _discoveryEntities.Add(entity);
-            await entity.Tell(BroadcastDiscovered.Default);
+            if (State.DiscoveryEntities.Add(entity))
+            {
+                MarkDirty();
+                await entity.Tell(BroadcastDiscovered.Default);
+            }
         }
 
         Task IWorldPartition.UnsubscribeDiscovery(IEntity entity)
         {
-            _discoveryEntities.Remove(entity);
+            if (State.DiscoveryEntities.Remove(entity))
+                MarkDirty();
             return Task.CompletedTask;
         }
+
+        private void MarkDirty()
+        {
+            ValueStorage.IsDirty = true;
+        }
+
+        internal class StateHolder
+        {
+            public HashSet<IEntity> DiscoveryEntities { get; set; }
+
+            public StateHolder()
+            {
+            }
+
+            public StateHolder(InitializeStateMark mark)
+            {
+                DiscoveryEntities = new HashSet<IEntity>();
+            }
+        }
     }
 }

+ 8 - 3
src/MineCase.Server.Interfaces/Components/EntityMessages.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Text;
 using MineCase.Engine;
 using MineCase.Server.Game.Entities;
+using MineCase.World;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.Components
@@ -10,9 +11,7 @@ namespace MineCase.Server.Components
     [Immutable]
     public sealed class GameTick : IEntityMessage
     {
-        public TimeSpan DeltaTime { get; set; }
-
-        public long WorldAge { get; set; }
+        public GameTickArgs Args { get; set; }
     }
 
     [Immutable]
@@ -21,6 +20,12 @@ namespace MineCase.Server.Components
         public static readonly Disable Default = new Disable();
     }
 
+    [Immutable]
+    public sealed class Enable : IEntityMessage
+    {
+        public static readonly Enable Default = new Enable();
+    }
+
     [Immutable]
     public sealed class CollisionWith : IEntityMessage
     {

+ 6 - 0
src/MineCase.Server.Interfaces/Game/Entities/EntityMessages.cs

@@ -64,6 +64,12 @@ namespace MineCase.Server.Game.Entities.Components
         public static readonly AskPlayerDescription Default = new AskPlayerDescription();
     }
 
+    [Immutable]
+    public sealed class BeginLogin : IEntityMessage
+    {
+        public static readonly BeginLogin Default = new BeginLogin();
+    }
+
     [Immutable]
     public class SpawnEntity : IEntityMessage
     {

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

@@ -18,9 +18,5 @@ namespace MineCase.Server.Game
         Task SendChatMessage(IUser sender, String message);
 
         Task SendChatMessage(IUser sender, IUser receiver, String messages);
-
-        Task Subscribe(ITickable tickable);
-
-        Task Unsubscribe(ITickable tickable);
     }
 }

+ 8 - 1
src/MineCase.Server.Interfaces/User/IUser.cs

@@ -8,7 +8,9 @@ using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Network;
 using MineCase.Server.World;
+using MineCase.World;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.User
 {
@@ -28,6 +30,8 @@ namespace MineCase.Server.User
 
         Task<IPlayer> GetPlayer();
 
+        Task Kick();
+
         Task SetClientPacketSink(IClientboundPacketSink sink);
 
         Task<IClientboundPacketSink> GetClientPacketSink();
@@ -38,12 +42,15 @@ namespace MineCase.Server.User
 
         Task SendChatMessage(Chat jsonData, Byte position);
 
-        Task OnGameTick(TimeSpan deltaTime, long worldAge);
+        [OneWay]
+        Task OnGameTick(GameTickArgs e);
 
         Task SetPacketRouter(IPacketRouter packetRouter);
 
         Task<Slot[]> GetInventorySlots();
 
         Task ForwardPacket(UncompressedPacket packet);
+
+        Task SetInventorySlot(int index, Slot slot);
     }
 }

+ 11 - 0
src/MineCase.Server.Interfaces/User/IUserLifeCycle.cs

@@ -0,0 +1,11 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Orleans;
+
+namespace MineCase.Server.User
+{
+    public interface IUserLifecycle : ILifecycleObservable
+    {
+    }
+}

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

@@ -24,5 +24,7 @@ namespace MineCase.Server.World
         Task<IBlockEntity> GetBlockEntity(int x, int y, int z);
 
         Task OnBlockNeighborChanged(int x, int y, int z, BlockWorldPos neighborPosition, BlockState oldState, BlockState newState);
+
+        Task OnGameTick(GameTickArgs e);
     }
 }

+ 3 - 0
src/MineCase.Server.Interfaces/World/IChunkTrackingHub.cs

@@ -6,6 +6,7 @@ using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Network;
 using MineCase.Server.User;
+using MineCase.World;
 using Orleans;
 
 namespace MineCase.Server.World
@@ -17,5 +18,7 @@ namespace MineCase.Server.World
         Task Unsubscribe(IPlayer player);
 
         Task<List<IPlayer>> GetTrackedPlayers();
+
+        Task OnGameTick(GameTickArgs e);
     }
 }

+ 3 - 9
src/MineCase.Server.Interfaces/World/ICollectableFinder.cs

@@ -7,6 +7,7 @@ using MineCase.Engine;
 using MineCase.Graphics;
 using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
+using MineCase.World;
 using Orleans;
 using Orleans.Concurrency;
 
@@ -14,15 +15,6 @@ namespace MineCase.Server.World
 {
     public interface ICollectableFinder : IAddressByPartition
     {
-        /*
-        Task Register(ICollectable collectable);
-
-        Task Unregister(ICollectable collectable);
-
-        Task<IReadOnlyCollection<ICollectable>> Collision(IEntity entity);
-
-        Task<IReadOnlyCollection<ICollectable>> CollisionInChunk(IEntity entity);
-        */
         Task RegisterCollider(IDependencyObject entity, Shape colliderShape);
 
         Task UnregisterCollider(IDependencyObject entity);
@@ -30,5 +22,7 @@ namespace MineCase.Server.World
         Task<IReadOnlyCollection<IDependencyObject>> CollisionInChunk(Shape colliderShape);
 
         Task SpawnPickup(Vector3 position, Immutable<Slot[]> slots);
+
+        Task OnGameTick(GameTickArgs e);
     }
 }

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

@@ -4,6 +4,7 @@ using System.Text;
 using System.Threading.Tasks;
 using MineCase.Engine;
 using MineCase.Server.Game.Entities;
+using MineCase.World;
 using Orleans;
 using Orleans.Concurrency;
 
@@ -11,8 +12,7 @@ namespace MineCase.Server.World
 {
     public interface ITickEmitter : IAddressByPartition
     {
-        [OneWay]
-        Task OnGameTick(TimeSpan deltaTime, long worldAge);
+        Task OnGameTick(GameTickArgs e);
 
         Task Subscribe(IDependencyObject observer);
 

+ 2 - 1
src/MineCase.Server.Interfaces/World/IWorld.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
 using MineCase.Server.Game;
+using MineCase.World;
 using MineCase.World.Generation;
 using Orleans;
 
@@ -16,7 +17,7 @@ namespace MineCase.Server.World
 
         Task<long> GetAge();
 
-        Task OnGameTick(TimeSpan deltaTime);
+        Task OnGameTick(GameTickArgs e);
 
         Task<int> GetSeed();
 

+ 2 - 2
src/MineCase.Server.Interfaces/World/IWorldPartition.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
 using MineCase.Server.Game.Entities;
+using MineCase.World;
 using Orleans.Concurrency;
 
 namespace MineCase.Server.World
@@ -13,8 +14,7 @@ namespace MineCase.Server.World
 
         Task Leave(IPlayer player);
 
-        [OneWay]
-        Task OnGameTick(TimeSpan deltaTime, long worldAge);
+        Task OnGameTick(GameTickArgs e);
 
         Task SubscribeDiscovery(IEntity entity);
 

+ 3 - 1
src/MineCase.Server/AppBootstrapper.cs

@@ -11,6 +11,7 @@ using System.Linq;
 using Autofac.Extensions.DependencyInjection;
 using Microsoft.IO;
 using Autofac;
+using MineCase.Server.Settings;
 
 namespace MineCase.Server
 {
@@ -21,6 +22,7 @@ namespace MineCase.Server
             services.AddOptions();
             services.AddLogging();
             services.AddSingleton<RecyclableMemoryStreamManager>();
+            services.Configure<PersistenceOptions>(Configuration.GetSection("persistenceOptions"));
 
             var container = new ContainerBuilder();
             container.Populate(services);
@@ -31,7 +33,7 @@ namespace MineCase.Server
         private static void ConfigureAppConfiguration(IConfigurationBuilder configurationBuilder)
         {
             configurationBuilder.SetBasePath(Directory.GetCurrentDirectory())
-                .AddJsonFile("config.json", true, false);
+                .AddJsonFile("config.json", false, false);
         }
 
         private static void SelectAssemblies()

+ 1 - 0
src/MineCase.Server/Dockerfile

@@ -4,4 +4,5 @@ WORKDIR /app
 EXPOSE 30000
 COPY ${source:-obj/Docker/publish} .
 COPY ${source:-obj/Docker/publish}/OrleansConfiguration.docker.xml OrleansConfiguration.dev.xml
+COPY ${source:-obj/Docker/publish}/config.docker.json config.json
 ENTRYPOINT ["dotnet", "MineCase.Server.dll"]

+ 8 - 0
src/MineCase.Server/MineCase.Server.csproj

@@ -54,6 +54,7 @@
   <ItemGroup>
     <None Remove="banned-ips.json" />
     <None Remove="banned-players.json" />
+    <None Remove="config.json" />
     <None Remove="ops.json" />
     <None Remove="OrleansConfiguration.docker.xml" />
     <None Remove="server.json" />
@@ -73,6 +74,12 @@
     <Content Include="banned-players.json">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
+    <Content Include="config.docker.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="config.json">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
     <Content Include="ops.json">
       <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
     </Content>
@@ -88,6 +95,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\MineCase.Serialization\MineCase.Serialization.csproj" />
     <ProjectReference Include="..\MineCase.Server.Grains\MineCase.Server.Grains.csproj" />
   </ItemGroup>
 

+ 21 - 3
src/MineCase.Server/Program.cs

@@ -1,9 +1,11 @@
-using Orleans.Hosting;
+using Microsoft.Extensions.Configuration;
+using Orleans.Hosting;
 using Orleans.Runtime.Configuration;
 using System;
 using System.Reflection;
 using System.Threading;
 using System.Threading.Tasks;
+using MineCase.Serialization.Serializers;
 
 namespace MineCase.Server
 {
@@ -13,21 +15,37 @@ namespace MineCase.Server
         private static ISiloHost _siloHost;
         private static Assembly[] _assemblies;
 
+        public static IConfiguration Configuration { get; private set; }
+
         static async Task Main(string[] args)
         {
+            Serializers.RegisterAll();
+
+            var configBuilder = new ConfigurationBuilder();
+            ConfigureAppConfiguration(configBuilder);
+            Configuration = configBuilder.Build();
+
             var builder = new SiloHostBuilder()
                 .ConfigureLogging(ConfigureLogging)
-                .ConfigureAppConfiguration(ConfigureAppConfiguration)
                 .UseConfiguration(LoadClusterConfiguration())
                 .UseServiceProviderFactory(ConfigureServices);
             SelectAssemblies();
             ConfigureApplicationParts(builder);
             _siloHost = builder.Build();
-            await _siloHost.StartAsync();
+            await StartAsync();
             Console.WriteLine("Press Ctrl+C to terminate...");
             Console.CancelKeyPress += (s, e) => _exitEvent.Set();
             _exitEvent.WaitOne();
+            Console.WriteLine("Stopping...");
             await _siloHost.StopAsync();
+            await _siloHost.Stopped;
+            Console.WriteLine("Stopped.");
+        }
+
+        private static async Task StartAsync()
+        {
+            Serializers.RegisterAll(_siloHost.Services);
+            await _siloHost.StartAsync();
         }
 
         private static ClusterConfiguration LoadClusterConfiguration()

+ 5 - 0
src/MineCase.Server/config.docker.json

@@ -0,0 +1,5 @@
+{
+  "persistenceOptions": {
+    "connectionString": "mongodb://minecase.persistdb:27017"
+  }
+}

+ 5 - 0
src/MineCase.Server/config.json

@@ -0,0 +1,5 @@
+{
+  "persistenceOptions": {
+    "connectionString": "mongodb://localhost:27017"
+  }
+}

+ 11 - 0
src/MineCase.sln

@@ -54,6 +54,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docker", "docker", "{C32ACB
 EndProject
 Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker\docker-compose.dcproj", "{2974A4BE-85D2-454D-ACDE-B4BE63993B95}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MineCase.Serialization", "MineCase.Serialization\MineCase.Serialization.csproj", "{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}"
+EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Appveyor|Any CPU = Appveyor|Any CPU
@@ -161,6 +163,14 @@ Global
 		{2974A4BE-85D2-454D-ACDE-B4BE63993B95}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{2974A4BE-85D2-454D-ACDE-B4BE63993B95}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{2974A4BE-85D2-454D-ACDE-B4BE63993B95}.TravisCI|Any CPU.ActiveCfg = TravisCI|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.Appveyor|Any CPU.ActiveCfg = Appveyor|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.Appveyor|Any CPU.Build.0 = Appveyor|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.Release|Any CPU.Build.0 = Release|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.TravisCI|Any CPU.ActiveCfg = TravisCI|Any CPU
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669}.TravisCI|Any CPU.Build.0 = TravisCI|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -179,6 +189,7 @@ Global
 		{D586FFBB-7265-47CF-AD80-77AC7ACB0FF1} = {5FE43A62-34E9-44CC-B5FF-D66EC916CFA1}
 		{D2E3FF57-9287-4584-B8A1-57E19D44E0AF} = {5FE43A62-34E9-44CC-B5FF-D66EC916CFA1}
 		{2974A4BE-85D2-454D-ACDE-B4BE63993B95} = {C32ACBC4-BA67-488B-96CD-ED6CF3255767}
+		{AA85C7DA-31CB-499B-A74F-CF4FDC8EF669} = {CB620063-55E2-4694-B1E0-E99BB032BF03}
 	EndGlobalSection
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {7AB995C3-E961-463C-8A55-3149C51761B5}

+ 7 - 1
tests/MineCase.Tests.sln

@@ -1,20 +1,26 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 15
-VisualStudioVersion = 15.0.26730.10
+VisualStudioVersion = 15.0.27004.2005
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MineCase.UnitTest", "UnitTest\MineCase.UnitTest.csproj", "{8EA43298-B73A-4A32-9A43-59D385ADA787}"
 EndProject
 Global
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
+		Appveyor|Any CPU = Appveyor|Any CPU
 		Debug|Any CPU = Debug|Any CPU
 		Release|Any CPU = Release|Any CPU
+		TravisCI|Any CPU = TravisCI|Any CPU
 	EndGlobalSection
 	GlobalSection(ProjectConfigurationPlatforms) = postSolution
+		{8EA43298-B73A-4A32-9A43-59D385ADA787}.Appveyor|Any CPU.ActiveCfg = Appveyor|Any CPU
+		{8EA43298-B73A-4A32-9A43-59D385ADA787}.Appveyor|Any CPU.Build.0 = Appveyor|Any CPU
 		{8EA43298-B73A-4A32-9A43-59D385ADA787}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
 		{8EA43298-B73A-4A32-9A43-59D385ADA787}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{8EA43298-B73A-4A32-9A43-59D385ADA787}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{8EA43298-B73A-4A32-9A43-59D385ADA787}.Release|Any CPU.Build.0 = Release|Any CPU
+		{8EA43298-B73A-4A32-9A43-59D385ADA787}.TravisCI|Any CPU.ActiveCfg = TravisCI|Any CPU
+		{8EA43298-B73A-4A32-9A43-59D385ADA787}.TravisCI|Any CPU.Build.0 = TravisCI|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE

+ 5 - 3
tests/UnitTest/MineCase.UnitTest.csproj

@@ -8,10 +8,10 @@
 
   <ItemGroup>
     <PackageReference Include="ImageSharp" Version="1.0.0-alpha9-00187" />
-    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0-preview-20170810-02" />
+    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.5.0" />
     <PackageReference Include="Newtonsoft.Json" Version="10.0.3" />
-    <PackageReference Include="xunit" Version="2.3.0-beta4-build3742" />
-    <PackageReference Include="xunit.runner.visualstudio" Version="2.3.0-beta4-build3742" />
+    <PackageReference Include="xunit" Version="2.3.1" />
+    <PackageReference Include="xunit.runner.visualstudio" Version="2.3.1" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004" PrivateAssets="All" />
     <PackageReference Include="System.Numerics.Vectors" Version="4.4.0" />
   </ItemGroup>
@@ -22,8 +22,10 @@
   
   <ItemGroup>
     <ProjectReference Include="..\..\src\MineCase.Algorithm\MineCase.Algorithm.csproj" />
+    <ProjectReference Include="..\..\src\MineCase.Engine\MineCase.Server.Engine.csproj" />
     <ProjectReference Include="..\..\src\MineCase.Nbt\MineCase.Nbt.csproj" />
     <ProjectReference Include="..\..\src\MineCase.Protocol\MineCase.Protocol.csproj" />
+    <ProjectReference Include="..\..\src\MineCase.Serialization\MineCase.Serialization.csproj" />
     <ProjectReference Include="..\..\src\MineCase.Server.Interfaces\MineCase.Server.Interfaces.csproj" />
   </ItemGroup>
 

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff