mc2mt/mineregion.py

927 lines
43 KiB
Python

# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software Foundation,
# Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# Contributors:
# Originally authored by Acro
# Modified by Phil B
#
#
#
# Acro's Python3.2 NBT Reader for Blender Importing Minecraft
#TODO Possible Key Options for the importer:
#TODO: load custom save locations, rather than default saves folder.
#good for backup/server game reading.
# what's a good way to swap out the world-choice dialogue for a custom path input??
#"Surface only": use the heightmap and only load surface.
#Load more than just the top level, obviously, cos of cliff
#walls, caves, etc. water should count as transparent for this process,
#as should glass, flowers, torches, portal; all nonsolid block types.
#"Load horizon" / "load radius": should be circular, or have options
#import bpy
#from bpy.props import FloatVectorProperty
#from mathutils import Vector
import numpy as npy
#import blockbuild
import sysutil
import blockconversion
#using blockbuild.createMCBlock(mcname, diffuseColour, mcfaceindices)
#faceindices order: (bottom, top, right, front, left, back)
#NB: this should probably change, as it was started by some uv errors.
import nbtreader
#level.dat, .mcr McRegion, .mca Anvil: all different formats, but all are NBT.
import sys, os, gzip
import datetime
from struct import calcsize, unpack, error as StructError
#tag classes: switch/override the read functions once they know what they are
#and interpret payload by making more taggy bits as needed inside self.
#maybe add mcpath as a context var so it can be accessed from operators.
REPORTING = {}
REPORTING['totalchunks'] = 0
totalchunks = 0
wseed = None #store chosen world's worldseed, handy for slimechunk calcs.
MCREGION_VERSION_ID = 0x4abc; # Check world's level.dat 'version' property for these.
ANVIL_VERSION_ID = 0x4abd; #
#TODO: Retrieve these from bpy.props properties stuck in the scene RNA.
EXCLUDED_BLOCKS = [1, 3, 87] #(1,3,87) # hack to reduce loading / slowdown: (1- Stone, 3- Dirt, 87 netherrack). Other usual suspects are Grass,Water, Leaves, Sand,StaticLava
LOAD_AROUND_3D_CURSOR = False #calculates 3D cursor as a Minecraft world position, and loads around that instead of player (or SMP world spawn) position
unknownBlockIDs = set()
OPTIONS = {}
#"Profile" execution checks for measuring whether optimisations are worth it:
REPORTING['blocksread'] = 0
REPORTING['blocksdropped'] = 0
t0 = datetime.datetime.now()
tReadAndBuffered = -1
tToMesh = -1
tChunk0 = -1 #these don't need to be globals - just store the difference in the arrays.
tChunkEnd = -1
tRegion0 = -1
tRegionEnd = -1
tChunkReadTimes = []
tRegionReadTimes = []
WORLD_ROOT = None
#MCBINPATH -- in /bin, zipfile open minecraft.jar, and get terrain.png.
#Feed directly into Blender, or save into the Blender temp dir, then import.
print("Mineblend saved games location: "+sysutil.getMCPath())
#Blockdata: [name, diffuse RGB triple, texture ID list, extra data? (XD/none),
# custom model shape (or None), shape params (or None if not custom mesh),
# and finally dictionary of Cycles params (see blockbuild.)
# TexID list is [bot, top, right, front, left back] or sometimes other orders/lengths if custom model
# Texture IDs are the 1d (2d) count of location of their 16x16 square within terrain.png in minecraft.jar
#Don't store a name for air. Ignore air.
# Order for Blender cube face creation is: [bottom, top, right, front, left, back]
BLOCKDATA = {0: ['Air'],
1: ['Stone', (116,116,116), [308]*6],
2: ['Grass', (95,159,53), [200,148,332,332,332,332]],
3: ['Dirt', (150, 108, 74), [200]*6],
4: ['Cobblestone', (94,94,94), [163]*6],
5: ['WoodenPlank', (159,132,77), [176]*6],
6: ['Sapling', (0,100,0), [20]*6, 'XD', 'cross'],
7: ['Bedrock', [51,51,51], [100]*6],
8: ['WaterFlo', (31,85,255), [2]*6, None, None, None, {'alpha': True}],
9: ['Water', (62,190,255), [2]*6, None, None, None, {'alpha': True}],
10: ['LavaFlo', (252,0,0), [0]*6, None, None, None, {'emit': 1.10, 'stencil': False}],
11: ['Lava', (230,0,0), [0]*6, None, None, None, {'emit': 1.10, 'stencil': False}],
12: ['Sand', (214,208,152), [243]*6],
13: ['Gravel', (154,135,135), [352]*6],
14: ['GoldOre', (252,238,75), [331]*6],
15: ['IronOre', (216,175,147), [395]*6],
16: ['CoalOre', (69,69,69), [161]*6],
17: ['Wood', (76,61,38), [452,452,451,451,451,451], 'XD'],
18: ['Leaves', (99,128,15), [425]*6, None, None, None, {'stencil': False, 'leaf': True}], #TODO: XD colour+texture.
19: ['Sponge', (206,206,70), [244]*6], # FIXME - wet sponge
20: ['Glass', (254,254,254), [263]*6, None, None, None, {'stencil': True}],
21: ['LapisLazuliOre', (28,87,198), [418]*6],
22: ['LapisLazuliBlock', (25,90,205), [417]*6],
23: ['Dispenser', (42,42,42), [262,262,261,41,261,261]], # TODO - front?
24: ['Sandstone', (215,209,153), [307,307,339,339,339,339], 'XD'], #!!
25: ['NoteBlock', (145,88,64), [398]*6], #python sound feature? @see dr epilepsy.
26: ['Bed'], #inset, directional. xd: if head/foot + dirs.
27: ['PwrRail', (204,93,22), [433]*6, 'XD', 'onehigh', None, {'stencil': True}], #meshtype-> "rail". define as 1/16thHeightBlock, read extra data to find orientation.
28: ['DetRail', (134,101,100), [465]*6, 'XD', 'onehigh', None, {'stencil': True}], #change meshtype to "rail" for purposes of slanted bits. later. PLANAR, too. no bottom face.
29: ['StickyPiston', (114,120,70), [109,491,493,493,493,493], 'XD', 'pstn'],
30: ['Cobweb', (237,237,237), [54]*6, 'none', 'cross', None, {'stencil': True}],
# tried 370, 434
31: ['TallGrass', (52,79,45), [213,213,213,213,213,213], 'XD', 'cross', None, {'stencil': True}],
32: ['DeadBush', (148,100,40), [225]*6, None, 'cross', None, {'stencil': True}],
33: ['Piston', (114,120,70), [491,494,493,493,493,493], 'XD', 'pstn'],
34: ['PistonHead', (188,152,98), [494]*6], #or top is 106 if sticky (extra data)
35: ['Wool', (235,235,235), [279]*6, 'XD'], #XD means use xtra data...
37: ['Dandelion', (204,211,2), [79]*6, 'no', 'cross', None, {'stencil': True}],
38: ['Rose', (247,7,15), [207]*6, 'no', 'cross', None, {'stencil': True}],
39: ['BrownMushrm', (204,153,120), [480]*6, 'no', 'cross', None, {'stencil': True}],
40: ['RedMushrm', (226,18,18), [481]*6, 'no', 'cross', None, {'stencil': True}],
41: ['GoldBlock', (255,241,68), [330]*6], # Todo: metalic
42: ['IronBlock', (230,230,230), [394]*6],
43: ['DblSlabs', (255,255,0), [53,53,21,21,21,21], 'XD', 'twoslab'], #xd for type
44: ['Slabs', (255,255,0), [53,53,21,21,21,21], 'XD', 'slab'], #xd for type
45: ['BrickBlock', (124,69,24), [101]*6],
46: ['TNT', (219,68,26), [245,309,277,277,277,277]],
47: ['Bookshelf', (180,144,90), [144,144,5,5,5,5]],
48: ['MossStone', (61,138,61), [164]*6],
49: ['Obsidian', (60,48,86), [141]*6],
50: ['Torch', (240,150,50), [426]*6, 'XD', 'inset', [0,6,7], {'stencil': True}],
51: ['Fire', (255,100,100), [56]*6, None, 'hash', None, {'emit': 1.0, 'stencil': True}], #TODO: Needed for Nether. maybe use hash mesh '#'
52: ['MonsterSpawner', (27,84,124), [65]*6, None, None, None, {'stencil': True}], #xtra data for what's spinning inside it??
53: ['WoodenStairs', (159,132,77), [4,4,4,4,4,4], 'XD', 'stairs'], # TODO
54: ['Chest', (164,114,39), [25,25,26,27,26,26], 'XD', 'chest'], #texface ordering is wrong # TODO
55: ['RedStnWire', (255,0,3), [434]*6, 'XD', 'onehigh', None, {'stencil': True}], #FSM-dependent, may need XD. Also, texture needs to act as bitmask alpha only, onto material colour on this thing. # TODO alpha color
56: ['DiamondOre', (93,236,245), [168]*6],
57: ['DiamondBlock', (93,236,245), [136]*6],
58: ['CraftingTbl', (160,105,60), [197,197,196,195,196,195]],
59: ['Seeds', (160,184,0), [310]*6, 'XD', 'crops', None, {'stencil': True}],
60: ['Farmland', (69,41,21), [200,110,200,200,200,200]],
61: ['Furnace', (42,42,42), [262,262,261,259,261,261]], #[bottom, top, right, front, left, back]
62: ['Burnace', (50,42,42), [262,262,261,259,261,261]],
63: ['SignPost', (159,132,77), [579]*6, 'XD', 'sign'],
64: ['WoodDoor', (145,109,56), [193,193,283,283,283,283], 'XD', 'door', None, {'stencil': True}], # FIXME top/bot
65: ['Ladder', (142,115,60), [416]*6, None, None, None, {'stencil': True}],
66: ['Rail', (172,136,82), [82]*6, 'XD', 'onehigh', None, {'stencil': True}], #to be refined for direction etc.
67: ['CobbleStairs', (77,77,77), [163]*6, 'XD', 'stairs'],
68: ['WallSign', (159,132,77), [579]*6, 'XD', 'wallsign'], #TODO: UVs! + Model!
69: ['Lever', (105,84,51), [426]*6, 'XD', 'lever'],
70: ['StnPressPlate', (110,110,110), [372]*6, 'no', 'onehigh'],
71: ['IronDoor', (183,183,183), [187,187,187,187,187,187], 'XD', 'door', None, {'stencil': True}], # TODO top/bot
72: ['WdnPressPlate', (159,132,77), [4]*6, 'none', 'onehigh'], #TODO
73: ['RedstOre', (151,3,3), [51]*6],
74: ['RedstOreGlowing', (255,3,3), [51]*6], #wth!
75: ['RedstTorchOff', (86,0,0), [83]*6, 'XD', 'inset', [0,6,7]], #TODO Proper RStorch mesh
76: ['RedstTorchOn', (253,0,0), [115]*6, 'XD', 'inset', [0,6,7]], #todo: 'rstorch'
77: ['StoneButton', (116,116,116), [1]*6, 'btn'], # TODO
78: ['Snow', (240,240,240), [180]*6, 'XD', 'onehigh'], #snow has height variants 0-7. 7 is full height block. Curses!
79: ['Ice', (220,220,255), [391]*6],
80: ['SnowBlock', (240,240,240), [180]*6], #xd determines height.
81: ['Cactus', (20,141,36), [70,70,38,38,38,38], 'none', 'cactus'],
82: ['ClayBlock', (170,174,190), [135]*6],
83: ['SugarCane', (130,168,89), [147]*6, None, 'cross', None, {'stencil': True}],
84: ['Jukebox', (145,88,64), [489,399,489,489,489,489]], #XD
85: ['Fence', (160,130,70), [4]*6, 'none', 'fence'], #fence mesh, extra data. #TODO
86: ['Pumpkin', (227,144,29), [113,113,17,464,17,17]],
87: ['Netherrack', (137,15,15), [488]*6],
88: ['SoulSand', (133,109,94), [212]*6],
89: ['Glowstone', (114,111,73), [329]*6, None, None, None, {'emit': 0.95, 'stencil': False}], #cycles: emitter!
90: ['Portal', (150,90,180), None], # TODO - shouldn't this be [208]*6?
91: ['JackOLantern',(227,144,29), [113,113,17,496,17,17], 'XD'], #needs its facing dir.
92: ['Cake', (184,93,39), [124,71,39,39,39,39], 'XD', 'inset', [0,8,1]], # TODO - bot
93: ['RedRepOff', (176,176,176), [179]*6, 'xdcircuit', 'onehigh'], #TODO 'redrep' meshtype
94: ['RedRepOn', (176,176,176), [211]*6, 'xdcircuit', 'onehigh'], #TODO 'redrep' meshtype
#95: ['LockedChest', (164,114,39), [25,25,26,27,26,26], 'xd', 'chest'], #texface order wrong (see #54)
# When stencil set, blocks are non-textured and opaque... unanticipated state?
95: ['StainedGlass', (164,114,39), [327]*6, 'XD', None, None, {'alpha': True}], #texface order wrong (see #54)
96: ['Trapdoor', (117,70,34), [373]*6, 'XD', 'inset', [0,13,0]],
97: ['HiddenSfish', (116,116,116), [335]*6],
98: ['StoneBricks', (100,100,100), [85]*6, 'XD'],
99: ['HgRedM', (210,177,125), [462]*6, 'XD'], #XD for part/variant/colour (stalk/main)
100: ['HgBrwM', (210,177,125), [461]*6, 'XD'],
101: ['IronBars', (171,171,173), [393]*6, 'XD', 'pane'],
102: ['GlassPane', (254,254,254), [263]*6, 'XD', 'pane', None, {'stencil': True}],
103: ['Melon', (166,166,39), [458,458,455,455,455,455]],
104: ['PumpkinStem'], # TODO 457?
105: ['MelonStem'], # TODO 457?
106: ['Vines', (39,98,13), [469]*6, 'XD', 'wallface'],
107: ['FenceGate', (143,115,73), [4]*6], #TODO
108: ['BrickStairs', (135,74,58), [101]*6, 'XD', 'stairs'], #TODO
109: ['StoneBrickStairs', (100,100,100), [85]*6, 'XD', 'stairs'], #TODO
110: ['Mycelium', (122,103,108), [200,483,482,482,482,482]], #useful to ignore option? as this is Dirt top in Mushroom Biomes.
111: ['LilyPad', (12,94,19), [22]*6, 'none', 'onehigh', None, {'stencil': True}],
112: ['NethrBrick', (48,24,28), [484]*6],
113: ['NethrBrickFence', (48,24,28), [484]*6, 'none', 'fence'],
114: ['NethrBrickStairs', (48,24,28), [484]*6, 'XD', 'stairs'],
115: ['NethrWart', (154,39,52), [487]*6],
116: ['EnchantTab', (116,30,29), [141,205,173,173,173,173], 'none', 'inset', [0,4,0]], #TODO enchantable with book?
117: ['BrewStnd', (207,227,186), [157]*6, 'x', 'brewstand'], #fully custom model # TODO
118: ['Cauldron', (55,55,55), [139,138,154,154,154,154]], #fully custom model # TODO
119: ['EndPortal', (0,0,0), None], #TODO
120: ['EndPortalFrame', (144,151,110), [237,175,78,46,46,46,46]],
121: ['EndStone', (144,151,110), [237]*6],
122: ['DragonEgg', (0,0,0)], #TODO
123: ['RedstLampOff', (140,80,44), [498]*6],
124: ['RedstLampOn', (247,201,138), [19]*6, None, None, None, {'emit': 0.95, 'stencil': False}],
129: ['EmeraldOre', (140,80,44), [109]*6],
133: ['EmeraldBlock', (140,80,44), [77]*6],
138: ['Beacon', (247,201,138), [96]*6, None, None, None, {'emit': 1.2, 'stencil': False}], # TODO - encased in glass
152: ['Redstone', (247,201,138), [337]*6],
153: ['NetherQuartzOre', (247,201,138), [369]*6],
155: ['Quartz', (247,201,138), [145]*6], # TODO - variants
159: ['StainedClay', (247,201,138), [384]*6, 'XD'],
162: ['Acacia', (247,201,138), [428,428,427,427,427,427]], # TODO - dark oak
168: ['Prismarine', (127, 255, 212), [432]*6, 'XD'],
169: ['SeaLantern', (247,201,138), [116]*6, None, None, None, {'emit': 1.2, 'stencil': False}], # TODO - encased in glass
170: ['HayBale', (247,201,138), [387,387,386,386,386,386]],
172: ['HardenedClay', (247,201,138), [353]*6],
173: ['BlockOfCoal', (247,201,138), [160]*6],
174: ['PackedIce', (247,201,138), [392]*6],
179: ['RedSandstone', (247,201,138), [306,306,242,242,242,242]*6]
}
#And anything new Mojang add in with each update!
BLOCKVARIANTS = {
#Saplings: normal, spruce, birch and jungle types
6: [ [''],
['Spruce', (57,90,57), [63]*6],
['Birch', (207,227,186), [79]*6],
['Jungle', (57,61,13), [30]*6]
],
17: [ [''],#normal wood (oak)
['Spruce',(76,61,38), [454,454,453,453,453,453]],
['Birch', (76,61,38), [448,448,431,431,431,431]],
['Jungle',(89,70,27), [450,450,449,449,449,449]],
],
#TODO: adjust leaf types, too!
24: [ [''],#normal 'cracked' sandstone
['Decor', (215,209,153), [403,403,301,301,301,301]],
['Smooth',(215,209,153), [403,403,371,371,371,371]],
],
# Tallgrass - TODO
#31: [ [''],
# ['', (,,), []*6],
# ['', (,,), []*6],
# ],
# Wool
35: [ [''],
['Orange', (255,150,54), [119]*6], #custom tex coords!
['Magenta', (227,74,240), [87]*6],
['LightBlue', (83,146,255), [23]*6],
['Yellow', (225,208,31), [311]*6],
['LightGreen', (67,218,53), [55]*6],
['Pink', (248,153,178), [151]*6],
['Grey', (75,75,75), [470]*6],
['LightGrey', (181,189,189), [247]*6],
['Cyan', (45,134,172), [438]*6],
['Purple', (134,53,204), [183]*6],
['Blue', (44,58,176), [374]*6],
['Brown', (99,59,32), [406]*6],
['DarkGreen', (64,89,27), [502]*6],
['Red', (188,51,46), [215]*6],
['Black', (28,23,23), [342]*6]
],
#doubleslabs
#38: [ [''],] # TODO - flowers
43: [ [''], #stone slabs (default)
['SndStn', (215,209,153), [339]*6],
['Wdn', (159,132,77), [176]*6],
['Cobl', (94,94,94), [163]*6],
['Brick', (124,69,24), [101]*6],
['StnBrk', (100,100,100), [85]*6],
[''],
],
#slabs
44: [ [''], #stone slabs (default)
['SndStn', (215,209,153), [192]*6],
['Wdn', (159,132,77), [4]*6],
['Cobl', (94,94,94), [16]*6],
['Brick', (124,69,24), [7]*6],
['StnBrk', (100,100,100), [54]*6],
[''],
],
50: [ [''], #nowt on 0...
['Ea'], #None for colour, none Tex, then: CUSTOM MESH
['We'],
['So'],
['Nr'],
['Up']
],
59: [ ['0', (160,184,0), [88]*6], #?
['1', (160,184,0), [89]*6],
['2', (160,184,0), [90]*6],
['3', (160,184,0), [91]*6],
['4', (160,184,0), [92]*6],
['5', (160,184,0), [93]*6],
['6', (160,184,0), [94]*6],
['7', (160,184,0), [95]*6],
],
# stained glass
95: [ ['White', (255,255,255), [327]*6],
['Orange', (255,150,54), [289]*6], #custom tex coords!
['Magenta', (227,74,240), [288]*6],
['LightBlue', (83,146,255), [267]*6],
['Yellow', (225,208,31), [328]*6],
['LightGreen', (67,218,53), [271]*6],
['Pink', (248,153,178), [323]*6],
['Grey', (75,75,75), [268]*6],
['LightGrey', (181,189,189), [326]*6],
['Cyan', (45,134,172), [270]*6],
['Purple', (134,53,204), [324]*6],
['Blue', (44,58,176), [265]*6],
['Brown', (99,59,32), [266]*6],
['DarkGreen', (64,89,27), [269]*6],
['Red', (188,51,46), [325]*6],
['Black', (28,23,23), [264]*6]
],
#stone brick moss/crack/circle variants:
98: [ [''],
['Mossy', (100,100,100), [181]*6],
['Cracked',(100,100,100), [149]*6],
['Circle', (100,100,100), [117]*6],
],
#hugebrownmush:
99: [ [''], #default (pores on all sides)
['CrTWN',(210,177,125),[142,126,142,142,126,126]],#1
['SdTN',(210,177,125),[142,126,142,142,142,126]],#2
['CrTEN',(210,177,125),[142,126,126,142,142,126]],#3
['SdTW',(210,177,125),[142,126,142,142,126,142]],#4
['Top',(210,177,125),[142,126,142,142,142,142]],#5
['SdTE',(210,177,125),[142,126,126,142,142,142]],#6
['CrTSW',(210,177,125),[142,126,142,126,126,142]],#7
['SdTS',(210,177,125),[142,126,142,126,142,142]],#8
['CrTES',(210,177,125),[142,126,126,126,142,142]],#9
['Stem',(215,211,200),[142,142,141,141,141,141]]#10
],
#hugeredmush:
100:[ [''], #default (pores on all sides)
['CrTWN',(188,36,34),[142,125,142,142,125,125]],#1
['SdTN',(188,36,34),[142,125,142,142,142,125]],#2
['CrTEN',(188,36,34),[142,125,125,142,142,125]],#3
['SdTW',(188,36,34),[142,125,142,142,125,142]],#4
['Top',(188,36,34),[142,125,142,142,142,142]],#5
['SdTE',(188,36,34),[142,125,125,142,142,142]],#6
['CrTSW',(188,36,34),[142,125,142,125,125,142]],#7
['SdTS',(188,36,34),[142,125,142,125,142,142]],#8
['CrTES',(188,36,34),[142,125,125,125,142,142]],#9
['Stem',(215,211,200),[142,142,141,141,141,141]]#10
],
# stained clay
159: [[''], # ['White', (255,255,255), [384]*6],
['Orange', (255,150,54), [363]*6], #custom tex coords!
['Magenta', (227,74,240), [362]*6],
['LightBlue', (83,146,255), [355]*6],
['Yellow', (225,208,31), [385]*6],
['LightGreen', (67,218,53), [361]*6],
['Pink', (248,153,178), [364]*6],
['Grey', (75,75,75), [358]*6],
['LightGrey', (181,189,189), [367]*6],
['Cyan', (45,134,172), [357]*6],
['Purple', (134,53,204), [365]*6],
['Blue', (44,58,176), [354]*6],
['Brown', (99,59,32), [356]*6],
['DarkGreen', (64,89,27), [359]*6],
['Red', (188,51,46), [366]*6],
['Black', (28,23,23), [354]*6]
],
# prismarine
168: [[''],
['Bricks', (127, 255, 212), [368]*6],
['Dark', (127, 255, 212), [400]*6],
]
}
def readLevelDat():
"""Reads the level.dat for info like the world name, player inventory..."""
lvlfile = gzip.open('level.dat', 'rb')
#first byte must be a 10 (TAG_Compound) containing all else.
#read a TAG_Compound...
#rootTag = Tag(lvlfile)
rootTag = nbtreader.TagReader.readNamedTag(lvlfile)[1] #don't care about the name... or do we? Argh, it's a named tag but we throw the blank name away.
print(rootTag.printTree(0)) #give it repr with an indent param...?
def readRegion(fname, vertexBuffer):
#A region has an 8-KILObyte header, of 1024 locations and 1024 timestamps.
#Then from 8196 onwards, it's chunk data and (arbitrary?) gaps.
#Chunks are zlib compressed & have their own structure, more on that later.
print('== Reading region %s ==' % fname)
rfile = open(fname, 'rb')
regionheader = rfile.read(8192)
chunklist = []
chunkcount = 0
cio = 0 #chunk index offset
while cio+4 <= 4096: #only up to end of the locations! (After that is timestamps)
cheadr = regionheader[cio:cio+4]
# 3 bytes "offset" -- how many 4kiB disk sectors away the chunk data is from the start of the file.
# 1 byte "sector count" -- how many 4kiB disk sectors long the chunk data is.
#(sector count is rounded up during save, so gives the last disk sector in which there's data for this chunk)
offset = unpack(">i", b'\x00'+cheadr[0:3])[0]
chunksectorcount = cheadr[3] #last of the 4 bytes is the size (in 4k sectors) of the chunk
chunksLoaded = 0
if offset != 0 and chunksectorcount != 0: #chunks not generated as those coordinates yet will be blank!
chunkdata = readChunk(rfile, offset, chunksectorcount) #TODO Make sure you seek back to where you were to start with ...
chunksLoaded += 1
chunkcount += 1
chunklist.append((offset,chunksectorcount))
cio += 4
rfile.close()
print("Region file %s contains %d chunks." % (fname, chunkcount))
return chunkcount
def toChunkPos(pX,pZ):
return (pX/16, pZ/16)
# def batchBuild(meshBuffer):
# #build all geom from pydata as meshes in one shot. :) This is fast.
# for meshname in (meshBuffer.keys()):
# me = bpy.data.meshes[meshname]
# me.from_pydata(meshBuffer[meshname], [], [])
# me.update()
# def mcToBlendCoord(chunkPos, blockPos):
# """Converts a Minecraft chunk X,Z pair and a Minecraft ordered X,Y,Z block
# location triple into a Blender coordinate vector Vx,Vy,Vz.
# Just remember: in Minecraft, Y points to the sky."""
#
# # Mapping Minecraft coords -> Blender coords
# # In Minecraft, +Z (west) <--- 0 ----> -Z (east), while North is -X and South is +X
# # In Blender, north is +Y, south is-Y, west is -X and east is +X.
# # So negate Z and map it as X, and negate X and map it as Y. It's slightly odd!
#
# vx = -(chunkPos[1] << 4) - blockPos[2]
# vy = -(chunkPos[0] << 4) - blockPos[0] # -x of chunkpos and -x of blockPos (x,y,z)
# vz = blockPos[1] #Minecraft's Y.
#
# return Vector((vx,vy,vz))
# def getMCBlockType(blockID, extraBits):
# """Gets reference to a block type mesh, or creates it if it doesn't exist.
# The mesh created depends on meshType from the global blockdata (whether it's torch or repeater, not a cube)
# These also have to be unique and differently named for directional versions of the same thing - eg track round a corner or up a slope.
# This also ensures material and name are set."""
# import blockbuild
# global OPTIONS #, BLOCKDATA (surely!?)
#
# bdat = BLOCKDATA[blockID]
#
# corename = bdat[0] # eg mcStone, mcTorch
#
# if len(bdat) > 1:
# colourtriple = bdat[1]
# else:
# colourtriple = [214,127,255] #shocking pink
#
# mcfaceindices = None #[]
# if len(bdat) > 2 and bdat[2] is not None:
# mcfaceindices = bdat[2]
#
# usesExtraBits = False
# if len(bdat) > 3:
# usesExtraBits = (bdat[3] == 'XD')
#
# if not usesExtraBits: #quick early create...
# landmeshname = "".join(["mc", corename])
# if landmeshname in bpy.data.meshes:
# return bpy.data.meshes[landmeshname]
# else:
# extraBits = None
#
# objectShape = "box" #but this can change based on extra data too...
# if len(bdat) > 4:
# objectShape = bdat[4]
#
# shapeParams = None
# if len(bdat) > 5: #and objectShape = 'insets'
# shapeParams = bdat[5]
#
# cycParams = None
# if OPTIONS['usecycles']:
# if len(bdat) > 6:
# cycParams = bdat[6]
# if cycParams is None:
# cycParams = {'emit': 0.0, 'stencil': False}
#
# nameVariant = ''
# if blockID in BLOCKVARIANTS:
# variants = BLOCKVARIANTS[blockID]
# if extraBits is not None and extraBits >= 0 and extraBits < len(variants):
# variantData = variants[extraBits]
# if len(variantData) > 0:
# nameVariant = variantData[0]
# #print("%d Block uses extra data: {%d}. So name variant is: %s" % (blockID, extraBits, nameVariant))
# #Now apply each available variant datum: RGB triple, texture faces, and blockbuild variation.
# if len(variantData) > 1: #read custom RGB
# colourtriple = variantData[1]
# if len(variantData) > 2:
# mcfaceindices = variantData[2]
# #mesh constructor...
# corename = "".join([corename, nameVariant])
# meshname = "".join(["mc", corename])
#
# dupblock = blockbuild.construct(blockID, corename, colourtriple, mcfaceindices, extraBits, objectShape, shapeParams, cycParams)
# blockname = dupblock.name
# landmeshname = "".join(["mc", blockname.replace('Block', '')])
#
# if landmeshname in bpy.data.meshes:
# return bpy.data.meshes[landmeshname]
#
# landmesh = bpy.data.meshes.new(landmeshname)
# landob = bpy.data.objects.new(landmeshname, landmesh)
# bpy.context.scene.objects.link(landob)
#
# global WORLD_ROOT #Will have been inited by now. Parent the land to it. (a bit messy, but... meh)
# landob.parent = WORLD_ROOT
# dupblock.parent = landob
# landob.dupli_type = "VERTS"
# return landmesh
# def slimeOn():
# """Creates the cloneable slime block (area marker) and a mesh to duplivert it."""
# if 'slimeChunks' in bpy.data.objects:
# return
#
# #Create cube! (maybe give it silly eyes...)
# #ensure 3d cursor at 0...
#
# bpy.ops.mesh.primitive_cube_add()
# slimeOb = bpy.context.object #get ref to last created ob.
# slimeOb.name = 'slimeMarker'
# #Make it chunk-sized. It starts 2x2x2
# bpy.ops.transform.resize(value=(8, 8, 8))
# bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
#
# # create material for the markers
# slimeMat = None
# smname = "mcSlimeMat"
# if smname in bpy.data.materials:
# slimeMat = bpy.data.materials[smname]
# else:
# slimeMat = bpy.data.materials.new(smname)
# #FIXME - hard code color
# slimeMat.diffuse_color = [86/256.0, 139.0/256.0, 72.0/256.0]
# slimeMat.diffuse_shader = 'OREN_NAYAR'
# slimeMat.diffuse_intensity = 0.8
# slimeMat.roughness = 0.909
# #slimeMat.use_shadeless = True #traceable false!
# slimeMat.use_transparency = True
# slimeMat.alpha = .25
#
# slimeOb.data.materials.append(slimeMat)
# slimeChunkmesh = bpy.data.meshes.new("slimeChunks")
# slimeChunkob = bpy.data.objects.new("slimeChunks", slimeChunkmesh)
# bpy.context.scene.objects.link(slimeChunkob)
# slimeOb.parent = slimeChunkob
# slimeChunkob.dupli_type = "VERTS"
# global WORLD_ROOT
# slimeChunkob.parent = WORLD_ROOT
#
#
# def batchSlimeChunks(slimes):
# #Populate all slime marker centres into the dupli-geom from pydata.
# me = bpy.data.meshes["slimeChunks"]
# me.from_pydata(slimes, [], [])
# me.update()
def getWorldSelectList():
worldList = []
MCSAVEPATH=sysutil.getMCSavePath()
if os.path.exists(MCSAVEPATH):
startpath = os.getcwd()
os.chdir(MCSAVEPATH)
saveList = os.listdir()
saveFolders = [f for f in saveList if os.path.isdir(f)]
wcount = 0
for sf in saveFolders:
if os.path.exists(sf + "/level.dat"):
#Read the actual world name (not just folder name)
wData = None
try:
with gzip.open(sf + '/level.dat', 'rb') as levelDat:
wData = nbtreader.readNBT(levelDat)
#catch errors if level.dat wasn't a gzip...
except IOError:
print("Unknown problem with level.dat format for %s" % sf)
continue
# FIXME - having a problem
try:
if 'LevelName' in wData.value['Data'].value:
wname = wData.value['Data'].value['LevelName'].value
else:
wname = "<no name>"
wsize = wData.value['Data'].value['SizeOnDisk'].value
readableSize = "(%0.1f)" % (wsize / (1024*1024))
worldList.append((sf, sf, wname + " " + readableSize))
wcount += 1
except KeyError:
print("key not found in %s" % wData.value['Data'])
os.chdir(startpath)
if worldList != []:
return worldList
else:
return None
def hasNether(worldFolder):
if worldFolder == "":
return False
worldList = []
MCSAVEPATH=sysutil.getMCSavePath()
if os.path.exists(MCSAVEPATH):
worldList = os.listdir(MCSAVEPATH)
if worldFolder in worldList:
wp = os.path.join(MCSAVEPATH, worldFolder, 'DIM-1')
return os.path.exists(wp)
#and: contains correct files? also check regions aren't empty.
return False
def hasEnd(worldFolder):
if worldFolder == "":
return False
worldList = []
MCSAVEPATH=sysutil.getMCSavePath()
if os.path.exists(MCSAVEPATH):
worldList = os.listdir(MCSAVEPATH)
if worldFolder in worldList:
wp = os.path.join(MCSAVEPATH, worldFolder, 'DIM1')
return os.path.exists(wp)
#and: contains correct files? also check regions aren't empty.
return False
def readMinecraftWorld(worldFolder, loadRadius, toggleOptions):
global unknownBlockIDs, wseed
global EXCLUDED_BLOCKS
global WORLD_ROOT
global OPTIONS, REPORTING
OPTIONS = toggleOptions
#timing/profiling:
global tChunkReadTimes
if worldFolder == "":
#World selected was blank. No saves. i.e. only when world list is empty
print("No valid saved worlds were available to load.")
return
# print("[!] OmitStone: ", toggleOptions['omitstone'])
if not OPTIONS['omitstone']:
EXCLUDED_BLOCKS = []
# print('[[[exluding these blocks: ', EXCLUDED_BLOCKS, ']]]')
worldList = []
MCSAVEPATH=sysutil.getMCSavePath()
if os.path.exists(MCSAVEPATH):
worldList = os.listdir(MCSAVEPATH)
#print("MC Path exists! %s" % os.listdir(MCPATH))
#wherever os was before, save it, and restore it after this completes.
os.chdir(MCSAVEPATH)
worldSelected = worldFolder
os.chdir(os.path.join(MCSAVEPATH, worldSelected))
# If there's a folder DIM-1 in the world folder, you've been to the Nether!
# ...And generated Nether regions.
if os.path.exists('DIM-1'):
if OPTIONS['loadnether']:
print('nether LOAD!')
else:
print('Nether is present, but not chosen to load.')
if os.path.exists('DIM1'):
if OPTIONS['loadend']:
print('load The End...')
else:
print('The End is present, but not chosen to load.')
#if the player didn't save out in those dimensions, we HAVE TO load at 3D cursor (or 0,0,0)
worldData = None
pSaveDim = None
worldFormat = 'mcregion' #assume initially
with gzip.open('level.dat', 'rb') as levelDat:
worldData = nbtreader.readNBT(levelDat)
#print(worlddata.printTree(0))
#Check if it's a multiplayer saved game (that's been moved into saves dir)
#These don't have the Player tag.
if 'Player' in worldData.value['Data'].value:
#It's singleplayer
pPos = [posFloat.value for posFloat in worldData.value['Data'].value['Player'].value['Pos'].value ] #in NBT, there's a lot of value...
pSaveDim = worldData.value['Data'].value['Player'].value['Dimension'].value
print('Player: '+str(pSaveDim)+', ppos: '+str(pPos))
else:
#It's multiplayer.
#Get SpawnX, SpawnY, SpawnZ and centre around those. OR
#TODO: Check for another subfolder: 'players'. Read each NBT .dat in
#there, create empties for all of them, but load around the first one.
spX = worldData.value['Data'].value['SpawnX'].value
spY = worldData.value['Data'].value['SpawnY'].value
spZ = worldData.value['Data'].value['SpawnZ'].value
pPos = [float(spX), float(spY), float(spZ)]
#create empty markers for each player.
#and: could it load multiplayer nether/end based on player loc?
if 'version' in worldData.value['Data'].value:
fmtVersion = worldData.value['Data'].value['version'].value
#19133 for Anvil. 19132 is McRegion.
if fmtVersion == MCREGION_VERSION_ID:
print("World is in McRegion format")
elif fmtVersion == ANVIL_VERSION_ID:
print("World is in Anvil format")
worldFormat = "anvil"
wseed = worldData.value['Data'].value['RandomSeed'].value #it's a Long
print("World Seed : %d" % (wseed)) # or self.report....
#NB: we load at cursor if player location undefined loading into Nether
if OPTIONS['atcursor'] or (OPTIONS['loadnether'] and (pSaveDim is None or int(pSaveDim) != -1)):
# cursorPos = bpy.context.scene.cursor_location
#that's an x,y,z vector (in Blender coords)
#convert to insane Minecraft coords! (Minecraft pos = -Y, Z, -X)
# pPos = [ -cursorPos[1], cursorPos[2], -cursorPos[0]]
pPos = [ 0, 0, 0]
if OPTIONS['loadnether']:
os.chdir(os.path.join("DIM-1", "region"))
elif OPTIONS['loadend']:
os.chdir(os.path.join("DIM1", "region"))
else:
os.chdir("region")
meshBuffer = {}
blockBuffer = {}
#Initialise the world root - an empty to parent all land objects to.
# WORLD_ROOT = bpy.data.objects.new(worldSelected, None) #,None => EMPTY!
# bpy.context.scene.objects.link(WORLD_ROOT)
# WORLD_ROOT.empty_draw_size = 2.0
# WORLD_ROOT.empty_draw_type = 'SPHERE'
regionfiles = []
regionreader = None
if worldFormat == 'mcregion':
regionfiles = [f for f in os.listdir() if f.endswith('.mcr')]
from mcregionreader import ChunkReader
regionreader = ChunkReader() #work it with the class, not an instance?
#all this importing is now very messy.
elif worldFormat == 'anvil':
regionfiles = [f for f in os.listdir() if f.endswith('.mca')]
from mcanvilreader import AnvilChunkReader
regionreader = AnvilChunkReader()
#except when loading nether...
playerChunk = toChunkPos(pPos[0], pPos[2]) # x, z
print("Loading %d blocks around centre." % loadRadius)
#loadRadius = 10 #Sane amount: 5 or 4.
# if not OPTIONS['atcursor']: #loading at player
# #Add an Empty to show where the player is. (+CENTRE CAMERA ON!)
# playerpos = bpy.data.objects.new('PlayerLoc', None)
# #set its coordinates...
# #convert Minecraft coordinate position of player into Blender coords:
# playerpos.location[0] = -pPos[2]
# playerpos.location[1] = -pPos[0]
# playerpos.location[2] = pPos[1]
# bpy.context.scene.objects.link(playerpos)
# playerpos.parent = WORLD_ROOT
#total chunk count across region files:
REPORTING['totalchunks'] = 0
# pX = int(playerChunk[0])
# pZ = int(playerChunk[1])
# ignore the player location, just load around origin
pX = 0
pZ = 0
print('Loading a square halfwidth of %d chunks around load position, so creating chunks: %d,%d to %d,%d' % (loadRadius, pX-loadRadius, pZ-loadRadius, pX+loadRadius, pZ+loadRadius))
# if (OPTIONS['showslimes']):
# # slimeOn()
# import slimes
# slimeBuffer = []
# FIXME - need deltaX/Y/Z to get array index
zeroAdjX = -1 * (pZ-loadRadius)
zeroAdjZ = -1 * (pX-loadRadius)
for z in range(pZ-loadRadius, pZ+loadRadius):
for x in range(pX-loadRadius, pX+loadRadius):
tChunk0 = datetime.datetime.now()
# if (OPTIONS['surfaceOnly']): # new method
# numElements=(loadRadius*2+1)*16 # chunks * blocks
# blockBuffer = npy.zeros((numElements,numElements,numElements))
#
# # FIXME - currently only supported by anvil reader
# regionreader.readChunk2(x,z, blockBuffer, zeroAdjX, zeroAdjZ)
# else: # old
regionreader.readChunk(x,z, meshBuffer) #may need to be further broken down to block level. maybe rename as loadChunk.
tChunk1 = datetime.datetime.now()
chunkTime = tChunk1 - tChunk0
tChunkReadTimes.append(chunkTime.total_seconds()) #tString = "%.2f seconds" % chunkTime.total_seconds() it's a float.
# if (OPTIONS['showslimes']):
# if slimes.isSlimeSpawn(wseed, x, z):
# slimeLoc = mcToBlendCoord((x,z), (8,8,8)) #(8,8,120)
# slimeLoc += Vector((0.5,0.5,-0.5))
# slimeBuffer.append(slimeLoc)
tBuild0 = datetime.datetime.now()
# batchBuild(meshBuffer)
# if (OPTIONS['showslimes']):
# batchSlimeChunks(slimeBuffer)
tBuild1 = datetime.datetime.now()
tBuildTime = tBuild1 - tBuild0
# print("Built meshes in %.2fs" % tBuildTime.total_seconds())
print("%s: loaded %d chunks" % (worldSelected, totalchunks))
if len(unknownBlockIDs) > 0:
print("Unknown new Minecraft datablock IDs encountered:")
print(" ".join(["%d" % bn for bn in unknownBlockIDs]))
#Viewport performance hides:
# if (OPTIONS['fasterViewport']):
# hideIfPresent('mcStone')
# hideIfPresent('mcDirt')
# hideIfPresent('mcSandstone')
# hideIfPresent('mcIronOre')
# hideIfPresent('mcGravel')
# hideIfPresent('mcCoalOre')
# hideIfPresent('mcBedrock')
# hideIfPresent('mcRedstoneOre')
#Profile/run stats:
chunkReadTotal = tChunkReadTimes[0]
for tdiff in tChunkReadTimes[1:]:
chunkReadTotal = chunkReadTotal + tdiff
print("Total read time: %.2fs" % chunkReadTotal) #I presume that's in seconds, ofc... hm.
chunkMRT = chunkReadTotal / len(tChunkReadTimes)
print("Mean chunk read time: %.2fs" % chunkMRT)
print("Block points processed: %d" % REPORTING['blocksread'])
blockMRT = REPORTING['blocksread'] / chunkReadTotal
print("Blocks read/second: %.2fs" % blockMRT)
# print("of those, verts dumped: %d" % REPORTING['blocksdropped'])
# if REPORTING['blocksread'] > 0:
# print("Difference (expected vertex count): %d" % (REPORTING['blocksread'] - REPORTING['blocksdropped']))
# print("Hollowing has made the scene %d%% lighter" % ((REPORTING['blocksdropped'] / REPORTING['blocksread']) * 100))
#increase viewport clip dist to see the world! (or decrease mesh sizes)
#bpy.types.Space...
#Actually: scale world root down to 0.05 by default?
# def hideIfPresent(mName):
# if mName in bpy.data.objects:
# bpy.data.objects[mName].hide = True
# Feature TODOs
# surface load (skin only, not block instances)
# torch, stairs, rails, redrep meshblocks.
# nether load
# mesh optimisations
# multiple loads per run -- need to name new meshes each time load performed, ie mcGrass.001
# ...