Преглед изворни кода

Implement inventory system (Part I) (#51)

* hmmm

* 添加 Inventory 的各种 SlotArea

* 添加 LeftClick 逻辑

* 添加 RightClick 逻辑

* 重构,添加 crafting 流程

* hmmm

* 实现 crafting 配方匹配

* 添加 data

* 完成 Crafting Table

* 更新 crafting

* 分离 Block / Item 逻辑

* 修复关闭窗口奔溃的问题

* 添加 BlockEntity

* 实现 Chest

* hmmm

* 添加 Inventory 的各种 SlotArea

* 添加 LeftClick 逻辑

* 添加 RightClick 逻辑

* 重构,添加 crafting 流程

* hmmm

* 实现 crafting 配方匹配

* 添加 data

* 完成 Crafting Table

* 更新 crafting

* 分离 Block / Item 逻辑

* 修复关闭窗口奔溃的问题

* 添加 BlockEntity

* 实现 Chest

* 修复 Chest Window无法打开的 bug

* rebase

* Fix a mistake in ChunkColumnGrain

* clean code style

* 完成 Large Chest

* 添加 Furnace 基架

* 添加 Furnace 逻辑

* 完成 Furnace

* 修改 ITickable
sunnycase пре 8 година
родитељ
комит
bc82851846
100 измењених фајлова са 4219 додато и 212 уклоњено
  1. 1 1
      .gitignore
  2. 971 0
      data/crafting.txt
  3. 185 0
      data/furnace.txt
  4. 202 0
      src/MineCase.Algorithm/CraftingRecipeMatcher.cs
  5. 60 0
      src/MineCase.Algorithm/FurnaceRecipeMatcher.cs
  6. 4 0
      src/MineCase.Algorithm/MineCase.Algorithm.csproj
  7. 1 0
      src/MineCase.Gateway/MineCase.Gateway.csproj
  8. 0 19
      src/MineCase.Protocol/Formats/Slot.cs
  9. 5 0
      src/MineCase.Protocol/MineCase.Protocol.csproj
  10. 0 1
      src/MineCase.Protocol/Protocol/Play/BlockBreakAnimation.cs
  11. 0 1
      src/MineCase.Protocol/Protocol/Play/BlockChange.cs
  12. 0 1
      src/MineCase.Protocol/Protocol/Play/ChatMessage.cs
  13. 45 0
      src/MineCase.Protocol/Protocol/Play/ClickWindow.cs
  14. 1 1
      src/MineCase.Protocol/Protocol/Play/ClientboundAnimation.cs
  15. 38 0
      src/MineCase.Protocol/Protocol/Play/CloseWindow.cs
  16. 21 0
      src/MineCase.Protocol/Protocol/Play/ConfirmTransaction.cs
  17. 39 0
      src/MineCase.Protocol/Protocol/Play/OpenWindow.cs
  18. 1 1
      src/MineCase.Protocol/Protocol/Play/PlayerBlockPlacement.cs
  19. 1 1
      src/MineCase.Protocol/Protocol/Play/PlayerDigging.cs
  20. 1 1
      src/MineCase.Protocol/Protocol/Play/SetSlot.cs
  21. 1 1
      src/MineCase.Protocol/Protocol/Play/WindowItems.cs
  22. 30 0
      src/MineCase.Protocol/Protocol/Play/WindowProperty.cs
  23. 0 1
      src/MineCase.Protocol/Serialization/BinaryReaderExtensions.cs
  24. 5 2
      src/MineCase.Protocol/Serialization/BinaryWriterExtensions.cs
  25. 24 1
      src/MineCase.Protocol/Serialization/SpanReader.cs
  26. 16 0
      src/MineCase.Server.Grains/Game/BlockEntities/BlockEntitiesModule.cs
  27. 48 0
      src/MineCase.Server.Grains/Game/BlockEntities/BlockEntity.cs
  28. 34 0
      src/MineCase.Server.Grains/Game/BlockEntities/BlockEntityGrain.cs
  29. 97 0
      src/MineCase.Server.Grains/Game/BlockEntities/ChestBlockEntityGrain.cs
  30. 168 0
      src/MineCase.Server.Grains/Game/BlockEntities/FurnaceBlockEntity.cs
  31. 81 0
      src/MineCase.Server.Grains/Game/Blocks/BlockHandler.cs
  32. 111 0
      src/MineCase.Server.Grains/Game/Blocks/ChestBlockHandler.cs
  33. 29 0
      src/MineCase.Server.Grains/Game/Blocks/CraftingTableBlockHandler.cs
  34. 16 0
      src/MineCase.Server.Grains/Game/Blocks/DefaultBlockHandler.cs
  35. 29 0
      src/MineCase.Server.Grains/Game/Blocks/FurnaceBlockHandler.cs
  36. 44 0
      src/MineCase.Server.Grains/Game/CraftingRecipesGrain.cs
  37. 14 6
      src/MineCase.Server.Grains/Game/Entities/PickupGrain.cs
  38. 120 15
      src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs
  39. 41 0
      src/MineCase.Server.Grains/Game/FurnaceRecipesGrain.cs
  40. 2 0
      src/MineCase.Server.Grains/Game/GameModule.cs
  41. 19 1
      src/MineCase.Server.Grains/Game/GameSession.cs
  42. 62 0
      src/MineCase.Server.Grains/Game/Items/ChestItemHandler.cs
  43. 18 0
      src/MineCase.Server.Grains/Game/Items/DefaultItemHandler.cs
  44. 137 0
      src/MineCase.Server.Grains/Game/Items/ItemHandler.cs
  45. 35 0
      src/MineCase.Server.Grains/Game/Windows/ChestWindowGrain.cs
  46. 24 0
      src/MineCase.Server.Grains/Game/Windows/CraftingWindowGrain.cs
  47. 42 0
      src/MineCase.Server.Grains/Game/Windows/FurnaceWindowGrain.cs
  48. 32 26
      src/MineCase.Server.Grains/Game/Windows/InventoryWindowGrain.cs
  49. 15 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/ArmorSlotArea.cs
  50. 34 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/ChestSlotArea.cs
  51. 112 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/CraftingSlotArea.cs
  52. 34 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/FurnaceSlotArea.cs
  53. 15 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/HotbarSlotArea.cs
  54. 15 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/InventorySlotArea.cs
  55. 33 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/InventorySlotAreaBase.cs
  56. 15 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/OffhandSlotArea.cs
  57. 171 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/SlotArea.cs
  58. 69 0
      src/MineCase.Server.Grains/Game/Windows/SlotAreas/TemporarySlotArea.cs
  59. 190 3
      src/MineCase.Server.Grains/Game/Windows/WindowGrain.cs
  60. 2 0
      src/MineCase.Server.Grains/Game/Windows/WindowsModule.cs
  61. 4 0
      src/MineCase.Server.Grains/MineCase.Server.Grains.csproj
  62. 52 1
      src/MineCase.Server.Grains/Network/PacketRouterGrain.Play.cs
  63. 41 1
      src/MineCase.Server.Grains/Network/Play/ClientPlayPacketGenerator.cs
  64. 11 1
      src/MineCase.Server.Grains/User/UserGrain.cs
  65. 119 88
      src/MineCase.Server.Grains/World/ChunkColumnGrain.cs
  66. 33 1
      src/MineCase.Server.Grains/World/ChunkTrackingHub.cs
  67. 20 0
      src/MineCase.Server.Grains/World/CollectableFinder.cs
  68. 47 0
      src/MineCase.Server.Interfaces/Game/BlockEntities/IBlockEntity.cs
  69. 22 0
      src/MineCase.Server.Interfaces/Game/BlockEntities/IChestBlockEntity.cs
  70. 18 0
      src/MineCase.Server.Interfaces/Game/BlockEntities/IFurnaceBlockEntity.cs
  71. 0 1
      src/MineCase.Server.Interfaces/Game/Entities/EntityMetadata/Pickup.cs
  72. 2 0
      src/MineCase.Server.Interfaces/Game/Entities/ICollectable.cs
  73. 1 2
      src/MineCase.Server.Interfaces/Game/Entities/IPickup.cs
  74. 21 2
      src/MineCase.Server.Interfaces/Game/Entities/IPlayer.cs
  75. 15 0
      src/MineCase.Server.Interfaces/Game/ICraftingRecipes.cs
  76. 14 0
      src/MineCase.Server.Interfaces/Game/IFurnaceRecipes.cs
  77. 5 1
      src/MineCase.Server.Interfaces/Game/IGameSession.cs
  78. 11 0
      src/MineCase.Server.Interfaces/Game/ITickable.cs
  79. 14 0
      src/MineCase.Server.Interfaces/Game/Windows/IChestWindow.cs
  80. 10 0
      src/MineCase.Server.Interfaces/Game/Windows/ICraftingWindow.cs
  81. 15 0
      src/MineCase.Server.Interfaces/Game/Windows/IFurnaceWindow.cs
  82. 6 3
      src/MineCase.Server.Interfaces/Game/Windows/IInventoryWindow.cs
  83. 20 2
      src/MineCase.Server.Interfaces/Game/Windows/IWindow.cs
  84. 1 0
      src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj
  85. 3 1
      src/MineCase.Server.Interfaces/User/IUser.cs
  86. 5 0
      src/MineCase.Server.Interfaces/World/BlockStateExtensions.cs
  87. 1 1
      src/MineCase.Server.Interfaces/World/ChunkColumnStorage.cs
  88. 3 6
      src/MineCase.Server.Interfaces/World/IChunkColumn.cs
  89. 6 1
      src/MineCase.Server.Interfaces/World/IChunkTrackingHub.cs
  90. 4 0
      src/MineCase.Server.Interfaces/World/ICollectableFinder.cs
  91. 1 1
      src/MineCase.Server.Interfaces/World/IWorld.cs
  92. 48 1
      src/MineCase.Server.Interfaces/World/Position.cs
  93. 44 1
      src/MineCase.Server.Interfaces/World/WorldExtensions.cs
  94. 9 0
      src/MineCase.Server/MineCase.Server.csproj
  95. 16 4
      src/MineCase.sln
  96. 2 7
      src/Minecase.Core/Block.cs
  97. 1 1
      src/Minecase.Core/Blocks.cs
  98. 1 1
      src/Minecase.Core/Chat.cs
  99. 12 0
      src/Minecase.Core/ClickAction.cs
  100. 1 1
      src/Minecase.Core/ClientboundAnimationId.cs

+ 1 - 1
.gitignore

@@ -259,7 +259,7 @@ paket-files/
 # Python Tools for Visual Studio (PTVS)
 __pycache__/
 *.pyc
-*.txt
+src/**/*.txt
 
 # vscode
 .vscode/

+ 971 - 0
data/crafting.txt

@@ -0,0 +1,971 @@
+# This file describes the crafting recipes that Minecase knows.
+# The syntax is as follows:
+#   <Line> = <Recipe>#<Comment>
+#   <Recipe> = <Result> = <Ingredient1> | <Ingredient2> | ... | <IngredientN>
+#   <IngredientN> = <ItemID>, <X1> : <Y1>, <X2> : <Y2>, ..., <Xn> : <Yn>
+#   <ItemID> = <ItemType> [^<DamageValue>]
+#   <Xn>, <Yn> = "1" .. "3", or "*" for any value. "*:*" can be replaced by a single "*".
+#   <Result> = <ItemType> [^<DamageValue>] [, <Count>]
+#
+# The Xn, Yn coordinates are a reference to the crafting grid:
+#   1:1 | 2:1 | 3:1
+#   1:2 | 2:2 | 3:2
+#   1:3 | 2:3 | 3:3
+#
+# <ItemType> can be either a number, or an item name (checked against items.ini)
+#
+# ^<DamageValue> is optional, if not present, the default damage for the given item is used
+#
+# If the DamageValue in the ingredients list is set to -1, the ingredient matches the specified item with any DamageValue.
+# This is used e. g. for "any planks -> sticks", or beds using any color wool etc.
+#
+# Ingredients with an asterisk for a coord will not match already matched crafting grid items. This enables simplifying some of the recipes,
+# e. g. hoe: "Iron, 2:1, *:1"
+#   -- this means "one iron at 2:1, and another one at either 1:1 or 3:1"
+#
+# To require multiple items of the same type in a slot, specify the slot number several times:
+# "Iron, 1:1, 2:2, 2:2"
+#   -- this means "take one iron from slot 1:1 and two irons from slot 2:2"
+# Note that asterisked items cannot require multiple items in a single slot.
+#
+# Note that due to technical problems, it is NOT advised to use asterisked ingredients in crossing directions, such as "*:1, "2:*".
+# The parser may be unable to match such a recipe to the crafting grid!
+#
+# Whitespace is optional. Use it reasonably. You CAN use any whitespaces (including tabs) in the middle of lines!
+
+
+
+
+
+#******************************************************#
+# Basic Crafts
+#
+
+# Need to list each of the four log types, otherwise all logs would get converted into apple planks (^0)
+
+WoodPlanks^Acacia, 4  = Wood2^Acacia, *
+WoodPlanks^Birch, 4   = Wood^Birch, *
+Chest				  = WoodPlanks^-1, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3
+WoodPlanks^DarkOak, 4 = Wood2^DarkOak, *
+EnderChest			  = EyeofEnder,  2:2 | Obsidian, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3
+Furnace				  = Cobblestone, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3
+WoodPlanks^Jungle, 4  = Wood^Jungle, *
+WoodPlanks^Oak, 4	  = Wood^Oak, * 
+WoodPlanks^Spruce, 4  = Wood^Spruce, *
+Stick, 4			  = WoodPlanks^-1, 2:2, 2:3
+Torch, 4			  = Stick, 1:2 | Coal^-1, 1:1
+TrappedChest		  = TripwireHook, * | Chest, *
+CraftingTable		  = WoodPlanks^-1, 1:1, 1:2, 2:1, 2:2
+
+
+
+
+
+#******************************************************#
+# Blocks
+#
+#Andesite, 2             = Diorite, * | Cobblestone, *
+#BoneBlock               = BoneMeal, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#BookShelf               = Planks^-1, 1:1, 2:1, 3:1, 1:3, 2:3, 3:3 | Book, 1:2, 2:2, 3:2
+#BrickBlock              = Brick,     1:1, 1:2, 2:1, 2:2
+#ChiseledQuartzBlock     = QuartzSlab, 1:1, 1:2
+#ChiseledRedSandstone    = RedSandstoneSlab, 1:1, 1:2
+#ChiseledStoneBrick      = StoneBrickSlab, 1:1, 1:2
+#ClayBlock               = Clay,      1:1, 1:2, 2:1, 2:2
+#CoalBlock               = Coal, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#CoarsedDirt, 4          = Dirt, 1:1, 2:2 | Gravel, 1:2, 2:1
+#CoarsedDirt, 4          = Gravel, 1:1, 2:2 | Dirt, 1:2, 2:1
+#DarkPrismarine          = PrismarineShard, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | Inksac, 2:2
+#DiamondBlock            = Diamond,     1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#Diorite, 2              = Cobblestone, * | NetherQuartz, *
+#EmeraldBlock            = Emerald,     1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#EndstoneBrick, 4        = Endstone, 1:1, 1:2, 2:1, 2:2
+#Glowstone               = GlowstoneDust, 1:1, 1:2, 2:1, 2:2
+#GoldBlock               = GoldIngot,   1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#Granite, 2              = Diorite, * | NetherQuartz, *
+#HayBale                 = Wheat, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#IronBlock               = IronIngot,   1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#JackOLantern            = Pumpkin, 1:1 | Torch, 1:2
+#LapisBlock              = LapisLazuli, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#Leather                 = RabbitHide, 1:1, 1:2, 2:1, 2:2
+#MossyCobblestone        = Cobblestone, * | Vines, *
+#MossyStoneBrick         = Stonebrick, * | Vines, *
+#NetherBrickBlock        = NetherBrick, 1:1, 1:2, 2:1, 2:2
+#NetherWartBlock         = NetherWart, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#OrnamentSandstone       = SandstoneSlab, 1:1, 1:2
+#PillarQuartzBlock, 2    = QuartzBlock, 1:1, 1:2
+#PolishedAndesite, 4     = Andesite,  1:1, 1:2, 2:1, 2:2
+#PolishedDiorite, 4      = Diorite,   1:1, 1:2, 2:1, 2:2
+#PolishedGranite, 4      = Granite,   1:1, 1:2, 2:1, 2:2
+#Prismarine              = PrismarineShard, 1:1, 1:2, 2:1, 2:2
+#PrismarineBricks        = PrismarineShard, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#PurpurBlock, 4          = PoppedChorusFruit, 1:1, 1:2, 2:1, 2:2
+#PurpurPillar, 1         = PurpurSlab, 1:1, 1:2
+#QuartzBlock             = NetherQuartz, 1:1, 1:2, 2:1, 2:2
+#RedNetherBrick          = NetherBrick, 1:1, 2:2 | NetherWart, 1:2, 2:1
+#RedNetherBrick          = NetherWart, 1:1, 2:2 | NetherBrick, 1:2, 2:1
+#RedSandstone            = RedSand, 1:1, 1:2, 2:1, 2:2
+#RedstoneBlock           = RedstoneDust,    1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#Sandstone               = Sand,      1:1, 1:2, 2:1, 2:2
+#SeaLantern              = PrismarineShard, 1:1, 1:3, 3:1, 3:3 | PrismarineCrystals, 1:2, 2:1, 2:2, 2:3, 3:2
+#SlimeBlock              = Slimeball, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#SmoothRedSandstone, 4   = RedSandstone, 1:1, 1:2, 2:1, 2:2
+#SmoothSandstone, 4      = Sandstone, 1:1, 1:2, 2:1, 2:2
+#SnowBlock               = SnowBall,  1:1, 1:2, 2:1, 2:2
+#StoneBrick, 4           = Stone,     1:1, 1:2, 2:1, 2:2
+#TNT                     = Gunpowder, 1:1, 3:1, 2:2, 1:3, 3:3 | Sand, 2:1, 1:2, 3:2, 2:3
+#Wool                    = String,    1:1, 1:2, 2:1, 2:2
+#
+## Slabs:
+#AcaciaWoodSlab,   6 = AcaciaPlanks,      1:1, 2:1, 3:1
+#BirchWoodSlab,    6 = BirchPlanks,       1:1, 2:1, 3:1
+#BrickSlab,        6 = BrickBlock,        1:1, 2:1, 3:1
+#CobblestoneSlab,  6 = Cobblestone,       1:1, 2:1, 3:1
+#DarkOakWoodSlab,  6 = DarkOakPlanks,     1:1, 2:1, 3:1
+#JungleWoodSlab,   6 = JunglePlanks,      1:1, 2:1, 3:1
+#NetherBrickSlab,  6 = NetherBrickBlock,  1:1, 2:1, 3:1
+#OakWoodSlab,      6 = OakPlanks,         1:1, 2:1, 3:1
+#PurpurSlab,       6 = PurpurBlock,       1:1, 2:1, 3:1
+#Quartzslab,       6 = QuartzBlock,       1:1, 2:1, 3:1
+#RedSandstoneSlab, 6 = RedSandstone^-1,   1:1, 2:1, 3:1
+#SandstoneSlab,    6 = OrnamentSandstone, 1:1, 2:1, 3:1
+#SandstoneSlab,    6 = Sandstone,         1:1, 2:1, 3:1
+#SandstoneSlab,    6 = SmoothSandstone,   1:1, 2:1, 3:1
+#SnowLayer,        6 = SnowBlock,         1:1, 2:1, 3:1
+#SpruceWoodSlab,   6 = SprucePlanks,      1:1, 2:1, 3:1
+#StonebrickSlab,   6 = StoneBrick,        1:1, 2:1, 3:1
+#StoneSlab,        6 = Stone,             1:1, 2:1, 3:1
+#
+## Stairs:
+#AcaciaWoodStairs, 4   = AcaciaPlanks,      1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#AcaciaWoodStairs, 4   = AcaciaPlanks,      3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#BirchWoodStairs, 4    = BirchPlanks,       1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#BirchWoodStairs, 4    = BirchPlanks,       3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#BrickStairs, 4        = BrickBlock,        1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#BrickStairs, 4        = BrickBlock,        3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#cobblestoneStairs, 4  = Cobblestone,       1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#cobblestoneStairs, 4  = Cobblestone,       3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#DarkOakWoodStairs, 4  = DarkOakPlanks,     1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#DarkOakWoodStairs, 4  = DarkOakPlanks,     3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#JungleWoodStairs, 4   = JunglePlanks,      1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#JungleWoodStairs, 4   = JunglePlanks,      3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#NetherBrickStairs, 4  = NetherBrickBlock,  1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#NetherBrickStairs, 4  = NetherBrickBlock,  3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#PurpurStairs, 4       = PurpurBlock,       1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#PurpurStairs, 4       = PurpurBlock,       3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#QuartzStairs, 4       = QuartzBlock,       1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#QuartzStairs, 4       = QuartzBlock,       3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#RedSandstoneStairs, 4 = RedSandstone^-1,   1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#RedSandstoneStairs, 4 = RedSandstone^-1,   3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#SandstoneStairs, 4    = OrnamentSandstone, 1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#SandstoneStairs, 4    = OrnamentSandstone, 3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#SandstoneStairs, 4    = Sandstone,         1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#SandstoneStairs, 4    = Sandstone,         3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#SandstoneStairs, 4    = SmoothSandstone,   1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#SandstoneStairs, 4    = SmoothSandstone,   3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#SpruceWoodStairs, 4   = SprucePlanks,      1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#SpruceWoodStairs, 4   = SprucePlanks,      3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#StoneBrickStairs, 4   = StoneBrick,        1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#StoneBrickStairs, 4   = StoneBrick,        3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#WoodStairs, 4         = OakPlanks,         1:1, 1:2, 2:2, 1:3, 2:3, 3:3
+#WoodStairs, 4         = OakPlanks,         3:1, 2:2, 3:2, 1:3, 2:3, 3:3
+#
+#
+#
+##******************************************************#
+## Tools
+##
+#
+## Axes:
+#DiamondAxe = Stick, 2:2, 2:3 | Diamond,     2:1, 1:1, 1:2
+#DiamondAxe = Stick, 2:2, 2:3 | Diamond,     2:1, 3:1, 3:2
+#GoldenAxe  = Stick, 2:2, 2:3 | GoldIngot,   2:1, 1:1, 1:2
+#GoldenAxe  = Stick, 2:2, 2:3 | GoldIngot,   2:1, 3:1, 3:2
+#IronAxe    = Stick, 2:2, 2:3 | IronIngot,   2:1, 1:1, 1:2
+#IronAxe    = Stick, 2:2, 2:3 | IronIngot,   2:1, 3:1, 3:2
+#StoneAxe   = Stick, 2:2, 2:3 | Cobblestone, 2:1, 1:1, 1:2
+#StoneAxe   = Stick, 2:2, 2:3 | Cobblestone, 2:1, 3:1, 3:2
+#WoodenAxe  = Stick, 2:2, 2:3 | Planks^-1,   2:1, 1:1, 1:2
+#WoodenAxe  = Stick, 2:2, 2:3 | Planks^-1,   2:1, 3:1, 3:2
+#
+## Pickaxes:
+#DiamondPickaxe = Stick, 2:2, 2:3 | Diamond,     1:1, 2:1, 3:1
+#GoldenPickaxe  = Stick, 2:2, 2:3 | GoldIngot,   1:1, 2:1, 3:1
+#IronPickaxe    = Stick, 2:2, 2:3 | IronIngot,   1:1, 2:1, 3:1
+#StonePickaxe   = Stick, 2:2, 2:3 | Cobblestone, 1:1, 2:1, 3:1
+#WoodenPickaxe  = Stick, 2:2, 2:3 | Planks^-1,   1:1, 2:1, 3:1
+#
+## Shovels:
+#DiamondShovel = Stick, 2:2, 2:3 | Diamond,     2:1
+#GoldenShovel  = Stick, 2:2, 2:3 | GoldIngot,   2:1
+#IronShovel    = Stick, 2:2, 2:3 | IronIngot,   2:1
+#StoneShovel   = Stick, 2:2, 2:3 | Cobblestone, 2:1
+#WoodenShovel  = Stick, 2:2, 2:3 | Planks^-1,   2:1
+#
+## Hoes:
+#DiamondHoe = Stick, 2:2, 2:3 | Diamond,     2:1, *:1
+#GoldenHoe  = Stick, 2:2, 2:3 | GoldIngot,   2:1, *:1
+#IronHoe    = Stick, 2:2, 2:3 | IronIngot,   2:1, *:1
+#StoneHoe   = Stick, 2:2, 2:3 | Cobblestone, 2:1, *:1
+#WoodenHoe  = Stick, 2:2, 2:3 | Planks^-1,   2:1, *:1
+#
+#Bucket         = IronIngot, 1:1, 2:2, 3:1
+#Compass        = IronIngot, 2:1, 1:2, 3:2, 2:3 | RedstoneDust, 2:2
+#EmptyMap       = Paper,     1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | Compass, 2:2
+#FireCharge, 3  = BlazePowder, *   | Coal, *    | Gunpowder, *
+#FishingRod     = Stick, 1:3, 2:2, 3:1          | String, 3:2, 3:3
+#FishingRod     = Stick, 3:3, 2:2, 1:1          | String, 1:2, 1:3
+#Lead, 2        = String, 1:1, 1:2, 2:1, 3:3    | Slimeball, 2:2
+#Lighter        = IronIngot, *                  | Flint, *
+#Shears         = IronIngot, 1:1, 2:2
+#Shears         = IronIngot, 2:1, 1:2
+#Watch          = GoldIngot, 2:1, 1:2, 3:2, 2:3 | RedstoneDust, 2:2
+#
+#
+#
+#
+#
+##******************************************************#
+## Weapons
+##
+#Arrow, 4         = Flint, 1:1             | Stick, 1:2       | Feather, 1:3
+#Bow              = Stick, 2:1, 1:2, 2:3   | String, 3:1, 3:2, 3:3
+#Bow              = Stick, 2:1, 3:2, 2:3   | String, 1:1, 1:2, 1:3
+#DiamondSword     = Stick, 2:3 | Diamond,     2:1, 2:2
+#GoldenSword      = Stick, 2:3 | GoldIngot,   2:1, 2:2
+#IronSword        = Stick, 2:3 | IronIngot,   2:1, 2:2
+#SpectralArrow, 2 = Arrow, 2:2 | GlowstoneDust, 1:2, 2:1, 2:3, 3:2
+#StoneSword       = Stick, 2:3 | Cobblestone, 2:1, 2:2
+#WoodenSword      = Stick, 2:3 | Planks^-1,   2:1, 2:2
+#
+#
+#
+#
+#
+#
+##******************************************************#
+## Armor
+##
+#
+## Helmets:
+#DiamondHelmet    = Diamond,   1:1, 2:1, 3:1, 1:2, 3:2
+#GoldenHelmet     = GoldIngot, 1:1, 2:1, 3:1, 1:2, 3:2
+#IronHelmet       = IronIngot, 1:1, 2:1, 3:1, 1:2, 3:2
+#LeatherHelmet    = Leather,   1:1, 2:1, 3:1, 1:2, 3:2
+#
+## Chestplates:
+#DiamondChestplate   = Diamond,   1:1, 3:1, 1:2, 2:2, 3:2, 1:3, 2:3, 3:3
+#GoldenChestplate    = GoldIngot, 1:1, 3:1, 1:2, 2:2, 3:2, 1:3, 2:3, 3:3
+#IronChestplate      = IronIngot, 1:1, 3:1, 1:2, 2:2, 3:2, 1:3, 2:3, 3:3
+#LeatherChestplate   = Leather,   1:1, 3:1, 1:2, 2:2, 3:2, 1:3, 2:3, 3:3
+#
+## Leggings:
+#DiamondLeggings   = Diamond,   1:1, 2:1, 3:1, 1:2, 3:2, 1:3, 3:3
+#GoldenLeggings    = GoldIngot, 1:1, 2:1, 3:1, 1:2, 3:2, 1:3, 3:3
+#IronLeggings      = IronIngot, 1:1, 2:1, 3:1, 1:2, 3:2, 1:3, 3:3
+#LeatherPants      = Leather,   1:1, 2:1, 3:1, 1:2, 3:2, 1:3, 3:3
+#
+## Boots:
+#DiamondBoots     = Diamond,   1:1, 3:1, 1:2, 3:2
+#GoldenBoots      = GoldIngot, 1:1, 3:1, 1:2, 3:2
+#IronBoots        = IronIngot, 1:1, 3:1, 1:2, 3:2
+#LeatherBoots     = Leather,   1:1, 3:1, 1:2, 3:2
+#
+## Shield:
+#Shield = IronIngot, 2:1 | Planks^-1, 1:1, 3:1, 1:2, 2:2, 3:2, 2:3
+#
+#
+#
+#
+##******************************************************#
+## Transportation
+##
+#AcaciaBoat       = AcaciaPlanks, 1:1, 3:1, 1:2, 2:2, 3:2
+#ActivatorRail, 6 = IronIngot, 1:1, 1:2, 1:3, 3:1, 3:2, 3:3 | Stick, 2:1, 2:3 | RedstoneTorchon, 2:2
+#BirchBoat        = BirchPlanks, 1:1, 3:1, 1:2, 2:2, 3:2
+#CarrotOnAStick   = FishingRod, 1:2 | Carrot, 2:3
+#DarkOakBoat      = DarkOakPlanks, 1:1, 3:1, 1:2, 2:2, 3:2
+#DetectorRail, 6  = IronIngot, 1:1, 3:1, 1:2, 3:2, 1:3, 3:3 | StonePlate, 2:2 | RedstoneDust, 2:3
+#hopperminecart   = Minecart, * | Hopper, *
+#JungleBoat       = JunglePlanks, 1:1, 3:1, 1:2, 2:2, 3:2
+#Minecart         = IronIngot, 1:1, 3:1, 1:2, 2:2, 3:2
+#OakBoat          = OakPlanks, 1:1, 3:1, 1:2, 2:2, 3:2
+#PoweredMinecart  = Minecart, * | Furnace, *
+#PoweredRail, 6   = GoldIngot, 1:1, 3:1, 1:2, 3:2, 1:3, 3:3 | Stick, 2:2      | RedstoneDust, 2:3
+#Rails, 16        = IronIngot, 1:1, 3:1, 1:2, 3:2, 1:3, 3:3 | Stick, 2:2
+#SpruceBoat       = SprucePlanks, 1:1, 3:1, 1:2, 2:2, 3:2
+#StorageMinecart  = Minecart, * | Chest, *
+#TNTMinecart      = Minecart, * | TNT, *
+#
+#
+#
+#
+##******************************************************#
+## Mechanisms
+##
+#AcaciaDoor, 3    = AcaciaPlanks,  1:1, 1:2, 1:3, 2:1, 2:2, 2:3
+#BirchDoor, 3     = BirchPlanks,   1:1, 1:2, 1:3, 2:1, 2:2, 2:3
+#Comparator       = RedstoneTorchOn, 2:1, 1:2, 3:2   | NetherQuartz, 2:2 | Stone, 1:3, 2:3, 3:3
+#DarkOakDoor, 3   = DarkOakPlanks, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3
+#DaylightSensor   = Glass, 1:1, 2:1, 3:1             | NetherQuartz, 1:2, 2:2, 3:2 | WoodenSlab^-1, 1:3, 2:3, 3:3
+#Dispenser        = Cobblestone, 1:1, 1:2, 1:3, 2:1, 3:1, 3:2, 3:3      | RedstoneDust, 2:3  | Bow, 2:2
+#Dropper          = Cobblestone, 1:1, 2:1, 3:1, 1:2, 1:3, 3:2, 3:3 | RedstoneDust, 2:3
+#heavyweightedpressureplate = IronIngot, 1:1, 2:1
+#Hopper           = IronIngot, 1:1, 3:1, 1:2, 3:2, 2:3 | Chest, 2:2
+#IronDoor, 3      = IronIngot, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3
+#IronTrapDoor     = IronIngot, 1:1, 1:2, 2:1, 2:2
+#Jukebox          = Planks^-1,   1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | Diamond,      2:2
+#JungleDoor, 3    = JunglePlanks,  1:1, 1:2, 1:3, 2:1, 2:2, 2:3
+#Lever            = Cobblestone, 1:2  | Stick, 1:1
+#lightweightedpressureplate = GoldIngot, 1:1, 2:1
+#NoteBlock        = Planks^-1,   1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | RedstoneDust, 2:2
+#Piston           = Planks^-1, 1:1, 2:1, 3:1 | RedstoneDust, 2:3 | Cobblestone, 1:2, 3:2, 1:3, 3:3 | IronIngot, 2:2
+#RedstoneLamp     = RedstoneDust, 2:1, 1:2, 3:2, 2:3 | Glowstone, 2:2
+#RedstoneTorchOn  = Stick, 1:2        | RedstoneDust, 1:1
+#Repeater         = Stone,  1:2, 2:2, 3:2            | RedstoneTorchOn, 1:1, 3:1 | RedstoneDust, 2:1
+#PurpleShulkerBox = ShulkerShell, 2:1, 2:3 | Chest, 2:2
+#SpruceDoor, 3    = SprucePlanks,  1:1, 1:2, 1:3, 2:1, 2:2, 2:3
+#StickyPiston     = Piston, * | SlimeBall, *
+#StoneButton      = Stone,     1:1
+#StonePlate       = Stone,     1:1, 2:1
+#TrapDoor, 2      = Planks^-1, 1:1, 2:1, 3:1, 1:2, 2:2, 3:2
+#TripwireHook, 2  = Planks^-1, 2:3 | stick, 2:2 | IronIngot, 2:1
+#WoodenButton     = Planks^-1, 1:1
+#WoodenDoor, 3    = OakPlanks,     1:1, 1:2, 1:3, 2:1, 2:2, 2:3
+#WoodPlate        = Planks^-1, 1:1, 2:1
+#
+#
+#
+#
+#
+##******************************************************#
+## Food
+##
+#Bowl, 4              = Planks^-1, 1:1, 2:2, 3:1
+#Bread                = Wheat, 1:1, 2:1, 3:1
+#Cake                 = MilkBucket, 1:1, 2:1, 3:1 | Sugar, 1:2, 3:2 | Egg, 2:2 | Wheat, 1:3, 2:3, 3:3
+#Cookie, 8            = Wheat, *, * | CocoaBeans, *
+#EnchantedGoldenApple = RedApple, 2:2 | GoldBlock, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3
+#GoldenApple          = RedApple, 2:2 | GoldIngot, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3
+#MelonBlock           = MelonSlice, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#MelonSeeds           = MelonSlice, *
+#MushroomStew         = Bowl, * | BrownMushroom, * | RedMushroom, *
+#BeetrootSoup         = Bowl, 2:3  | Beetroot, 1:1, 1:2, 2:1, 2:2, 3:1, 3:2
+#PumpkinPie           = Pumpkin, * | Sugar, * | egg, *
+#PumpkinSeeds, 4      = Pumpkin, *
+#RabbitStew           = Cooked Rabbit, 2:1 | Carrot, 1:2 | BakedPotato, 2:2 | BrownMushroom, 3:2 | Bowl, 2:3
+#RabbitStew           = Cooked Rabbit, 2:1 | Carrot, 1:2 | BakedPotato, 2:2 | RedMushroom, 3:2 | Bowl, 2:3
+#Sugar                = Sugarcane, *
+#Wheat, 9             = Haybale, *
+#
+#
+#
+#
+#
+##******************************************************#
+## Miscellaneous
+##
+#
+## Minerals:
+#Clay, 4         = ClayBlock, *
+#Coal, 9         = CoalBlock, *
+#Diamond, 9      = DiamondBlock, *
+#Emerald, 9      = EmeraldBlock, *
+#GoldIngot, 9    = GoldBlock, *
+#IronIngot       = IronNugget, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#IronIngot, 9    = IronBlock, *
+#LapisLazuli, 9  = LapisBlock, *
+#RedstoneDust, 9 = RedstoneBlock, *
+#SlimeBall, 9    = SlimeBlock, *
+#
+#AcaciaFence, 3           = AcaciaPlanks, 1:1, 1:2, 3:1, 3:2 | Stick, 2:1, 2:2
+#AcaciaFenceGate          = Stick, 1:1, 1:2, 3:1, 3:2 | AcaciaPlanks, 2:1, 2:2
+#Anvil                    = IronBlock, 1:1, 2:1, 3:1 | IronIngot, 2:2, 1:3, 2:3, 3:3
+#ArmorStand               = Stick, 1:1, 1:3, 2:1, 2:2, 3:1, 3:3 | StoneSlab, 2:3
+#Beacon                   = Glass, 1:1, 1:2, 2:1, 3:1, 3:2 | Obsidian, 1:3, 2:3, 3:3 | NetherStar, 2:2
+#BirchFence, 3            = BirchPlanks, 1:1, 1:2, 3:1, 3:2 | Stick, 2:1, 2:2
+#BirchFenceGate           = Stick, 1:1, 1:2, 3:1, 3:2 | BirchPlanks, 2:1, 2:2
+#Bookandquill             = Book, * | feather, * | inksac, *
+#Book                     = Paper, *, *, * | leather, *
+#Cobblestonewall, 6       = cobblestone, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#DarkOakFence, 3          = DarkOakPlanks, 1:1, 1:2, 3:1, 3:2 | Stick, 2:1, 2:2
+#DarkOakFenceGate         = Stick, 1:1, 1:2, 3:1, 3:2  | DarkOakPlanks, 2:1, 2:2
+#EndCrystal               = Glass, 1:1, 1:2, 1:3, 2:1, 3:1, 3:2, 3:3 | EyeOfEnder, 2:2 | GhastTear, 2:3
+#EndRod, 4                = BlazeRod, 1:1 | PoppedChorusFruit, 1:2
+#EyeOfEnder               = EnderPearl, * | BlazePowder, *
+#Fence, 3                 = OakPlanks, 1:1, 1:2, 3:1, 3:2 | Stick, 2:1, 2:2
+#FenceGate                = Stick, 1:1, 1:2, 3:1, 3:2 | OakPlanks, 2:1, 2:2
+#FlowerPot                = Brick, 1:2, 2:3, 3:2
+#GlassPane, 16            = Glass, 1:1, 2:1, 3:1, 1:2, 2:2, 3:2
+#GoldIngot                = GoldNugget, 1:1, 1:2, 1:3, 2:1, 2:2, 2:3, 3:1, 3:2, 3:3
+#IronBars, 16             = IronIngot, 1:1, 2:1, 3:1, 1:2, 2:2, 3:2
+#IronNugget, 9            = IronIngot, *
+#ItemFrame                = Stick, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | Leather, 2:2
+#JungleFence, 3           = JunglePlanks, 1:1, 1:2, 3:1, 3:2 | Stick, 2:1, 2:2
+#JungleFenceGate          = Stick, 1:1, 1:2, 3:1, 3:2 | JunglePlanks, 2:1, 2:2
+#Ladder, 3                = Stick, 1:1, 3:1, 1:2, 2:2, 3:2, 1:3, 3:3
+#mossycobblestonewall, 6  = mossycobblestone, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#NetherBrickFence, 6      = NetherBrickBlock, 1:1, 2:1, 3:1, 1:2, 2:2, 3:2
+#Painting                 = Stick, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | Wool^-1, 2:2
+#Paper, 3                 = Sugarcane, 1:1, 2:1, 3:1
+#Sign, 3                  = Planks^-1, 1:1, 2:1, 3:1, 1:2, 2:2, 3:2 | Stick, 2:3
+#SpruceFence, 3           = SprucePlanks, 1:1, 1:2, 3:1, 3:2 | Stick, 2:1, 2:2
+#SpruceFenceGate          = Stick, 1:1, 1:2, 3:1, 3:2 | SprucePlanks, 2:1, 2:2
+#
+## These are just the basic ones, you can add various shapes and stuff to each of them
+## ToDo: Add the various shapes (saved in NBT-Tags, not in meta)
+## Banners:
+#
+#BlackBanner         = Stick, 2:3 | BlackWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#BlueBanner          = Stick, 2:3 | BlueWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#BrownBanner         = Stick, 2:3 | BrownWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#CyanBanner          = Stick, 2:3 | CyanWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#GrayBanner          = Stick, 2:3 | GrayWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#GreenBanner         = Stick, 2:3 | GreenWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#LightBlueBanner     = Stick, 2:3 | LightBlueWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#LightGrayBanner     = Stick, 2:3 | LightGrayWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#LimeBanner          = Stick, 2:3 | LimeWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#MagentaBanner       = Stick, 2:3 | MagentaWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#OrangeBanner        = Stick, 2:3 | OrangeWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#PinkBanner          = Stick, 2:3 | PinkWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#PurpleBanner        = Stick, 2:3 | PurpleWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#RedBanner           = Stick, 2:3 | RedWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#WhiteBanner         = Stick, 2:3 | WhiteWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#YellowBanner        = Stick, 2:3 | YellowWool, 1:1, 1:2, 2:1, 2:2, 2:1, 2:2
+#
+#
+#
+#
+#
+##******************************************************#
+## Dyes
+##
+#
+#RedDye, 1     = Beetroot, *
+#RedDye, 2     = Rose, *
+#WhiteDye, 3   = Bone, *
+#YellowDye, 2  = Dandelion, *
+#
+## Color mixing, duals:
+#CyanDye, 2    = GreenDye, *  | BlueDye, *
+#GrayDye, 2    = BlackDye, *  | WhiteDye, *
+#LimeDye, 2    = GreenDye, *  | WhiteDye, *
+#LtBlueDye, 2  = BlueDye, *   | WhiteDye, *
+#LtGrayDye, 2  = GrayDye, *   | WhiteDye, *
+#MagentaDye, 2 = PurpleDye, * | PinkDye, *
+#OrangeDye, 2  = YellowDye, * | RedDye, *
+#PinkDye, 2    = RedDye, *    | WhiteDye, *
+#PurpleDye, 2  = RedDye, *    | BlueDye, *
+#
+## triplets:
+#LtGrayDye, 3  = BlackDye, *  | WhiteDye, *, *
+#MagentaDye, 3 = BlueDye, *   | PinkDye, *  | RedDye, *
+#
+## quads:
+#MagentaDye, 4 = BlueDye, *   | WhiteDye, * | RedDye, *, *
+#
+#
+#
+#
+##******************************************************#
+## Concrete Powder:
+##
+#White_Concrete_Powder         = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | BoneMeal, 2:2
+#Orange_Concrete_Powder        = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | OrangeDye, 2:2
+#Magenta_Concrete_Powder       = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | MagentaDye, 2:2
+#Light_Blue_Concrete_Powder    = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | LightBlueDye, 2:2
+#Yellow_Concrete_Powder        = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | YellowDye, 2:2
+#Lime_Concrete_Powder          = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | LimeDye, 2:2
+#Pink_Concrete_Powder          = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | PinkDye, 2:2
+#Gray_Concrete_Powder          = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | GrayDye, 2:2
+#Light_Gray_Concrete_Powder    = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | LightGrayDye, 2:2
+#Cyan_Concrete_Powder          = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | CyanDye, 2:2
+#Blue_Concrete_Powder          = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | BlueDye, 2:2
+#Brown_Concrete_Powder         = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | BrownDye, 2:2
+#Green_Concrete_Powder         = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | GreenDye, 2:2
+#Red_Concrete_Powder           = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | RedDye, 2:2
+#Black_Concrete_Powder         = Sand, 1:1, 3:1, 1:3, 3:3 | Gravel, 2:1, 1:2, 3:2, 2:3 | BlackDye, 2:2
+#
+##******************************************************#
+## Colored shulker boxes:
+##
+#BlackShulkerBox     = BlackShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = BlueShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = BrownShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = CyanShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = GrayShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = GreenShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = LightBlueShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = LightGrayShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = LimeShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = MagentaShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = OrangeShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = PinkShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = PurpleShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = RedShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = WhiteShulkerBox, * | BlackDye, *
+#BlackShulkerBox     = YellowShulkerBox, * | BlackDye, *
+#BlueShulkerBox      = BlackShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = BlueShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = BrownShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = CyanShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = GrayShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = GreenShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = LightBlueShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = LightGrayShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = LimeShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = MagentaShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = OrangeShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = PinkShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = PurpleShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = RedShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = WhiteShulkerBox, * | BlueDye, *
+#BlueShulkerBox      = YellowShulkerBox, * | BlueDye, *
+#BrownShulkerBox     = BlackShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = BlueShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = BrownShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = CyanShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = GrayShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = GreenShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = LightBlueShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = LightGrayShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = LimeShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = MagentaShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = OrangeShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = PinkShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = PurpleShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = RedShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = WhiteShulkerBox, * | BrownDye, *
+#BrownShulkerBox     = YellowShulkerBox, * | BrownDye, *
+#CyanShulkerBox      = BlackShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = BlueShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = BrownShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = CyanShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = GrayShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = GreenShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = LightBlueShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = LightGrayShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = LimeShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = MagentaShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = OrangeShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = PinkShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = PurpleShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = RedShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = WhiteShulkerBox, * | CyanDye, *
+#CyanShulkerBox      = YellowShulkerBox, * | CyanDye, *
+#GrayShulkerBox      = BlackShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = BlueShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = BrownShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = CyanShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = GrayShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = GreenShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = LightBlueShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = LightGrayShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = LimeShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = MagentaShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = OrangeShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = PinkShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = PurpleShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = RedShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = WhiteShulkerBox, * | GrayDye, *
+#GrayShulkerBox      = YellowShulkerBox, * | GrayDye, *
+#GreenShulkerBox     = BlackShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = BlueShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = BrownShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = CyanShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = GrayShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = GreenShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = LightBlueShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = LightGrayShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = LimeShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = MagentaShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = OrangeShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = PinkShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = PurpleShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = RedShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = WhiteShulkerBox, * | GreenDye, *
+#GreenShulkerBox     = YellowShulkerBox, * | GreenDye, *
+#LightBlueShulkerBox = BlackShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = BlueShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = BrownShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = CyanShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = GrayShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = GreenShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = LightBlueShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = LightGrayShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = LimeShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = MagentaShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = OrangeShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = PinkShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = PurpleShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = RedShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = WhiteShulkerBox, * | LightBlueDye, *
+#LightBlueShulkerBox = YellowShulkerBox, * | LightBlueDye, *
+#LightGrayShulkerBox = BlackShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = BlueShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = BrownShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = CyanShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = GrayShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = GreenShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = LightBlueShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = LightGrayShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = LimeShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = MagentaShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = OrangeShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = PinkShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = PurpleShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = RedShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = WhiteShulkerBox, * | LightGrayDye, *
+#LightGrayShulkerBox = YellowShulkerBox, * | LightGrayDye, *
+#LimeShulkerBox      = BlackShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = BlueShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = BrownShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = CyanShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = GrayShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = GreenShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = LightBlueShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = LightGrayShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = LimeShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = MagentaShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = OrangeShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = PinkShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = PurpleShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = RedShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = WhiteShulkerBox, * | LimeDye, *
+#LimeShulkerBox      = YellowShulkerBox, * | LimeDye, *
+#MagentaShulkerBox    = BlackShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = BlueShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = BrownShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = CyanShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = GrayShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = GreenShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = LightBlueShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = LightGrayShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = LimeShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = MagentaShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = OrangeShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = PinkShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = PurpleShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = RedShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = WhiteShulkerBox, * | MagentaDye, *
+#MagentaShulkerBox    = YellowShulkerBox, * | MagentaDye, *
+#OrangeShulkerBox     = BlackShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = BlueShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = BrownShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = CyanShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = GrayShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = GreenShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = LightBlueShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = LightGrayShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = LimeShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = MagentaShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = OrangeShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = PinkShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = PurpleShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = RedShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = WhiteShulkerBox, * | OrangeDye, *
+#OrangeShulkerBox     = YellowShulkerBox, * | OrangeDye, *
+#PinkShulkerBox       = BlackShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = BlueShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = BrownShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = CyanShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = GrayShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = GreenShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = LightBlueShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = LightGrayShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = LimeShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = MagentaShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = OrangeShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = PinkShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = PurpleShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = RedShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = WhiteShulkerBox, * | PinkDye, *
+#PinkShulkerBox       = YellowShulkerBox, * | PinkDye, *
+#PurpleShulkerBox     = BlackShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = BlueShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = BrownShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = CyanShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = GrayShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = GreenShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = LightBlueShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = LightGrayShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = LimeShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = MagentaShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = OrangeShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = PinkShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = PurpleShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = RedShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = WhiteShulkerBox, * | PurpleDye, *
+#PurpleShulkerBox     = YellowShulkerBox, * | PurpleDye, *
+#RedShulkerBox        = BlackShulkerBox, * | RedDye, *
+#RedShulkerBox        = BlueShulkerBox, * | RedDye, *
+#RedShulkerBox        = BrownShulkerBox, * | RedDye, *
+#RedShulkerBox        = CyanShulkerBox, * | RedDye, *
+#RedShulkerBox        = GrayShulkerBox, * | RedDye, *
+#RedShulkerBox        = GreenShulkerBox, * | RedDye, *
+#RedShulkerBox        = LightBlueShulkerBox, * | RedDye, *
+#RedShulkerBox        = LightGrayShulkerBox, * | RedDye, *
+#RedShulkerBox        = LimeShulkerBox, * | RedDye, *
+#RedShulkerBox        = MagentaShulkerBox, * | RedDye, *
+#RedShulkerBox        = OrangeShulkerBox, * | RedDye, *
+#RedShulkerBox        = PinkShulkerBox, * | RedDye, *
+#RedShulkerBox        = PurpleShulkerBox, * | RedDye, *
+#RedShulkerBox        = RedShulkerBox, * | RedDye, *
+#RedShulkerBox        = WhiteShulkerBox, * | RedDye, *
+#RedShulkerBox        = YellowShulkerBox, * | RedDye, *
+#WhiteShulkerBox      = BlackShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = BlueShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = BrownShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = CyanShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = GrayShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = GreenShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = LightBlueShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = LightGrayShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = LimeShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = MagentaShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = OrangeShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = PinkShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = PurpleShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = RedShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = WhiteShulkerBox, * | BoneMeal, *
+#WhiteShulkerBox      = YellowShulkerBox, * | BoneMeal, *
+#YellowShulkerBox     = BlackShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = BlueShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = BrownShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = CyanShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = GrayShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = GreenShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = LightBlueShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = LightGrayShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = LimeShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = MagentaShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = OrangeShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = PinkShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = PurpleShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = RedShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = WhiteShulkerBox, * | YellowDye, *
+#YellowShulkerBox     = YellowShulkerBox, * | YellowDye, *
+#
+##******************************************************#
+## Colored wool:
+##
+#BlackWool     = WhiteWool, * | BlackDye, *
+#BlueWool      = WhiteWool, * | BlueDye, *
+#BrownWool     = WhiteWool, * | BrownDye, *
+#CyanWool      = WhiteWool, * | CyanDye, *
+#GrayWool      = WhiteWool, * | GrayDye, *
+#GreenWool     = WhiteWool, * | GreenDye, *
+#LightBlueWool = WhiteWool, * | LightBlueDye, *
+#LightGrayWool = WhiteWool, * | LightGrayDye, *
+#LimeWool      = WhiteWool, * | LimeDye, *
+#MagentaWool   = WhiteWool, * | MagentaDye, *
+#OrangeWool    = WhiteWool, * | OrangeDye, *
+#PinkWool      = WhiteWool, * | PinkDye, *
+#PurpleWool    = WhiteWool, * | PurpleDye, *
+#RedWool       = WhiteWool, * | RedDye, *
+#WhiteWool     = Wool^-1, *   | BoneMeal, *
+#YellowWool    = WhiteWool, * | YellowDye, *
+#
+#BlackCarpet, 3     = BlackWool, 1:1, 2:1
+#BlueCarpet, 3      = BlueWool, 1:1, 2:1
+#BrownCarpet, 3     = BrownWool, 1:1, 2:1
+#CyanCarpet, 3      = CyanWool, 1:1, 2:1
+#GrayCarpet, 3      = GrayWool, 1:1, 2:1
+#GreenCarpet, 3     = GreenWool, 1:1, 2:1
+#LightBlueCarpet, 3 = LightBlueWool, 1:1, 2:1
+#LightGrayCarpet, 3 = LightGrayWool, 1:1, 2:1
+#LimeCarpet, 3      = LimeWool, 1:1, 2:1
+#MagentaCarpet, 3   = MagentaWool, 1:1, 2:1
+#OrangeCarpet, 3    = OrangeWool, 1:1, 2:1
+#PinkCarpet, 3      = PinkWool, 1:1, 2:1
+#PurpleCarpet, 3    = PurpleWool, 1:1, 2:1
+#RedCarpet, 3       = RedWool, 1:1, 2:1
+#WhiteCarpet, 3     = WhiteWool, 1:1, 2:1
+#YellowCarpet, 3    = YellowWool, 1:1, 2:1
+#
+##******************************************************#
+## Stained Glass:
+##
+#BlackStainedGlass, 8     = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BlackDye, 2:2
+#BlueStainedGlass, 8      = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BlueDye, 2:2
+#BrownStainedGlass, 8     = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BrownDye, 2:2
+#CyanStainedGlass, 8      = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | CyanDye, 2:2
+#GrayStainedGlass, 8      = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | GrayDye, 2:2
+#GreenStainedGlass, 8     = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | GreenDye, 2:2
+#LightBlueStainedGlass, 8 = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | LightBlueDye, 2:2
+#LightGrayStainedGlass, 8 = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | LightGrayDye, 2:2
+#LimeStainedGlass, 8      = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | LimeDye, 2:2
+#MagentaStainedGlass, 8   = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | MagentaDye, 2:2
+#OrangeStainedGlass, 8    = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | OrangeDye, 2:2
+#PinkStainedGlass, 8      = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | PinkDye, 2:2
+#RedStainedGlass, 8       = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | RedDye, 2:2
+#VioletStainedGlass, 8    = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | VioletDye, 2:2
+#WhiteStainedGlass, 8     = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BoneMeal, 2:2
+#YellowStainedGlass, 8    = Glass, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | YellowDye, 2:2
+#
+##******************************************************#
+## Stained Glass Pane:
+##
+#BlackStainedGlassPane , 16    = BlackStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#BlueStainedGlassPane, 16      = BlueStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#BrownStainedGlassPane, 16     = BrownStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#CyanStainedGlassPane, 16      = CyanStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#GrayStainedGlassPane, 16      = GrayStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#GreenStainedGlassPane, 16     = GreenStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#LightBlueStainedGlassPane, 16 = LightBlueStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#LightGrayStainedGlassPane, 16 = LightGrayStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#LimeStainedGlassPane, 16      = LimeStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#MagentaStainedGlassPane, 16   = MagentaStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#OrangeStainedGlassPane, 16    = OrangeStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#PinkStainedGlassPane, 16      = PinkStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#RedStainedGlassPane, 16       = RedStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#VioletStainedGlassPane, 16    = VioletStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#WhiteStainedGlassPane, 16     = WhiteStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#YellowStainedGlassPane, 16    = YellowStainedGlass, 1:2, 1:3, 2:2, 2:3, 3:2, 3:3
+#
+##******************************************************#
+## Stained Clay:
+##
+#BlackStainedClay, 8     = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BlackDye, 2:2
+#BlueStainedClay, 8      = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BlueDye, 2:2
+#BrownStainedClay, 8     = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BrownDye, 2:2
+#CyanStainedClay, 8      = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | CyanDye, 2:2
+#GrayStainedClay, 8      = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | GrayDye, 2:2
+#GreenStainedClay, 8     = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | GreenDye, 2:2
+#LightBlueStainedClay, 8 = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | LightBlueDye, 2:2
+#LightGrayStainedClay, 8 = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | LightGrayDye, 2:2
+#LimeStainedClay, 8      = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | LimeDye, 2:2
+#MagentaStainedClay, 8   = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | MagentaDye, 2:2
+#OrangeStainedClay, 8    = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | OrangeDye, 2:2
+#PinkStainedClay, 8      = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | PinkDye, 2:2
+#RedStainedClay, 8       = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | RedDye, 2:2
+#VioletStainedClay, 8    = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | VioletDye, 2:2
+#WhiteStainedClay, 8     = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | BoneMeal, 2:2
+#YellowStainedClay, 8    = HardenedClay, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3 | YellowDye, 2:2
+#
+##******************************************************#
+## Enchantment & Brewing
+##
+#BlazePowder, 2     = BlazeRod, *
+#BrewingStand       = Cobblestone, 1:2, 2:2, 3:2 | BlazeRod, 2:1
+#Cauldron           = IronIngot, 1:1, 3:1, 1:2, 3:2, 1:3, 2:3, 3:3
+#EnchantmentTable   = Obsidian, 1:3, 2:3, 3:3, 2:2 | Diamond, 1:2, 3:2 | Book, 2:1
+#FermentedSpiderEye = SpiderEye, * | Sugar, * | BrownMushroom, *
+#GlassBottle, 3     = Glass, 1:1, 2:2, 3:1
+#GlisteringMelon    = MelonSlice, 2:2 | GoldNugget, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3
+#GoldenCarrot       = Carrot, 2:2 | GoldNugget, 1:1, 1:2, 1:3, 2:1, 2:3, 3:1, 3:2, 3:3
+#GoldNugget, 9      = GoldIngot, *
+#MagmaCream         = SlimeBall, * | BlazePowder, *
+#
+##******************************************************#
+## Dyed Armor 
+## Do not modify
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, *
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherHelmet = LeatherHelmet^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, *
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherChestplate = LeatherChestplate^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#
+#LeatherPants = LeatherPants^-1, * | Dye^-1, *
+#LeatherPants = LeatherPants^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherPants = LeatherPants^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherPants = LeatherPants^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherPants = LeatherPants^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherPants = LeatherPants^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherPants = LeatherPants^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherPants = LeatherPants^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, *
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#LeatherBoots = LeatherBoots^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, * | Dye^-1, *
+#
+##******************************************************#
+## Fireworks & Co.
+## (Best not to add non-vanilla items to this as it will cause internal firework data handling code to log warnings)
+#
+## Ballistic firework rockets - plain and with firework star, all with 1 - 3 gunpowder
+#FireworkRocket   = FireworkStar, * | Paper, * | Gunpowder, *
+#FireworkRocket   = FireworkStar, * | Paper, * | Gunpowder, * | Gunpowder, *
+#FireworkRocket   = FireworkStar, * | Paper, * | Gunpowder, * | Gunpowder, * | Gunpowder, *
+#FireworkRocket   = Paper, * | Gunpowder, *
+#FireworkRocket   = Paper, * | Gunpowder, * | Gunpowder, *
+#FireworkRocket   = Paper, * | Gunpowder, * | Gunpowder, * | Gunpowder, *
+#
+## Radioactive firework stars
+## Plain powder and dye
+#FireworkStar     = Gunpowder, * | Dye ^-1, *
+#
+## Powder and effect, with effect combining
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Glowdust, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Glowdust, * | Diamond, *
+#
+## Powder and shape (no shape combining possible)
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Feather, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Firecharge, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Goldnugget, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | SkeletonHead ^-1, *
+#
+## Power and shape (no shape combining possible), combined with effect
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Feather, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Feather, * | Glowdust, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Firecharge, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Firecharge, * | Glowdust, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Goldnugget, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Goldnugget, * | Glowdust, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | SkeletonHead ^-1, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | SkeletonHead ^-1, * | Glowdust, *
+#
+## Power and shape (no shape combining possible), combined with effect (with effect combining)
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Feather, * | Glowdust, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Firecharge, * | Glowdust, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | Goldnugget, * | Glowdust, * | Diamond, *
+#FireworkStar     = Gunpowder, * | Dye ^-1, * | SkeletonHead ^-1, * | Glowdust, * | Diamond, *
+#
+## Star fade colour-change
+#FireworkStar     = FireworkStar, * | Dye ^-1, *
+#FireworkStar     = FireworkStar, * | Dye ^-1, * | Dye ^-1, *
+#FireworkStar     = FireworkStar, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, *
+#FireworkStar     = FireworkStar, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, *
+#FireworkStar     = FireworkStar, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, *
+#FireworkStar     = FireworkStar, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, *
+#FireworkStar     = FireworkStar, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, *
+#FireworkStar     = FireworkStar, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, * | Dye ^-1, *
+#
+#
+#
+##******************************************************#
+## Bed different colors
+#Bed^0                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^0, 1:1, 2:1, 3:1
+#Bed^1                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^1, 1:1, 2:1, 3:1
+#Bed^2                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^2, 1:1, 2:1, 3:1
+#Bed^3                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^3, 1:1, 2:1, 3:1
+#Bed^4                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^4, 1:1, 2:1, 3:1
+#Bed^5                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^5, 1:1, 2:1, 3:1
+#Bed^6                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^6, 1:1, 2:1, 3:1
+#Bed^7                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^7, 1:1, 2:1, 3:1
+#Bed^8                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^8, 1:1, 2:1, 3:1
+#Bed^9                    = Planks^-1, 1:2, 2:2, 3:2 | Wool^9, 1:1, 2:1, 3:1
+#Bed^10                   = Planks^-1, 1:2, 2:2, 3:2 | Wool^10, 1:1, 2:1, 3:1
+#Bed^11                   = Planks^-1, 1:2, 2:2, 3:2 | Wool^11, 1:1, 2:1, 3:1
+#Bed^12                   = Planks^-1, 1:2, 2:2, 3:2 | Wool^12, 1:1, 2:1, 3:1
+#Bed^13                   = Planks^-1, 1:2, 2:2, 3:2 | Wool^13, 1:1, 2:1, 3:1
+#Bed^14                   = Planks^-1, 1:2, 2:2, 3:2 | Wool^14, 1:1, 2:1, 3:1
+#Bed^15                   = Planks^-1, 1:2, 2:2, 3:2 | Wool^15, 1:1, 2:1, 3:1

+ 185 - 0
data/furnace.txt

@@ -0,0 +1,185 @@
+#*****************#
+# Furnace Recipes #
+#*****************#
+#
+#
+#******************************************************#
+#                Basic Notation Help
+#
+# **** Item Definition ****
+# An Item is defined by an Item Type, an amount (and damage)
+# The damage is optional, and if not specified it's assumed to be 0
+#
+# Cactus Green example:
+#    351     ^   2    ( ,   1    )
+#   ItemType ^ Damage ( , Amount )
+# or simple use the item name (marked in items.ini):
+#   CactusGreen ( , 1 )
+#
+#
+# **** Recipe and result ****
+#
+# Cobble @ 200 = Stone -> Produces 1 smooth stone from 1 cobblestone in 200 ticks (10 seconds)
+#
+# Write in full:
+#    Cobble  ^   0    ,   1    @  200  =    1     ^   1    ,   1
+#   ItemType ^ Damage , Amount @ ticks = ItemType ^ Damage , Amount
+#
+#
+# **** Fuel ****
+#
+# !17:1 = 300 -> 1 Wood burns for 300 ticks (15 s)
+#
+#  !     Wood   ,   1    =  300
+# Fuel ItemType , Amount = ticks
+#
+#******************************************************#
+
+
+
+
+
+#--------------------------
+# Smelting recipes
+
+#Beef                = Steak
+#BlackTerracotta     = BlackGlazedTerracotta
+#BlueTerracotta      = BlueGlazedTerracotta
+#BrownTerracotta     = BrownGlazedTerracotta
+#Cactus              = CactusGreen
+#ChainmailBoots      = IronNugget
+#ChainmailChestplate = IronNugget
+#ChainmailHelmet     = IronNugget
+#ChainmailLeggings   = IronNugget
+#Chicken             = CookedChicken
+#ChorusFruit         = PoppedChorusFruit
+#Clay                = Brick
+#ClayBlock           = HardenedClay
+#CoalOre             = Coal
+#Cobblestone         = Stone
+#CrackedStonebrick   = Stonebrick
+#CyanTerracotta      = CyanGlazedTerracotta
+#DiamondOre          = Diamond
+#EmeraldOre          = Emerald
+#Fish                = CookedFish
+#GoldOre             = GoldIngot
+#GoldAxe             = GoldNugget
+#GoldBoots           = GoldNugget
+#GoldChestplate      = GoldNugget
+#GoldHorseArmor      = GoldNugget
+#GoldHelmet          = GoldNugget
+#GoldHoe             = GoldNugget
+#GoldPants           = GoldNugget
+#GoldPickaxe         = GoldNugget
+#GoldShovel          = GoldNugget
+#GoldSword           = GoldNugget
+#GrayTerracotta      = GrayGlazedTerracotta
+#GreenTerracotta     = GreenGlazedTerracotta
+#IronOre             = IronIngot
+#IronAxe             = IronNugget
+#IronBoots           = IronNugget
+#IronChestplate      = IronNugget
+#IronHorseArmor      = IronNugget
+#IronHelmet          = IronNugget
+#IronHoe             = IronNugget
+#IronLeggings        = IronNugget
+#IronPickaxe         = IronNugget
+#IronShovel          = IronNugget
+#IronSword           = IronNugget
+#LapisOre            = LapisLazuli
+#LightBlueTerracotta = LightBlueGlazedTerracotta
+#LightGrayTerracotta = LightGrayGlazedTerracotta
+#LimeTerracotta      = LimeGlazedTerracotta
+Wood^-1              = Coal^Charcoal
+Wood2^-1             = Coal^Charcoal
+#MagentaTerracotta   = MagentaGlazedTerracotta
+#Mutton              = CookedMutton
+#NetherQuartzOre     = NetherQuartz
+#Netherrack          = NetherBrick
+#OrangeTerracotta    = OrangeGlazedTerracotta
+#PinkTerracotta      = PinkGlazedTerracotta
+#Porkchop            = CookedPorkchop
+#Potato              = BakedPotato
+#PurpleTerracotta    = PurpleGlazedTerracotta
+#Rabbit              = CookedRabbit
+#RedTerracotta       = RedGlazedTerracotta
+#RedstoneOre         = Redstone
+#Salmon              = CookedSalmon
+#Sand                = Glass
+#StoneBrick          = CrackedStoneBricks
+#WetSponge           = Sponge
+#WhiteTerracotta     = WhiteGlazedTerracotta
+#YellowTerracotta    = YellowGlazedTerracotta
+
+
+
+
+#--------------------------
+# Fuels
+
+#! AcaciaBoat      = 400    # -> 20 sec
+#! AcaciaDoor      = 200    # -> 10 sec
+#! AcaciaFence     = 300    # -> 15 sec
+#! AcaciaFenceGate = 300    # -> 15 sec
+#! AcaciaStairs    = 300    # -> 15 sec
+#! Banner          = 300    # -> 15 sec
+#! BirchBoat       = 400    # -> 20 sec
+#! BirchDoor       = 200    # -> 10 sec
+#! BirchFence      = 300    # -> 15 sec
+#! BirchFenceGate  = 300    # -> 15 sec
+#! BirchStairs     = 300    # -> 15 sec
+#! BlazeRod        = 2400   # -> 120 sec
+#! Boat            = 400    # -> 20 sec
+#! Bookshelf       = 300    # -> 15 sec
+#! Bow             = 300    # -> 15 sec
+#! Bowl            = 100    # -> 5 sec
+#! BrownMushroomBlock = 300 # -> 15 sec
+#! Carpet          = 67     # -> 3.35 sec
+#! CharCoal        = 1600   # -> 80 sec
+#! Chest           = 300    # -> 15 sec
+! Coal^-1          = 1600   # -> 80 sec
+! BlockOfCoal      = 16000  # -> 800 sec
+#! CraftingTable   = 300    # -> 15 sec
+#! DarkOakBoat     = 400    # -> 20 sec
+#! DarkOakDoor     = 200    # -> 10 sec
+#! DarkOakFence    = 300    # -> 15 sec
+#! DarkOakFenceGate = 300   # -> 15 sec
+#! DarkOakStairs   = 300    # -> 15 sec
+#! DaylightSensor  = 300    # -> 15 sec
+#! Fence           = 300    # -> 15 sec
+#! FenceGate       = 300    # -> 15 sec
+#! FishingRod      = 300    # -> 15 sec
+#! Jukebox         = 300    # -> 15 sec
+#! JungleBoat      = 400    # -> 20 sec
+#! JungleDoor      = 200    # -> 10 sec
+#! JungleFence     = 300    # -> 15 sec
+#! JungleFenceGate = 300    # -> 15 sec
+#! JungleStairs    = 300    # -> 15 sec
+#! Ladder          = 300    # -> 15 sec
+#! Lavabucket      = 20000  # -> 1000 sec
+! Wood^-1          = 300    # -> 15 sec
+! Wood2^-1         = 300    # -> 15 sec
+#! NoteBlock       = 300    # -> 15 sec
+#! OakStairs       = 300    # -> 15 sec
+#! Planks          = 300    # -> 15 sec
+#! RedMushroomBlock = 300   # -> 15 sec
+#! Sapling         = 100    # -> 5 sec
+#! Sign            = 200    # -> 10 sec
+#! SpruceBoat      = 400    # -> 20 sec
+#! SpruceDoor      = 200    # -> 10 sec
+#! SpruceFence     = 300    # -> 15 sec
+#! SpruceFenceGate = 300    # -> 15 sec
+#! SpruceStairs    = 300    # -> 15 sec
+#! Stick           = 100    # -> 5 sec
+#! Trapdoor        = 300    # -> 15 sec
+#! TrappedChest    = 300    # -> 15 sec
+#! WoodenAxe       = 200    # -> 10 sec
+#! WoodenButton    = 100    # -> 5 sec
+#! WoodenDoor      = 200    # -> 10 sec
+#! WoodenHoe       = 200    # -> 10 sec
+#! WoodenPressurePlate = 300 # -> 15 sec
+#! WoodenPickaxe   = 200    # -> 10 sec
+#! WoodenSlab      = 150    # -> 7.5 sec
+#! WoodenShovel    = 200    # -> 10 sec
+#! WoodenSword     = 200    # -> 10 sec
+#! Wool            = 100    # -> 5 sec

+ 202 - 0
src/MineCase.Algorithm/CraftingRecipeMatcher.cs

@@ -0,0 +1,202 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Algorithm
+{
+    public class FindCraftingRecipeResult
+    {
+        public Slot Result { get; set; }
+
+        public Slot[,] AfterTake { get; set; }
+    }
+
+    public class CraftingRecipeMatcher
+    {
+        private List<CraftingRecipe> _recipes;
+
+        public CraftingRecipeMatcher(List<CraftingRecipe> recipes)
+        {
+            _recipes = recipes;
+        }
+
+        public FindCraftingRecipeResult FindRecipe(Slot[,] craftingGrid)
+        {
+            int gridLeft = 2, gridTop = 2;
+            int gridRight = 0, gridBottom = 0;
+
+            for (int x = 0; x < craftingGrid.GetUpperBound(0) + 1; x++)
+            {
+                for (int y = 0; y < craftingGrid.GetUpperBound(1) + 1; y++)
+                {
+                    if (!craftingGrid[x, y].IsEmpty)
+                    {
+                        gridRight = Math.Max(x, gridRight);
+                        gridBottom = Math.Max(y, gridBottom);
+                        gridLeft = Math.Min(x, gridLeft);
+                        gridTop = Math.Min(y, gridTop);
+                    }
+                }
+            }
+
+            var gridWidth = gridRight - gridLeft + 1;
+            var gridHeight = gridBottom - gridTop + 1;
+
+            if (gridWidth <= 0 || gridHeight <= 0) return null;
+            var newGrid = CropCraftingGrid(craftingGrid, gridLeft, gridTop, gridWidth, gridHeight);
+            var result = FindRecipeCropped(newGrid);
+            if (result != null)
+            {
+                result.AfterTake = RestoreCroppedCraftingGrid(result.AfterTake, gridLeft, gridTop, craftingGrid.GetUpperBound(0) + 1, craftingGrid.GetUpperBound(1) + 1);
+                return result;
+            }
+
+            return null;
+        }
+
+        private FindCraftingRecipeResult FindRecipeCropped(Slot[,] craftingGrid)
+        {
+            var gridWidth = craftingGrid.GetUpperBound(0) + 1;
+            var gridHeight = craftingGrid.GetUpperBound(1) + 1;
+
+            foreach (var recipe in _recipes)
+            {
+                var maxOfX = gridWidth - recipe.Width;
+                var maxOfY = gridHeight - recipe.Height;
+
+                for (int x = 0; x <= maxOfX; x++)
+                {
+                    for (int y = 0; y <= maxOfY; y++)
+                    {
+                        var result = MatchRecipe(craftingGrid, recipe, x, y);
+                        if (result != null) return result;
+                    }
+                }
+            }
+
+            return null;
+        }
+
+        private FindCraftingRecipeResult MatchRecipe(Slot[,] craftingGrid, CraftingRecipe recipe, int xOffset, int yOffset)
+        {
+            var gridWidth = craftingGrid.GetUpperBound(0) + 1;
+            var gridHeight = craftingGrid.GetUpperBound(1) + 1;
+
+            var hasMatched = new bool[gridWidth, gridHeight];
+            var after = (Slot[,])craftingGrid.Clone();
+            for (int i = 0; i < recipe.Inputs.Length; i++)
+            {
+                ref var recipeSlot = ref recipe.Inputs[i];
+
+                // Anywhere, 稍后处理
+                if (recipeSlot.X < 0 || recipeSlot.Y < 0) continue;
+
+                var x = recipeSlot.X + xOffset;
+                var y = recipeSlot.Y + yOffset;
+                ref var gridSlot = ref craftingGrid[x, y];
+                if (recipeSlot.X >= gridWidth || recipeSlot.Y >= gridHeight ||
+                    recipeSlot.Slot.BlockId != gridSlot.BlockId ||
+                    recipeSlot.Slot.ItemCount > gridSlot.ItemCount ||
+                    (recipeSlot.Slot.ItemDamage >= 0 && recipeSlot.Slot.ItemDamage != gridSlot.ItemDamage))
+                    return null;
+                hasMatched[recipeSlot.X + xOffset, recipeSlot.Y + yOffset] = true;
+                after[x, y].ItemCount -= recipeSlot.Slot.ItemCount;
+                after[x, y].MakeEmptyIfZero();
+            }
+
+            // 处理 Anywhere
+            for (int i = 0; i < recipe.Inputs.Length; i++)
+            {
+                ref var recipeSlot = ref recipe.Inputs[i];
+
+                // Anywhere, 稍后处理
+                if (recipeSlot.X >= 0 && recipeSlot.Y >= 0) continue;
+
+                int startX = 0, endX = gridWidth - 1;
+                int startY = 0, endY = gridHeight - 1;
+                if (recipeSlot.X >= 0)
+                    startX = endX = recipeSlot.X;
+                if (recipeSlot.Y >= 0)
+                    startY = endY = recipeSlot.Y;
+                bool found = false;
+                for (int x = startX; x <= endX; x++)
+                {
+                    for (int y = startY; y <= endY; y++)
+                    {
+                        if (hasMatched[x, y]) continue;
+                        ref var gridSlot = ref craftingGrid[x, y];
+                        if (gridSlot.BlockId == recipeSlot.Slot.BlockId &&
+                            (recipeSlot.Slot.ItemDamage < 0 || gridSlot.ItemDamage == recipeSlot.Slot.ItemDamage))
+                        {
+                            hasMatched[x, y] = true;
+                            found = true;
+                            after[x, y].ItemCount -= recipeSlot.Slot.ItemCount;
+                            break;
+                        }
+                    }
+
+                    if (found) break;
+                }
+
+                if (!found) return null;
+            }
+
+            // 最后检查一下
+            for (int x = 0; x < gridWidth; x++)
+            {
+                for (int y = 0; y < gridHeight; y++)
+                {
+                    if (!hasMatched[x, y] && !craftingGrid[x, y].IsEmpty)
+                        return null;
+                }
+            }
+
+            return new FindCraftingRecipeResult
+            {
+                Result = recipe.Output,
+                AfterTake = after
+            };
+        }
+
+        private static Slot[,] CropCraftingGrid(Slot[,] sourceGrid, int gridLeft, int gridTop, int gridWidth, int gridHeight)
+        {
+            var slots = new Slot[gridWidth, gridHeight];
+            for (int y = 0; y < gridHeight; y++)
+            {
+                for (int x = 0; x < gridWidth; x++)
+                {
+                    slots[x, y] = sourceGrid[x + gridLeft, y + gridTop];
+                }
+            }
+
+            NormalizeSlots(slots);
+            return slots;
+        }
+
+        private Slot[,] RestoreCroppedCraftingGrid(Slot[,] craftingGrid, int gridLeft, int gridTop, int originWidth, int originHeight)
+        {
+            var slots = new Slot[originWidth, originHeight];
+            for (int y = 0; y < craftingGrid.GetUpperBound(1) + 1; y++)
+            {
+                for (int x = 0; x < craftingGrid.GetUpperBound(0) + 1; x++)
+                {
+                    slots[x + gridLeft, y + gridTop] = craftingGrid[x, y];
+                }
+            }
+
+            NormalizeSlots(slots);
+            return slots;
+        }
+
+        private static void NormalizeSlots(Slot[,] slots)
+        {
+            for (int x = 0; x < slots.GetUpperBound(0) + 1; x++)
+            {
+                for (int y = 0; y < slots.GetUpperBound(1) + 1; y++)
+                {
+                    slots[x, y].MakeEmptyIfZero();
+                }
+            }
+        }
+    }
+}

+ 60 - 0
src/MineCase.Algorithm/FurnaceRecipeMatcher.cs

@@ -0,0 +1,60 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Algorithm
+{
+    public class FindFurnaceRecipeResult
+    {
+        public FurnaceRecipe Recipe { get; set; }
+
+        public FurnaceFuel Fuel { get; set; }
+    }
+
+    public class FurnaceRecipeMatcher
+    {
+        private List<FurnaceRecipe> _recipes;
+        private List<FurnaceFuel> _fuels;
+
+        public FurnaceRecipeMatcher(List<FurnaceRecipe> recipes, List<FurnaceFuel> fuels)
+        {
+            _recipes = recipes;
+            _fuels = fuels;
+        }
+
+        public FindFurnaceRecipeResult FindRecipe(Slot input, Slot fuel)
+        {
+            if (input.IsEmpty || fuel.IsEmpty) return null;
+
+            FurnaceRecipe recipe = null;
+            foreach (var item in _recipes)
+            {
+                if (item.Input.BlockId == input.BlockId
+                    && (item.Input.ItemDamage == -1 || item.Input.ItemDamage == input.ItemDamage)
+                    && item.Input.ItemCount <= input.ItemCount)
+                {
+                    recipe = item;
+                    break;
+                }
+            }
+
+            if (recipe == null) return null;
+
+            foreach (var item in _fuels)
+            {
+                if (item.Slot.BlockId == input.BlockId
+                    && (item.Slot.ItemDamage == -1 || item.Slot.ItemDamage == input.ItemDamage)
+                    && item.Slot.ItemCount <= input.ItemCount)
+                {
+                    return new FindFurnaceRecipeResult
+                    {
+                        Recipe = recipe,
+                        Fuel = item
+                    };
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 4 - 0
src/MineCase.Algorithm/MineCase.Algorithm.csproj

@@ -15,4 +15,8 @@
     <AdditionalFiles Include="..\..\build\stylecop.json" />
   </ItemGroup>
 
+  <ItemGroup>
+    <ProjectReference Include="..\Minecase.Core\MineCase.Core.csproj" />
+  </ItemGroup>
+
 </Project>

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

@@ -31,6 +31,7 @@
   </ItemGroup>
 
   <ItemGroup>
+    <ProjectReference Include="..\Minecase.Core\MineCase.Core.csproj" />
     <ProjectReference Include="..\MineCase.Protocol\MineCase.Protocol.csproj" />
     <ProjectReference Include="..\MineCase.Server.Interfaces\MineCase.Server.Interfaces.csproj" />
   </ItemGroup>

+ 0 - 19
src/MineCase.Protocol/Formats/Slot.cs

@@ -1,19 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Text;
-
-namespace MineCase.Formats
-{
-    public sealed class Slot
-    {
-        public static Slot Empty { get; } = new Slot();
-
-        public bool IsEmpty => BlockId == -1;
-
-        public short BlockId { get; set; } = -1;
-
-        public byte ItemCount { get; set; }
-
-        public short ItemDamage { get; set; }
-    }
-}

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

@@ -23,4 +23,9 @@
     <AdditionalFiles Include="..\..\build\stylecop.json" />
   </ItemGroup>
 
+  <ItemGroup>
+    <ProjectReference Include="..\Minecase.Core\MineCase.Core.csproj" />
+    <ProjectReference Include="..\MineCase.NBT\MineCase.Nbt.csproj" />
+  </ItemGroup>
+
 </Project>

+ 0 - 1
src/MineCase.Protocol/Protocol/Play/BlockBreakAnimation.cs

@@ -1,5 +1,4 @@
 using System.IO;
-using MineCase.Formats;
 using MineCase.Serialization;
 using Orleans.Concurrency;
 

+ 0 - 1
src/MineCase.Protocol/Protocol/Play/BlockChange.cs

@@ -2,7 +2,6 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using MineCase.Formats;
 using MineCase.Serialization;
 using Orleans.Concurrency;
 

+ 0 - 1
src/MineCase.Protocol/Protocol/Play/ChatMessage.cs

@@ -2,7 +2,6 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using MineCase.Formats;
 using MineCase.Serialization;
 using Orleans.Concurrency;
 

+ 45 - 0
src/MineCase.Protocol/Protocol/Play/ClickWindow.cs

@@ -0,0 +1,45 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+using MineCase.Serialization;
+using Orleans.Concurrency;
+
+namespace MineCase.Protocol.Play
+{
+    [Immutable]
+    [Packet(0x08)]
+    public sealed class ClickWindow
+    {
+        [SerializeAs(DataType.Byte)]
+        public byte WindowId;
+
+        [SerializeAs(DataType.Short)]
+        public short Slot;
+
+        [SerializeAs(DataType.Byte)]
+        public byte Button;
+
+        [SerializeAs(DataType.Short)]
+        public short ActionNumber;
+
+        [SerializeAs(DataType.VarInt)]
+        public uint Mode;
+
+        [SerializeAs(DataType.Slot)]
+        public Slot ClickedItem;
+
+        public static ClickWindow Deserialize(ref SpanReader br)
+        {
+            return new ClickWindow
+            {
+                WindowId = br.ReadAsByte(),
+                Slot = br.ReadAsShort(),
+                Button = br.ReadAsByte(),
+                ActionNumber = br.ReadAsShort(),
+                Mode = br.ReadAsVarInt(out _),
+                ClickedItem = br.ReadAsSlot()
+            };
+        }
+    }
+}

+ 1 - 1
src/MineCase.Protocol/Protocol/Play/ClientboundAnimation.cs

@@ -1,5 +1,5 @@
 using System.IO;
-using MineCase.Formats;
+
 using MineCase.Serialization;
 using Orleans.Concurrency;
 

+ 38 - 0
src/MineCase.Protocol/Protocol/Play/CloseWindow.cs

@@ -0,0 +1,38 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MineCase.Serialization;
+using Orleans.Concurrency;
+
+namespace MineCase.Protocol.Play
+{
+    [Immutable]
+    [Packet(0x09)]
+    public sealed class ServerboundCloseWindow
+    {
+        [SerializeAs(DataType.Byte)]
+        public byte WindowId;
+
+        public static ServerboundCloseWindow Deserialize(ref SpanReader br)
+        {
+            return new ServerboundCloseWindow
+            {
+                WindowId = br.ReadAsByte()
+            };
+        }
+    }
+
+    [Immutable]
+    [Packet(0x12)]
+    public sealed class ClientboundCloseWindow : ISerializablePacket
+    {
+        [SerializeAs(DataType.Byte)]
+        public byte WindowId;
+
+        public void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsByte(WindowId);
+        }
+    }
+}

+ 21 - 0
src/MineCase.Protocol/Protocol/Play/ConfirmTransaction.cs

@@ -30,4 +30,25 @@ namespace MineCase.Protocol.Play
             };
         }
     }
+
+    [Immutable]
+    [Packet(0x11)]
+    public sealed class ClientboundConfirmTransaction : ISerializablePacket
+    {
+        [SerializeAs(DataType.Byte)]
+        public byte WindowId;
+
+        [SerializeAs(DataType.Short)]
+        public short ActionNumber;
+
+        [SerializeAs(DataType.Boolean)]
+        public bool Accepted;
+
+        public void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsByte(WindowId);
+            bw.WriteAsShort(ActionNumber);
+            bw.WriteAsBoolean(Accepted);
+        }
+    }
 }

