Example #1
0
def main():
    """Creates a specified region and downloads files from USGS."""
    # example:
    # ./GetRegion.py --name BlockIsland --ymax 41.2378 --ymin 41.1415 --xmin -71.6202 --xmax -71.5332

    # defaults
    default_orthoIDs     = ','.join(Region.productIDs['ortho'])
    default_elevationIDs = ','.join(Region.productIDs['elevation'])
    default_landcoverIDs = ','.join(Region.productIDs['landcover'])

    # parse options and get results
    parser = argparse.ArgumentParser(description='Create regions and download files from USGS.')
    parser.add_argument('--name', required=True, type=str, help='name of the region to be generated')
    parser.add_argument('--xmax', required=True, type=float, help='easternmost longitude (west is negative)')
    parser.add_argument('--xmin', required=True, type=float, help='westernmost longitude (west is negative)')
    parser.add_argument('--ymax', required=True, type=float, help='northernmost latitude (south is negative)')
    parser.add_argument('--ymin', required=True, type=float, help='southernmost longitude (south is negative)')
    parser.add_argument('--tilesize', type=int, help='tilesize value (default %d)' % Region.tilesize)
    parser.add_argument('--scale', type=int, help='scale value (default %d)' % Region.scale)
    parser.add_argument('--vscale', type=int, help='vscale value (default %d)' % Region.vscale)
    parser.add_argument('--trim', type=int, help='trim value (default %d)' % Region.trim)
    parser.add_argument('--sealevel', type=int, help='sealevel value (default %d)' % Region.sealevel)
    parser.add_argument('--maxdepth', type=int, help='maxdepth value (default %d)' % Region.maxdepth)
    parser.add_argument('--orthoIDs', default=default_orthoIDs, type=checkOrthoIDs, help='ordered list of product IDs (default %s)' % default_orthoIDs)
    parser.add_argument('--elevationIDs', default=default_elevationIDs, type=checkElevationIDs, help='ordered list of product IDs (default %s)' % default_elevationIDs)
    parser.add_argument('--landcoverIDs', default=default_landcoverIDs, type=checkLandcoverIDs, help='ordered list of product IDs (default %s)' % default_landcoverIDs)
    parser.add_argument('--enable-ore', action='store_true', dest='doOre', default=False, help='enable ore generation')
    parser.add_argument('--enable-schematics', action='store_true', dest='doSchematics', default=False, help='enable schematic usage')
    parser.add_argument("-v", "--verbosity", action="count", \
                        help="increase output verbosity")
    parser.add_argument("-q", "--quiet", action="store_true", \
                        help="suppress informational output")
    args = parser.parse_args()

    # set up logging
    log_level = klog_levels.LOG_INFO
    if args.quiet:
        log_level = klog_levels.LOG_ERROR
    if args.verbosity:
        # v=1 is DEBUG 1, v=2 is DEBUG 2, and so on
        log_level += args.verbosity
    log = klogger(log_level)

    # create the region
    log.log_info("Creating new region %s..." % args.name)
    myRegion = Region(name=args.name, xmax=args.xmax, xmin=args.xmin, ymax=args.ymax, ymin=args.ymin, scale=args.scale, vscale=args.vscale, trim=args.trim, tilesize=args.tilesize, sealevel=args.sealevel, maxdepth=args.maxdepth, oiIDs=args.orthoIDs, lcIDs=args.landcoverIDs, elIDs=args.elevationIDs, doOre=args.doOre, doSchematics=args.doSchematics)

    log.log_info("Retrieving files...")
    myRegion.log = log
    myRegion.getfiles()
