diff options
Diffstat (limited to 'mcmap')
| -rw-r--r-- | mcmap/biomes.go | 67 | ||||
| -rw-r--r-- | mcmap/block.go | 377 | ||||
| -rw-r--r-- | mcmap/chunk.go | 81 | ||||
| -rw-r--r-- | mcmap/common.go | 5 | ||||
| -rw-r--r-- | mcmap/examples/emeraldfinder/main.go | 53 | ||||
| -rw-r--r-- | mcmap/prechunk.go | 257 | ||||
| -rw-r--r-- | mcmap/region.go | 246 | ||||
| -rw-r--r-- | mcmap/regionfile.go | 101 | 
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 +} | 
