Source code for bravo.beta.protocol

# vim: set fileencoding=utf8 :

from itertools import product, chain
import json
from time import time
from urlparse import urlunparse

from twisted.internet import reactor
from twisted.internet.defer import (DeferredList, inlineCallbacks,
                                    maybeDeferred, succeed)
from twisted.internet.protocol import Protocol, connectionDone
from twisted.internet.task import cooperate, deferLater, LoopingCall
from twisted.internet.task import TaskDone, TaskFailed
from twisted.protocols.policies import TimeoutMixin
from twisted.python import log
from twisted.web.client import getPage

from bravo import version
from bravo.beta.structures import BuildData, Settings
from bravo.blocks import blocks, items
from bravo.chunk import CHUNK_HEIGHT
from bravo.entity import Sign
from bravo.errors import BetaClientError, BuildError
from bravo.ibravo import (IChatCommand, IPreBuildHook, IPostBuildHook,
    IWindowOpenHook, IWindowClickHook, IWindowCloseHook,
    IPreDigHook, IDigHook, ISignHook, IUseHook)
from bravo.infini.factory import InfiniClientFactory
from bravo.inventory.windows import InventoryWindow
from bravo.location import Location, Orientation, Position
from bravo.motd import get_motd
from bravo.beta.packets import parse_packets, make_packet, make_error_packet
from bravo.plugin import retrieve_plugins
from bravo.policy.dig import dig_policies
from bravo.utilities.coords import adjust_coords_for_face, split_coords
from bravo.utilities.chat import complete, username_alternatives
from bravo.utilities.maths import circling, clamp, sorted_by_distance
from bravo.utilities.temporal import timestamp_from_clock

# States of the protocol.
(STATE_UNAUTHENTICATED, STATE_AUTHENTICATED, STATE_LOCATED) = range(3)

SUPPORTED_PROTOCOL = 78