+ 39 - 0
src/MineCase.Protocol/Protocol/Play/OpenWindow.cs

@@ -0,0 +1,39 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MineCase.Serialization;
+using Orleans.Concurrency;
+
+namespace MineCase.Protocol.Play
+{
+    [Immutable]
+    [Packet(0x13)]
+    public sealed class OpenWindow : ISerializablePacket
+    {
+        [SerializeAs(DataType.Byte)]
+        public byte WindowId;
+
+        [SerializeAs(DataType.String)]
+        public string WindowType;
+
+        [SerializeAs(DataType.Chat)]
+        public Chat WindowTitle;
+
+        [SerializeAs(DataType.Byte)]
+        public byte NumberOfSlots;
+
+        [SerializeAs(DataType.Byte)]
+        public byte? EntityId;
+
+        public void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsByte(WindowId);
+            bw.WriteAsString(WindowType);
+            bw.WriteAsChat(WindowTitle);
+            bw.WriteAsByte(NumberOfSlots);
+            if (EntityId.HasValue)
+                bw.WriteAsByte(EntityId.Value);
+        }
+    }
+}

+ 1 - 1
src/MineCase.Protocol/Protocol/Play/PlayerBlockPlacement.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using MineCase.Formats;
+
 using MineCase.Serialization;
 using Orleans.Concurrency;
 

