Initial working code
parent
c0e43df1df
commit
b98126a76f
|
@ -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>
|
|
@ -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>
|
|
@ -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
|
|
@ -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.
|
@ -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()
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -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()
|
|
@ -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.
|
|
@ -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.
|
||||||
|
|
|
@ -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
|
||||||
|
# ...
|
Binary file not shown.
|
@ -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,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")
|
|
@ -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
|
Binary file not shown.
|
@ -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()
|
|
@ -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()
|
||||||
|
|
|
@ -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.
|
Loading…
Reference in New Issue