summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKevin Chabowski <kevin@kch42.de>2013-08-11 23:01:55 +0200
committerKevin Chabowski <kevin@kch42.de>2013-08-11 23:01:55 +0200
commitb9be995232e8299d504e7898a9d6c5384664b8ee (patch)
tree791b91d23fa261680de231ea4b8d8bb8dc451437
downloadgomcmap-b9be995232e8299d504e7898a9d6c5384664b8ee.tar.gz
gomcmap-b9be995232e8299d504e7898a9d6c5384664b8ee.tar.bz2
gomcmap-b9be995232e8299d504e7898a9d6c5384664b8ee.zip
Initial commit.
Reading maps is already working :-D
-rw-r--r--mcmap/biomes.go67
-rw-r--r--mcmap/block.go377
-rw-r--r--mcmap/chunk.go81
-rw-r--r--mcmap/common.go5
-rw-r--r--mcmap/examples/emeraldfinder/main.go53
-rw-r--r--mcmap/prechunk.go257
-rw-r--r--mcmap/region.go246
-rw-r--r--mcmap/regionfile.go101
8 files changed, 1187 insertions, 0 deletions
diff --git a/mcmap/biomes.go b/mcmap/biomes.go
new file mode 100644
index 0000000..daccad1
--- /dev/null
+++ b/mcmap/biomes.go
@@ -0,0 +1,67 @@
+package mcmap
+
+type Biome int8
+
+// Names and values from: http://www.minecraftwiki.net/wiki/Data_values
+
+// Valid values for Biome
+const (
+ BioOcean = 0
+ BioPlains = 1
+ BioDesert = 2
+ BioExtremeHills = 3
+ BioForest = 4
+ BioTaiga = 5
+ BioSwampland = 6
+ BioRiver = 7
+ BioHell = 8
+ BioSky = 9
+ BioFrozenOcean = 10
+ BioFrozenRiver = 11
+ BioIcePlains = 12
+ BioIceMountains = 13
+ BioMushroomIsland = 14
+ BioMushroomIslandShore = 15
+ BioBeach = 16
+ BioDesertHills = 17
+ BioForestHills = 18
+ BioTaigaHills = 19
+ BioExtremeHillsEdge = 20
+ BioJungle = 21
+ BioJungleHills = 22
+ BioUncalculated = -1
+)
+
+var biomeNames = map[Biome]string{
+ BioOcean: "Ocean",
+ BioPlains: "Plains",
+ BioDesert: "Desert",
+ BioExtremeHills: "Extreme Hills",
+ BioForest: "Forest",
+ BioTaiga: "Taiga",
+ BioSwampland: "Swampland",
+ BioRiver: "River",
+ BioHell: "Hell",
+ BioSky: "Sky",
+ BioFrozenOcean: "Frozen Ocean",
+ BioFrozenRiver: "Frozen River",
+ BioIcePlains: "Ice Plains",
+ BioIceMountains: "Ice Mountains",
+ BioMushroomIsland: "Mushroom Island",
+ BioMushroomIslandShore: "Mushroom Island Shore",
+ BioBeach: "Beach",
+ BioDesertHills: "Desert Hills",
+ BioForestHills: "Forest Hills",
+ BioTaigaHills: "Taiga Hills",
+ BioExtremeHillsEdge: "Extreme Hills Edge",
+ BioJungle: "Jungle",
+ BioJungleHills: "Jungle Hills",
+ BioUncalculated: "(Uncalculated)",
+}
+
+func (b Biome) String() string {
+ if s, ok := biomeNames[b]; ok {
+ return s
+ }
+ return "(Unknown)"
+}
diff --git a/mcmap/block.go b/mcmap/block.go
new file mode 100644
index 0000000..91dc374
--- /dev/null
+++ b/mcmap/block.go
@@ -0,0 +1,377 @@
+package mcmap
+
+import (
+ "github.com/kch42/gonbt/nbt"
+)
+
+type BlockID uint16
+
+type Block struct {
+ ID BlockID
+ Data byte // Actually only a half-byte.
+ BlockLight, SkyLight byte // Also, only half-bytes.
+ TileEntity nbt.TagCompound // The x, y and z values in here can be ignored, will automatically be fixed on saving.
+ Tick *TileTick // If nil, no TileTick info is available for this block
+}
+
+type TileTick struct {
+ i, t, p int32
+ hasP bool
+}
+
+func (tt *TileTick) I() int32 { return tt.i }
+func (tt *TileTick) T() int32 { return tt.t }
+func (tt *TileTick) P() int32 { return tt.p }
+
+func (tt *TileTick) SetI(i int32) { tt.i = i }
+func (tt *TileTick) SetT(t int32) { tt.t = t }
+
+func (tt *TileTick) SetP(p int32) {
+ tt.p = p
+ tt.hasP = true
+}
+
+// Names and values from: http://www.minecraftwiki.net/wiki/Data_values
+
+// Valid values for BlockID
+const (
+ BlkAir = 0
+ BlkStone = 1
+ BlkGrassBlock = 2
+ BlkDirt = 3
+ BlkCobblestone = 4
+ BlkWoodPlanks = 5
+ BlkSaplings = 6
+ BlkBedrock = 7
+ BlkWater = 8
+ BlkStationaryWater = 9
+ BlkLava = 10
+ BlkStationaryLava = 11
+ BlkSand = 12
+ BlkGravel = 13
+ BlkGoldOre = 14
+ BlkIronOre = 15
+ BlkCoalOre = 16
+ BlkWood = 17
+ BlkLeaves = 18
+ BlkSponge = 19
+ BlkGlass = 20
+ BlkLapisLazuliOre = 21
+ BlkLapisLazuliBlock = 22
+ BlkDispenser = 23
+ BlkSandstone = 24
+ BlkNoteBlock = 25
+ BlkBed = 26
+ BlkPoweredRail = 27
+ BlkDetectorRail = 28
+ BlkStickyPiston = 29
+ BlkCobweb = 30
+ BlkGrass = 31
+ BlkDeadBush = 32
+ BlkPiston = 33
+ BlkPistonExtension = 34
+ BlkWool = 35
+ BlkBlockMovedByPiston = 36
+ BlkDandelion = 37
+ BlkRose = 38
+ BlkBrownMushroom = 39
+ BlkRedMushroom = 40
+ BlkBlockOfGold = 41
+ BlkBlockOfIron = 42
+ BlkDoubleSlabs = 43
+ BlkSlabs = 44
+ BlkBricks = 45
+ BlkTNT = 46
+ BlkBookshelf = 47
+ BlkMossStone = 48
+ BlkObsidian = 49
+ BlkTorch = 50
+ BlkFire = 51
+ BlkMonsterSpawner = 52
+ BlkOakWoodStairs = 53
+ BlkChest = 54
+ BlkRedstoneWire = 55
+ BlkDiamondOre = 56
+ BlkBlockOfDiamond = 57
+ BlkCraftingTable = 58
+ BlkWheat = 59
+ BlkFarmland = 60
+ BlkFurnace = 61
+ BlkBurningFurnace = 62
+ BlkSignPost = 63
+ BlkWoodenDoor = 64
+ BlkLadders = 65
+ BlkRail = 66
+ BlkCobblestoneStairs = 67
+ BlkWallSign = 68
+ BlkLever = 69
+ BlkStonePressurePlate = 70
+ BlkIronDoor = 71
+ BlkWoodenPressurePlate = 72
+ BlkRedstoneOre = 73
+ BlkGlowingRedstoneOre = 74
+ BlkRedstoneTorchInactive = 75
+ BlkRedstoneTorchActive = 76
+ BlkStoneButton = 77
+ BlkSnow = 78
+ BlkIce = 79
+ BlkSnowBlock = 80
+ BlkCactus = 81
+ BlkClay = 82
+ BlkSugarCane = 83
+ BlkJukebox = 84
+ BlkFence = 85
+ BlkPumpkin = 86
+ BlkNetherrack = 87
+ BlkSoulSand = 88
+ BlkGlowstone = 89
+ BlkNetherPortal = 90
+ BlkJackOLantern = 91
+ BlkCakeBlock = 92
+ BlkRedstoneRepeaterInactive = 93
+ BlkRedstoneRepeaterActive = 94
+ BlkLockedChest = 95
+ BlkTrapdoor = 96
+ BlkMonsterEgg = 97
+ BlkStoneBricks = 98
+ BlkHugeBrownMushroom = 99
+ BlkHugeRedMushroom = 100
+ BlkIronBars = 101
+ BlkGlassPane = 102
+ BlkMelon = 103
+ BlkPumpkinStem = 104
+ BlkMelonStem = 105
+ BlkVines = 106
+ BlkFenceGate = 107
+ BlkBrickStairs = 108
+ BlkStoneBrickStairs = 109
+ BlkMycelium = 110
+ BlkLilyPad = 111
+ BlkNetherBrick = 112
+ BlkNetherBrickFence = 113
+ BlkNetherBrickStairs = 114
+ BlkNetherWart = 115
+ BlkEnchantmentTable = 116
+ BlkBrewingStand = 117
+ BlkCauldron = 118
+ BlkEndPortal = 119
+ BlkEndPortalBlock = 120
+ BlkEndStone = 121
+ BlkDragonEgg = 122
+ BlkRedstoneLampInactive = 123
+ BlkRedstoneLampActive = 124
+ BlkWoodenDoubleSlab = 125
+ BlkWoodenSlab = 126
+ BlkCocoa = 127
+ BlkSandstoneStairs = 128
+ BlkEmeraldOre = 129
+ BlkEnderChest = 130
+ BlkTripwireHook = 131
+ BlkTripwire = 132
+ BlkBlockOfEmerald = 133
+ BlkSpruceWoodStairs = 134
+ BlkBirchWoodStairs = 135
+ BlkJungleWoodStairs = 136
+ BlkCommandBlock = 137
+ BlkBeacon = 138
+ BlkCobblestoneWall = 139
+ BlkFlowerPot = 140
+ BlkCarrots = 141
+ BlkPotatoes = 142
+ BlkWoodenButton = 143
+ BlkMobHead = 144
+ BlkAnvil = 145
+ BlkTrappedChest = 146
+ BlkWeightedPressurePlateLight = 147
+ BlkWeightedPressurePlateHeavy = 148
+ BlkRedstoneComparatorInactive = 149
+ BlkRedstoneComparatorActive = 150
+ BlkDaylightSensor = 151
+ BlkBlockOfRedstone = 152
+ BlkNetherQuartzOre = 153
+ BlkHopper = 154
+ BlkBlockOfQuartz = 155
+ BlkQuartzStairs = 156
+ BlkActivatorRail = 157
+ BlkDropper = 158
+ BlkStainedClay = 159
+ BlkHayBlock = 170
+ BlkCarpet = 171
+ BlkHardenedClay = 172
+ BlkBlockOfCoal = 173
+)
+
+var blockNames = map[BlockID]string{
+ BlkAir: "Air",
+ BlkStone: "Stone",
+ BlkGrassBlock: "Grass Block",
+ BlkDirt: "Dirt",
+ BlkCobblestone: "Cobblestone",
+ BlkWoodPlanks: "Wood Planks",
+ BlkSaplings: "Saplings",
+ BlkBedrock: "Bedrock",
+ BlkWater: "Water",
+ BlkStationaryWater: "Stationary water",
+ BlkLava: "Lava",
+ BlkStationaryLava: "Stationary lava",
+ BlkSand: "Sand",
+ BlkGravel: "Gravel",
+ BlkGoldOre: "Gold Ore",
+ BlkIronOre: "Iron Ore",
+ BlkCoalOre: "Coal Ore",
+ BlkWood: "Wood",
+ BlkLeaves: "Leaves",
+ BlkSponge: "Sponge",
+ BlkGlass: "Glass",
+ BlkLapisLazuliOre: "Lapis Lazuli Ore",
+ BlkLapisLazuliBlock: "Lapis Lazuli Block",
+ BlkDispenser: "Dispenser",
+ BlkSandstone: "Sandstone",
+ BlkNoteBlock: "Note Block",
+ BlkBed: "Bed",
+ BlkPoweredRail: "Powered Rail",
+ BlkDetectorRail: "Detector Rail",
+ BlkStickyPiston: "Sticky Piston",
+ BlkCobweb: "Cobweb",
+ BlkGrass: "Grass",
+ BlkDeadBush: "Dead Bush",
+ BlkPiston: "Piston",
+ BlkPistonExtension: "Piston Extension",
+ BlkWool: "Wool",
+ BlkBlockMovedByPiston: "Block moved by Piston",
+ BlkDandelion: "Dandelion",
+ BlkRose: "Rose",
+ BlkBrownMushroom: "Brown Mushroom",
+ BlkRedMushroom: "Red Mushroom",
+ BlkBlockOfGold: "Block of Gold",
+ BlkBlockOfIron: "Block of Iron",
+ BlkDoubleSlabs: "Double Slabs",
+ BlkSlabs: "Slabs",
+ BlkBricks: "Bricks",
+ BlkTNT: "TNT",
+ BlkBookshelf: "Bookshelf",
+ BlkMossStone: "Moss Stone",
+ BlkObsidian: "Obsidian",
+ BlkTorch: "Torch",
+ BlkFire: "Fire",
+ BlkMonsterSpawner: "Monster Spawner",
+ BlkOakWoodStairs: "Oak Wood Stairs",
+ BlkChest: "Chest",
+ BlkRedstoneWire: "Redstone Wire",
+ BlkDiamondOre: "Diamond Ore",
+ BlkBlockOfDiamond: "Block of Diamond",
+ BlkCraftingTable: "Crafting Table",
+ BlkWheat: "Wheat",
+ BlkFarmland: "Farmland",
+ BlkFurnace: "Furnace",
+ BlkBurningFurnace: "Burning Furnace",
+ BlkSignPost: "Sign Post",
+ BlkWoodenDoor: "Wooden Door",
+ BlkLadders: "Ladders",
+ BlkRail: "Rail",
+ BlkCobblestoneStairs: "Cobblestone Stairs",
+ BlkWallSign: "Wall Sign",
+ BlkLever: "Lever",
+ BlkStonePressurePlate: "Stone Pressure Plate",
+ BlkIronDoor: "Iron Door",
+ BlkWoodenPressurePlate: "Wooden Pressure Plate",
+ BlkRedstoneOre: "Redstone Ore",
+ BlkGlowingRedstoneOre: "Glowing Redstone Ore",
+ BlkRedstoneTorchInactive: "Redstone Torch (inactive)",
+ BlkRedstoneTorchActive: "Redstone Torch (active)",
+ BlkStoneButton: "Stone Button",
+ BlkSnow: "Snow",
+ BlkIce: "Ice",
+ BlkSnowBlock: "Snow Block",
+ BlkCactus: "Cactus",
+ BlkClay: "Clay",
+ BlkSugarCane: "Sugar Cane",
+ BlkJukebox: "Jukebox",
+ BlkFence: "Fence",
+ BlkPumpkin: "Pumpkin",
+ BlkNetherrack: "Netherrack",
+ BlkSoulSand: "Soul Sand",
+ BlkGlowstone: "Glowstone",
+ BlkNetherPortal: "Nether Portal",
+ BlkJackOLantern: "Jack 'o' Lantern",
+ BlkCakeBlock: "Cake Block",
+ BlkRedstoneRepeaterInactive: "Redstone Repeater (inactive)",
+ BlkRedstoneRepeaterActive: "Redstone Repeater (active)",
+ BlkLockedChest: "Locked Chest",
+ BlkTrapdoor: "Trapdoor",
+ BlkMonsterEgg: "Monster Egg",
+ BlkStoneBricks: "Stone Bricks",
+ BlkHugeBrownMushroom: "Huge Brown Mushroom",
+ BlkHugeRedMushroom: "Huge Red Mushroom",
+ BlkIronBars: "Iron Bars",
+ BlkGlassPane: "Glass Pane",
+ BlkMelon: "Melon",
+ BlkPumpkinStem: "Pumpkin Stem",
+ BlkMelonStem: "Melon Stem",
+ BlkVines: "Vines",
+ BlkFenceGate: "Fence Gate",
+ BlkBrickStairs: "Brick Stairs",
+ BlkStoneBrickStairs: "Stone Brick Stairs",
+ BlkMycelium: "Mycelium",
+ BlkLilyPad: "Lily Pad",
+ BlkNetherBrick: "Nether Brick",
+ BlkNetherBrickFence: "Nether Brick Fence",
+ BlkNetherBrickStairs: "Nether Brick Stairs",
+ BlkNetherWart: "Nether Wart",
+ BlkEnchantmentTable: "Enchantment Table",
+ BlkBrewingStand: "Brewing Stand",
+ BlkCauldron: "Cauldron",
+ BlkEndPortal: "End Portal",
+ BlkEndPortalBlock: "End Portal Block",
+ BlkEndStone: "End Stone",
+ BlkDragonEgg: "Dragon Egg",
+ BlkRedstoneLampInactive: "Redstone Lamp (inactive)",
+ BlkRedstoneLampActive: "Redstone Lamp (active)",
+ BlkWoodenDoubleSlab: "Wooden Double Slab",
+ BlkWoodenSlab: "Wooden Slab",
+ BlkCocoa: "Cocoa",
+ BlkSandstoneStairs: "Sandstone Stairs",
+ BlkEmeraldOre: "Emerald Ore",
+ BlkEnderChest: "Ender Chest",
+ BlkTripwireHook: "Tripwire Hook",
+ BlkTripwire: "Tripwire",
+ BlkBlockOfEmerald: "Block of Emerald",
+ BlkSpruceWoodStairs: "Spruce Wood Stairs",
+ BlkBirchWoodStairs: "Birch Wood Stairs",
+ BlkJungleWoodStairs: "Jungle Wood Stairs",
+ BlkCommandBlock: "Command Block",
+ BlkBeacon: "Beacon",
+ BlkCobblestoneWall: "Cobblestone Wall",
+ BlkFlowerPot: "Flower Pot",
+ BlkCarrots: "Carrots",
+ BlkPotatoes: "Potatoes",
+ BlkWoodenButton: "Wooden Button",
+ BlkMobHead: "Mob Head",
+ BlkAnvil: "Anvil",
+ BlkTrappedChest: "Trapped Chest",
+ BlkWeightedPressurePlateLight: "Weighted Pressure Plate (Light)",
+ BlkWeightedPressurePlateHeavy: "Weighted Pressure Plate (Heavy)",
+ BlkRedstoneComparatorInactive: "Redstone Comparator (inactive)",
+ BlkRedstoneComparatorActive: "Redstone Comparator (active)",
+ BlkDaylightSensor: "Daylight Sensor",
+ BlkBlockOfRedstone: "Block of Redstone",
+ BlkNetherQuartzOre: "Nether Quartz Ore",
+ BlkHopper: "Hopper",
+ BlkBlockOfQuartz: "Block of Quartz",
+ BlkQuartzStairs: "Quartz Stairs",
+ BlkActivatorRail: "Activator Rail",
+ BlkDropper: "Dropper",
+ BlkStainedClay: "Stained Clay",
+ BlkHayBlock: "Hay Block",
+ BlkCarpet: "Carpet",
+ BlkHardenedClay: "Hardened Clay",
+ BlkBlockOfCoal: "Block of Coal",
+}
+
+func (b BlockID) String() string {
+ if s, ok := blockNames[b]; ok {
+ return s
+ }
+
+ return "(unused)"
+}
diff --git a/mcmap/chunk.go b/mcmap/chunk.go
new file mode 100644
index 0000000..22f9dfa
--- /dev/null
+++ b/mcmap/chunk.go
@@ -0,0 +1,81 @@
+package mcmap
+
+import (
+ "errors"
+ "github.com/kch42/gonbt/nbt"
+ "time"
+)
+
+func calcBlockOffset(x, y, z int) int {
+ if (x < 0) || (y < 0) || (z < 0) || (x >= 16) || (y >= 256) || (z >= 16) {
+ panic(errors.New("Can't calculate Block offset, coordinates out of range."))
+ }
+
+ return x + (z * 16) + (y * 256)
+}
+
+// BlockToChunk calculates the chunk (cx, cz) and the block position in this chunk(rbx, rbz) of a block position given global coordinates.
+func BlockToChunk(bx, bz int) (cx, cz, rbx, rbz int) {
+ cx = bx << 4
+ cz = bz << 4
+ rbx = ((cx % 16) + 16) % 16
+ rbz = ((cz % 16) + 16) % 16
+ return
+}
+
+// ChunkToBlock calculates the global position of a block, given the chunk position (cx, cz) and the plock position in that chunk (rbx, rbz).
+func ChunkToBlock(cx, cz, rbx, rbz int) (bx, bz int) {
+ bx = cx*16 + rbx
+ bz = cz*16 + rbz
+ return
+}
+
+// Chunk represents a 16*16*256 Chunk of the region.
+type Chunk struct {
+ Entities []nbt.TagCompound
+
+ x, z int32
+
+ lastUpdate int64
+ populated bool
+ inhabitatedTime int64
+ ts time.Time
+
+ heightMap []int32 // Note: Ordered ZX
+ blockLight, skyLight []byte // Note: Ordered YZX, only half-bytes
+
+ modified bool
+ blocks []Block // NOTE: Ordered YZX
+ biomes []Biome // NOTE: Orderes XZ
+}
+
+// MarkModified needs to be called, if some data of the chunk was modified.
+func (c *Chunk) MarkModified() { c.modified = true }
+
+// Coords returns the Chunk's coordinates.
+func (c *Chunk) Coords() (X, Z int32) { return c.x, c.z }
+
+// Block gives you a reference to the Block located at x, y, z. If you modify the block data, you need to call the MarkModified() function of the chunk.
+//
+// x and z must be in [0, 15], y in [0, 255]. Otherwise a nil pointer is returned.
+func (c *Chunk) Block(x, y, z int) *Block {
+ off := calcBlockOffset(x, y, z)
+ if off < 0 {
+ return nil
+ }
+
+ return &(c.blocks[off])
+}
+
+// Height returns the height at x, z.
+//
+// x and z must be in [0, 15]. Height will panic, if this is violated!
+func (c *Chunk) Height(x, z int) int {
+ if (x < 0) || (x > 15) || (z < 0) || (z > 15) {
+ panic(errors.New("x or z parameter was out of range"))
+ }
+
+ return int(c.heightMap[z*16+x])
+}
+
+// TODO: func (c *Chunk) RecalcHeightMap()
diff --git a/mcmap/common.go b/mcmap/common.go
new file mode 100644
index 0000000..9955555
--- /dev/null
+++ b/mcmap/common.go
@@ -0,0 +1,5 @@
+package mcmap
+
+type XZPos struct {
+ X, Z int
+}
diff --git a/mcmap/examples/emeraldfinder/main.go b/mcmap/examples/emeraldfinder/main.go
new file mode 100644
index 0000000..c437d41
--- /dev/null
+++ b/mcmap/examples/emeraldfinder/main.go
@@ -0,0 +1,53 @@
+package main
+
+import (
+ "flag"
+ "fmt"
+ "github.com/kch42/gomcmap/mcmap"
+ "os"
+)
+
+func main() {
+ path := flag.String("path", "", "Path to region directory")
+ flag.Parse()
+
+ if *path == "" {
+ flag.Usage()
+ os.Exit(1)
+ }
+
+ region, err := mcmap.OpenRegion(*path)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Could not open region: %s", err)
+ os.Exit(1)
+ }
+
+chunkLoop:
+ for chunkPos := range region.AllChunks() {
+ cx, cz := chunkPos.X, chunkPos.Z
+ chunk, err := region.Chunk(cx, cz)
+ switch err {
+ case nil:
+ case mcmap.NotAvailable:
+ continue chunkLoop
+ default:
+ fmt.Fprintf(os.Stderr, "Error while getting chunk (%d, %d): %s", cx, cz, err)
+ os.Exit(1)
+ }
+
+ for y := 0; y < 256; y++ {
+ for x := 0; x < 16; x++ {
+ for z := 0; z < 16; z++ {
+ blk := chunk.Block(x, y, z)
+ if blk.ID == mcmap.BlkEmeraldOre {
+ absx, absz := mcmap.ChunkToBlock(cx, cz, x, z)
+ fmt.Printf("%d, %d, %d\n", absx, y, absz)
+ }
+ }
+ }
+ }
+
+ chunk = nil
+ region.UnloadChunk(cx, cz)
+ }
+}
diff --git a/mcmap/prechunk.go b/mcmap/prechunk.go
new file mode 100644
index 0000000..aff3548
--- /dev/null
+++ b/mcmap/prechunk.go
@@ -0,0 +1,257 @@
+package mcmap
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "github.com/kch42/gonbt/nbt"
+ "time"
+)
+
+const (
+ _ = iota
+ compressGZip
+ compressZlib
+)
+
+type preChunk struct {
+ ts time.Time
+ data []byte
+ compression byte
+}
+
+var (
+ UnknownCompression = errors.New("Unknown chunk compression")
+)
+
+func halfbyte(b []byte, i int) byte {
+ if i%2 == 0 {
+ return b[i/2] & 0x0f
+ }
+ return (b[i/2] >> 4) & 0x0f
+}
+
+func extractCoord(tc nbt.TagCompound) (x, y, z int, err error) {
+ var _x, _y, _z int32
+ if _x, err = tc.GetInt("x"); err != nil {
+ return
+ }
+ if _y, err = tc.GetInt("y"); err != nil {
+ return
+ }
+ _z, err = tc.GetInt("z")
+ x, y, z = int(_x), int(_y), int(_z)
+ return
+}
+
+func (pc *preChunk) getLevelTag() (nbt.TagCompound, error) {
+ r := bytes.NewReader(pc.data)
+
+ var root nbt.Tag
+ var err error
+ switch pc.compression {
+ case compressGZip:
+ root, _, err = nbt.ReadGzipdNamedTag(r)
+ case compressZlib:
+ root, _, err = nbt.ReadZlibdNamedTag(r)
+ default:
+ err = UnknownCompression
+ }
+
+ if err != nil {
+ return nil, err
+ }
+
+ if root.Type != nbt.TAG_Compound {
+ return nil, errors.New("Root tag is not a TAG_Compound")
+ }
+
+ lvl, err := root.Payload.(nbt.TagCompound).GetCompound("Level")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read Level tag: %s", err)
+ }
+
+ return lvl, nil
+}
+
+func (pc *preChunk) toChunk() (*Chunk, error) {
+ c := Chunk{ts: pc.ts}
+
+ lvl, err := pc.getLevelTag()
+ if err != nil {
+ return nil, err
+ }
+
+ c.x, err = lvl.GetInt("xPos")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read xPos tag: %s", err)
+ }
+ c.z, err = lvl.GetInt("zPos")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read zPos tag: %s", err)
+ }
+
+ c.lastUpdate, err = lvl.GetLong("LastUpdate")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read LastUpdate tag: %s", err)
+ }
+
+ populated, err := lvl.GetByte("TerrainPopulated")
+ switch err {
+ case nil:
+ case nbt.NotFound:
+ populated = 1
+ default:
+ return nil, fmt.Errorf("Could not read TerrainPopulated tag: %s", err)
+ }
+ c.populated = (populated == 1)
+
+ c.inhabitatedTime, err = lvl.GetLong("InhabitedTime")
+ switch err {
+ case nil:
+ case nbt.NotFound:
+ c.inhabitatedTime = 0
+ default:
+ return nil, fmt.Errorf("Could not read InhabitatedTime tag: %s", err)
+ }
+
+ c.biomes = make([]Biome, 256)
+ biomes, err := lvl.GetByteArray("Biomes")
+ switch err {
+ case nil:
+ for i, bio := range biomes {
+ c.biomes[i] = Biome(bio)
+ }
+ case nbt.NotFound:
+ for i := 0; i < 256; i++ {
+ c.biomes[i] = BioUncalculated
+ }
+ default:
+ return nil, fmt.Errorf("Could not read Biomes tag: %s", err)
+ }
+
+ c.heightMap, err = lvl.GetIntArray("HeightMap")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read HeightMap tag: %s", err)
+ }
+
+ ents, err := lvl.GetList("Entities")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read Entities tag: %s", err)
+ }
+ if ents.Type != nbt.TAG_Compound {
+ c.Entities = []nbt.TagCompound{}
+ } else {
+ c.Entities = make([]nbt.TagCompound, len(ents.Elems))
+ for i, ent := range ents.Elems {
+ c.Entities[i] = ent.(nbt.TagCompound)
+ }
+ }
+
+ sections, err := lvl.GetList("Sections")
+ if (err != nil) || (sections.Type != nbt.TAG_Compound) {
+ return nil, fmt.Errorf("Could not read Section tag: %s", err)
+ }
+
+ c.blocks = make([]Block, 16*16*256)
+ for _, _section := range sections.Elems {
+ section := _section.(nbt.TagCompound)
+
+ y, err := section.GetByte("Y")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read Section -> Y tag: %s", err)
+ }
+ off := int(y) * 4096
+
+ blocks, err := section.GetByteArray("Blocks")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read Section -> Blocks tag: %s", err)
+ }
+ blocksAdd := make([]byte, 4096)
+ add, err := section.GetByteArray("Add")
+ switch err {
+ case nil:
+ for i := 0; i < 4096; i++ {
+ blocksAdd[i] = halfbyte(add, i)
+ }
+ case nbt.NotFound:
+ default:
+ return nil, fmt.Errorf("Could not read Section -> Add tag: %s", err)
+ }
+
+ blkData, err := section.GetByteArray("Data")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read Section -> Data tag: %s", err)
+ }
+ blockLight, err := section.GetByteArray("BlockLight")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read Section -> BlockLight tag: %s", err)
+ }
+ skyLight, err := section.GetByteArray("SkyLight")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read Section -> SkyLight tag: %s", err)
+ }
+
+ for i := 0; i < 4096; i++ {
+ c.blocks[off+i] = Block{
+ ID: BlockID(uint16(blocks[i]) | (uint16(blocksAdd[i]) << 8)),
+ Data: halfbyte(blkData, i),
+ BlockLight: halfbyte(blockLight, i),
+ SkyLight: halfbyte(skyLight, i)}
+ }
+ }
+
+ tileEnts, err := lvl.GetList("TileEntities")
+ if err != nil {
+ return nil, fmt.Errorf("Could not read TileEntities tag: %s", err)
+ }
+ if tileEnts.Type == nbt.TAG_Compound {
+ for _, _tEnt := range tileEnts.Elems {
+ tEnt := _tEnt.(nbt.TagCompound)
+ x, y, z, err := extractCoord(tEnt)
+ if err != nil {
+ return nil, fmt.Errorf("Could not Extract coords: %s", err)
+ }
+
+ _, _, x, z = BlockToChunk(x, z)
+
+ c.blocks[calcBlockOffset(x, y, z)].TileEntity = tEnt
+ }
+ }
+
+ tileTicks, err := lvl.GetList("TileTicks")
+ if (err == nil) && (tileTicks.Type == nbt.TAG_Compound) {
+ for _, _tTick := range tileTicks.Elems {
+ tTick := _tTick.(nbt.TagCompound)
+ x, y, z, err := extractCoord(tTick)
+ if err != nil {
+ return nil, fmt.Errorf("Could not Extract coords: %s", err)
+ }
+
+ _, _, x, z = BlockToChunk(x, z)
+
+ x %= 16
+ z %= 16
+
+ tick := TileTick{}
+ if tick.i, err = tTick.GetInt("i"); err != nil {
+ return nil, fmt.Errorf("Could not read i of a TileTag tag: %s", err)
+ }
+ if tick.t, err = tTick.GetInt("t"); err != nil {
+ return nil, fmt.Errorf("Could not read t of a TileTag tag: %s", err)
+ }
+ switch tick.p, err = tTick.GetInt("p"); err {
+ case nil:
+ tick.hasP = true
+ case nbt.NotFound:
+ tick.hasP = false
+ default:
+ return nil, fmt.Errorf("Could not read p of a TileTag tag: %s", err)
+ }
+
+ c.blocks[calcBlockOffset(x, y, z)].Tick = &tick
+ }
+ }
+
+ return &c, nil
+}
diff --git a/mcmap/region.go b/mcmap/region.go
new file mode 100644
index 0000000..cbb9f58
--- /dev/null
+++ b/mcmap/region.go
@@ -0,0 +1,246 @@
+package mcmap
+
+import (
+ "errors"
+ "fmt"
+ "math"
+ "os"
+ "regexp"
+ "strconv"
+)
+
+var (
+ NotAvailable = errors.New("Chunk or Superchunk not available")
+)
+
+type superchunk struct {
+ preChunks map[XZPos]*preChunk
+ chunks map[XZPos]*Chunk
+ modified bool
+}
+
+type Region struct {
+ path string
+ superchunksAvail map[XZPos]bool
+
+ superchunks map[XZPos]*superchunk
+}
+
+var mcaRegex = regexp.MustCompile(`^r\.([0-9-]+)\.([0-9-]+)\.mca$`)
+
+// OpenRegion opens a region directory.
+func OpenRegion(path string) (*Region, error) {
+ rv := &Region{
+ path: path,
+ superchunksAvail: make(map[XZPos]bool),
+ superchunks: make(map[XZPos]*superchunk),
+ }
+
+ f, err := os.Open(path)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+
+ fi, err := f.Stat()
+ if err != nil {
+ return nil, err
+ }
+
+ if !fi.IsDir() {
+ return nil, fmt.Errorf("%s is not a directory", path)
+ }
+
+ names, err := f.Readdirnames(-1)
+ if err != nil {
+ return nil, err
+ }
+ for _, name := range names {
+ match := mcaRegex.FindStringSubmatch(name)
+ if len(match) == 3 {
+ // We ignore the error here. The Regexp already ensures that the inputs are numbers.
+ x, _ := strconv.ParseInt(match[1], 10, 32)
+ z, _ := strconv.ParseInt(match[2], 10, 32)
+
+ rv.superchunksAvail[XZPos{int(x), int(z)}] = true
+ }
+ }
+
+ return rv, nil
+}
+
+// MaxDims calculates the approximate maximum x, z dimensions of this region in number of chunks. The actual maximum dimensions might be a bit smaller.
+func (reg *Region) MaxDims() (xmin, xmax, zmin, zmax int) {
+ if len(reg.superchunksAvail) == 0 {
+ return 0, 0, 0, 0
+ }
+
+ xmin = math.MaxInt32
+ zmin = math.MaxInt32
+ xmax = math.MinInt32
+ zmax = math.MinInt32
+
+ for pos := range reg.superchunksAvail {
+ if pos.X < xmin {
+ xmin = pos.X
+ }
+ if pos.Z < zmin {
+ zmin = pos.Z
+ }
+ if pos.X > xmax {
+ xmax = pos.X
+ }
+ if pos.Z > zmax {
+ zmax = pos.Z
+ }
+ }
+
+ xmax++
+ zmax++
+ xmin *= 16
+ xmax *= 16
+ zmin *= 16
+ zmax *= 16
+ return
+}
+
+func chunkToSuperchunk(cx, cz int) (scx, scz, rx, rz int) {
+ scx = cx >> 5
+ scz = cz >> 5
+ rx = ((cx % 32) + 32) % 32
+ rz = ((cz % 32) + 32) % 32
+ return
+}
+
+func superchunkToChunk(scx, scz, rx, rz int) (cx, cz int) {
+ cx = scx*32 + rx
+ cz = scz*32 + rz
+ return
+}
+
+func (reg *Region) loadSuperchunk(pos XZPos) error {
+ if !reg.superchunksAvail[pos] {
+ return NotAvailable
+ }
+ fname := fmt.Sprintf("%s%cr.%d.%d.mca", reg.path, os.PathSeparator, pos.X, pos.Z)
+
+ f, err := os.Open(fname)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+
+ pcs, err := readRegionFile(f)
+ if err != nil {
+ return err
+ }
+
+ reg.superchunks[pos] = &superchunk{
+ preChunks: pcs,
+ chunks: make(map[XZPos]*Chunk),
+ }
+ return nil
+}
+
+func (reg *Region) cleanSuperchunks() error {
+ del := make(map[XZPos]bool)
+
+ for scPos, sc := range reg.superchunks {
+ if len(sc.chunks) > 0 {
+ return nil
+ }
+
+ if sc.modified {
+ // TODO: Save superchunk to region file
+ }
+
+ del[scPos] = true
+ }
+
+ for scPos, _ := range del {
+ delete(reg.superchunks, scPos)
+ }
+
+ return nil
+}
+
+func (reg *Region) Chunk(x, z int) (*Chunk, error) {
+ scx, scz, cx, cz := chunkToSuperchunk(x, z)
+ scPos := XZPos{scx, scz}
+ cPos := XZPos{cx, cz}
+
+ sc, ok := reg.superchunks[scPos]
+ if !ok {
+ if err := reg.loadSuperchunk(scPos); err != nil {
+ return nil, err
+ }
+ sc = reg.superchunks[scPos]
+ }
+
+ chunk, ok := sc.chunks[cPos]
+ if !ok {
+ pc, ok := sc.preChunks[cPos]
+ if !ok {
+ return nil, NotAvailable
+ }
+
+ var err error
+ if chunk, err = pc.toChunk(); err != nil {
+ return nil, err
+ }
+ sc.chunks[cPos] = chunk
+ }
+
+ if err := reg.cleanSuperchunks(); err != nil {
+ return nil, err
+ }
+
+ return chunk, nil
+}
+
+// UnloadChunk marks a chunk as unused. If all chunks of a superchunk are marked as unused, the superchunk will be unloaded and saved (if needed).
+func (reg *Region) UnloadChunk(x, z int) {
+ scx, scz, cx, cz := chunkToSuperchunk(x, z)
+ scPos := XZPos{scx, scz}
+ cPos := XZPos{cx, cz}
+
+ sc, ok := reg.superchunks[scPos]
+ if !ok {
+ return
+ }
+
+ chunk, ok := sc.chunks[cPos]
+ if !ok {
+ return
+ }
+
+ if chunk.modified {
+ // TODO: Save to prechunks
+
+ chunk.modified = false
+ sc.modified = true
+ }
+
+ delete(sc.chunks, cPos)
+}
+
+// AllChunks returns a channel that will give you the positions of all possibly available chunks in an efficient order.
+//
+// Note the "possibly available", you still have to check, if the chunk could actually be loaded.
+func (reg *Region) AllChunks() <-chan XZPos {
+ ch := make(chan XZPos)
+ go func(ch chan<- XZPos) {
+ for spos, _ := range reg.superchunksAvail {
+ scx, scz := spos.X, spos.Z
+ for rx := 0; rx < 16; rx++ {
+ for rz := 0; rz < 16; rz++ {
+ cx, cz := superchunkToChunk(scx, scz, rx, rz)
+ ch <- XZPos{cx, cz}
+ }
+ }
+ }
+ close(ch)
+ }(ch)
+
+ return ch
+}
diff --git a/mcmap/regionfile.go b/mcmap/regionfile.go
new file mode 100644
index 0000000..1be08ea
--- /dev/null
+++ b/mcmap/regionfile.go
@@ -0,0 +1,101 @@
+package mcmap
+
+import (
+ "bytes"
+ "encoding/binary"
+ "github.com/kch42/kagus"
+ "io"
+ "time"
+)
+
+const sectorSize = 4096
+
+type chunkOffTs struct {
+ offset, size int64
+ ts time.Time
+}
+
+func (cOff chunkOffTs) readPreChunk(r io.ReadSeeker) (*preChunk, error) {
+ pc := preChunk{ts: cOff.ts}
+
+ if _, err := r.Seek(cOff.offset, 0); err != nil {
+ return nil, err
+ }
+
+ lr := io.LimitReader(r, cOff.size)
+
+ var length uint32
+ if err := binary.Read(lr, binary.BigEndian, &length); err != nil {
+ return nil, err
+ }
+ lr = io.LimitReader(lr, int64(length))
+
+ compType, err := kagus.ReadByte(lr)
+ if err != nil {
+ return nil, err
+ }
+ pc.compression = compType
+
+ buf := new(bytes.Buffer)
+ if _, err := io.Copy(buf, lr); err != nil {
+ return nil, err
+ }
+ pc.data = buf.Bytes()
+
+ return &pc, err
+
+}
+
+func readRegionFile(r io.ReadSeeker) (map[XZPos]*preChunk, error) {
+ if _, err := r.Seek(0, 0); err != nil {
+ return nil, err
+ }
+
+ offs := make(map[XZPos]*chunkOffTs)
+
+ for z := 0; z < 32; z++ {
+ for x := 0; x < 32; x++ {
+ var location uint32
+ if err := binary.Read(r, binary.BigEndian, &location); err != nil {
+ return nil, err
+ }
+
+ if location == 0 {
+ continue
+ }
+
+ offs[XZPos{x, z}] = &chunkOffTs{
+ offset: int64((location >> 8) * sectorSize),
+ size: int64((location & 0xff) * sectorSize),
+ }
+ }
+ }
+
+ for z := 0; z < 32; z++ {
+ for x := 0; x < 32; x++ {
+ pos := XZPos{x, z}
+
+ var ts int32
+ if err := binary.Read(r, binary.BigEndian, &ts); err != nil {
+ return nil, err
+ }
+
+ if _, ok := offs[pos]; !ok {
+ continue
+ }
+
+ offs[pos].ts = time.Unix(int64(ts), 0)
+ }
+ }
+
+ preChunks := make(map[XZPos]*preChunk)
+ for pos, cOff := range offs {
+ pc, err := cOff.readPreChunk(r)
+ if err != nil {
+ return nil, err
+ }
+ preChunks[pos] = pc
+ }
+
+ return preChunks, nil
+}