+ 1 - 1
src/MineCase.Protocol/Protocol/Play/PlayerDigging.cs

@@ -1,7 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
-using MineCase.Formats;
+
 using MineCase.Serialization;
 
 namespace MineCase.Protocol.Play

+ 1 - 1
src/MineCase.Protocol/Protocol/Play/SetSlot.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using MineCase.Formats;
+
 using MineCase.Serialization;
 using Orleans.Concurrency;
 

+ 1 - 1
src/MineCase.Protocol/Protocol/Play/WindowItems.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.IO;
 using System.Text;
-using MineCase.Formats;
+
 using MineCase.Serialization;
 using Orleans.Concurrency;
 

+ 30 - 0
src/MineCase.Protocol/Protocol/Play/WindowProperty.cs

@@ -0,0 +1,30 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using MineCase.Serialization;
+using Orleans.Concurrency;
+
+namespace MineCase.Protocol.Play
+{
+    [Immutable]
+    [Packet(0x15)]
+    public sealed class WindowProperty : ISerializablePacket
+    {
+        [SerializeAs(DataType.Byte)]
+        public byte WindowId;
+
+        [SerializeAs(DataType.Short)]
+        public short Property;
+
+        [SerializeAs(DataType.Short)]
+        public short Value;
+
+        public void Serialize(BinaryWriter bw)
+        {
+            bw.WriteAsByte(WindowId);
+            bw.WriteAsShort(Property);
+            bw.WriteAsShort(Value);
+        }
+    }
+}

