0

Merge branch 'signs' into devel

This commit is contained in:
Andrew Chin
2012-03-11 19:10:28 -04:00
14 changed files with 507 additions and 42 deletions

View File

@@ -443,6 +443,21 @@ values. The valid configuration keys are listed below.
removed some tiles, you may need to do some manual deletion on the
remote side.
.. _option_markers:
``markers``
This controls the display of markers, signs, and other points of interest
in the output HTML. It should be a list of filter functions.
.. note::
Setting this configuration option alone does nothing. In order to get
markers and signs on our map, you must also run the genPO script. See
the :doc:`Signs and markers<signs>` section for more details and documenation.
**Default:** ``[]`` (an empty list)
.. _customrendermodes:
Custom Rendermodes and Rendermode Primitives

View File

@@ -181,6 +181,7 @@ Documentation Contents
building
running
config
signs
win_tut/windowsguide
faq
design/designdoc

75
docs/signs.rst Normal file
View File

@@ -0,0 +1,75 @@
.. _signsmarkers:
=================
Signs and Markers
=================
The Overviewer can display signs, markers, and other points of interest on your
map. This works a little differently than it has in the past, so be sure to read
these docs carefully.
In these docs, we use the term POI (or point of interest) to refer to entities and
tileentities.
Configuration File
==================
Filter Functions
----------------
A filter function is a python function that is used to figure out if a given POI
should be part of a markerSet of not. The function should accept one argument
(a dictionary, also know as an associative array), and return a boolean::
def signFilter(poi):
"All signs"
return poi['id'] == 'Sign'
The single argument will either a TileEntity, or an Entity taken directly from
the chunk file. In this example, this function returns true only if the type
of entity is a sign. For more information of TileEntities and Entities, see
the `Chunk Format <http://www.minecraftwiki.net/wiki/Chunk_format>`_ page on
the Minecraft Wiki.
.. note::
The doc string ("All signs" in this example) is important. It is the label
that appears in your rendered map
A more advanced filter may also look at other entity fields, such as the sign text::
def goldFilter(poi):
"Gold"
return poi['id'] == 'Sign' and (\
'gold' in poi['Text1] or
'gold' in poi['Text2'])
This looks for the word 'gold' in either the first or second line of the signtext.
Since writing these filters can be a little tedious, a set of predefined filters
functions are provided. See the :ref:`predefined_filter_functions` section for
details.
Render Dictionary Key
---------------------
Each render can specify a list of zero or more filter functions. Each of these
filter functions become a selectable item in the 'Signs' drop-down menu in the
rendered map. For example::
renders['myrender'] = {
'world': 'myworld',
'title': "Example",
'markers': [allFilter, anotherFilter],
}
.. _predefined_filter_functions:
Predefined Filter Functions
===========================
TODO write some filter functions, then document them here

132
genPOI.py Executable file
View File

@@ -0,0 +1,132 @@
#!/usr/bin/python2
'''
genPOI.py
Scans regionsets for TileEntities and Entities, filters them, and writes out
POI/marker info.
A markerSet is list of POIs to display on a tileset. It has a display name,
and a group name.
markersDB.js holds a list of POIs in each group
markers.js holds a list of which markerSets are attached to each tileSet
'''
import os
import logging
import json
from optparse import OptionParser
from overviewer_core import logger
from overviewer_core import nbt
from overviewer_core import configParser, world
helptext = """
%prog --config=<config file>"""
logger.configure()
def handleSigns(rset, outputdir, render, rname):
if hasattr(rset, "_pois"):
return
logging.info("Looking for entities in %r", rset)
filters = render['markers']
rset._pois = dict(TileEntities=[], Entities=[])
for (x,z,mtime) in rset.iterate_chunks():
data = rset.get_chunk(x,z)
rset._pois['TileEntities'] += data['TileEntities']
rset._pois['Entities'] += data['Entities']
def main():
parser = OptionParser(usage=helptext)
parser.add_option("--config", dest="config", action="store", help="Specify the config file to use.")
options, args = parser.parse_args()
if not options.config:
parser.print_help()
return
# Parse the config file
mw_parser = configParser.MultiWorldParser()
mw_parser.parse(options.config)
try:
config = mw_parser.get_validated_config()
except Exception:
logging.exception("An error was encountered with your configuration. See the info below.")
return 1
destdir = config['outputdir']
# saves us from creating the same World object over and over again
worldcache = {}
markersets = set()
markers = dict()
for rname, render in config['renders'].iteritems():
try:
worldpath = config['worlds'][render['world']]
except KeyError:
logging.error("Render %s's world is '%s', but I could not find a corresponding entry in the worlds dictionary.",
rname, render['world'])
return 1
render['worldname_orig'] = render['world']
render['world'] = worldpath
# find or create the world object
if (render['world'] not in worldcache):
w = world.World(render['world'])
worldcache[render['world']] = w
else:
w = worldcache[render['world']]
rset = w.get_regionset(render['dimension'])
if rset == None: # indicates no such dimension was found:
logging.error("Sorry, you requested dimension '%s' for %s, but I couldn't find it", render['dimension'], render_name)
return 1
for f in render['markers']:
markersets.add((f, rset))
name = f.__name__ + hex(hash(f))[-4:] + "_" + hex(hash(rset))[-4:]
try:
l = markers[rname]
l.append(dict(groupName=name, displayName = f.__doc__))
except KeyError:
markers[rname] = [dict(groupName=name, displayName=f.__doc__),]
handleSigns(rset, os.path.join(destdir, rname), render, rname)
logging.info("Done scanning regions")
logging.info("Writing out javascript files")
markerSetDict = dict()
for (flter, rset) in markersets:
# generate a unique name for this markerset. it will not be user visible
name = flter.__name__ + hex(hash(flter))[-4:] + "_" + hex(hash(rset))[-4:]
markerSetDict[name] = dict(created=False, raw=[])
for poi in rset._pois['TileEntities']:
if flter(poi):
markerSetDict[name]['raw'].append(poi)
#print markerSetDict
with open(os.path.join(destdir, "markersDB.js"), "w") as output:
output.write("var markersDB=")
json.dump(markerSetDict, output, indent=2)
output.write(";\n");
with open(os.path.join(destdir, "markers.js"), "w") as output:
output.write("var markers=")
json.dump(markers, output, indent=2)
output.write(";\n");
with open(os.path.join(destdir, "baseMarkers.js"), "w") as output:
output.write("overviewer.util.injectMarkerScript('markersDB.js');\n")
output.write("overviewer.util.injectMarkerScript('markers.js');\n")
output.write("overviewer.collections.haveSigns=true;\n")
logging.info("Done")
if __name__ == "__main__":
main()

View File

@@ -406,6 +406,7 @@ dir but you forgot to put quotes around the directory, since it contains spaces.
# only pass to the TileSet the options it really cares about
render['name'] = render_name # perhaps a hack. This is stored here for the asset manager
tileSetOpts = util.dict_subset(render, ["name", "imgformat", "renderchecks", "rerenderprob", "bgcolor", "imgquality", "optimizeimg", "rendermode", "worldname_orig", "title", "dimension", "changelist"])
tileSetOpts.update({"spawn": w.find_true_spawn()}) # TODO find a better way to do this
tset = tileset.TileSet(rset, assetMrg, tex, tileSetOpts, tileset_dir)
tilesets.append(tset)

View File

@@ -42,11 +42,6 @@ directory.
self.outputdir = outputdir
self.renders = dict()
# stores Points Of Interest to be mapped with markers
# This is a dictionary of lists of dictionaries
# Each regionset's name is a key in this dictionary
self.POI = dict()
# look for overviewerConfig in self.outputdir
try:
with open(os.path.join(self.outputdir, "overviewerConfig.js")) as c:
@@ -65,13 +60,6 @@ directory.
return dict()
def found_poi(self, regionset, poi_type, contents, chunkX, chunkY):
if regionset.name not in self.POI.keys():
POI[regionset.name] = []
# TODO based on the type, so something
POI[regionset.name].append
def initialize(self, tilesets):
"""Similar to finalize() but calls the tilesets' get_initial_data()
instead of get_persistent_data() to compile the generated javascript
@@ -152,6 +140,12 @@ directory.
global_assets = os.path.join(util.get_program_path(), "web_assets")
mirror_dir(global_assets, self.outputdir)
# write a dummy baseMarkers.js if none exists
if not os.path.exists(os.path.join(self.outputdir, "baseMarkers.js")):
with open(os.path.join(self.outputdir, "baseMarkers.js"), "w") as f:
f.write("// if you wants signs, please see genPOI.py\n");
# create overviewer.js from the source js files
js_src = os.path.join(util.get_program_path(), "overviewer_core", "data", "js_src")
if not os.path.isdir(js_src):

View File

@@ -29,7 +29,19 @@ overviewer.collections = {
*/
'infoWindow': null,
'worldViews': []
'worldViews': [],
'haveSigns': false,
/**
* Hold the raw marker data for each tilest
*/
'markerInfo': {},
/**
* holds a reference to the spawn marker.
*/
'spawnMarker': null,
};
overviewer.classes = {

View File

@@ -58,6 +58,13 @@ overviewer.util = {
var coordsdiv = new overviewer.views.CoordboxView({tagName: 'DIV'});
coordsdiv.render();
if (overviewer.collections.haveSigns) {
var signs = new overviewer.views.SignControlView();
signs.registerEvents(signs);
}
var spawnmarker = new overviewer.views.SpawnIconView();
// Update coords on mousemove
google.maps.event.addListener(overviewer.map, 'mousemove', function (event) {
coordsdiv.updateCoords(event.latLng);
@@ -72,6 +79,7 @@ overviewer.util = {
overviewer.mapView.updateCurrentTileset();
compass.render();
spawnmarker.render();
// re-center on the last viewport
var currentWorldView = overviewer.mapModel.get("currentWorldView");
@@ -102,6 +110,7 @@ overviewer.util = {
overviewer.map.setZoom(zoom);
}
});
var worldSelector = new overviewer.views.WorldSelectorView({tagName:'DIV'});
@@ -116,15 +125,43 @@ overviewer.util = {
// Jump to the hash if given
overviewer.util.initHash();
overviewer.util.initializeMarkers();
/*
overviewer.util.initializeMapTypes();
overviewer.util.initializeMap();
overviewer.util.initializeMarkers();
overviewer.util.initializeRegions();
overviewer.util.createMapControls();
*/
},
'injectMarkerScript': function(url) {
var m = document.createElement('script'); m.type = 'text/javascript'; m.async = false;
m.src = url;
var s = document.getElementsByTagName('script')[0]; s.parentNode.appendChild(m);
},
'initializeMarkers': function() {
return;
},
'createMarkerInfoWindow': function(marker) {
var windowContent = '<div class="infoWindow"><img src="' + marker.icon +
'"/><p>' + marker.title.replace(/\n/g,'<br/>') + '</p></div>';
var infowindow = new google.maps.InfoWindow({
'content': windowContent
});
google.maps.event.addListener(marker, 'click', function() {
if (overviewer.collections.infoWindow) {
overviewer.collections.infoWindow.close();
}
infowindow.open(overviewer.map, marker);
overviewer.collections.infoWindow = infowindow;
});
},
/**
* This adds some methods to these classes because Javascript is stupid
* and this seems like the best way to avoid re-creating the same methods
@@ -239,7 +276,6 @@ overviewer.util = {
var zoomLevels = model.get("zoomLevels");
var north_direction = model.get('north_direction');
//console.log("fromWorldToLatLng: north_direction is %r", north_direction);
// the width and height of all the highest-zoom tiles combined,
// inverted
@@ -406,7 +442,6 @@ overviewer.util = {
// save this info is a nice easy to parse format
var currentWorldView = overviewer.mapModel.get("currentWorldView");
currentWorldView.options.lastViewport = [x,y,z,zoom];
//console.log("Updated lastViewport: %r" , [x,y,z,zoom]);
window.location.replace("#/" + Math.floor(x) + "/" + Math.floor(y) + "/" + Math.floor(z) + "/" + zoom + "/" + w + "/" + maptype);
},
'updateHash': function() {

View File

@@ -3,13 +3,9 @@ overviewer.views= {}
overviewer.views.WorldView = Backbone.View.extend({
initialize: function(opts) {
//console.log("WorldView::initialize()");
//console.log(this.model.get("tileSets"));
this.options.mapTypes = [];
this.options.mapTypeIds = [];
this.model.get("tileSets").each(function(tset, index, list) {
//console.log(" eaching");
//console.log(" Working on tileset %s" , tset.get("name"));
var ops = {
getTileUrl: overviewer.gmap.getTileUrlGenerator(tset.get("path"), tset.get("base"), tset.get("imgextension")),
'tileSize': new google.maps.Size(
@@ -57,7 +53,6 @@ overviewer.views.WorldSelectorView = Backbone.View.extend({
"change select": "changeWorld"
},
changeWorld: function() {
//console.log("change world!");
var selectObj = this.$("select")[0];
var selectedOption = selectObj.options[selectObj.selectedIndex];
@@ -120,11 +115,8 @@ overviewer.views.CoordboxView = Backbone.View.extend({
overviewer.views.GoogleMapView = Backbone.View.extend({
initialize: function(opts) {
//console.log(this);
this.options.map = null;
var curWorld = this.model.get("currentWorldView").model;
//console.log("Current world:");
//console.log(curWorld);
var curTset = curWorld.get("tileSets").at(0);
@@ -145,12 +137,6 @@ overviewer.views.GoogleMapView = Backbone.View.extend({
var mapOptions = {};
//
curWorld.get("tileSets").each(function(elem, index, list) {
//console.log("Setting up map for:");
//console.log(elem);
//console.log("for %s generating url func with %s and %s", elem.get("name"), elem.get("path"), elem.get("base"));
});
// init the map with some default options. use the first tileset in the first world
this.options.mapOptions = {
zoom: curTset.get("defaultZoom"),
@@ -174,7 +160,6 @@ overviewer.views.GoogleMapView = Backbone.View.extend({
// register every ImageMapType with the map
$.each(overviewer.collections.worldViews, function( index, worldView) {
$.each(worldView.options.mapTypes, function(i_index, maptype) {
//console.log("registered %s with the maptype registery", worldView.model.get("name") + maptype.shortname);
overviewer.map.mapTypes.set(overviewerConfig.CONST.mapDivId +
worldView.model.get("name") + maptype.shortname , maptype);
});
@@ -185,7 +170,6 @@ overviewer.views.GoogleMapView = Backbone.View.extend({
* Should be called when the current world has changed in GoogleMapModel
*/
render: function() {
//console.log("GoogleMapView::render()");
var view = this.model.get("currentWorldView");
this.options.mapOptions.mapTypeControlOptions = {
mapTypeIds: view.options.mapTypeIds};
@@ -200,14 +184,11 @@ overviewer.views.GoogleMapView = Backbone.View.extend({
* Keeps track of the currently visible tileset
*/
updateCurrentTileset: function() {
//console.log("GoogleMapView::updateCurrentTileset()");
var currentWorldView = this.model.get("currentWorldView");
var gmapCurrent = overviewer.map.getMapTypeId();
for (id in currentWorldView.options.mapTypeIds) {
if (currentWorldView.options.mapTypeIds[id] == gmapCurrent) {
//console.log("updating currenttileset");
this.options.currentTileSet = currentWorldView.model.get("tileSets").at(id);
//console.log(this);
}
}
@@ -219,3 +200,188 @@ overviewer.views.GoogleMapView = Backbone.View.extend({
});
/**
* SignControlView
*/
overviewer.views.SignControlView = Backbone.View.extend({
/** SignControlView::initialize
*/
initialize: function(opts) {
$(this.el).addClass("customControl");
overviewer.map.controls[google.maps.ControlPosition.TOP_RIGHT].push(this.el);
},
registerEvents: function(me) {
google.maps.event.addListener(overviewer.map, 'maptypeid_changed', function(event) {
overviewer.mapView.updateCurrentTileset();
// workaround IE issue. bah!
if (typeof markers=="undefined") { return; }
me.render();
// hide markers, if necessary
// for each markerSet, check:
// if the markerSet isnot part of this tileset, hide all of the markers
var curMarkerSet = overviewer.mapView.options.currentTileSet.attributes.path;
var dataRoot = markers[curMarkerSet];
if (!dataRoot) {
// this tileset has no signs, so hide all of them
for (markerSet in markersDB) {
if (markersDB[markerSet].created) {
jQuery.each(markersDB[markerSet].raw, function(i, elem) {
elem.markerObj.setVisible(false);
});
}
}
return;
}
var groupsForThisTileSet = jQuery.map(dataRoot, function(elem, i) { return elem.groupName;})
for (markerSet in markersDB) {
if (jQuery.inArray(markerSet, groupsForThisTileSet) == -1){
// hide these
if (markersDB[markerSet].created) {
jQuery.each(markersDB[markerSet].raw, function(i, elem) {
elem.markerObj.setVisible(false);
});
}
markersDB[markerSet].checked=false;
}
// make sure the checkboxes checked if necessary
$("[_mc_groupname=" + markerSet + "]").attr("checked", markersDB[markerSet].checked);
}
});
},
/**
* SignControlView::render
*/
render: function() {
var curMarkerSet = overviewer.mapView.options.currentTileSet.attributes.path;
//var dataRoot = overviewer.collections.markerInfo[curMarkerSet];
var dataRoot = markers[curMarkerSet];
this.el.innerHTML=""
// if we have no markerSets for this tileset, do nothing:
if (!dataRoot) { return; }
var controlText = document.createElement('DIV');
controlText.innerHTML = "Signs";
var controlBorder = document.createElement('DIV');
$(controlBorder).addClass('top');
this.el.appendChild(controlBorder);
controlBorder.appendChild(controlText);
var dropdownDiv = document.createElement('DIV');
$(dropdownDiv).addClass('dropDown');
this.el.appendChild(dropdownDiv);
dropdownDiv.innerHTML='';
// add the functionality to toggle visibility of the items
$(controlText).click(function() {
$(controlBorder).toggleClass('top-active');
$(dropdownDiv).toggle();
});
// add some menus
for (i in dataRoot) {
var group = dataRoot[i];
this.addItem({label: group.displayName, groupName:group.groupName, action:function(this_item, checked) {
markersDB[this_item.groupName].checked = checked;
jQuery.each(markersDB[this_item.groupName].raw, function(i, elem) {
elem.markerObj.setVisible(checked);
});
}});
}
iconURL = overviewerConfig.CONST.image.signMarker;
//dataRoot['markers'] = [];
//
for (i in dataRoot) {
var groupName = dataRoot[i].groupName;
if (!markersDB[groupName].created) {
for (j in markersDB[groupName].raw) {
var entity = markersDB[groupName].raw[j];
var marker = new google.maps.Marker({
'position': overviewer.util.fromWorldToLatLng(entity.x,
entity.y, entity.z, overviewer.mapView.options.currentTileSet),
'map': overviewer.map,
'title': jQuery.trim(entity.Text1 + "\n" + entity.Text2 + "\n" + entity.Text3 + "\n" + entity.Text4),
'icon': iconURL,
'visible': false
});
if (entity['id'] == 'Sign') {
overviewer.util.createMarkerInfoWindow(marker);
}
jQuery.extend(entity, {markerObj: marker});
}
markersDB[groupName].created = true;
}
}
},
addItem: function(item) {
var itemDiv = document.createElement('div');
var itemInput = document.createElement('input');
itemInput.type='checkbox';
// give it a name
$(itemInput).data('label',item.label);
$(itemInput).attr("_mc_groupname", item.groupName);
jQuery(itemInput).click((function(local_item) {
return function(e) {
item.action(local_item, e.target.checked);
};
})(item));
this.$(".dropDown")[0].appendChild(itemDiv);
itemDiv.appendChild(itemInput);
var textNode = document.createElement('text');
if(item.icon) {
textNode.innerHTML = '<img width="15" height="15" src="' +
item.icon + '">' + item.label + '<br/>';
} else {
textNode.innerHTML = item.label + '<br/>';
}
itemDiv.appendChild(textNode);
},
});
/**
* SpawnIconView
*/
overviewer.views.SpawnIconView = Backbone.View.extend({
render: function() {
//
var curTileSet = overviewer.mapView.options.currentTileSet;
if (overviewer.collections.spawnMarker) {
overviewer.collections.spawnMarker.setMap(null);
overviewer.collections.spawnMarker = null;
}
var spawn = curTileSet.get("spawn");
if (spawn) {
overviewer.collections.spawnMarker = new google.maps.Marker({
'position': overviewer.util.fromWorldToLatLng(spawn[0],
spawn[1], spawn[2], overviewer.mapView.options.currentTileSet),
'map': overviewer.map,
'title': 'spawn',
'icon': overviewerConfig.CONST.image.spawnMarker,
'visible': false
});
overviewer.collections.spawnMarker.setVisible(true);
}
}
});

View File

@@ -17,6 +17,7 @@
<script type="text/javascript" src="backbone.js"></script>
<script type="text/javascript" src="overviewerConfig.js"></script>
<script type="text/javascript" src="overviewer.js"></script>
<script type="text/javascript" src="baseMarkers.js"></script>
</head>

View File

@@ -75,6 +75,7 @@ renders = Setting(required=True, default=util.OrderedDict(),
"rerenderprob": Setting(required=True, validator=validateFloat, default=0),
"crop": Setting(required=False, validator=validateCrop, default=None),
"changelist": Setting(required=False, validator=validateStr, default=None),
"markers": Setting(required=False, validator=validateMarkers, default=[]),
# Remove this eventually (once people update their configs)
"worldname": Setting(required=False, default=None,

View File

@@ -43,6 +43,13 @@ def checkBadEscape(s):
fixed = True
return (fixed, fixed_string)
def validateMarkers(filterlist):
if type(filterlist) != list:
raise ValidationException("Markers must specify a list of filters")
for x in filterlist:
if not callable(x):
raise ValidationException("%r must be a function"% x)
return filterlist
def validateWorldPath(worldpath):
_, worldpath = checkBadEscape(worldpath)

View File

@@ -518,6 +518,8 @@ class TileSet(object):
last_rendertime = self.max_chunk_mtime,
imgextension = self.imgextension,
)
if (self.regionset.get_type() == "overworld"):
d.update({"spawn": self.options.get("spawn")})
try:
d['north_direction'] = self.regionset.north_dir
except AttributeError:

View File

@@ -19,6 +19,8 @@ import os.path
from glob import glob
import logging
import hashlib
import time
import random
import numpy
@@ -91,8 +93,21 @@ class World(object):
if not os.path.exists(os.path.join(self.worlddir, "level.dat")):
raise ValueError("level.dat not found in %s" % self.worlddir)
# Hard-code this to only work with format version 19133, "Anvil"
data = nbt.load(os.path.join(self.worlddir, "level.dat"))[1]['Data']
# it seems that reading a level.dat file is unstable, particularly with respect
# to the spawnX,Y,Z variables. So we'll try a few times to get a good reading
# empirically, it seems that 0,50,0 is a "bad" reading
retrycount = 0
while (data['SpawnX'] == 0 and data['SpawnY'] == 50 and data['SpawnZ'] ==0 ):
logging.debug("bad level read, retrying")
time.sleep(random.random())
data = nbt.load(os.path.join(self.worlddir, "level.dat"))[1]['Data']
retrycount += 1
if retrycount > 10:
raise Exception("Failed to correctly read level.dat")
# Hard-code this to only work with format version 19133, "Anvil"
if not ('version' in data and data['version'] == 19133):
logging.critical("Sorry, This version of Minecraft-Overviewer only works with the 'Anvil' chunk format")
raise ValueError("World at %s is not compatible with Overviewer" % self.worlddir)
@@ -163,7 +178,7 @@ class World(object):
# location
## read spawn info from level.dat
data = self.data
data = self.leveldat
disp_spawnX = spawnX = data['SpawnX']
spawnY = data['SpawnY']
disp_spawnZ = spawnZ = data['SpawnZ']
@@ -175,24 +190,32 @@ class World(object):
## clamp spawnY to a sane value, in-chunk value
if spawnY < 0:
spawnY = 0
if spawnY > 127:
spawnY = 127
if spawnY > 255:
spawnY = 255
# Open up the chunk that the spawn is in
regionset = self.get_regionset(0)
regionset = self.get_regionset("overworld")
try:
chunk = regionset.get_chunk(chunkX, chunkZ)
except ChunkDoesntExist:
return (spawnX, spawnY, spawnZ)
def getBlock(y):
"This is stupid and slow but I don't care"
targetSection = spawnY//16
for section in chunk['Sections']:
if section['Y'] == targetSection:
blockArray = section['Blocks']
return blockArray[inChunkX, inChunkZ, y % 16]
blockArray = chunk['Blocks']
## The block for spawn *within* the chunk
inChunkX = spawnX - (chunkX*16)
inChunkZ = spawnZ - (chunkZ*16)
## find the first air block
while (blockArray[inChunkX, inChunkZ, spawnY] != 0) and spawnY < 127:
while (getBlock(spawnY) != 0) and spawnY < 256:
spawnY += 1
return spawnX, spawnY, spawnZ