2016-07-09 17:21:20 -05:00

595 lines
24 KiB
Python
Executable File

#
# Code under the MIT license by Alexander Pruss
#
"""
Make a moving vehicle out of whatever blocks the player is standing near.
python [options [name]]
options can include:
b: want an airtight bubble in the vehicle for going underwater
n: non-destructive mode
q: don't want the vehicle to flash as it is scanned
d: liquids don't count as terrain
l: load vehicle from vehicles/name.py
s: save vehicle to vehicles/name.py and quit
The vehicle detection algorithm works as follows:
first, search for nearest non-terrain block within distance SCAN_DISTANCE of the player
second, get the largest connected set of non-terrain blocks, including diagonal connections, up to
distance MAX_DISTANCE in each coordinate
in bubble mode, add the largest set of air blocks, excluding diagonal connections, or a small bubble about the
player if the the vehicle is not airtight
"""
from mcpi.minecraft import *
from mcpi.block import *
from math import *
from sys import maxsize
from copy import copy
from ast import literal_eval
import re
def getSavePath(directory, extension):
import Tkinter
from tkFileDialog import asksaveasfilename
master = Tkinter.Tk()
master.attributes("-topmost", True)
path = asksaveasfilename(initialdir=directory,filetypes=['vehicle {*.'+extension+'}'],defaultextension="."+extension,title="Save")
master.destroy()
return path
def getLoadPath(directory, extension):
import Tkinter
from tkFileDialog import askopenfilename
master = Tkinter.Tk()
master.attributes("-topmost", True)
path = askopenfilename(initialdir=directory,filetypes=['vehicle {*.'+extension+'}'],title="Open")
master.destroy()
return path
class Vehicle():
# the following blocks do not count as part of the vehicle
TERRAIN = set((AIR.id,WATER_FLOWING.id,WATER_STATIONARY.id,GRASS.id,DIRT.id,LAVA_FLOWING.id,
LAVA_STATIONARY.id,GRASS.id,DOUBLE_TALLGRASS.id,GRASS_TALL.id,BEDROCK.id,GRAVEL.id,SAND.id))
LIQUIDS = set((WATER_FLOWING.id,WATER_STATIONARY.id,LAVA_FLOWING.id,LAVA_STATIONARY.id))
# ideally, the following blocks are drawn last and erased first
NEED_SUPPORT = set((SAPLING.id,WATER_FLOWING.id,LAVA_FLOWING.id,GRASS_TALL.id,34,FLOWER_YELLOW.id,
FLOWER_CYAN.id,MUSHROOM_BROWN.id,MUSHROOM_RED.id,TORCH.id,63,DOOR_WOOD.id,LADDER.id,
66,68,69,70,DOOR_IRON.id,72,75,76,77,SUGAR_CANE.id,93,94,96,104,105,106,108,111,
113,115,116,117,122,127,131,132,141,142,143,145,147,148,149,150,151,154,157,
167,CARPET.id,SUNFLOWER.id,176,177,178,183,184,185,186,187,188,189,190,191,192,
193,194,195,196,197))
SCAN_DISTANCE = 5
MAX_DISTANCE = 30
stairDirectionsClockwise = [2, 1, 3, 0]
stairToClockwise = [3, 1, 0, 2]
chestToClockwise = [0,0,0,2,3,1,0,0]
chestDirectionsClockwise = [2,5,3,4]
STAIRS = set((STAIRS_COBBLESTONE.id, STAIRS_WOOD.id, 108, 109, 114, 128, 134, 135, 136, 156, 163, 164, 180))
DOORS = set((DOOR_WOOD.id,193,194,195,196,197,DOOR_IRON.id))
LADDERS_FURNACES_CHESTS_SIGNS_ETC = set((LADDER.id, FURNACE_ACTIVE.id, FURNACE_INACTIVE.id, CHEST.id, 130, 146, 68, 154, 23, 33, 36))
REDSTONE_COMPARATORS_REPEATERS = set((93,94,149,150,356,404))
EMPTY = {}
def __init__(self,mc,nondestructive=False):
self.mc = mc
self.nondestructive = nondestructive
self.highWater = -maxsize-1
self.baseVehicle = {}
if hasattr(Minecraft, 'getBlockWithNBT'):
self.getBlockWithData = self.mc.getBlockWithNBT
self.setBlockWithData = self.mc.setBlockWithNBT
else:
self.getBlockWithData = self.mc.getBlockWithData
self.setBlockWithData = self.mc.setBlock
self.curVehicle = {}
self.curRotation = 0
self.curLocation = None
self.saved = {}
self.baseAngle = 0
@staticmethod
def keyFunction(dict,erase,pos):
ns = pos in dict and dict[pos].id in Vehicle.NEED_SUPPORT
if ns:
return (True, pos not in erase or erase[pos].id not in Vehicle.NEED_SUPPORT,
pos[0],pos[2],pos[1])
else:
return (False, pos not in erase or erase[pos].id not in Vehicle.NEED_SUPPORT,
pos[1],pos[0],pos[2])
@staticmethod
def box(x0,y0,z0,x1,y1,z1):
for x in range(x0,x1+1):
for y in range(y0,y1+1):
for z in range(z0,z1+1):
yield (x,y,z)
def getSeed(self,x0,y0,z0):
scanned = set()
for r in range(0,Vehicle.SCAN_DISTANCE+1):
for x,y,z in Vehicle.box(-r,-r,-r,r,r,r):
if x*x+y*y+z*z <= r*r and (x,y,z) not in scanned:
blockId = self.mc.getBlock(x+x0,y+y0,z+z0)
scanned.add((x,y,z))
if blockId not in Vehicle.TERRAIN:
return (x0+x,y0+y,z0+z)
return None
def save(self,filename):
f = open(filename,"w")
f.write("baseAngle,highWater,baseVehicle="+repr((self.baseAngle,self.highWater,self.baseVehicle))+"\n")
f.close()
def load(self,filename):
with open(filename) as f:
data = ''.join(f.readlines())
result = re.search("=\\s*(.*)",data)
if result is None:
raise ValueError
# Check to ensure only function called is Block() by getting literal_eval to
# raise an exception when "Block" is removed and the result isn't a literal.
# This SHOULD make the eval call safe, though USE AT YOUR OWN RISK. Ideally,
# one would walk the ast parse tree and use a whitelist.
literal_eval(result.group(1).replace("Block",""))
self.baseAngle,self.highWater,self.baseVehicle = eval(result.group(1))
self.curLocation = None
def safeSetBlockWithData(self,pos,block):
"""
Draw block, making sure buttons are not depressed. This is to fix a glitch where launching
the vehicle script from a commandblock resulted in re-pressing of the button.
"""
if block.id == WOOD_BUTTON.id or block.id == STONE_BUTTON.id:
block = Block(block.id, block.data & ~0x08)
self.setBlockWithData(pos,block)
def scan(self,x0,y0,z0,angle=0,flash=True):
positions = {}
self.curLocation = (x0,y0,z0)
self.curRotation = 0
self.baseAngle = angle
seed = self.getSeed(x0,y0,z0)
if seed is None:
return {}
block = self.getBlockWithData(seed)
self.curVehicle = {seed:block}
if flash and block.id not in Vehicle.NEED_SUPPORT:
self.mc.setBlock(seed,GLOWSTONE_BLOCK)
newlyAdded = set(self.curVehicle.keys())
searched = set()
searched.add(seed)
while len(newlyAdded)>0:
adding = set()
self.mc.postToChat("Added "+str(len(newlyAdded))+" blocks")
for q in newlyAdded:
for x,y,z in Vehicle.box(-1,-1,-1,1,1,1):
pos = (x+q[0],y+q[1],z+q[2])
if pos not in searched:
if ( abs(pos[0]-x0) <= Vehicle.MAX_DISTANCE and
abs(pos[1]-y0) <= Vehicle.MAX_DISTANCE and
abs(pos[2]-z0) <= Vehicle.MAX_DISTANCE ):
searched.add(pos)
block = self.getBlockWithData(pos)
if block.id in Vehicle.TERRAIN:
if ((block.id == WATER_STATIONARY.id or block.id == WATER_FLOWING.id) and
self.highWater < pos[1]):
self.highWater = pos[1]
else:
self.curVehicle[pos] = block
adding.add(pos)
if flash and block.id not in Vehicle.NEED_SUPPORT:
self.mc.setBlock(pos,GLOWSTONE_BLOCK)
newlyAdded = adding
self.baseVehicle = {}
for pos in self.curVehicle:
self.baseVehicle[(pos[0]-x0,pos[1]-y0,pos[2]-z0)] = self.curVehicle[pos]
if flash:
import time
for pos in sorted(self.curVehicle, key=lambda a : Vehicle.keyFunction(self.curVehicle,Vehicle.EMPTY,a)):
self.safeSetBlockWithData(pos,self.curVehicle[pos])
def angleToRotation(self,angle):
return int(round((angle-self.baseAngle)/90.)) % 4
def draw(self,x,y,z,angle=0):
self.curLocation = (x,y,z)
self.curRotation = self.angleToRotation(angle)
self.curVehicle = {}
self.saved = {}
vehicle = Vehicle.rotate(self.baseVehicle,self.curRotation)
for pos in sorted(vehicle, key=lambda a : Vehicle.keyFunction(vehicle,Vehicle.EMPTY,a)):
drawPos = (pos[0] + x, pos[1] + y, pos[2] + z)
if self.nondestructive:
self.saved[drawPos] = self.getBlockWithData(drawPos)
self.safeSetBlockWithData(drawPos,vehicle[pos])
self.curVehicle[drawPos] = vehicle[pos]
def blankBehind(self):
for pos in self.saved:
self.saved[pos] = self.defaultFiller(pos)
def erase(self):
todo = {}
for pos in self.curVehicle:
if self.nondestructive and pos in self.saved:
todo[pos] = self.saved[pos]
else:
todo[pos] = self.defaultFiller(pos)
for pos in sorted(todo, key=lambda x : Vehicle.keyFunction(todo,Vehicle.EMPTY,x)):
self.safeSetBlockWithData(pos,todo[pos])
self.saved = {}
self.curVehicle = {}
def setVehicle(self,dict,startAngle=None):
if not startAngle is None:
self.baseAngle = startAngle
self.baseVehicle = dict
def setHighWater(self,y):
self.highWater = y;
def addBubble(self):
positions = set()
positions.add((0,0,0))
newlyAdded = set()
newlyAdded.add((0,0,0))
while len(newlyAdded) > 0:
adding = set()
for q in newlyAdded:
for x,y,z in [(-1,0,0),(1,0,0),(0,1,0),(0,-1,0),(0,0,1),(0,0,-1)]:
pos = (x+q[0],y+q[1],z+q[2])
if (abs(pos[0]) >= Vehicle.MAX_DISTANCE or
abs(pos[1]) >= Vehicle.MAX_DISTANCE or
abs(pos[2]) >= Vehicle.MAX_DISTANCE):
self.mc.postToChat("Vehicle is not airtight!")
positions = set()
for x1 in range(-1,2):
for y1 in range(-1,2):
for z1 in range(-1,2):
if (x1,y1,z1) not in self.baseVehicle:
self.baseVehicle[(x1,y1,z1)] = AIR
if (0,2,0) not in self.baseVehicle:
self.baseVehicle[(x,y+2,z)] = AIR
return
if pos not in positions and pos not in self.baseVehicle:
positions.add(pos)
adding.add(pos)
newlyAdded = adding
if (0,0,0) in self.baseVehicle:
del positions[(0,0,0)]
for pos in positions:
self.baseVehicle[pos] = AIR
# TODO: rotate blocks other than stairs and buttons
@staticmethod
def rotateBlock(block,amount):
if block.id in Vehicle.STAIRS:
meta = block.data
return Block(block.id, (meta & ~0x03) |
Vehicle.stairDirectionsClockwise[(Vehicle.stairToClockwise[meta & 0x03] + amount) % 4])
elif block.id in Vehicle.LADDERS_FURNACES_CHESTS_SIGNS_ETC:
high = block.data & 0x08
meta = block.data & 0x07
if meta < 2:
return block
block = copy(block)
block.data = high | Vehicle.chestDirectionsClockwise[(Vehicle.chestToClockwise[meta] + amount) % 4]
return block
elif block.id == STONE_BUTTON.id or block.id == WOOD_BUTTON.id:
direction = block.data & 0x07
if direction < 1 or direction > 4:
return block
direction = 1 + Vehicle.stairDirectionsClockwise[(Vehicle.stairToClockwise[direction-1] + amount) % 4]
return Block(block.id, (block.data & ~0x07) | direction)
elif block.id in Vehicle.REDSTONE_COMPARATORS_REPEATERS:
return Block(block.id, (block.data & ~0x03) | (((block.data & 0x03) + amount) & 0x03))
elif block.id == 96 or block.id == 167:
# trapdoors
meta = block.data
return Block(block.id, (meta & ~0x03) |
Vehicle.stairDirectionsClockwise[(Vehicle.stairToClockwise[meta & 0x03] - amount) % 4])
elif block.id in Vehicle.DOORS:
meta = block.data
if meta & 0x08:
return block
else:
return Block(block.id, (meta & ~0x03) | (((meta & 0x03) + amount) & 0x03))
else:
return block
@staticmethod
def rotate(dict, amount):
out = {}
amount = amount % 4
if amount == 0:
return dict
elif amount == 1:
for pos in dict:
out[(-pos[2],pos[1],pos[0])] = Vehicle.rotateBlock(dict[pos],amount)
elif amount == 2:
for pos in dict:
out[(-pos[0],pos[1],-pos[2])] = Vehicle.rotateBlock(dict[pos],amount)
else:
for pos in dict:
out[(pos[2],pos[1],-pos[0])] = Vehicle.rotateBlock(dict[pos],amount)
return out
def defaultFiller(self,pos):
return WATER_STATIONARY if self.highWater is not None and pos[1] <= self.highWater else AIR
@staticmethod
def translate(base,x,y,z):
out = {}
for pos in base:
out[(x+pos[0],y+pos[1],z+pos[2])] = base[pos]
return out
def moveTo(self,x,y,z,angleDegrees=0):
rotation = self.angleToRotation(angleDegrees)
if self.curRotation == rotation and (x,y,z) == self.curLocation:
return
base = Vehicle.rotate(self.baseVehicle, rotation)
newVehicle = Vehicle.translate(base,x,y,z)
todo = {}
erase = {}
for pos in self.curVehicle:
if pos not in newVehicle:
if self.nondestructive and pos in self.saved:
todo[pos] = self.saved[pos]
del self.saved[pos]
else:
todo[pos] = self.defaultFiller(pos)
else:
erase[pos] = self.curVehicle[pos]
for pos in newVehicle:
block = newVehicle[pos]
if pos not in self.curVehicle or self.curVehicle[pos] != block:
todo[pos] = block
if pos not in self.curVehicle and self.nondestructive:
curBlock = self.getBlockWithData(pos)
if curBlock == block:
del todo[pos]
self.saved[pos] = curBlock
erase[pos] = curBlock
for pos in sorted(todo, key=lambda x : Vehicle.keyFunction(todo,erase,x)):
self.safeSetBlockWithData(pos,todo[pos])
self.curVehicle = newVehicle
self.curLocation = (x,y,z)
self.curRotation = rotation
if __name__ == '__main__':
import time
import sys
import os
def save(name):
directory = os.path.join(os.path.dirname(sys.argv[0]),"vehicles")
try:
os.mkdir(directory)
except:
pass
if name:
path = os.path.join(directory,name+".py")
else:
path = getSavePath(directory, "py")
if not path:
minecraft.postToChat('Canceled')
return
vehicle.save(path)
minecraft.postToChat('Vehicle saved in '+path)
def load(name):
directory = os.path.join(os.path.dirname(sys.argv[0]),"vehicles")
if name:
path = os.path.join(directory,name+".py")
else:
path = getLoadPath(directory, "py")
if not path:
minecraft.postToChat('Canceled')
return
vehicle.load(path)
minecraft.postToChat('Vehicle loaded from '+path)
def chatHelp():
minecraft.postToChat("vlist: list vehicles")
minecraft.postToChat("verase: erase vehicle and exit")
minecraft.postToChat("vsave [filename]: save vehicle")
minecraft.postToChat("vload [filename]: load vehicle")
minecraft.postToChat("vdriver [EntityName]: set driver to entity (omit for player) [Jam only]")
def doScanRectangularPrism(vehicle, basePos, startRot):
minecraft.postToChat("Indicate extreme points with sword right-click.")
minecraft.postToChat("Double-click when done.")
corner1 = [None,None,None]
corner2 = [None,None,None]
prevHit = None
done = False
minecraft.events.pollBlockHits()
while not done:
hits = minecraft.events.pollBlockHits()
if len(hits) > 0:
for h in hits:
if prevHit != None and h.pos == prevHit.pos:
done = True
break
if corner1[0] == None or h.pos.x < corner1[0]: corner1[0] = h.pos.x
if corner1[1] == None or h.pos.y < corner1[1]: corner1[1] = h.pos.y
if corner1[2] == None or h.pos.z < corner1[2]: corner1[2] = h.pos.z
if corner2[0] == None or h.pos.x > corner2[0]: corner2[0] = h.pos.x
if corner2[1] == None or h.pos.y > corner2[1]: corner2[1] = h.pos.y
if corner2[2] == None or h.pos.z > corner2[2]: corner2[2] = h.pos.z
minecraft.postToChat(""+str(corner2[0]-corner1[0]+1)+"x"+str(corner2[1]-corner1[1]+1)+"x"+str(corner2[2]-corner1[2]+1))
prevHit = h
else:
prevHit = None
time.sleep(0.25)
minecraft.postToChat("Scanning region")
dict = {}
for x in range(corner1[0],corner2[0]+1):
for y in range(corner1[1],corner2[1]+1):
for z in range(corner1[2],corner2[2]+1):
block = vehicle.getBlockWithData(x,y,z)
if block.id != AIR.id and block.id != WATER_STATIONARY.id and block.id != WATER_FLOWING.id:
pos = (x-basePos.x,y-basePos.y,z-basePos.z)
dict[pos] = block
minecraft.postToChat("Found "+str(len(dict))+" blocks")
vehicle.setVehicle(dict, startRot)
bubble = False
nondestructive = False
flash = True
doLoad = False
doSave = False
scanRectangularPrism = False
exitAfterDraw = False
noInitialRotate = False
if len(sys.argv)>1:
for x in sys.argv[1]:
if x == 'b':
bubble = True
elif x == 'n':
nondestructive = True
elif x == 'q':
flash = False
elif x == 'd':
Vehicle.TERRAIN -= Vehicle.LIQUIDS
elif x == 's':
saveName = sys.argv[2] if len(sys.argv)>2 else None
doSave = True
elif x == 'l':
loadName = sys.argv[2] if len(sys.argv)>2 else None
doLoad = True
elif x == 'L':
loadName = sys.argv[2] if len(sys.argv)>2 else None
doLoad = True
exitAfterDraw = True
elif x == 'r':
scanRectangularPrism = True
minecraft = Minecraft()
getRotation = minecraft.player.getRotation
getTilePos = minecraft.player.getTilePos
vehiclePos = getTilePos()
startRot = getRotation()
vehicle = Vehicle(minecraft,nondestructive)
if doLoad:
load(loadName)
elif scanRectangularPrism:
doScanRectangularPrism(vehicle,vehiclePos,startRot)
else:
minecraft.postToChat("Scanning vehicle")
vehicle.scan(vehiclePos.x,vehiclePos.y,vehiclePos.z,startRot,flash)
minecraft.postToChat("Number of blocks: "+str(len(vehicle.baseVehicle)))
if bubble:
minecraft.postToChat("Scanning for air bubble")
vehicle.addBubble()
if len(vehicle.baseVehicle) == 0:
minecraft.postToChat("Make a vehicle and then stand on or in it when starting this script.")
exit()
if doSave:
save(saveName)
exit()
minecraft.postToChat("Saved: exiting.")
if exitAfterDraw:
minecraft.postToChat("Drawing")
vehicle.draw(vehiclePos.x,vehiclePos.y,vehiclePos.z,startRot)
minecraft.postToChat("Done")
exit(0)
minecraft.postToChat("Now walk around.")
entity = None
try:
minecraft.events.pollChatPosts()
except:
pass
while True:
pos = getTilePos()
vehicle.moveTo(pos.x,pos.y,pos.z,getRotation())
try:
chats = minecraft.events.pollChatPosts()
for post in chats:
args = post.message.split()
if len(args)>0:
if args[0] == 'vhelp':
chatHelp()
elif args[0] == 'verase':
vehicle.erase()
exit()
elif args[0] == 'vsave':
if len(args) > 1:
save(args[1])
else:
save(None)
#chatHelp()
elif args[0] == 'vlist':
try:
out = None
dir = os.path.join(os.path.dirname(sys.argv[0]),"vehicles")
for f in os.listdir(dir):
if f.endswith(".py"):
if out is not None:
out += ' '+f[:-3]
else:
out = f[:-3]
if out is None:
minecraft.postToChat('No saved vehicles')
else:
minecraft.postToChat(out)
except:
minecraft.postToChat('Error listing (maybe no directory?)')
elif args[0] == 'vload':
try:
save("_backup")
minecraft.postToChat('Old vehicle saved as "_backup".')
load(args[1] if len(args)>=2 else None)
except:
minecraft.postToChat("Error loading")
elif args[0] == 'vdriver':
if entity != None:
minecraft.removeEntity(entity)
entity = None
else:
direction = minecraft.player.getDirection()*10
direction.y = 0
minecraft.player.setPos(pos + direction)
if len(args) > 1:
try:
entity = minecraft.spawnEntity(args[1],pos.x,pos.y,pos.z,'{CustomName:"'+args[1]+'"}')
getRotation = lambda: minecraft.entity.getRotation(entity)
getTilePos = lambda: minecraft.entity.getTilePos(entity)
except:
minecraft.postToChat('Error spawning '+args[1])
else:
getRotation = minecraft.player.getRotation
getTilePos = minecraft.player.getTilePos
except RequestError:
pass
time.sleep(0.25)