+ 0 - 1
src/MineCase.Protocol/Serialization/BinaryReaderExtensions.cs

@@ -4,7 +4,6 @@ using System.IO;
 using System.Runtime.CompilerServices;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
 
 namespace MineCase.Serialization
 {

+ 5 - 2
src/MineCase.Protocol/Serialization/BinaryWriterExtensions.cs

@@ -4,7 +4,7 @@ using System.Diagnostics;
 using System.IO;
 using System.Runtime.CompilerServices;
 using System.Text;
-using MineCase.Formats;
+
 using MineCase.Protocol;
 using MineCase.Protocol.Play;
 
@@ -118,7 +118,10 @@ namespace MineCase.Serialization
             {
                 bw.WriteAsByte(slot.ItemCount);
                 bw.WriteAsShort(slot.ItemDamage);
-                bw.WriteAsByte(0);
+                if (slot.NBT != null)
+                    slot.NBT.WriteTo(bw.BaseStream);
+                else
+                    bw.WriteAsByte(0);
             }
         }
 

+ 24 - 1
src/MineCase.Protocol/Serialization/SpanReader.cs

@@ -4,7 +4,8 @@ using System.Collections.Generic;
 using System.IO;
 using System.Runtime.CompilerServices;
 using System.Text;
-using MineCase.Formats;
+
+using MineCase.Nbt;
 
 namespace MineCase.Serialization
 {
@@ -62,6 +63,12 @@ namespace MineCase.Serialization
             return value;
         }
 
+        public byte PeekAsByte()
+        {
+            var value = _span.ReadBigEndian<byte>();
+            return value;
+        }
+
         public byte ReadAsByte()
         {
             var value = _span.ReadBigEndian<byte>();
@@ -139,6 +146,22 @@ namespace MineCase.Serialization
             return (int)value;
         }
 
+        public Slot ReadAsSlot()
+        {
+            var slot = new Slot { BlockId = ReadAsShort() };
+            if (!slot.IsEmpty)
+            {
+                slot.ItemCount = ReadAsByte();
+                slot.ItemDamage = ReadAsShort();
+                if (PeekAsByte() == 0)
+                    Advance(1);
+                else
+                    slot.NBT = new NbtFile(new MemoryStream(ReadAsByteArray()), false);
+            }
+
+            return slot;
+        }
+
         private ReadOnlySpan<byte> ReadBytes(int length)
         {
             var bytes = _span.Slice(0, length);

+ 16 - 0
src/MineCase.Server.Grains/Game/BlockEntities/BlockEntitiesModule.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Autofac;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    internal class BlockEntitiesModule : Module
+    {
+        protected override void Load(ContainerBuilder builder)
+        {
+            builder.RegisterType<ChestBlockEntityGrain>();
+            builder.RegisterType<FurnaceBlockEntity>();
+        }
+    }
+}

+ 48 - 0
src/MineCase.Server.Grains/Game/BlockEntities/BlockEntity.cs

@@ -0,0 +1,48 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using MineCase.Server.World;
+using Orleans;
+using Orleans.Runtime;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    public static class BlockEntity
+    {
+        private static readonly Dictionary<BlockId, MethodInfo> _blockEntityTypes;
+
+        static BlockEntity()
+        {
+            var genGetGrain = typeof(BlockEntity).GetMethod(nameof(GetBlockEntity), BindingFlags.NonPublic | BindingFlags.Static);
+
+            _blockEntityTypes = (from t in typeof(IBlockEntity).Assembly.DefinedTypes
+                                 where t.IsInterface && t.ImplementedInterfaces.Contains(typeof(IBlockEntity))
+                                 let attrs = t.GetCustomAttributes<BlockEntityAttribute>()
+                                 from attr in attrs
+                                 select new
+                                 {
+                                     BlockId = attr.BlockId,
+                                     Method = genGetGrain.MakeGenericMethod(t)
+                                 }).ToDictionary(o => o.BlockId, o => o.Method);
+        }
+
+        private static IBlockEntity GetBlockEntity<TGrain>(IGrainFactory grainFactory, string key)
+            where TGrain : IBlockEntity
+        {
+            return grainFactory.GetGrain<TGrain>(key).Cast<IBlockEntity>();
+        }
+
+        public static IBlockEntity Create(IGrainFactory grainFactory, IWorld world, BlockWorldPos position, BlockId blockId)
+        {
+            if (_blockEntityTypes.TryGetValue(blockId, out var method))
+            {
+                var key = world.MakeBlockEntityKey(position);
+                return (IBlockEntity)method.Invoke(null, new object[] { grainFactory, key });
+            }
+
+            return null;
+        }
+    }
+}

+ 34 - 0
src/MineCase.Server.Grains/Game/BlockEntities/BlockEntityGrain.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    internal abstract class BlockEntityGrain : Grain, IBlockEntity
+    {
+        protected IWorld World { get; private set; }
+
+        protected BlockWorldPos Position { get; private set; }
+
+        public override Task OnActivateAsync()
+        {
+            var keys = this.GetWorldAndBlockEntityPosition();
+            World = GrainFactory.GetGrain<IWorld>(keys.worldKey);
+            Position = keys.position;
+            return base.OnActivateAsync();
+        }
+
+        public virtual Task Destroy()
+        {
+            return Task.CompletedTask;
+        }
+
+        public virtual Task OnCreated()
+        {
+            return Task.CompletedTask;
+        }
+    }
+}

+ 97 - 0
src/MineCase.Server.Grains/Game/BlockEntities/ChestBlockEntityGrain.cs

@@ -0,0 +1,97 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows;
+using MineCase.Server.Game.Windows.SlotAreas;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    [Reentrant]
+    internal class ChestBlockEntityGrain : BlockEntityGrain, IChestBlockEntity
+    {
+        private Slot[] _slots;
+        private IChestBlockEntity _neightborEntity;
+
+        public override Task OnActivateAsync()
+        {
+            _slots = Enumerable.Repeat(Slot.Empty, ChestSlotArea.ChestSlotsCount).ToArray();
+            return base.OnActivateAsync();
+        }
+
+        public Task<Slot> GetSlot(int slotIndex)
+        {
+            return Task.FromResult(_slots[slotIndex]);
+        }
+
+        public Task SetSlot(int slotIndex, Slot item)
+        {
+            _slots[slotIndex] = item;
+            return Task.CompletedTask;
+        }
+
+        private IChestWindow _chestWindow;
+
+        public async Task UseBy(IPlayer player)
+        {
+            var masterEntity = FindMasterEntity(_neightborEntity);
+            if (masterEntity.GetPrimaryKeyString() == this.GetPrimaryKeyString())
+            {
+                if (_chestWindow == null)
+                {
+                    _chestWindow = GrainFactory.GetGrain<IChestWindow>(Guid.NewGuid());
+                    await _chestWindow.SetEntities((_neightborEntity == null ?
+                        new[] { this.AsReference<IChestBlockEntity>() } : new[] { this.AsReference<IChestBlockEntity>(), _neightborEntity }).AsImmutable());
+                }
+
+                await player.OpenWindow(_chestWindow);
+            }
+            else
+            {
+                await masterEntity.UseBy(player);
+            }
+        }
+
+        public override async Task Destroy()
+        {
+            if (_chestWindow != null)
+                await _chestWindow.Destroy();
+        }
+
+        public async Task ClearNeighborEntity()
+        {
+            _neightborEntity = null;
+            if (_chestWindow != null)
+            {
+                await _chestWindow.Destroy();
+                await _chestWindow.SetEntities(new[] { this.AsReference<IChestBlockEntity>() }.AsImmutable());
+            }
+        }
+
+        public async Task SetNeighborEntity(IChestBlockEntity chestEntity)
+        {
+            _neightborEntity = chestEntity;
+            if (_chestWindow != null)
+            {
+                await _chestWindow.Destroy();
+                await _chestWindow.SetEntities(new[] { this.AsReference<IChestBlockEntity>(), chestEntity }.AsImmutable());
+            }
+        }
+
+        private IChestBlockEntity FindMasterEntity(IChestBlockEntity neighborEntity)
+        {
+            if (neighborEntity == null)
+                return this.AsReference<IChestBlockEntity>();
+
+            // 按 X, Z 排序取最小
+            return (from e in new[] { this.AsReference<IChestBlockEntity>(), neighborEntity }
+                    let pos = e.GetBlockEntityPosition()
+                    orderby pos.X, pos.Z
+                    select e).First();
+        }
+    }
+}

+ 168 - 0
src/MineCase.Server.Grains/Game/BlockEntities/FurnaceBlockEntity.cs

@@ -0,0 +1,168 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Algorithm;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows;
+using MineCase.Server.Game.Windows.SlotAreas;
+using MineCase.Server.World;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    [Reentrant]
+    internal class FurnaceBlockEntity : BlockEntityGrain, IFurnaceBlockEntity
+    {
+        private Slot[] _slots;
+
+        private bool _isCooking;
+        private FindFurnaceRecipeResult _currentRecipe;
+        private int _fuelLeft;
+        private int _maxFuelTime;
+        private int _cookProgress;
+        private int _maxProgress;
+
+        public override Task OnActivateAsync()
+        {
+            _slots = Enumerable.Repeat(Slot.Empty, FurnaceSlotArea.FurnaceSlotsCount).ToArray();
+            _currentRecipe = null;
+            _isCooking = false;
+            _fuelLeft = 0;
+            _maxFuelTime = 0;
+            _cookProgress = 0;
+            _maxFuelTime = 200;
+            return base.OnActivateAsync();
+        }
+
+        public Task<Slot> GetSlot(int slotIndex)
+        {
+            return Task.FromResult(_slots[slotIndex]);
+        }
+
+        public async Task SetSlot(int slotIndex, Slot item)
+        {
+            _slots[slotIndex] = item;
+            if (_currentRecipe == null)
+                await UpdateRecipe();
+        }
+
+        private async Task UpdateRecipe()
+        {
+            var recipe = await GrainFactory.GetGrain<IFurnaceRecipes>(0).FindRecipe(_slots[0], _slots[1]);
+            if (recipe != null)
+            {
+                if (_slots[2].IsEmpty || _slots[2].CanStack(recipe.Recipe.Output))
+                {
+                    _currentRecipe = recipe;
+                    return;
+                }
+            }
+
+            _currentRecipe = null;
+        }
+
+        private IFurnaceWindow _furnaceWindow;
+
+        public async Task UseBy(IPlayer player)
+        {
+            if (_furnaceWindow == null)
+            {
+                _furnaceWindow = GrainFactory.GetGrain<IFurnaceWindow>(Guid.NewGuid());
+                await _furnaceWindow.SetEntity(this);
+            }
+
+            await player.OpenWindow(_furnaceWindow);
+        }
+
+        public override async Task OnCreated()
+        {
+            var chunkPos = Position.ToChunkWorldPos();
+            var tracker = GrainFactory.GetGrain<IChunkTrackingHub>(World.MakeChunkTrackingHubKey(chunkPos.X, chunkPos.Z));
+            await tracker.Subscribe(this);
+        }
+
+        public override async Task Destroy()
+        {
+            var chunkPos = Position.ToChunkWorldPos();
+            var tracker = GrainFactory.GetGrain<IChunkTrackingHub>(World.MakeChunkTrackingHubKey(chunkPos.X, chunkPos.Z));
+            await tracker.Unsubscribe(this);
+        }
+
+        public async Task OnGameTick(TimeSpan deltaTime, long worldAge)
+        {
+            if (_currentRecipe != null)
+            {
+                if (!_isCooking)
+                    await StartCooking();
+                if (_cookProgress == 0)
+                    await TakeIngredient();
+                if (_fuelLeft == 0)
+                    await TakeFuel();
+                if (_cookProgress == _maxProgress)
+                {
+                    if (_slots[2].IsEmpty)
+                        _slots[2] = _currentRecipe.Recipe.Output;
+                    else
+                        _slots[2].ItemCount += _currentRecipe.Recipe.Output.ItemCount;
+                    if (_furnaceWindow != null)
+                        await _furnaceWindow.BroadcastSlotChanged(2, _slots[2]);
+                    _cookProgress = 0;
+                    await UpdateRecipe();
+                }
+
+                if (_currentRecipe != null)
+                {
+                    _cookProgress++;
+                    _fuelLeft--;
+                }
+            }
+            else if (_isCooking)
+            {
+                await StopCooking();
+            }
+
+            if (_furnaceWindow != null)
+                await _furnaceWindow.OnGameTick(deltaTime, worldAge);
+        }
+
+        private async Task TakeFuel()
+        {
+            _slots[1].ItemCount -= _currentRecipe.Fuel.Slot.ItemCount;
+            _slots[1].MakeEmptyIfZero();
+            _maxFuelTime = _fuelLeft = _currentRecipe.Fuel.Time;
+            if (_furnaceWindow != null)
+                await _furnaceWindow.BroadcastSlotChanged(0, _slots[1]);
+        }
+
+        private async Task TakeIngredient()
+        {
+            _slots[0].ItemCount -= _currentRecipe.Recipe.Input.ItemCount;
+            _slots[0].MakeEmptyIfZero();
+            _cookProgress = 0;
+            _maxProgress = _currentRecipe.Recipe.Time;
+            if (_furnaceWindow != null)
+                await _furnaceWindow.BroadcastSlotChanged(0, _slots[0]);
+        }
+
+        private async Task StartCooking()
+        {
+            _isCooking = true;
+            var facing = (FacingDirectionType)(await World.GetBlockState(GrainFactory, Position)).MetaValue;
+            await World.SetBlockState(GrainFactory, Position, BlockStates.BurningFurnace(facing));
+        }
+
+        private async Task StopCooking()
+        {
+            _isCooking = false;
+            var facing = (FacingDirectionType)(await World.GetBlockState(GrainFactory, Position)).MetaValue;
+            await World.SetBlockState(GrainFactory, Position, BlockStates.Furnace(facing));
+        }
+
+        public Task<(int fuelLeft, int maxFuelTime, int cookProgress, int maxProgress)> GetCookingState()
+        {
+            return Task.FromResult((_fuelLeft, _maxFuelTime, _cookProgress, _maxProgress));
+        }
+    }
+}

