diff --git a/.project b/.project new file mode 100644 index 0000000..8b2a184 --- /dev/null +++ b/.project @@ -0,0 +1,17 @@ + + + mc2mt + + + + + + org.python.pydev.PyDevBuilder + + + + + + org.python.pydev.pythonNature + + diff --git a/.pydevproject b/.pydevproject new file mode 100644 index 0000000..57bf285 --- /dev/null +++ b/.pydevproject @@ -0,0 +1,8 @@ + + + +/${PROJECT_DIR_NAME} + +python 3.0 +python3 + diff --git a/.settings/de.loskutov.anyedit.AnyEditTools.prefs b/.settings/de.loskutov.anyedit.AnyEditTools.prefs new file mode 100644 index 0000000..564979b --- /dev/null +++ b/.settings/de.loskutov.anyedit.AnyEditTools.prefs @@ -0,0 +1,18 @@ +activeContentFilterList=*.makefile,makefile,*.Makefile,Makefile,Makefile.*,*.mk,MANIFEST.MF,.project +addNewLine=true +convertActionOnSaave=AnyEdit.CnvrtTabToSpaces +eclipse.preferences.version=1 +fixLineDelimiters=false +ignoreBlankLinesWhenTrimming=false +inActiveContentFilterList= +javaTabWidthForJava=true +org.eclipse.jdt.ui.editor.tab.width=2 +projectPropsEnabled=false +removeTrailingSpaces=true +replaceAllSpaces=false +replaceAllTabs=false +saveAndAddLine=false +saveAndConvert=false +saveAndFixLineDelimiters=false +saveAndTrim=false +useModulo4Tabs=false diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..87d81fc --- /dev/null +++ b/__init__.py @@ -0,0 +1,286 @@ +# io_import_minecraft + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# bl_info = { +# "name": "Import: Minecraft b1.7+", +# "description": "Importer for viewing Minecraft worlds", +# "author": "Adam Crossan (acro)", +# "version": (1,6,3), +# "blender": (2, 6, 0), +# "api": 41226, +# "location": "File > Import > Minecraft", +# "warning": '', # used for warning icon and text in addons panel +# "wiki_url": "http://randomsamples.info/project/mineblend", +# "category": "Import-Export"} + +DEBUG_SCENE=False + +# To support reload properly, try to access a package var, if it's there, reload everything +#if "bpy" in locals(): +# import imp +# if "mineregion" in locals(): +# imp.reload(mineregion) + +#import bpy +#from bpy.props import StringProperty, FloatProperty, IntProperty, BoolProperty, EnumProperty +import imp +import mineregion + +#def setSceneProps(scn): +# #Set up scene-level properties +# bpy.types.Scene.MCLoadNether = BoolProperty( +# name = "Load Nether", +# description = "Load Nether (if present) instead of Overworld.", +# default = False) + +# scn['MCLoadNether'] = False +# return +#setSceneProps(bpy.context.scene) + +# def createTestScene(): +# bpy.ops.scene.new(type='NEW') +# bpy.context.scene.render.engine = 'CYCLES' +# # plane +# bpy.ops.mesh.primitive_plane_add(radius=1, view_align=True, enter_editmode=False, location=(0,0,0), rotation=(0,0,0), layers = (True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# bpy.ops.transform.resize(value=(10,10,10), constraint_axis=(False, False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) +# bpy.ops.material.new() +# # cube +# bpy.ops.mesh.primitive_cube_add(radius=1, view_align=True, enter_editmode=False, location=(0,0,0), rotation=(0,0,0), layers = (True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# # FIXME - error +# #bpy.context.space_data.context='MATERIAL' +# bpy.ops.transform.translate(value=(0.55,0.17,1.14), constraint_axis=(False,False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) +# # set material to leaves? +# bpy.ops.object.editmode_toggle() +# bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) +# # uv mapping - how do we tell blender? +# #bpy.ops.transform.resize(value=(0.0368432,0.0368432,0.0368432), constraint_axis=(False,False,False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) +# #bpy.ops.transform.translate(value=(-0.202301, 0.07906, 0), constraint_axis=(False,False,False), constraint_orientation='GLOBAL', mirror=False, proportional_falloff='SMOOTH', proportional_size=1) +# bpy.ops.object.editmode_toggle() +# # lights... +# bpy.ops.object.lamp_add(type='SUN', view_align=True, location=(-8.12878,5.39259,9.70453), rotation=(-0.383973,0,0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# # camera... +# bpy.ops.object.camera_add(view_align=True, enter_editmode=False, location=(-8.12878,-9.13302,7.87796), rotation=(0,0,0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# #bpy.context.space_data.context='CONSTRAINT' +# bpy.ops.object.constraint_add(type='TRACK_TO') +# bpy.context.object.constraints["Track To"].target = bpy.data.objects["Cube.001"] +# bpy.context.object.constraints["Track To"].track_axis = 'TRACK_NEGATIVE_Z' +# bpy.context.object.constraints["Track To"].up_axis = 'UP_Y' + + +#Menu 'button' for the import menu (which calls the world selector)... +# class MinecraftWorldSelector(bpy.types.Operator): +#"""An operator defining a dialogue for choosing one on-disk Minecraft world to load. +#This supplants the need to call the file selector, since +"""Minecraft worlds require a preset specific folder structure of multiple files which cannot be selected singly.""" + +bl_idname = "mcraft.selectworld" +bl_label = "Select Minecraft World" + +#bl_space_type = "PROPERTIES" +#Possible placements for these: +bl_region_type = "WINDOW" + + +#TODO: Make this much more intuitive for the user! +#Would be better if could define min[x,y,z] and max[x,y,z] and load between these point +mcLoadAtCursor = False #Loads as if 3D cursor offset in viewport was the player (load) position. +mcLowLimit = 60 #The lowest depth layer to load. (High=256, Sea=64, Low=0) +mcHighLimit = 128 #The highest layer to load. (High=256, Sea=64, Low=0) + +mcLoadRadius = 10 # 'Load Radius - The half-width of the load range around load-pos. + # e.g, 4 will load 9x9 chunks around the load centre + # WARNING! Above 10, this gets slow and eats LOTS of memory! +mcOmitStone = False # When True, do not import common blocks such as stone & dirt blocks (overworld) or netherrack (nether). +mcDimenSelectList = '0' #Which dimension should be loaded? - '0'=Overworld; '1'=Nether, '2'=The End +mcShowSlimeSpawns = False #'Display green markers showing slime-spawn locations +mcUseCyclesMats = False #Blender Setting: Set up default materials for use with Cycles Render Engine instead of Blender Internal +mcFasterViewport = False #Blender Setting: Disable display of common blocks (stone, dirt, etc.) in the viewport for better performance. +mcSurfaceOnly = False #Omit underground blocks. Significantly better viewing and rendering performance. +mcOmitMobs = True # When True, do not load mobs (creepers, skeletons, zombies, etc.) in world +#may need to define loadnether and loadend as operators...? + +# omit Dirt toggle option. + +# height-limit option (only load down to a specific height) -- could be semi-dynamic and delve deeper when air value for the +# column in question turns out to be lower than the loading threshold anyway. + +#surfaceOnly ==> only load surface, discard underground areas. Doesn't count for nether. +# Load Nether is, obviously, only available if selected world has nether) +# Load End. Who has The End?! Not I! + +#When specifying a property of type EnumProperty, ensure you call the constructing method correctly. +#Note that items is a set of (identifier, value, description) triples, and default is a string unless you switch on options=ENUM_FLAG in which case make default a set of 1 string. +#Need a better way to handle this variable: (possibly set it as a screen property) + +# import mineregion +wlist = mineregion.getWorldSelectList() +if wlist is not None: + revwlist = wlist[::-1] + mcWorldSelectList = wlist[0][0] #Which Minecraft save should be loaded? +else: + mcWorldSelectList = 0 #Which Minecraft save should be loaded? + + #TODO: on select, check presence of DIM-1 etc. +print("List of Worlds: wlist:: ", wlist) + +netherWorlds = [w[0] for w in wlist if mineregion.hasNether(w[0])] +print("List of worlds with Nether: ", netherWorlds) + +endWorlds = [e[0] for e in wlist if mineregion.hasEnd(e[0])] +print("List of worlds with The End: ", endWorlds) + +#my_worldlist = bpy.props.EnumProperty(items=[('0', "A", "The A'th item"), ('1', 'B', "Bth item"), ('2', 'C', "Cth item"), ('3', 'D', "dth item"), ('4', 'E', 'Eth item')][::-1], default='2', name="World", description="Which Minecraft save should be loaded?") + +# def execute(self, context): + #self.report({"INFO"}, "Loading world: " + str(self.mcWorldSelectList)) + #thread.sleep(30) + #self.report({"WARNING"}, "Foo!") + + #from . import mineregion +# scn = context.scene + +mcLoadDimenNether = True if mcDimenSelectList=='1' else False +mcLoadDimenEnd = True if mcDimenSelectList=='2' else False +# FIXME - when omitmobs is false, mobs will sometimes still not be imported (related to reload issue?) +opts = {"omitstone": mcOmitStone, "showslimes": mcShowSlimeSpawns, "atcursor": mcLoadAtCursor, + "highlimit": mcHighLimit, "lowlimit": mcLowLimit, + "loadnether": mcLoadDimenNether, "loadend": mcLoadDimenEnd, + "usecycles": mcUseCyclesMats, "omitmobs": mcOmitMobs, + "fasterViewport": mcFasterViewport, "surfaceOnly": mcSurfaceOnly} +print(str(mcWorldSelectList)) +print(str(opts)) +#get selected world name instead via bpy.ops.mcraft.worldselected -- the enumeration as a property/operator...? +mineregion.readMinecraftWorld(str(mcWorldSelectList), mcLoadRadius, opts) +# for s in bpy.context.area.spaces: # iterate all space in the active area +# if s.type == "VIEW_3D": # check if space is a 3d-view +# space = s +# space.clip_end = 10000.0 +#run minecraftLoadChunks +#if DEBUG_SCENE: +# createTestScene() + +# return {'FINISHED'} + + +# def invoke(self, context, event): +# context.window_manager.invoke_props_dialog(self, width=350,height=250) +# return {'RUNNING_MODAL'} +# +# +# def draw(self, context): +# layout = self.layout +# col = layout.column() +# col.label(text="Choose import options") +# +# row = col.row() +# row.prop(self, "mcLoadAtCursor") +# +# row = col.row() +# +# sub = col.split(percentage=0.5) +# colL = sub.column(align=True) +# colL.prop(self, "mcShowSlimeSpawns") +# +# cycles = None +# if hasattr(bpy.context.scene, 'cycles'): +# cycles = bpy.context.scene.cycles +# row2 = col.row() +# if cycles is not None: +# row2.active = (cycles is not None) +# row2.prop(self, "mcUseCyclesMats") +# +# row3 = col.row() +# row3.prop(self, "mcOmitStone") +# row3.prop(self, "mcOmitMobs") +# +# row = col.row() +# row.prop(self,"mcFasterViewport") +# #row.prop(self,"mcSurfaceOnly") +# +# #if cycles: +# #like this from properties_data_mesh.py: +# ##layout = self.layout +# ##mesh = context.mesh +# ##split = layout.split() +# ##col = split.column() +# ##col.prop(mesh, "use_auto_smooth") +# ##sub = col.column() +# ##sub.active = mesh.use_auto_smooth +# ##sub.prop(mesh, "auto_smooth_angle", text="Angle") +# #row.operator( +# #row.prop(self, "mcLoadEnd") #detect folder first (per world...) +# +# #label: "loading limits" +# row = layout.row() +# row.prop(self, "mcLowLimit") +# row = layout.row() +# row.prop(self, "mcHighLimit") +# row = layout.row() +# row.prop(self, "mcLoadRadius") +# +# row = layout.row() +# row.prop(self, "mcDimenSelectList") +# #col = layout.column() +# +# row = layout.row() +# row.prop(self, "mcWorldSelectList") +# #row.operator("mcraft.worldlist", icon='') +# col = layout.column() + +# def worldchange(self, context): +# ##UPDATE (ie read then write back the value of) the property in the panel +# #that needs to be updated. ensure it's in the scene so we can get it... +# #bpy.ops.mcraft.selectworld('INVOKE_DEFAULT') +# #if the new world selected has nether, then update the nether field... +# #in fact, maybe do that even if it doesn't. +# #context.scene['MCLoadNether'] = True +# return {'FINISHED'} +# +# class MineMenuItemOperator(bpy.types.Operator): +# bl_idname = "mcraft.launchselector" +# bl_label = "Needs label but label not used" +# +# def execute(self, context): +# bpy.ops.mcraft.selectworld('INVOKE_DEFAULT') +# return {'FINISHED'} +# +# bpy.utils.register_class(MinecraftWorldSelector) +# bpy.utils.register_class(MineMenuItemOperator) +#bpy.utils.register_class(MCraft_PT_worldlist) + +#Forumsearch tip!! FINDME: +#Another way would be to update a property that is displayed in your panel via layout.prop(). AFAIK these are watched and cause a redraw on update. +# +# def mcraft_filemenu_func(self, context): +# self.layout.operator("mcraft.launchselector", text="Minecraft (.region)", icon='MESH_CUBE') +# +# +# def register(): +# #bpy.utils.register_module(__name__) +# bpy.types.INFO_MT_file_import.append(mcraft_filemenu_func) # adds the operator action func to the filemenu +# +# def unregister(): +# #bpy.utils.unregister_module(__name__) +# bpy.types.INFO_MT_file_import.remove(mcraft_filemenu_func) # removes the operator action func from the filemenu +# +# if __name__ == "__main__": +# register() diff --git a/__pycache__/mcanvilreader.cpython-34.pyc b/__pycache__/mcanvilreader.cpython-34.pyc new file mode 100644 index 0000000..12ad75a Binary files /dev/null and b/__pycache__/mcanvilreader.cpython-34.pyc differ diff --git a/__pycache__/mcregionreader.cpython-34.pyc b/__pycache__/mcregionreader.cpython-34.pyc new file mode 100644 index 0000000..cca8768 Binary files /dev/null and b/__pycache__/mcregionreader.cpython-34.pyc differ diff --git a/__pycache__/mineregion.cpython-34.pyc b/__pycache__/mineregion.cpython-34.pyc new file mode 100644 index 0000000..ca3fb19 Binary files /dev/null and b/__pycache__/mineregion.cpython-34.pyc differ diff --git a/__pycache__/nbtreader.cpython-34.pyc b/__pycache__/nbtreader.cpython-34.pyc new file mode 100644 index 0000000..9cb29f3 Binary files /dev/null and b/__pycache__/nbtreader.cpython-34.pyc differ diff --git a/__pycache__/sysutil.cpython-34.pyc b/__pycache__/sysutil.cpython-34.pyc new file mode 100644 index 0000000..bd80d8d Binary files /dev/null and b/__pycache__/sysutil.cpython-34.pyc differ diff --git a/blender.py b/blender.py new file mode 100644 index 0000000..e29928d --- /dev/null +++ b/blender.py @@ -0,0 +1,302 @@ +# io_import_minecraft + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +bl_info = { + "name": "Import: Minecraft b1.7+", + "description": "Importer for viewing Minecraft worlds", + "author": "Adam Crossan (acro)", + "version": (1,6,3), + "blender": (2, 6, 0), + "api": 41226, + "location": "File > Import > Minecraft", + "warning": '', # used for warning icon and text in addons panel + "wiki_url": "http://randomsamples.info/project/mineblend", + "category": "Import-Export"} + +DEBUG_SCENE=False + +# To support reload properly, try to access a package var, if it's there, reload everything +if "bpy" in locals(): + import imp + if "mineregion" in locals(): + imp.reload(mineregion) + +import bpy +from bpy.props import StringProperty, FloatProperty, IntProperty, BoolProperty, EnumProperty +from . import mineregion + +#def setSceneProps(scn): +# #Set up scene-level properties +# bpy.types.Scene.MCLoadNether = BoolProperty( +# name = "Load Nether", +# description = "Load Nether (if present) instead of Overworld.", +# default = False) + +# scn['MCLoadNether'] = False +# return +#setSceneProps(bpy.context.scene) + +def createTestScene(): + bpy.ops.scene.new(type='NEW') + bpy.context.scene.render.engine = 'CYCLES' + # plane + bpy.ops.mesh.primitive_plane_add(radius=1, view_align=True, enter_editmode=False, location=(0,0,0), rotation=(0,0,0), layers = (True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) + bpy.ops.transform.resize(value=(10,10,10), constraint_axis=(False, False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) + bpy.ops.material.new() + # cube + bpy.ops.mesh.primitive_cube_add(radius=1, view_align=True, enter_editmode=False, location=(0,0,0), rotation=(0,0,0), layers = (True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) + # FIXME - error + #bpy.context.space_data.context='MATERIAL' + bpy.ops.transform.translate(value=(0.55,0.17,1.14), constraint_axis=(False,False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) + # set material to leaves? + bpy.ops.object.editmode_toggle() + bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) + # uv mapping - how do we tell blender? + #bpy.ops.transform.resize(value=(0.0368432,0.0368432,0.0368432), constraint_axis=(False,False,False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) + #bpy.ops.transform.translate(value=(-0.202301, 0.07906, 0), constraint_axis=(False,False,False), constraint_orientation='GLOBAL', mirror=False, proportional_falloff='SMOOTH', proportional_size=1) + bpy.ops.object.editmode_toggle() + # lights... + bpy.ops.object.lamp_add(type='SUN', view_align=True, location=(-8.12878,5.39259,9.70453), rotation=(-0.383973,0,0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) + # camera... + bpy.ops.object.camera_add(view_align=True, enter_editmode=False, location=(-8.12878,-9.13302,7.87796), rotation=(0,0,0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) + #bpy.context.space_data.context='CONSTRAINT' + bpy.ops.object.constraint_add(type='TRACK_TO') + bpy.context.object.constraints["Track To"].target = bpy.data.objects["Cube.001"] + bpy.context.object.constraints["Track To"].track_axis = 'TRACK_NEGATIVE_Z' + bpy.context.object.constraints["Track To"].up_axis = 'UP_Y' + + +#Menu 'button' for the import menu (which calls the world selector)... +class MinecraftWorldSelector(bpy.types.Operator): + """An operator defining a dialogue for choosing one on-disk Minecraft world to load. +This supplants the need to call the file selector, since Minecraft worlds require +a preset specific folder structure of multiple files which cannot be selected singly.""" + + bl_idname = "mcraft.selectworld" + bl_label = "Select Minecraft World" + + #bl_space_type = "PROPERTIES" + #Possible placements for these: + bl_region_type = "WINDOW" + + mcLoadAtCursor = bpy.props.BoolProperty(name='Use 3D Cursor as Player', description='Loads as if 3D cursor offset in viewport was the player (load) position.', default=False) + + #TODO: Make this much more intuitive for the user! + mcLowLimit = bpy.props.IntProperty(name='Load Floor', description='The lowest depth layer to load. (High=256, Sea=64, Low=0)', min=0, max=256, step=1, default=60, subtype='UNSIGNED') + mcHighLimit = bpy.props.IntProperty(name='Load Ceiling', description='The highest layer to load. (High=256, Sea=64, Low=0)', min=0, max=256, step=1, default=128, subtype='UNSIGNED') + + mcLoadRadius = bpy.props.IntProperty(name='Load Radius', description="""The half-width of the load range around load-pos. +e.g, 4 will load 9x9 chunks around the load centre +WARNING! Above 10, this gets slow and eats LOTS of memory!""", min=1, max=50, step=1, default=5, subtype='UNSIGNED') #soft_min, soft_max? + #optimiser algorithms/detail omissions + + mcOmitStone = bpy.props.BoolProperty(name='Omit common blocks', description='When checked, do not import common blocks such as stone & dirt blocks (overworld) or netherrack (nether). Significantly improves performance... good for preview imports.', default=False) + + mcDimenSelectList = bpy.props.EnumProperty(items=[('0', 'Overworld', 'Overworld'), ('1', 'Nether', 'Nether'), ('2', 'The End', 'The End')][::1], name="Dimension", description="Which dimension should be loaded?") #default='0' + + mcShowSlimeSpawns = bpy.props.BoolProperty(name='Slime Spawns', description='Display green markers showing slime-spawn locations', default=False) + + mcUseCyclesMats = bpy.props.BoolProperty(name='Use Cycles', description='Set up default materials for use with Cycles Render Engine instead of Blender Internal', default=True) + + mcFasterViewport = bpy.props.BoolProperty(name='Faster viewport', description='Disable display of common blocks (stone, dirt, etc.) in the viewport for better performance. These block types will still be rendered.', default=True) + + mcSurfaceOnly = bpy.props.BoolProperty(name='Surface only', description='Omit underground blocks. Significantly better viewing and rendering performance.', default=False) # FIXME - not yet + + # TODO + #mcGroupBlocks = bpy.props.BoolProperty(name='Group blocks', description='Omit underground blocks. Significantly better viewing and rendering performance.', default=True) + + mcOmitMobs = bpy.props.BoolProperty(name='Omit Mobs', description='When checked, do not load mobs (creepers, skeletons, zombies, etc.) in world', default=True) + #may need to define loadnether and loadend as operators...? + + # omit Dirt toggle option. + + # height-limit option (only load down to a specific height) -- could be semi-dynamic and delve deeper when air value for the + # column in question turns out to be lower than the loading threshold anyway. + + #surfaceOnly ==> only load surface, discard underground areas. Doesn't count for nether. + # Load Nether is, obviously, only available if selected world has nether) + # Load End. Who has The End?! Not I! + + #When specifying a property of type EnumProperty, ensure you call the constructing method correctly. + #Note that items is a set of (identifier, value, description) triples, and default is a string unless you switch on options=ENUM_FLAG in which case make default a set of 1 string. + #Need a better way to handle this variable: (possibly set it as a screen property) + + from . import mineregion + wlist = mineregion.getWorldSelectList() + if wlist is not None: + revwlist = wlist[::-1] + #temp debug REMOVE! + ###dworld = None + ###wnamelist = [w[0] for w in revwlist] + ###if "AnviliaWorld" in wnamelist: + #####build the item for it to be default-selected...? Or work out if ENUM_FLAG is on? + ### dworld = "%d" % wnamelist.index("AnviliaWorld") #set(["AnviliaWorld"]) + ###if dworld is None: + mcWorldSelectList = bpy.props.EnumProperty(items=wlist[::-1], name="World", description="Which Minecraft save should be loaded?") #default='0', update=worldchange + ###else: + ### mcWorldSelectList = bpy.props.EnumProperty(items=wlist[::-1], name="World", description="Which Minecraft save should be loaded?", default=dworld) #, options={'ENUM_FLAG'} + else: + mcWorldSelectList = bpy.props.EnumProperty(items=[], name="World", description="Which Minecraft save should be loaded?") #, update=worldchange + + #TODO: on select, check presence of DIM-1 etc. + #print("wlist:: ", wlist) + netherWorlds = [w[0] for w in wlist if mineregion.hasNether(w[0])] + #print("List of worlds with Nether: ", netherWorlds) + + endWorlds = [e[0] for e in wlist if mineregion.hasEnd(e[0])] + #print("List of worlds with The End: ", endWorlds) + + #my_worldlist = bpy.props.EnumProperty(items=[('0', "A", "The A'th item"), ('1', 'B', "Bth item"), ('2', 'C', "Cth item"), ('3', 'D', "dth item"), ('4', 'E', 'Eth item')][::-1], default='2', name="World", description="Which Minecraft save should be loaded?") + + def execute(self, context): + #self.report({"INFO"}, "Loading world: " + str(self.mcWorldSelectList)) + #thread.sleep(30) + #self.report({"WARNING"}, "Foo!") + + #from . import mineregion + scn = context.scene + + mcLoadDimenNether = True if (self.mcDimenSelectList=='1') else False + mcLoadDimenEnd = True if (self.mcDimenSelectList=='2') else False + # FIXME - when omitmobs is unchecked, mobs will sometimes still not be imported (related to reload issue?) + opts = {"omitstone": self.mcOmitStone, "showslimes": self.mcShowSlimeSpawns, "atcursor": self.mcLoadAtCursor, + "highlimit": self.mcHighLimit, "lowlimit": self.mcLowLimit, + "loadnether": mcLoadDimenNether, "loadend": mcLoadDimenEnd, + "usecycles": self.mcUseCyclesMats, "omitmobs": self.mcOmitMobs, + "fasterViewport": self.mcFasterViewport, "surfaceOnly": self.mcSurfaceOnly} + #print(str(opts)) + #get selected world name instead via bpy.ops.mcraft.worldselected -- the enumeration as a property/operator...? + mineregion.readMinecraftWorld(str(self.mcWorldSelectList), self.mcLoadRadius, opts) + for s in bpy.context.area.spaces: # iterate all space in the active area + if s.type == "VIEW_3D": # check if space is a 3d-view + space = s + space.clip_end = 10000.0 + #run minecraftLoadChunks + if DEBUG_SCENE: + createTestScene() + + return {'FINISHED'} + + + def invoke(self, context, event): + context.window_manager.invoke_props_dialog(self, width=350,height=250) + return {'RUNNING_MODAL'} + + + def draw(self, context): + layout = self.layout + col = layout.column() + col.label(text="Choose import options") + + row = col.row() + row.prop(self, "mcLoadAtCursor") + + row = col.row() + + sub = col.split(percentage=0.5) + colL = sub.column(align=True) + colL.prop(self, "mcShowSlimeSpawns") + + cycles = None + if hasattr(bpy.context.scene, 'cycles'): + cycles = bpy.context.scene.cycles + row2 = col.row() + if cycles is not None: + row2.active = (cycles is not None) + row2.prop(self, "mcUseCyclesMats") + + row3 = col.row() + row3.prop(self, "mcOmitStone") + row3.prop(self, "mcOmitMobs") + + row = col.row() + row.prop(self,"mcFasterViewport") + #row.prop(self,"mcSurfaceOnly") + + #if cycles: + #like this from properties_data_mesh.py: + ##layout = self.layout + ##mesh = context.mesh + ##split = layout.split() + ##col = split.column() + ##col.prop(mesh, "use_auto_smooth") + ##sub = col.column() + ##sub.active = mesh.use_auto_smooth + ##sub.prop(mesh, "auto_smooth_angle", text="Angle") + #row.operator( + #row.prop(self, "mcLoadEnd") #detect folder first (per world...) + + #label: "loading limits" + row = layout.row() + row.prop(self, "mcLowLimit") + row = layout.row() + row.prop(self, "mcHighLimit") + row = layout.row() + row.prop(self, "mcLoadRadius") + + row = layout.row() + row.prop(self, "mcDimenSelectList") + #col = layout.column() + + row = layout.row() + row.prop(self, "mcWorldSelectList") + #row.operator("mcraft.worldlist", icon='') + col = layout.column() + +def worldchange(self, context): + ##UPDATE (ie read then write back the value of) the property in the panel + #that needs to be updated. ensure it's in the scene so we can get it... + #bpy.ops.mcraft.selectworld('INVOKE_DEFAULT') + #if the new world selected has nether, then update the nether field... + #in fact, maybe do that even if it doesn't. + #context.scene['MCLoadNether'] = True + return {'FINISHED'} + +class MineMenuItemOperator(bpy.types.Operator): + bl_idname = "mcraft.launchselector" + bl_label = "Needs label but label not used" + + def execute(self, context): + bpy.ops.mcraft.selectworld('INVOKE_DEFAULT') + return {'FINISHED'} + +bpy.utils.register_class(MinecraftWorldSelector) +bpy.utils.register_class(MineMenuItemOperator) +#bpy.utils.register_class(MCraft_PT_worldlist) + +#Forumsearch tip!! FINDME: +#Another way would be to update a property that is displayed in your panel via layout.prop(). AFAIK these are watched and cause a redraw on update. + +def mcraft_filemenu_func(self, context): + self.layout.operator("mcraft.launchselector", text="Minecraft (.region)", icon='MESH_CUBE') + + +def register(): + #bpy.utils.register_module(__name__) + bpy.types.INFO_MT_file_import.append(mcraft_filemenu_func) # adds the operator action func to the filemenu + +def unregister(): + #bpy.utils.unregister_module(__name__) + bpy.types.INFO_MT_file_import.remove(mcraft_filemenu_func) # removes the operator action func from the filemenu + +if __name__ == "__main__": + register() diff --git a/blender.py~ b/blender.py~ new file mode 100644 index 0000000..e69de29 diff --git a/blockbuild.py b/blockbuild.py new file mode 100644 index 0000000..b00234d --- /dev/null +++ b/blockbuild.py @@ -0,0 +1,1543 @@ +# 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 + +import bpy +from mathutils import * +import os, sys +import sysutil + +DEBUG_BBUV = False +DEBUG_SHADER = False + +#create class for the other functions? BlockBuilder. + +#NICEIF: SpaceView3D.grid_subdivisions = 16 (so they're MC pixel-based) + +TERRAIN_TEXTURE_ATLAS_NAME = 'textures_0.png' # based on the filename used by Minecraft (was terrain.png) +TEXTURE_ATLAS_UNITS = 32 # Number of textures x & y in the texture atlas (was 16) +TEXTURE_ATLAS_PIXELS_PER_UNIT = 16 +TEXTURE_ATLAS_PIXELS = 512 # Pixel w & h of the texture atlas (TEXTURE_ATLAS_UNITS*TEXTURE_ATLAS_PIXELS_PER_UNIT) (was 256) + +def getTextureAtlasU(faceTexId): + return faceTexId % TEXTURE_ATLAS_UNITS + +def getTextureAtlasV(faceTexId): + return int(faceTexId / TEXTURE_ATLAS_UNITS) #int division. + +def getUVUnit(): + #The normalised size of a tx tile within the texture image. + return 1/TEXTURE_ATLAS_UNITS + +def isBMesh(): + majorver = bpy.app.version[0] * 100 + bpy.app.version[1] + return majorver > 262 + #return int(bpy.app.build_revision) > 43451 + +#class BlockBuilder: +# """Defines methods for creating whole-block Minecraft blocks with correct texturing - just needs minecraft path.""" + +def construct(blockID, basename, diffuseRGB, cubeTexFaces, extraData, constructType="box", shapeParams=None, cycParams=None): + # find block function/constructor that matches the construct type. + + #if it's a simple cube... + #stairs + #onehigh + #torch + block = None + if constructType == 'box': + block = createMCBlock(basename, diffuseRGB, cubeTexFaces, cycParams) #extra data + elif constructType == 'onehigh': + block = createInsetMCBlock(basename, diffuseRGB, cubeTexFaces, [0,15,0], cycParams) + elif constructType == '00track': + block = createTrack(basename, diffuseRGB, cubeTexFaces, extraData, cycParams) + #elif constructType == 'hash': #or crop? Is it the same? crops, etc. + elif constructType == 'cross': + block = createXBlock(basename, diffuseRGB, cubeTexFaces, extraData, cycParams) + elif constructType == 'stair': + block = createStairBlock(basename, diffuseRGB, cubeTexFaces, extraData, cycParams) + elif constructType == 'fence': + block = createFenceBlock(basename, diffuseRGB, cubeTexFaces, shapeParams, cycParams) # for this, shape params will be NESW flags. + elif constructType == 'inset': #make an inset box (requires shapeParams) + block = createInsetMCBlock(basename, diffuseRGB, cubeTexFaces, shapeParams, cycParams) #shapeprms must be a 3-list + else: + block = createMCBlock(basename, diffuseRGB, cubeTexFaces, cycParams) #extra data # soon to be removed as a catch-all! + return block + +def getMCTex(): + tname = 'mcTexBlocks' + if tname in bpy.data.textures: + return bpy.data.textures[tname] + + print("creating fresh new minecraft terrain texture") + texNew = bpy.data.textures.new(tname, 'IMAGE') + texNew.image = getMCImg() + # FIXME + #texNew.image.use_premultiply = True + texNew.image.alpha_mode = 'PREMUL' + texNew.use_alpha = True + texNew.use_preview_alpha = True + texNew.use_interpolation = False + texNew.filter_type = 'BOX' #no AA - nice minecraft pixels! + +def getMCImg(): + MCPATH = sysutil.getMCPath() + osdir = os.getcwd() #original os folder before jumping to temp. + if TERRAIN_TEXTURE_ATLAS_NAME in bpy.data.images: + return bpy.data.images[TERRAIN_TEXTURE_ATLAS_NAME] + else: + img = None + temppath = os.path.sep.join([MCPATH, TERRAIN_TEXTURE_ATLAS_NAME]) + print("Mineblend loading terrain: "+temppath) + + if os.path.exists(temppath): + img = bpy.data.images.load(temppath) + else: + # generate a placeholder image for the texture if terrain.png doesn't exist (rather than failing) + print("no terrain texture found... creating empty") + img = bpy.data.images.new(TERRAIN_TEXTURE_ATLAS_NAME, 1024, 1024, True, False) + img.source = 'FILE' + os.chdir(osdir) + return img + + +def getCyclesMCImg(): + #Ideally, we want a very large version of terrain.png to hack around + #cycles' inability to give us control of Alpha in 2.61 + #However, for now it just gives a separate instance of the normal one that + #will need to be scaled up manually (ie replace this image to fix all transparent noodles) + #todo: proper interpolation via nodes + + if 'hiResTerrain.png' not in bpy.data.images: + im1 = None + if TERRAIN_TEXTURE_ATLAS_NAME not in bpy.data.images: + im1 = getMCImg() + else: + im1 = bpy.data.images[TERRAIN_TEXTURE_ATLAS_NAME] + + #Create second version/instance of it. + im2 = im1.copy() + im2.name = 'hiResTerrain.png' + #scale that up / modify... somehow? Add no-interpolation nodes + + return bpy.data.images['hiResTerrain.png'] + + +def createBMeshBlockCubeUVs(blockname, me, matrl, faceIndices): #assume me is a cube mesh. RETURNS **NAME** of the uv layer created. + """Uses faceIndices, a list of per-face MC texture indices, to unwrap + the cube's faces onto their correct places on terrain.png. + Face order for faceIndices is [Bottom,Top,Right,Front,Left,Back]""" + #print("Creating bmesh uvs for: %s" % blockname) + if faceIndices is None: + print("Warning: no face texture for %s" % blockname) + return + + __listtype = type([]) + if type(faceIndices) != __listtype: + if (type(faceIndices) == type(0)): + faceIndices = [faceIndices]*6 + print("Applying singular value to all 6 faces") + else: + print("setting material and uvs for %s: non-numerical face list" % blockname) + print(faceIndices) + raise IndexError("improper face assignment data!") + + if matrl.name not in me.materials: + me.materials.append(matrl) + + uname = blockname + 'UVs' + if uname in me.uv_textures: + blockUVLayer = me.uv_textures[uname] + else: + blockUVLayer = me.uv_textures.new(name=uname) + + #blockUVLoop = me.uv_loop_layers[-1] #works prior to 2.63?? + blockUVLoop = me.uv_layers.active + uvData = blockUVLoop.data + + #bmesh face indices - a mapping to the new cube order + #faceIndices face order is [Bottom,Top,Right,Front,Left,Back] + #BMESH loop face order is [left,back,right,front,bottom,top] (for default cube) + if DEBUG_BBUV: + print("createBMeshBlockCubeUVs "+blockname+" attempting to get faces: "+str(faceIndices)) + bmfi = [faceIndices[4], faceIndices[5], faceIndices[2], faceIndices[3], faceIndices[0], faceIndices[1]] + + #get the loop, and iterate it based on the me.polygons face info. yay! + #The order is a bit off from what might be expected, though... + #And the uv order goes uv2 <-- uv1 + # | ^ + # v | + # uv3 --> uv4 + # It's anticlockwise from top right. + + #the 4 always-the-same offsets from the uv tile to get its corners + #(anticlockwise from top right). + #TODO: get image dimension to automagically work with hi-res texture packs. + uvUnit = getUVUnit() + #16px is 1/16th of the a 256x256 terrain.png. etc. + #calculation of the tile location will get the top left corner, via "* 16". + + # these are the default face uvs, ie topright, topleft, botleft, botright. + uvcorners = [(uvUnit, 0.0), (0.0,0.0), (0.0, -uvUnit), (uvUnit,-uvUnit)] + #uvUnit is subtracted, as Y(v) counts up from image bottom, but I count 0 from top + #top is rotated from default + uvcornersTop = [(uvUnit,-uvUnit), (uvUnit, 0.0), (0.0,0.0), (0.0, -uvUnit)] # 4,1,2,3 + #bottom is rotated and flipped from default + uvcornersBot = [(0.0, -uvUnit), (0.0,0.0), (uvUnit, 0.0), (uvUnit,-uvUnit)] # 3,2,1,4 + + #we have to assign each UV in sequence of the 'loop' for the whole mesh: 24 for a cube. + + xim = getMCImg() + meshtexfaces = blockUVLayer.data.values() + + matrl.game_settings.alpha_blend = 'CLIP' + matrl.game_settings.use_backface_culling = False + + faceNo = 0 #or enumerate me.polygons? + #face order is: [left,back,right,front,bottom,top] + for pface in me.polygons: + face = meshtexfaces[faceNo] + face.image = xim + faceTexId = bmfi[faceNo] + # FIXME - old 16x16 (256x256px) texture map + ##calculate the face location on the uvmap + #mcTexU = faceTexId % 16 + #mcTexV = int(faceTexId / 16) #int division. + ##multiply by square size to get U1,V1 (topleft): + #u1 = (mcTexU * 16.0) / 256.0 # or >> 4 (div by imagesize to get as fraction) + #v1 = (mcTexV * 16.0) / 256.0 # .. + + # New 25x19 (512x512px) texture map + mcTexU = getTextureAtlasU(faceTexId) + mcTexV = getTextureAtlasV(faceTexId) + + u1 = (mcTexU * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS + v1 = (mcTexV * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS + + v1 = 1.0 - v1 #y goes low to high #DEBUG print("That means u1,v1 is %f,%f" % (u1,v1)) + ##DEBUG + if DEBUG_BBUV: + print("mcTexU,mcTexV "+str(mcTexU)+", "+str(mcTexV)+" - u1, v1 %d,%d" % (u1,v1)) + #print("minecraft chunk texture x,y within image: %d,%d" % (mcTexU, mcTexV)) + #if DEBUG_BBUV: + # print("createBMeshBlockCubeUVs "+blockname+" u1, v1: %d,%d" % (u1, v1)) + + loopPolyStart = pface.loop_start #where its verts start in the loop. Yay! + #if loop total's not 4, need to work with ngons or tris or do more complex stuff. + loopPolyCount = pface.loop_total + loopPolyEnd = loopPolyStart + loopPolyCount + + corners = uvcorners + if faceNo == 5: #top face + corners = uvcornersTop + elif faceNo == 4: #bottom face + corners = uvcornersBot + uvx = 0 + for uvc in range(loopPolyStart, loopPolyEnd): + offset = corners[uvx] # 0..3 + mcUV = Vector((u1+offset[0], v1+offset[1])) + #apply the calculated face uv + vert offset to the current loop element + + if DEBUG_BBUV: + print("offset "+str(offset)+", mvUV "+str(mcUV)) + uvData[uvc].uv = mcUV + uvx += 1 + faceNo += 1 + + me.tessface_uv_textures.data.update() #a guess. does this actually help? YES! Without it all the world's grey and textureless! + + return "".join([blockname, 'UVs']) + + +def createBlockCubeUVs(blockname, me, matrl, faceIndices): #assume me is a cube mesh. RETURNS **NAME** of the uv layer created. + #Use faceIndices, a list of per-face MC texture square indices, to unwrap + #the cube's faces to correct places on terrain.png + if faceIndices is None: + print("Warning: no face texture for %s" % blockname) + return + + #Face order is [Bottom,Top,Right,Front,Left,Back] + __listtype = type([]) + if type(faceIndices) != __listtype: + if (type(faceIndices) == type(0)): + faceIndices = [faceIndices]*6 + print("Applying singular value to all 6 faces") + else: + print("setting material and uvs for %s: non-numerical face list" % blockname) + print(faceIndices) + raise IndexError("improper face assignment data!") + + if matrl.name not in me.materials: + me.materials.append(matrl) + + uname = blockname + 'UVs' + blockUVLayer = me.uv_textures.new(uname) #assuming it's not so assigned already, ofc. + xim = getMCImg() + meshtexfaces = blockUVLayer.data.values() + + #Legacy compatibility feature: before 2.60, the alpha clipping is set not + #via the 'game_settings' but in the material... + bver = bpy.app.version[0] + bpy.app.version[1] / 100.0 #eg 2.59 + if bver >= 2.6: + matrl.game_settings.alpha_blend = 'CLIP' + matrl.game_settings.use_backface_culling = False + + for fnum, fid in enumerate(faceIndices): + face = meshtexfaces[fnum] + face.image = xim + if bver < 2.6: + face.blend_type = 'ALPHA' + #use_image + + #Pick UV square off the 2D texture surface based on its Minecraft texture 'index' + #eg 160 for lapis, 49 for glass, etc. + + mcTexU = getTextureAtlasU(fid) + mcTexV = getTextureAtlasV(fid) + + + #multiply by square size to get U1,V1: + u1 = (mcTexU * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS # or >> 4 (div by imagesize to get as fraction) + v1 = (mcTexV * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS # .. + v1 = 1.0 - v1 #y goes low to high for some reason. + + #DEBUG print("That means u1,v1 is %f,%f" % (u1,v1)) + #16px will be 1/16th of the image. + #The image is 256px wide and tall. + + uvUnit = getUVUnit() + + mcUV1 = Vector((u1,v1)) + mcUV2 = Vector((u1+uvUnit,v1)) + mcUV3 = Vector((u1+uvUnit,v1-uvUnit)) #subtract uvunit for y + mcUV4 = Vector((u1, v1-uvUnit)) + + #DEBUG + if DEBUG_BBUV: + print("createBlockCubeUVs Creating UVs for face with values: %f,%f to %f,%f" % (u1,v1,mcUV3[0], mcUV3[1])) + + #We assume the cube faces are always the same order. + #So, face 0 is the bottom. + if fnum == 1: # top + face.uv1 = mcUV2 + face.uv2 = mcUV1 + face.uv3 = mcUV4 + face.uv4 = mcUV3 + elif fnum == 5: #back + face.uv1 = mcUV1 + face.uv2 = mcUV4 + face.uv3 = mcUV3 + face.uv4 = mcUV2 + else: #bottom (0) and all the other sides.. + face.uv1 = mcUV3 + face.uv2 = mcUV2 + face.uv3 = mcUV1 + face.uv4 = mcUV4 + + return "".join([blockname, 'UVs']) + + #References for UV stuff: + +#http://www.blender.org/forum/viewtopic.php?t=15989&view=previous&sid=186e965799143f26f332f259edd004f4 + + #newUVs = cubeMesh.uv_textures.new('lapisUVs') + #newUVs.data.values() -> list... readonly? + + #contains one item per face... + #each item is a bpy_struct MeshTextureFace + #each has LOADS of options + + # .uv1 is a 2D Vector(u,v) + #they go: + + # uv1 --> uv2 + # | + # V + # uv4 <-- uv3 + # + # .. I think + +## For comments/explanation, see above. +def createInsetUVs(blockname, me, matrl, faceIndices, insets): + """Returns name of UV layer created.""" + __listtype = type([]) + if type(faceIndices) != __listtype: + if (type(faceIndices) == type(0)): + faceIndices = [faceIndices]*6 + print("Applying singular value to all 6 faces") + else: + print("setting material and uvs for %s: non-numerical face list" % blockname) + print(faceIndices) + raise IndexError("improper face assignment data!") + + #faceindices: array of minecraft material indices into the terrain.png. + #Face order is [Bottom,Top,Right,Front,Left,Back] + uname = blockname + 'UVs' + blockUVLayer = me.uv_textures.new(uname) + + xim = getMCImg() + #ADD THE MATERIAL! ...but why not earlier than this? uv layer add first? + if matrl.name not in me.materials: + me.materials.append(matrl) + + meshtexfaces = blockUVLayer.data.values() + bver = bpy.app.version[0] + bpy.app.version[1] / 100.0 #eg 2.59 + if bver >= 2.6: + matrl.game_settings.alpha_blend = 'CLIP' + matrl.game_settings.use_backface_culling = False + + #Insets are [bottom,top,sides] + uvUnit = getUVUnit() + uvPixl = uvUnit / TEXTURE_ATLAS_UNITS + iB = insets[0] * uvPixl + iT = insets[1] * uvPixl + iS = insets[2] * uvPixl + for fnum, fid in enumerate(faceIndices): + face = meshtexfaces[fnum] + face.image = xim + + if bver < 2.6: + face.blend_type = 'ALPHA' + + #Pick UV square off the 2D texture surface based on its Minecraft index + #eg 160 for lapis, 49 for glass... etc, makes for x,y: + mcTexU = getTextureAtlasU(fid) + mcTexV = getTextureAtlasV(fid) + #DEBUG print("MC chunk tex x,y in image: %d,%d" % (mcTexU, mcTexV)) + #multiply by square size to get U1,V1: + + u1 = (mcTexU * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS # or >> 4 (div by imagesize to get as fraction) + v1 = (mcTexV * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS + v1 = 1.0 - v1 #y goes low to high for some reason. (er...) + #DEBUG print("That means u1,v1 is %f,%f" % (u1,v1)) + + #16px will be 1/16th of the image. + #The image is 256px wide and tall. + + mcUV1 = Vector((u1,v1)) + mcUV2 = Vector((u1+uvUnit,v1)) + mcUV3 = Vector((u1+uvUnit,v1-uvUnit)) #subtract uvunit for y + mcUV4 = Vector((u1, v1-uvUnit)) + + #DEBUG print("Creating UVs for face with values: %f,%f to %f,%f" % (u1,v1,mcUV3[0], mcUV3[1])) + + #can we assume the cube faces are always the same order? It seems so, yes. + #So, face 0 is the bottom. + if fnum == 0: #bottom + face.uv1 = mcUV3 + face.uv2 = mcUV2 + face.uv3 = mcUV1 + face.uv4 = mcUV4 + + face.uv3 = Vector((face.uv3[0]+iS, face.uv3[1]-iS)) + face.uv2 = Vector((face.uv2[0]-iS, face.uv2[1]-iS)) + face.uv1 = Vector((face.uv1[0]-iS, face.uv1[1]+iS)) + face.uv4 = Vector((face.uv4[0]+iS, face.uv4[1]+iS)) + + elif fnum == 1: # top + face.uv1 = mcUV2 + face.uv2 = mcUV1 + face.uv3 = mcUV4 + face.uv4 = mcUV3 + + #do insets! OMG, they really ARE anticlockwise. ffs. + #why wasn't it right the very, very first time?! + ## Nope. This is messed up. The error is endemic and spread + #through all uv application in this script. + #vertex ordering isn't the problem, script references have + #confused the entire issue. + # uv1(2)-> uv2 (1) + # | + # V + # uv4(3) <-- uv3(4) + face.uv2 = Vector((face.uv2[0]+iS, face.uv2[1]-iS)) + face.uv1 = Vector((face.uv1[0]-iS, face.uv1[1]-iS)) + face.uv4 = Vector((face.uv4[0]-iS, face.uv4[1]+iS)) + face.uv3 = Vector((face.uv3[0]+iS, face.uv3[1]+iS)) + + elif fnum == 5: #back + face.uv1 = mcUV1 + face.uv2 = mcUV4 + face.uv3 = mcUV3 + face.uv4 = mcUV2 + + face.uv1 = Vector((face.uv1[0]+iS, face.uv1[1]-iT)) + face.uv4 = Vector((face.uv4[0]-iS, face.uv4[1]-iT)) + face.uv3 = Vector((face.uv3[0]-iS, face.uv3[1]+iB)) + face.uv2 = Vector((face.uv2[0]+iS, face.uv2[1]+iB)) + + else: #all the other sides.. + face.uv1 = mcUV3 + face.uv2 = mcUV2 + face.uv3 = mcUV1 + face.uv4 = mcUV4 + + face.uv3 = Vector((face.uv3[0]+iS, face.uv3[1]-iT)) + face.uv2 = Vector((face.uv2[0]-iS, face.uv2[1]-iT)) + face.uv1 = Vector((face.uv1[0]-iS, face.uv1[1]+iB)) + face.uv4 = Vector((face.uv4[0]+iS, face.uv4[1]+iB)) + + + return "".join([blockname, 'UVs']) + + +def createBMeshInsetUVs(blockname, me, matrl, faceIndices, insets): + """Uses faceIndices, a list of per-face MC texture indices, to unwrap + the cube's faces onto their correct places on terrain.png. + Uses 3 insets ([bottom,top,sides]) to indent UVs per-face. + Face order for faceIndices is [Bottom,Top,Right,Front,Left,Back]""" + #print("Creating bmesh uvs for: %s" % blockname) + if faceIndices is None: + print("Warning: no face texture for %s" % blockname) + return + + __listtype = type([]) + if type(faceIndices) != __listtype: + if (type(faceIndices) == type(0)): + faceIndices = [faceIndices]*6 + print("Applying singular value to all 6 faces") + else: + print("setting material and uvs for %s: non-numerical face list" % blockname) + print(faceIndices) + raise IndexError("improper face assignment data!") + + if matrl.name not in me.materials: + me.materials.append(matrl) + + uname = blockname + 'UVs' + if uname in me.uv_textures: + blockUVLayer = me.uv_textures[uname] + else: + blockUVLayer = me.uv_textures.new(name=uname) + + #blockUVLoop = me.uv_loop_layers[-1] #Works prior to 2.63! no it doesn't!! + blockUVLoop = me.uv_layers.active + uvData = blockUVLoop.data + + bmfi = [faceIndices[4], faceIndices[5], faceIndices[2], faceIndices[3], faceIndices[0], faceIndices[1]] + uvUnit = getUVUnit() + #Insets are [bottom,top,sides] + uvPixl = uvUnit / TEXTURE_ATLAS_UNITS + iB = insets[0] * uvPixl #insetBottom + iT = insets[1] * uvPixl #insetTop + iS = insets[2] * uvPixl #insetSides + + #Sorry. This array set is going to be dense, horrible, and impenetrable. + #For the simple version of this, see createBMeshUVs, not the insets one + #uvcorners is for sides. Xvalues affected by iS + uvcorners = [(uvUnit-iS, 0.0-iT), (0.0+iS,0.0-iT), (0.0+iS, -uvUnit+iB), (uvUnit-iS,-uvUnit+iB)] + uvcornersTop = [(uvUnit-iS,-uvUnit+iS), (uvUnit-iS, 0.0-iS), (0.0+iS,0.0-iS), (0.0+iS, -uvUnit+iS)] # 4,1,2,3 + uvcornersBot = [(0.0+iS, -uvUnit+iS), (0.0+iS,0.0-iS), (uvUnit-iS, 0.0-iS), (uvUnit-iS,-uvUnit+iS)] # 3,2,1,4 + + xim = getMCImg() + meshtexfaces = blockUVLayer.data.values() + + matrl.game_settings.alpha_blend = 'CLIP' + matrl.game_settings.use_backface_culling = False + + faceNo = 0 #or enumerate me.polygons? + #face order is: [left,back,right,front,bottom,top] + for pface in me.polygons: + face = meshtexfaces[faceNo] + face.image = xim + faceTexId = bmfi[faceNo] + #calculate the face location on the uvmap + mcTexU = getTextureAtlasU(faceTexId) + mcTexV = getTextureAtlasV(faceTexId) + #DEBUG + if DEBUG_BBUV: + print("createBMeshInsetUVs minecraft chunk texture x,y within image: %d,%d" % (mcTexU, mcTexV)) + + #multiply by square size to get U1,V1 (topleft): + u1 = (mcTexU * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS # or >> 4 (div by imagesize to get as fraction) + v1 = (mcTexV * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS # .. + v1 = 1.0 - v1 #y goes low to high #DEBUG print("That means u1,v1 is %f,%f" % (u1,v1)) + + loopPolyStart = pface.loop_start #where its verts start in the loop. Yay! + #if loop total's not 4, need to work with ngons or tris or do more complex stuff. + loopPolyCount = pface.loop_total + loopPolyEnd = loopPolyStart + loopPolyCount + + corners = uvcorners + if faceNo == 5: #top face + corners = uvcornersTop + elif faceNo == 4: #bottom face + corners = uvcornersBot + uvx = 0 + for uvc in range(loopPolyStart, loopPolyEnd): + offset = corners[uvx] # 0..3 + mcUV = Vector((u1+offset[0], v1+offset[1])) + #apply the calculated face uv + vert offset to the current loop element + + uvData[uvc].uv = mcUV + uvx += 1 + faceNo += 1 + + me.tessface_uv_textures.data.update() #Without this, all the world is grey and textureless! + + return "".join([blockname, 'UVs']) + + +# Cycles materials. createNGmc* are for Node Groups and create*CyclesMat are the for the materials that use them. +# +# Aside from simplifying the individual material layouts, the reason for using Node Groups extensively is to allow for users to easily customize the overall look of their scene (i.e. rather than having to modify dozens of materials, some changes can have global effect by modifying a single Node Group, depending on the change desired) + +MC_SHADER_TEX="mcShaderTex" +MC_SHADER_DIFFUSE="mcShaderDiffuse" +MC_SHADER_STENCIL="mcShaderStencil" +MC_SHADER_STENCIL_COLORED="mcShaderStencilColored" +MC_GROUP_TEX_OUTPUT="Color" +MC_GROUP_DIFFUSE_OUTPUT="BSDF" +MC_GROUP_STENCIL_OUTPUT="Shader" +MC_GROUP_STENCIL_COLORED_OUTPUT="Shader" +TYPE_NODE_GROUP_INPUT="NodeGroupInput" +TYPE_NODE_GROUP_OUTPUT="NodeGroupOutput" +TYPE_NODE_GROUP="ShaderNodeGroup" +BSDF_OUTPUT="BSDF" +FACTOR_INPUT="Fac" + +def createNGmcTexture(): + """Node Group for texture. This is a simple texture atlas mapping""" + if DEBUG_SHADER: + print("createNGmcTexture") + ng = bpy.data.node_groups.new(MC_SHADER_TEX,"ShaderNodeTree") + ngo = ng.nodes.new(type=TYPE_NODE_GROUP_OUTPUT) + texCoord = ng.nodes.new(type="ShaderNodeTexCoord") + imageTex = ng.nodes.new(type="ShaderNodeTexImage") + imageTex.image = getMCImg() + imageTex.interpolation = "Closest" + + ng.links.new(imageTex.inputs[0],texCoord.outputs[2]) # link the texCoord uv to the imageTex vector + ng.links.new(ngo.inputs[0],imageTex.outputs[0]) + ng.links.new(ngo.inputs[1],imageTex.outputs[1]) + + texCoord.location = Vector((-200, 200)) + imageTex.location = Vector((0, 200)) + ngo.location = Vector((200, 200)) + +def setNodeGroup(node,ngName): + if DEBUG_SHADER: + print("setNodeGroup: "+ngName) + # FIXME - is there a better way to use a node group from within a node group? + node.name=ngName + node.label=ngName + node.node_tree=bpy.data.node_groups[ngName] + +def createNGmcDiffuse(): + """Node Group for diffuse materials""" + if DEBUG_SHADER: + print("createNGmcDiffuse") + ng = bpy.data.node_groups.new(MC_SHADER_DIFFUSE,"ShaderNodeTree") + ngo = ng.nodes.new(type=TYPE_NODE_GROUP_OUTPUT) + tex = ng.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(tex,MC_SHADER_TEX) + diffuse = ng.nodes.new(type="ShaderNodeBsdfDiffuse") + + ng.links.new(diffuse.inputs[0],tex.outputs[0]) # link the texCoord uv to the imageTex vector + ng.links.new(ngo.inputs[0],diffuse.outputs[0]) + ng.links.new(ngo.inputs[1],tex.outputs[1]) # For stained glass etc + + tex.location = Vector((0, 0)) + diffuse.location = Vector((200, 200)) + ngo.location = Vector((400, 0)) + +#def createNGmcStencil(): # FIXME - how to handle alternate node data flows? (i.e. loopback / inner node group issue) +# ng = bpy.data.node_groups.new(MC_SHADER_STENCIL,"ShaderNodeTree") +# ngo = ng.nodes.new(type="NodeGroupOutput") +# ngi = ng.nodes.new(type="NodeGroupInput") +# diffNode = ng.nodes.new(type="ShaderNodeGroup") +# setNodeGroup(diffNode,MC_SHADER_DIFFUSE) +# +# links = ng.links +# +# rgbtobwNode = ng.nodes.new(type="ShaderNodeRGBToBW") +# gtNode = ng.nodes.new(type="ShaderNodeMath") +# gtNode.name = "AlphaBlackGT" +# gtNode.operation = 'GREATER_THAN' +# gtNode.inputs[0].default_value = 0.001 +# +# transpNode = ng.nodes.new(type="ShaderNodeBsdfTransparent") +# mixNode = ng.nodes.new(type="ShaderNodeMixShader") +# +# ngi.location = Vector((-200,0)) +# diffNode.location = Vector((0,0)) +# rgbtobwNode.location = Vector((200,200)) +# gtNode.location = Vector((400,200)) +# transpNode.location = Vector((400,-200)) +# mixNode.location = Vector((600,0)) +# ngo.location = Vector((800,0)) +# +# links.new(input=diffNode.outputs[MC_GROUP_DIFFUSE_OUTPUT], output=rgbtobwNode.inputs['Color']) +# links.new(input=rgbtobwNode.outputs['Val'], output=gtNode.inputs[1]) +# links.new(input=gtNode.outputs['Value'], output=mixNode.inputs['Fac']) +# +# #links.new(input=diffNode.outputs[MC_GROUP_DIFFUSE_OUTPUT], output=diff2Node.inputs['Color']) +# #links.new(input=diff2Node.outputs['BSDF'], output=mixNode.inputs[1]) +# +# # leave the options open +# #links.new(input=diffNode.outputs['BSDF'], output=mixNode.inputs[1]) +# +# links.new(input=transpNode.outputs['BSDF'], output=mixNode.inputs[2]) +# +# links.new(input=ngi.outputs[0], output=mixNode.inputs[1]) +# +# #links.new(input=mixNode.outputs['Shader'], output=ngo.inputs['Surface']) +# links.new(input=mixNode.outputs['Shader'], output=ngo.inputs[0]) +# links.new(input=diffNode.outputs['BSDF'], output=ngo.inputs[1]) + +def createNGmcStencil(): + """Node Group for stencil materials (i.e. colored textures with alpha)""" + if DEBUG_SHADER: + print("createNGmcStencil") + ng = bpy.data.node_groups.new(MC_SHADER_STENCIL,"ShaderNodeTree") + ngo = ng.nodes.new(type=TYPE_NODE_GROUP_OUTPUT) + ngi = ng.nodes.new(type=TYPE_NODE_GROUP_INPUT) + inNode = ng.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(inNode,MC_SHADER_TEX) + + links = ng.links + + diffNode = ng.nodes.new(type="ShaderNodeBsdfDiffuse") + rgbtobwNode = ng.nodes.new(type="ShaderNodeRGBToBW") + gtNode = ng.nodes.new(type="ShaderNodeMath") + gtNode.name = "AlphaBlackGT" + gtNode.operation = 'GREATER_THAN' + gtNode.inputs[0].default_value = 0.001 + + transpNode = ng.nodes.new(type="ShaderNodeBsdfTransparent") + mixNode = ng.nodes.new(type="ShaderNodeMixShader") + + ngi.location = Vector((-200,0)) + inNode.location = Vector((0,0)) + rgbtobwNode.location = Vector((200,200)) + diffNode.location = Vector((200,0)) + gtNode.location = Vector((400,200)) + transpNode.location = Vector((400,-200)) + mixNode.location = Vector((600,0)) + ngo.location = Vector((800,0)) + + links.new(input=inNode.outputs[MC_GROUP_TEX_OUTPUT], output=rgbtobwNode.inputs['Color']) + links.new(input=rgbtobwNode.outputs['Val'], output=gtNode.inputs[1]) + links.new(input=gtNode.outputs['Value'], output=mixNode.inputs[FACTOR_INPUT]) + + # leave the options open + links.new(input=inNode.outputs[MC_GROUP_TEX_OUTPUT], output=diffNode.inputs['Color']) + links.new(input=diffNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[1]) + + links.new(input=transpNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[2]) + + links.new(input=mixNode.outputs['Shader'], output=ngo.inputs[0]) + +def createNGmcStencilColored(): + """Node Group for colored stencil materials (i.e. grey scale texture with alpha that needs to be colored)""" + if DEBUG_SHADER: + print("createNGmcStencilColored") + ng = bpy.data.node_groups.new(MC_SHADER_STENCIL_COLORED,"ShaderNodeTree") + ngo = ng.nodes.new(type=TYPE_NODE_GROUP_OUTPUT) + ngi = ng.nodes.new(type=TYPE_NODE_GROUP_INPUT) + texNode = ng.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(texNode,MC_SHADER_TEX) + + links = ng.links + + rgbtobwNode = ng.nodes.new(type="ShaderNodeRGBToBW") + gtNode = ng.nodes.new(type="ShaderNodeMath") + gtNode.name = "AlphaBlackGT" + gtNode.operation = 'GREATER_THAN' + gtNode.inputs[0].default_value = 0.001 + + transpNode = ng.nodes.new(type="ShaderNodeBsdfTransparent") + alphaMixNode = ng.nodes.new(type="ShaderNodeMixShader") + + ngi.location = Vector((-200,0)) + texNode.location = Vector((0,0)) + rgbtobwNode.location = Vector((200,200)) + gtNode.location = Vector((400,200)) + transpNode.location = Vector((400,-200)) + alphaMixNode.location = Vector((600,0)) + ngo.location = Vector((800,0)) + + links.new(input=texNode.outputs[MC_GROUP_TEX_OUTPUT], output=rgbtobwNode.inputs['Color']) + links.new(input=rgbtobwNode.outputs['Val'], output=gtNode.inputs[1]) + links.new(input=gtNode.outputs['Value'], output=alphaMixNode.inputs[FACTOR_INPUT]) + + links.new(input=transpNode.outputs[BSDF_OUTPUT], output=alphaMixNode.inputs[2]) + + links.new(input=alphaMixNode.outputs['Shader'], output=ngo.inputs[0]) + + ## 'colored' specific portion of material + colorMixNode = ng.nodes.new(type="ShaderNodeMixRGB") + #colorMixNode.inputs[1].name="Dark color" + #colorMixNode.inputs[2].name="Light color" + colorDiffNode = ng.nodes.new(type="ShaderNodeBsdfDiffuse") + + colorMixNode.location = Vector((0,400)) + colorDiffNode.location = Vector((200,400)) + + links.new(input=rgbtobwNode.outputs['Val'], output=colorMixNode.inputs[FACTOR_INPUT]) + # FIXME - material will not render correctly when names are set (i.e. even though the viewport looks fine, the rgb color mix needs to be re-added and links re-established for successful (i.e. non-black) render.) + #colorMixNode.inputs[1].name="Dark color" + #colorMixNode.inputs[2].name="Light color" + #links.new(input=ngi.outputs[0], output=colorMixNode.inputs["Dark color"]) + #links.new(input=ngi.outputs[1], output=colorMixNode.inputs["Light color"]) + links.new(input=ngi.outputs[0], output=colorMixNode.inputs[1]) + links.new(input=ngi.outputs[1], output=colorMixNode.inputs[2]) + + links.new(input=colorMixNode.outputs["Color"], output=colorDiffNode.inputs["Color"]) + links.new(input=colorDiffNode.outputs[BSDF_OUTPUT], output=alphaMixNode.inputs[1]) + ngi.outputs[1].name="Dark color" + ngi.outputs[2].name="Light color" + +def createNodeGroups(): + """Create node groups if they don't already exist""" + if DEBUG_SHADER: + print("createNodeGroups") + existsNode = bpy.data.node_groups.get(MC_SHADER_DIFFUSE) + if existsNode==None: + createNGmcTexture() + createNGmcDiffuse() + createNGmcStencil() + createNGmcStencilColored() + +def removeExistingDiffuseNode(ntree): + olddif = ntree.nodes['Diffuse BSDF'] + ntree.nodes.remove(olddif) + +def createDiffuseCyclesMat(mat): + """Create a basic textured, diffuse material that uses existing UV mapping into texture atlas""" + if DEBUG_SHADER: + print("createDiffuseCyclesMat") + #compatibility with Blender 2.5x: + if not hasattr(bpy.context.scene, 'cycles'): + print("No cycles support... skipping") + return + + #Switch render engine to Cycles. Yippee ki-yay! + if bpy.context.scene.render.engine != 'CYCLES': + bpy.context.scene.render.engine = 'CYCLES' + + mat.use_nodes = True + + #maybe check number of nodes - there should be 2. + ntree = mat.node_tree + mcdif = ntree.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(mcdif,MC_SHADER_DIFFUSE) + removeExistingDiffuseNode(ntree) + matOutNode = ntree.nodes['Material Output'] + ntree.links.new(input=mcdif.outputs[MC_GROUP_DIFFUSE_OUTPUT], output=matOutNode.inputs['Surface']) + mcdif.location = Vector((0,0)) + matOutNode.location = Vector((200,0)) + +def createEmissionCyclesMat(mat, emitAmt): + """Emissive materials such as lava, glowstone, etc""" + if DEBUG_SHADER: + print("createEmissionCyclesMat") + if bpy.context.scene.render.engine != 'CYCLES': + bpy.context.scene.render.engine = 'CYCLES' + + mat.use_nodes = True + + ntree = mat.node_tree #there will now be 4 nodes in there, one of them being the diffuse shader. + removeExistingDiffuseNode(ntree) + diffNode = ntree.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(diffNode,MC_SHADER_TEX) + matNode = ntree.nodes['Material Output'] + emitNode = ntree.nodes.new(type='ShaderNodeEmission') + nodes = ntree.nodes + links = ntree.links + + diffNode.location = Vector((0,0)) + emitNode.location = Vector((200,0)) + matNode.location = Vector((400,0)) + + #change links: delete the old links and add new ones. + + emitNode.inputs['Strength'].default_value = float(emitAmt) #set this from the EMIT value of data passed in. + + bsdfDiffSockOut = diffNode.outputs[MC_GROUP_TEX_OUTPUT] + emitSockOut = emitNode.outputs[0] + + for nl in links: + print("link "+str(nl)) + if nl.to_socket == matNode: + links.remove(nl) + + links.new(input=diffNode.outputs[0], output=emitNode.inputs[0]) + links.new(input=emitNode.outputs[0], output=matNode.inputs['Surface']) + + +def createStencilCyclesMat(mat): + """Stencil materials such as flowers""" + if DEBUG_SHADER: + print("createStencilCyclesMat") + #Ensure Cycles is in use + if bpy.context.scene.render.engine != 'CYCLES': + bpy.context.scene.render.engine = 'CYCLES' + mat.use_nodes = True + + ntree = mat.node_tree + nodes = ntree.nodes + links = ntree.links + inNode = ntree.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(inNode,MC_SHADER_TEX) + diffNode = nodes["Diffuse BSDF"] # reuse the one that already exists + matNode = nodes['Material Output'] + + rgbtobwNode = ntree.nodes.new(type="ShaderNodeRGBToBW") + gtNode = ntree.nodes.new(type="ShaderNodeMath") + gtNode.name = "AlphaBlackGT" + gtNode.operation = 'GREATER_THAN' + gtNode.inputs[0].default_value = 0.001 + + transpNode = ntree.nodes.new(type="ShaderNodeBsdfTransparent") + mixNode = ntree.nodes.new(type="ShaderNodeMixShader") + + inNode.location = Vector((0,0)) + diffNode.location = Vector((200,0)) + rgbtobwNode.location = Vector((200,200)) + gtNode.location = Vector((400,200)) + transpNode.location = Vector((400,-200)) + mixNode.location = Vector((600,0)) + matNode.location = Vector((800,0)) + + for nl in links: + if nl.to_socket == matNode: + links.remove(nl) + + links.new(input=inNode.outputs[MC_GROUP_TEX_OUTPUT], output=rgbtobwNode.inputs['Color']) + links.new(input=rgbtobwNode.outputs['Val'], output=gtNode.inputs[1]) + links.new(input=gtNode.outputs['Value'], output=mixNode.inputs[FACTOR_INPUT]) + + links.new(input=inNode.outputs[MC_GROUP_TEX_OUTPUT], output=diffNode.inputs["Color"]) + links.new(input=diffNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[1]) + + links.new(input=transpNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[2]) + + links.new(input=mixNode.outputs['Shader'], output=matNode.inputs['Surface']) + + +def createLeafCyclesMat(mat): + """Colored stencil materials such as leaves""" + if DEBUG_SHADER: + print("createLeafCyclesMat") + """Very similar to the transparent (glass) material but different enough to need its own""" + #Ensure Cycles is in use + if bpy.context.scene.render.engine != 'CYCLES': + bpy.context.scene.render.engine = 'CYCLES' + mat.use_nodes = True + + ntree = mat.node_tree #there will now be 4 nodes in there, one of them being the diffuse shader. + removeExistingDiffuseNode(ntree) + nodes = ntree.nodes + links = ntree.links + stencilNode = ntree.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(stencilNode,MC_SHADER_STENCIL_COLORED) + matNode = nodes['Material Output'] + darkColorNode = ntree.nodes.new(type="ShaderNodeRGB") + darkColorNode.outputs[0].default_value = (0.01, 0.0185002, 0.0137021, 1) + lightColorNode = ntree.nodes.new(type="ShaderNodeRGB") + lightColorNode.outputs[0].default_value = (0.098, 0.238398, 0.135633, 1) + + darkColorNode.location = Vector((0, 200)) + lightColorNode.location = Vector((0, 0)) + stencilNode.location = Vector((400,0)) + matNode.location = Vector((600,0)) + + links.new(input=darkColorNode.outputs['Color'], output=stencilNode.inputs[0]) + links.new(input=lightColorNode.outputs['Color'], output=stencilNode.inputs[1]) + links.new(input=stencilNode.outputs['Shader'], output=matNode.inputs['Surface']) + +def createLeafCyclesMatOld(mat): + """Very similar to the transparent (glass) material but different enough to need its own""" + if DEBUG_SHADER: + print("createLeafCyclesMat") + #Ensure Cycles is in use + if bpy.context.scene.render.engine != 'CYCLES': + bpy.context.scene.render.engine = 'CYCLES' + mat.use_nodes = True + + ntree = mat.node_tree #there will now be 4 nodes in there, one of them being the diffuse shader. + nodes = ntree.nodes + links = ntree.links + removeExistingDiffuseNode(ntree) + diffNode = ntree.nodes.new(type=TYPE_NODE_GROUP) + setNodeGroup(diffNode,MC_SHADER_TEX) + matNode = nodes['Material Output'] + + rgbtobwNode = ntree.nodes.new(type="ShaderNodeRGBToBW") + gtNode = ntree.nodes.new(type="ShaderNodeMath") + gtNode.name = "AlphaBlackGT" + gtNode.operation = 'GREATER_THAN' + gtNode.inputs[0].default_value = 0.001 + + transpNode = ntree.nodes.new(type="ShaderNodeBsdfTransparent") + mixNode = ntree.nodes.new(type="ShaderNodeMixShader") + + diffNode.location = Vector((0,0)) + rgbtobwNode.location = Vector((200,200)) + gtNode.location = Vector((400,200)) + transpNode.location = Vector((400,-200)) + mixNode.location = Vector((600,0)) + matNode.location = Vector((800,0)) + + for nl in links: + if nl.to_socket == matNode: + links.remove(nl) + + links.new(input=diffNode.outputs[MC_GROUP_TEX_OUTPUT], output=rgbtobwNode.inputs['Color']) + links.new(input=rgbtobwNode.outputs['Val'], output=gtNode.inputs[1]) + links.new(input=gtNode.outputs['Value'], output=mixNode.inputs[FACTOR_INPUT]) + + # Leaf difference: feed the matl color into transparent... not needed? FIXME + links.new(input=transpNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[2]) + + links.new(input=mixNode.outputs['Shader'], output=matNode.inputs['Surface']) + + # Leaf specific portion of material + lcrampNode = ntree.nodes.new(type="ShaderNodeValToRGB") + lcrampNode.color_ramp.elements[1].color = (0.098, 0.238398, 0.135633, 1) + lcrampNode.color_ramp.elements[0].color = (0.01, 0.0185002, 0.0137021, 1) + ldiffNode = ntree.nodes.new(type="ShaderNodeBsdfDiffuse") + + lcrampNode.location = Vector((400,500)) + ldiffNode.location = Vector((700,500)) + + links.new(input=rgbtobwNode.outputs['Val'], output=lcrampNode.inputs[FACTOR_INPUT]) + links.new(input=lcrampNode.outputs['Color'], output=ldiffNode.inputs['Color']) + links.new(input=ldiffNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[1]) + +def createPlainAlphaCyclesMat(mat): + """Partially transparent materials such as stained glass""" + if DEBUG_SHADER: + print("createPlainAlphaCyclesMat") + #Ensure Cycles is in use + if bpy.context.scene.render.engine != 'CYCLES': + bpy.context.scene.render.engine = 'CYCLES' + mat.use_nodes = True + + ntree = mat.node_tree + nodes = ntree.nodes + + createDiffuseCyclesMat(mat) + diffNode = nodes[MC_SHADER_DIFFUSE] + matNode = nodes['Material Output'] + + transpNode = ntree.nodes.new(type="ShaderNodeBsdfTransparent") + mixNode = ntree.nodes.new(type="ShaderNodeMixShader") + + diffNode.location = Vector((0,0)) + transpNode.location = Vector((200,-200)) + mixNode.location = Vector((400,0)) + matNode.location = Vector((600,0)) + + links = ntree.links + # FIXME - does reversing order of inputs give the right effect for stained glass? + links.new(input=diffNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[2]) + links.new(input=diffNode.outputs['Alpha'], output=mixNode.inputs[FACTOR_INPUT]) + links.new(input=transpNode.outputs[BSDF_OUTPUT], output=mixNode.inputs[1]) + links.new(input=mixNode.outputs['Shader'], output=matNode.inputs['Surface']) + + +def setupCyclesMat(material, cyclesParams): + if DEBUG_SHADER: + print("setupCyclesMat") + createNodeGroups() + if 'emit' in cyclesParams: + emitAmt = cyclesParams['emit'] + if emitAmt > 0.0: + createEmissionCyclesMat(material, emitAmt) + return + + if 'stencil' in cyclesParams and cyclesParams['stencil']: #must be boolean true + if 'ovr' in cyclesParams: + #get the overlay colour, and create a transp overlay material. + return + #not overlay + createStencilCyclesMat(material) + return + + if 'alpha' in cyclesParams and cyclesParams['alpha']: + createPlainAlphaCyclesMat(material) + return + + if 'leaf' in cyclesParams and cyclesParams['leaf']: + createLeafCyclesMat(material) + return + + createDiffuseCyclesMat(material) + + +def getMCMat(blocktype, rgbtriple, cyclesParams=None): #take cycles params Dictionary - ['type': DIFF/EMIT/TRANSP, 'emitAmt': 0.0] + """Creates or returns a general-use default Minecraft material.""" + matname = 'mc' + blocktype + 'Mat' + + if matname in bpy.data.materials: + return bpy.data.materials[matname] + + blockMat = bpy.data.materials.new(matname) + ## ALL-MATERIAL DEFAULTS + blockMat.use_transparency = True # surely not for everything!? not stone,dirt,etc! + blockMat.alpha = 0.0 + blockMat.specular_alpha = 0.0 + blockMat.specular_intensity = 0.0 + + ##TODO: blockMat.use_transparent_shadows - on recving objects (solids) + ##TODO: Cast transparent shadows from translucent things like water. + if rgbtriple is not None: + #create the solid shaded-view material colour + diffusecolour = [n/256.0 for n in rgbtriple] + blockMat.diffuse_color = diffusecolour + blockMat.diffuse_shader = 'OREN_NAYAR' + blockMat.diffuse_intensity = 0.8 + blockMat.roughness = 0.909 + else: + #create a blank/obvious 'unhelpful' material. + blockMat.diffuse_color = [214,127,255] #shocking pink + return blockMat + + +############################################################################### +# Primary Block-Shape Creation Functions # +############################################################################### + +def createCubeMesh(): + bpy.context.scene.cursor_location = (0.0, 0.0, 0.0) + bpy.ops.mesh.primitive_cube_add() + blockOb = bpy.context.object + bpy.ops.transform.resize(value=(0.5, 0.5, 0.5)) + bpy.ops.object.transform_apply(location=False, rotation=False, scale=True) + return blockOb + + +def createInsetMCBlock(mcname, colourtriple, mcfaceindices, insets=[0,0,0], cyclesParams=None): + """With no insets (the default), creates a full-size cube. +Else uses [bottom,top,sides] to inset the cube size and UV coords. +Side insets are applied symmetrically around the cube; maximum side inset is 7. +Units are in Minecraft texels - so from 1 to 15. Inset 16 is an error.""" + blockname = mcname + 'Block' + if blockname in bpy.data.objects: + return bpy.data.objects[blockname] + + pxlUnit = getUVUnit() + bpy.ops.object.mode_set(mode='OBJECT') #just to be sure... needed? + blockOb = createCubeMesh() + blockOb.name = blockname + mesh = blockOb.data + meshname = blockname + 'Mesh' + mesh.name = meshname + + #Inset the mesh + verts = mesh.vertices + + if isBMesh(): #inset the mesh, bmesh-version. + #loop the verts per face, change their .co by the inset amount. + #tverts = mesh.tessfaces.data.vertices # unneeded.. + #polygon face order is: [left,back,right,front,bottom,top] + leface = mesh.polygons[0] + bkface = mesh.polygons[1] + rgface = mesh.polygons[2] + frface = mesh.polygons[3] + botface= mesh.polygons[4] + topface= mesh.polygons[5] + + else: + botface = mesh.faces[0] + topface = mesh.faces[1] + rgface = mesh.faces[2] + frface = mesh.faces[3] + leface = mesh.faces[4] + bkface = mesh.faces[5] + + bi = insets[0] * pxlUnit + ti = insets[1] * pxlUnit + si = insets[2] * pxlUnit + + #does this need to be enforced as global rather than local coords? + #There are ways to inset these along their normal directions, + #but it's complex to understand, so I'll just inset all sides. :( + for v in topface.vertices: + vtx = verts[v] + vp = vtx.co + vtx.co = Vector((vp[0], vp[1], vp[2]-ti)) + + for v in botface.vertices: + vtx = verts[v] + vp = vtx.co + vtx.co = Vector((vp[0], vp[1], vp[2]+bi)) + + for v in rgface.vertices: + vtx = verts[v] + vp = vtx.co + vtx.co = Vector((vp[0]-si, vp[1], vp[2])) + + for v in frface.vertices: + vtx = verts[v] + vp = vtx.co + vtx.co = Vector((vp[0], vp[1]+si, vp[2])) + + for v in leface.vertices: + vtx = verts[v] + vp = vtx.co + vtx.co = Vector((vp[0]+si, vp[1], vp[2])) + + for v in bkface.vertices: + vtx = verts[v] + vp = vtx.co + vtx.co = Vector((vp[0], vp[1]-si, vp[2])) + + #Fetch/setup the material. + blockMat = getMCMat(mcname, colourtriple, cyclesParams) + + mcTexture = getMCTex() + blockMat.texture_slots.add() #it has 18, but unassignable... + mTex = blockMat.texture_slots[0] + mTex.texture = mcTexture + #set as active texture slot? + + mTex.texture_coords = 'UV' + mTex.use_map_alpha = True #mibbe not needed? + + mcuvs = None + if isBMesh(): + mcuvs = createBMeshInsetUVs(mcname, mesh, blockMat, mcfaceindices, insets) + else: + mcuvs = createInsetUVs(mcname, mesh, blockMat, mcfaceindices, insets) + + if mcuvs is not None: + mTex.uv_layer = mcuvs + + bpy.ops.object.mode_set(mode='EDIT') + bpy.ops.mesh.select_all(action='SELECT') + bpy.ops.transform.rotate(value=(-1.5708), axis=(0, 0, 1), constraint_axis=(False, False, True), constraint_orientation='GLOBAL') + bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + #last, setup cycles on the material if user asked for it. + if cyclesParams is not None: + setupCyclesMat(blockMat, cyclesParams) + + return blockOb + + +def createMCBlock(mcname, colourtriple, mcfaceindices, cyclesParams=None): + """Creates a new minecraft WHOLE-block if it doesn't already exist, properly textured. + Array order for mcfaceindices is: [bottom, top, right, front, left, back]""" + + #Has an instance of this blocktype already been made? + blockname = mcname + 'Block' + if blockname in bpy.data.objects: + return bpy.data.objects[blockname] + + blockOb = createCubeMesh() + blockOb.name = blockname + blockMesh = blockOb.data + meshname = blockname + 'Mesh' + blockMesh.name = meshname + + #Fetch/setup the material. + blockMat = getMCMat(mcname, colourtriple, cyclesParams) + +# #ADD THE MATERIAL! (conditional on it already being applied?) +# blockMesh.materials.append(blockMat) # previously is in the uvtex creation function for some reason... + + mcTexture = getMCTex() + blockMat.texture_slots.add() #it has 18, but unassignable... + mTex = blockMat.texture_slots[0] + mTex.texture = mcTexture + #set as active texture slot? + + mTex.texture_coords = 'UV' + mTex.use_map_alpha = True #mibbe not needed? + + mcuvs = None + if isBMesh(): + mcuvs = createBMeshBlockCubeUVs(mcname, blockMesh, blockMat, mcfaceindices) + else: + mcuvs = createBlockCubeUVs(mcname, blockMesh, blockMat, mcfaceindices) + + if mcuvs is not None: + mTex.uv_layer = mcuvs + #array order is: [bottom, top, right, front, left, back] + + #for the cube's faces to align correctly to Minecraft north, based on the UV assignments I've bodged, correct it all by spinning the verts after the fact. :p + # -90degrees in Z. (clockwise a quarter turn) + # Or, I could go through a crapload more UV assignment stuff, which is no fun at all. + #bpy ENSURE MEDIAN rotation point, not 3d cursor pos. + + bpy.ops.object.mode_set(mode='EDIT') + #bpy.ops.objects.editmode_toggle() + bpy.ops.mesh.select_all(action='SELECT') + #don't want toggle! Want "ON"! + bpy.ops.transform.rotate(value=(-1.5708), axis=(0, 0, 1), constraint_axis=(False, False, True), constraint_orientation='GLOBAL') + #bpy.ops.mesh.select_all(action='DESELECT') + bpy.ops.object.mode_set(mode='OBJECT') + + #last, setup cycles on the material if user asked for it. + if cyclesParams is not None: + setupCyclesMat(blockMat, cyclesParams) + + return blockOb + +def createFenceBlock(mcname, colourtriple, mcfaceindices, shapeParams, cyclesParams=None): + #create a central upright fencepost; determine side attachments during load process. ... + #mcname + "fencePost" + block = createInsetMCBlock(mcname, colourtriple, mcfaceindices, [0,0,6], cyclesParams) + print("Fence added. Shape params: %s" % shapeParams.__repr__) + return block + + +def createXBlock(basename, diffuseRGB, mcfaceindices, extraData, cycParams): + """Creates an x-shaped billboard block if it doesn't already exist, + properly textured. Array order for mcfaceindices is: [\, /]. + A single item facelist will be applied to both faces of the X.""" + + #Has one of this blocktype already been made? + blockname = basename + 'Block' + if blockname in bpy.data.objects: + return bpy.data.objects[blockname] + + if not isBMesh(): + return createMCBlock(basename, diffuseRGB, mcfaceindices, cycParams) + + import bmesh + #BMesh-create X + m = bmesh.new() + xverts = [ (-0.45,0.45,0.5), #v1 + (0.45,-0.45,0.5), + (0.45,-0.45,-0.5), + (-0.45,0.45,-0.5), #v4 + (0.45,0.45,0.5), #v5 + (-0.45,-0.45,0.5), + (-0.45,-0.45,-0.5), + (0.45,0.45,-0.5) #v8 + ] + + for v in xverts: + m.verts.new(v) + + #Looks like you can slice bm.verts! Nice! + f1 = m.faces.new(m.verts[0:4]) + f2 = m.faces.new(m.verts[4:]) + + meshname = blockname + 'Mesh' + crossMesh = bpy.data.meshes.new(meshname) + m.to_mesh(crossMesh) + crossOb = bpy.data.objects.new(blockname, crossMesh) + #link it in! Unlike the primitive cube, it doesn't self-link. + bpy.context.scene.objects.link(crossOb) + + #Fetch/setup the material. + crossMat = getMCMat(basename, diffuseRGB, cycParams) + mcTexture = getMCTex() + crossMat.texture_slots.add() #it has 18, but unassignable. + mTex = crossMat.texture_slots[0] + mTex.texture = mcTexture + #set as active texture slot? + + mTex.texture_coords = 'UV' + mTex.use_map_alpha = True + + mcuvs = None + mcuvs = createBMeshXBlockUVs(basename, crossMesh, crossMat, mcfaceindices) + if mcuvs is not None: + mTex.uv_layer = mcuvs + + #last, setup cycles on the material if user asked for it. + if cycParams is not None: + setupCyclesMat(crossMat, cycParams) + + return crossOb + + +def createBMeshXBlockUVs(blockname, me, matrl, faceIndices): #assume me is an X mesh. Returns name of the uv layer created. + """Uses faceIndices, a list of per-face MC texture indices, to unwrap + the X's faces onto their correct places on terrain.png. + Face order for faceIndices is [\,/]""" + + if faceIndices is None: + print("Warning: no face texture for %s" % blockname) + return + + __listtype = type([]) + if type(faceIndices) != __listtype: + if (type(faceIndices) == type(0)): + faceIndices = [faceIndices]*6 + print("Applying singular value to all 6 faces") + else: + print("setting material and uvs for %s: non-numerical face list" % blockname) + print(faceIndices) + raise IndexError("improper face assignment data!") + + if matrl.name not in me.materials: + me.materials.append(matrl) + + uname = blockname + 'UVs' + if uname in me.uv_textures: + blockUVLayer = me.uv_textures[uname] + else: + blockUVLayer = me.uv_textures.new(name=uname) + + #blockUVLoop = me.uv_loop_layers[-1] #works prior to 2.63?! + blockUVLoop = me.uv_layers.active + uvData = blockUVLoop.data + + #face indices: our X mesh is put together in the right order, so + #should be just face 0, face 1 in the loop. + + if len(faceIndices) == 1: + fOnly = faceIndices[0] + faceIndices = [fOnly, fOnly] #probably totally unecessary safety. + + bmfi = [faceIndices[0], faceIndices[1]] + uvUnit = getUVUnit() + #offsets from topleft of any uv 'tile' to its vert corners (CCW from TR): + uvcorners = [(uvUnit, 0.0), (0.0,0.0), (0.0, -uvUnit), (uvUnit,-uvUnit)] + #we assign each UV in sequence of the 'loop' for the whole mesh: 8 for an X + + xim = getMCImg() + meshtexfaces = blockUVLayer.data.values() + + matrl.game_settings.alpha_blend = 'CLIP' + matrl.game_settings.use_backface_culling = False + + #faceNo = 0 #or enumerate me.polygons? + #face order is: [\,/] + for faceNo, pface in enumerate(me.polygons): + face = meshtexfaces[faceNo] + face.image = xim + faceTexId = bmfi[faceNo] + #calculate the face location on the uvmap + mcTexU = getTextureAtlasU(faceTexId) + mcTexV = getTextureAtlasV(faceTexId) + #multiply by square size to get U1,V1 (topleft): + u1 = (mcTexU * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS # or >> 4 (div by imagesize to get as fraction) + v1 = (mcTexV * TEXTURE_ATLAS_PIXELS_PER_UNIT) / TEXTURE_ATLAS_PIXELS # .. + v1 = 1.0 - v1 #y goes low to high #DEBUG print("That means u1,v1 is %f,%f" % (u1,v1)) + + #DEBUG + if DEBUG_BBUV: + print("createBMeshXBlockUVs %s u1,v1 %f,%f" % (blockname,u1,v1)) + loopPolyStart = pface.loop_start #where its verts start in loop. :D + #if loop total's not 4, need to work with ngons/tris or do more complex stuff. + loopPolyCount = pface.loop_total + loopPolyEnd = loopPolyStart + loopPolyCount + + corners = uvcorners + for n, loopV in enumerate(range(loopPolyStart, loopPolyEnd)): + offset = corners[n] # 0..3 + mcUV = Vector((u1+offset[0], v1+offset[1])) + uvData[loopV].uv = mcUV + #faceNo += 1 + + #a guess. does this actually help? YES! Without it all the world's grey and textureless! + me.tessface_uv_textures.data.update() + #but then, sometimes it's grey anyway. :( + + return "".join([blockname, 'UVs']) + + +def createStairsBlock(basename, diffuseRGB, mcfaceindices, extraData, cycParams): + """Creates a stairs block if it doesn't already exist, + properly textured. Will create new stair blocks by material, + direction and inversion.""" + #DOES THE FACING DETERMINE THE UV UNWRAP? The public needs to know! if so... nuts! must be easier way? Can do cube mapping and rotate tex space?? + + #Has one of this already been made? + #... get direction and bytes unpack verticality + + blockname = basename + 'Block' + if blockname in bpy.data.objects: + return bpy.data.objects[blockname] + + if not isBMesh(): + return createMCBlock(basename, diffuseRGB, mcfaceindices, cycParams) + + import bmesh + #BMesh-create X + + stair = bmesh.new() + #Stair Vertices + sverts = [ (0.5,0.5,0.5), #v0 + (0.5,0.5,-0.5), #v1 + (0.5,-0.5,-0.5), #v2 + (0.5,-0.5,0), #v3 + (0.5,0,0), #v4 + (0.5,0,0.5), #v5 -- X+ facing stair profile done. + (-0.5,0.5,0.5), #v6 + (-0.5,0.5,-0.5), #v7 + (-0.5,-0.5,-0.5), #v8 + (-0.5,-0.5,0), #v9 + (-0.5,0,0), #v10 + (-0.5,0,0.5), #v11 -- X- facing stair profile done. + #would it be a good idea or a bad idea to reverse order of these latter 6? + ] + + for v in sverts: + stair.verts.new(v) + svs = stair.verts + #now the faces. in a specific order we can follow for unwrapping later + + #in a stair mesh, we'll have R1,R2 ; stairfacings(vertical) higher,lower; L1,L2; BACK; Top(tip),Top(midstep); Bottom. Maybe. Rearrange for cube order. + sf1 = stair.faces.new([svs[0], svs[5], svs[4], svs[1]]) #r1 + sf2 = stair.faces.new([svs[4], svs[3], svs[2], svs[1]]) #r2 + sf3 = stair.faces.new([svs[5], svs[11], svs[10],svs[4]]) #vertical topstair face + sf4 = stair.faces.new([svs[3], svs[9], svs[8],svs[2]]) #vertical bottomstair face + sf5 = stair.faces.new([svs[9], svs[10], svs[7],svs[8]]) #lface1 (lower..) + sf6 = stair.faces.new([svs[11],svs[6],svs[7],svs[10]]) #lface2 (upright higher bit) + sf7 = stair.faces.new([svs[6], svs[0], svs[1],svs[7]]) #back + sf8 = stair.faces.new([svs[0], svs[6], svs[11],svs[5]]) #topface, topstep + sf9 = stair.faces.new([svs[4], svs[10], svs[9],svs[3]]) #topface, midstep + sf10= stair.faces.new([svs[7], svs[1], svs[2],svs[8]]) #bottom + + #check the extra data for direction and upside-downness. + + + + sm = bpy.data.meshes.new("StairMesh") + stob = bpy.data.objects.new("Stair", sm) + bpy.context.scene.objects.link(stob) + stair.to_mesh(sm) + + #f1 = m.faces.new([v1,v2,v3,v4]) + + + #loop1 = f1.loops[0] + + #me = bpy.data.meshes.new("Foo") + #ob = bpy.data.objects.new("Bar", me) + #bpy.context.scene.objects.link(ob) + + + pass + + + + + + + + + + +# ################################################# + +#if __name__ == "__main__": +# #BlockBuilder.create ... might tidy up namespace. +# #nublock = createMCBlock("Glass", (1,2,3), [49]*6) +# #nublock2 = createInsetMCBlock("Torch", (240,150,50), [80]*6, [0,6,7]) + +# nublock3 = createInsetMCBlock("Chest", (164,114,39), [25,25,26,27,26,26], [0,1,1]) diff --git a/blockbuild.pyc b/blockbuild.pyc new file mode 100644 index 0000000..6fdafd4 Binary files /dev/null and b/blockbuild.pyc differ diff --git a/mc2mt.py b/mc2mt.py new file mode 100644 index 0000000..479af16 --- /dev/null +++ b/mc2mt.py @@ -0,0 +1,285 @@ +# io_import_minecraft + +# ##### BEGIN GPL LICENSE BLOCK ##### +# +# 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. +# +# ##### END GPL LICENSE BLOCK ##### + +# + +# bl_info = { +# "name": "Import: Minecraft b1.7+", +# "description": "Importer for viewing Minecraft worlds", +# "author": "Adam Crossan (acro)", +# "version": (1,6,3), +# "blender": (2, 6, 0), +# "api": 41226, +# "location": "File > Import > Minecraft", +# "warning": '', # used for warning icon and text in addons panel +# "wiki_url": "http://randomsamples.info/project/mineblend", +# "category": "Import-Export"} + +DEBUG_SCENE=False + +# To support reload properly, try to access a package var, if it's there, reload everything +#if "bpy" in locals(): +# import imp +# if "mineregion" in locals(): +# imp.reload(mineregion) + +#import bpy +#from bpy.props import StringProperty, FloatProperty, IntProperty, BoolProperty, EnumProperty +import imp +import mineregion + +#def setSceneProps(scn): +# #Set up scene-level properties +# bpy.types.Scene.MCLoadNether = BoolProperty( +# name = "Load Nether", +# description = "Load Nether (if present) instead of Overworld.", +# default = False) + +# scn['MCLoadNether'] = False +# return +#setSceneProps(bpy.context.scene) + +# def createTestScene(): +# bpy.ops.scene.new(type='NEW') +# bpy.context.scene.render.engine = 'CYCLES' +# # plane +# bpy.ops.mesh.primitive_plane_add(radius=1, view_align=True, enter_editmode=False, location=(0,0,0), rotation=(0,0,0), layers = (True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# bpy.ops.transform.resize(value=(10,10,10), constraint_axis=(False, False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) +# bpy.ops.material.new() +# # cube +# bpy.ops.mesh.primitive_cube_add(radius=1, view_align=True, enter_editmode=False, location=(0,0,0), rotation=(0,0,0), layers = (True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# # FIXME - error +# #bpy.context.space_data.context='MATERIAL' +# bpy.ops.transform.translate(value=(0.55,0.17,1.14), constraint_axis=(False,False, False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) +# # set material to leaves? +# bpy.ops.object.editmode_toggle() +# bpy.ops.uv.unwrap(method='CONFORMAL', margin=0.001) +# # uv mapping - how do we tell blender? +# #bpy.ops.transform.resize(value=(0.0368432,0.0368432,0.0368432), constraint_axis=(False,False,False), constraint_orientation='GLOBAL', mirror=False, proportional='DISABLED', proportional_edit_falloff='SMOOTH', proportional_size=1) +# #bpy.ops.transform.translate(value=(-0.202301, 0.07906, 0), constraint_axis=(False,False,False), constraint_orientation='GLOBAL', mirror=False, proportional_falloff='SMOOTH', proportional_size=1) +# bpy.ops.object.editmode_toggle() +# # lights... +# bpy.ops.object.lamp_add(type='SUN', view_align=True, location=(-8.12878,5.39259,9.70453), rotation=(-0.383973,0,0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# # camera... +# bpy.ops.object.camera_add(view_align=True, enter_editmode=False, location=(-8.12878,-9.13302,7.87796), rotation=(0,0,0), layers=(True, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False, False)) +# #bpy.context.space_data.context='CONSTRAINT' +# bpy.ops.object.constraint_add(type='TRACK_TO') +# bpy.context.object.constraints["Track To"].target = bpy.data.objects["Cube.001"] +# bpy.context.object.constraints["Track To"].track_axis = 'TRACK_NEGATIVE_Z' +# bpy.context.object.constraints["Track To"].up_axis = 'UP_Y' + + +#Menu 'button' for the import menu (which calls the world selector)... +# class MinecraftWorldSelector(bpy.types.Operator): +#"""An operator defining a dialogue for choosing one on-disk Minecraft world to load. +#This supplants the need to call the file selector, since +"""Minecraft worlds require a preset specific folder structure of multiple files which cannot be selected singly.""" + +bl_idname = "mcraft.selectworld" +bl_label = "Select Minecraft World" + +#bl_space_type = "PROPERTIES" +#Possible placements for these: +bl_region_type = "WINDOW" + + +#TODO: Make this much more intuitive for the user! +#Would be better if could define min[x,y,z] and max[x,y,z] and load between these point +mcLoadAtCursor = False #Loads as if 3D cursor offset in viewport was the player (load) position. +mcLowLimit = 60 #The lowest depth layer to load. (High=256, Sea=64, Low=0) +mcHighLimit = 128 #The highest layer to load. (High=256, Sea=64, Low=0) + +mcLoadRadius = 5 # 'Load Radius - The half-width of the load range around load-pos. + # e.g, 4 will load 9x9 chunks around the load centre + # WARNING! Above 10, this gets slow and eats LOTS of memory! +mcOmitStone = False # When True, do not import common blocks such as stone & dirt blocks (overworld) or netherrack (nether). +mcDimenSelectList = '0' #Which dimension should be loaded? - 0=Overworld; 1=Nether, 2=The End +mcShowSlimeSpawns = False #'Display green markers showing slime-spawn locations +mcUseCyclesMats = False #Blender Setting: Set up default materials for use with Cycles Render Engine instead of Blender Internal +mcFasterViewport = False #Blender Setting: Disable display of common blocks (stone, dirt, etc.) in the viewport for better performance. +mcSurfaceOnly = False #Omit underground blocks. Significantly better viewing and rendering performance. +mcOmitMobs = True # When True, do not load mobs (creepers, skeletons, zombies, etc.) in world +#may need to define loadnether and loadend as operators...? + +# omit Dirt toggle option. + +# height-limit option (only load down to a specific height) -- could be semi-dynamic and delve deeper when air value for the +# column in question turns out to be lower than the loading threshold anyway. + +#surfaceOnly ==> only load surface, discard underground areas. Doesn't count for nether. +# Load Nether is, obviously, only available if selected world has nether) +# Load End. Who has The End?! Not I! + +#When specifying a property of type EnumProperty, ensure you call the constructing method correctly. +#Note that items is a set of (identifier, value, description) triples, and default is a string unless you switch on options=ENUM_FLAG in which case make default a set of 1 string. +#Need a better way to handle this variable: (possibly set it as a screen property) + +# import mineregion +wlist = mineregion.getWorldSelectList() +if wlist is not None: + revwlist = wlist[::-1] + mcWorldSelectList = 0 #Which Minecraft save should be loaded? +else: + mcWorldSelectList = 0 #Which Minecraft save should be loaded? + + #TODO: on select, check presence of DIM-1 etc. +print("List of Worlds: wlist:: ", wlist) + +netherWorlds = [w[0] for w in wlist if mineregion.hasNether(w[0])] +print("List of worlds with Nether: ", netherWorlds) + +endWorlds = [e[0] for e in wlist if mineregion.hasEnd(e[0])] +print("List of worlds with The End: ", endWorlds) + +#my_worldlist = bpy.props.EnumProperty(items=[('0', "A", "The A'th item"), ('1', 'B', "Bth item"), ('2', 'C', "Cth item"), ('3', 'D', "dth item"), ('4', 'E', 'Eth item')][::-1], default='2', name="World", description="Which Minecraft save should be loaded?") + +# def execute(self, context): + #self.report({"INFO"}, "Loading world: " + str(self.mcWorldSelectList)) + #thread.sleep(30) + #self.report({"WARNING"}, "Foo!") + + #from . import mineregion +# scn = context.scene + +mcLoadDimenNether = True if mcDimenSelectList=='1' else False +mcLoadDimenEnd = True if mcDimenSelectList=='2' else False +# FIXME - when omitmobs is false, mobs will sometimes still not be imported (related to reload issue?) +opts = {"omitstone": mcOmitStone, "showslimes": mcShowSlimeSpawns, "atcursor": mcLoadAtCursor, + "highlimit": mcHighLimit, "lowlimit": mcLowLimit, + "loadnether": mcLoadDimenNether, "loadend": mcLoadDimenEnd, + "usecycles": mcUseCyclesMats, "omitmobs": mcOmitMobs, + "fasterViewport": mcFasterViewport, "surfaceOnly": mcSurfaceOnly} +#print(str(opts)) +#get selected world name instead via bpy.ops.mcraft.worldselected -- the enumeration as a property/operator...? +mineregion.readMinecraftWorld(str(mcWorldSelectList), mcLoadRadius, opts) +# for s in bpy.context.area.spaces: # iterate all space in the active area +# if s.type == "VIEW_3D": # check if space is a 3d-view +# space = s +# space.clip_end = 10000.0 +#run minecraftLoadChunks +#if DEBUG_SCENE: +# createTestScene() + +# return {'FINISHED'} + + +# def invoke(self, context, event): +# context.window_manager.invoke_props_dialog(self, width=350,height=250) +# return {'RUNNING_MODAL'} +# +# +# def draw(self, context): +# layout = self.layout +# col = layout.column() +# col.label(text="Choose import options") +# +# row = col.row() +# row.prop(self, "mcLoadAtCursor") +# +# row = col.row() +# +# sub = col.split(percentage=0.5) +# colL = sub.column(align=True) +# colL.prop(self, "mcShowSlimeSpawns") +# +# cycles = None +# if hasattr(bpy.context.scene, 'cycles'): +# cycles = bpy.context.scene.cycles +# row2 = col.row() +# if cycles is not None: +# row2.active = (cycles is not None) +# row2.prop(self, "mcUseCyclesMats") +# +# row3 = col.row() +# row3.prop(self, "mcOmitStone") +# row3.prop(self, "mcOmitMobs") +# +# row = col.row() +# row.prop(self,"mcFasterViewport") +# #row.prop(self,"mcSurfaceOnly") +# +# #if cycles: +# #like this from properties_data_mesh.py: +# ##layout = self.layout +# ##mesh = context.mesh +# ##split = layout.split() +# ##col = split.column() +# ##col.prop(mesh, "use_auto_smooth") +# ##sub = col.column() +# ##sub.active = mesh.use_auto_smooth +# ##sub.prop(mesh, "auto_smooth_angle", text="Angle") +# #row.operator( +# #row.prop(self, "mcLoadEnd") #detect folder first (per world...) +# +# #label: "loading limits" +# row = layout.row() +# row.prop(self, "mcLowLimit") +# row = layout.row() +# row.prop(self, "mcHighLimit") +# row = layout.row() +# row.prop(self, "mcLoadRadius") +# +# row = layout.row() +# row.prop(self, "mcDimenSelectList") +# #col = layout.column() +# +# row = layout.row() +# row.prop(self, "mcWorldSelectList") +# #row.operator("mcraft.worldlist", icon='') +# col = layout.column() + +# def worldchange(self, context): +# ##UPDATE (ie read then write back the value of) the property in the panel +# #that needs to be updated. ensure it's in the scene so we can get it... +# #bpy.ops.mcraft.selectworld('INVOKE_DEFAULT') +# #if the new world selected has nether, then update the nether field... +# #in fact, maybe do that even if it doesn't. +# #context.scene['MCLoadNether'] = True +# return {'FINISHED'} +# +# class MineMenuItemOperator(bpy.types.Operator): +# bl_idname = "mcraft.launchselector" +# bl_label = "Needs label but label not used" +# +# def execute(self, context): +# bpy.ops.mcraft.selectworld('INVOKE_DEFAULT') +# return {'FINISHED'} +# +# bpy.utils.register_class(MinecraftWorldSelector) +# bpy.utils.register_class(MineMenuItemOperator) +#bpy.utils.register_class(MCraft_PT_worldlist) + +#Forumsearch tip!! FINDME: +#Another way would be to update a property that is displayed in your panel via layout.prop(). AFAIK these are watched and cause a redraw on update. +# +# def mcraft_filemenu_func(self, context): +# self.layout.operator("mcraft.launchselector", text="Minecraft (.region)", icon='MESH_CUBE') +# +# +# def register(): +# #bpy.utils.register_module(__name__) +# bpy.types.INFO_MT_file_import.append(mcraft_filemenu_func) # adds the operator action func to the filemenu +# +# def unregister(): +# #bpy.utils.unregister_module(__name__) +# bpy.types.INFO_MT_file_import.remove(mcraft_filemenu_func) # removes the operator action func from the filemenu +# +# if __name__ == "__main__": +# register() diff --git a/mcanvilreader.py b/mcanvilreader.py new file mode 100644 index 0000000..494289b --- /dev/null +++ b/mcanvilreader.py @@ -0,0 +1,486 @@ +import os +#import bpy + +from struct import unpack #, error as StructError +import nbtreader, mcregionreader +from mineregion import OPTIONS, EXCLUDED_BLOCKS, BLOCKDATA, REPORTING, unknownBlockIDs, WORLD_ROOT +##..yuck: they're immutable and don't return properly except for the dict-type ones. Get rid of this in next cleanup. + +from math import floor + +class AnvilChunkReader(mcregionreader.ChunkReader): + + #readBlock( bX, bZ (by?) ... ignoring 'region' boundaries and chunk boundaries? We need an ignore-chunk-boundaries level of abstraction + + def getSingleBlock(chunkXZ, blockXYZ): #returns the value and extradata bits for a single block of given absolute x,y,z block coords within chunk cx,cz. or None if area not generated. + #y is value from 0..255 + cx, cy = chunkXZ + bX,bY,bZ = blockXYZ + rX = floor(cx / 32) # is this the same as >> 8 ?? + rZ = floor(cz / 32) + rHdrOffset = ((cx % 32) + (cz % 32) * 32) * 4 + rFile = "r.%d.%d.mca" % (rx, rz) + if not os.path.exists(rFile): + return None + with open(rFile, 'rb') as regionfile: + regionfile.seek(rheaderoffset) + cheadr = regionfile.read(4) + dataoffset = unpack(">i", b'\x00'+cheadr[0:3])[0] + chunksectorcount = cheadr[3] + if dataoffset == 0 and chunksectorcount == 0: + return None #Region exists, but the chunk we're after was never created within it. + else: + #possibly check for cached chunk data here, under the cx,cz in a list of already-loaded sets. + chunkdata = AnvilChunkReader._readChunkData(regionfile, dataoffset, chunksectorcount) + chunkLvl = chunkdata.value['Level'].value + sections = chunkLvl['Sections'].value + #each section is a 16x16x16 piece of chunk, with a Y-byte from 0-15, so that the 'y' value is 16*that + in-section-Y-value + #some sections can be skipped, so we must iterate to find the right one with the 'Y' we expect. + bSection = bY / 16 + sect = None + for section in sections: + secY = section.value['Y'].value + if secY == bSection: + sect = section.value + if sect is None: + return None + blockData = sec['Blocks'].value #a TAG_Byte_Array value (bytes object). Blocks is 16x16 bytes + extraData = sec['Data'].value #BlockLight, Data and SkyLight are 16x16 "4-bit cell" additional data arrays. + sY = dY % 16 + blockIndex = (sY * 16 + dZ) * 16 + dX + blockID = blockData[ blockIndex ] + return blockID #, extravalue) + #NB: this can be made massively more efficient by storing 4 'neighbour chunk' data reads for every chunk properly processed. + #Don't need to do diagonals, even. + + + + + def readChunk2(self, chunkPosX, chunkPosZ, blockBuffer, zeroAdjX, zeroAdjY): + # FIXME - implement me! + return + + def readChunk(self, chunkPosX, chunkPosZ, vertexBuffer): # aka "readChunkFromRegion" ... + """Loads chunk located at the X,Z chunk location provided.""" + + global REPORTING + + #region containing a given chunk is found thusly: floor of c over 32 + regionX = floor(chunkPosX / 32) + regionZ = floor(chunkPosZ / 32) + + rheaderoffset = ((chunkPosX % 32) + (chunkPosZ % 32) * 32) * 4 + + #print("Reading chunk %d,%d from region %d,%d" %(chunkPosX, chunkPosZ, regionX,regionZ)) + + rfileName = "r.%d.%d.mca" % (regionX, regionZ) + if not os.path.exists(rfileName): + #Can't load: it doesn't exist! + print("No such region generated.") + return + + with open(rfileName, 'rb') as regfile: + # header for the chunk we want is at... + #The location in the region file of a chunk at (x, z) (in chunk coordinates) can be found at byte offset 4 * ((x mod 32) + (z mod 32) * 32) in its McRegion file. + #Its timestamp can be found 4096 bytes later in the file + regfile.seek(rheaderoffset) + cheadr = regfile.read(4) + dataoffset = unpack(">i", b'\x00'+cheadr[0:3])[0] + chunksectorcount = cheadr[3] + + if dataoffset == 0 and chunksectorcount == 0: + pass + #print("Region exists, but chunk has never been created within it.") + else: + chunkdata = AnvilChunkReader._readChunkData(regfile, dataoffset, chunksectorcount) #todo: rename that function! + #Geometry creation! etc... If surface only, can get heights etc from lightarray? + + #top level tag in NBT is an unnamed TAG_Compound, for some reason, containing a named TAG_Compound "Level" + chunkLvl = chunkdata.value['Level'].value + #chunkXPos = chunkLvl['xPos'].value + #chunkZPos = chunkLvl['zPos'].value + #print("Reading blocks for chunk: (%d, %d)\n" % (chunkXPos, chunkZPos)) + AnvilChunkReader._readBlocks(chunkLvl, vertexBuffer) + #print("Loaded chunk %d,%d" % (chunkPosX,chunkPosZ)) + + REPORTING['totalchunks'] += 1 + + + def _readChunkData(bstream, chunkOffset, chunkSectorCount): #rename this! + #get the datastring out of the file... + import io, zlib + + #cf = open(fname, 'rb') + initialPos = bstream.tell() + + cstart = chunkOffset * 4096 #4 kiB + clen = chunkSectorCount * 4096 + bstream.seek(cstart) #this bstream is the region file + + chunkHeaderAndData = bstream.read(clen) + + #chunk header stuff is: + # 4 bytes: length (of remaining data) + # 1 byte : compression type (1 - gzip - unused; 2 - zlib: it should always be this in actual fact) + # then the rest, is length-1 bytes of compressed (zlib) NBT data. + + chunkDLength = unpack(">i", chunkHeaderAndData[0:4])[0] + chunkDCompression = chunkHeaderAndData[4] + if chunkDCompression != 2: + print("Not a zlib-compressed chunk!?") + raise StringError() #MinecraftSomethingError, perhaps. + + chunkZippedBytes = chunkHeaderAndData[5:] + + #could/should check that chunkZippedBytes is same length as chunkDLength-1. + + #put the regionfile byte stream back to where it started: + bstream.seek(initialPos) + + #Read the compressed chunk data + zipper = zlib.decompressobj() + chunkData = zipper.decompress(chunkZippedBytes) + chunkDataAsFile = io.BytesIO(chunkData) + chunkNBT = nbtreader.readNBT(chunkDataAsFile) + + return chunkNBT + + def getSectionBlock(blockLoc, sectionDict): + """Fetches a block from section NBT data.""" + (bX,bY,bZ) = blockLoc + secY = bY >> 4 #/ 16 + if secY not in sectionDict: + return None + sect = sectionDict[secY] + sY = bY & 0xf #mod 16 + bIndex = (sY * 16 + bZ) * 16 + bX + #bitshift, or run risk of int casts + dat = sect['Blocks'].value + return dat[bIndex] + + #Hollow volumes optimisation (version1: in-chunk only) + def _isExposedBlock(blockCoord, chunkXZ, secBlockData, sectionDict, blockID, skyHighLimit, depthLimit): #another param: neighbourChunkData[] - a 4-list of NBT stuff... + (dX,dY,dZ) = blockCoord + #fail-fast. checks if all ortho adjacent neighbours fall inside this chunk. + #EASY! Because it's 0-15 for both X and Z. For Y, we're iterating upward, + #so get the previous value (the block below) passed in. + + if blockID == 18: #leaves #and glass? and other exemptions? + return True + + if dX == 0 or dX == 15 or dY == 0 or dZ == 0 or dZ == 15: + #get neighbour directly + return True #instead, check neigbouring chunks... + + + #we can no longer get the block below or above easily as we might be iterating +x, -16x, or +z at any given step. + if dY == skyHighLimit or dY == depthLimit: + return True + + ySect = dY / 16 ## all this dividing integers by 16! I ask you! (>> 4)! + yBoff = dY % 16 ## &= 0x0f + #if you are on a section boundary, need next section for block above. else + + #GLOBALS (see readBlocks, below) + CHUNKSIZE_X = 16 #static consts - global? + CHUNKSIZE_Z = 16 + #new layout goes YZX. improves compression, apparently. + ##_Y_SHIFT = 7 # 2**7 is 128. use for fast multiply + ##_YZ_SHIFT = 11 #16 * 128 is 2048, which is 2**11 + + #check above (Y+1) + #either it's in the same section (quick/easy lookup) or it's in another section (still quite easy - next array over) + #or, it's in another chunk. in which case, check chunkreadcache for the 4 adjacent. Failing this, it's the worse case and + #we need to read into a whole new chunk data grab. + if yBoff == 15: + upBlock = AnvilChunkReader.getSectionBlock((dX,dY+1,dZ), sectionDict) + if upBlock != blockID: + return True + else: + #get it from current section + upIndex = ((yBoff+1) * 16 + dZ) * 16 + dX + upBlock = secBlockData[ upIndex ] + if upBlock != blockID: + return True + + #Check below (Y-1): + if yBoff == 0: + downBlock = AnvilChunkReader.getSectionBlock((dX,dY-1,dZ), sectionDict) + if downBlock != blockID: + return True + else: + downIndex = ((yBoff-1) * 16 + dZ) * 16 + dX + dnBlock = secBlockData[downIndex] + if dnBlock != blockID: + return True + + #Have checked above and below; now check all sides. Same section, but maybe different chunks... + #Check X-1 (leftward) + leftIndex = (yBoff * 16 + dZ) * 16 + (dX-1) + #ngbIndex = dY + (dZ << _Y_SHIFT) + ((dX-1) << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + try: + neighbour = secBlockData[leftIndex] + except IndexError: + print("Bogus index cockup: %d. Blockdata len is 16x16x16 bytes (4096)." % leftIndex) + quit() + if neighbour != blockID: + return True + + #Check X+1 + rightIndex = (yBoff * 16 + dZ) * 16 + (dX+1) + #ngbIndex = dY + (dZ << _Y_SHIFT) + ((dX+1) << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = secBlockData[rightIndex] + if neighbour != blockID: + return True + + #Check Z-1 + ngbIndex = (yBoff * 16 + (dZ-1)) * 16 + dX + #ngbIndex = dY + ((dZ-1) << _Y_SHIFT) + (dX << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = secBlockData[ngbIndex] + if neighbour != blockID: + return True + + #Check Z+1 + ngbIndex = (yBoff * 16 + (dZ+1)) * 16 + dX + #ngbIndex = dY + ((dZ+1) << _Y_SHIFT) + (dX << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = secBlockData[ngbIndex] + if neighbour != blockID: + return True + + return False + + + #nb: 0 is bottom bedrock, 256 (255?) is top of sky. Sea is 64. + def _readBlocks(chunkLevelData, vertexBuffer): + """readBlocks(chunkLevelData) -> takes a named TAG_Compound 'Level' containing a chunk's Anvil Y-Sections, each of which 0-15 has blocks, data, heightmap, xpos,zpos, etc. + Adds the data points into a 'vertexBuffer' which is a per-named-type dictionary of ????'s. That later is made into Blender geometry via from_pydata.""" + #TODO: also TileEntities and Entities. Entities will generally be an empty list. + #TileEntities are needed for some things to define fully... + + #TODO: Keep an 'adjacent chunk cache' for neighbourhood is-exposed checks. + + global unknownBlockIDs, OPTIONS, REPORTING + + #chunkLocation = 'xPos' 'zPos' ... + chunkX = chunkLevelData['xPos'].value + chunkZ = chunkLevelData['zPos'].value + biomes = chunkLevelData['Biomes'].value #yields a TAG_Byte_Array value (bytes object) of len 256 (16x16) + #heightmap = chunkLevelData['HeightMap'].value + #'TileEntities' -- surely need this for piston data and stuff, no? + + entities = chunkLevelData['Entities'].value # load ze sheeps!! # a list of tag-compounds. + #omitmobs = OPTIONS['omitmobs'] + if not OPTIONS['omitmobs']: + AnvilChunkReader._loadEntities(entities) + + skyHighLimit = OPTIONS['highlimit'] + depthLimit = OPTIONS['lowlimit'] + + CHUNKSIZE_X = 16 + CHUNKSIZE_Z = 16 + SECTNSIZE_Y = 16 + + ##_Y_SHIFT = 7 # 2**7 is 128. use for fast multiply + ##_YZ_SHIFT = 11 #16 * 128 is 2048, which is 2**11 + sections = chunkLevelData['Sections'].value + + #each section is a 16x16x16 piece of chunk, with a Y-byte from 0-15, so that the 'y' value is 16*that + in-section-Y-value + + #iterate through all block Y values from bedrock to max height (minor step through X,Z.) + #bearing in mind some can be skipped out. + + #sectionDict => a dictionary of sections, indexed by Y. + sDict = {} + for section in sections: + sY = section.value['Y'].value + sDict[sY] = section.value + + for section in sections: + sec = section.value + secY = sec['Y'].value * SECTNSIZE_Y + + #if (secY + 16) < lowlimit, skip this section. no need to load it. + if (secY+16 < depthLimit): + continue + + if (secY > skyHighLimit): + return + + #Now actually proceed with adding in the section's block data. + blockData = sec['Blocks'].value #yields a TAG_Byte_Array value (bytes object). Blocks is 16x16 bytes + extraData = sec['Data'].value #BlockLight, Data and SkyLight are 16x16 "4-bit cell" additional data arrays. + + #get starting Y from heightmap, ignoring excess height iterations... + #heightByte = heightMap[dX + (dZ << 4)] # z * 16 + #heightByte = 255 #quickFix: start from tip top, for now + #if heightByte > skyHighLimit: + # heightByte = skyHighLimit + + #go y 0 to 16... + for sy in range(16): + dY = secY + sy + + if dY < depthLimit: + continue + if dY > skyHighLimit: + return + + # dataX will be dX, blender X will be bX. + for dZ in range(CHUNKSIZE_Z): + #print("looping chunk z %d" % dZ) + for dX in range(CHUNKSIZE_X): + #oneBlockLeft = 0 #data value of the block 1 back to the left (-X) from where we are now. (for neighbour comparisons) + #ie microcached 'last item read'. needs tweaked for chunk crossover... + + ##blockIndex = (dZ << _Y_SHIFT) + (dX << _YZ_SHIFT) # max number of bytes in a chunk is 32768. this is coming in at 32839 for XYZ: (15,71,8) + ##blockIndex = (dZ * 16) + dX + #YZX ((y * 16 + z) * 16 + x + blockIndex = (sy * 16 + dZ) * 16 + dX + blockID = blockData[ blockIndex ] + + #except IndexError: + # print("X:%d Y:%d Z %d, blockID from before: %d, cx,cz: %d,%d. Blockindex: %d" % (dX,dY,dZ,blockID,chunkX,chunkZ, blockIndex)) + # raise IndexError + + #create this block in the output! + if blockID != 0 and blockID not in EXCLUDED_BLOCKS: # 0 is air + REPORTING['blocksread'] += 1 + + #hollowness test: + if blockID in BLOCKDATA: +# if AnvilChunkReader._isExposedBlock((dX,dY,dZ), (chunkX, chunkZ), blockData, sDict, blockID, skyHighLimit, depthLimit): + #TODO: Make better version of this check, counting across chunks and regions. + #Load extra data (if applicable to blockID): + #if it has extra data, grab 4 bits from extraData + datOffset = (int(blockIndex /2)) #divided by 2 + datHiBits = blockIndex % 2 #odd or even, will be hi or low nibble + extraDatByte = extraData[datOffset] # should be a byte of which we only want part. + hiMask = 0b11110000 + loMask = 0b00001111 + extraValue = None + if datHiBits: + #get high 4, and shift right 4. + extraValue = loMask & (extraDatByte >> 4) + else: + #mask hi 4 off. + extraValue = extraDatByte & loMask + #create block in corresponding blockmesh + AnvilChunkReader.createBlock(blockID, (chunkX, chunkZ), (dX,dY,dZ), extraValue, vertexBuffer) +# else: + # REPORTING['blocksdropped'] += 1 + else: + #print("Unrecognised Block ID: %d" % blockID) + #createUnknownMeshBlock() + unknownBlockIDs.add(blockID) + + #TAG_Byte("Y"): 0 + #TAG_Byte_Array("Blocks"): [4096 bytes array] + #TAG_Byte_Array("BlockLight"): [2048 bytes array] + #TAG_Byte_Array("Data"): [2048 bytes array] + #TAG_Byte_Array("SkyLight"): [2048 bytes array] + ##TAG_Byte_Array("Add"): [2048 bytes array] ##Only appears if it's needed! + + def _loadEntities(entities): + global WORLD_ROOT + for e in entities: + eData = e.value + + etypename = eData['id'].value #eg 'Sheep' + ename = "en%sMarker" % etypename + epos = [p.value for p in eData['Pos'].value] #list[3] of double + erot = [r.value for r in eData['Rotation'].value] #list[2] of float ([0] orientation (angle round Z-axis) and [1] 0.00, probably y-tilt. + + #instantiate and rotate-in a placeholder object for this (and add to controlgroup or parent to something handy.) + #translate to blend coords, too. + entMarker = bpy.data.objects.new(ename, None) + #set its coordinates... + #convert Minecraft coordinate position of player into Blender coords: + entMarker.location[0] = -epos[2] + entMarker.location[1] = -epos[0] + entMarker.location[2] = epos[1] + + #also, set its z-rotation to erot[0]... + #entMarker.rotation[2] = erot[0] + + bpy.context.scene.objects.link(entMarker) + entMarker.parent = WORLD_ROOT + + + +##NB! Future blocks will require the Add tag to be checked and mixed in! +#Each section also has a "Add" tag, which is a DataLayer byte array just like +#"Data". The "Add" tag is not included in the converter since the old format +#never had block ids above 255. This extra tag is created whenever a block +#requires it, so the getTile() method needs to check if the array exists and +#then combine it with the default block data. In other words, +#blockId = (add << 8) + baseId. + + # Blocks, Data, Skylight, ... heightmap + #Blocks contain the block ids; Data contains the extra info: 4 bits of lighting info + 4 bits of 'extra fields' + # eg Lamp direction, crop wetness, etc. + # Heightmap gives us quick access to the top surface of everything - ie optimise out iterating through all sky blocks. + + #To access a specific block from either the block or data array from XYZ coordinates, use the following formula: + # Index = x + (y * Height + z) * Width + + ##Note that the old format is XZY ((x * 16 + z) * 16 + y) and the new format is YZX ((y * 16 + z) * 16 + x) + + #16x16 (256) ints of heightmap data. Each int records the lowest level + #in each column where the light from the sky is at full strength. Speeds up + #computing of the SkyLight. Note: This array's indexes are ordered Z,X + #whereas the other array indexes are ordered X,Z,Y. + + #loadedData -> we buffer everything into lists, then batch-create the + #vertices later. This makes the model build in Blender many, many times faster + + #list of named, distinct material meshes. add vertices to each, only in batches. + #Optimisation: 'Hollow volumes': only add if there is at least 1 orthogonal non-same-type neighbour. + #Aggressive optimisation: only load if there is 1 air orthogonal neighbour (or transparent materials). + + + + + +# 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. +# And 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 createBlock(blockID, chunkPos, blockPos, extraBlockData, vertBuffer): + """adds a vertex to the blockmesh for blockID in the relevant location.""" + print("AnvilChunkReader.createBlock") + print("blockID: " + str(blockID)) + print("chunkPos: " + str(chunkPos)) + print("blockPos: " + str(blockPos)) + print("extraBlockData: " + str(extraBlockData)) + print("") +# print("vertBuffer: " + str(vertBuffer)) + +# chunkpos is X,Z; blockpos is x,y,z for block. +# mesh = getMCBlockType(blockID, extraBlockData) #this could be inefficient. Perhaps create all the types at the start, then STOP MAKING THIS CHECK! +# if mesh is None: +# return +# +# typeName = mesh.name +# vertex = mcToBlendCoord(chunkPos, blockPos) +# +# if typeName in vertBuffer: +# vertBuffer[typeName].append(vertex) +# else: +# vertBuffer[typeName] = [vertex] + + #xyz is local to the 'stone' mesh for example. but that's from 0 (world). + #regionfile can be found from chunkPos. + #Chunkpos is an X,Z pair. + #Blockpos is an X,Y,Z triple - within chunk. diff --git a/mcregionreader.py b/mcregionreader.py new file mode 100644 index 0000000..3e5f55a --- /dev/null +++ b/mcregionreader.py @@ -0,0 +1,295 @@ +# FIXME - obsolete and likely no longer working as of 1.6.3... any reason to keep around? + +import os + +from struct import unpack #, error as StructError +import nbtreader +from mineregion import OPTIONS, EXCLUDED_BLOCKS, BLOCKDATA, REPORTING, unknownBlockIDs #, getMCBlockType, mcToBlendCoord #yuck! +##..yuck: they're immutable and don't return properly except for the dict-type ones. Get rid of this in next cleanup. + +class ChunkReader: + + #readBlock( cX,cZ,(sY?), (bX,bY,bZ) ... ) ignoring 'region' boundaries and chunk boundaries? We need an ignore-chunk-boundaries level of abstraction + + def readChunk(self, chunkPosX, chunkPosZ, vertexBuffer): # aka "readChunkFromRegion" ... + """Loads chunk located at the X,Z chunk location provided.""" + from math import floor + global REPORTING + + #region containing a given chunk is found thusly: floor of c over 32 + regionX = floor(chunkPosX / 32) + regionZ = floor(chunkPosZ / 32) + + rheaderoffset = ((chunkPosX % 32) + (chunkPosZ % 32) * 32) * 4 + + #print("Reading chunk %d,%d from region %d,%d" %(chunkPosX, chunkPosZ, regionX,regionZ)) + + rfileName = "r.%d.%d.mcr" % (regionX, regionZ) + if not os.path.exists(rfileName): + #Can't load: it doesn't exist! + print("No such region generated.") + return + + with open(rfileName, 'rb') as regfile: + # header for the chunk we want is at... + #The location in the region file of a chunk at (x, z) (in chunk coordinates) can be found at byte offset 4 * ((x mod 32) + (z mod 32) * 32) in its McRegion file. + #Its timestamp can be found 4096 bytes later in the file + regfile.seek(rheaderoffset) + cheadr = regfile.read(4) + dataoffset = unpack(">i", b'\x00'+cheadr[0:3])[0] + chunksectorcount = cheadr[3] + + if dataoffset == 0 and chunksectorcount == 0: + pass + #print("Region exists, but chunk has never been created within it.") + else: + chunkdata = self._readChunkData(regfile, dataoffset, chunksectorcount) #todo: rename that function! + #Geometry creation! etc... If surface only, can get heights etc from lightarray? + + #top level tag in NBT is an unnamed TAG_Compound, for some reason, containing a named TAG_Compound "Level" + chunkLvl = chunkdata.value['Level'].value + #chunkXPos = chunkLvl['xPos'].value + #chunkZPos = chunkLvl['zPos'].value + #print("Reading blocks for chunk: (%d, %d)\n" % (chunkXPos, chunkZPos)) + ChunkReader.readBlocks(chunkLvl, vertexBuffer) + #print("Loaded chunk %d,%d" % (chunkPosX,chunkPosZ)) + + REPORTING['totalchunks'] += 1 + + + def _readChunkData(self, bstream, chunkOffset, chunkSectorCount): #rename this! + #get the datastring out of the file... + import io, zlib + + #cf = open(fname, 'rb') + initialPos = bstream.tell() + + cstart = chunkOffset * 4096 #4 kiB + clen = chunkSectorCount * 4096 + bstream.seek(cstart) #this bstream is the region file + + chunkHeaderAndData = bstream.read(clen) + + #chunk header stuff is: + # 4 bytes: length (of remaining data) + # 1 byte : compression type (1 - gzip - unused; 2 - zlib: it should always be this in actual fact) + # then the rest, is length-1 bytes of compressed (zlib) NBT data. + + chunkDLength = unpack(">i", chunkHeaderAndData[0:4])[0] + chunkDCompression = chunkHeaderAndData[4] + if chunkDCompression != 2: + print("Not a zlib-compressed chunk!?") + raise StringError() #MinecraftSomethingError, perhaps. + + chunkZippedBytes = chunkHeaderAndData[5:] + + #could/should check that chunkZippedBytes is same length as chunkDLength-1. + + #put the regionfile byte stream back to where it started: + bstream.seek(initialPos) + + #Read the compressed chunk data + zipper = zlib.decompressobj() + chunkData = zipper.decompress(chunkZippedBytes) + chunkDataAsFile = io.BytesIO(chunkData) + chunkNBT = nbtreader.readNBT(chunkDataAsFile) + + return chunkNBT + + + #Hollow volumes optimisation (version1: in-chunk only) + def _isExposedBlock(dX,dY,dZ, blockData, blockID, idAbove, skyHighLimit, depthLimit): + #fail-fast. checks if all ortho adjacent neighbours fall inside this chunk. + #EASY! Because it's 0-15 for both X and Z. For Y, we're iterating downward, + #so get the previous value (the block above) passed in. + + if dX == 0 or dX == 15 or dY == 0 or dZ == 0 or dZ == 15 or blockID == 18: #leaves + return True + + if idAbove != blockID: + return True + + if dY == skyHighLimit or dY == depthLimit: + return True + + #GLOBALS (see readBlocks, below) + CHUNKSIZE_X = 16 #static consts - global? + CHUNKSIZE_Y = 128 + CHUNKSIZE_Z = 16 + _Y_SHIFT = 7 # 2**7 is 128. use for fast multiply + _YZ_SHIFT = 11 #16 * 128 is 2048, which is 2**11 + + #Check below: + ngbIndex = dY-1 + (dZ << _Y_SHIFT) + (dX << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = blockData[ngbIndex] + if neighbour != blockID: + return True + + #Now checked above and below. Check all sides. + #Check -X + ngbIndex = dY + (dZ << _Y_SHIFT) + ((dX-1) << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = blockData[ngbIndex] + if neighbour != blockID: + return True + + #Check +X + ngbIndex = dY + (dZ << _Y_SHIFT) + ((dX+1) << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = blockData[ngbIndex] + if neighbour != blockID: + return True + + #Check -Z + ngbIndex = dY + ((dZ-1) << _Y_SHIFT) + (dX << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = blockData[ngbIndex] + if neighbour != blockID: + return True + + #Check +Z + ngbIndex = dY + ((dZ+1) << _Y_SHIFT) + (dX << _YZ_SHIFT) #Check this lookup in readBlocks, below! Can it go o.o.b.? + neighbour = blockData[ngbIndex] + if neighbour != blockID: + return True + + return False + + + #nb: 0 is bottom bedrock, 128 is top of sky. Sea is 64. + def readBlocks(chunkLevelData, vertexBuffer): + """readBlocks(chunkLevelData) -> takes a named TAG_Compound 'Level' containing a chunk's blocks, data, heightmap, xpos,zpos, etc. + Adds the data points into a 'vertexBuffer' which is a per-named-type dictionary of ????'s. That later is made into Blender geometry via from_pydata.""" + #TODO: also TileEntities and Entities. Entities will generally be an empty list. + #TileEntities are needed for some things to define fully... + + global unknownBlockIDs + global OPTIONS, REPORTING + #skyHighLimit=128 + #depthLimit=0 + skyHighLimit = OPTIONS['highlimit'] + if skyHighLimit > 127: + skyHighLimit = 127 + depthLimit = OPTIONS['lowlimit'] + + #chunkLocation = 'xPos' 'zPos' ... + chunkX = chunkLevelData['xPos'].value + chunkZ = chunkLevelData['zPos'].value + + CHUNKSIZE_X = 16 #static consts - global? + CHUNKSIZE_Y = 128 + CHUNKSIZE_Z = 16 + + _Y_SHIFT = 7 # 2**7 is 128. use for fast multiply + _YZ_SHIFT = 11 #16 * 128 is 2048, which is 2**11 + + # Blocks, Data, Skylight, ... heightmap + #Blocks contain the block ids; Data contains the extra info: 4 bits of lighting info + 4 bits of 'extra fields' + # eg Lamp direction, crop wetness, etc. + # Heightmap gives us quick access to the top surface of everything - ie optimise out iterating through all sky blocks. + + #To access a specific block from either the block or data array from XYZ coordinates, use the following formula: + # Index = x + (y * Height + z) * Width + + #naive starting point: LOAD ALL THE BLOCKS! :D + + blockData = chunkLevelData['Blocks'].value #yields a TAG_Byte_Array value (bytes object) + heightMap = chunkLevelData['HeightMap'].value + extraData = chunkLevelData['Data'].value + + #256 bytes of heightmap data. 16 x 16. Each byte records the lowest level + #in each column where the light from the sky is at full strength. Speeds up + #computing of the SkyLight. Note: This array's indexes are ordered Z,X + #whereas the other array indexes are ordered X,Z,Y. + + #loadedData -> we buffer everything into lists, then batch-create the + #vertices later. This makes the model build in Blender many, many times faster + + #list of named, distinct material meshes. add vertices to each, only in batches. + #Optimisation: 'Hollow volumes': only add if there is at least 1 orthogonal non-same-type neighbour. + #Aggressive optimisation: only load if there is 1 air orthogonal neighbour (or transparent materials). + + # dataX will be dX, blender X will be bX. + for dX in range(CHUNKSIZE_X): + #print("looping chunk x %d" % dX) + for dZ in range(CHUNKSIZE_Z): #-1, -1, -1): + #get starting Y from heightmap, ignoring excess height iterations. + #heightByte = heightMap[dX + (dZ << 4)] # z * 16 + heightByte = 127 #Fix: always start from very top... for now + #This makes nether load properly, plus missed objects in overworld + #omitted due to lighting calculations being wrong. + if heightByte > skyHighLimit: + heightByte = skyHighLimit + #gives the LOWEST LEVEL where light is max. Start at this value, and y-- until we hit bedrock at y == 0. + dY = heightByte + oneBlockAbove = 0 #data value of the block 1 up from where we are now. (for neighbour comparisons) + #for dY in range(CHUNKSIZE_Y): # naive method (iterate all) + while dY >= depthLimit: + + blockIndex = dY + (dZ << _Y_SHIFT) + (dX << _YZ_SHIFT) # max number of bytes in a chunk is 32768. this is coming in at 32839 for XYZ: (15,71,8) + blockID = blockData[ blockIndex ] + + #except IndexError: + # print("X:%d Y:%d Z %d, blockID from before: %d, cx,cz: %d,%d. Blockindex: %d" % (dX,dY,dZ,blockID,chunkX,chunkZ, blockIndex)) + # raise IndexError + + #create this block in the output! + if blockID != 0 and blockID not in EXCLUDED_BLOCKS: # 0 is air + REPORTING['blocksread'] += 1 + + #hollowness test: + + if blockID in BLOCKDATA: + + if ChunkReader._isExposedBlock(dX,dY,dZ, blockData, blockID, oneBlockAbove, skyHighLimit, depthLimit): + #TODO: Make better version of this check, counting across chunks and regions. + #Load extra data (if applicable to blockID): + #if it has extra data, grab 4 bits from extraData + datOffset = (int(blockIndex /2)) #divided by 2 + datHiBits = blockIndex % 2 #odd or even, will be hi or low nibble + extraDatByte = extraData[datOffset] # should be a byte of which we only want part. + hiMask = 0b11110000 + loMask = 0b00001111 + extraValue = None + if datHiBits: + #get high 4, and shift right 4. + extraValue = loMask & (extraDatByte >> 4) + else: + #mask hi 4 off. + extraValue = extraDatByte & loMask + #create block in corresponding blockmesh + ChunkReader.createBlock(blockID, (chunkX, chunkZ), (dX,dY,dZ), extraValue, vertexBuffer) + else: + REPORTING['blocksdropped'] += 1 + else: + #print("Unrecognised Block ID: %d" % blockID) + #createUnknownMeshBlock() + unknownBlockIDs.add(blockID) + dY -= 1 + oneBlockAbove = blockID # set 'last read block' to current value + + + def createBlock(blockID, chunkPos, blockPos, extraBlockData, vertBuffer): + """adds a vertex to the blockmesh for blockID in the relevant location.""" + print("ChunkReader.createBlock") +# print("blockID: " + str(blockID)) +# print("chunkPos: " + str(chunkPos)) + print("blockPos: " + str(blockPos)) +# print("extraBlockData: " + str(extraBlockData)) +# print("vertBuffer: " + str(vertBuffer)) +# print("") + #chunkpos is X,Z; blockpos is x,y,z for block. +# mesh = getMCBlockType(blockID, extraBlockData) #this could be inefficient. Perhaps create all the types at the start, then STOP MAKING THIS CHECK! +# if mesh is None: +# return +# +# typeName = mesh.name +# vertex = mcToBlendCoord(chunkPos, blockPos) +# +# if typeName in vertBuffer: +# vertBuffer[typeName].append(vertex) +# else: +# vertBuffer[typeName] = [vertex] + + #xyz is local to the 'stone' mesh for example. but that's from 0 (world). + #regionfile can be found from chunkPos. + #Chunkpos is an X,Z pair. + #Blockpos is an X,Y,Z triple - within chunk. + diff --git a/mineregion.py b/mineregion.py new file mode 100644 index 0000000..587e389 --- /dev/null +++ b/mineregion.py @@ -0,0 +1,929 @@ +# 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 +#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 mcToMTCoord(chunkPos, blockPos): + """Converts a Minecraft chunk X,Z pair and a Minecraft ordered X,Y,Z block + Just remember: in Minecraft, Y points to the sky.""" + # In Minecraft, +Z (west) <--- 0 ----> -Z (east), while North is -X and South is +X + + 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 [vx,vy,vz] + +# 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 = "" + + 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]) + + 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 chunk reads 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']) +# 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 +# ... diff --git a/mineregion.pyc b/mineregion.pyc new file mode 100644 index 0000000..741388a Binary files /dev/null and b/mineregion.pyc differ diff --git a/nbtreader.py b/nbtreader.py new file mode 100644 index 0000000..3fc7869 --- /dev/null +++ b/nbtreader.py @@ -0,0 +1,272 @@ + +# NBT Reader module + +from struct import calcsize, unpack, error as StructError + +# An NBT file contains one root TAG_Compound. +TAG_END = 0 +TAG_BYTE = 1 +TAG_SHORT = 2 +TAG_INT = 3 +TAG_LONG = 4 +TAG_FLOAT = 5 +TAG_DOUBLE = 6 +TAG_BYTE_ARRAY = 7 +TAG_STRING = 8 +TAG_LIST = 9 +TAG_COMPOUND = 10 +TAG_INT_ARRAY = 11 + +INDENTCHAR = " " + + +#to read level.dat: compound, long, list short byte. int. ... end. + +#Why not just do this as a 10 element array of classes, and instantiate them as list[6](bstream) ?! MAGIC! +# that's what the py NBT guy does already! +# See struct - for handling types and bitpacking and converting to/from bytes. + +#pass classes around as objects. ie class Tag... we now have Tag in the namespace and can instantiate it by calling that one's __init__ method. + +# Note that ONLY Named Tags carry the name and tagType data. Explicitly identified Tags (such as TAG_String) only contains the payload. + +# read binary, py 3.2 etc, you get a bytes object. +# seek(pos-in-file), tell() (number of bytes read) and read(n) read n bytes... + + +class TagReader: + #a class to generate tags based on ids. + + def readNamedTag(bstream): + """Reads a named Tag from the bytestream provided. Returns a tuple of (name, tag) (where tag object is the payload). Name will be empty for Tag_END. """ + #print("Reading Named Tag\n") + tbyte = bstream.read(1)[0] # read 1 byte and get its numerical value #read 1 byte, switch type generated depending (stream-reader type 'abstract?' factory + #print("Byte read: %d" % tbyte) + tname = TAG_String(bstream).value + #print("Name read: %s" % tname) + #print("RNamedT - name is %s" %tname) + tpayload = TAGLIST[tbyte](bstream) + tpayload.name = tname + return (tname, tpayload) + #object type = bleh based on the number 0-255 you just read. Which should be a 10... for TAG_Compound. + + +def readNBT(bstream): + rootname, rootTag = TagReader.readNamedTag(bstream) + rootTag.name = rootname + + #check if not at end of string and read more NBT tags if present...? + #nfile.close() + return rootTag + + + ##DONT PASS THE TYPE IN TO EVERY INSTANCE WHEN ITS ALWAYS THE SAME! DEFINE IT AS A CLASS VAR IN THE SUBCLASSES. + + + + + + +class Tag: + type = None + + def __init__(self, bstream): + """Reads self-building data for this type from the bytestream given, until a complete tag instance is ready.""" + # Tag itself doesn't do this. Must be overridden. + self.name = "" + ## named tags..? Are named tags only named when in a tag_compound that defines their names? And tag_compounds are always named? + #self.value = "" needed? + #payload... varies by subclass. + self._parseContent(bstream) + + #Needed at all?! + def __readName(self, bstream): + """Only if called on a named tag .... will this be needed. may be Defined instead ... as a class method later""" + raise NotImplementedError(self.__class__.__name__) + pass + + def _parseContent(self, bstream): + raise NotImplementedError(self.__class__.__name__) + pass # raise notimplemented...? # SUBCLASSES IMPLEMENT THIS! + + #external code. not sure about these at all. + #Printing / bitformatting as tree + def toString(self): + return self.__class__.__name__ + ('("%s")'%self.name if self.name else "") + ": " + self.__repr__() #huh... self.repr build tree + + def printTree(self, indent=0): + return (INDENTCHAR*indent) + self.toString() + + + #could just skip this class....? +class TAG_End(Tag): + type = TAG_END + + def _parseContent(self, bstream): + pass + #so, in fact... no need for this at all!?! + + +class _TAG_Numeric(Tag): + """parses one of the numeric types (actual type defined by subclass)""" + #uses struct bitformats (within each subclass) to parse the value from the data stream... + bitformat = "" #class, not instance, var.nB: make this something that will crash badly if not overwritten properly! + + def __init__(self, bstream): + #if self.bitformat == "": + # print("INCONCEIVABLE!") + # raise NotImplementedError(self.__class__.__name__) + #print("fmt is: %s" % self.bitformat) + self.size = calcsize(self.bitformat) + super(_TAG_Numeric, self).__init__(bstream) + + def _parseContent(self, bstream): + #struct parse it using bitformat. + self.value = unpack(self.bitformat, bstream.read(self.size))[0] #[0] because this always returns a tuple + + def __repr__(self): + return "%d" % self.value + +class TAG_Byte(_TAG_Numeric): + bitformat = ">b" # class variable, NOT INSTANCE VARIABLE. + #easy, it's read 1 byte! + #def __parseContent(self, bstream): + # self.value = bstream.read(1)[0] #grab next 1 byte in stream. That's the TAG_Byte's payload. + # #or rather, set bitformat to ">c" + +class TAG_Short(_TAG_Numeric): +# type = TAG_SHORT + bitformat = ">h" + +class TAG_Int(_TAG_Numeric): + bitformat = ">i" + +class TAG_Long(_TAG_Numeric): +# id = TAG_LONG + bitformat = ">q" + +class TAG_Float(_TAG_Numeric): +# id = TAG_FLOAT + bitformat = ">f" + + def __repr__(self): + return "%0.2f" % self.value + +class TAG_Double(_TAG_Numeric): +# id = TAG_DOUBLE + bitformat = ">d" + + def __repr__(self): + return "%0.2f" % self.value + +class TAG_Byte_Array(Tag): + type = TAG_BYTE_ARRAY + def _parseContent(self, bstream): + #read the length, then grab the bytes. + length = TAG_Int(bstream) + self.value = bstream.read(length.value) #read n bytes from the file, where n is the numerical value of the length. Hope this works OK! + + def __repr__(self): + return "[%d bytes array]" % len(self.value) + +class TAG_String(Tag): + type = TAG_STRING + + def _parseContent(self, bstream): + #print ("Parsing TAG_String") + length = TAG_Short(bstream) + readbytes = bstream.read(length.value) + if len(readbytes) != length.value: + raise StructError() + self.value = readbytes.decode('utf-8') #unicode(read, "utf-8") + + def __repr__(self): + return self.value + +class TAG_List(Tag): + type = TAG_LIST + + def _parseContent(self, bstream): + tagId = TAG_Byte(bstream).value + length = TAG_Int(bstream).value + self.value = [] + for t in range(length): + self.value.append(TAGLIST[tagId](bstream)) #so that's just the tags, not the repeated type ids. makes sense. + + def __repr__(self): # use repr for outputting payload values, but printTree(indent) for outputting all. Perhaps. + if len(self.value) > 0: + return "%d items of type %s\r\n" % (len(self.value), self.value[0].__class__.__name__) #"\r\n".join([k for k in self.value.keys()]) #to be redone! + else: + return "Empty List: No Items!" + #represent self as nothing (type and name already output in printtree by the super().printTree call. Take a new line, and the rest will be output as subelements... + + def printTree(self, indent): + outstr = super(TAG_List, self).printTree(indent) + for tag in self.value: + outstr += indent*INDENTCHAR + tag.printTree(indent+1) + "\r\n" + + return outstr + + +class TAG_Compound(Tag): + type = TAG_COMPOUND + #A sequential list of Named Tags. This array keeps going until a TAG_End is found. + #NB: "Named tags" are: + #byte tagType + #TAG_String name + #[payload] + + # This is where things get named. All names must be unique within the tag-compound. So its value is a dict. + # it's named. so first thing is, read name. + # then, keep on reading until you get a Tag_END + #but, in-place create tags as you go and add them to an internal tag list... + #essentially this parses the PAYLOAD of a named TAG_Compound... + def _parseContent(self, bstream): + #tagnext = readNamedTag() + + self.value = {} + #print("Parsing TAG_Compound!") + readType = bstream.read(1)[0] #rly? + #print("First compound inner tag type byte is: %d" % readType) + while readType != TAG_END: + tname = TAG_String(bstream).value + #print ("Tag name read as: %s" % tname) + payload = TAGLIST[readType](bstream) + payload.name = tname + self.value[tname] = payload + readType = bstream.read(1)[0] + + def __repr__(self): # use repr for outputting payload values, but printTree(indent) for outputting all. Perhaps. + return "\r\n" + #represent self as nothing (type and name already output in printtree by the super().printTree call. Take a new line, and the rest will be output as subelements... + + def printTree(self, indent): + outstr = super(TAG_Compound, self).printTree(indent) + keys = self.value.keys() + for k in keys: + outstr += indent*INDENTCHAR + self.value[k].printTree(indent+1) + "\r\n" + + return outstr + +class TAG_Int_Array(Tag): + type = TAG_INT_ARRAY + def _parseContent(self, bstream): + #read the length, then grab the bytes. split those out as 4-byte integers. we hope... + tagLen = TAG_Int(bstream) + #read out all other values as tag_ints too. + ilength = tagLen.value + self.value = [] + for t in range(ilength): + self.value.append(TAG_Int(bstream).value) + + def __repr__(self): + #printslist = [str(i) for i in self.value] + #prout = ', '.join(printslist) + #return "[%d ints array] [%s]" % (len(self.value), prout) + return "[%d ints array]" % len(self.value) + + +TAGLIST = {TAG_BYTE: TAG_Byte, TAG_SHORT: TAG_Short, TAG_INT: TAG_Int, + TAG_LONG:TAG_Long, TAG_FLOAT:TAG_Float, TAG_DOUBLE:TAG_Double, + TAG_BYTE_ARRAY:TAG_Byte_Array, TAG_STRING:TAG_String, + TAG_LIST: TAG_List, TAG_COMPOUND:TAG_Compound, TAG_INT_ARRAY: TAG_Int_Array} diff --git a/scrapCode b/scrapCode new file mode 100644 index 0000000..e69de29 diff --git a/slimes.py b/slimes.py new file mode 100644 index 0000000..9ae32c9 --- /dev/null +++ b/slimes.py @@ -0,0 +1,47 @@ +# Javarandom slime Python-version test harness. + +from . import javarandom + +rnd = javarandom.Random + +def isSlimeSpawn(worldSeed, xPos, zPos): + rnd = javarandom.Random(worldSeed + jlong(xPos * xPos * 0x4c1906) + jlong(xPos * 0x5ac0db) + jlong(zPos * zPos) * 0x4307a7 + jlong(zPos * 0x5f24f) ^ 0x3ad8025f) + return rnd.nextInt(10) == 0 + +#Totally crucial! +def jlong(i): + # Python and Java don't agree on how ints work. + # Python 3 in particular treats everything as long. + #The seed A term in the RNG was wrong, before... + #This converts the unsigned generated int into a signed int if necessary. + i = (i & 0xffffffff) #vital! + + if i & (1 << 31): + i -= (1 << 32) + + return i + + +if __name__ == '__main__': + worldseed = 4784223057510287643 #Afarundria's seed. + +# for z in range(64): +# for x in range(64): +# isSlime = isSlimeSpawn(worldseed,x,z) +# print("[%d,%d: %d]" % (x,z,isSlime), end="\r\n") + + +# #write out all the seeds the above line of code would generate!! +# for z in range(64): +# for x in range(64): +# seeda = jlong(x * x * 0x4c1906) # BASTARD OF A 2's COMPLEMENT! +# seedb = jlong(x * 0x5ac0db) +# seedc = jlong(z * z) * 0x4307a7 +# seedd = jlong(z * 0x5f24f) ^ 0x3ad8025f +# +# seeder = (worldseed + seeda + seedb + seedc + seedd) +# #The seed line is INCORRECT!! +# # Here's the exact line of Java I'm trying to replicate: +# # Random rnd = new Random(seed + (long) (xPosition * xPosition * 0x4c1906) + (long) (xPosition * 0x5ac0db) + (long) (zPosition * zPosition) * 0x4307a7L + (long) (zPosition * 0x5f24f) ^ 0x3ad8025f); + +# print("[%d,%d: %d] {%d,%d,%d,%d}" % (x,z,seeder,seeda,seedb,seedc,seedd), end="\r\n") \ No newline at end of file diff --git a/sysutil.py b/sysutil.py new file mode 100644 index 0000000..8bc1f90 --- /dev/null +++ b/sysutil.py @@ -0,0 +1,19 @@ +import os, sys + +#TODO: tidy this up to one location (double defined here from mineregion) +MCPATH = '' +if sys.platform == 'darwin': + MCPATH = os.path.join(os.environ['HOME'], 'Library', 'Application Support', 'minecraft') +elif sys.platform == 'linux': + MCPATH = os.path.join(os.environ['HOME'], '.minecraft') +else: + MCPATH = os.path.join(os.environ['APPDATA'], '.minecraft') +# This needs to be set by the addon during initial inclusion. Set as a bpy.props.StringProperty within the Scene, then refer to it all over this addon. + +MCSAVEPATH = os.path.join(MCPATH, 'saves/') + +def getMCPath(): + return MCPATH + +def getMCSavePath(): + return MCSAVEPATH diff --git a/sysutil.pyc b/sysutil.pyc new file mode 100644 index 0000000..0c00c92 Binary files /dev/null and b/sysutil.pyc differ diff --git a/tomtsschem.py b/tomtsschem.py new file mode 100644 index 0000000..fb8987a --- /dev/null +++ b/tomtsschem.py @@ -0,0 +1,327 @@ +# Minetest MTS schematic exporter MCEdit filter +# by sfan5 + +import zlib +import struct +import sys +import os +import time +from thread import start_new_thread, allocate_lock + +displayName = "Export to Minetest MTS schematic" + +#Reference MC: http://hydra-media.cursecdn.com/minecraft.gamepedia.com/8/8c/DataValuesBeta.png +#Reference MT: +# https://github.com/minetest/minetest_game/blob/master/mods/default/nodes.lua +# https://github.com/minetest/minetest_game/blob/master/mods/wool/init.lua +# https://github.com/minetest/minetest_game/blob/master/mods/stairs/init.lua +# https://github.com/minetest/minetest_game/blob/master/mods/flowers/init.lua +conversionTable = [ + #blockid blockdata minetest-nodename + #blockdata -1 means ignore + #blockdata -2 means copy without change + #blockdata -3 means copy and convert the mc facedir value to mt facedir + #blockdata -4 is for stairs to support upside down ones + + (1 , -1, "default:stone"), + (2 , -1, "default:dirt_with_grass"), + (3 , -1, "default:dirt"), + (4 , -1, "default:cobble"), + (5 , 3, "default:junglewood"), + (5 , -1, "default:wood"), + (6 , 3, "default:junglesapling"), + (6 , -1, "default:sapling"), + (7 , -1, "default:nyancat_rainbow"), # FIXME Bedrock + (8 , -1, "default:water_flowing"), + (9 , -1, "default:water_source"), + (10 , -1, "default:lava_flowing"), + (11 , -1, "default:lava_source"), + (12 , -1, "default:sand"), + (13 , -1, "default:gravel"), + (14 , -1, "default:stone_with_gold"), + (15 , -1, "default:stone_with_iron"), + (16 , -1, "default:stone_with_coal"), + (17 , 3, "default:jungletree"), + (17 , -1, "default:tree"), + (18 , 3, "default:jungleleaves"), + (18 , -1, "default:leaves"), + (20 , -1, "default:glass"), + (21 , -1, "default:stone_with_copper"), + (22 , -1, "default:copperblock"), + (24 , 1, "default:sandstonebrick"), + (24 , -1, "default:sandstone"), + (31 , 0, "default:dry_shrub"), + (31 , 1, "default:grass_4"), + (31 , 2, "default:grass_3"), + (31 , -1, "default:grass_1"), + (32 , -1, "default:dry_shrub"), + (35 , 0, "wool:white"), + (35 , 1, "wool:orange"), + (35 , 4, "wool:yellow"), + (35 , 5, "wool:green"), + (35 , 6, "wool:pink"), + (35 , 7, "wool:dark_grey"), + (35 , 8, "wool:grey"), + (35 , 9, "wool:cyan"), + (35 , 10, "wool:violet"), + (35 , 11, "wool:blue"), + (35 , 12, "wool:brown"), + (35 , 13, "wool:dark_green"), + (35 , 14, "wool:red"), + (35 , 15, "wool:black"), + (37 , -1, "flowers:dandelion_yellow"), + (38 , 0, "flowers:rose"), + (38 , 4, "flowers:tulip"), + (38 , 5, "flowers:tulip"), + (38 , 6, "flowers:tulip"), + (38 , 7, "flowers:tulip"), + (38 , 8, "flowers:dandelion_white"), + (38 , -1, "flowers:geranium"), # Convert all other flowers to a geranium + (41 , -1, "default:goldblock"), + (42 , -1, "default:steelblock"), + (43 , 1, "default:sandstone"), + (43 , 2, "default:wood"), + (43 , 3, "default:cobble"), + (43 , 4, "default:brick"), + (43 , 5, "default:stonebrick"), + (44 , 0, "stairs:slab_stone"), + (44 , 1, "stairs:slab_sandstone"), + (44 , 2, "stairs:slab_wood"), + (44 , 3, "stairs:slab_cobble"), + (44 , 4, "stairs:slab_brick"), + (44 , 5, "stairs:slab_stonebrick"), + (44 , 8, "stairs:slab_stoneupside_down"), + (44 , 9, "stairs:slab_sandstoneupside_down"), + (44 , 10, "stairs:slab_woodupside_down"), + (44 , 11, "stairs:slab_cobbleupside_down"), + (44 , 12, "stairs:slab_brickupside_down"), + (44 , 13, "stairs:slab_stonebrickupside_down"), + (45 , -1, "default:brick"), + (47 , -1, "default:bookshelf"), + (48 , -1, "default:mossycobble"), + (49 , -1, "default:obsidian"), + (50 , -3, "default:torch"), + (51 , -1, "fire:basic_flame"), + (53 , -4, "stairs:stair_wood"), + (54 , -1, "default:chest"), + (56 , -1, "default:stone_with_diamond"), + (57 , -1, "default:diamondblock"), + (61 , -1, "default:furnace"), + (62 , -1, "default:furnace_active"), + (63 , -1, "default:sign_wood"), + (64 , -1, "doors:door_wood_t_1"), + (65 , -1, "default:ladder"), + (66 , -1, "default:rail"), + (67 , -4, "stairs:stair_cobble"), + (68 , -3, "default:sign_wood"), + (71 , -1, "doors:door_steel_t_1"), + (78 , -1, "default:snow"), + (79 , -1, "default:ice"), + (80 , -1, "default:snowblock"), + (81 , -1, "default:cactus"), + (82 , -1, "default:clay"), + (83 , -1, "default:papyrus"), + (85 , -1, "default:fence_wood"), + (98 , -1, "default:stonebrick"), + (108, -4, "stairs:stair_brick"), + (109, -3, "stairs:stair_stonebrick"), + (125, 3, "default:junglewood"), + (125, -1, "default:wood"), + (126, 3, "stairs:slab_junglewood"), + (126, -1, "stairs:slab_wood"), + (128, -4, "stairs:stair_sandstone"), + (129, -1, "default:stone_with_mese"), + (133, -1, "default:mese"), + (134, -4, "stairs:stair_wood"), + (135, -4, "stairs:stair_wood"), + (136, -4, "stairs:stair_junglewood"), + + #Mesecons section + # Reference: https://github.com/Jeija/minetest-mod-mesecons + (25 , -1, "mesecons_noteblock:noteblock", "mesecons"), + (29 , -3, "mesecons_pistons:piston_sticky_off", "mesecons"), + (33 , -3, "mesecons_pistons:piston_normal_off", "mesecons"), + (55 , -1, "mesecons:wire_00000000_off", "mesecons"), + (69 , -3, "mesecons_walllever:wall_lever_off", "mesecons"), + (70 , -1, "mesecons_pressureplates:pressure_plate_stone_off", "mesecons"), + (72 , -1, "mesecons_pressureplates:pressure_plate_wood_off", "mesecons"), + (73 , -1, "default:stone_with_mese", "mesecons"), + (74 , -1, "default:stone_with_mese", "mesecons"), + (75 , -3, "mesecons_torch:torch_off", "mesecons"), + (76 , -3, "mesecons_torch:torch_on", "mesecons"), + (77 , -3, "mesecons_button:button_off", "mesecons"), + (93 , -3, "mesecons_delayer:delayer_off_1", "mesecons"), + (94 , -3, "mesecons_delayer:delayer_on_1", "mesecons"), + (123, -1, "mesecons_lightstone_red_off", "mesecons"), + (124, -1, "mesecons_lightstone_red_on", "mesecons"), + (137, -1, "mesecons_commandblock:commandblock_off", "mesecons"), + (151, -1, "mesecons_solarpanel:solar_panel_off", "mesecons"), + (152, -1, "default:mese", "mesecons"), + + #Nether section + # Reference: https://github.com/PilzAdam/nether/blob/master/init.lua + (43 , 6, "nether:brick", "nether"), + (87 , -1, "nether:rack", "nether"), + (88 , -1, "nether:sand", "nether"), + (89 , -1, "nether:glowstone", "nether"), + (90 , -3, "nether:portal", "nether"), +] + +inputs = ( + ("Output filename", "string"), + ("Compression level (1=fastest, 9=best)", ("7", "1", "2", "3", "4", "5", "6", "8", "9")), + ("Enabled Mods", "label"), + ("Mesecons", ("No", "Yes")), + ("Nether", ("No", "Yes")), +) + +numconverted = 0 +numconverted_lastsec = 0 +numconverted_lastsec_lock = allocate_lock() +nps_thread_exit = False + +def nps_thread(): + global numconverted, numconverted_lastsec, numconverted_lastsec_lock + while not nps_thread_exit: + time.sleep(1) + numconverted_lastsec_lock.acquire() + numconverted_lastsec = numconverted + numconverted_lastsec_lock.release() + +def mc2mtFacedir(blockdata): + #Minetest + # x+ = 2 + # x- = 3 + # z+ = 1 + # z- = 0 + #Minecraft + # x+ = 3 + # x- = 1 + # z+ = 0 + # z- = 2 + tbl = { + 3: 2, + 1: 3, + 0: 1, + 2: 0, + } + return tbl.get(blockdata, 0) + +def mc2mtstairs(tpl): + if tpl[1] >= 4: + return (tpl[0] + "upside_down", mc2mtFacedir(tpl[1] - 4)) + else: + return (tpl[0], mc2mtFacedir(tpl[1])) + + +def findConversion(blockid, blockdata, mods): + if blockid == 0: + return None + for cnv in conversionTable: + if blockid != cnv[0]: + continue + if len(cnv) >= 4: + if mods.get(cnv[3], False) == False: + continue + if cnv[1] == -1: + return (cnv[2], 0) + elif cnv[1] == -2: + return (cnv[2], blockdata) + elif cnv[1] == -3: + return (cnv[2], mc2mtFacedir(blockdata)) + elif cnv[1] == -4: + return mc2mtstairs((cnv[2], blockdata)) + elif cnv[1] != blockdata: + continue + return (cnv[2], 0) + return None + +def perform(level, box, options): + global numconverted, numconverted_lastsec, numconverted_lastsec_lock + def getnodeid(arr, nn): + if not nn in arr: + arr.append(nn) + return arr.index(nn) + def crout(): + # zlib object, compressed data + return [zlib.compressobj(1), ""] + def wrout(where, what): + where[1] += where[0].compress(what) + def retrout(where): + cd = where[1] + where[0].flush() + del where[0] + return zlib.decompress(cd) + + try: + f = open(options["Output filename"] + ".mts", 'w') + except: + raise + + size = (box.maxx - box.minx, box.maxy - box.miny, box.maxz - box.minz) + complvl = int(options["Compression level (1=fastest, 9=best)"]) + print("Saving file at: %s/%s.mts" % (os.getcwd(), options["Output filename"])) + + mods = {} + for arg in options.keys(): + if options[arg] == "Yes": + mods[arg.lower()] = True + + nodenames = [] + outdata1 = crout() + outdata2 = crout() + + numnodes = size[0] * size[1] * size[2] + print_pc_interval = numnodes * 0.0025 + print_counter = 0 + nps_avg = 0 + start_new_thread(nps_thread, ()) + start = time.time() + sys.stdout.write("\n") + + for z in xrange(box.minz, box.maxz): + for y in xrange(box.miny, box.maxy): + for x in xrange(box.minx, box.maxx): + c = findConversion(level.blockAt(x, y, z), level.blockDataAt(x, y, z), mods) + if c == None: + wrout(outdata1, struct.pack("!H", getnodeid(nodenames, "air"))) + wrout(outdata2, "\x00") + else: + wrout(outdata1, struct.pack("!H", getnodeid(nodenames, c[0]))) + wrout(outdata2, chr(c[1])) + print_counter += 1 + numconverted += 1 + if print_counter >= print_pc_interval: + numconverted_lastsec_lock.acquire() + nps_avg = (nps_avg + (numconverted - numconverted_lastsec)) / 2 + sys.stdout.write( + "\r%0.2f%% done, %d nodes / sec, ETA: %d sec(s) " + % ( + (float(numconverted) / numnodes) * 100, + nps_avg, + float(numnodes) / nps_avg, + ) + ) + numconverted_lastsec_lock.release() + sys.stdout.flush() + print_counter = 0 + + compr = zlib.compressobj(complvl) + outdata = "" + outdata += compr.compress(retrout(outdata1)) + outdata += compr.compress("\xff" * numnodes) + outdata += compr.compress(retrout(outdata2)) + outdata += compr.flush() + del compr + end = time.time() + sys.stdout.write("\rFinished in %0.3f seconds!" % (end-start,)) + sys.stdout.flush() + + f.write("MTSM") + f.write(struct.pack("!HHHH", 3, size[0], size[1], size[2])) + for i in range(size[1]): + f.write(chr(0xff)) + f.write(struct.pack("!H", len(nodenames))) + for nn in nodenames: + f.write(struct.pack("!H", len(nn)) + nn) + f.write(outdata) + f.close() diff --git a/tomtweschem.py b/tomtweschem.py new file mode 100644 index 0000000..2ce59d1 --- /dev/null +++ b/tomtweschem.py @@ -0,0 +1,247 @@ +# Minecraft to Minetest WE schematic MCEdit filter +# by sfan5 + +displayName = "-> Minetest WE schematic" + +#Reference MC: http://media-mcw.cursecdn.com/8/8c/DataValuesBeta.png +#Reference MT: +# https://github.com/minetest/common/blob/master/mods/default/init.lua +# https://github.com/minetest/common/blob/master/mods/wool/init.lua +# https://github.com/minetest/common/blob/master/mods/stairs/init.lua +conversionTable = [ + #blockid blockdata minetest-nodename + #blockdata -1 means ignore + #blockdata -2 means copy without change + #blockdata -3 means copy and convert the mc facedir value to mt facedir + #blockdata -4 is for stairs to support upside down ones + + (1 , -1, "default:stone"), + (2 , -1, "default:dirt_with_grass"), + (3 , -1, "default:dirt"), + (4 , -1, "default:cobble"), + (5 , 3, "default:junglewood"), + (5 , -1, "default:wood"), + (6 , 3, "default:junglesapling"), + (6 , -1, "default:sapling"), + (7 , -1, "default:nyancat_rainbow"), # FIXME Bedrock + (8 , -1, "default:water_flowing"), + (9 , -1, "default:water_source"), + (10 , -1, "default:lava_flowing"), + (11 , -1, "default:lava_source"), + (12 , -1, "default:sand"), + (13 , -1, "default:gravel"), + (14 , -1, "default:stone_with_gold"), + (15 , -1, "default:stone_with_iron"), + (16 , -1, "default:stone_with_coal"), + (17 , 3, "default:jungletree"), + (17 , -1, "default:tree"), + (18 , 3, "default:jungleleaves"), + (18 , -1, "default:leaves"), + (20 , -1, "default:glass"), + (21 , -1, "default:stone_with_copper"), + (22 , -1, "default:copperblock"), + (24 , 1, "default:sandstonebrick"), + (24 , -1, "default:sandstone"), + (31 , 0, "default:dry_shrub"), + (31 , 1, "default:grass_4"), + (31 , 2, "default:grass_3"), + (31 , -1, "default:grass_1"), + (32 , -1, "default:dry_shrub"), + (35 , 0, "wool:white"), + (35 , 1, "wool:orange"), + (35 , 4, "wool:yellow"), + (35 , 5, "wool:green"), + (35 , 6, "wool:pink"), + (35 , 7, "wool:dark_grey"), + (35 , 8, "wool:grey"), + (35 , 9, "wool:cyan"), + (35 , 10, "wool:violet"), + (35 , 11, "wool:blue"), + (35 , 12, "wool:brown"), + (35 , 13, "wool:dark_green"), + (35 , 14, "wool:red"), + (35 , 15, "wool:black"), + (37 , -1, "flowers:dandelion_yellow"), + (38 , -1, "flowers:rose"), + (41 , -1, "default:goldblock"), + (42 , -1, "default:steelblock"), + (43 , 1, "default:sandstone"), + (43 , 2, "default:wood"), + (43 , 3, "default:cobble"), + (43 , 4, "default:brick"), + (43 , 5, "default:stonebrick"), + (44 , 0, "stairs:slab_stone"), + (44 , 1, "stairs:slab_sandstone"), + (44 , 2, "stairs:slab_wood"), + (44 , 3, "stairs:slab_cobble"), + (44 , 4, "stairs:slab_brick"), + (44 , 5, "stairs:slab_stonebrick"), + (44 , 8, "stairs:slab_stoneupside_down"), + (44 , 9, "stairs:slab_sandstoneupside_down"), + (44 , 10, "stairs:slab_woodupside_down"), + (44 , 11, "stairs:slab_cobbleupside_down"), + (44 , 12, "stairs:slab_brickupside_down"), + (44 , 13, "stairs:slab_stonebrickupside_down"), + (45 , -1, "default:brick"), + (47 , -1, "default:bookshelf"), + (48 , -1, "default:mossycobble"), + (49 , -1, "default:obsidian"), + (50 , -3, "default:torch"), + (51 , -1, "fire:basic_flame"), + (53 , -4, "stairs:stair_wood"), + (54 , -1, "default:chest"), + (56 , -1, "default:stone_with_diamond"), + (57 , -1, "default:diamondblock"), + (61 , -1, "default:furnace"), + (62 , -1, "default:furnace_active"), + (63 , -1, "default:sign_wood"), + (64 , -1, "doors:door_wood_t_1"), + (65 , -1, "default:ladder"), + (66 , -1, "default:rail"), + (67 , -4, "stairs:stair_cobble"), + (68 , -3, "default:sign_wood"), + (71 , -1, "doors:door_steel_t_1"), + (78 , -1, "default:snow"), + (79 , -1, "default:ice"), + (80 , -1, "default:snowblock"), + (81 , -1, "default:cactus"), + (82 , -1, "default:clay"), + (83 , -1, "default:papyrus"), + (85 , -1, "default:fence_wood"), + (98 , -1, "default:stonebrick"), + (108, -4, "stairs:stair_brick"), + (109, -3, "stairs:stair_stonebrick"), + (125, 3, "default:junglewood"), + (125, -1, "default:wood"), + (126, 3, "stairs:slab_junglewood"), + (126, -1, "stairs:slab_wood"), + (128, -4, "stairs:stair_sandstone"), + (129, -1, "default:stone_with_mese"), + (133, -1, "default:mese"), + (134, -4, "stairs:stair_wood"), + (135, -4, "stairs:stair_wood"), + (136, -4, "stairs:stair_junglewood"), + + #Mesecons section + # Reference: https://github.com/Jeija/minetest-mod-mesecons/blob/master/mesecons_alias/init.lua + (25 , -1, "mesecons_noteblock:noteblock", "mesecons"), + (29 , -3, "mesecons_pistons:piston_sticky_off", "mesecons"), + (33 , -3, "mesecons_pistons:piston_normal_off", "mesecons"), + (55 , -1, "mesecons:wire_00000000_off", "mesecons"), + (69 , -3, "mesecons_walllever:wall_lever_off", "mesecons"), + (70 , -1, "mesecons_pressureplates:pressure_plate_stone_off", "mesecons"), + (72 , -1, "mesecons_pressureplates:pressure_plate_wood_off", "mesecons"), + (73 , -1, "default:stone_with_mese", "mesecons"), + (74 , -1, "default:stone_with_mese", "mesecons"), + (75 , -3, "mesecons_torch:torch_off", "mesecons"), + (76 , -3, "mesecons_torch:torch_on", "mesecons"), + (77 , -3, "mesecons_button:button_off", "mesecons"), + (93 , -3, "mesecons_delayer:delayer_off_1", "mesecons"), + (94 , -3, "mesecons_delayer:delayer_on_1", "mesecons"), + (123, -1, "mesecons_lightstone_red_off", "mesecons"), + (124, -1, "mesecons_lightstone_red_on", "mesecons"), + (137, -1, "mesecons_commandblock:commandblock_off", "mesecons"), + (151, -1, "mesecons_solarpanel:solar_panel_off", "mesecons"), + (152, -1, "default:mese", "mesecons"), + + #Nether section + # Reference: https://github.com/PilzAdam/nether/blob/master/init.lua + (43 , 6, "nether:brick", "nether"), + (87 , -1, "nether:rack", "nether"), + (88 , -1, "nether:sand", "nether"), + (89 , -1, "nether:glowstone", "nether"), + (90 , -3, "nether:portal", "nether"), + + #Riesenpilz Section + # Reference: https://github.com/HybridDog/riesenpilz/blob/master/init.lua + (39 , -1, "riesenpilz:brown", "riesenpilz"), + (40 , -1, "riesenpilz:red", "riesenpilz"), + (99 , -3, "riesenpilz:head_brown", "riesenpilz"), + (100, -3, "riesenpilz:head_brown", "riesenpilz"), +] + +inputs = ( + ("Output filename", "string"), + ("Enabled Mods", "label"), + ("Mesecons", ("False", "True")), + ("Nether", ("False", "True")), + ("Riesenpilz", ("False", "True")), +) + +def mc2mtFacedir(blockdata): + #Minetest + # x+ = 2 + # x- = 3 + # z+ = 1 + # z- = 0 + #Minecraft + # x+ = 3 + # x- = 1 + # z+ = 0 + # z- = 2 + tbl = { + 3: 2, + 1: 3, + 0: 1, + 2: 0, + } + return tbl.get(blockdata, 0) + +def mc2mtstairs(tpl): + if tpl[1] >= 4: + return (tpl[0] + "upside_down", mc2mtFacedir(tpl[1] - 4)) + else: + return (tpl[0], mc2mtFacedir(tpl[1])) + + +def findConversion(blockid, blockdata, mods): + if blockid == 0: + return None + for cnv in conversionTable: + if blockid != cnv[0]: + continue + if len(cnv) >= 4: + if mods.get(cnv[3], False) == False: + continue + if cnv[1] == -1: + return (cnv[2], 0) + elif cnv[1] == -2: + return (cnv[2], blockdata) + elif cnv[1] == -3: + return (cnv[2], mc2mtFacedir(blockdata)) + elif cnv[1] == -4: + return mc2mtstairs((cnv[2], blockdata)) + elif cnv[1] != blockdata: + continue + return (cnv[2], 0) + return None + +def perform(level, box, options): + try: + f = open("../" + options["Output filename"] + ".we", 'w') + except: + raise + + origin = ( + box.minx + int((box.maxx - box.minx) / 2), + box.miny + int((box.maxy - box.miny) / 2), + box.minz + int((box.maxz - box.minz) / 2), + ) + + mods = {} + for arg in options.keys(): + if options[arg] == "True": + mods[arg.lower()] = True + + for x in xrange(box.minx, box.maxx): + for z in xrange(box.minz, box.maxz): + for y in xrange(box.miny, box.maxy): + c = findConversion(level.blockAt(x, y, z), level.blockDataAt(x, y, z), mods) + if c == None: + continue + calcpos = (x - origin[0], y - origin[1], z - origin[2]) + fmttpl = calcpos + (c[0], level.blockLightAt(x, y, z), c[1]) + f.write("%d %d %d %s %d %d\n" % fmttpl) + + f.close() + diff --git a/writeNoodleBuilder.py b/writeNoodleBuilder.py new file mode 100644 index 0000000..668c067 --- /dev/null +++ b/writeNoodleBuilder.py @@ -0,0 +1,66 @@ +import bpy + +def writeNoodleBuilder(material): + """Takes a node tree from a material, and writes out a new script that creates + the node tree in question.""" + + nodeTree = material.node_tree + + nodes = [] + links = [] + + for n in nodeTree.nodes: + pos = (n.location[0], n.location[1]) + ins = [] #record default values where set. + op = None + + for i in n.inputs: + if hasattr(i, 'default_value'): + ins.append((i.name, i.default_value)) + #else don't need it. + + if hasattr(n, "operation"): + #it's a mathnode with operation, etc etc + op = n.operation + + nodes.append((n.name, n.label, pos, ins, n.type, op)) + + for l in nodeTree.links: + linkData = (l.from_node.name, l.from_socket.name, l.to_node.name, l.to_socket.name) + links.append(linkData) + + #create a script to recreate this exact layout on demand. + + t = bpy.data.texts.new(name="noodleBuilder1.py") + t.write("import bpy\n") + t.write("mat = bpy.data.materials['JIMMY']\nntree = mat.node_tree\n") + t.write("ntree.nodes.clear()\n\n#Now recreate from scripted structure:\n") + # + for nspec in nodes: + #line to create the node and position it: + t.write("nn = ntree.nodes.new(type=\"{0}\")\n".format(nspec[4])) + t.write("nn.name = \"{0}\"\n".format(nspec[0])) + if nspec[5] is not None: + t.write("nn.operation = '{0}'\n".format(nspec[5])) + if nspec[1] != "": + t.write("nn.label = \"%s\"\n" % nspec[1]) + t.write("nn.location = Vector(({:.3f}, {:.3f}))\n".format(nspec[2][0], nspec[2][1])) + for ins in nspec[3]: + t.write("nn.inputs['"+ins[0]+"'].default_value = "+ ins[1].__repr__() + "\n") #doesn't work for text-type values + t.write("#link creation\n") + t.write("nd = ntree.nodes\nlinks = ntree.links\n") + for lspec in links: + #it's from_node, from_socket, to_node, to_socket. + #Creation lines look like this: + t.write("links.new(input=nd['%s'].outputs['%s'], output=nd['%s'].inputs['%s'])\n" % lspec) + + + + + +#Now use this to get the node script for material X! +mat = bpy.data.materials['RailMat'] + +writeNoodleBuilder(mat) + +#Check scripts! There should now be a new one. \ No newline at end of file