def pre_build_hook(self, player, builddata): item, metadata, x, y, z, face = builddata if item.slot != blocks["chest"].slot: returnValue((True, builddata, False)) x, y, z = adjust_coords_for_face((x, y, z), face) bigx, smallx, bigz, smallz = split_coords(x, z) # chest orientation according to players position if face == "-y" or face == "+y": orientation = ('+x', '+z', '-x', '-z')[((int(player.location.yaw) \ - 45 + 360) % 360) / 90] else: orientation = face # Chests have some restrictions on building: # you cannot connect more than two chests. (notchian) ccs = chestsAround(self.factory, (x, y, z)) ccn = len(ccs) if ccn > 1: # cannot build three or more connected chests returnValue((False, builddata, True)) chunk = yield self.factory.world.request_chunk(bigx, bigz) if ccn == 0: metadata = blocks["chest"].orientation(orientation) elif ccn == 1: # check gonna-be-connected chest is not connected already n = len(chestsAround(self.factory, ccs[0])) if n != 0: returnValue((False, builddata, True)) # align both blocks correctly (since 1.8) # get second block x2, y2, z2 = ccs[0] bigx2, smallx2, bigz2, smallz2 = split_coords(x2, z2) # new chests orientation axis according to blocks position pair = x - x2, z - z2 ornt = {(0, 1): "x", (0, -1): "x", (1, 0): "z", (-1, 0): "z"}[pair] # if player is faced another direction, fix it if orientation[1] != ornt: # same sign with proper orientation # XXX Probably notchian logic is different here # but this one works well enough orientation = orientation[0] + ornt metadata = blocks["chest"].orientation(orientation) # update second block's metadata if bigx == bigx2 and bigz == bigz2: # both blocks are in same chunk chunk2 = chunk else: chunk2 = yield self.factory.world.request_chunk(bigx2, bigz2) chunk2.set_metadata((smallx2, y2, smallz2), metadata) # Not much to do, just tell the chunk about this tile. chunk.tiles[smallx, y, smallz] = ChestTile(smallx, y, smallz) builddata = builddata._replace(metadata=metadata) returnValue((True, builddata, False))
def open_hook(self, player, container, block): """ The ``player`` is a Player's protocol The ``container`` is a 0x64 message The ``block`` is a block we trying to open :returns: None or window object """ if block != blocks["chest"].slot: returnValue(None) bigx, smallx, bigz, smallz = split_coords(container.x, container.z) chunk = yield self.factory.world.request_chunk(bigx, bigz) chests_around = chestsAround(self.factory, (container.x, container.y, container.z)) chests_around_num = len(chests_around) if chests_around_num == 0: # small chest chest = self.get_chest_tile(chunk, (smallx, container.y, smallz)) if chest is None: returnValue(None) coords = bigx, smallx, bigz, smallz, container.y window = ChestWindow(player.wid, player.player.inventory, chest.inventory, coords) elif chests_around_num == 1: # large chest # process second chest coordinates x2, y2, z2 = chests_around[0] bigx2, smallx2, bigz2, smallz2 = split_coords(x2, z2) if bigx == bigx2 and bigz == bigz2: # both chest blocks are in same chunk chunk2 = chunk else: chunk2 = yield self.factory.world.request_chunk(bigx2, bigz2) chest1 = self.get_chest_tile(chunk, (smallx, container.y, smallz)) chest2 = self.get_chest_tile(chunk2, (smallx2, container.y, smallz2)) if chest1 is None or chest2 is None: returnValue(None) c1 = bigx, smallx, bigz, smallz, container.y c2 = bigx2, smallx2, bigz2, smallz2, container.y # We shall properly order chest inventories if c1 < c2: window = LargeChestWindow(player.wid, player.player.inventory, chest1.inventory, chest2.inventory, c1) else: window = LargeChestWindow(player.wid, player.player.inventory, chest2.inventory, chest1.inventory, c2) else: log.msg("Chest at (%d, %d, %d) have three chests connected" % (container.x, container.y, container.z)) returnValue(None) player.windows.append(window) returnValue(window)
def pre_build_hook(self, player, builddata): item, metadata, x, y, z, face = builddata if item.slot == items["sign"].slot: # Buildin' a sign, puttin' it on a wall... builddata = builddata._replace(block=blocks["wall-sign"]) # Offset coords according to face. if face == "-x": builddata = builddata._replace(metadata=0x4) x -= 1 elif face == "+x": builddata = builddata._replace(metadata=0x5) x += 1 elif face == "-y": # Ceiling Sign is watching you read. returnValue((False, builddata)) elif face == "+y": # Put +Y signs on signposts. We're fancy that way. Also, # calculate the proper orientation based on player # orientation. # 180 degrees around to orient the signs correctly, and then # 23 degrees to get the sign to midpoint correctly. metadata = ((player.location.yaw + 180) * 16 // 360) % 0xf builddata = builddata._replace(block=blocks["signpost"], metadata=metadata) y += 1 elif face == "-z": builddata = builddata._replace(metadata=0x2) z -= 1 elif face == "+z": builddata = builddata._replace(metadata=0x3) z += 1 bigx, smallx, bigz, smallz = split_coords(x, z) # Let's build a sign! chunk = yield factory.world.request_chunk(bigx, bigz) s = Sign(smallx, y, smallz) chunk.tiles[smallx, y, smallz] = s elif item.slot == blocks["chest"].slot: x, y, z = adjust_coords_for_face((x, y, z), face) bigx, smallx, bigz, smallz = split_coords(x, z) # Not much to do, just tell the chunk about this chest. chunk = yield factory.world.request_chunk(bigx, bigz) c = Chest(smallx, y, smallz) chunk.tiles[smallx, y, smallz] = c returnValue((True, builddata))
def update_chunks(self): x, chaff, z, chaff = split_coords(self.location.x, self.location.z) new = set((i + x, j + z) for i, j in circle) old = set(self.chunks.iterkeys()) added = new - old discarded = old - new # Perhaps some explanation is in order. # The cooperate() function iterates over the iterable it is fed, # without tying up the reactor, by yielding after each iteration. The # inner part of the generator expression generates all of the chunks # around the currently needed chunk, and it sorts them by distance to # the current chunk. The end result is that we load chunks one-by-one, # nearest to furthest, without stalling other clients. if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass self.chunk_tasks = [cooperate(task) for task in ( self.enable_chunk(i, j) for i, j in sorted(added, key=lambda t: (t[0] - x)**2 + (t[1] - z)**2) ), (self.disable_chunk(i, j) for i, j in discarded) ]
def position_changed(self): x, chaff, z, chaff = split_coords(self.location.x, self.location.z) # Inform everybody of our new location. packet = make_packet("teleport", eid=self.player.eid, x=self.location.x * 32, y=self.location.y * 32, z=self.location.z * 32, yaw=int(self.location.theta * 255 / (2 * pi)) % 256, pitch=int(self.location.phi * 255 / (2 * pi)) % 256, ) self.factory.broadcast_for_others(packet, self) self.update_chunks() for entity in self.entities_near(2): if entity.name != "Item": continue if self.player.inventory.add(entity.item, entity.quantity): packet = make_packet("collect", eid=entity.eid, destination=self.player.eid) self.factory.broadcast(packet) packet = make_packet("destroy", eid=entity.eid) self.factory.broadcast(packet) packet = self.player.inventory.save_to_packet() self.transport.write(packet) self.factory.destroy_entity(entity)
def entities_near(self, radius): """ Obtain the entities within a radius of this player. Radius is measured in blocks. """ chunk_radius = int(radius // 16 + 1) chunkx, chaff, chunkz, chaff = split_coords(self.location.x, self.location.z) minx = chunkx - chunk_radius maxx = chunkx + chunk_radius + 1 minz = chunkz - chunk_radius maxz = chunkz + chunk_radius + 1 for x, z in product(xrange(minx, maxx), xrange(minz, maxz)): if (x, z) not in self.chunks: continue chunk = self.chunks[x, z] yieldables = [entity for entity in chunk.entities if self.location.distance(entity.location) <= radius] for i in yieldables: yield i
def sign(self, container): bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't handle sign in chunk (%d, %d)!" % (bigx, bigz)) return if (smallx, container.y, smallz) in chunk.tiles: new = False s = chunk.tiles[smallx, container.y, smallz] else: new = True s = Sign(smallx, container.y, smallz) chunk.tiles[smallx, container.y, smallz] = s s.text1 = container.line1 s.text2 = container.line2 s.text3 = container.line3 s.text4 = container.line4 chunk.dirty = True # The best part of a sign isn't making one, it's showing everybody # else on the server that you did. packet = make_packet("sign", container) self.factory.broadcast_for_chunk(packet, bigx, bigz) # Run sign hooks. for hook in self.sign_hooks: hook.sign_hook(self.factory, chunk, container.x, container.y, container.z, [s.text1, s.text2, s.text3, s.text4], new)
def open_or_close(self, world, point): """ Toggle the state of the door : open it if it was closed, close it if it was open. """ x, y, z = point[0], point[1], point[2] bigx, x, bigz, z = split_coords(x, z) d = world.request_chunk(bigx, bigz) @d.addCallback def cb(chunk): block = chunk.get_block((x, y, z)) if block not in Door.doors: # already removed return metadata = chunk.get_metadata((x, y, z)) chunk.set_metadata((x, y, z), metadata ^ DOOR_IS_SWUNG) # Finding out which block is the door's top block. if (metadata & DOOR_TOP_BLOCK) != 0: other_y = y - 1 else: other_y = y + 1 other_block = chunk.get_block((x, other_y, z)) if other_block in Door.doors: metadata = chunk.get_metadata((x, other_y, z)) chunk.set_metadata((x, other_y, z), metadata ^ DOOR_IS_SWUNG) # Flush changed chunk self.factory.flush_chunk(chunk)
def entities_near(self, radius): """ Obtain the entities within a radius of this player. Radius is measured in blocks. """ chunk_radius = int(radius // 16 + 1) chunkx, chaff, chunkz, chaff = split_coords(self.location.pos.x, self.location.pos.z) minx = chunkx - chunk_radius maxx = chunkx + chunk_radius + 1 minz = chunkz - chunk_radius maxz = chunkz + chunk_radius + 1 for x, z in product(xrange(minx, maxx), xrange(minz, maxz)): if (x, z) not in self.chunks: continue chunk = self.chunks[x, z] yieldables = [ entity for entity in chunk.entities if self.location.distance(entity.location) <= (radius * 32) ] for i in yieldables: yield i
def send_initial_chunk_and_location(self): bigx, smallx, bigz, smallz = split_coords(self.location.x, self.location.z) # Spawn the 25 chunks in a square around the spawn, *before* spawning # the player. Otherwise, there's a funky Beta 1.2 bug which causes the # player to not be able to move. d = cooperate( self.enable_chunk(i, j) for i, j in product( xrange(bigx - 3, bigx + 3), xrange(bigz - 3, bigz + 3) ) ).whenDone() # Don't dare send more chunks beyond the initial one until we've # spawned. d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: packet = make_packet("chat", message=self.motd.replace("<tagline>", get_motd())) d.addCallback(lambda none: self.transport.write(packet)) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks())
def update_chunks(self): x, y, z = self.location.pos.to_block() x, chaff, z, chaff = split_coords(x, z) new = set((i + x, j + z) for i, j in circle) old = set(self.chunks.iterkeys()) added = new - old discarded = old - new # Perhaps some explanation is in order. # The cooperate() function iterates over the iterable it is fed, # without tying up the reactor, by yielding after each iteration. The # inner part of the generator expression generates all of the chunks # around the currently needed chunk, and it sorts them by distance to # the current chunk. The end result is that we load chunks one-by-one, # nearest to furthest, without stalling other clients. if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass self.chunk_tasks = [ cooperate( self.enable_chunk(i, j) for i, j in sorted( added, key=lambda t: (t[0] - x)**2 + (t[1] - z)**2)), cooperate(self.disable_chunk(i, j) for i, j in discarded) ]
def build(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land build packet. Discard it for now. return # Is the target being selected? bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't select in chunk (%d, %d)!" % (bigx, bigz)) return if chunk.get_block((smallx, container.y, smallz)) == blocks["workbench"].slot: i = Workbench() sync_inventories(self.player.inventory, i) self.windows[self.wid] = i packet = make_packet("window-open", wid=self.wid, type="workbench", title="Hurp", slots=2) self.wid += 1 self.transport.write(packet) return # Ignore clients that think -1 is placeable. if container.primary == -1: return # Special case when face is "noop": Update the status of the currently # held block rather than placing a new block. if container.face == "noop": return if container.primary in blocks: block = blocks[container.primary] elif container.primary in items: block = items[container.primary] else: log.err("Ignoring request to place unknown block %d" % container.primary) return # it's the top of the world, you can't build here if container.y == 127 and container.face == "+y": return # Run pre-build hooks. These hooks are able to interrupt the build # process. builddata = BuildData(block, 0x0, container.x, container.y, container.z, container.face) for hook in self.pre_build_hooks: cont, builddata = yield maybeDeferred(hook.pre_build_hook, self.player, builddata) if not cont: break # Run the build. try: builddata = self.prepare_build(builddata) yield maybeDeferred(self.run_build, builddata) except BuildError, e: print e return
def run(self): """ Starts a mob's loop process """ xcoord, chaff, zcoord, chaff = split_coords(self.location.x, self.location.z) self.chunk_coords = (xcoord,1, zcoord) # XXX The one is redundant, fix it self.loop = LoopingCall(self.update) self.loop.start(.2)
def update_location(self): bigx, smallx, bigz, smallz = split_coords(self.location.x, self.location.z) chunk = self.chunks[bigx, bigz] height = chunk.height_at(smallx, smallz) + 2 self.location.y = height packet = self.location.save_to_packet() self.transport.write(packet)
def decorated(self, coords, *args, **kwargs): x, y, z = coords bigx, smallx, bigz, smallz = split_coords(x, z) d = self.request_chunk(bigx, bigz) def cb(chunk): return f(self, chunk, (smallx, y, smallz), *args, **kwargs) d.addCallback(cb) return d
def test_split_coords(self): cases = { (0, 0): (0, 0, 0, 0), (1, 1): (0, 1, 0, 1), (16, 16): (1, 0, 1, 0), (-1, -1): (-1, 15, -1, 15), (-16, -16): (-1, 0, -1, 0), } for case in cases: self.assertEqual(split_coords(*case), cases[case])
def decorated(self, coords, *args, **kwargs): x, y, z = coords bigx, smallx, bigz, smallz = split_coords(x, z) d = self.request_chunk(bigx, bigz) @d.addCallback def cb(chunk): return f(self, chunk, (smallx, y, smallz), *args, **kwargs) return d
def decorated(self, coords, *args, **kwargs): x, y, z = coords bigx, smallx, bigz, smallz = split_coords(x, z) if (bigx, bigz) in self.chunk_cache: chunk = self.chunk_cache[bigx, bigz] elif (bigx, bigz) in self.dirty_chunk_cache: chunk = self.dirty_chunk_cache[bigx, bigz] else: raise ChunkNotLoaded("Chunk (%d, %d) isn't loaded") return f(self, chunk, (smallx, y, smallz), *args, **kwargs)
def open_or_close(self, coords): x, y, z = coords bigx, x, bigz, z = split_coords(x, z) d = self.factory.world.request_chunk(bigx, bigz) @d.addCallback def cb(chunk): block = chunk.get_block((x, y, z)) if block != blocks["trapdoor"].slot: # already removed return metadata = chunk.get_metadata((x, y, z)) chunk.set_metadata((x, y, z), metadata ^ DOOR_IS_SWUNG) self.factory.flush_chunk(chunk)
def decorated(self, coords, *args, **kwargs): x, y, z = coords bigx, smallx, bigz, smallz = split_coords(x, z) bigcoords = bigx, bigz if bigcoords in self.chunk_cache: chunk = self.chunk_cache[bigcoords] elif bigcoords in self.dirty_chunk_cache: chunk = self.dirty_chunk_cache[bigcoords] else: raise ChunkNotLoaded("Chunk (%d, %d) isn't loaded" % bigcoords) return f(self, chunk, (smallx, y, smallz), *args, **kwargs)
def send_initial_chunk_and_location(self): """ Send the initial chunks and location. This method sends more than one chunk; since Beta 1.2, it must send nearly fifty chunks before the location can be safely sent. """ # Disable located hooks. We'll re-enable them at the end. self.state = STATE_AUTHENTICATED log.msg("Initial, position %d, %d, %d" % self.location.pos) x, y, z = self.location.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) # Spawn the 49 chunks in a square around the spawn, *before* spawning # the player. Otherwise, there's a funky Beta 1.2 bug which causes the # player to not be able to move. d = gatherResults([ self.enable_chunk(i, j) for i, j in product(xrange(bigx - 3, bigx + 3), xrange(bigz - 3, bigz + 3)) ]) # What to do if we can't load a given chunk? Just kick 'em. d.addErrback(lambda fail: self.error("Couldn't load a chunk... :c")) # Don't dare send more chunks beyond the initial one until we've # spawned. Once we've spawned, set our status to LOCATED and then # update_location() will work. @d.addCallback def located(none): self.state = STATE_LOCATED # Ensure that we're above-ground. self.ascend(0) d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: @d.addCallback def motd(none): self.write_packet("chat", message=self.motd.replace( "<tagline>", get_motd())) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks())
def decorated(self, coords, *args, **kwargs): x, y, z = coords # Fail early if Y is OOB. if not 0 <= y < CHUNK_HEIGHT: raise ImpossibleCoordinates("Y value %d is impossible" % y) bigx, smallx, bigz, smallz = split_coords(x, z) d = self.request_chunk(bigx, bigz) @d.addCallback def cb(chunk): return f(self, chunk, (smallx, y, smallz), *args, **kwargs) return d
def send_initial_chunk_and_location(self): """ Send the initial chunks and location. This method sends more than one chunk; since Beta 1.2, it must send nearly fifty chunks before the location can be safely sent. """ # Disable located hooks. We'll re-enable them at the end. self.state = STATE_AUTHENTICATED log.msg("Initial, position %d, %d, %d" % self.location.pos) x, y, z = self.location.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) # Spawn the 49 chunks in a square around the spawn, *before* spawning # the player. Otherwise, there's a funky Beta 1.2 bug which causes the # player to not be able to move. d = gatherResults([self.enable_chunk(i, j) for i, j in product( xrange(bigx - 3, bigx + 3), xrange(bigz - 3, bigz + 3) ) ]) # What to do if we can't load a given chunk? Just kick 'em. d.addErrback(lambda fail: self.error("Couldn't load a chunk... :c")) # Don't dare send more chunks beyond the initial one until we've # spawned. Once we've spawned, set our status to LOCATED and then # update_location() will work. @d.addCallback def located(none): self.state = STATE_LOCATED # Ensure that we're above-ground. self.ascend(0) d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: @d.addCallback def motd(none): self.write_packet("chat", message=self.motd.replace("<tagline>", get_motd())) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks())
def send_initial_chunk_and_location(self): """ Send the initial chunks and location. This method sends more than one chunk; since Beta 1.2, it must send nearly fifty chunks before the location can be safely sent. """ # Disable located hooks. We'll re-enable them at the end. self.state = STATE_AUTHENTICATED log.msg("Initial, position %d, %d, %d" % self.location.pos) x, y, z = self.location.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) # Send the chunk that the player will stand on. The other chunks are # not so important. There *used* to be a bug, circa Beta 1.2, that # required lots of surrounding geometry to be present, but that's been # fixed. d = self.enable_chunk(bigx, bigz) # What to do if we can't load a given chunk? Just kick 'em. d.addErrback(lambda fail: self.error("Couldn't load a chunk... :c")) # Don't dare send more chunks beyond the initial one until we've # spawned. Once we've spawned, set our status to LOCATED and then # update_location() will work. @d.addCallback def located(none): self.state = STATE_LOCATED # Ensure that we're above-ground. self.ascend(0) # XXX for now, drop down from way up high down onto the ground. # ascend() is busted, I think, and there's no good way to get up # this high. self.location.pos = self.location.pos._replace(y=250 * 32) d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: @d.addCallback def motd(none): self.write_packet("chat", message=self.motd.replace("<tagline>", get_motd())) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks())
def decorated(self, coords, *args, **kwargs): x, y, z = coords # Fail early if Y is OOB. if not 0 <= y < CHUNK_HEIGHT: raise ImpossibleCoordinates("Y value %d is impossible" % y) bigx, smallx, bigz, smallz = split_coords(x, z) bigcoords = bigx, bigz chunk = self._cache.get(bigcoords) if chunk is None: raise ChunkNotLoaded("Chunk (%d, %d) isn't loaded" % bigcoords) return f(self, chunk, (smallx, y, smallz), *args, **kwargs)
def play_notes(self, notes): """ Play some music. Send a sequence of notes to the player. ``notes`` is a finite iterable of pairs of instruments and pitches. There is no way to time notes; if staggered playback is desired (and it usually is!), then ``play_notes()`` should be called repeatedly at the appropriate times. This method turns the block beneath the player into a note block, plays the requested notes through it, then turns it back into the original block, all without actually modifying the chunk. """ x, y, z = self.location.pos.to_block() if y: y -= 1 bigx, smallx, bigz, smallz = split_coords(x, z) if (bigx, bigz) not in self.chunks: return block = self.chunks[bigx, bigz].get_block((smallx, y, smallz)) meta = self.chunks[bigx, bigz].get_metadata((smallx, y, smallz)) self.write_packet("block", x=x, y=y, z=z, type=blocks["note-block"].slot, meta=0) for instrument, pitch in notes: self.write_packet("note", x=x, y=y, z=z, pitch=pitch, instrument=instrument) self.write_packet("block", x=x, y=y, z=z, type=block, meta=meta)
def pre_build_hook(self, player, builddata): item, metadata, x, y, z, face = builddata if item.slot != items["sign"].slot: returnValue((True, builddata, False)) # Buildin' a sign, puttin' it on a wall... builddata = builddata._replace(block=blocks["wall-sign"]) # Offset coords according to face. if face == "-x": builddata = builddata._replace(metadata=0x4) x -= 1 elif face == "+x": builddata = builddata._replace(metadata=0x5) x += 1 elif face == "-y": # Ceiling Sign is watching you read. returnValue((False, builddata, False)) elif face == "+y": # Put +Y signs on signposts. We're fancy that way. Also, # calculate the proper orientation based on player # orientation. # 180 degrees around to orient the signs correctly, and then # 23 degrees to get the sign to midpoint correctly. yaw = player.location.ori.to_degs()[0] metadata = ((yaw + 180) * 16 // 360) % 0xf builddata = builddata._replace(block=blocks["signpost"], metadata=metadata) y += 1 elif face == "-z": builddata = builddata._replace(metadata=0x2) z -= 1 elif face == "+z": builddata = builddata._replace(metadata=0x3) z += 1 bigx, smallx, bigz, smallz = split_coords(x, z) # Let's build a sign! chunk = yield self.factory.world.request_chunk(bigx, bigz) s = SignTile(smallx, y, smallz) chunk.tiles[smallx, y, smallz] = s returnValue((True, builddata, False))
def ascend(self, count): """ Ascend to the next XZ-plane. ``count`` is the number of ascensions to perform, and may be zero in order to force this player to not be standing inside a block. :returns: bool of whether the ascension was successful This client must be located for this method to have any effect. """ if self.state != STATE_LOCATED: return False x, y, z = self.location.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) chunk = self.chunks[bigx, bigz] column = [chunk.get_block((smallx, i, smallz)) for i in range(CHUNK_HEIGHT)] # Special case: Ascend at most once, if the current spot isn't good. if count == 0: if (not column[y]) or column[y + 1] or column[y + 2]: # Yeah, we're gonna need to move. count += 1 else: # Nope, we're fine where we are. return True for i in xrange(y, 255): # Find the next spot above us which has a platform and two empty # blocks of air. if column[i] and (not column[i + 1]) and not column[i + 2]: count -= 1 if not count: break else: return False self.location.pos = self.location.pos._replace(y=i * 32) return True
def update_chunks(self): # Don't send chunks unless we're located. if self.state != STATE_LOCATED: return x, y, z = self.location.pos.to_block() x, chaff, z, chaff = split_coords(x, z) # These numbers come from a couple spots, including minecraftwiki, but # I verified them experimentally using torches and pillars to mark # distances on each setting. ~ C. distances = { "tiny": 2, "short": 4, "far": 16, } radius = distances.get(self.settings.distance, 8) new = set(circling(x, z, radius)) old = set(self.chunks.iterkeys()) added = new - old discarded = old - new # Perhaps some explanation is in order. # The cooperate() function iterates over the iterable it is fed, # without tying up the reactor, by yielding after each iteration. The # inner part of the generator expression generates all of the chunks # around the currently needed chunk, and it sorts them by distance to # the current chunk. The end result is that we load chunks one-by-one, # nearest to furthest, without stalling other clients. if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass to_enable = sorted_by_distance(added, x, z) self.chunk_tasks = [ cooperate(self.enable_chunk(i, j) for i, j in to_enable), cooperate(self.disable_chunk(i, j) for i, j in discarded), ]
def send_initial_chunk_and_location(self): """ Send the initial chunks and location. This method sends more than one chunk; since Beta 1.2, it must send nearly fifty chunks before the location can be safely sent. """ # Disable located hooks. We'll re-enable them at the end. self.state = STATE_AUTHENTICATED log.msg("Initial, position %d, %d, %d" % self.location.pos) x, y, z = self.location.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) # Send the chunk that the player will stand on. The other chunks are # not so important. There *used* to be a bug, circa Beta 1.2, that # required lots of surrounding geometry to be present, but that's been # fixed. d = self.enable_chunk(bigx, bigz) # What to do if we can't load a given chunk? Just kick 'em. d.addErrback(lambda fail: self.error("Couldn't load a chunk... :c")) # Don't dare send more chunks beyond the initial one until we've # spawned. Once we've spawned, set our status to LOCATED and then # update_location() will work. @d.addCallback def located(none): self.state = STATE_LOCATED # Ensure that we're above-ground. self.ascend(0) d.addCallback(lambda none: self.update_location()) d.addCallback(lambda none: self.position_changed()) # Send the MOTD. if self.motd: @d.addCallback def motd(none): self.send_chat(self.motd.replace("<tagline>", get_motd())) # Finally, start the secondary chunk loop. d.addCallback(lambda none: self.update_chunks())
def update_entities(self): """ Update all entities covered by this factory. """ # XXX this method could cause chunks to be generated :c points = set() for player in self.protocols.itervalues(): x = player.location.x z = player.location.z bigx, chaff, bigz, chaff = split_coords(x, z) new = set((i + bigx, j + bigz) for i, j in circle) points.update(new) for x, y in points: d = self.world.request_chunk(x, y) d.addCallback(lambda chunk: chunk.update_entities(self))
def pre_build_hook(self, player, builddata): item, metadata, x, y, z, face = builddata if item.slot != blocks["furnace"].slot: returnValue((True, builddata, False)) x, y, z = adjust_coords_for_face((x, y, z), face) bigx, smallx, bigz, smallz = split_coords(x, z) # the furnace cannot be oriented up or down if face == "-y" or face == "+y": orientation = ('+x', '+z', '-x', '-z')[((int(player.location.yaw) \ - 45 + 360) % 360) / 90] metadata = blocks["furnace"].orientation(orientation) builddata = builddata._replace(metadata=metadata) # Not much to do, just tell the chunk about this tile. chunk = yield self.factory.world.request_chunk(bigx, bigz) chunk.tiles[smallx, y, smallz] = FurnaceTile(smallx, y, smallz) returnValue((True, builddata, False))
def open_hook(self, player, container, block): """ The ``player`` is a Player's protocol The ``container`` is a 0x64 message The ``block`` is a block we trying to open :returns: None or window object """ if block not in (blocks["furnace"].slot, blocks["burning-furnace"].slot): returnValue(None) bigx, smallx, bigz, smallz = split_coords(container.x, container.z) chunk = yield factory.world.request_chunk(bigx, bigz) furnace = self.get_furnace_tile(chunk, (smallx, container.y, smallz)) if furnace is None: returnValue(None) coords = bigx, smallx, bigz, smallz, container.y window = FurnaceWindow(player.wid, player.player.inventory, furnace.inventory, coords) player.windows.append(window) returnValue(window)
def chat_command(self, username, parameters): protocol = self.factory.protocols[username] l = protocol.player.location x, y, z = l.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) chunk = self.factory.world.sync_request_chunk((x, y, z)) column = [chunk.get_block((smallx, i, smallz)) for i in range(256)] # Find the next spot below us which has a platform and two empty # blocks of air. while y > 0: y -= 1 if column[y] and not column[y + 1] and not column[y + 2]: break else: return ("Couldn't find anywhere to descend!",) l.pos = l.pos._replace(y=y) protocol.send_initial_chunk_and_location() return ("Descended!",)
def open_hook(self, player, container, block): """ The ``player`` is a Player's protocol The ``container`` is a 0x64 message The ``block`` is a block we trying to open :returns: None or window object """ if block not in (blocks["furnace"].slot, blocks["burning-furnace"].slot): returnValue(None) bigx, smallx, bigz, smallz = split_coords(container.x, container.z) chunk = yield self.factory.world.request_chunk(bigx, bigz) furnace = self.get_furnace_tile(chunk, (smallx, container.y, smallz)) if furnace is None: returnValue(None) coords = bigx, smallx, bigz, smallz, container.y window = FurnaceWindow(player.wid, player.player.inventory, furnace.inventory, coords) player.windows.append(window) returnValue(window)
def chat_command(self, username, parameters): protocol = factory.protocols[username] l = protocol.player.location x, y, z = l.pos.to_block() bigx, smallx, bigz, smallz = split_coords(x, z) chunk = factory.world.sync_request_chunk((x, y, z)) column = chunk.get_column(smallx, smallz) # Find the next spot below us which has a platform and two empty # blocks of air. while y > 0: y -= 1 if column[y] and not column[y + 1] and not column[y + 2]: break else: return ("Couldn't find anywhere to descend!", ) l.pos = l.pos._replace(y=y) protocol.send_initial_chunk_and_location() return ("Descended!", )
def chat_command(self, username, parameters): protocol = factory.protocols[username] x = protocol.player.location.x z = protocol.player.location.z bigx, smallx, bigz, smallz = split_coords(x, z) chunk = yield factory.world.request_chunk(bigx, bigz) column = chunk.get_column(smallx, smallz) y = protocol.player.location.y # Find the next spot below us which has a platform and two empty # blocks of air. while y > 0: y -= 1 if column[y] and not column[y + 1] and not column[y + 2]: break else: returnValue(("Couldn't find anywhere to descend!",)) protocol.player.location.y = y protocol.send_initial_chunk_and_location() returnValue(("Descended!",))
def play_notes(self, notes): """ Play some music. Send a sequence of notes to the player. ``notes`` is a finite iterable of pairs of instruments and pitches. There is no way to time notes; if staggered playback is desired (and it usually is!), then ``play_notes()`` should be called repeatedly at the appropriate times. This method turns the block beneath the player into a note block, plays the requested notes through it, then turns it back into the original block, all without actually modifying the chunk. """ x, y, z = self.location.x, self.location.y, self.location.z if y: y -= 1 bigx, smallx, bigz, smallz = split_coords(x, z) if (bigx, bigz) not in self.chunks: return block = self.chunks[bigx, bigz].get_block((smallx, y, smallz)) meta = self.chunks[bigx, bigz].get_metadata((smallx, y, smallz)) self.write_packet("block", x=x, y=y, z=z, type=blocks["note-block"].slot, meta=0) for (instrument, pitch) in notes: self.write_packet("note", x=x, y=y, z=z, pitch=pitch, instrument=instrument) self.write_packet("block", x=x, y=y, z=z, type=block, meta=meta)
def build(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land build packet. Discard it for now. return # Is the target being selected? bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't select in chunk (%d, %d)!" % (bigx, bigz)) return if (chunk.get_block((smallx, container.y, smallz)) == blocks["workbench"].slot): i = Workbench() sync_inventories(self.player.inventory, i) self.windows[self.wid] = i packet = make_packet("window-open", wid=self.wid, type="workbench", title="Hurp", slots=2) self.wid += 1 self.transport.write(packet) return # Ignore clients that think -1 is placeable. if container.primary == -1: return # Special case when face is "noop": Update the status of the currently # held block rather than placing a new block. if container.face == "noop": return if container.primary in blocks: block = blocks[container.primary] elif container.primary in items: block = items[container.primary] else: log.err("Ignoring request to place unknown block %d" % container.primary) return # it's the top of the world, you can't build here if container.y == 127 and container.face == '+y': return # Run pre-build hooks. These hooks are able to interrupt the build # process. builddata = BuildData(block, 0x0, container.x, container.y, container.z, container.face) for hook in self.pre_build_hooks: cont, builddata = yield maybeDeferred(hook.pre_build_hook, self.player, builddata) if not cont: break # Run the build. try: yield maybeDeferred(self.run_build, builddata) except BuildError: return newblock = builddata.block.slot coords = builddata.x, builddata.y, builddata.z # Run post-build hooks. These are merely callbacks which cannot # interfere with the build process, largely because the build process # already happened. for hook in self.post_build_hooks: yield maybeDeferred(hook.post_build_hook, self.player, coords, builddata.block) # Feed automatons. for automaton in self.factory.automatons: if newblock in automaton.blocks: automaton.feed((builddata.x, builddata.y, builddata.z)) # Re-send inventory. # XXX this could be optimized if/when inventories track damage. packet = self.player.inventory.save_to_packet() self.transport.write(packet) # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk)
def digging(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land dig packet. Discard it for now. return # Player drops currently holding item/block. if (container.state == "dropped" and container.face == "-y" and container.x == 0 and container.y == 0 and container.z == 0): i = self.player.inventory holding = i.holdables[self.player.equipped] if holding: primary, secondary, count = holding if i.consume((primary, secondary), self.player.equipped): dest = self.location.in_front_of(2) dest.y += 1 coords = (int(dest.x * 32) + 16, int(dest.y * 32) + 16, int(dest.z * 32) + 16) self.factory.give(coords, (primary, secondary), 1) # Re-send inventory. packet = self.player.inventory.save_to_packet() self.transport.write(packet) # If no items in this slot are left, this player isn't # holding an item anymore. if i.holdables[self.player.equipped] is None: packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=65535, secondary=0 ) self.factory.broadcast_for_others(packet, self) return bigx, smallx, bigz, smallz = split_coords(container.x, container.z) coords = smallx, container.y, smallz try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't dig in chunk (%d, %d)!" % (bigx, bigz)) return block = chunk.get_block((smallx, container.y, smallz)) if container.state == "started": tool = self.player.inventory.holdables[self.player.equipped] # Check to see whether we should break this block. if self.dig_policy.is_1ko(block, tool): self.run_dig_hooks(chunk, coords, blocks[block]) else: # Set up a timer for breaking the block later. dtime = time() + self.dig_policy.dig_time(block, tool) self.last_dig = coords, block, dtime elif container.state == "stopped": # The client thinks it has broken a block. We shall see. if not self.last_dig: return oldcoords, oldblock, dtime = self.last_dig if oldcoords != coords or oldblock != block: # Nope! self.last_dig = None return dtime -= time() # When enough time has elapsed, run the dig hooks. d = deferLater(reactor, max(dtime, 0), self.run_dig_hooks, chunk, coords, blocks[block]) d.addCallback(lambda none: setattr(self, "last_dig", None))
def setMetadata(self, coords, meta): factory.world.set_metadata(coords, meta) bigx, smallx, bigz, smallz = split_coords(coords[0], coords[2]) self.touchedChunks.add( (bigx, bigz) )
def post_build_hook(self, player, coords, block): bigx, smallx, bigz, smallz = split_coords(coords[0], coords[2]) chunk = yield factory.world.request_chunk(bigx, bigz) self.dig_hook(chunk, smallx, coords[1], smallz, block)
def digging(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land dig packet. Discard it for now. return # Player drops currently holding item/block. if (container.state == "dropped" and container.face == "-y" and container.x == 0 and container.y == 0 and container.z == 0): i = self.player.inventory holding = i.holdables[self.player.equipped] if holding: primary, secondary, count = holding if i.consume((primary, secondary), self.player.equipped): dest = self.location.in_front_of(2) coords = dest.pos._replace(y=dest.pos.y + 1) self.factory.give(coords, (primary, secondary), 1) # Re-send inventory. packet = self.inventory.save_to_packet() self.transport.write(packet) # If no items in this slot are left, this player isn't # holding an item anymore. if i.holdables[self.player.equipped] is None: packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=65535, secondary=0) self.factory.broadcast_for_others(packet, self) return if container.state == "shooting": self.shoot_arrow() return bigx, smallx, bigz, smallz = split_coords(container.x, container.z) coords = smallx, container.y, smallz try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't dig in chunk (%d, %d)!" % (bigx, bigz)) return block = chunk.get_block((smallx, container.y, smallz)) if container.state == "started": # Run pre dig hooks for hook in self.pre_dig_hooks: cancel = yield maybeDeferred( hook.pre_dig_hook, self.player, (container.x, container.y, container.z), block) if cancel: return tool = self.player.inventory.holdables[self.player.equipped] # Check to see whether we should break this block. if self.dig_policy.is_1ko(block, tool): self.run_dig_hooks(chunk, coords, blocks[block]) else: # Set up a timer for breaking the block later. dtime = time() + self.dig_policy.dig_time(block, tool) self.last_dig = coords, block, dtime elif container.state == "stopped": # The client thinks it has broken a block. We shall see. if not self.last_dig: return oldcoords, oldblock, dtime = self.last_dig if oldcoords != coords or oldblock != block: # Nope! self.last_dig = None return dtime -= time() # When enough time has elapsed, run the dig hooks. d = deferLater(reactor, max(dtime, 0), self.run_dig_hooks, chunk, coords, blocks[block]) d.addCallback(lambda none: setattr(self, "last_dig", None))
def build(self, container): if container.x == -1 and container.z == -1 and container.y == 255: # Lala-land build packet. Discard it for now. return # Is the target being selected? bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't select in chunk (%d, %d)!" % (bigx, bigz)) return # Try to open it first for hook in self.open_hooks: window = yield maybeDeferred( hook.open_hook, self, container, chunk.get_block((smallx, container.y, smallz))) if window: self.write_packet("window-open", wid=window.wid, type=window.identifier, title=window.title, slots=window.slots_num) packet = window.save_to_packet() self.transport.write(packet) # window opened return # Ignore clients that think -1 is placeable. if container.primary == -1: return # Special case when face is "noop": Update the status of the currently # held block rather than placing a new block. if container.face == "noop": return if container.primary in blocks: block = blocks[container.primary] elif container.primary in items: block = items[container.primary] else: log.err("Ignoring request to place unknown block %d" % container.primary) return # it's the top of the world, you can't build here if container.y == 127 and container.face == '+y': return # Run pre-build hooks. These hooks are able to interrupt the build # process. builddata = BuildData(block, 0x0, container.x, container.y, container.z, container.face) for hook in self.pre_build_hooks: cont, builddata, cancel = yield maybeDeferred( hook.pre_build_hook, self.player, builddata) if cancel: # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) return if not cont: break # Run the build. try: yield maybeDeferred(self.run_build, builddata) except BuildError: return newblock = builddata.block.slot coords = adjust_coords_for_face( (builddata.x, builddata.y, builddata.z), builddata.face) # Run post-build hooks. These are merely callbacks which cannot # interfere with the build process, largely because the build process # already happened. for hook in self.post_build_hooks: yield maybeDeferred(hook.post_build_hook, self.player, coords, builddata.block) # Feed automatons. for automaton in self.factory.automatons: if newblock in automaton.blocks: automaton.feed(coords) # Re-send inventory. # XXX this could be optimized if/when inventories track damage. packet = self.inventory.save_to_packet() self.transport.write(packet) # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk)
def build(self, container): """ Handle a build packet. Several things must happen. First, the packet's contents need to be examined to ensure that the packet is valid. A check is done to see if the packet is opening a windowed object. If not, then a build is run. """ # Is the target within our purview? We don't do a very strict # containment check, but we *do* require that the chunk be loaded. bigx, smallx, bigz, smallz = split_coords(container.x, container.z) try: chunk = self.chunks[bigx, bigz] except KeyError: self.error("Couldn't select in chunk (%d, %d)!" % (bigx, bigz)) return target = blocks[chunk.get_block((smallx, container.y, smallz))] # Attempt to open a window. from bravo.policy.windows import window_for_block window = window_for_block(target) if window is not None: # We have a window! self.windows[self.wid] = window identifier, title, slots = window.open() self.write_packet("window-open", wid=self.wid, type=identifier, title=title, slots=slots) self.wid += 1 return # Try to open it first for hook in self.open_hooks: window = yield maybeDeferred(hook.open_hook, self, container, chunk.get_block((smallx, container.y, smallz))) if window: self.write_packet("window-open", wid=window.wid, type=window.identifier, title=window.title, slots=window.slots_num) packet = window.save_to_packet() self.transport.write(packet) # window opened return # Ignore clients that think -1 is placeable. if container.primary == -1: return # Special case when face is "noop": Update the status of the currently # held block rather than placing a new block. if container.face == "noop": return # If the target block is vanishable, then adjust our aim accordingly. if target.vanishes: container.face = "+y" container.y -= 1 if container.primary in blocks: block = blocks[container.primary] elif container.primary in items: block = items[container.primary] else: log.err("Ignoring request to place unknown block 0x%x" % container.primary) return # Run pre-build hooks. These hooks are able to interrupt the build # process. builddata = BuildData(block, 0x0, container.x, container.y, container.z, container.face) for hook in self.pre_build_hooks: cont, builddata, cancel = yield maybeDeferred(hook.pre_build_hook, self.player, builddata) if cancel: # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk) return if not cont: break # Run the build. try: yield maybeDeferred(self.run_build, builddata) except BuildError: return newblock = builddata.block.slot coords = adjust_coords_for_face( (builddata.x, builddata.y, builddata.z), builddata.face) # Run post-build hooks. These are merely callbacks which cannot # interfere with the build process, largely because the build process # already happened. for hook in self.post_build_hooks: yield maybeDeferred(hook.post_build_hook, self.player, coords, builddata.block) # Feed automatons. for automaton in self.factory.automatons: if newblock in automaton.blocks: automaton.feed(coords) # Re-send inventory. # XXX this could be optimized if/when inventories track damage. packet = self.inventory.save_to_packet() self.transport.write(packet) # Flush damaged chunks. for chunk in self.chunks.itervalues(): self.factory.flush_chunk(chunk)