+ 81 - 0
src/MineCase.Server.Grains/Game/Blocks/BlockHandler.cs

@@ -0,0 +1,81 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.Blocks
+{
+    public abstract class BlockHandler
+    {
+        public BlockId BlockId { get; }
+
+        public abstract bool IsUsable { get; }
+
+        public BlockHandler(BlockId blockId)
+        {
+            BlockId = blockId;
+        }
+
+        private static readonly Dictionary<BlockId, Type> _blockHandlerTypes;
+        private static readonly ConcurrentDictionary<BlockId, BlockHandler> _blockHandlers = new ConcurrentDictionary<BlockId, BlockHandler>();
+        private static readonly BlockHandler _defaultBlockHandler = new DefaultBlockHandler();
+
+        static BlockHandler()
+        {
+            _blockHandlerTypes = (from t in typeof(BlockHandler).Assembly.DefinedTypes
+                                  where !t.IsAbstract && t.IsSubclassOf(typeof(BlockHandler))
+                                  let attrs = t.GetCustomAttributes<BlockHandlerAttribute>()
+                                  from attr in attrs
+                                  select new
+                                  {
+                                      BlockId = attr.BlockId,
+                                      Type = t
+                                  }).ToDictionary(o => o.BlockId, o => o.Type.AsType());
+        }
+
+        public static BlockHandler Create(BlockId blockId)
+        {
+            if (_blockHandlerTypes.TryGetValue(blockId, out var type))
+                return _blockHandlers.GetOrAdd(blockId, k => (BlockHandler)Activator.CreateInstance(type, k));
+            return _defaultBlockHandler;
+        }
+
+        public virtual Task UseBy(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos blockPosition, Vector3 cursorPosition)
+        {
+            return Task.CompletedTask;
+        }
+
+        public virtual Task<bool> CanBeAt(BlockWorldPos position, IGrainFactory grainFactory, IWorld world)
+        {
+            return Task.FromResult(true);
+        }
+
+        public virtual Task OnNeighborChanged(BlockWorldPos selfPosition, BlockWorldPos neighborPosition, BlockState oldState, BlockState newState, IGrainFactory grainFactory, IWorld world)
+        {
+            return Task.CompletedTask;
+        }
+
+        public virtual Task OnPlaced(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos position, BlockState blockState)
+        {
+            return Task.CompletedTask;
+        }
+    }
+
+    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
+    public sealed class BlockHandlerAttribute : Attribute
+    {
+        public BlockId BlockId { get; }
+
+        public BlockHandlerAttribute(BlockId blockId)
+        {
+            BlockId = blockId;
+        }
+    }
+}

+ 111 - 0
src/MineCase.Server.Grains/Game/Blocks/ChestBlockHandler.cs

@@ -0,0 +1,111 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.Blocks
+{
+    [BlockHandler(BlockId.Chest)]
+    public class ChestBlockHandler : BlockHandler
+    {
+        public override bool IsUsable => true;
+
+        public ChestBlockHandler(BlockId blockId)
+            : base(blockId)
+        {
+        }
+
+        public static readonly (int x, int z)[] CrossCoords = new[]
+        {
+            (-1, 0), (0, -1), (1, 0), (0, 1)
+        };
+
+        public override async Task<bool> CanBeAt(BlockWorldPos position, IGrainFactory grainFactory, IWorld world)
+        {
+            bool hasNeighbor = false;
+            foreach (var crossCoord in CrossCoords)
+            {
+                var blockState = await world.GetBlockState(grainFactory, position.X + crossCoord.x, position.Y, position.Z + crossCoord.z);
+                if (blockState.Id != (uint)BlockId) continue;
+
+                if (hasNeighbor) return false;
+                hasNeighbor = true;
+
+                // 再检查隔壁的隔壁是不是箱子
+                foreach (var crossCoord2 in CrossCoords)
+                {
+                    var blockState2 = await world.GetBlockState(grainFactory, position.X + crossCoord.x + crossCoord2.x, position.Y, position.Z + crossCoord.z + crossCoord2.z);
+                    if (blockState2.Id == (uint)BlockId) return false;
+                }
+            }
+
+            return await base.CanBeAt(position, grainFactory, world);
+        }
+
+        public override async Task UseBy(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos blockPosition, Vector3 cursorPosition)
+        {
+            var entity = (await world.GetBlockEntity(grainFactory, blockPosition)).Cast<IChestBlockEntity>();
+            await entity.UseBy(player);
+        }
+
+        public override async Task OnNeighborChanged(BlockWorldPos selfPosition, BlockWorldPos neighborPosition, BlockState oldState, BlockState newState, IGrainFactory grainFactory, IWorld world)
+        {
+            if (oldState.Id == (uint)BlockId.Chest)
+            {
+                var entity = (await world.GetBlockEntity(grainFactory, selfPosition)).Cast<IChestBlockEntity>();
+                await entity.ClearNeighborEntity();
+            }
+
+            if (newState.Id == (uint)BlockId.Chest)
+            {
+                await world.SetBlockState(grainFactory, selfPosition, newState);
+                var entity = (await world.GetBlockEntity(grainFactory, selfPosition)).Cast<IChestBlockEntity>();
+                await entity.SetNeighborEntity((await world.GetBlockEntity(grainFactory, neighborPosition)).Cast<IChestBlockEntity>());
+            }
+        }
+
+        public override async Task OnPlaced(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos position, BlockState blockState)
+        {
+            BlockWorldPos? neighborPosition = null;
+            foreach (var crossCoord in CrossCoords)
+            {
+                var neighborPos = new BlockWorldPos(position.X + crossCoord.x, position.Y, position.Z + crossCoord.z);
+                var neighborState = await world.GetBlockState(grainFactory, neighborPos);
+                if (neighborState.Id == (uint)BlockId)
+                {
+                    neighborPosition = neighborPos;
+                    break;
+                }
+            }
+
+            if (neighborPosition.HasValue)
+            {
+                var entity = (await world.GetBlockEntity(grainFactory, position)).Cast<IChestBlockEntity>();
+                var neightborEntity = (await world.GetBlockEntity(grainFactory, neighborPosition.Value)).Cast<IChestBlockEntity>();
+                await entity.SetNeighborEntity(neightborEntity);
+            }
+        }
+
+        public static FacingDirectionType PlayerYawToFacing(float yaw)
+        {
+            yaw += 90 + 45;  // So its not aligned with axis
+
+            if (yaw > 360.0f)
+                yaw -= 360.0f;
+
+            if ((yaw >= 0.0f) && (yaw < 90.0f))
+                return FacingDirectionType.FacingWest;
+            else if ((yaw >= 180) && (yaw < 270))
+                return FacingDirectionType.FacingEast;
+            else if ((yaw >= 90) && (yaw < 180))
+                return FacingDirectionType.FacingNorth;
+            else
+                return FacingDirectionType.FacingSouth;
+        }
+    }
+}

+ 29 - 0
src/MineCase.Server.Grains/Game/Blocks/CraftingTableBlockHandler.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.Blocks
+{
+    [BlockHandler(BlockId.CraftingTable)]
+    public sealed class CraftingTableBlockHandler : BlockHandler
+    {
+        public CraftingTableBlockHandler(BlockId blockId)
+            : base(blockId)
+        {
+        }
+
+        public override bool IsUsable => true;
+
+        public override async Task UseBy(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos blockPosition, Vector3 cursorPosition)
+        {
+            var window = grainFactory.GetGrain<ICraftingWindow>(Guid.NewGuid());
+            await player.OpenWindow(window);
+        }
+    }
+}

+ 16 - 0
src/MineCase.Server.Grains/Game/Blocks/DefaultBlockHandler.cs

@@ -0,0 +1,16 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Server.Game.Blocks
+{
+    public class DefaultBlockHandler : BlockHandler
+    {
+        public DefaultBlockHandler()
+            : base(0)
+        {
+        }
+
+        public override bool IsUsable => false;
+    }
+}

+ 29 - 0
src/MineCase.Server.Grains/Game/Blocks/FurnaceBlockHandler.cs

@@ -0,0 +1,29 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.Blocks
+{
+    [BlockHandler(BlockId.Furnace)]
+    public class FurnaceBlockHandler : BlockHandler
+    {
+        public override bool IsUsable => true;
+
+        public FurnaceBlockHandler(BlockId blockId)
+            : base(blockId)
+        {
+        }
+
+        public override async Task UseBy(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos blockPosition, Vector3 cursorPosition)
+        {
+            var entity = (await world.GetBlockEntity(grainFactory, blockPosition)).Cast<IFurnaceBlockEntity>();
+            await entity.UseBy(player);
+        }
+    }
+}

+ 44 - 0
src/MineCase.Server.Grains/Game/CraftingRecipesGrain.cs

@@ -0,0 +1,44 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.CompilerServices;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.FileProviders;
+using MineCase.Algorithm;
+using MineCase.Server.World;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game
+{
+    [StatelessWorker]
+    internal class CraftingRecipesGrain : Grain, ICraftingRecipes
+    {
+        private const string _recipesFileName = "crafting.txt";
+
+        private CraftingRecipeMatcher _recipeMatcher;
+        private IFileProvider _fileProvider;
+
+        public CraftingRecipesGrain()
+        {
+            _fileProvider = new PhysicalFileProvider(AppContext.BaseDirectory);
+        }
+
+        public Task<FindCraftingRecipeResult> FindRecipe(Immutable<Slot[,]> craftingGrid)
+        {
+            return Task.FromResult(_recipeMatcher.FindRecipe(craftingGrid.Value));
+        }
+
+        public override async Task OnActivateAsync()
+        {
+            var file = _fileProvider.GetFileInfo(_recipesFileName);
+
+            var recipeLoader = new CraftingRecipeLoader();
+            using (var sr = new StreamReader(file.CreateReadStream()))
+                await recipeLoader.LoadRecipes(sr);
+            _recipeMatcher = new CraftingRecipeMatcher(recipeLoader.Recipes);
+        }
+    }
+}

+ 14 - 6
src/MineCase.Server.Grains/Game/Entities/PickupGrain.cs

@@ -3,25 +3,30 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Server.World;
 using Orleans;
 
 namespace MineCase.Server.Game.Entities
 {
-    internal class PickupGrain : EntityGrain, IPickup, ICollectable
+    internal class PickupGrain : EntityGrain, IPickup
     {
         private EntityMetadata.Pickup _metadata;
 
         public async Task CollectBy(IPlayer player)
         {
-            if (await player.Collect(EntityId, _metadata.Item))
+            var after = await player.Collect(EntityId, _metadata.Item);
+            if (after.IsEmpty)
             {
                 var chunkPos = GetChunkPosition();
                 await GrainFactory.GetGrain<ICollectableFinder>(World.MakeCollectableFinderKey(chunkPos.x, chunkPos.z)).Unregister(this);
                 await GetBroadcastGenerator().DestroyEntities(new[] { EntityId });
                 DeactivateOnIdle();
             }
+            else if (_metadata.Item.ItemCount != after.ItemCount)
+            {
+                await SetItem(after);
+            }
         }
 
         public override Task OnActivateAsync()
@@ -30,6 +35,12 @@ namespace MineCase.Server.Game.Entities
             return base.OnActivateAsync();
         }
 
+        public async Task Register()
+        {
+            var chunkPos = GetChunkPosition();
+            await GrainFactory.GetGrain<ICollectableFinder>(World.MakeCollectableFinderKey(chunkPos.x, chunkPos.z)).Register(this);
+        }
+
         public async Task SetItem(Slot item)
         {
             _metadata.Item = item;
@@ -41,9 +52,6 @@ namespace MineCase.Server.Game.Entities
             UUID = uuid;
             await SetPosition(position);
             await GetBroadcastGenerator().SpawnObject(EntityId, uuid, 2, position, 0, 0, 0);
-
-            var chunkPos = GetChunkPosition();
-            await GrainFactory.GetGrain<ICollectableFinder>(World.MakeCollectableFinderKey(chunkPos.x, chunkPos.z)).Register(this);
         }
     }
 }

+ 120 - 15
src/MineCase.Server.Grains/Game/Entities/PlayerGrain.cs

@@ -5,8 +5,9 @@ using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
 using Microsoft.Extensions.DependencyInjection;
-using MineCase.Formats;
 using MineCase.Protocol.Play;
+using MineCase.Server.Game.Blocks;
+using MineCase.Server.Game.Items;
 using MineCase.Server.Game.Windows;
 using MineCase.Server.Network;
 using MineCase.Server.Network.Play;
@@ -24,7 +25,11 @@ namespace MineCase.Server.Game.Entities
         private ClientPlayPacketGenerator _generator;
 
         private string _name;
+        private Slot[] _inventorySlots;
+        private Slot _draggedSlot;
+        private short _heldSlot;
         private IInventoryWindow _inventory;
+        private Dictionary<byte, WindowContext> _windows;
 
         public Task<IInventoryWindow> GetInventory() => Task.FromResult(_inventory);
 
@@ -59,7 +64,7 @@ namespace MineCase.Server.Game.Entities
 
         public async Task SendWholeInventory()
         {
-            var slots = await _inventory.GetSlots();
+            var slots = await _inventory.GetSlots(this);
             await _generator.WindowItems(0, slots);
         }
 
@@ -68,7 +73,6 @@ namespace MineCase.Server.Game.Entities
             _generator = new ClientPlayPacketGenerator(await user.GetClientPacketSink());
             _user = user;
             _health = MaxHealth;
-            await _inventory.SetUser(user);
         }
 
         public async Task SendHealth()
@@ -109,6 +113,13 @@ namespace MineCase.Server.Game.Entities
 
         public async Task NotifyLoggedIn()
         {
+            _inventorySlots = await _user.GetInventorySlots();
+            _windows = new Dictionary<byte, WindowContext>
+            {
+                { 0, new WindowContext { Window = _inventory } }
+            };
+            _draggedSlot = Slot.Empty;
+            _heldSlot = 0;
             await SendWholeInventory();
             await SendExperience();
             await SendPlayerListAddPlayer(new[] { this });
@@ -170,15 +181,10 @@ namespace MineCase.Server.Game.Entities
                 await World.SetBlockState(GrainFactory, location.X, location.Y, location.Z, newState);
 
                 var chunk = location.GetChunk();
-                await GetBroadcastGenerator(chunk.chunkX, chunk.chunkZ).BlockChange(location, newState);
 
                 // 产生 Pickup
-                var pickup = GrainFactory.GetGrain<IPickup>(World.MakeEntityKey(await World.NewEntityId()));
-                await World.AttachEntity(pickup);
-                await pickup.Spawn(
-                    Guid.NewGuid(),
-                    new Vector3(location.X + 0.5f, location.Y + 0.5f, location.Z + 0.5f));
-                await pickup.SetItem(new Slot { BlockId = (short)oldState.Id, ItemDamage = (short)oldState.MetaValue, ItemCount = 1 });
+                var finder = GrainFactory.GetGrain<ICollectableFinder>(World.MakeCollectableFinderKey(chunk.chunkX, chunk.chunkZ));
+                await finder.SpawnPickup(location, new[] { new Slot { BlockId = (short)oldState.Id, ItemCount = 1 } }.AsImmutable());
             }
         }
 
@@ -210,19 +216,107 @@ namespace MineCase.Server.Game.Entities
                                select c.CollectBy(this));
         }
 
-        public async Task<bool> Collect(uint collectedEntityId, Slot item)
+        public async Task<Slot> Collect(uint collectedEntityId, Slot item)
         {
-            await GetBroadcastGenerator().CollectItem(collectedEntityId, EntityId, item.ItemCount);
-            return await _inventory.AddItem(item);
+            var after = await _inventory.DistributeStack(this, item);
+            if (item.ItemCount != after.ItemCount)
+                await GetBroadcastGenerator().CollectItem(collectedEntityId, EntityId, (byte)(item.ItemCount - after.ItemCount));
+            return after;
         }
 
-        public Task PlaceBlock(Position location, EntityInteractHand hand, PlayerDiggingFace face, Vector3 cursorPosition)
+        public async Task PlaceBlock(Position location, EntityInteractHand hand, PlayerDiggingFace face, Vector3 cursorPosition)
         {
-            return Task.CompletedTask;
+            if (face != PlayerDiggingFace.Special)
+            {
+                var blockState = await World.GetBlockState(GrainFactory, location);
+                var blockHandler = BlockHandler.Create((BlockId)blockState.Id);
+                if (blockHandler.IsUsable)
+                {
+                    await blockHandler.UseBy(this, GrainFactory, World, location, cursorPosition);
+                }
+                else
+                {
+                    var slotIndex = await _inventory.GetHotbarGlobalIndex(this, _heldSlot);
+                    var slot = await _inventory.GetSlot(this, slotIndex);
+                    if (!slot.IsEmpty)
+                    {
+                        var itemHandler = ItemHandler.Create((uint)slot.BlockId);
+                        if (itemHandler.IsPlaceable)
+                            await itemHandler.PlaceBy(this, GrainFactory, World, location, _inventory, slotIndex, face, cursorPosition);
+                    }
+                }
+            }
         }
 
         public Task SetHeldItem(short slot)
         {
+            _heldSlot = slot;
+            return Task.CompletedTask;
+        }
+
+        public Task<Slot> GetInventorySlot(int index)
+        {
+            return Task.FromResult(_inventorySlots[index]);
+        }
+
+        public Task SetInventorySlot(int index, Slot slot)
+        {
+            _inventorySlots[index] = slot;
+            return Task.CompletedTask;
+        }
+
+        public Task<byte> GetWindowId(IWindow window)
+        {
+            return Task.FromResult(_windows.First(o => o.Value.Window.GetPrimaryKey() == window.GetPrimaryKey()).Key);
+        }
+
+        private WindowContext GetWindow(byte windowId)
+        {
+            return _windows[windowId];
+        }
+
+        public async Task SetDraggedSlot(Slot item)
+        {
+            _draggedSlot = item;
+            await _generator.SetSlot(0xFF, -1, item);
+        }
+
+        public Task<Slot> GetDraggedSlot() => Task.FromResult(_draggedSlot);
+
+        public async Task ClickWindow(byte windowId, short slot, ClickAction clickAction, short actionNumber, Slot clickedItem)
+        {
+            var window = GetWindow(windowId);
+            await window.Window.Click(this, slot, clickAction, clickedItem);
+            await _generator.ConfirmTransaction(windowId, window.ActionNumber++, true);
+        }
+
+        public async Task CloseWindow(byte windowId)
+        {
+            await GetWindow(windowId).Window.Close(this);
+            if (windowId != 0)
+                _windows.Remove(windowId);
+        }
+
+        public Task OpenWindow(IWindow window)
+        {
+            var id = (from w in _windows
+                      where w.Value.Window.GetPrimaryKey() == window.GetPrimaryKey()
+                      select (byte?)w.Key).FirstOrDefault();
+            if (id == null)
+            {
+                for (byte i = 1; i <= byte.MaxValue; i++)
+                {
+                    if (!_windows.ContainsKey(i))
+                    {
+                        id = i;
+                        _windows.Add(i, new WindowContext { Window = window });
+                        break;
+                    }
+                }
+            }
+
+            if (id != null)
+                window.OpenWindow(this).Ignore();
             return Task.CompletedTask;
         }
 
@@ -231,5 +325,16 @@ namespace MineCase.Server.Game.Entities
             _isOnGround = state;
             return Task.CompletedTask;
         }
+
+        public Task<(float pitch, float yaw)> GetLook()
+        {
+            return Task.FromResult((_pitch, _yaw));
+        }
+
+        private class WindowContext
+        {
+            public IWindow Window;
+            public short ActionNumber;
+        }
     }
 }

+ 41 - 0
src/MineCase.Server.Grains/Game/FurnaceRecipesGrain.cs

@@ -0,0 +1,41 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading.Tasks;
+using Microsoft.Extensions.FileProviders;
+using MineCase.Algorithm;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game
+{
+    [StatelessWorker]
+    internal class FurnaceRecipesGrain : Grain, IFurnaceRecipes
+    {
+        private const string _recipesFileName = "furnace.txt";
+
+        private FurnaceRecipeMatcher _recipeMatcher;
+        private IFileProvider _fileProvider;
+
+        public FurnaceRecipesGrain()
+        {
+            _fileProvider = new PhysicalFileProvider(AppContext.BaseDirectory);
+        }
+
+        public override async Task OnActivateAsync()
+        {
+            var file = _fileProvider.GetFileInfo(_recipesFileName);
+
+            var recipeLoader = new FurnaceRecipeLoader();
+            using (var sr = new StreamReader(file.CreateReadStream()))
+                await recipeLoader.LoadRecipes(sr);
+            _recipeMatcher = new FurnaceRecipeMatcher(recipeLoader.Recipes, recipeLoader.Fuels);
+        }
+
+        public Task<FindFurnaceRecipeResult> FindRecipe(Slot input, Slot fuel)
+        {
+            return Task.FromResult(_recipeMatcher.FindRecipe(input, fuel));
+        }
+    }
+}

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

@@ -12,6 +12,8 @@ namespace MineCase.Server.Game
             builder.RegisterType<GameSession>();
             builder.RegisterType<ChunkSenderGrain>();
             builder.RegisterType<ChunkSenderJobWorker>();
+            builder.RegisterType<CraftingRecipesGrain>();
+            builder.RegisterType<FurnaceRecipesGrain>();
         }
     }
 }

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

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Protocol.Play;
 using MineCase.Server.Network.Play;
 using MineCase.Server.User;
@@ -21,11 +21,14 @@ namespace MineCase.Server.Game
         private IDisposable _gameTick;
         private DateTime _lastGameTickTime;
 
+        private HashSet<ITickable> _tickables;
+
         public override async Task OnActivateAsync()
         {
             _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));
         }
 
@@ -92,9 +95,12 @@ namespace MineCase.Server.Game
             var deltaTime = now - _lastGameTickTime;
             _lastGameTickTime = now;
 
+            var worldAge = await _world.GetAge();
             await _world.OnGameTick(deltaTime);
             await Task.WhenAll(from u in _users.Keys
                                select u.OnGameTick(deltaTime));
+            await Task.WhenAll(from u in _tickables
+                               select u.OnGameTick(deltaTime, worldAge));
         }
 
         private Task<Chat> CreateStandardChatMessage(string name, string message)
@@ -116,6 +122,18 @@ 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; }

+ 62 - 0
src/MineCase.Server.Grains/Game/Items/ChestItemHandler.cs

@@ -0,0 +1,62 @@
+using System;
+using System.Collections.Generic;
+using System.Numerics;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.Blocks;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.Items
+{
+    [ItemHandler(BlockId.Chest)]
+    public class ChestItemHandler : ItemHandler
+    {
+        public override bool IsUsable => false;
+
+        public override bool IsPlaceable => true;
+
+        public ChestItemHandler(uint itemId)
+            : base(itemId)
+        {
+        }
+
+        protected override async Task<BlockState> ConvertToBlock(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos position, Slot slot)
+        {
+            int neighborIdx = -1;
+            for (int i = 0; i < ChestBlockHandler.CrossCoords.Length; i++)
+            {
+                var crossCoord = ChestBlockHandler.CrossCoords[i];
+                var blockState = await world.GetBlockState(grainFactory, position.X + crossCoord.x, position.Y, position.Z + crossCoord.z);
+                if (blockState.Id == (uint)BlockId.Chest)
+                {
+                    neighborIdx = i;
+                    break;
+                }
+            }
+
+            var yaw = (await player.GetLook()).yaw;
+            FacingDirectionType facing;
+            switch (neighborIdx)
+            {
+                case 0:
+                case 2:
+                    // The neighbor is in the X axis, form a X-axis-aligned dblchest:
+                    facing = ((yaw >= -90) && (yaw < 90)) ? FacingDirectionType.FacingNorth : FacingDirectionType.FacingSouth;
+                    break;
+                case 1:
+                case 3:
+                    // The neighbor is in the Z axis, form a Z-axis-aligned dblchest:
+                    facing = (yaw < 0) ? FacingDirectionType.FacingWest : FacingDirectionType.FacingEast;
+                    break;
+                default:
+                    facing = ChestBlockHandler.PlayerYawToFacing(yaw);
+                    break;
+            }
+
+            return BlockStates.Chest(facing);
+        }
+    }
+}

+ 18 - 0
src/MineCase.Server.Grains/Game/Items/DefaultItemHandler.cs

@@ -0,0 +1,18 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Server.Game.Items
+{
+    public class DefaultItemHandler : ItemHandler
+    {
+        public DefaultItemHandler()
+            : base(0)
+        {
+        }
+
+        public override bool IsUsable => false;
+
+        public override bool IsPlaceable => true;
+    }
+}

+ 137 - 0
src/MineCase.Server.Grains/Game/Items/ItemHandler.cs