Example #2
0
def main():

    # parse options and get results
    parser = argparse.ArgumentParser(
        description=
        'Converts a single building from a Collada file and pastes into a Minecraft world'
    )
    parser.add_argument('--model', required=True, type=str, \
                           help='relative or absolute path to .kmz file containing Collada model and assets')
    parser.add_argument('--world', required=True, type=str, \
                           help='path to main folder of a target Minecraft world')
    parser.add_argument("-v", "--verbosity", action="count", \
                        help="increase output verbosity")
    parser.add_argument("-q", "--quiet", action="store_true", \
                        help="suppress informational output")
    args = parser.parse_args()

    # set up logging
    log_level = klog_levels.LOG_INFO
    if args.quiet:
        log_level = klog_levels.LOG_ERROR
    if args.verbosity:
        # v=1 is DEBUG 1, v=2 is DEBUG 2, and so on
        log_level += args.verbosity
    log = klogger(log_level)

    # Name of the model that we'll be processing
    filename = args.model
    log.log_info("Converting %s and placing into %s" % \
                 (os.path.basename(filename), os.path.basename(args.world)))

    # Determine where to paste into target world
    zipf = zipfile.ZipFile(args.model, 'r')
    kmldata = minidom.parse(zipf.open('doc.kml'))
    zipf = None

    # Determine location information
    location = kmldata.getElementsByTagName('Location')[0]
    latitude = float(
        location.getElementsByTagName('latitude')[0].childNodes[0].data)
    longitude = float(
        location.getElementsByTagName('longitude')[0].childNodes[0].data)
    altmode = str(
        kmldata.getElementsByTagName('altitudeMode')[0].childNodes[0].data)
    altitude = float(
        location.getElementsByTagName('altitude')[0].childNodes[0].data)

    # Determine orientation information
    orientation = kmldata.getElementsByTagName('Orientation')[0]
    heading = float(
        orientation.getElementsByTagName('heading')[0].childNodes[0].data)
    kmldata = None
    if abs(heading) > 1.0:
        log.log_fatal("Model specifies heading of %f, but this script does" \
                      " not support model rotation" % heading)

    # Get information about the target world
    yamlfile = open(os.path.join(args.world, 'Region.yaml'), 'r')
    yamlfile.readline()  # discard first line
    myRegion = yaml.safe_load(yamlfile)
    yamlfile.close()

    # Check important things
    if myRegion["scale"] != 1 or myRegion["vscale"] != 1:
        log.log_fatal("Currently only scale=1 and vscale=1 are allowed")

    # Compute the world utm (x,y) for this model. Oddly enough, we can use these
    # coordinates directly (for the 1:1 scale case. This script just handles that)
    llextents = myRegion['wgs84extents']['elevation']
    easting, northing, utmzone, utmletter = utmll.from_latlon(
        latitude, longitude)
    northing = (myRegion['tiles']['ymin'] + myRegion['tiles']['ymax']) * myRegion['tilesize'] \
               - northing
    log.log_debug(1, "Base easting = %d, northing = %d in UTM Zone %d%s" % \
                     (easting, northing, utmzone, utmletter))
    modelBaseLoc = [easting, northing, 0]

    log.log_debug(1,"Loc: %.10f,%.10f => %d,%d within %s" % \
                     (latitude, longitude, modelBaseLoc[0], modelBaseLoc[1], str(llextents)))

    # Open the model and determine its extents
    model = collada.Collada(
        filename,
        ignore=[collada.DaeUnsupportedError, collada.DaeBrokenRefError])
    maxs = array([-1e99, -1e99, -1e99])
    mins = array([1e99, 1e99, 1e99])
    mr = ModelRecurse(log)
    mins, maxs = mr.recurse_model(model, "extents", [mins, maxs])
    log.log_info("Computed model extents: [%f, %f, %f,] to [%f, %f, %f]" %
                 (mins[0], mins[1], mins[2], maxs[0], maxs[1], maxs[2]))

    # some sort of scaling information
    scale = [.01, .01, .01]
    if model.assetInfo != None and model.assetInfo.unitmeter != None:
        log.log_debug(1,"This model contains units, %f %s per meter" % \
                            (model.assetInfo.unitmeter, model.assetInfo.unitname))
        scale = model.assetInfo.unitmeter
        scale = [scale, scale, scale]

    t2v = Tri2Voxel(model, log)
    t2v.scale = array(scale)
    t2v.geom_prep(mins, maxs)

    # Use extents and modelBaseLoc to compute the world coordinate that
    # corresponds to the output array's [0,0,0]
    #cornerBase = t2v.tvoffset[0] * t2v.tvscale[0]
    cornerBase = np.multiply(t2v.scale, array([-mins[0], maxs[1], 0]))
    modelBaseLoc -= cornerBase
    modelBaseLoc = [round(x) for x in modelBaseLoc]
    log.log_debug(2,"cornerBase is %s, yielding modelBaseLoc of %s" % \
                  (str(cornerBase), str(modelBaseLoc)))

    # Convert
    mr.recurse_model(model, "convert", t2v)  # Do the conversion!

    # Fix orientation
    t2v.arr3d_id = np.fliplr(t2v.arr3d_id)  # Fix block ID array
    t2v.arr3d_dt = np.fliplr(t2v.arr3d_dt)  # Fix damage val array

    # Print some stats
    ar1 = np.count_nonzero(t2v.arr3d_id)
    ar01 = np.prod(t2v.arrdim)
    log.log_info("%d/%d voxels filled (%.2f%% fill level)" %
                 (ar1, ar01, 100 * ar1 / ar01))
    log.log_info("t2v reports %d voxels changed" % t2v.voxchg)

    # Compute world-scaled altitude information
    # This must be done after the level height is adjusted, otherwise one of the
    # (loaded, cached) chunks will have an incorrect height.
    if altmode == "absolute":
        sealevel = myRegion['sealevel'] if 'sealevel' in myRegion else 64
        modelAltBase = int(altitude * myRegion['vscale'] + sealevel)
    elif altmode == "relativeToGround":
        level = mclevel.fromFile(os.path.join(args.world, "level.dat"))

        xbase = int(round(modelBaseLoc[0] + cornerBase[0]))
        zbase = int(round(modelBaseLoc[1] + cornerBase[1]))
        chunk = level.getChunk(int(xbase / 16.), int(zbase / 16.))
        voxcol = chunk.Blocks[xbase % 16, zbase % 16, :]
        voxtop = [i for i, e in enumerate(voxcol) if e != 0][-1] + 1
        modelAltBase = int(voxtop + modelBaseLoc[2])
        chunk = None
        level.close()
        level = None

    else:
        log.log_fatal("Unknown altitude mode in KML file.")
    log.log_info("Model base altitude is %d meters (voxels)" % modelAltBase)

    # Compute new world height
    worldheight = int(modelAltBase + t2v.arrdim[2])
    worldheight |= worldheight >> 1
    worldheight |= worldheight >> 2
    worldheight |= worldheight >> 4
    worldheight |= worldheight >> 8
    worldheight |= worldheight >> 16
    worldheight += 1

    # Open MC level for computation
    level = mclevel.fromFile(os.path.join(args.world, "level.dat"))

    if worldheight > level.Height:
        log.log_info("World height increased from %d to %d meters" % \
                     (level.Height,worldheight))
        level.Height = worldheight
        level.root_tag["Data"]["worldHeight"] = nbt.TAG_Int(worldheight)
    else:
        log.log_info("World height unmodified at %d meters" % worldheight)

    # Figure out what chunks will be modified
    chunksx = [int(np.floor(modelBaseLoc[0]/16.)), \
               int(np.floor((modelBaseLoc[0]+t2v.arrdim[0])/16.))]
    chunksz = [int(np.floor(modelBaseLoc[1]/16.)), \
               int(np.floor((modelBaseLoc[1]+t2v.arrdim[1])/16.))]

    # Modify the chunks with new building data
    for x in xrange(chunksx[0], 1 + chunksx[1]):
        for z in xrange(chunksz[0], 1 + chunksz[1]):

            # Chunk sub-selection
            chunk = level.getChunk(x, z)
            xmin = max(0, modelBaseLoc[0] - 16 * x)
            xmax = min(16, t2v.arrdim[0] + modelBaseLoc[0] - 16 * x)
            zmin = max(0, modelBaseLoc[1] - 16 * z)
            zmax = min(16, t2v.arrdim[1] + modelBaseLoc[1] - 16 * z)

            # Model sub-selection
            mxmin = (16 * x) + xmin - modelBaseLoc[0]
            mzmin = (16 * z) + zmin - modelBaseLoc[1]

            log.log_debug(2,"Copying region %d,%d,%d to %d,%d,%d" % \
                  (xmin,modelAltBase,zmin,xmax,(modelAltBase+t2v.arrdim[2]),zmax))
            log.log_debug(2,"From model %d,%d,%d to %d,%d,%d" % \
                  (mxmin,0,mzmin,mxmin+(xmax-xmin),t2v.arrdim[2],mzmin+(zmax-zmin)))
            if xmax <= 0 or zmax <= 0:
                log.log_debug(1, "Skipping out-of-bounds copy")
                continue

            # Checking to make sure numpy isn't going to pitch a fit
            shapes = [
                t2v.arrdim[2],
                chunk.Data[xmin, zmin, modelAltBase:(modelAltBase +
                                                     t2v.arrdim[2])].shape[0]
            ]
            if shapes[0] != shapes[1]:
                log.log_fatal("Cannot store resulting model. Chunk (%d,%d) selected height %d does not match " \
                                          "model matrix height %d" % (x, z, shapes[0], shapes[1]))

            inp = chunk.Blocks[xmin:xmax,zmin:zmax, \
                               modelAltBase:(modelAltBase+t2v.arrdim[2])]

            # Data first because Blocks must retain its 0s
            ind = chunk.Data[xmin:xmax,zmin:zmax, \
                             modelAltBase:(modelAltBase+t2v.arrdim[2])]
            chunk.Data[xmin:xmax,zmin:zmax, \
                       modelAltBase:(modelAltBase+t2v.arrdim[2])] = \
            np.where(inp != 0, ind, \
                     t2v.arr3d_dt[mxmin:mxmin + (xmax-xmin), mzmin:mzmin + (zmax-zmin), :])

            # Blocks second.
            chunk.Blocks[xmin:xmax,zmin:zmax, \
                         modelAltBase:(modelAltBase+t2v.arrdim[2])] = \
            np.where(inp != 0, inp, \
                     t2v.arr3d_id[mxmin:mxmin + (xmax-xmin), mzmin:mzmin + (zmax-zmin), :])

            # And mark the chunk.
            chunk.chunkChanged()

    log.log_info("Relighting level...")
    level.generateLights()
    log.log_info("Saving level...")
    level.saveInPlace()