[docs]class BetaServerProtocol(object, Protocol, TimeoutMixin): """ The Minecraft Alpha/Beta server protocol. This class is mostly designed to be a skeleton for featureful clients. It tries hard to not step on the toes of potential subclasses. """ excess = "" packet = None state = STATE_UNAUTHENTICATED buf = "" parser = None handler = None player = None username = None settings = Settings() motd = "Bravo Generic Beta Server" _health = 20 _latency = 0 def __init__(self): self.chunks = dict() self.windows = {} self.wid = 1 self.location = Location() self.handlers = { 0x00: self.ping, 0x02: self.handshake, 0x03: self.chat, 0x07: self.use, 0x09: self.respawn, 0x0a: self.grounded, 0x0b: self.position, 0x0c: self.orientation, 0x0d: self.location_packet, 0x0e: self.digging, 0x0f: self.build, 0x10: self.equip, 0x12: self.animate, 0x13: self.action, 0x15: self.pickup, 0x65: self.wclose, 0x66: self.waction, 0x6a: self.wacknowledge, 0x6b: self.wcreative, 0x82: self.sign, 0xca: self.client_settings, 0xcb: self.complete, 0xcc: self.settings_packet, 0xfe: self.poll, 0xff: self.quit, } self._ping_loop = LoopingCall(self.update_ping) self.setTimeout(30) # Low-level packet handlers # Try not to hook these if possible, since they offer no convenient # abstractions or protections.
[docs] def ping(self, container): """ Hook for ping packets. By default, this hook will examine the timestamps on incoming pings, and use them to estimate the current latency of the connected client. """ now = timestamp_from_clock(reactor) then = container.pid self.latency = now - then
[docs] def handshake(self, container): """ Hook for handshake packets. Override this to customize how logins are handled. By default, this method will only confirm that the negotiated wire protocol is the correct version, copy data out of the packet and onto the protocol, and then run the ``authenticated`` callback. This method will call the ``pre_handshake`` method hook prior to logging in the client. """ self.username = container.username if container.protocol < SUPPORTED_PROTOCOL: # Kick old clients. self.error("This server doesn't support your ancient client.") return elif container.protocol > SUPPORTED_PROTOCOL: # Kick new clients. self.error("This server doesn't support your newfangled client.") return log.msg("Handshaking with client, protocol version %d" % container.protocol) if not self.pre_handshake(): log.msg("Pre-handshake hook failed; kicking client") self.error("You failed the pre-handshake hook.") return players = min(self.factory.limitConnections, 20) self.write_packet("login", eid=self.eid, leveltype="default", mode=self.factory.mode, dimension=self.factory.world.dimension, difficulty="peaceful", unused=0, maxplayers=players) self.authenticated()
[docs] def pre_handshake(self): """ Whether this client should be logged in. """ return True
[docs] def chat(self, container): """ Hook for chat packets. """
[docs] def use(self, container): """ Hook for use packets. """
[docs] def respawn(self, container): """ Hook for respawn packets. """
[docs] def grounded(self, container): """ Hook for grounded packets. """ self.location.grounded = bool(container.grounded)
[docs] def position(self, container): """ Hook for position packets. """ # Refuse to handle any new position information while we are # relocating. Clients mess this up frequently, and it's fairly racy, # so don't consider this to be exceptional. Just ignore this one # packet and continue. if self.state != STATE_LOCATED: return self.grounded(container.grounded) old_position = self.location.pos position = Position.from_player(container.position.x, container.position.y, container.position.z) altered = False dx, dy, dz = old_position - position if any(abs(d) >= 64 for d in (dx, dy, dz)): # Whoa, slow down there, cowboy. You're moving too fast. We're # gonna ignore this position change completely, because it's # either bogus or ignoring a recent teleport. altered = True else: self.location.pos = position self.location.stance = container.position.stance # Santitize location. This handles safety boundaries, illegal stance, # etc. altered = self.location.clamp() or altered # If, for any reason, our opinion on where the client should be # located is different than theirs, force them to conform to our point # of view. if altered: log.msg("Not updating bogus position!") self.update_location() # If our position actually changed, fire the position change hook. if old_position != position: self.position_changed()
[docs] def orientation(self, container): """ Hook for orientation packets. """ self.grounded(container.grounded) old_orientation = self.location.ori orientation = Orientation.from_degs(container.orientation.rotation, container.orientation.pitch) self.location.ori = orientation if old_orientation != orientation: self.orientation_changed()
[docs] def location_packet(self, container): """ Hook for location packets. """ self.position(container) self.orientation(container)
[docs] def digging(self, container): """ Hook for digging packets. """
[docs] def build(self, container): """ Hook for build packets. """
[docs] def equip(self, container): """ Hook for equip packets. """
[docs] def pickup(self, container): """ Hook for pickup packets. """
[docs] def animate(self, container): """ Hook for animate packets. """
[docs] def action(self, container): """ Hook for action packets. """
[docs] def wclose(self, container): """ Hook for wclose packets. """
[docs] def waction(self, container): """ Hook for waction packets. """
[docs] def wacknowledge(self, container): """ Hook for wacknowledge packets. """
[docs] def wcreative(self, container): """ Hook for creative inventory action packets. """
[docs] def sign(self, container): """ Hook for sign packets. """
[docs] def client_settings(self, container): """ Hook for interaction setting packets. """ self.settings.update_interaction(container)
[docs] def complete(self, container): """ Hook for tab-completion packets. """
[docs] def settings_packet(self, container): """ Hook for presentation setting packets. """ self.settings.update_presentation(container)
[docs] def poll(self, container): """ Hook for poll packets. By default, queries the parent factory for some data, and replays it in a specific format to the requester. The connection is then closed at both ends. This functionality is used by Beta 1.8 clients to poll servers for status. """ log.msg("Poll data: %r" % container.data) players = unicode(len(self.factory.protocols)) max_players = unicode(self.factory.limitConnections or 1000000) data = [ u"ยง1", unicode(SUPPORTED_PROTOCOL), u"Bravo %s" % version, self.motd, players, max_players, ] response = u"\u0000".join(data) self.error(response)
[docs] def quit(self, container): """ Hook for quit packets. By default, merely logs the quit message and drops the connection. Even if the connection is not dropped, it will be lost anyway since the client will close the connection. It's better to explicitly let it go here than to have zombie protocols. """ log.msg("Client is quitting: %s" % container.message) self.transport.loseConnection() # Twisted-level data handlers and methods # Please don't override these needlessly, as they are pretty solid and # shouldn't need to be touched.
def dataReceived(self, data): self.buf += data packets, self.buf = parse_packets(self.buf) if packets: self.resetTimeout() for header, payload in packets: if header in self.handlers: d = maybeDeferred(self.handlers[header], payload) @d.addErrback def eb(failure): log.err("Error while handling packet 0x%.2x" % header) log.err(failure) return None else: log.err("Didn't handle parseable packet 0x%.2x!" % header) log.err(payload) def connectionLost(self, reason=connectionDone): if self._ping_loop.running: self._ping_loop.stop() def timeoutConnection(self): self.error("Connection timed out") # State-change callbacks # Feel free to override these, but call them at some point.
[docs] def authenticated(self): """ Called when the client has successfully authenticated with the server. """ self.state = STATE_AUTHENTICATED self._ping_loop.start(30) # Event callbacks # These are meant to be overriden.
[docs] def orientation_changed(self): """ Called when the client moves. This callback is only for orientation, not position. """ pass
[docs] def position_changed(self): """ Called when the client moves. This callback is only for position, not orientation. """ pass # Convenience methods for consolidating code and expressing intent. I # hear that these are occasionally useful. If a method in this section can # be used, then *PLEASE* use it; not using it is the same as open-coding # whatever you're doing, and only hurts in the long run.
[docs] def write_packet(self, header, **payload): """ Send a packet to the client. """ self.transport.write(make_packet(header, **payload))
[docs] def update_ping(self): """ Send a keepalive to the client. """ timestamp = timestamp_from_clock(reactor) self.write_packet("ping", pid=timestamp)
[docs] def update_location(self): """ Send this client's location to the client. Also let other clients know where this client is. """ # Don't bother trying to update things if the position's not yet # synchronized. We could end up jettisoning them into the void. if self.state != STATE_LOCATED: return x, y, z = self.location.pos yaw, pitch = self.location.ori.to_fracs() # Inform everybody of our new location. packet = make_packet("teleport", eid=self.player.eid, x=x, y=y, z=z, yaw=yaw, pitch=pitch) self.factory.broadcast_for_others(packet, self) # Inform ourselves of our new location. packet = self.location.save_to_packet() self.transport.write(packet)
[docs] 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
[docs] def error(self, message): """ Error out. This method sends ``message`` to the client as a descriptive error message, then closes the connection. """ log.msg("Error: %r" % message) self.transport.write(make_error_packet(message)) self.transport.loseConnection()
[docs] 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)
[docs] def send_chat(self, message): """ Send a chat message back to the client. """ data = json.dumps({"text": message}) self.write_packet("chat", message=data) # Automatic properties. Assigning to them causes the client to be notified # of changes.
@property def health(self): return self._health @health.setter def health(self, value): if not 0 <= value <= 20: raise BetaClientError("Invalid health value %d" % value) if self._health != value: self.write_packet("health", hp=value, fp=0, saturation=0) self._health = value @property def latency(self): return self._latency @latency.setter def latency(self, value): # Clamp the value to not exceed the boundaries of the packet. This is # necessary even though, in theory, a ping this high is bad news. value = clamp(value, 0, 65535) # Check to see if this is a new value, and if so, alert everybody. if self._latency != value: packet = make_packet("players", name=self.username, online=True, ping=value) self.factory.broadcast(packet) self._latency = value
[docs]class KickedProtocol(BetaServerProtocol): """ A very simple Beta protocol that helps enforce IP bans, Max Connections, and Max Connections Per IP. This protocol disconnects people as soon as they connect, with a helpful message. """ def __init__(self, reason=None): BetaServerProtocol.__init__(self) if reason: self.reason = reason else: self.reason = ( "This server doesn't like you very much." " I don't like you very much either.") def connectionMade(self): self.error("%s" % self.reason)
[docs]class BetaProxyProtocol(BetaServerProtocol): """ A ``BetaServerProtocol`` that proxies for an InfiniCraft client. """ gateway = "server.wiki.vg" def handshake(self, container): self.write_packet("handshake", username="-") def login(self, container): self.username = container.username self.write_packet("login", protocol=0, username="", seed=0, dimension="earth") url = urlunparse(("http", self.gateway, "/node/0/0/", None, None, None)) d = getPage(url) d.addCallback(self.start_proxy) def start_proxy(self, response): log.msg("Response: %s" % response) log.msg("Starting proxy...") address, port = response.split(":") self.add_node(address, int(port))
[docs] def add_node(self, address, port): """ Add a new node to this client. """ from twisted.internet.endpoints import TCP4ClientEndpoint log.msg("Adding node %s:%d" % (address, port)) endpoint = TCP4ClientEndpoint(reactor, address, port, 5) self.node_factory = InfiniClientFactory() d = endpoint.connect(self.node_factory) d.addCallback(self.node_connected) d.addErrback(self.node_connect_error)
def node_connected(self, protocol): log.msg("Connected new node!") def node_connect_error(self, reason): log.err("Couldn't connect node!") log.err(reason)
[docs]class BravoProtocol(BetaServerProtocol): """ A ``BetaServerProtocol`` suitable for serving MC worlds to clients. This protocol really does need to be hooked up with a ``BravoFactory`` or something very much like it. """ chunk_tasks = None time_loop = None eid = 0 last_dig = None def __init__(self, config, name): BetaServerProtocol.__init__(self) self.config = config self.config_name = "world %s" % name # Retrieve the MOTD. Only needs to be done once. self.motd = self.config.getdefault(self.config_name, "motd", "BravoServer") def register_hooks(self): log.msg("Registering client hooks...") plugin_types = { "open_hooks": IWindowOpenHook, "click_hooks": IWindowClickHook, "close_hooks": IWindowCloseHook, "pre_build_hooks": IPreBuildHook, "post_build_hooks": IPostBuildHook, "pre_dig_hooks": IPreDigHook, "dig_hooks": IDigHook, "sign_hooks": ISignHook, "use_hooks": IUseHook, } for t in plugin_types: setattr(self, t, getattr(self.factory, t)) log.msg("Registering policies...") if self.factory.mode == "creative": self.dig_policy = dig_policies["speedy"] else: self.dig_policy = dig_policies["notchy"] log.msg("Registered client plugin hooks!")
[docs] def pre_handshake(self): """ Set up username and get going. """ if self.username in self.factory.protocols: # This username's already taken; find a new one. for name in username_alternatives(self.username): if name not in self.factory.protocols: self.username = name break else: self.error("Your username is already taken.") return False return True
@inlineCallbacks def authenticated(self): BetaServerProtocol.authenticated(self) # Init player, and copy data into it. self.player = yield self.factory.world.load_player(self.username) self.player.eid = self.eid self.location = self.player.location # Init players' inventory window. self.inventory = InventoryWindow(self.player.inventory) # *Now* we are in our factory's list of protocols. Be aware. self.factory.protocols[self.username] = self # Announce our presence. self.factory.chat("%s is joining the game..." % self.username) packet = make_packet("players", name=self.username, online=True, ping=0) self.factory.broadcast(packet) # Craft our avatar and send it to already-connected other players. packet = make_packet("create", eid=self.player.eid) packet += self.player.save_to_packet() self.factory.broadcast_for_others(packet, self) # And of course spawn all of those players' avatars in our client as # well. for protocol in self.factory.protocols.itervalues(): # Skip over ourselves; otherwise, the client tweaks out and # usually either dies or locks up. if protocol is self: continue self.write_packet("create", eid=protocol.player.eid) packet = protocol.player.save_to_packet() packet += protocol.player.save_equipment_to_packet() self.transport.write(packet) # Send spawn and inventory. spawn = self.factory.world.level.spawn packet = make_packet("spawn", x=spawn[0], y=spawn[1], z=spawn[2]) packet += self.inventory.save_to_packet() self.transport.write(packet) # TODO: Send Abilities (0xca) # TODO: Update Health (0x08) # TODO: Update Experience (0x2b) # Send weather. self.transport.write(self.factory.vane.make_packet()) self.send_initial_chunk_and_location() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(10) def orientation_changed(self): # Bang your head! yaw, pitch = self.location.ori.to_fracs() packet = make_packet("entity-orientation", eid=self.player.eid, yaw=yaw, pitch=pitch) self.factory.broadcast_for_others(packet, self) def position_changed(self): # Send chunks. self.update_chunks() for entity in self.entities_near(2): if entity.name != "Item": continue left = self.player.inventory.add(entity.item, entity.quantity) if left != entity.quantity: if left != 0: # partial collect entity.quantity = left else: packet = make_packet("collect", eid=entity.eid, destination=self.player.eid) packet += make_packet("destroy", count=1, eid=[entity.eid]) self.factory.broadcast(packet) self.factory.destroy_entity(entity) packet = self.inventory.save_to_packet() self.transport.write(packet)
[docs] 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, chunkz = self.location.pos.to_chunk() 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 chat(self, container): # data = json.loads(container.data) log.msg("Chat! %r" % container.data) if container.message.startswith("/"): commands = retrieve_plugins(IChatCommand, factory=self.factory) # Register aliases. for plugin in commands.values(): for alias in plugin.aliases: commands[alias] = plugin params = container.message[1:].split(" ") command = params.pop(0).lower() if command and command in commands: def cb(iterable): for line in iterable: self.send_chat(line) def eb(error): self.send_chat("Error: %s" % error.getErrorMessage()) d = maybeDeferred(commands[command].chat_command, self.username, params) d.addCallback(cb) d.addErrback(eb) else: self.send_chat("Unknown command: %s" % command) else: # Send the message up to the factory to be chatified. message = "<%s> %s" % (self.username, container.message) self.factory.chat(message)
[docs] def use(self, container): """ For each entity in proximity (4 blocks), check if it is the target of this packet and call all hooks that stated interested in this type. """ nearby_players = self.factory.players_near(self.player, 4) for entity in chain(self.entities_near(4), nearby_players): if entity.eid == container.target: for hook in self.use_hooks[entity.name]: hook.use_hook(self.factory, self.player, entity, container.button == 0) break
@inlineCallbacks 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, count=1, 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))
[docs] def run_dig_hooks(self, chunk, coords, block): """ Destroy a block and run the post-destroy dig hooks. """ x, y, z = coords if block.breakable: chunk.destroy(coords) l = [] for hook in self.dig_hooks: l.append(maybeDeferred(hook.dig_hook, chunk, x, y, z, block)) dl = DeferredList(l) dl.addCallback(lambda none: self.factory.flush_chunk(chunk))
@inlineCallbacks
[docs] 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)
def run_build(self, builddata): block, metadata, x, y, z, face = builddata # Don't place items as blocks. if block.slot not in blocks: raise BuildError("Couldn't build item %r as block" % block) # Check for orientable blocks. if not metadata and block.orientable(): metadata = block.orientation(face) if metadata is None: # Oh, I guess we can't even place the block on this face. raise BuildError("Couldn't orient block %r on face %s" % (block, face)) # Make sure we can remove it from the inventory first. if not self.player.inventory.consume((block.slot, 0), self.player.equipped): # Okay, first one was a bust; maybe we can consume the related # block for dropping instead? if not self.player.inventory.consume(block.drop, self.player.equipped): raise BuildError("Couldn't consume %r from inventory" % block) # Offset coords according to face. x, y, z = adjust_coords_for_face((x, y, z), face) # Set the block and data. dl = [self.factory.world.set_block((x, y, z), block.slot)] if metadata: dl.append(self.factory.world.set_metadata((x, y, z), metadata)) return DeferredList(dl) def equip(self, container): self.player.equipped = container.slot # Inform everyone about the item the player is holding now. item = self.player.inventory.holdables[self.player.equipped] if item is None: # Empty slot. Use signed short -1. primary, secondary = -1, 0 else: primary, secondary, count = item packet = make_packet("entity-equipment", eid=self.player.eid, slot=0, primary=primary, count=1, secondary=secondary ) self.factory.broadcast_for_others(packet, self) def pickup(self, container): self.factory.give((container.x, container.y, container.z), (container.primary, container.secondary), container.count) def animate(self, container): # Broadcast the animation of the entity to everyone else. Only swing # arm is send by notchian clients. packet = make_packet("animate", eid=self.player.eid, animation=container.animation ) self.factory.broadcast_for_others(packet, self) def wclose(self, container): wid = container.wid if wid == 0: # WID 0 is reserved for the client inventory. pass elif wid in self.windows: w = self.windows.pop(wid) w.close() else: self.error("WID %d doesn't exist." % wid) def waction(self, container): wid = container.wid if wid in self.windows: w = self.windows[wid] result = w.action(container.slot, container.button, container.token, container.shift, container.primary) self.write_packet("window-token", wid=wid, token=container.token, acknowledged=result) else: self.error("WID %d doesn't exist." % wid)
[docs] def wcreative(self, container): """ A slot was altered in creative mode. """ # XXX Sometimes the container doesn't contain all of this information. # What then? applied = self.inventory.creative(container.slot, container.primary, container.secondary, container.count) if applied: # Inform other players about changes to this player's equipment. equipped_slot = self.player.equipped + 36 if container.slot == equipped_slot: packet = make_packet("entity-equipment", eid=self.player.eid, # XXX why 0? why not the actual slot? slot=0, primary=container.primary, count=1, secondary=container.secondary, ) self.factory.broadcast_for_others(packet, self)
def shoot_arrow(self): # TODO 1. Create arrow entity: arrow = Arrow(self.factory, self.player) # 2. Register within the factory: self.factory.register_entity(arrow) # 3. Run it: arrow.run() pass 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)
[docs] def complete(self, container): """ Attempt to tab-complete user names. """ needle = container.autocomplete usernames = self.factory.protocols.keys() results = complete(needle, usernames) self.write_packet("tab", autocomplete=results)
[docs] def settings_packet(self, container): """ Acknowledge a change of settings and update chunk distance. """ super(BravoProtocol, self).settings_packet(container) self.update_chunks()
def disable_chunk(self, x, z): key = x, z log.msg("Disabling chunk %d, %d" % key) if key not in self.chunks: log.msg("...But the chunk wasn't loaded!") return # Remove the chunk from cache. chunk = self.chunks.pop(key) eids = [e.eid for e in chunk.entities] self.write_packet("destroy", count=len(eids), eid=eids) # Clear chunk data on the client. self.write_packet("chunk", x=x, z=z, continuous=False, primary=0x0, add=0x0, data="")
[docs] def enable_chunk(self, x, z): """ Request a chunk. This function will asynchronously obtain the chunk, and send it on the wire. :returns: `Deferred` that will be fired when the chunk is obtained, with no arguments """ log.msg("Enabling chunk %d, %d" % (x, z)) if (x, z) in self.chunks: log.msg("...But the chunk was already loaded!") return succeed(None) d = self.factory.world.request_chunk(x, z) @d.addCallback def cb(chunk): self.chunks[x, z] = chunk return chunk d.addCallback(self.send_chunk) return d
def send_chunk(self, chunk): log.msg("Sending chunk %d, %d" % (chunk.x, chunk.z)) packet = chunk.save_to_packet() self.transport.write(packet) for entity in chunk.entities: packet = entity.save_to_packet() self.transport.write(packet) for entity in chunk.tiles.itervalues(): if entity.name == "Sign": packet = entity.save_to_packet() self.transport.write(packet)
[docs] 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_chunks(self): # Don't send chunks unless we're located. if self.state != STATE_LOCATED: return x, z = self.location.pos.to_chunk() # 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 update_time(self): time = int(self.factory.time) self.write_packet("time", timestamp=time, time=time % 24000)
[docs] def connectionLost(self, reason=connectionDone): """ Cleanup after a lost connection. Most of the time, these connections are lost cleanly; we don't have any cleanup to do in the unclean case since clients don't have any kind of pending state which must be recovered. Remember, the connection can be lost before identification and authentication, so ``self.username`` and ``self.player`` can be None. """ if self.username and self.player: self.factory.world.save_player(self.username, self.player) if self.player: self.factory.destroy_entity(self.player) packet = make_packet("destroy", count=1, eid=[self.player.eid]) self.factory.broadcast(packet) if self.username: packet = make_packet("players", name=self.username, online=False, ping=0) self.factory.broadcast(packet) self.factory.chat("%s has left the game." % self.username) self.factory.teardown_protocol(self) # We are now torn down. After this point, there will be no more # factory stuff, just our own personal stuff. del self.factory if self.time_loop: self.time_loop.stop() if self.chunk_tasks: for task in self.chunk_tasks: try: task.stop() except (TaskDone, TaskFailed): pass