@@ -0,0 +1,137 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+using System.Numerics;
+using System.Reflection;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.Blocks;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows;
+using MineCase.Server.Network.Play;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.Items
+{
+    public abstract class ItemHandler
+    {
+        public uint ItemId { get; }
+
+        public abstract bool IsUsable { get; }
+
+        public abstract bool IsPlaceable { get; }
+
+        public ItemHandler(uint itemId)
+        {
+            ItemId = itemId;
+        }
+
+        private static readonly Dictionary<uint, Type> _itemHandlerTypes;
+        private static readonly ConcurrentDictionary<uint, ItemHandler> _itemHandlers = new ConcurrentDictionary<uint, ItemHandler>();
+        private static readonly ItemHandler _defaultItemHandler = new DefaultItemHandler();
+
+        static ItemHandler()
+        {
+            _itemHandlerTypes = (from t in typeof(ItemHandler).Assembly.DefinedTypes
+                                 where !t.IsAbstract && t.IsSubclassOf(typeof(ItemHandler))
+                                 let attrs = t.GetCustomAttributes<ItemHandlerAttribute>()
+                                 from attr in attrs
+                                 select new
+                                 {
+                                     ItemId = attr.ItemId,
+                                     Type = t
+                                 }).ToDictionary(o => o.ItemId, o => o.Type.AsType());
+        }
+
+        public static ItemHandler Create(uint itemId)
+        {
+            if (_itemHandlerTypes.TryGetValue(itemId, out var type))
+                return _itemHandlers.GetOrAdd(itemId, k => (ItemHandler)Activator.CreateInstance(type, k));
+            return _defaultItemHandler;
+        }
+
+        public virtual async Task<bool> PlaceBy(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos position, IInventoryWindow inventoryWindow, int slotIndex, PlayerDiggingFace face, Vector3 cursorPosition)
+        {
+            if (IsPlaceable)
+            {
+                AddFace(ref position, face);
+                var blockState = await world.GetBlockState(grainFactory, position);
+                if ((BlockId)blockState.Id == BlockId.Air)
+                {
+                    var slot = await inventoryWindow.GetSlot(player, slotIndex);
+                    if (!slot.IsEmpty)
+                    {
+                        var newState = await ConvertToBlock(player, grainFactory, world, position, slot);
+                        var blockHandler = BlockHandler.Create((BlockId)newState.Id);
+                        if (await blockHandler.CanBeAt(position, grainFactory, world))
+                        {
+                            var chunk = position.GetChunk();
+                            await world.SetBlockState(grainFactory, position, newState);
+
+                            slot.ItemCount--;
+                            slot.MakeEmptyIfZero();
+                            await inventoryWindow.SetSlot(player, slotIndex, slot);
+
+                            await blockHandler.OnPlaced(player, grainFactory, world, position, newState);
+                            return true;
+                        }
+                    }
+                }
+            }
+
+            return false;
+        }
+
+        protected virtual Task<BlockState> ConvertToBlock(IPlayer player, IGrainFactory grainFactory, IWorld world, BlockWorldPos position, Slot slot)
+        {
+            return Task.FromResult(new BlockState { Id = (uint)slot.BlockId, MetaValue = (uint)slot.ItemDamage });
+        }
+
+        private void AddFace(ref BlockWorldPos location, PlayerDiggingFace face, bool inverse = false)
+        {
+            switch (face)
+            {
+                case PlayerDiggingFace.Bottom:
+                    location.Y--;
+                    break;
+                case PlayerDiggingFace.Top:
+                    location.Y++;
+                    break;
+                case PlayerDiggingFace.North:
+                    location.Z--;
+                    break;
+                case PlayerDiggingFace.South:
+                    location.Z++;
+                    break;
+                case PlayerDiggingFace.West:
+                    location.X--;
+                    break;
+                case PlayerDiggingFace.East:
+                    location.X++;
+                    break;
+                case PlayerDiggingFace.Special:
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(face));
+            }
+        }
+    }
+
+    [AttributeUsage(AttributeTargets.Class, Inherited = false, AllowMultiple = true)]
+    public sealed class ItemHandlerAttribute : Attribute
+    {
+        public uint ItemId { get; }
+
+        public ItemHandlerAttribute(BlockId itemId)
+        {
+            ItemId = (uint)itemId;
+        }
+
+        public ItemHandlerAttribute(ItemId itemId)
+        {
+            ItemId = (uint)itemId;
+        }
+    }
+}

+ 35 - 0
src/MineCase.Server.Grains/Game/Windows/ChestWindowGrain.cs

@@ -0,0 +1,35 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using MineCase.Server.Game.Windows.SlotAreas;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.Windows
+{
+    internal class ChestWindowGrain : WindowGrain, IChestWindow
+    {
+        protected override string WindowType => "minecraft:chest";
+
+        private Chat _title;
+
+        protected override Chat Title => _title;
+
+        public Task SetEntities(Immutable<IChestBlockEntity[]> entities)
+        {
+            SlotAreas.Clear();
+
+            foreach (var entity in entities.Value)
+                SlotAreas.Add(new ChestSlotArea(entity, this, GrainFactory));
+            SlotAreas.Add(new InventorySlotArea(this, GrainFactory));
+            SlotAreas.Add(new HotbarSlotArea(this, GrainFactory));
+
+            if (entities.Value.Length < 2)
+                _title = new Chat("Chest");
+            else
+                _title = new Chat("Large chest");
+            return Task.CompletedTask;
+        }
+    }
+}

+ 24 - 0
src/MineCase.Server.Grains/Game/Windows/CraftingWindowGrain.cs

@@ -0,0 +1,24 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.Windows.SlotAreas;
+
+namespace MineCase.Server.Game.Windows
+{
+    internal class CraftingWindowGrain : WindowGrain, ICraftingWindow
+    {
+        protected override string WindowType => "minecraft:crafting_table";
+
+        protected override Chat Title { get; } = new Chat("Crafting Table");
+
+        public override Task OnActivateAsync()
+        {
+            SlotAreas.Add(new CraftingSlotArea(3, this, GrainFactory));
+            SlotAreas.Add(new InventorySlotArea(this, GrainFactory));
+            SlotAreas.Add(new HotbarSlotArea(this, GrainFactory));
+
+            return base.OnActivateAsync();
+        }
+    }
+}

+ 42 - 0
src/MineCase.Server.Grains/Game/Windows/FurnaceWindowGrain.cs

@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using MineCase.Server.Game.Windows.SlotAreas;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.Windows
+{
+    internal class FurnaceWindowGrain : WindowGrain, IFurnaceWindow
+    {
+        protected override string WindowType => "minecraft:furnace";
+
+        protected override Chat Title { get; } = new Chat("Furnace");
+
+        private IFurnaceBlockEntity _furnaceEntity;
+
+        public async Task OnGameTick(TimeSpan deltaTime, long worldAge)
+        {
+            if (worldAge % 100 == 0)
+            {
+                var properties = await _furnaceEntity.GetCookingState();
+                await BroadcastWindowProperty(0, (short)properties.fuelLeft);
+                await BroadcastWindowProperty(1, (short)properties.maxFuelTime);
+                await BroadcastWindowProperty(2, (short)properties.cookProgress);
+                await BroadcastWindowProperty(3, (short)properties.maxProgress);
+            }
+        }
+
+        public Task SetEntity(IFurnaceBlockEntity furnaceEntity)
+        {
+            SlotAreas.Clear();
+
+            SlotAreas.Add(new FurnaceSlotArea(furnaceEntity, this, GrainFactory));
+            SlotAreas.Add(new InventorySlotArea(this, GrainFactory));
+            SlotAreas.Add(new HotbarSlotArea(this, GrainFactory));
+            _furnaceEntity = furnaceEntity;
+            return Task.CompletedTask;
+        }
+    }
+}

+ 32 - 26
src/MineCase.Server.Grains/Game/Windows/InventoryWindowGrain.cs

@@ -2,7 +2,9 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows.SlotAreas;
 using MineCase.Server.Network.Play;
 using MineCase.Server.User;
 using Orleans;
@@ -11,36 +13,40 @@ namespace MineCase.Server.Game.Windows
 {
     internal class InventoryWindowGrain : WindowGrain, IInventoryWindow
     {
-        private IUser _user;
-        private ClientPlayPacketGenerator _generator;
+        protected override string WindowType => string.Empty;
+
+        protected override Chat Title { get; } = new Chat("Inventory");
+
+        public override Task OnActivateAsync()
+        {
+            SlotAreas.Add(new CraftingSlotArea(2, this, GrainFactory));
+            SlotAreas.Add(new ArmorSlotArea(this, GrainFactory));
+            SlotAreas.Add(new InventorySlotArea(this, GrainFactory));
+            SlotAreas.Add(new HotbarSlotArea(this, GrainFactory));
+            SlotAreas.Add(new OffhandSlotArea(this, GrainFactory));
+
+            return base.OnActivateAsync();
+        }
+
+        public override Task<Slot> DistributeStack(IPlayer player, Slot item)
+        {
+            return DistributeStack(player, new[] { SlotAreas[3], SlotAreas[2] }, item, false);
+        }
+
+        public Task UseItem(IPlayer player, int slotIndex)
+        {
+            var slotArea = GlobalSlotIndexToLocal(slotIndex);
+            return slotArea.slotArea.TryUseItem(player, slotArea.slotIndex);
+        }
 
-        public async Task<bool> AddItem(Slot item)
+        public Task<Slot> GetHotbarItem(IPlayer player, int slotIndex)
         {
-            int index = -1;
-            for (int i = 0; i < Slots.Count; i++)
-            {
-                if (Slots[i].BlockId == item.BlockId)
-                {
-                    index = i;
-                    Slots[i].ItemCount += item.ItemCount;
-                    break;
-                }
-            }
-
-            if (index == -1)
-            {
-                Slots.Add(item);
-                index = Slots.Count - 1;
-            }
-
-            await _generator.SetSlot(0, (short)(index + 36), Slots[index]);
-            return true;
+            return SlotAreas[3].GetSlot(player, slotIndex);
         }
 
-        public async Task SetUser(IUser user)
+        public Task<int> GetHotbarGlobalIndex(IPlayer player, int slotIndex)
         {
-            _user = user;
-            _generator = new ClientPlayPacketGenerator(await user.GetClientPacketSink());
+            return Task.FromResult(LocalSlotIndexToGlobal(SlotAreas[3], slotIndex));
         }
     }
 }

+ 15 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/ArmorSlotArea.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Orleans;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal class ArmorSlotArea : InventorySlotAreaBase
+    {
+        public ArmorSlotArea(WindowGrain window, IGrainFactory grainFactory)
+            : base(ArmorSlotsCount, ArmorOffsetInContainer, window, grainFactory)
+        {
+        }
+    }
+}

+ 34 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/ChestSlotArea.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using MineCase.Server.Game.Entities;
+using Orleans;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal class ChestSlotArea : SlotArea
+    {
+        public const int ChestSlotsCount = 9 * 3;
+
+        private readonly IChestBlockEntity _chestEntity;
+
+        public ChestSlotArea(IChestBlockEntity chestEntity, WindowGrain window, IGrainFactory grainFactory)
+            : base(ChestSlotsCount, window, grainFactory)
+        {
+            _chestEntity = chestEntity;
+        }
+
+        public override Task<Slot> GetSlot(IPlayer player, int slotIndex)
+        {
+            return _chestEntity.GetSlot(slotIndex);
+        }
+
+        public override async Task SetSlot(IPlayer player, int slotIndex, Slot slot)
+        {
+            await _chestEntity.SetSlot(slotIndex, slot);
+            await BroadcastSlotChanged(slotIndex, slot);
+        }
+    }
+}

+ 112 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/CraftingSlotArea.cs

@@ -0,0 +1,112 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using MineCase.Server.Game.Entities;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal class CraftingSlotArea : TemporarySlotArea
+    {
+        private readonly int _gridSize;
+        private Slot[,] _afterSlots;
+
+        public CraftingSlotArea(int gridSize, WindowGrain window, IGrainFactory grainFactory)
+            : base(gridSize * gridSize + 1, window, grainFactory)
+        {
+            _gridSize = gridSize;
+        }
+
+        public override async Task Click(IPlayer player, int slotIndex, ClickAction clickAction, Slot clickedItem)
+        {
+            if (slotIndex == 0)
+            {
+                var draggedSlot = await player.GetDraggedSlot();
+                var result = await GetSlot(player, 0);
+                bool taken = false;
+                switch (clickAction)
+                {
+                    case ClickAction.LeftMouseClick:
+                    case ClickAction.RightMouseClick:
+                        if (draggedSlot.IsEmpty)
+                        {
+                            await player.SetDraggedSlot(result);
+                            taken = true;
+                        }
+                        else if (draggedSlot.CanStack(result) && draggedSlot.ItemCount + result.ItemCount <= MaxStackCount)
+                        {
+                            draggedSlot.ItemCount += result.ItemCount;
+                            await player.SetDraggedSlot(draggedSlot);
+                            taken = true;
+                        }
+
+                        break;
+                    default:
+                        break;
+                }
+
+                if (taken)
+                {
+                    await SetSlot(player, 0, Slot.Empty);
+                    await SetCraftingGrid(player, _afterSlots);
+                }
+            }
+            else
+            {
+                await base.Click(player, slotIndex, clickAction, clickedItem);
+            }
+
+            await UpdateRecipe(player);
+        }
+
+        private async Task UpdateRecipe(IPlayer player)
+        {
+            var grid = GetCraftingGrid(player);
+            var recipe = await GrainFactory.GetGrain<ICraftingRecipes>(0).FindRecipe(grid.AsImmutable());
+            if (recipe != null)
+            {
+                _afterSlots = recipe.AfterTake;
+                await SetSlot(player, 0, recipe.Result);
+            }
+            else
+            {
+                _afterSlots = null;
+                await SetSlot(player, 0, Slot.Empty);
+            }
+        }
+
+        private Slot[,] GetCraftingGrid(IPlayer player)
+        {
+            var grid = new Slot[_gridSize, _gridSize];
+
+            int x = 0, y = 0;
+            foreach (var slot in GetSlots(player).Skip(1))
+            {
+                grid[x++, y] = slot;
+                if (x == _gridSize)
+                {
+                    x = 0;
+                    y++;
+                }
+            }
+
+            return grid;
+        }
+
+        private async Task SetCraftingGrid(IPlayer player, Slot[,] afterSlots)
+        {
+            int index = 1;
+            for (int y = 0; y < afterSlots.GetUpperBound(1) + 1; y++)
+            {
+                for (int x = 0; x < afterSlots.GetUpperBound(0) + 1; x++)
+                {
+                    await SetSlot(player, index++, afterSlots[x, y]);
+                }
+            }
+        }
+    }
+}

+ 34 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/FurnaceSlotArea.cs

@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using MineCase.Server.Game.Entities;
+using Orleans;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal class FurnaceSlotArea : SlotArea
+    {
+        public const int FurnaceSlotsCount = 3;
+
+        private readonly IFurnaceBlockEntity _furnaceEntity;
+
+        public FurnaceSlotArea(IFurnaceBlockEntity furnaceEntity, WindowGrain window, IGrainFactory grainFactory)
+            : base(FurnaceSlotsCount, window, grainFactory)
+        {
+            _furnaceEntity = furnaceEntity;
+        }
+
+        public override Task<Slot> GetSlot(IPlayer player, int slotIndex)
+        {
+            return _furnaceEntity.GetSlot(slotIndex);
+        }
+
+        public override async Task SetSlot(IPlayer player, int slotIndex, Slot slot)
+        {
+            await _furnaceEntity.SetSlot(slotIndex, slot);
+            await BroadcastSlotChanged(slotIndex, slot);
+        }
+    }
+}

+ 15 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/HotbarSlotArea.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Orleans;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal class HotbarSlotArea : InventorySlotAreaBase
+    {
+        public HotbarSlotArea(WindowGrain window, IGrainFactory grainFactory)
+            : base(HotbarSlotsCount, HotbarOffsetInContainer, window, grainFactory)
+        {
+        }
+    }
+}

+ 15 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/InventorySlotArea.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Orleans;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal class InventorySlotArea : InventorySlotAreaBase
+    {
+        public InventorySlotArea(WindowGrain window, IGrainFactory grainFactory)
+            : base(InventorySlotsCount, InventoryOffsetInContainer, window, grainFactory)
+        {
+        }
+    }
+}

+ 33 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/InventorySlotAreaBase.cs

@@ -0,0 +1,33 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+using MineCase.Server.Game.Entities;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal abstract class InventorySlotAreaBase : SlotArea
+    {
+        protected int OffsetInContainer { get; }
+
+        public InventorySlotAreaBase(int slotsCount, int offsetInContainer, WindowGrain window, IGrainFactory grainFactory)
+            : base(slotsCount, window, grainFactory)
+        {
+            OffsetInContainer = offsetInContainer;
+        }
+
+        public override Task<Slot> GetSlot(IPlayer player, int slotIndex)
+        {
+            return player.GetInventorySlot(slotIndex + OffsetInContainer);
+        }
+
+        public override async Task SetSlot(IPlayer player, int slotIndex, Slot slot)
+        {
+            await player.SetInventorySlot(slotIndex + OffsetInContainer, slot);
+            await NotifySlotChanged(player, slotIndex, slot);
+        }
+    }
+}

+ 15 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/OffhandSlotArea.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Orleans;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal class OffhandSlotArea : InventorySlotAreaBase
+    {
+        public OffhandSlotArea(WindowGrain window, IGrainFactory grainFactory)
+            : base(OffhandSlotsCount, OffhandOffsetInContainer, window, grainFactory)
+        {
+        }
+    }
+}

+ 171 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/SlotArea.cs

@@ -0,0 +1,171 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+
+using MineCase.Server.Game.Entities;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal abstract class SlotArea
+    {
+        public const int ArmorSlotsCount = 4;
+        public const int InventorySlotsCount = 27;
+        public const int HotbarSlotsCount = 9;
+        public const int OffhandSlotsCount = 1;
+
+        public const int UserSlotsCount = ArmorSlotsCount + InventorySlotsCount + HotbarSlotsCount + OffhandSlotsCount;
+
+        public const int ArmorOffsetInContainer = 0;
+        public const int InventoryOffsetInContainer = ArmorOffsetInContainer + ArmorSlotsCount;
+        public const int HotbarOffsetInContainer = InventoryOffsetInContainer + InventorySlotsCount;
+        public const int OffhandOffsetInContainer = HotbarOffsetInContainer + HotbarSlotsCount;
+
+        public int SlotsCount { get; }
+
+        protected WindowGrain Window { get; }
+
+        protected IGrainFactory GrainFactory { get; }
+
+        public SlotArea(int slotsCount, WindowGrain window, IGrainFactory grainFactory)
+        {
+            SlotsCount = slotsCount;
+            Window = window;
+            GrainFactory = grainFactory;
+        }
+
+        public abstract Task<Slot> GetSlot(IPlayer player, int slotIndex);
+
+        public abstract Task SetSlot(IPlayer player, int slotIndex, Slot slot);
+
+        protected const byte MaxStackCount = 64;
+
+        public virtual async Task<Slot> DistributeStack(IPlayer player, Slot item, bool canUseEmptySlot, bool fillFromBack)
+        {
+            for (int i = 0; i < SlotsCount && !item.IsEmpty; i++)
+            {
+                int slotIndex = fillFromBack ? (SlotsCount - 1 - i) : i;
+
+                var targetSlot = await GetSlot(player, slotIndex);
+                if (canUseEmptySlot && targetSlot.IsEmpty)
+                {
+                    await SetSlot(player, slotIndex, item);
+                    return Slot.Empty;
+                }
+                else if (TryStackSlot(ref item, ref targetSlot))
+                {
+                    await SetSlot(player, slotIndex, targetSlot);
+                }
+            }
+
+            return item;
+        }
+
+        public virtual async Task TryUseItem(IPlayer player, int slotIndex)
+        {
+            var slot = await GetSlot(player, slotIndex);
+            if (!slot.IsEmpty && slot.ItemCount >= 1)
+            {
+                slot.ItemCount--;
+                slot.MakeEmptyIfZero();
+                await SetSlot(player, slotIndex, slot);
+            }
+        }
+
+        protected Task NotifySlotChanged(IPlayer player, int slotIndex, Slot item)
+        {
+            return Window.NotifySlotChanged(this, player, slotIndex, item);
+        }
+
+        protected Task BroadcastSlotChanged(int slotIndex, Slot item)
+        {
+            return Window.BroadcastSlotChanged(this, slotIndex, item);
+        }
+
+        public virtual async Task Click(IPlayer player, int slotIndex, ClickAction clickAction, Slot clickedItem)
+        {
+            var slot = await GetSlot(player, slotIndex);
+            var draggedSlot = await player.GetDraggedSlot();
+            switch (clickAction)
+            {
+                case ClickAction.LeftMouseClick:
+                    if (draggedSlot.IsEmpty || !draggedSlot.CanStack(slot))
+                    {
+                        // 交换
+                        await SetSlot(player, slotIndex, draggedSlot);
+                        await player.SetDraggedSlot(slot);
+                    }
+                    else if (TryStackSlot(ref draggedSlot, ref slot))
+                    {
+                        // 堆叠到最大
+                        await SetSlot(player, slotIndex, slot);
+                        await player.SetDraggedSlot(draggedSlot);
+                    }
+
+                    break;
+                case ClickAction.RightMouseClick:
+                    if (draggedSlot.IsEmpty)
+                    {
+                        // 取一半
+                        if (!slot.IsEmpty)
+                        {
+                            var takeCount = (byte)Math.Ceiling(slot.ItemCount / 2.0f);
+                            draggedSlot = slot.WithItemCount(takeCount);
+                            slot.ItemCount -= takeCount;
+                            slot.MakeEmptyIfZero();
+
+                            await SetSlot(player, slotIndex, slot);
+                            await player.SetDraggedSlot(draggedSlot);
+                        }
+                    }
+                    else if (slot.IsEmpty || draggedSlot.CanStack(slot))
+                    {
+                        // 放一个
+                        if (draggedSlot.ItemCount > 0 && (slot.IsEmpty || slot.ItemCount < MaxStackCount))
+                        {
+                            draggedSlot.ItemCount--;
+                            if (slot.IsEmpty)
+                                slot = draggedSlot.WithItemCount(1);
+                            else
+                                slot.ItemCount++;
+                            draggedSlot.MakeEmptyIfZero();
+
+                            await SetSlot(player, slotIndex, slot);
+                            await player.SetDraggedSlot(draggedSlot);
+                        }
+                    }
+                    else
+                    {
+                        // 交换
+                        await SetSlot(player, slotIndex, draggedSlot);
+                        await player.SetDraggedSlot(slot);
+                    }
+
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        protected bool TryStackSlot(ref Slot source, ref Slot target)
+        {
+            if (target.ItemCount <= MaxStackCount && target.CanStack(source))
+            {
+                var toStack = (byte)Math.Min(source.ItemCount, MaxStackCount - target.ItemCount);
+                target.ItemCount += toStack;
+                source.ItemCount -= toStack;
+                source.MakeEmptyIfZero();
+                return true;
+            }
+
+            return false;
+        }
+
+        public virtual Task Close(IPlayer player)
+        {
+            return Task.CompletedTask;
+        }
+    }
+}

+ 69 - 0
src/MineCase.Server.Grains/Game/Windows/SlotAreas/TemporarySlotArea.cs

@@ -0,0 +1,69 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+using MineCase.Server.Game.Entities;
+using MineCase.Server.World;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.Windows.SlotAreas
+{
+    internal abstract class TemporarySlotArea : SlotArea
+    {
+        private readonly Dictionary<IPlayer, Slot[]> _tempSlotsMap = new Dictionary<IPlayer, Slot[]>();
+
+        public TemporarySlotArea(int slotsCount, WindowGrain window, IGrainFactory grainFactory)
+            : base(slotsCount, window, grainFactory)
+        {
+        }
+
+        public override Task<Slot> GetSlot(IPlayer player, int slotIndex)
+        {
+            return Task.FromResult(GetSlots(player)[slotIndex]);
+        }
+
+        public override Task SetSlot(IPlayer player, int slotIndex, Slot slot)
+        {
+            GetSlots(player)[slotIndex] = slot;
+            return NotifySlotChanged(player, slotIndex, slot);
+        }
+
+        protected Slot[] GetSlots(IPlayer player)
+        {
+            if (!_tempSlotsMap.TryGetValue(player, out var slots))
+            {
+                slots = Enumerable.Repeat(Slot.Empty, SlotsCount).ToArray();
+                _tempSlotsMap.Add(player, slots);
+            }
+
+            return slots;
+        }
+
+        public override async Task Close(IPlayer player)
+        {
+            var slots = GetSlots(player);
+            var items = (from s in slots.Skip(1)
+                         where !s.IsEmpty
+                         select s).ToArray();
+            if (items.Length != 0)
+            {
+                var position = await player.GetPosition();
+                var chunk = await player.GetChunkPosition();
+                var world = GrainFactory.GetGrain<IWorld>(player.GetWorldAndEntityId().worldKey);
+
+                // 产生 Pickup
+                var finder = GrainFactory.GetGrain<ICollectableFinder>(world.MakeCollectableFinderKey(chunk.x, chunk.z));
+                await finder.SpawnPickup(
+                    new Position { X = (int)position.X, Y = (int)position.Y, Z = (int)position.Z },
+                    items.AsImmutable());
+                _tempSlotsMap.Remove(player);
+
+                for (int i = 0; i < slots.Length; i++)
+                    await NotifySlotChanged(player, i, Slot.Empty);
+            }
+        }
+    }
+}

+ 190 - 3
src/MineCase.Server.Grains/Game/Windows/WindowGrain.cs

@@ -1,8 +1,14 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+using Microsoft.Extensions.DependencyInjection;
+using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows.SlotAreas;
+using MineCase.Server.Network;
+using MineCase.Server.Network.Play;
+using MineCase.Server.World;
 using Orleans;
 
 namespace MineCase.Server.Game.Windows
@@ -11,14 +17,195 @@ namespace MineCase.Server.Game.Windows
     {
         protected List<Slot> Slots { get; } = new List<Slot>();
 
+        protected List<SlotArea> SlotAreas { get; } = new List<SlotArea>();
+
+        protected IWorld World { get; private set; }
+
+        protected abstract string WindowType { get; }
+
+        protected abstract Chat Title { get; }
+
+        protected virtual byte? EntityId => null;
+
+        private Dictionary<IPlayer, IClientboundPacketSink> _players;
+
+        public override Task OnActivateAsync()
+        {
+            _players = new Dictionary<IPlayer, IClientboundPacketSink>();
+            return base.OnActivateAsync();
+        }
+
         public Task<uint> GetSlotCount()
         {
             return Task.FromResult((uint)Slots.Count);
         }
 
-        public Task<IReadOnlyList<Slot>> GetSlots()
+        public async Task<IReadOnlyList<Slot>> GetSlots(IPlayer player)
+        {
+            var slots = new List<Slot>();
+            foreach (var slotArea in SlotAreas)
+            {
+                for (int i = 0; i < slotArea.SlotsCount; i++)
+                    slots.Add(await slotArea.GetSlot(player, i));
+            }
+
+            return slots;
+        }
+
+        internal async Task NotifySlotChanged(SlotArea slotArea, IPlayer player, int slotIndex, Slot item)
+        {
+            new ClientPlayPacketGenerator(await (await player.GetUser()).GetClientPacketSink())
+                .SetSlot(await player.GetWindowId(this), (short)LocalSlotIndexToGlobal(slotArea, slotIndex), item).Ignore();
+        }
+
+        internal Task BroadcastSlotChanged(SlotArea slotArea, int slotIndex, Slot item)
+        {
+            var globalIndex = (short)LocalSlotIndexToGlobal(slotArea, slotIndex);
+            async Task SendSetSlot(IPlayer player, IClientboundPacketSink sink)
+            {
+                var id = await player.GetWindowId(this);
+                await new ClientPlayPacketGenerator(sink).SetSlot(id, globalIndex, item);
+            }
+
+            Task.WhenAll(from p in _players select SendSetSlot(p.Key, p.Value)).Ignore();
+            return Task.CompletedTask;
+        }
+
+        protected int LocalSlotIndexToGlobal(SlotArea slotArea, int slotIndex)
+        {
+            for (int i = 0; i < SlotAreas.Count; i++)
+            {
+                if (SlotAreas[i] == slotArea)
+                    break;
+                slotIndex += SlotAreas[i].SlotsCount;
+            }
+
+            return slotIndex;
+        }
+
+        protected (SlotArea slotArea, int slotIndex) GlobalSlotIndexToLocal(int slotIndex)
+        {
+            for (int i = 0; i < SlotAreas.Count; i++)
+            {
+                if (slotIndex < SlotAreas[i].SlotsCount)
+                    return (SlotAreas[i], slotIndex);
+                slotIndex -= SlotAreas[i].SlotsCount;
+            }
+
+            throw new ArgumentOutOfRangeException(nameof(slotIndex));
+        }
+
+        public virtual Task<Slot> DistributeStack(IPlayer player, Slot item)
+        {
+            return DistributeStack(player, SlotAreas, item, false);
+        }
+
+        protected async Task<Slot> DistributeStack(IPlayer player, IReadOnlyList<SlotArea> slotAreas, Slot item, bool fillFromBack)
+        {
+            // 先使用已有的 Slot,再使用空 Slot
+            for (int pass = 0; pass < 2; pass++)
+            {
+                foreach (var slotArea in slotAreas)
+                {
+                    item = await slotArea.DistributeStack(player, item, pass == 1, fillFromBack);
+                    if (item.IsEmpty) break;
+                }
+            }
+
+            return item;
+        }
+
+        public async Task Click(IPlayer player, int slotIndex, ClickAction clickAction, Slot clickedItem)
+        {
+            switch (clickAction)
+            {
+                case ClickAction.LeftMouseClick:
+                case ClickAction.RightMouseClick:
+                    var slot = GlobalSlotIndexToLocal(slotIndex);
+                    await slot.slotArea.Click(player, slot.slotIndex, clickAction, clickedItem);
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        public async Task Close(IPlayer player)
+        {
+            foreach (var slotArea in SlotAreas)
+                await slotArea.Close(player);
+            _players.Remove(player);
+        }
+
+        private byte GetNonInventorySlotsCount()
+        {
+            byte num = 0;
+            foreach (var slotArea in SlotAreas)
+            {
+                if (slotArea is TemporarySlotArea || slotArea is InventorySlotAreaBase) continue;
+                num += (byte)slotArea.SlotsCount;
+            }
+
+            return num;
+        }
+
+        public async Task OpenWindow(IPlayer player)
+        {
+            var slots = await GetSlots(player);
+            var sink = await (await player.GetUser()).GetClientPacketSink();
+            var generator = new ClientPlayPacketGenerator(sink);
+
+            var id = await player.GetWindowId(this);
+            await generator.OpenWindow(id, WindowType, Title, GetNonInventorySlotsCount(), EntityId);
+            await generator.WindowItems(id, slots);
+            _players.Add(player, sink);
+        }
+
+        public Task<Slot> GetSlot(IPlayer player, int slotIndex)
+        {
+            var area = GlobalSlotIndexToLocal(slotIndex);
+            return area.slotArea.GetSlot(player, area.slotIndex);
+        }
+
+        public Task SetSlot(IPlayer player, int slotIndex, Slot item)
+        {
+            var area = GlobalSlotIndexToLocal(slotIndex);
+            return area.slotArea.SetSlot(player, area.slotIndex, item);
+        }
+
+        public async Task Destroy()
+        {
+            async Task SendCloseWindow(IPlayer player, IClientboundPacketSink sink)
+            {
+                var id = await player.GetWindowId(this);
+                await new ClientPlayPacketGenerator(sink).CloseWindow(id);
+            }
+
+            await Task.WhenAll(from p in _players select SendCloseWindow(p.Key, p.Value));
+            await Task.WhenAll(from p in _players.Keys select Close(p));
+        }
+
+        public Task BroadcastSlotChanged(int slotIndex, Slot item)
+        {
+            async Task SendSetSlot(IPlayer player, IClientboundPacketSink sink)
+            {
+                var id = await player.GetWindowId(this);
+                await new ClientPlayPacketGenerator(sink).SetSlot(id, (short)slotIndex, item);
+            }
+
+            Task.WhenAll(from p in _players select SendSetSlot(p.Key, p.Value)).Ignore();
+            return Task.CompletedTask;
+        }
+
+        protected Task BroadcastWindowProperty(short property, short value)
         {
-            return Task.FromResult<IReadOnlyList<Slot>>(Slots);
+            async Task SendWindowProperty(IPlayer player, IClientboundPacketSink sink)
+            {
+                var id = await player.GetWindowId(this);
+                await new ClientPlayPacketGenerator(sink).WindowProperty(id, property, value);
+            }
+
+            Task.WhenAll(from p in _players select SendWindowProperty(p.Key, p.Value)).Ignore();
+            return Task.CompletedTask;
         }
     }
 }

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

@@ -10,6 +10,8 @@ namespace MineCase.Server.Game.Windows
         protected override void Load(ContainerBuilder builder)
         {
             builder.RegisterType<InventoryWindowGrain>();
+            builder.RegisterType<CraftingWindowGrain>();
+            builder.RegisterType<FurnaceWindowGrain>();
         }
     }
 }

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

