Initial working code

master
David 2015-02-07 22:43:21 +00:00
parent c0e43df1df
commit b98126a76f
26 changed files with 5147 additions and 0 deletions

17
.project Normal file
View File

@ -0,0 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>mc2mt</name>
<comment></comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>

8
.pydevproject Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>
<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}</path>
</pydev_pathproperty>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python 3.0</pydev_property>
<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">python3</pydev_property>
</pydev_project>

View File

@ -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

286
__init__.py Normal file
View File

@ -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 #####
# <pep8 compliant>
# 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()

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

302
blender.py Normal file
View File

@ -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 #####
# <pep8 compliant>
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()

0
blender.py~ Normal file
View File

1543
blockbuild.py Normal file

File diff suppressed because it is too large Load Diff

BIN
blockbuild.pyc Normal file

Binary file not shown.

285
mc2mt.py Normal file
View File

@ -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 #####
# <pep8 compliant>
# 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()

486
mcanvilreader.py Normal file
View File

@ -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.

295
mcregionreader.py Normal file
View File

@ -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.

929
mineregion.py Normal file
View File

@ -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 = "<no name>"
wsize = wData.value['Data'].value['SizeOnDisk'].value
readableSize = "(%0.1f)" % (wsize / (1024*1024))
worldList.append((sf, sf, wname + " " + readableSize))
wcount += 1
except KeyError:
print("key not found in %s" % wData.value['Data'])
os.chdir(startpath)
if worldList != []:
return worldList
else:
return None
def hasNether(worldFolder):
if worldFolder == "":
return False
worldList = []
MCSAVEPATH=sysutil.getMCSavePath()
if os.path.exists(MCSAVEPATH):
worldList = os.listdir(MCSAVEPATH)
if worldFolder in worldList:
wp = os.path.join(MCSAVEPATH, worldFolder, 'DIM-1')
return os.path.exists(wp)
#and: contains correct files? also check regions aren't empty.
return False
def hasEnd(worldFolder):
if worldFolder == "":
return False
worldList = []
MCSAVEPATH=sysutil.getMCSavePath()
if os.path.exists(MCSAVEPATH):
worldList = os.listdir(MCSAVEPATH)
if worldFolder in worldList:
wp = os.path.join(MCSAVEPATH, worldFolder, 'DIM1')
return os.path.exists(wp)
#and: contains correct files? also check regions aren't empty.
return False
def readMinecraftWorld(worldFolder, loadRadius, toggleOptions):
global unknownBlockIDs, wseed
global EXCLUDED_BLOCKS
global WORLD_ROOT
global OPTIONS, REPORTING
OPTIONS = toggleOptions
#timing/profiling:
global tChunkReadTimes
if worldFolder == "":
#World selected was blank. No saves. i.e. only when world list is empty
print("No valid saved worlds were available to load.")
return
# print("[!] OmitStone: ", toggleOptions['omitstone'])
if not OPTIONS['omitstone']:
EXCLUDED_BLOCKS = []
# print('[[[exluding these blocks: ', EXCLUDED_BLOCKS, ']]]')
worldList = []
MCSAVEPATH=sysutil.getMCSavePath()
if os.path.exists(MCSAVEPATH):
worldList = os.listdir(MCSAVEPATH)
#print("MC Path exists! %s" % os.listdir(MCPATH))
#wherever os was before, save it, and restore it after this completes.
os.chdir(MCSAVEPATH)
worldSelected = worldFolder
os.chdir(os.path.join(MCSAVEPATH, worldSelected))
# If there's a folder DIM-1 in the world folder, you've been to the Nether!
# ...And generated Nether regions.
if os.path.exists('DIM-1'):
if OPTIONS['loadnether']:
print('nether LOAD!')
else:
print('Nether is present, but not chosen to load.')
if os.path.exists('DIM1'):
if OPTIONS['loadend']:
print('load The End...')
else:
print('The End is present, but not chosen to load.')
#if the player didn't save out in those dimensions, we HAVE TO load at 3D cursor (or 0,0,0)
worldData = None
pSaveDim = None
worldFormat = 'mcregion' #assume initially
with gzip.open('level.dat', 'rb') as levelDat:
worldData = nbtreader.readNBT(levelDat)
#print(worlddata.printTree(0))
#Check if it's a multiplayer saved game (that's been moved into saves dir)
#These don't have the Player tag.
if 'Player' in worldData.value['Data'].value:
#It's singleplayer
pPos = [posFloat.value for posFloat in worldData.value['Data'].value['Player'].value['Pos'].value ] #in NBT, there's a lot of value...
pSaveDim = worldData.value['Data'].value['Player'].value['Dimension'].value
print('Player: '+str(pSaveDim)+', ppos: '+str(pPos))
else:
#It's multiplayer.
#Get SpawnX, SpawnY, SpawnZ and centre around those. OR
#TODO: Check for another subfolder: 'players'. Read each NBT .dat in
#there, create empties for all of them, but load around the first one.
spX = worldData.value['Data'].value['SpawnX'].value
spY = worldData.value['Data'].value['SpawnY'].value
spZ = worldData.value['Data'].value['SpawnZ'].value
pPos = [float(spX), float(spY), float(spZ)]
#create empty markers for each player.
#and: could it load multiplayer nether/end based on player loc?
if 'version' in worldData.value['Data'].value:
fmtVersion = worldData.value['Data'].value['version'].value
#19133 for Anvil. 19132 is McRegion.
if fmtVersion == MCREGION_VERSION_ID:
print("World is in McRegion format")
elif fmtVersion == ANVIL_VERSION_ID:
print("World is in Anvil format")
worldFormat = "anvil"
wseed = worldData.value['Data'].value['RandomSeed'].value #it's a Long
print("World Seed : %d" % (wseed)) # or self.report....
#NB: we load at cursor if player location undefined loading into Nether
if OPTIONS['atcursor'] or (OPTIONS['loadnether'] and (pSaveDim is None or int(pSaveDim) != -1)):
# cursorPos = bpy.context.scene.cursor_location
#that's an x,y,z vector (in Blender coords)
#convert to insane Minecraft coords! (Minecraft pos = -Y, Z, -X)
# pPos = [ -cursorPos[1], cursorPos[2], -cursorPos[0]]
pPos = [ 0, 0, 0]
if OPTIONS['loadnether']:
os.chdir(os.path.join("DIM-1", "region"))
elif OPTIONS['loadend']:
os.chdir(os.path.join("DIM1", "region"))
else:
os.chdir("region")
meshBuffer = {}
blockBuffer = {}
#Initialise the world root - an empty to parent all land objects to.
# WORLD_ROOT = bpy.data.objects.new(worldSelected, None) #,None => EMPTY!
# bpy.context.scene.objects.link(WORLD_ROOT)
# WORLD_ROOT.empty_draw_size = 2.0
# WORLD_ROOT.empty_draw_type = 'SPHERE'
regionfiles = []
regionreader = None
if worldFormat == 'mcregion':
regionfiles = [f for f in os.listdir() if f.endswith('.mcr')]
from mcregionreader import ChunkReader
regionreader = ChunkReader() #work it with the class, not an instance?
#all this importing is now very messy.
elif worldFormat == 'anvil':
regionfiles = [f for f in os.listdir() if f.endswith('.mca')]
from mcanvilreader import AnvilChunkReader
regionreader = AnvilChunkReader()
#except when loading nether...
playerChunk = toChunkPos(pPos[0], pPos[2]) # x, z
print("Loading %d blocks around centre." % loadRadius)
#loadRadius = 10 #Sane amount: 5 or 4.
# if not OPTIONS['atcursor']: #loading at player
# #Add an Empty to show where the player is. (+CENTRE CAMERA ON!)
# playerpos = bpy.data.objects.new('PlayerLoc', None)
# #set its coordinates...
# #convert Minecraft coordinate position of player into Blender coords:
# playerpos.location[0] = -pPos[2]
# playerpos.location[1] = -pPos[0]
# playerpos.location[2] = pPos[1]
# bpy.context.scene.objects.link(playerpos)
# playerpos.parent = WORLD_ROOT
#total chunk count across region files:
REPORTING['totalchunks'] = 0
pX = int(playerChunk[0])
pZ = int(playerChunk[1])
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
# ...

BIN
mineregion.pyc Normal file

Binary file not shown.

272
nbtreader.py Normal file
View File

@ -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}

0
scrapCode Normal file
View File

47
slimes.py Normal file
View File

@ -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")

19
sysutil.py Normal file
View File

@ -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

BIN
sysutil.pyc Normal file

Binary file not shown.

327
tomtsschem.py Normal file
View File

@ -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()

247
tomtweschem.py Normal file
View File

@ -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()

66
writeNoodleBuilder.py Normal file
View File

@ -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.