# # 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)