@@ -5,11 +5,14 @@
     <RootNamespace>MineCase.Server</RootNamespace>
     <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
     <CodeAnalysisRuleSet>../../build/Analyzers.ruleset</CodeAnalysisRuleSet>
+    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
   </PropertyGroup>
 
   <ItemGroup>
     <PackageReference Include="Autofac" Version="4.6.1" />
     <PackageReference Include="Microsoft.Extensions.Logging" Version="2.0.0" />
+    <PackageReference Include="Microsoft.Extensions.FileProviders.Abstractions" Version="2.0.0" />
+    <PackageReference Include="Microsoft.Extensions.FileProviders.Physical" Version="2.0.0" />
     <PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="1.2.2" />
     <PackageReference Include="Microsoft.Orleans.Core" Version="2.0.0-preview2-20170724" />
     <PackageReference Include="StyleCop.Analyzers" Version="1.1.0-beta004" PrivateAssets="All" />
@@ -19,6 +22,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\MineCase.Algorithm\MineCase.Algorithm.csproj" />
+    <ProjectReference Include="..\Minecase.Core\MineCase.Core.csproj" />
     <ProjectReference Include="..\MineCase.Protocol\MineCase.Protocol.csproj" />
     <ProjectReference Include="..\MineCase.Server.Interfaces\MineCase.Server.Interfaces.csproj" />
   </ItemGroup>

+ 52 - 1
src/MineCase.Server.Grains/Network/PacketRouterGrain.Play.cs

@@ -4,7 +4,7 @@ using System.IO;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Protocol;
 using MineCase.Protocol.Play;
 using MineCase.Serialization;
@@ -95,6 +95,16 @@ namespace MineCase.Server.Network
                 case 0x20:
                     innerPacket = DeferPacket(UseItem.Deserialize(ref br));
                     break;
+
+                // Click Window
+                case 0x08:
+                    innerPacket = DeferPacket(ClickWindow.Deserialize(ref br));
+                    break;
+
+                // Close Window
+                case 0x09:
+                    innerPacket = DeferPacket(ServerboundCloseWindow.Deserialize(ref br));
+                    break;
                 default:
                     throw new InvalidDataException($"Unrecognizable packet id: 0x{packet.PacketId:X2}.");
             }
@@ -224,9 +234,50 @@ namespace MineCase.Server.Network
             return Task.CompletedTask;
         }
 
+        private async Task DispatchPacket(ClickWindow packet)
+        {
+            try
+            {
+                var player = await _user.GetPlayer();
+                player.ClickWindow(packet.WindowId, packet.Slot, ToClickAction(packet.Button, packet.Mode), packet.ActionNumber, packet.ClickedItem).Ignore();
+            }
+            catch
+            {
+            }
+        }
+
+        private async Task DispatchPacket(ServerboundCloseWindow packet)
+        {
+            var player = await _user.GetPlayer();
+            player.CloseWindow(packet.WindowId).Ignore();
+        }
+
         private Game.PlayerDiggingFace ConvertDiggingFace(Protocol.Play.PlayerDiggingFace face)
         {
             return (Game.PlayerDiggingFace)face;
         }
+
+        private ClickAction ToClickAction(byte button, uint mode)
+        {
+            switch (mode)
+            {
+                case 0:
+                    switch (button)
+                    {
+                        case 0:
+                            return ClickAction.LeftMouseClick;
+                        case 1:
+                            return ClickAction.RightMouseClick;
+                        default:
+                            break;
+                    }
+
+                    break;
+                default:
+                    break;
+            }
+
+            throw new NotSupportedException("This button-mode is not supported");
+        }
     }
 }

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

@@ -5,7 +5,7 @@ using System.Linq;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Protocol.Play;
 using MineCase.Serialization;
 using MineCase.Server.Game;
@@ -306,6 +306,46 @@ namespace MineCase.Server.Network.Play
                 BlockId = blockState.ToUInt32()
             });
         }
+
+        public Task ConfirmTransaction(byte windowId, short actionNumber, bool accepted)
+        {
+            return Sink.SendPacket(new ClientboundConfirmTransaction
+            {
+                WindowId = windowId,
+                ActionNumber = actionNumber,
+                Accepted = accepted
+            });
+        }
+
+        public Task OpenWindow(byte windowId, string windowType, Chat windowTitle, byte numberOfSlots, byte? entityId)
+        {
+            return Sink.SendPacket(new OpenWindow
+            {
+                WindowId = windowId,
+                WindowType = windowType,
+                WindowTitle = windowTitle,
+                NumberOfSlots = numberOfSlots,
+                EntityId = entityId
+            });
+        }
+
+        public Task CloseWindow(byte windowId)
+        {
+            return Sink.SendPacket(new ClientboundCloseWindow
+            {
+                WindowId = windowId
+            });
+        }
+
+        public Task WindowProperty(byte windowId, short property, short value)
+        {
+            return Sink.SendPacket(new WindowProperty
+            {
+                WindowId = windowId,
+                Property = property,
+                Value = value
+            });
+        }
     }
 
     [Flags]

+ 11 - 1
src/MineCase.Server.Grains/User/UserGrain.cs

@@ -4,9 +4,10 @@ using System.Linq;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
+using MineCase.Server.Game.Windows.SlotAreas;
 using MineCase.Server.Network;
 using MineCase.Server.Network.Play;
 using MineCase.Server.World;
@@ -39,6 +40,8 @@ namespace MineCase.Server.User
 
         private IPlayer _player;
 
+        private Slot[] _slots;
+
         public override async Task OnActivateAsync()
         {
             if (string.IsNullOrEmpty(_worldId))
@@ -50,6 +53,11 @@ namespace MineCase.Server.User
 
             _world = await GrainFactory.GetGrain<IWorldAccessor>(0).GetWorld(_worldId);
             _chunkLoader = GrainFactory.GetGrain<IUserChunkLoader>(this.GetPrimaryKey());
+            _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()
@@ -214,6 +222,8 @@ namespace MineCase.Server.User
             return _chunkLoader.SetViewDistance(viewDistance);
         }
 
+        public Task<Slot[]> GetInventorySlots() => Task.FromResult(_slots);
+
         private enum UserState : uint
         {
             None,

+ 119 - 88
src/MineCase.Server.Grains/World/ChunkColumnGrain.cs

@@ -4,139 +4,170 @@ using System.Linq;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using MineCase.Server.Game.Blocks;
 using MineCase.Server.Game.Entities;
+using MineCase.Server.Network.Play;
 using MineCase.Server.Settings;
 using MineCase.Server.World.Generation;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [Reentrant]
     internal class ChunkColumnGrain : Grain, IChunkColumn
     {
         private IWorld _world;
         private int _chunkX;
         private int _chunkZ;
 
+        private bool _generated = false;
         private ChunkColumnStorage _state;
-        private Dictionary<IPlayer, Vector3> _players;
+        private Dictionary<BlockChunkPos, IBlockEntity> _blockEntities;
 
-        public async Task AttachPlayer(IPlayer player)
+        public async Task<BlockState> GetBlockState(int x, int y, int z)
         {
-            _players.Add(player, await player.GetPosition());
+            await EnsureChunkGenerated();
+            return _state[x, y, z];
         }
 
-        public Task DetachPlayer(IPlayer player)
+        public async Task<ChunkColumnStorage> GetState()
         {
-            _players.Remove(player);
-            return Task.CompletedTask;
+            await EnsureChunkGenerated();
+            return _state;
         }
 
-        public Task<BlockState> GetBlockState(int x, int y, int z) => Task.FromResult(_state[x, y, z]);
-
-        public Task<IReadOnlyCollection<(IPlayer player, Vector3 position)>> GetPlayers() =>
-            Task.FromResult<IReadOnlyCollection<(IPlayer player, Vector3 position)>>(_players.Select(o => (o.Key, o.Value)).ToList());
+        public override Task OnActivateAsync()
+        {
+            var key = this.GetWorldAndChunkPosition();
+            _world = GrainFactory.GetGrain<IWorld>(key.worldKey);
+            _chunkX = key.x;
+            _chunkZ = key.z;
+            _blockEntities = new Dictionary<BlockChunkPos, IBlockEntity>();
+            return Task.CompletedTask;
+        }
 
-        public Task<ChunkColumnStorage> GetState() => Task.FromResult(_state);
-        /*
-        var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(1);
-        GeneratorSettings settings = new GeneratorSettings
+        public static readonly (int x, int z)[] CrossCoords = new[]
         {
-            Seed = 1,
+            (-1, 0), (0, -1), (1, 0), (0, 1)
         };
-        ChunkColumn chunkColumn = await generator.Generate(_chunkX, _chunkZ, settings);
-        return chunkColumn;
-        */
-
-        /*
-        var blocks = new Block[16 * 16 * 16];
-        var index = 0;
-        for (int y = 0; y < 16; y++)
+
+        public async Task SetBlockState(int x, int y, int z, BlockState blockState)
         {
-            for (int x = 0; x < 16; x++)
+            await EnsureChunkGenerated();
+            var oldState = _state[x, y, z];
+
+            if (oldState != blockState)
             {
-                for (int z = 0; z < 16; z++)
+                _state[x, y, z] = blockState;
+
+                var chunkPos = new BlockChunkPos(x, y, z);
+                var blockWorldPos = chunkPos.ToBlockWorldPos(new ChunkWorldPos(_chunkX, _chunkZ));
+                await GetBroadcastGenerator().BlockChange(blockWorldPos, blockState);
+
+                if (oldState.Id != blockState.Id)
                 {
-                    if (y == 0)
-                        blocks[index] = new Block { Id = 1, SkyLight = 0xF };
-                    else
-                        blocks[index] = new Block { Id = 0, SkyLight = 0xF };
-                    index++;
+                    bool replaceOld = true;
+                    var newEntity = BlockEntity.Create(GrainFactory, _world, blockWorldPos, (BlockId)blockState.Id);
+
+                    // 删除旧的 BlockEntity
+                    if (_blockEntities.TryGetValue(chunkPos, out var entity))
+                    {
+                        if (newEntity != null && entity.GetPrimaryKeyString() == newEntity.GetPrimaryKeyString())
+                            replaceOld = false;
+
+                        if (replaceOld)
+                        {
+                            await entity.Destroy();
+                            _blockEntities.Remove(chunkPos);
+                        }
+                    }
+
+                    // 添加新的 BlockEntity
+                    if (newEntity != null && replaceOld)
+                    {
+                        _blockEntities.Add(chunkPos, newEntity);
+                        await newEntity.OnCreated();
+                    }
                 }
+
+                // 通知周围 Block 更改
+                await Task.WhenAll(CrossCoords.Select(crossCoord =>
+               {
+                   var neighborPos = blockWorldPos;
+                   neighborPos.X += crossCoord.x;
+                   neighborPos.Z += crossCoord.z;
+                   var chunk = neighborPos.GetChunk();
+                   var blockChunkPos = neighborPos.ToBlockChunkPos();
+                   return GrainFactory.GetGrain<IChunkColumn>(_world.MakeChunkColumnKey(chunk.chunkX, chunk.chunkZ)).OnBlockNeighborChanged(
+                       blockChunkPos.X, blockChunkPos.Y, blockChunkPos.Z, blockWorldPos, oldState, blockState);
+               }));
             }
         }
 
-        return Task.FromResult(new ChunkColumn
+        private async Task EnsureChunkGenerated()
         {
-            Biomes = Enumerable.Repeat<byte>(0, 256).ToArray(),
-            SectionBitMask = 0b1111_1111_1111_1111,
-            Sections = new[]
+            if (!_generated)
             {
-                new ChunkSection
+                var serverSetting = GrainFactory.GetGrain<IServerSettings>(0);
+                string worldType = (await serverSetting.GetSettings()).LevelType;
+                if (worldType == "DEFAULT" || worldType == "default")
                 {
-                    BitsPerBlock = 13,
-                    Blocks = blocks
+                    var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(await _world.GetSeed());
+                    GeneratorSettings settings = new GeneratorSettings
+                    {
+                    };
+                    _state = await generator.Generate(_world, _chunkX, _chunkZ, settings);
                 }
-            }.Concat(Enumerable.Repeat(
-                new ChunkSection
+                else if (worldType == "FLAT" || worldType == "flat")
                 {
-                    BitsPerBlock = 13,
-                    Blocks = Enumerable.Repeat(new Block { Id = 0, SkyLight = 0xF }, 16 * 16 * 16).ToArray()
-                }, 15)).ToArray()
-        });
-        */
-
-        public override async Task OnActivateAsync()
-        {
-            var key = this.GetWorldAndChunkPosition();
-            _world = GrainFactory.GetGrain<IWorld>(key.worldKey);
-            _chunkX = key.x;
-            _chunkZ = key.z;
-            _players = new Dictionary<IPlayer, Vector3>();
+                    var generator = GrainFactory.GetGrain<IChunkGeneratorFlat>(await _world.GetSeed());
+                    GeneratorSettings settings = new GeneratorSettings
+                    {
+                        FlatBlockId = new BlockState?[]
+                        {
+                            BlockStates.Bedrock(),
+                            BlockStates.Stone(),
+                            BlockStates.Stone(),
+                            BlockStates.Dirt(),
+                            BlockStates.Dirt(),
+                            BlockStates.Grass()
+                        }
+                    };
+                    _state = await generator.Generate(_world, _chunkX, _chunkZ, settings);
+                }
+                else
+                {
+                    var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(await _world.GetSeed());
+                    GeneratorSettings settings = new GeneratorSettings
+                    {
+                    };
+                    _state = await generator.Generate(_world, _chunkX, _chunkZ, settings);
+                }
 
-            await EnsureChunkGenerated();
+                _generated = true;
+            }
         }
 
-        public Task SetBlockState(int x, int y, int z, BlockState blockState)
+        protected ClientPlayPacketGenerator GetBroadcastGenerator()
         {
-            _state[x, y, z] = blockState;
-            return Task.CompletedTask;
+            return new ClientPlayPacketGenerator(GrainFactory.GetGrain<IChunkTrackingHub>(_world.MakeChunkTrackingHubKey(_chunkX, _chunkZ)));
         }
 
-        public Task UpdatePlayerPosition(IPlayer player, Vector3 position)
+        public Task<IBlockEntity> GetBlockEntity(int x, int y, int z)
         {
-            _players[player] = position;
-            return Task.CompletedTask;
+            if (_blockEntities.TryGetValue(new BlockChunkPos(x, y, z), out var entity))
+                return Task.FromResult(entity);
+            return Task.FromResult<IBlockEntity>(null);
         }
 
-        private async Task EnsureChunkGenerated()
+        public Task OnBlockNeighborChanged(int x, int y, int z, BlockWorldPos neighborPosition, BlockState oldState, BlockState newState)
         {
-            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());
-                GeneratorSettings settings = new GeneratorSettings
-                {
-                };
-                _state = await generator.Generate(_world, _chunkX, _chunkZ, settings);
-            }
-            else if (worldType == "FLAT" || worldType == "flat")
-            {
-                var generator = GrainFactory.GetGrain<IChunkGeneratorFlat>(await _world.GetSeed());
-                GeneratorSettings settings = new GeneratorSettings
-                {
-                    FlatBlockId = new BlockState?[] { BlockStates.Stone(), BlockStates.Dirt(), BlockStates.Grass() }
-                };
-                _state = await generator.Generate(_world, _chunkX, _chunkZ, settings);
-            }
-            else
-            {
-                var generator = GrainFactory.GetGrain<IChunkGeneratorOverworld>(await _world.GetSeed());
-                GeneratorSettings settings = new GeneratorSettings
-                {
-                };
-                _state = await generator.Generate(_world, _chunkX, _chunkZ, settings);
-            }
+            var block = _state[x, y, z];
+            var blockHandler = BlockHandler.Create((BlockId)block.Id);
+            var selfPosition = new BlockChunkPos(x, y, z).ToBlockWorldPos(new ChunkWorldPos(_chunkX, _chunkZ));
+            return blockHandler.OnNeighborChanged(selfPosition, neighborPosition, oldState, newState, GrainFactory, _world);
         }
     }
-}
+}

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

@@ -1,8 +1,10 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Text;
 using System.Threading.Tasks;
 using MineCase.Protocol;
+using MineCase.Server.Game;
 using MineCase.Server.Network;
 using MineCase.Server.User;
 using Orleans;
@@ -10,11 +12,14 @@ using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [Reentrant]
     internal class ChunkTrackingHub : Grain, IChunkTrackingHub
     {
         private readonly IPacketPackager _packetPackager;
         private Dictionary<IUser, IClientboundPacketSink> _trackingUsers;
         private BroadcastPacketSink _broadcastPacketSink;
+        private HashSet<ITickable> _tickables;
+        private IGameSession _gameSession;
 
         public ChunkTrackingHub(IPacketPackager packetPackager)
         {
@@ -24,10 +29,23 @@ namespace MineCase.Server.World
         public override Task OnActivateAsync()
         {
             _trackingUsers = new Dictionary<IUser, IClientboundPacketSink>();
+            _tickables = new HashSet<ITickable>();
             _broadcastPacketSink = new BroadcastPacketSink(_trackingUsers.Values, _packetPackager);
+            _gameSession = GrainFactory.GetGrain<IGameSession>(this.GetWorldAndChunkPosition().worldKey);
             return base.OnActivateAsync();
         }
 
+        public Task OnGameTick(TimeSpan deltaTime, long worldAge)
+        {
+            if (_tickables.Count != 0)
+            {
+                return Task.WhenAll(from t in _tickables
+                                    select t.OnGameTick(deltaTime, worldAge));
+            }
+
+            return Task.CompletedTask;
+        }
+
         public Task SendPacket(ISerializablePacket packet)
         {
             return _broadcastPacketSink.SendPacket(packet);
@@ -42,11 +60,25 @@ namespace MineCase.Server.World
         {
             if (!_trackingUsers.ContainsKey(user))
                 _trackingUsers.Add(user, await user.GetClientPacketSink());
+            await _gameSession.Subscribe(this);
+        }
+
+        public Task Subscribe(ITickable tickableEntity)
+        {
+            _tickables.Add(tickableEntity);
+            return Task.CompletedTask;
         }
 
-        public Task Unsubscribe(IUser user)
+        public async Task Unsubscribe(IUser user)
         {
             _trackingUsers.Remove(user);
+            if (_tickables.Count == 0)
+                await _gameSession.Unsubscribe(this);
+        }
+
+        public Task Unsubscribe(ITickable tickableEntity)
+        {
+            _tickables.Remove(tickableEntity);
             return Task.CompletedTask;
         }
     }

+ 20 - 0
src/MineCase.Server.Grains/World/CollectableFinder.cs

@@ -1,19 +1,25 @@
 using System;
 using System.Collections.Generic;
+using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
+
 using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
+    [Reentrant]
     internal class CollectableFinder : Grain, ICollectableFinder
     {
+        private IWorld _world;
         private List<ICollectable> _collectables;
 
         public override Task OnActivateAsync()
         {
+            _world = GrainFactory.GetGrain<IWorld>(this.GetWorldAndChunkPosition().worldKey);
             _collectables = new List<ICollectable>();
             return base.OnActivateAsync();
         }
@@ -40,5 +46,19 @@ namespace MineCase.Server.World
             _collectables.Remove(collectable);
             return Task.CompletedTask;
         }
+
+        public async Task SpawnPickup(Position location, Immutable<Slot[]> slots)
+        {
+            foreach (var slot in slots.Value)
+            {
+                var pickup = GrainFactory.GetGrain<IPickup>(_world.MakeEntityKey(await _world.NewEntityId()));
+                await _world.AttachEntity(pickup);
+                await pickup.Spawn(
+                    Guid.NewGuid(),
+                    new Vector3(location.X + 0.5f, location.Y + 0.5f, location.Z + 0.5f));
+                await pickup.SetItem(slot);
+                pickup.Register().Ignore();
+            }
+        }
     }
 }