Example #3
0
def main():

    # parse options and get results
    parser = argparse.ArgumentParser(
        description="Converts a single building from a Collada file and pastes into a Minecraft world"
    )
    parser.add_argument(
        "--model",
        required=True,
        type=str,
        help="relative or absolute path to .kmz file containing Collada model and assets",
    )
    parser.add_argument("--world", required=True, type=str, help="path to main folder of a target Minecraft world")
    parser.add_argument("-v", "--verbosity", action="count", help="increase output verbosity")
    parser.add_argument("-q", "--quiet", action="store_true", help="suppress informational output")
    args = parser.parse_args()

    # set up logging
    log_level = klog_levels.LOG_INFO
    if args.quiet:
        log_level = klog_levels.LOG_ERROR
    if args.verbosity:
        # v=1 is DEBUG 1, v=2 is DEBUG 2, and so on
        log_level += args.verbosity
    log = klogger(log_level)

    # Name of the model that we'll be processing
    filename = args.model
    log.log_info("Converting %s and placing into %s" % (os.path.basename(filename), os.path.basename(args.world)))

    # Determine where to paste into target world
    zipf = zipfile.ZipFile(args.model, "r")
    kmldata = minidom.parse(zipf.open("doc.kml"))
    zipf = None

    # Determine location information
    location = kmldata.getElementsByTagName("Location")[0]
    latitude = float(location.getElementsByTagName("latitude")[0].childNodes[0].data)
    longitude = float(location.getElementsByTagName("longitude")[0].childNodes[0].data)
    altmode = str(kmldata.getElementsByTagName("altitudeMode")[0].childNodes[0].data)
    altitude = float(location.getElementsByTagName("altitude")[0].childNodes[0].data)

    # Determine orientation information
    orientation = kmldata.getElementsByTagName("Orientation")[0]
    heading = float(orientation.getElementsByTagName("heading")[0].childNodes[0].data)
    kmldata = None
    if abs(heading) > 1.0:
        log.log_fatal("Model specifies heading of %f, but this script does" " not support model rotation" % heading)

        # Get information about the target world
    yamlfile = open(os.path.join(args.world, "Region.yaml"), "r")
    yamlfile.readline()  # discard first line
    myRegion = yaml.safe_load(yamlfile)
    yamlfile.close()

    # Check important things
    if myRegion["scale"] != 1 or myRegion["vscale"] != 1:
        log.log_fatal("Currently only scale=1 and vscale=1 are allowed")

        # Compute the world utm (x,y) for this model. Oddly enough, we can use these
        # coordinates directly (for the 1:1 scale case. This script just handles that)
    llextents = myRegion["wgs84extents"]["elevation"]
    easting, northing, utmzone, utmletter = utmll.from_latlon(latitude, longitude)
    northing = (myRegion["tiles"]["ymin"] + myRegion["tiles"]["ymax"]) * myRegion["tilesize"] - northing
    log.log_debug(1, "Base easting = %d, northing = %d in UTM Zone %d%s" % (easting, northing, utmzone, utmletter))
    modelBaseLoc = [easting, northing, 0]

    log.log_debug(
        1,
        "Loc: %.10f,%.10f => %d,%d within %s" % (latitude, longitude, modelBaseLoc[0], modelBaseLoc[1], str(llextents)),
    )

    # Open the model and determine its extents
    model = collada.Collada(filename, ignore=[collada.DaeUnsupportedError, collada.DaeBrokenRefError])
    maxs = array([-1e99, -1e99, -1e99])
    mins = array([1e99, 1e99, 1e99])
    mr = ModelRecurse(log)
    mins, maxs = mr.recurse_model(model, "extents", [mins, maxs])
    log.log_info(
        "Computed model extents: [%f, %f, %f,] to [%f, %f, %f]" % (mins[0], mins[1], mins[2], maxs[0], maxs[1], maxs[2])
    )

    # some sort of scaling information
    scale = [0.01, 0.01, 0.01]
    if model.assetInfo != None and model.assetInfo.unitmeter != None:
        log.log_debug(
            1, "This model contains units, %f %s per meter" % (model.assetInfo.unitmeter, model.assetInfo.unitname)
        )
        scale = model.assetInfo.unitmeter
        scale = [scale, scale, scale]

    t2v = Tri2Voxel(model, log)
    t2v.scale = array(scale)
    t2v.geom_prep(mins, maxs)

    # Use extents and modelBaseLoc to compute the world coordinate that
    # corresponds to the output array's [0,0,0]
    # cornerBase = t2v.tvoffset[0] * t2v.tvscale[0]
    cornerBase = np.multiply(t2v.scale, array([-mins[0], maxs[1], 0]))
    modelBaseLoc -= cornerBase
    modelBaseLoc = [round(x) for x in modelBaseLoc]
    log.log_debug(2, "cornerBase is %s, yielding modelBaseLoc of %s" % (str(cornerBase), str(modelBaseLoc)))

    # Convert
    mr.recurse_model(model, "convert", t2v)  # Do the conversion!

    # Fix orientation
    t2v.arr3d_id = np.fliplr(t2v.arr3d_id)  # Fix block ID array
    t2v.arr3d_dt = np.fliplr(t2v.arr3d_dt)  # Fix damage val array

    # Print some stats
    ar1 = np.count_nonzero(t2v.arr3d_id)
    ar01 = np.prod(t2v.arrdim)
    log.log_info("%d/%d voxels filled (%.2f%% fill level)" % (ar1, ar01, 100 * ar1 / ar01))
    log.log_info("t2v reports %d voxels changed" % t2v.voxchg)

    # Compute world-scaled altitude information
    # This must be done after the level height is adjusted, otherwise one of the
    # (loaded, cached) chunks will have an incorrect height.
    if altmode == "absolute":
        sealevel = myRegion["sealevel"] if "sealevel" in myRegion else 64
        modelAltBase = int(altitude * myRegion["vscale"] + sealevel)
    elif altmode == "relativeToGround":
        level = mclevel.fromFile(os.path.join(args.world, "level.dat"))

        xbase = int(round(modelBaseLoc[0] + cornerBase[0]))
        zbase = int(round(modelBaseLoc[1] + cornerBase[1]))
        chunk = level.getChunk(int(xbase / 16.0), int(zbase / 16.0))
        voxcol = chunk.Blocks[xbase % 16, zbase % 16, :]
        voxtop = [i for i, e in enumerate(voxcol) if e != 0][-1] + 1
        modelAltBase = int(voxtop + modelBaseLoc[2])
        chunk = None
        level.close()
        level = None

    else:
        log.log_fatal("Unknown altitude mode in KML file.")
    log.log_info("Model base altitude is %d meters (voxels)" % modelAltBase)

    # Compute new world height
    worldheight = int(modelAltBase + t2v.arrdim[2])
    worldheight |= worldheight >> 1
    worldheight |= worldheight >> 2
    worldheight |= worldheight >> 4
    worldheight |= worldheight >> 8
    worldheight |= worldheight >> 16
    worldheight += 1

    # Open MC level for computation
    level = mclevel.fromFile(os.path.join(args.world, "level.dat"))

    if worldheight > level.Height:
        log.log_info("World height increased from %d to %d meters" % (level.Height, worldheight))
        level.Height = worldheight
        level.root_tag["Data"]["worldHeight"] = nbt.TAG_Int(worldheight)
    else:
        log.log_info("World height unmodified at %d meters" % worldheight)

        # Figure out what chunks will be modified
    chunksx = [int(np.floor(modelBaseLoc[0] / 16.0)), int(np.floor((modelBaseLoc[0] + t2v.arrdim[0]) / 16.0))]
    chunksz = [int(np.floor(modelBaseLoc[1] / 16.0)), int(np.floor((modelBaseLoc[1] + t2v.arrdim[1]) / 16.0))]

    # Modify the chunks with new building data
    for x in xrange(chunksx[0], 1 + chunksx[1]):
        for z in xrange(chunksz[0], 1 + chunksz[1]):

            # Chunk sub-selection
            chunk = level.getChunk(x, z)
            xmin = max(0, modelBaseLoc[0] - 16 * x)
            xmax = min(16, t2v.arrdim[0] + modelBaseLoc[0] - 16 * x)
            zmin = max(0, modelBaseLoc[1] - 16 * z)
            zmax = min(16, t2v.arrdim[1] + modelBaseLoc[1] - 16 * z)

            # Model sub-selection
            mxmin = (16 * x) + xmin - modelBaseLoc[0]
            mzmin = (16 * z) + zmin - modelBaseLoc[1]

            log.log_debug(
                2,
                "Copying region %d,%d,%d to %d,%d,%d"
                % (xmin, modelAltBase, zmin, xmax, (modelAltBase + t2v.arrdim[2]), zmax),
            )
            log.log_debug(
                2,
                "From model %d,%d,%d to %d,%d,%d"
                % (mxmin, 0, mzmin, mxmin + (xmax - xmin), t2v.arrdim[2], mzmin + (zmax - zmin)),
            )
            if xmax <= 0 or zmax <= 0:
                log.log_debug(1, "Skipping out-of-bounds copy")
                continue

                # Checking to make sure numpy isn't going to pitch a fit
            shapes = [t2v.arrdim[2], chunk.Data[xmin, zmin, modelAltBase : (modelAltBase + t2v.arrdim[2])].shape[0]]
            if shapes[0] != shapes[1]:
                log.log_fatal(
                    "Cannot store resulting model. Chunk (%d,%d) selected height %d does not match "
                    "model matrix height %d" % (x, z, shapes[0], shapes[1])
                )

            inp = chunk.Blocks[xmin:xmax, zmin:zmax, modelAltBase : (modelAltBase + t2v.arrdim[2])]

            # Data first because Blocks must retain its 0s
            ind = chunk.Data[xmin:xmax, zmin:zmax, modelAltBase : (modelAltBase + t2v.arrdim[2])]
            chunk.Data[xmin:xmax, zmin:zmax, modelAltBase : (modelAltBase + t2v.arrdim[2])] = np.where(
                inp != 0, ind, t2v.arr3d_dt[mxmin : mxmin + (xmax - xmin), mzmin : mzmin + (zmax - zmin), :]
            )

            # Blocks second.
            chunk.Blocks[xmin:xmax, zmin:zmax, modelAltBase : (modelAltBase + t2v.arrdim[2])] = np.where(
                inp != 0, inp, t2v.arr3d_id[mxmin : mxmin + (xmax - xmin), mzmin : mzmin + (zmax - zmin), :]
            )

            # And mark the chunk.
            chunk.chunkChanged()

    log.log_info("Relighting level...")
    level.generateLights()
    log.log_info("Saving level...")
    level.saveInPlace()
