From b9be995232e8299d504e7898a9d6c5384664b8ee Mon Sep 17 00:00:00 2001 From: Kevin Chabowski Date: Sun, 11 Aug 2013 23:01:55 +0200 Subject: Initial commit. Reading maps is already working :-D --- mcmap/biomes.go | 67 +++++++ mcmap/block.go | 377 +++++++++++++++++++++++++++++++++++ mcmap/chunk.go | 81 ++++++++ mcmap/common.go | 5 + mcmap/examples/emeraldfinder/main.go | 53 +++++ mcmap/prechunk.go | 257 ++++++++++++++++++++++++ mcmap/region.go | 246 +++++++++++++++++++++++ mcmap/regionfile.go | 101 ++++++++++ 8 files changed, 1187 insertions(+) create mode 100644 mcmap/biomes.go create mode 100644 mcmap/block.go create mode 100644 mcmap/chunk.go create mode 100644 mcmap/common.go create mode 100644 mcmap/examples/emeraldfinder/main.go create mode 100644 mcmap/prechunk.go create mode 100644 mcmap/region.go create mode 100644 mcmap/regionfile.go 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 +} -- cgit v1.2.3-70-g09d2