+ 47 - 0
src/MineCase.Server.Interfaces/Game/BlockEntities/IBlockEntity.cs

@@ -0,0 +1,47 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.World;
+using Orleans;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    public interface IBlockEntity : IGrainWithStringKey
+    {
+        Task OnCreated();
+
+        Task Destroy();
+    }
+
+    [AttributeUsage(AttributeTargets.Interface, Inherited = false, AllowMultiple = true)]
+    public sealed class BlockEntityAttribute : Attribute
+    {
+        public BlockId BlockId { get; }
+
+        public BlockEntityAttribute(BlockId blockId)
+        {
+            BlockId = blockId;
+        }
+    }
+
+    public static class BlockEntityExtensions
+    {
+        public static string MakeBlockEntityKey(this IWorld world, BlockWorldPos position)
+        {
+            return $"{world.GetPrimaryKeyString()},{position.X},{position.Y},{position.Z}";
+        }
+
+        public static BlockWorldPos GetBlockEntityPosition(this IBlockEntity blockEntity)
+        {
+            var key = blockEntity.GetPrimaryKeyString().Split(',');
+            return new BlockWorldPos(int.Parse(key[1]), int.Parse(key[2]), int.Parse(key[3]));
+        }
+
+        public static (string worldKey, BlockWorldPos position) GetWorldAndBlockEntityPosition(this IBlockEntity blockEntity)
+        {
+            var key = blockEntity.GetPrimaryKeyString().Split(',');
+            return (key[0], new BlockWorldPos(int.Parse(key[1]), int.Parse(key[2]), int.Parse(key[3])));
+        }
+    }
+}

+ 22 - 0
src/MineCase.Server.Interfaces/Game/BlockEntities/IChestBlockEntity.cs

@@ -0,0 +1,22 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.Entities;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    [BlockEntity(BlockId.Chest)]
+    public interface IChestBlockEntity : IBlockEntity
+    {
+        Task<Slot> GetSlot(int slotIndex);
+
+        Task SetSlot(int slotIndex, Slot item);
+
+        Task UseBy(IPlayer player);
+
+        Task ClearNeighborEntity();
+
+        Task SetNeighborEntity(IChestBlockEntity chestEntity);
+    }
+}

+ 18 - 0
src/MineCase.Server.Interfaces/Game/BlockEntities/IFurnaceBlockEntity.cs

@@ -0,0 +1,18 @@
+using System.Threading.Tasks;
+using MineCase.Server.Game.Entities;
+
+namespace MineCase.Server.Game.BlockEntities
+{
+    [BlockEntity(BlockId.Furnace)]
+    [BlockEntity(BlockId.BurningFurnace)]
+    public interface IFurnaceBlockEntity : IBlockEntity, ITickable
+    {
+        Task<Slot> GetSlot(int slotIndex);
+
+        Task SetSlot(int slotIndex, Slot item);
+
+        Task UseBy(IPlayer player);
+
+        Task<(int fuelLeft, int maxFuelTime, int cookProgress, int maxProgress)> GetCookingState();
+    }
+}

+ 0 - 1
src/MineCase.Server.Interfaces/Game/Entities/EntityMetadata/Pickup.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections.Generic;
 using System.Text;
-using MineCase.Formats;
 
 namespace MineCase.Server.Game.Entities.EntityMetadata
 {

+ 2 - 0
src/MineCase.Server.Interfaces/Game/Entities/ICollectable.cs

@@ -8,5 +8,7 @@ namespace MineCase.Server.Game.Entities
     public interface ICollectable : IEntity
     {
         Task CollectBy(IPlayer player);
+
+        Task Register();
     }
 }

+ 1 - 2
src/MineCase.Server.Interfaces/Game/Entities/IPickup.cs

@@ -3,11 +3,10 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
 
 namespace MineCase.Server.Game.Entities
 {
-    public interface IPickup : IEntity
+    public interface IPickup : IEntity, ICollectable
     {
         Task Spawn(Guid uuid, Vector3 position);
 

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

@@ -3,11 +3,12 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Protocol.Play;
 using MineCase.Server.Game.Windows;
 using MineCase.Server.Network;
 using MineCase.Server.User;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.Game.Entities
 {
@@ -29,10 +30,12 @@ namespace MineCase.Server.Game.Entities
 
         Task SendExperience();
 
-        Task<bool> Collect(uint collectedEntityId, Slot item);
+        Task<Slot> Collect(uint collectedEntityId, Slot item);
 
         Task SendPlayerListAddPlayer(IReadOnlyList<IPlayer> player);
 
+        Task OpenWindow(IWindow window);
+
         Task NotifyLoggedIn();
 
         Task SendPositionAndLook();
@@ -43,6 +46,8 @@ namespace MineCase.Server.Game.Entities
 
         Task SetLook(float yaw, float pitch, bool onGround);
 
+        Task<(float pitch, float yaw)> GetLook();
+
         Task StartDigging(Position location, PlayerDiggingFace face);
 
         Task CancelDigging(Position location, PlayerDiggingFace face);
@@ -55,6 +60,20 @@ namespace MineCase.Server.Game.Entities
 
         Task SetHeldItem(short slot);
 
+        Task<Slot> GetInventorySlot(int index);
+
+        Task SetInventorySlot(int index, Slot slot);
+
+        Task<byte> GetWindowId(IWindow window);
+
+        Task SetDraggedSlot(Slot item);
+
+        Task<Slot> GetDraggedSlot();
+
+        Task ClickWindow(byte windowId, short slot, ClickAction clickAction, short actionNumber, Slot clickedItem);
+
+        Task CloseWindow(byte windowId);
+
         Task SetOnGround(bool state);
     }
 }

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

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Algorithm;
+using Orleans;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game
+{
+    public interface ICraftingRecipes : IGrainWithIntegerKey
+    {
+        Task<FindCraftingRecipeResult> FindRecipe(Immutable<Slot[,]> craftingGrid);
+    }
+}

+ 14 - 0
src/MineCase.Server.Interfaces/Game/IFurnaceRecipes.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Algorithm;
+using Orleans;
+
+namespace MineCase.Server.Game
+{
+    public interface IFurnaceRecipes : IGrainWithIntegerKey
+    {
+        Task<FindFurnaceRecipeResult> FindRecipe(Slot input, Slot fuel);
+    }
+}

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

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Protocol.Play;
 using MineCase.Server.User;
 using Orleans;
@@ -18,5 +18,9 @@ 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);
     }
 }

+ 11 - 0
src/MineCase.Server.Interfaces/Game/ITickable.cs

@@ -0,0 +1,11 @@
+using System;
+using System.Threading.Tasks;
+using Orleans;
+
+namespace MineCase.Server.Game
+{
+    public interface ITickable : IGrainWithStringKey
+    {
+        Task OnGameTick(TimeSpan deltaTime, long worldAge);
+    }
+}

+ 14 - 0
src/MineCase.Server.Interfaces/Game/Windows/IChestWindow.cs

@@ -0,0 +1,14 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+using Orleans.Concurrency;
+
+namespace MineCase.Server.Game.Windows
+{
+    public interface IChestWindow : IWindow
+    {
+        Task SetEntities(Immutable<IChestBlockEntity[]> entities);
+    }
+}

+ 10 - 0
src/MineCase.Server.Interfaces/Game/Windows/ICraftingWindow.cs

@@ -0,0 +1,10 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase.Server.Game.Windows
+{
+    public interface ICraftingWindow : IWindow
+    {
+    }
+}

+ 15 - 0
src/MineCase.Server.Interfaces/Game/Windows/IFurnaceWindow.cs

@@ -0,0 +1,15 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
+
+namespace MineCase.Server.Game.Windows
+{
+    public interface IFurnaceWindow : IWindow
+    {
+        Task SetEntity(IFurnaceBlockEntity furnaceEntity);
+
+        Task OnGameTick(TimeSpan deltaTime, long worldAge);
+    }
+}

+ 6 - 3
src/MineCase.Server.Interfaces/Game/Windows/IInventoryWindow.cs

@@ -2,7 +2,8 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
+using MineCase.Server.Game.Entities;
 using MineCase.Server.User;
 using Orleans;
 
@@ -10,8 +11,10 @@ namespace MineCase.Server.Game.Windows
 {
     public interface IInventoryWindow : IWindow
     {
-        Task SetUser(IUser user);
+        Task<Slot> GetHotbarItem(IPlayer player, int slotIndex);
+
+        Task UseItem(IPlayer player, int slotIndex);
 
-        Task<bool> AddItem(Slot item);
+        Task<int> GetHotbarGlobalIndex(IPlayer player, int slotIndex);
     }
 }

+ 20 - 2
src/MineCase.Server.Interfaces/Game/Windows/IWindow.cs

@@ -2,7 +2,9 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
+using MineCase.Server.Game.Entities;
+using MineCase.Server.World;
 using Orleans;
 
 namespace MineCase.Server.Game.Windows
@@ -11,6 +13,22 @@ namespace MineCase.Server.Game.Windows
     {
         Task<uint> GetSlotCount();
 
-        Task<IReadOnlyList<Slot>> GetSlots();
+        Task<Slot> GetSlot(IPlayer player, int slotIndex);
+
+        Task SetSlot(IPlayer player, int slotIndex, Slot item);
+
+        Task<IReadOnlyList<Slot>> GetSlots(IPlayer player);
+
+        Task<Slot> DistributeStack(IPlayer player, Slot item);
+
+        Task Click(IPlayer player, int slotIndex, ClickAction clickAction, Slot clickedItem);
+
+        Task Close(IPlayer player);
+
+        Task OpenWindow(IPlayer player);
+
+        Task Destroy();
+
+        Task BroadcastSlotChanged(int slotIndex, Slot item);
     }
 }

+ 1 - 0
src/MineCase.Server.Interfaces/MineCase.Server.Interfaces.csproj

@@ -16,6 +16,7 @@
 
   <ItemGroup>
     <ProjectReference Include="..\MineCase.Algorithm\MineCase.Algorithm.csproj" />
+    <ProjectReference Include="..\Minecase.Core\MineCase.Core.csproj" />
     <ProjectReference Include="..\MineCase.Protocol\MineCase.Protocol.csproj" />
   </ItemGroup>
 

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

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using MineCase.Server.Network;
@@ -47,5 +47,7 @@ namespace MineCase.Server.User
         Task SetPacketRouter(IPacketRouter packetRouter);
 
         Task SetViewDistance(int viewDistance);
+
+        Task<Slot[]> GetInventorySlots();
     }
 }

+ 5 - 0
src/MineCase.Server.Interfaces/World/BlockStateExtensions.cs

@@ -42,5 +42,10 @@ namespace MineCase.Server.World
                 return true;
             }
         }
+
+        public static uint ToUInt32(this BlockState blockState)
+        {
+            return ChunkSectionStorage.ToUInt32(ref blockState);
+        }
     }
 }

+ 1 - 1
src/MineCase.Server.Interfaces/World/ChunkColumnStorage.cs

@@ -139,7 +139,7 @@ namespace MineCase.Server.World
             return ((y * ChunkConstants.BlockEdgeWidthInSection) + z) * ChunkConstants.BlockEdgeWidthInSection + x;
         }
 
-        internal static uint SerializeBlockState(BlockState blockState)
+        internal static uint ToUInt32(ref BlockState blockState)
         {
             return ((blockState.Id & _idMask) << _bitsMeta) | (blockState.MetaValue & _metaMask);
         }

+ 3 - 6
src/MineCase.Server.Interfaces/World/IChunkColumn.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.Numerics;
 using System.Text;
 using System.Threading.Tasks;
+using MineCase.Server.Game.BlockEntities;
 using MineCase.Server.Game.Entities;
 using Orleans;
 
@@ -16,13 +17,9 @@ namespace MineCase.Server.World
 
         Task SetBlockState(int x, int y, int z, BlockState blockState);
 
-        Task AttachPlayer(IPlayer player);
+        Task<IBlockEntity> GetBlockEntity(int x, int y, int z);
 
-        Task UpdatePlayerPosition(IPlayer player, Vector3 position);
-
-        Task DetachPlayer(IPlayer player);
-
-        Task<IReadOnlyCollection<(IPlayer player, Vector3 position)>> GetPlayers();
+        Task OnBlockNeighborChanged(int x, int y, int z, BlockWorldPos neighborPosition, BlockState oldState, BlockState newState);
     }
 
     public static class ChunkColumnExtensions

+ 6 - 1
src/MineCase.Server.Interfaces/World/IChunkTrackingHub.cs

@@ -2,17 +2,22 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
+using MineCase.Server.Game;
 using MineCase.Server.Network;
 using MineCase.Server.User;
 using Orleans;
 
 namespace MineCase.Server.World
 {
-    public interface IChunkTrackingHub : IGrainWithStringKey, IPacketSink
+    public interface IChunkTrackingHub : IGrainWithStringKey, ITickable, IPacketSink
     {
         Task Subscribe(IUser user);
 
         Task Unsubscribe(IUser user);
+
+        Task Subscribe(ITickable tickable);
+
+        Task Unsubscribe(ITickable tickable);
     }
 
     public static class ChunkTrackingHubExtensions

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

@@ -2,9 +2,11 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
+
 using MineCase.Server.Game;
 using MineCase.Server.Game.Entities;
 using Orleans;
+using Orleans.Concurrency;
 
 namespace MineCase.Server.World
 {
@@ -17,6 +19,8 @@ namespace MineCase.Server.World
         Task<IReadOnlyCollection<ICollectable>> Collision(IEntity entity);
 
         Task<IReadOnlyCollection<ICollectable>> CollisionInChunk(IEntity entity);
+
+        Task SpawnPickup(Position location, Immutable<Slot[]> slots);
     }
 
     public static class CollectableFinderExtensions

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

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+
 using MineCase.Server.Game;
 using MineCase.Server.World.Generation;
 using Orleans;

+ 48 - 1
src/MineCase.Server.Interfaces/World/Position.cs

@@ -48,6 +48,16 @@ namespace MineCase.Server.World
             Z = z;
         }
 
+        public static implicit operator BlockWorldPos(Position position)
+        {
+            return new BlockWorldPos(position.X, position.Y, position.Z);
+        }
+
+        public static implicit operator Position(BlockWorldPos position)
+        {
+            return new Position { X = position.X, Y = position.Y, Z = position.Z };
+        }
+
         public BlockChunkPos ToBlockChunkPos()
         {
             int blockPosX = X % ChunkConstants.BlockEdgeWidthInSection;
@@ -97,7 +107,7 @@ namespace MineCase.Server.World
         }
     }
 
-    public struct BlockChunkPos
+    public struct BlockChunkPos : IEquatable<BlockChunkPos>
     {
         public int X { get; set; }
 
@@ -149,6 +159,43 @@ namespace MineCase.Server.World
             int z = pos1.Z - pos2.Z;
             return Math.Sqrt(x * x + y * y + z * z);
         }
+
+        public override string ToString()
+        {
+            return $"{X}, {Y}, {Z}";
+        }
+
+        public override bool Equals(object obj)
+        {
+            return obj is BlockChunkPos && Equals((BlockChunkPos)obj);
+        }
+
+        public bool Equals(BlockChunkPos other)
+        {
+            return X == other.X &&
+                   Y == other.Y &&
+                   Z == other.Z;
+        }
+
+        public override int GetHashCode()
+        {
+            var hashCode = -307843816;
+            hashCode = hashCode * -1521134295 + base.GetHashCode();
+            hashCode = hashCode * -1521134295 + X.GetHashCode();
+            hashCode = hashCode * -1521134295 + Y.GetHashCode();
+            hashCode = hashCode * -1521134295 + Z.GetHashCode();
+            return hashCode;
+        }
+
+        public static bool operator ==(BlockChunkPos pos1, BlockChunkPos pos2)
+        {
+            return pos1.Equals(pos2);
+        }
+
+        public static bool operator !=(BlockChunkPos pos1, BlockChunkPos pos2)
+        {
+            return !(pos1 == pos2);
+        }
     }
 
     public struct BlockSectionPos

+ 44 - 1
src/MineCase.Server.Interfaces/World/WorldExtensions.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.Text;
 using System.Threading.Tasks;
-using MineCase.Formats;
+using MineCase.Server.Game.BlockEntities;
 using Orleans;
 
 namespace MineCase.Server.World
@@ -132,6 +132,11 @@ namespace MineCase.Server.World
             return (MakeRelativeBlockOffset(blockPosition.X).chunk, MakeRelativeBlockOffset(blockPosition.Z).chunk);
         }
 
+        public static (int chunkX, int chunkZ) GetChunk(this BlockWorldPos blockPosition)
+        {
+            return (MakeRelativeBlockOffset(blockPosition.X).chunk, MakeRelativeBlockOffset(blockPosition.Z).chunk);
+        }
+
         public static (int chunk, int block) MakeRelativeBlockOffset(int value)
         {
             var chunk = value / ChunkConstants.BlockEdgeWidthInSection;
@@ -144,5 +149,43 @@ namespace MineCase.Server.World
 
             return (chunk, block);
         }
+
+        /// <summary>
+        /// Gets the state of the block.
+        /// </summary>
+        /// <param name="world">The world Grain.</param>
+        /// <param name="grainFactory">The grain factory.</param>
+        /// <param name="x">The x.</param>
+        /// <param name="y">The y.</param>
+        /// <param name="z">The z.</param>
+        /// <returns>方块类型</returns>
+        public static Task<IBlockEntity> GetBlockEntity(this IWorld world, IGrainFactory grainFactory, int x, int y, int z)
+        {
+            var xOffset = MakeRelativeBlockOffset(x);
+            var zOffset = MakeRelativeBlockOffset(z);
+            var chunkColumnKey = world.MakeChunkColumnKey(xOffset.chunk, zOffset.chunk);
+            return grainFactory.GetGrain<IChunkColumn>(chunkColumnKey).GetBlockEntity(
+                xOffset.block,
+                y,
+                zOffset.block);
+        }
+
+        /// <summary>
+        /// Gets the state of the block.
+        /// </summary>
+        /// <param name="world">The world Grain.</param>
+        /// <param name="grainFactory">The grain factory.</param>
+        /// <param name="pos">The position.</param>
+        /// <returns>方块类型</returns>
+        public static Task<IBlockEntity> GetBlockEntity(this IWorld world, IGrainFactory grainFactory, BlockWorldPos pos)
+        {
+            var xOffset = MakeRelativeBlockOffset(pos.X);
+            var zOffset = MakeRelativeBlockOffset(pos.Z);
+            var chunkColumnKey = world.MakeChunkColumnKey(xOffset.chunk, zOffset.chunk);
+            return grainFactory.GetGrain<IChunkColumn>(chunkColumnKey).GetBlockEntity(
+                xOffset.block,
+                pos.Y,
+                zOffset.block);
+        }
     }
 }

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

@@ -31,6 +31,15 @@
     <None Remove="*.log;*.txt" />
   </ItemGroup>
 
+  <ItemGroup>
+    <Content Include="..\..\data\crafting.txt" Link="crafting.txt">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+    <Content Include="..\..\data\furnace.txt" Link="furnace.txt">
+      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
+    </Content>
+  </ItemGroup>
+
   <ItemGroup>
     <ProjectReference Include="..\MineCase.Server.Grains\MineCase.Server.Grains.csproj" />
   </ItemGroup>

+ 16 - 4
src/MineCase.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
 # Visual Studio 15
-VisualStudioVersion = 15.0.26730.10
+VisualStudioVersion = 15.0.26730.12
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MineCase.Server", "MineCase.Server\MineCase.Server.csproj", "{8E71CBEC-5804-4125-B651-C78426E57C8C}"
 EndProject
@@ -30,10 +30,15 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MineCase.Algorithm", "MineC
 EndProject
 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{D53C0015-5761-4018-A50A-63C919C00D35}"
 EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MineCase.Core", "Minecase.Core\MineCase.Core.csproj", "{B7B1A959-72F3-42C6-B138-93C8D654F139}"
+EndProject
+Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "data", "data", "{7DC8CDBD-087D-4F3C-B765-CA77783BECA5}"
+	ProjectSection(SolutionItems) = preProject
+		..\data\crafting.txt = ..\data\crafting.txt
+		..\data\furnace.txt = ..\data\furnace.txt
+	EndProjectSection
+EndProject
 Global
-	GlobalSection(Performance) = preSolution
-		HasPerformanceSessions = true
-	EndGlobalSection
 	GlobalSection(SolutionConfigurationPlatforms) = preSolution
 		Debug|Any CPU = Debug|Any CPU
 		Release|Any CPU = Release|Any CPU
@@ -71,6 +76,10 @@ Global
 		{47C1F452-E59C-42E3-8799-BF09444D8384}.Debug|Any CPU.Build.0 = Debug|Any CPU
 		{47C1F452-E59C-42E3-8799-BF09444D8384}.Release|Any CPU.ActiveCfg = Release|Any CPU
 		{47C1F452-E59C-42E3-8799-BF09444D8384}.Release|Any CPU.Build.0 = Release|Any CPU
+		{B7B1A959-72F3-42C6-B138-93C8D654F139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+		{B7B1A959-72F3-42C6-B138-93C8D654F139}.Debug|Any CPU.Build.0 = Debug|Any CPU
+		{B7B1A959-72F3-42C6-B138-93C8D654F139}.Release|Any CPU.ActiveCfg = Release|Any CPU
+		{B7B1A959-72F3-42C6-B138-93C8D654F139}.Release|Any CPU.Build.0 = Release|Any CPU
 	EndGlobalSection
 	GlobalSection(SolutionProperties) = preSolution
 		HideSolutionNode = FALSE
@@ -81,4 +90,7 @@ Global
 	GlobalSection(ExtensibilityGlobals) = postSolution
 		SolutionGuid = {7AB995C3-E961-463C-8A55-3149C51761B5}
 	EndGlobalSection
+	GlobalSection(Performance) = preSolution
+		HasPerformanceSessions = true
+	EndGlobalSection
 EndGlobal

+ 2 - 7
src/MineCase.Server.Interfaces/World/Block.cs → src/Minecase.Core/Block.cs

@@ -2,7 +2,7 @@
 using System.Collections.Generic;
 using System.Text;
 
-namespace MineCase.Server.World
+namespace MineCase
 {
     public enum BlockId : uint
     {
@@ -397,7 +397,7 @@ namespace MineCase.Server.World
     /// <summary>
     /// Specifies the color of the wool, stained terracotta, stained glass and carpet.
     /// </summary>
-    public enum ColorType : uint
+    public enum BlockColorType : uint
     {
         White = 0,
         Orange = 1,
@@ -1290,10 +1290,5 @@ namespace MineCase.Server.World
         {
             return !(state1 == state2);
         }
-
-        public uint ToUInt32()
-        {
-            return ChunkSectionStorage.SerializeBlockState(this);
-        }
     }
 }

+ 1 - 1
src/MineCase.Server.Interfaces/World/Blocks.cs → src/Minecase.Core/Blocks.cs

@@ -2,7 +2,7 @@ using System;
 using System.Collections.Generic;
 using System.Text;
 
-namespace MineCase.Server.World
+namespace MineCase
 {
     public static class BlockStates
     {

+ 1 - 1
src/MineCase.Protocol/Formats/Chat.cs → src/Minecase.Core/Chat.cs

@@ -3,7 +3,7 @@ using System.Collections.Generic;
 using Newtonsoft.Json;
 using Newtonsoft.Json.Linq;
 
-namespace MineCase.Formats
+namespace MineCase
 {
     /// <summary>
     /// Available types for color of Compoent.

+ 12 - 0
src/Minecase.Core/ClickAction.cs

@@ -0,0 +1,12 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+
+namespace MineCase
+{
+    public enum ClickAction
+    {
+        LeftMouseClick,
+        RightMouseClick
+    }
+}

+ 1 - 1
src/MineCase.Protocol/Formats/ClientboundAnimationID.cs → src/Minecase.Core/ClientboundAnimationId.cs

@@ -1,4 +1,4 @@
-namespace MineCase.Formats
+namespace MineCase
 {
     public enum ClientboundAnimationId : byte
     {

Неке датотеке нису приказане због велике количине промена