Example #4
0
def main():
    """Builds a region."""
    # example:
    # ./BuildRegion.py --name BlockIsland

    # parse options and get results
    parser = argparse.ArgumentParser(description='Builds Minecraft worlds from regions.')
    parser.add_argument('--name', required=True, type=str, \
                        help='name of the region to be built')
    parser.add_argument('--single', action='store_true', \
                        help='enable single-threaded mode for debugging or profiling')
    parser.add_argument('--safemerge', action='store_true', \
                        help='use \"safer\" method of merging tiles together')
    parser.add_argument("-v", "--verbosity", action="count", \
                        help="increase output verbosity")
    parser.add_argument("-q", "--quiet", action="store_true", \
                        help="suppress informational output")
    args = parser.parse_args()

    # set up logging
    log_level = klog_levels.LOG_INFO
    if args.quiet:
        log_level = klog_levels.LOG_ERROR
    if args.verbosity:
        # v=1 is DEBUG 1, v=2 is DEBUG 2, and so on
        log_level += args.verbosity
    log = klogger(log_level)

    # build the region
    log.log_info("Building region %s..." % args.name)
    yamlfile = file(os.path.join('Regions', args.name, 'Region.yaml'))
    myRegion = yaml.load(yamlfile)
    yamlfile.close()

    # exit if map does not exist
    if not os.path.exists(myRegion.mapname):
        log.log_fatal("No map file exists!")

    # tree and ore variables
    treeobjs = dict([(tree.name, tree) for tree in treeObjs])
    trees = dict([(name, list()) for name in treeobjs])
    oreobjs = dict([(ore.name, ore) for ore in oreObjs])
    ores = dict([(name, list()) for name in oreobjs])

    # generate overall world
    worlddir = os.path.join('Worlds', args.name)
    world = mclevel.MCInfdevOldLevel(worlddir, create=True)
    peak = [0, 0, 0]
    save(world)
    world = None

    # generate individual tiles
    tilexrange = xrange(myRegion.tiles['xmin'], myRegion.tiles['xmax'])
    tileyrange = xrange(myRegion.tiles['ymin'], myRegion.tiles['ymax'])
    name = myRegion.name
    tiles = [(log, name, x, y) for x, y in product(tilexrange, tileyrange)]
    if args.single:
        # single process version
        log.log_warn("Single-threaded region merge")
        for tile in tiles:
            buildtile(tile)
    else:
        # multi-process version
        pool = Pool()
        rs = pool.map_async(buildtile, tiles)
        pool.close()
        while not(rs.ready()):
            remaining = rs._number_left
            log.log_info("Waiting for %s buildtile tasks to complete..." %
                         remaining)
            time.sleep(10)
        pool.join()        # Just as a precaution.

    # Necessary for tile-welding -> regions
    cleanmkdir(worlddir)
    cleanmkdir(os.path.join(worlddir, 'region'))

    # Generate regions
    if not(args.safemerge):
        regionsize = 32 * 16
        regionxrange = xrange(int(floor(myRegion.tiles['xmin'] * (myRegion.tilesize / float(regionsize)))), \
                              int(ceil(myRegion.tiles['xmax'] * (myRegion.tilesize / float(regionsize)))))
        regionyrange = xrange(int(floor(myRegion.tiles['ymin'] * (myRegion.tilesize / float(regionsize)))), \
                              int(ceil(myRegion.tiles['ymax'] * (myRegion.tilesize / float(regionsize)))))
    
        regions = [(log, name, x, y) for x, y in product(regionxrange, regionyrange)]
    
        # merge individual tiles into regions
        log.log_info("Merging %d tiles into one world..." % len(tiles))
        for tile in tiles:
            (dummy, name, x, y) = tile
            tiledir = os.path.join('Regions', name, 'Tiles', '%dx%d' % (x, y))
            if not(os.path.isfile(os.path.join(tiledir, 'Tile.yaml'))):
                log.log_fatal("The following tile is missing. Please re-run this script:\n%s" % \
                      os.path.join(tiledir, 'Tile.yaml'))

        if args.single:
            # single process version
            log.log_warn("Single-threaded region merge")
            for region in regions:
                buildregion(region)
        else:
            # multi-process version
            pool = Pool()
            rs = pool.map_async(buildregion, regions)
            pool.close()
            while not(rs.ready()):
                remaining = rs._number_left
                log.log_info("Waiting for %s buildregion tasks to complete..." %
                             remaining)
                time.sleep(10)
            pool.join()        # Just as a precaution.

    world = mclevel.MCInfdevOldLevel(worlddir, create=True)
    if not(args.safemerge):
        mcoffsetx = myRegion.tiles['xmin'] * myRegion.tilesize
        mcoffsetz = myRegion.tiles['ymin'] * myRegion.tilesize
        mcsizex   = (myRegion.tiles['xmax'] - myRegion.tiles['xmin']) * myRegion.tilesize
        mcsizez   = (myRegion.tiles['ymax'] - myRegion.tiles['ymin']) * myRegion.tilesize
        tilebox = box.BoundingBox((mcoffsetx, 0, mcoffsetz), (mcsizex, world.Height, mcsizez))
        world.createChunksInBox(tilebox)

    for tile in tiles:
        (dummy, name, x, y) = tile
        tiledir = os.path.join('Regions', name, 'Tiles', '%dx%d' % (x, y))
        tilefile = file(os.path.join(tiledir, 'Tile.yaml'))
        newtile = yaml.load(tilefile)
        tilefile.close()
        if (newtile.peak[1] > peak[1]):
            peak = newtile.peak
        for treetype in newtile.trees:
            trees.setdefault(treetype, []).extend(newtile.trees[treetype])
        if myRegion.doOre:
            for oretype in newtile.ores:
                ores.setdefault(oretype, []).extend(newtile.ores[oretype])
        if args.safemerge:
            tileworld = mclevel.MCInfdevOldLevel(tiledir, create=False)
            world.copyBlocksFrom(tileworld, tileworld.bounds, tileworld.bounds.origin)
            tileworld = False

    # plant trees in our world
    log.log_info("Planting %d trees at the region level..." % \
                 sum([len(trees[treetype]) for treetype in trees]))
    Tree.placetreesinregion(trees, treeobjs, world)

    # deposit ores in our world
    if myRegion.doOre:
        log.log_info("Depositing %d ores at the region level..." % \
                     sum([len(ores[oretype]) for oretype in ores]))
        Ore.placeoreinregion(ores, oreobjs, world)

    # tie up loose ends
    world.setPlayerGameType(1)
    setspawnandsave(world, peak)
    oldyamlpath = os.path.join('Regions', args.name, 'Region.yaml')
    newyamlpath = os.path.join('Worlds', args.name, 'Region.yaml')
    shutil.copy(oldyamlpath, newyamlpath)
    shutil.rmtree(os.path.join('Regions', name, 'Tiles'))