class BravoFactory(Factory): """ A ``Factory`` that creates ``BravoProtocol`` objects when connected to. """ implements(IPushProducer) protocol = BravoProtocol timestamp = None time = 0 day = 0 eid = 1 handshake_hook = None login_hook = None interface = "" def __init__(self, name): """ Create a factory and world. ``name`` is the string used to look up factory-specific settings from the configuration. :param str name: internal name of this factory """ self.name = name self.config_name = "world %s" % name self.port = configuration.getint(self.config_name, "port") self.interface = configuration.getdefault(self.config_name, "host", "") def startFactory(self): log.msg("Initializing factory for world '%s'..." % self.name) self.world = World(self.name) self.world.factory = self if configuration.has_option(self.config_name, "perm_cache"): cache_level = configuration.getint(self.config_name, "perm_cache") self.world.enable_cache(cache_level) self.protocols = dict() log.msg("Starting timekeeping...") self.timestamp = time() self.time = self.world.time self.update_season() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(2) authenticator = configuration.get(self.config_name, "authenticator") selected = retrieve_named_plugins(IAuthenticator, [authenticator])[0] log.msg("Using authenticator %s" % selected.name) self.handshake_hook = selected.handshake self.login_hook = selected.login generators = configuration.getlist(self.config_name, "generators") generators = retrieve_sorted_plugins(ITerrainGenerator, generators) log.msg("Using generators %s" % ", ".join(i.name for i in generators)) self.world.pipeline = generators automatons = configuration.getlist(self.config_name, "automatons") automatons = retrieve_named_plugins(IAutomaton, automatons) log.msg("Using automatons %s" % ", ".join(i.name for i in automatons)) self.automatons = automatons self.chat_consumers = set() log.msg("Factory successfully initialized for world '%s'!" % self.name) def buildProtocol(self, addr): """ Create a protocol. This overriden method provides early player entity registration, as a solution to the username/entity race that occurs on login. """ banned = self.world.serializer.load_plugin_data("banned_ips") for ip in banned.split(): if addr.host == ip: # Use BannedProtocol with extreme prejudice. log.msg("Kicking banned IP %s" % addr) p = BannedProtocol() p.factory = self return p log.msg("Starting connection for %s" % addr) p = self.protocol(self.name) p.factory = self self.register_entity(p) return p def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity. Handles entity registration as well as instantiation. """ location = Location() location.x = x location.y = y location.z = z entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.add(entity)) d.addCallback(lambda none: log.msg("Created entity %s" % entity)) return entity def register_entity(self, entity): """ Registers an entity with this factory. Registration is perhaps too fancy of a name; this method merely makes sure that the entity has a unique and usable entity ID. """ if not entity.eid: self.eid += 1 entity.eid = self.eid log.msg("Registered entity %s" % entity) def destroy_entity(self, entity): """ Destroy an entity. The factory doesn't have to know about entities, but it is a good place to put this logic. """ bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.discard(entity)) d.addCallback(lambda none: log.msg("Destroyed entity %s" % entity)) def update_time(self): """ Update the in-game timer. The timer goes from 0 to 24000, both of which are high noon. The clock increments by 20 every second. Days are 20 minutes long. The day clock is incremented every in-game day, which is every 20 minutes. The day clock goes from 0 to 360, which works out to a reset once every 5 days. This is a Babylonian in-game year. """ t = time() self.time += 20 * (t - self.timestamp) self.timestamp = t while self.time > 24000: self.time -= 24000 self.day += 1 while self.day > 360: self.day -= 360 self.update_season() def update_season(self): """ Update the world's season. """ plugins = configuration.getlistdefault(self.config_name, "seasons", []) for plugin in retrieve_named_plugins(ISeason, plugins): if plugin.day == self.day: self.world.season = plugin def chat(self, message): """ Relay chat messages. Chat messages are sent to all connected clients, as well as to anybody consuming this factory. """ for consumer in self.chat_consumers: consumer.write((self, message)) # Prepare the message for chat packeting. for user in self.protocols: message = message.replace(user, chat_name(user)) message = sanitize_chat(message) packet = make_packet("chat", message=message) self.broadcast(packet) def broadcast(self, packet): """ Broadcast a packet to all connected players. """ for player in self.protocols.itervalues(): player.transport.write(packet) def broadcast_for_others(self, packet, protocol): """ Broadcast a packet to all players except the originating player. Useful for certain packets like player entity spawns which should never be reflexive. """ for player in self.protocols.itervalues(): if player is not protocol: player.transport.write(packet) def broadcast_for_chunk(self, packet, x, z): """ Broadcast a packet to all players that have a certain chunk loaded. `x` and `z` are chunk coordinates, not block coordinates. """ for player in self.protocols.itervalues(): if (x, z) in player.chunks: player.transport.write(packet) def flush_chunk(self, chunk): """ Flush a damaged chunk to all players that have it loaded. """ if chunk.is_damaged(): packet = chunk.get_damage_packet() for player in self.protocols.itervalues(): if (chunk.x, chunk.z) in player.chunks: player.transport.write(packet) chunk.clear_damage() def give(self, coords, block, quantity): """ Spawn a pickup at the specified coordinates. The coordinates need to be in pixels, not blocks. If the size of the stack is too big, multiple stacks will be dropped. :param tuple coords: coordinates, in pixels :param tuple block: key of block or item to drop :param int quantity: number of blocks to drop in the stack """ x, y, z = coords while quantity > 0: entity = self.create_entity(x // 32, y // 32, z // 32, "Item", item=block, quantity=min(quantity, 64)) packet = entity.save_to_packet() packet += make_packet("create", eid=entity.eid) self.broadcast(packet) quantity -= 64 def players_near(self, player, radius): """ Obtain other players within a radius of a given player. Radius is measured in blocks. """ for i in (p for p in self.protocols.itervalues() if player.location.distance(p.location) <= radius and p.player != player): yield i.player def stopFactory(self): """ Called before factory stops listening on ports. Used to perform shutdown tasks. """ if not self.world.saving: return log.msg("Shutting down; flushing world data...") # Flush all dirty chunks to disk. for chunk in self.world.dirty_chunk_cache.itervalues(): self.world.save_chunk(chunk) # Write back current world time. self.world.time = self.time self.world.serializer.save_level(self.world) log.msg("World data saved!") def pauseProducing(self): pass def resumeProducing(self): pass def stopProducing(self): pass
class TestWorldChunks(unittest.TestCase): def setUp(self): self.name = "unittest" self.bcp = BravoConfigParser() self.bcp.add_section("world unittest") self.bcp.set("world unittest", "url", "") self.bcp.set("world unittest", "serializer", "memory") self.w = World(self.bcp, self.name) self.w.pipeline = [] self.w.start() def tearDown(self): self.w.stop() def test_trivial(self): pass @inlineCallbacks def test_request_chunk_identity(self): first = yield self.w.request_chunk(0, 0) second = yield self.w.request_chunk(0, 0) self.assertIs(first, second) @inlineCallbacks def test_request_chunk_cached_identity(self): # Turn on the cache and get a few chunks in there, then request a # chunk that is in the cache. yield self.w.enable_cache(1) first = yield self.w.request_chunk(0, 0) second = yield self.w.request_chunk(0, 0) self.assertIs(first, second) @inlineCallbacks def test_get_block(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = array("B") chunk.blocks.fromstring(os.urandom(32768)) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = yield self.w.get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_metadata(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.metadata = array("B") chunk.metadata.fromstring(os.urandom(32768)) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. metadata = yield self.w.get_metadata((x, y, z)) self.assertEqual(metadata, chunk.get_metadata((x, y, z))) @inlineCallbacks def test_get_block_readback(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = array("B") chunk.blocks.fromstring(os.urandom(32768)) # Evict the chunk and grab it again. yield self.w.save_chunk(chunk) del chunk chunk = yield self.w.request_chunk(0, 0) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = yield self.w.get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_block_readback_negative(self): chunk = yield self.w.request_chunk(-1, -1) # Fill the chunk with random stuff. chunk.blocks = array("B") chunk.blocks.fromstring(os.urandom(32768)) # Evict the chunk and grab it again. yield self.w.save_chunk(chunk) del chunk chunk = yield self.w.request_chunk(-1, -1) for x, y, z in product(xrange(2), repeat=3): block = yield self.w.get_block((x - 16, y, z - 16)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_metadata_readback(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.metadata = array("B") chunk.metadata.fromstring(os.urandom(32768)) # Evict the chunk and grab it again. yield self.w.save_chunk(chunk) del chunk chunk = yield self.w.request_chunk(0, 0) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. metadata = yield self.w.get_metadata((x, y, z)) self.assertEqual(metadata, chunk.get_metadata((x, y, z))) @inlineCallbacks def test_world_level_mark_chunk_dirty(self): chunk = yield self.w.request_chunk(0, 0) # Reload chunk. yield self.w.save_chunk(chunk) del chunk chunk = yield self.w.request_chunk(0, 0) self.assertFalse(chunk.dirty) self.w.mark_dirty((12, 64, 4)) chunk = yield self.w.request_chunk(0, 0) self.assertTrue(chunk.dirty) @inlineCallbacks def test_world_level_mark_chunk_dirty_offset(self): chunk = yield self.w.request_chunk(1, 2) # Reload chunk. yield self.w.save_chunk(chunk) del chunk chunk = yield self.w.request_chunk(1, 2) self.assertFalse(chunk.dirty) self.w.mark_dirty((29, 64, 43)) chunk = yield self.w.request_chunk(1, 2) self.assertTrue(chunk.dirty) @inlineCallbacks def test_sync_get_block(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = array("B") chunk.blocks.fromstring(os.urandom(32768)) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = self.w.sync_get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) def test_sync_get_block_unloaded(self): self.assertRaises(ChunkNotLoaded, self.w.sync_get_block, (0, 0, 0)) def test_sync_get_metadata_neighboring(self): """ Even if a neighboring chunk is loaded, the target chunk could still be unloaded. Test with sync_get_metadata() to increase test coverage. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertRaises(ChunkNotLoaded, self.w.sync_get_metadata, (16, 0, 0)) return d
class TestGrass(TestCase): def setUp(self): self.bcp = BravoConfigParser() self.bcp.add_section("world unittest") self.bcp.set("world unittest", "url", "") self.bcp.set("world unittest", "serializer", "memory") self.w = World(self.bcp, "unittest") self.w.pipeline = [] self.w.start() self.f = GrassMockFactory() self.f.world = self.w self.w.factory = self.f plugins = retrieve_plugins(IAutomaton, factory=self.f) self.hook = plugins["grass"] def tearDown(self): self.w.stop() def test_trivial(self): pass @inlineCallbacks def test_not_dirt(self): """ Blocks which aren't dirt by the time they're processed will be ignored. """ chunk = yield self.w.request_chunk(0, 0) chunk.set_block((0, 0, 0), blocks["bedrock"].slot) # Run the loop once. self.hook.feed((0, 0, 0)) self.hook.process() # We shouldn't have any pending blocks now. self.assertFalse(self.hook.tracked) @inlineCallbacks def test_unloaded_chunk(self): """ The grass automaton can't load chunks, so it will stop tracking blocks on the edge of the loaded world. """ chunk = yield self.w.request_chunk(0, 0) chunk.set_block((0, 0, 0), blocks["dirt"].slot) # Run the loop once. self.hook.feed((0, 0, 0)) self.hook.process() # We shouldn't have any pending blocks now. self.assertFalse(self.hook.tracked) @inlineCallbacks def test_surrounding(self): """ When surrounded by eight grassy neighbors, dirt should turn into grass immediately. """ chunk = yield self.w.request_chunk(0, 0) # Set up grassy surroundings. for x, z in product(xrange(0, 3), repeat=2): chunk.set_block((x, 0, z), blocks["grass"].slot) # Our lone Cinderella. chunk.set_block((1, 0, 1), blocks["dirt"].slot) # Do the actual hook run. This should take exactly one run. self.hook.feed((1, 0, 1)) self.hook.process() self.assertFalse(self.hook.tracked) self.assertEqual(chunk.get_block((1, 0, 1)), blocks["grass"].slot) def test_surrounding_not_dirt(self): """ Blocks which aren't dirt by the time they're processed will be ignored, even when surrounded by grass. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): # Set up grassy surroundings. for x, z in product(xrange(0, 3), repeat=2): chunk.set_block((x, 0, z), blocks["grass"].slot) chunk.set_block((1, 0, 1), blocks["bedrock"].slot) # Run the loop once. self.hook.feed((1, 0, 1)) self.hook.process() # We shouldn't have any pending blocks now. self.assertFalse(self.hook.tracked) return d @inlineCallbacks def test_surrounding_obstructed(self): """ Grass can't grow on blocks which have other blocks on top of them. """ chunk = yield self.w.request_chunk(0, 0) # Set up grassy surroundings. for x, z in product(xrange(0, 3), repeat=2): chunk.set_block((x, 0, z), blocks["grass"].slot) # Put an obstruction on top. chunk.set_block((1, 1, 1), blocks["stone"].slot) # Our lone Cinderella. chunk.set_block((1, 0, 1), blocks["dirt"].slot) # Do the actual hook run. This should take exactly one run. self.hook.feed((1, 0, 1)) self.hook.process() self.assertFalse(self.hook.tracked) self.assertEqual(chunk.get_block((1, 0, 1)), blocks["dirt"].slot) @inlineCallbacks def test_above(self): """ Grass spreads downwards. """ chunk = yield self.w.request_chunk(0, 0) # Set up grassy surroundings. for x, z in product(xrange(0, 3), repeat=2): chunk.set_block((x, 1, z), blocks["grass"].slot) chunk.destroy((1, 1, 1)) # Our lone Cinderella. chunk.set_block((1, 0, 1), blocks["dirt"].slot) # Do the actual hook run. This should take exactly one run. self.hook.feed((1, 0, 1)) self.hook.process() self.assertFalse(self.hook.tracked) self.assertEqual(chunk.get_block((1, 0, 1)), blocks["grass"].slot) def test_two_of_four(self): """ Grass should eventually spread to all filled-in plots on a 2x2 grid. Discovered by TkTech. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for x, y, z in product(xrange(0, 4), xrange(0, 2), xrange(0, 4)): chunk.set_block((x, y, z), blocks["grass"].slot) for x, z in product(xrange(1, 3), repeat=2): chunk.set_block((x, 1, z), blocks["dirt"].slot) self.hook.feed((1, 1, 1)) self.hook.feed((2, 1, 1)) self.hook.feed((1, 1, 2)) self.hook.feed((2, 1, 2)) # Run to completion. This can take varying amounts of time # depending on the RNG, but it should be fairly speedy. # XXX patch the RNG so we can do this deterministically while self.hook.tracked: self.hook.process() self.assertEqual(chunk.get_block((1, 1, 1)), blocks["grass"].slot) self.assertEqual(chunk.get_block((2, 1, 1)), blocks["grass"].slot) self.assertEqual(chunk.get_block((1, 1, 2)), blocks["grass"].slot) self.assertEqual(chunk.get_block((2, 1, 2)), blocks["grass"].slot)
class BravoFactory(Factory): """ A ``Factory`` that creates ``BravoProtocol`` objects when connected to. """ implements(IPushProducer) protocol = BravoProtocol timestamp = None time = 0 day = 0 handshake_hook = None login_hook = None interface = "" def __init__(self, name): """ Create a factory and world. ``name`` is the string used to look up factory-specific settings from the configuration. :param str name: internal name of this factory """ log.msg("Initializing factory for world '%s'..." % name) self.name = name self.config_name = "world %s" % name self.port = configuration.getint(self.config_name, "port") self.interface = configuration.getdefault(self.config_name, "host", "") self.world = World(name) self.world.factory = self if configuration.has_option(self.config_name, "perm_cache"): cache_level = configuration.getint(self.config_name, "perm_cache") self.world.enable_cache(cache_level) self.protocols = dict() self.eid = 1 self.time = self.world.time self.time_loop = LoopingCall(self.update_time) self.time_loop.start(2) authenticator = configuration.get(self.config_name, "authenticator") selected = retrieve_named_plugins(IAuthenticator, [authenticator])[0] log.msg("Using authenticator %s" % selected.name) self.handshake_hook = selected.handshake self.login_hook = selected.login generators = configuration.getlist(self.config_name, "generators") generators = retrieve_sorted_plugins(ITerrainGenerator, generators) log.msg("Using generators %s" % ", ".join(i.name for i in generators)) self.world.pipeline = generators self.chat_consumers = set() log.msg("Factory successfully initialized for world '%s'!" % name) def buildProtocol(self, addr): """ Create a protocol. This overriden method provides early player entity registration, as a solution to the username/entity race that occurs on login. """ banned = self.world.serializer.load_plugin_data("banned_ips") for ip in banned.split(): if addr.host == ip: # Use BannedProtocol with extreme prejudice. log.msg("Kicking banned IP %s" % addr) p = BannedProtocol() p.factory = self return p log.msg("Starting connection for %s" % addr) p = self.protocol(self.name) p.factory = self self.register_entity(p) return p def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity. Handles entity registration as well as instantiation. """ location = Location() location.x = x location.y = y location.z = z entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.add(entity)) d.addCallback(lambda none: log.msg("Created entity %s" % entity)) return entity def register_entity(self, entity): """ Registers an entity with this factory. Registration is perhaps too fancy of a name; this method merely makes sure that the entity has a unique and usable entity ID. """ if not entity.eid: self.eid += 1 entity.eid = self.eid log.msg("Registered entity %s" % entity) def destroy_entity(self, entity): """ Destroy an entity. The factory doesn't have to know about entities, but it is a good place to put this logic. """ bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.discard(entity)) d.addCallback(lambda none: log.msg("Destroyed entity %s" % entity)) def update_time(self): """ Update the in-game timer. The timer goes from 0 to 24000, both of which are high noon. The clock increments by 20 every second. Days are 20 minutes long. The day clock is incremented every in-game day, which is every 20 minutes. The day clock goes from 0 to 360, which works out to a reset once every 5 days. This is a Babylonian in-game year. """ if self.timestamp is None: # First run since the start of the factory; re-init everything. self.timestamp = time() self.update_season() t = time() self.time += 20 * (t - self.timestamp) self.timestamp = t while self.time > 24000: self.time -= 24000 self.day += 1 while self.day > 360: self.day -= 360 self.update_season() def update_season(self): """ Update the world's season. """ plugins = configuration.getlistdefault(self.config_name, "seasons", []) for plugin in retrieve_named_plugins(ISeason, plugins): if plugin.day == self.day: self.world.season = plugin def chat(self, message): """ Relay chat messages. Chat messages are sent to all connected clients, as well as to anybody consuming this factory. """ for consumer in self.chat_consumers: consumer.write((self, message)) # Prepare the message for chat packeting. for user in self.protocols: message = message.replace(user, chat_name(user)) message = sanitize_chat(message) packet = make_packet("chat", message=message) self.broadcast(packet) def broadcast(self, packet): """ Broadcast a packet to all connected players. """ for player in self.protocols.itervalues(): player.transport.write(packet) def broadcast_for_others(self, packet, protocol): """ Broadcast a packet to all players except the originating player. Useful for certain packets like player entity spawns which should never be reflexive. """ for player in self.protocols.itervalues(): if player is not protocol: player.transport.write(packet) def broadcast_for_chunk(self, packet, x, z): """ Broadcast a packet to all players that have a certain chunk loaded. `x` and `z` are chunk coordinates, not block coordinates. """ for player in self.protocols.itervalues(): if (x, z) in player.chunks: player.transport.write(packet) def flush_chunk(self, chunk): """ Flush a damaged chunk to all players that have it loaded. """ if chunk.is_damaged(): packet = chunk.get_damage_packet() for player in self.protocols.itervalues(): if (chunk.x, chunk.z) in player.chunks: player.transport.write(packet) chunk.clear_damage() def give(self, coords, block, quantity): """ Spawn a pickup at the specified coordinates. The coordinates need to be in pixels, not blocks. If the size of the stack is too big, multiple stacks will be dropped. :param tuple coords: coordinates, in pixels :param tuple block: key of block or item to drop :param int quantity: number of blocks to drop in the stack """ x, y, z = coords while quantity > 0: entity = self.create_entity(x // 32, y // 32, z // 32, "Item", item=block, quantity=min(quantity, 64)) packet = entity.save_to_packet() packet += make_packet("create", eid=entity.eid) self.broadcast(packet) quantity -= 64 def players_near(self, player, radius): """ Obtain other players within a radius of a given player. Radius is measured in blocks. """ for i in (p for p in self.protocols.itervalues() if player.location.distance(p.location) <= radius and p.player != player): yield i.player def stopFactory(self): """ Called before factory stops listening on ports. Used to perform shutdown tasks. """ if not self.world.saving: return log.msg("Shutting down; flushing world data...") # Flush all dirty chunks to disk. for chunk in self.world.dirty_chunk_cache.itervalues(): self.world.save_chunk(chunk) # Write back current world time. self.world.time = self.time self.world.serializer.save_level(self.world) log.msg("World data saved!") def pauseProducing(self): pass def resumeProducing(self): pass def stopProducing(self): pass
class BravoFactory(Factory): """ A ``Factory`` that creates ``BravoProtocol`` objects when connected to. """ implements(IPushProducer) protocol = BravoProtocol timestamp = None time = 0 day = 0 eid = 1 handshake_hook = None login_hook = None interfaces = [] def __init__(self, config, name): """ Create a factory and world. ``name`` is the string used to look up factory-specific settings from the configuration. :param str name: internal name of this factory """ self.name = name self.config = config self.config_name = "world %s" % name self.world = World(self.config, self.name) self.world.factory = self self.protocols = dict() self.connectedIPs = defaultdict(int) self.mode = self.config.get(self.config_name, "mode") if self.mode not in ("creative", "survival"): raise Exception("Unsupported mode %s" % self.mode) self.limitConnections = self.config.getintdefault(self.config_name, "limitConnections", 0) self.limitPerIP = self.config.getintdefault(self.config_name, "limitPerIP", 0) self.vane = WeatherVane(self) def startFactory(self): log.msg("Initializing factory for world '%s'..." % self.name) authenticator = self.config.get(self.config_name, "authenticator") selected = retrieve_named_plugins(IAuthenticator, [authenticator])[0] log.msg("Using authenticator %s" % selected.name) self.handshake_hook = selected.handshake self.login_hook = selected.login # Get our plugins set up. self.register_plugins() log.msg("Starting world...") self.world.start() # Start up the permanent cache. # has_option() is not exactly desirable, but it's appropriate here # because we don't want to take any action if the key is unset. if self.config.has_option(self.config_name, "perm_cache"): cache_level = self.config.getint(self.config_name, "perm_cache") self.world.enable_cache(cache_level) log.msg("Starting timekeeping...") self.timestamp = reactor.seconds() self.time = self.world.time self.update_season() self.time_loop = LoopingCall(self.update_time) self.time_loop.start(2) log.msg("Starting entity updates...") # Start automatons. for automaton in self.automatons: automaton.start() self.chat_consumers = set() log.msg("Factory successfully initialized for world '%s'!" % self.name) def stopFactory(self): """ Called before factory stops listening on ports. Used to perform shutdown tasks. """ log.msg("Shutting down world...") # Stop automatons. Technically, they may not actually halt until their # next iteration, but that is close enough for us, probably. # Automatons are contracted to not access the world after stop() is # called. for automaton in self.automatons: automaton.stop() # Evict plugins as soon as possible. Can't be done before stopping # automatons. self.unregister_plugins() self.time_loop.stop() # Write back current world time. This must be done before stopping the # world. self.world.time = self.time # And now stop the world. self.world.stop() log.msg("World data saved!") def buildProtocol(self, addr): """ Create a protocol. This overriden method provides early player entity registration, as a solution to the username/entity race that occurs on login. """ banned = self.world.serializer.load_plugin_data("banned_ips") # Do IP bans first. for ip in banned.split(): if addr.host == ip: # Use KickedProtocol with extreme prejudice. log.msg("Kicking banned IP %s" % addr.host) p = KickedProtocol("Sorry, but your IP address is banned.") p.factory = self return p # We are ignoring values less that 1, but making sure not to go over # the connection limit. if (self.limitConnections and len(self.protocols) >= self.limitConnections): log.msg("Reached maximum players, turning %s away." % addr.host) p = KickedProtocol("The player limit has already been reached." " Please try again later.") p.factory = self return p # Do our connection-per-IP check. if (self.limitPerIP and self.connectedIPs[addr.host] >= self.limitPerIP): log.msg("At maximum connections for %s already, dropping." % addr.host) p = KickedProtocol("There are too many players connected from this IP.") p.factory = self return p else: self.connectedIPs[addr.host] += 1 # If the player wasn't kicked, let's continue! log.msg("Starting connection for %s" % addr) p = self.protocol(self.config, self.name) p.host = addr.host p.factory = self self.register_entity(p) # Copy our hooks to the protocol. p.register_hooks() return p def teardown_protocol(self, protocol): """ Do internal bookkeeping on behalf of a protocol which has been disconnected. Did you know that "bookkeeping" is one of the few words in English which has three pairs of double letters in a row? """ username = protocol.username host = protocol.host if username in self.protocols: del self.protocols[username] self.connectedIPs[host] -= 1 def set_username(self, protocol, username): """ Attempt to set a new username for a protocol. :returns: whether the username was changed """ # If the username's already taken, refuse it. if username in self.protocols: return False if protocol.username in self.protocols: # This protocol's known under another name, so remove it. del self.protocols[protocol.username] # Set the username. self.protocols[username] = protocol protocol.username = username return True def register_plugins(self): """ Setup plugin hooks. """ log.msg("Registering client plugin hooks...") plugin_types = { "automatons": IAutomaton, "generators": ITerrainGenerator, "seasons": ISeason, "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, } pp = {"factory": self} for t, interface in plugin_types.iteritems(): l = self.config.getlistdefault(self.config_name, t, []) if issubclass(interface, ISortedPlugin): plugins = retrieve_sorted_plugins(interface, l, parameters=pp) else: plugins = retrieve_named_plugins(interface, l, parameters=pp) log.msg("Using %s: %s" % (t.replace("_", " "), ", ".join(plugin.name for plugin in plugins))) setattr(self, t, plugins) # Assign generators to the world pipeline. self.world.pipeline = self.generators # Use hooks have special funkiness. uh = self.use_hooks self.use_hooks = defaultdict(list) for plugin in uh: for target in plugin.targets: self.use_hooks[target].append(plugin) def unregister_plugins(self): log.msg("Unregistering client plugin hooks...") for name in [ "automatons", "generators", "seasons", "open_hooks", "click_hooks", "close_hooks", "pre_build_hooks", "post_build_hooks", "dig_hooks", "sign_hooks", "use_hooks", ]: delattr(self, name) def create_entity(self, x, y, z, name, **kwargs): """ Spawn an entirely new entity. Handles entity registration as well as instantiation. """ location = Location() location.x = x location.y = y location.z = z entity = entities[name](eid=0, location=location, **kwargs) self.register_entity(entity) bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) d.addCallback(lambda chunk: chunk.entities.add(entity)) d.addCallback(lambda none: log.msg("Created entity %s" % entity)) if hasattr(entity,'loop'): # XXX Maybe just send the entity object to the manager? self.world.mob_manager.start_mob(entity) return entity def register_entity(self, entity): """ Registers an entity with this factory. Registration is perhaps too fancy of a name; this method merely makes sure that the entity has a unique and usable entity ID. """ if not entity.eid: self.eid += 1 entity.eid = self.eid log.msg("Registered entity %s" % entity) def destroy_entity(self, entity): """ Destroy an entity. The factory doesn't have to know about entities, but it is a good place to put this logic. """ bigx = entity.location.x // 16 bigz = entity.location.z // 16 d = self.world.request_chunk(bigx, bigz) @d.addCallback def cb(chunk): chunk.entities.discard(entity) chunk.dirty = True log.msg("Destroyed entity %s" % entity) def update_time(self): """ Update the in-game timer. The timer goes from 0 to 24000, both of which are high noon. The clock increments by 20 every second. Days are 20 minutes long. The day clock is incremented every in-game day, which is every 20 minutes. The day clock goes from 0 to 360, which works out to a reset once every 5 days. This is a Babylonian in-game year. """ t = reactor.seconds() self.time += 20 * (t - self.timestamp) self.timestamp = t days, self.time = divmod(self.time, 24000) if days: self.day += days self.day %= 360 self.update_season() def broadcast_time(self): packet = make_packet("time", timestamp=int(self.time)) self.broadcast(packet) def update_season(self): """ Update the world's season. """ all_seasons = sorted(self.seasons, key=lambda s: s.day) # Get all the seasons that we have past the start date of this year. # We are looking for the season which is closest to our current day, # without going over; I call this the Price-is-Right style of season # handling. :3 past_seasons = [s for s in all_seasons if s.day <= self.day] if past_seasons: # The most recent one is the one we are in self.world.season = past_seasons[-1] elif all_seasons: # We haven't past any seasons yet this year, so grab the last one # from 'last year' self.world.season = all_seasons[-1] else: # No seasons enabled. self.world.season = None def chat(self, message): """ Relay chat messages. Chat messages are sent to all connected clients, as well as to anybody consuming this factory. """ for consumer in self.chat_consumers: consumer.write((self, message)) # Prepare the message for chat packeting. for user in self.protocols: message = message.replace(user, chat_name(user)) message = sanitize_chat(message) log.msg("Chat: %s" % message.encode("utf8")) packet = make_packet("chat", message=message) self.broadcast(packet) def broadcast(self, packet): """ Broadcast a packet to all connected players. """ for player in self.protocols.itervalues(): player.transport.write(packet) def broadcast_for_others(self, packet, protocol): """ Broadcast a packet to all players except the originating player. Useful for certain packets like player entity spawns which should never be reflexive. """ for player in self.protocols.itervalues(): if player is not protocol: player.transport.write(packet) def broadcast_for_chunk(self, packet, x, z): """ Broadcast a packet to all players that have a certain chunk loaded. `x` and `z` are chunk coordinates, not block coordinates. """ for player in self.protocols.itervalues(): if (x, z) in player.chunks: player.transport.write(packet) def scan_chunk(self, chunk): """ Tell automatons about this chunk. """ for automaton in self.automatons: automaton.scan(chunk) def flush_chunk(self, chunk): """ Flush a damaged chunk to all players that have it loaded. """ if chunk.is_damaged(): packet = chunk.get_damage_packet() for player in self.protocols.itervalues(): if (chunk.x, chunk.z) in player.chunks: player.transport.write(packet) chunk.clear_damage() def flush_all_chunks(self): """ Flush any damage anywhere in this world to all players. This is a sledgehammer which should be used sparingly at best, and is only well-suited to plugins which touch multiple chunks at once. In other words, if I catch you using this in your plugin needlessly, I'm gonna have a chat with you. """ for chunk in chain(self.world.chunk_cache.itervalues(), self.world.dirty_chunk_cache.itervalues()): self.flush_chunk(chunk) def give(self, coords, block, quantity): """ Spawn a pickup at the specified coordinates. The coordinates need to be in pixels, not blocks. If the size of the stack is too big, multiple stacks will be dropped. :param tuple coords: coordinates, in pixels :param tuple block: key of block or item to drop :param int quantity: number of blocks to drop in the stack """ x, y, z = coords while quantity > 0: entity = self.create_entity(x // 32, y // 32, z // 32, "Item", item=block, quantity=min(quantity, 64)) packet = entity.save_to_packet() packet += make_packet("create", eid=entity.eid) self.broadcast(packet) quantity -= 64 def players_near(self, player, radius): """ Obtain other players within a radius of a given player. Radius is measured in blocks. """ for i in (p for p in self.protocols.itervalues() if player.location.distance(p.location) <= radius and p.player != player): yield i.player def pauseProducing(self): pass def resumeProducing(self): pass def stopProducing(self): pass
class TestWater(TestCase): def setUp(self): # Set up world. self.name = "unittest" self.bcp = BravoConfigParser() self.bcp.add_section("world unittest") self.bcp.set("world unittest", "url", "") self.bcp.set("world unittest", "serializer", "memory") self.w = World(self.bcp, self.name) self.w.pipeline = [] self.w.start() # And finally the mock factory. self.f = PhysicsMockFactory() self.f.world = self.w # Using dig hook to grab the plugin since the build hook was nuked in # favor of the automaton interface. self.p = bravo.plugin.retrieve_plugins(IDigHook, factory=self.f) self.hook = self.p["water"] def tearDown(self): self.w.stop() self.hook.stop() def test_trivial(self): pass def test_update_fluid_negative(self): """ update_fluid() should always return False for Y at the bottom of the world. """ self.assertFalse(self.hook.update_fluid(self.w, (0, -1, 0), False)) def test_update_fluid_unloaded(self): self.assertRaises(ChunkNotLoaded, self.hook.update_fluid, self.w, (0, 0, 0), False) def test_update_fluid(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertTrue(self.hook.update_fluid(self.w, (0, 0, 0), False)) self.assertEqual(self.w.sync_get_block((0, 0, 0)), blocks["water"].slot) self.assertEqual(self.w.sync_get_metadata((0, 0, 0)), 0) return d def test_update_fluid_metadata(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertTrue(self.hook.update_fluid(self.w, (0, 0, 0), False, 1)) self.assertEqual(self.w.sync_get_metadata((0, 0, 0)), 1) return d def test_update_fluid_falling(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertTrue(self.hook.update_fluid(self.w, (0, 0, 0), True)) self.assertEqual(self.w.sync_get_metadata((0, 0, 0)), 8) return d def test_zero_y(self): """ Double-check that water placed on the very bottom of the world doesn't cause internal errors. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium; if any exceptions happen, # they will bubble up. while self.hook.tracked: self.hook.process() def test_spring_spread(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((1, 0, 1), blocks["spring"].slot) self.hook.tracked.add((1, 0, 1)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((2, 0, 1), (1, 0, 2), (0, 0, 1), (1, 0, 0)): self.assertEqual(chunk.get_block(coords), blocks["water"].slot) self.assertEqual(chunk.get_metadata(coords), 0x0) return d def test_spring_spread_edge(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((1, 0, 0), (0, 0, 1)): self.assertEqual(chunk.get_block(coords), blocks["water"].slot) self.assertEqual(chunk.get_metadata(coords), 0x0) return d def test_fluid_spread_edge(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((2, 0, 0), (1, 0, 1), (0, 0, 2)): self.assertEqual(chunk.get_block(coords), blocks["water"].slot) self.assertEqual(chunk.get_metadata(coords), 0x1) return d @inlineCallbacks def test_spring_fall(self): """ Falling water should appear below springs. """ self.w.set_block((0, 1, 0), blocks["spring"].slot) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() block = yield self.w.get_block((0, 0, 0)) metadata = yield self.w.get_metadata((0, 0, 0)) self.assertEqual(block, blocks["water"].slot) self.assertEqual(metadata, 0x8) @inlineCallbacks def test_spring_fall_dig(self): """ Destroying ground underneath spring should allow water to continue falling downwards. """ self.w.set_block((0, 1, 0), blocks["spring"].slot) self.w.set_block((0, 0, 0), blocks["dirt"].slot) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() #dig away dirt under spring self.w.destroy((0, 0, 0)) self.hook.tracked.add((0, 1, 0)) while self.hook.tracked: self.hook.process() block = yield self.w.get_block((0, 0, 0)) self.assertEqual(block, blocks["water"].slot) def test_spring_fall_dig_offset(self): """ Destroying ground next to a spring should cause a waterfall effect. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((1, 1, 0), blocks["spring"].slot) chunk.set_block((1, 0, 0), blocks["dirt"].slot) chunk.set_block((1, 0, 1), blocks["dirt"].slot) self.hook.tracked.add((1, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Dig away the dirt next to the dirt under the spring, and simulate # the dig hook by adding the block above it. chunk.destroy((1, 0, 1)) self.hook.tracked.add((1, 1, 1)) while self.hook.tracked: self.hook.process() self.assertEqual(chunk.get_block((1, 0, 1)), blocks["water"].slot) return d def test_trench(self): """ Fluid should not spread across the top of existing fluid. This test is for a specific kind of trench-digging pattern. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 2, 0), blocks["spring"].slot) chunk.set_block((0, 1, 0), blocks["dirt"].slot) self.hook.tracked.add((0, 2, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Dig the dirt. self.w.destroy((0, 1, 0)) self.hook.tracked.add((0, 1, 1)) self.hook.tracked.add((0, 2, 0)) self.hook.tracked.add((1, 1, 0)) while self.hook.tracked: self.hook.process() block = chunk.get_block((0, 2, 2)) self.assertEqual(block, blocks["air"].slot) @inlineCallbacks def test_obstacle(self): """ Test that obstacles are flowed around correctly. """ yield self.w.set_block((0, 0, 0), blocks["spring"].slot) yield self.w.set_block((1, 0, 0), blocks["stone"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Make sure that the water level behind the stone is 0x3, not 0x0. metadata = yield self.w.get_metadata((2, 0, 0)) self.assertEqual(metadata, 0x3) @inlineCallbacks def test_sponge(self): """ Test that sponges prevent water from spreading near them. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.w.set_block((3, 0, 0), blocks["sponge"].slot) self.hook.tracked.add((0, 0, 0)) self.hook.tracked.add((3, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Make sure that water did not spread near the sponge. block = yield self.w.get_block((1, 0, 0)) self.assertNotEqual(block, blocks["water"].slot) def test_sponge_absorb_spring(self): """ Test that sponges can absorb springs and will cause all of the surrounding water to dry up. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() self.w.set_block((1, 0, 0), blocks["sponge"].slot) self.hook.tracked.add((1, 0, 0)) while self.hook.tracked: self.hook.process() for coords in ((0, 0, 0), (0, 0, 1)): block = yield self.w.get_block(coords) self.assertEqual(block, blocks["air"].slot) # Make sure that water did not spread near the sponge. block = yield self.w.get_block((1, 0, 0)) self.assertNotEqual(block, blocks["water"].slot) return d @inlineCallbacks def test_sponge_salt(self): """ Test that sponges don't "salt the earth" or have any kind of lasting effects after destruction. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() chunk = yield self.w.request_chunk(0, 0) # Take a snapshot at the base level, with a clever slice. before = chunk.sections[0].blocks[:256], chunk.sections[0].metadata[:256] self.w.set_block((3, 0, 0), blocks["sponge"].slot) self.hook.tracked.add((3, 0, 0)) while self.hook.tracked: self.hook.process() self.w.destroy((3, 0, 0)) self.hook.tracked.add((3, 0, 0)) while self.hook.tracked: self.hook.process() # Make another snapshot, for comparison. after = chunk.sections[0].blocks[:256], chunk.sections[0].metadata[:256] # Make sure that the sponge didn't permanently change anything. self.assertEqual(before, after) @inlineCallbacks def test_spring_remove(self): """ Test that water dries up if no spring is providing it. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Remove the spring. self.w.destroy((0, 0, 0)) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1)): block = yield self.w.get_block(coords) self.assertEqual(block, blocks["air"].slot) @inlineCallbacks def test_spring_underneath_keepalive(self): """ Test that springs located at a lower altitude than stray water do not keep that stray water alive. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.w.set_block((0, 1, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Remove the upper spring. self.w.destroy((0, 1, 0)) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Check that the upper water blocks dried out. Don't care about the # lower ones in this test. for coords in ((1, 1, 0), (-1, 1, 0), (0, 1, 1), (0, 1, -1)): block = yield self.w.get_block(coords) self.assertEqual(block, blocks["air"].slot)
class TestRedstone(TestCase): def setUp(self): # Set up world. self.name = "unittest" self.bcp = BravoConfigParser() self.bcp.add_section("world unittest") self.bcp.set("world unittest", "url", "") self.bcp.set("world unittest", "serializer", "memory") self.w = World(self.bcp, self.name) self.w.pipeline = [] self.w.start() # And finally the mock factory. self.f = RedstoneMockFactory() self.f.world = self.w self.p = retrieve_plugins(IDigHook, factory=self.f) self.hook = self.p["redstone"] def tearDown(self): self.w.stop() def test_trivial(self): pass def test_and_gate(self): """ AND gates should work. This test also bumps up against a chunk boundary intentionally. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i1, i2, o in ( (False, False, False), (True, False, False), (False, True, False), (True, True, True), ): # Reset the hook. self.hook.asic = Asic() # The tableau. chunk.set_block((1, 1, 1), blocks["sand"].slot) chunk.set_block((1, 1, 2), blocks["sand"].slot) chunk.set_block((1, 1, 3), blocks["sand"].slot) chunk.set_block((1, 2, 1), blocks["redstone-torch"].slot) chunk.set_metadata((1, 2, 1), blocks["redstone-torch"].orientation("+y")) chunk.set_block((1, 2, 3), blocks["redstone-torch"].slot) chunk.set_metadata((1, 2, 3), blocks["redstone-torch"].orientation("+y")) chunk.set_block((1, 2, 2), blocks["redstone-wire"].slot) # Output torch. chunk.set_block((2, 1, 2), blocks["redstone-torch"].slot) chunk.set_metadata((2, 1, 2), blocks["redstone-torch"].orientation("+x")) # Attach the levers to the sand block. orientation = blocks["lever"].orientation("-x") iblock, imetadata = truthify_block(i1, blocks["lever"].slot, orientation) chunk.set_block((0, 1, 1), iblock) chunk.set_metadata((0, 1, 1), imetadata) iblock, imetadata = truthify_block(i2, blocks["lever"].slot, orientation) chunk.set_block((0, 1, 3), iblock) chunk.set_metadata((0, 1, 3), imetadata) # Run the circuit, starting at the switches. Six times: # Lever (x2), sand (x2), torch (x2), wire, block, torch. self.hook.feed((0, 1, 1)) self.hook.feed((0, 1, 3)) self.hook.process() self.hook.process() self.hook.process() self.hook.process() self.hook.process() self.hook.process() block = chunk.get_block((2, 1, 2)) metadata = chunk.get_metadata((2, 1, 2)) self.assertEqual((block, metadata), truthify_block(o, block, metadata)) return d def test_or_gate(self): """ OR gates should work. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i1, i2, o in ( (False, False, False), (True, False, True), (False, True, True), (True, True, True), ): # Reset the hook. self.hook.asic = Asic() # The tableau. chunk.set_block((1, 1, 2), blocks["sand"].slot) chunk.set_block((1, 2, 2), blocks["redstone-torch"].slot) chunk.set_metadata((1, 2, 2), blocks["redstone-torch"].orientation("+y")) chunk.set_block((2, 2, 2), blocks["redstone-wire"].slot) chunk.set_block((2, 1, 2), blocks["sand"].slot) chunk.set_block((3, 1, 2), blocks["redstone-torch"].slot) chunk.set_metadata((3, 1, 2), blocks["redstone-torch"].orientation("+x")) # Attach the levers to the sand block. orientation = blocks["lever"].orientation("-z") iblock, imetadata = truthify_block(i1, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 1), iblock) chunk.set_metadata((1, 1, 1), imetadata) orientation = blocks["lever"].orientation("+z") iblock, imetadata = truthify_block(i2, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 3), iblock) chunk.set_metadata((1, 1, 3), imetadata) # Run the circuit, starting at the switches. Six times: # Lever (x2), sand, torch, wire, sand, torch. self.hook.feed((1, 1, 1)) self.hook.feed((1, 1, 3)) self.hook.process() self.hook.process() self.hook.process() self.hook.process() self.hook.process() self.hook.process() block = chunk.get_block((3, 1, 2)) metadata = chunk.get_metadata((3, 1, 2)) self.assertEqual((block, metadata), truthify_block(o, block, metadata)) return d def test_nor_gate(self): """ NOR gates should work. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i1, i2, o in ( (False, False, True), (True, False, False), (False, True, False), (True, True, False), ): # Reset the hook. self.hook.asic = Asic() # The tableau. chunk.set_block((1, 1, 2), blocks["sand"].slot) chunk.set_block((2, 1, 2), blocks["redstone-torch"].slot) # Attach the levers to the sand block. orientation = blocks["lever"].orientation("-z") iblock, imetadata = truthify_block(i1, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 1), iblock) chunk.set_metadata((1, 1, 1), imetadata) orientation = blocks["lever"].orientation("+z") iblock, imetadata = truthify_block(i2, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 3), iblock) chunk.set_metadata((1, 1, 3), imetadata) # Attach the torch to the sand block too. orientation = blocks["redstone-torch"].orientation("+x") chunk.set_metadata((2, 1, 2), orientation) # Run the circuit, starting at the switches. Three times: # Lever (x2), sand, torch. self.hook.feed((1, 1, 1)) self.hook.feed((1, 1, 3)) self.hook.process() self.hook.process() self.hook.process() block = chunk.get_block((2, 1, 2)) metadata = chunk.get_metadata((2, 1, 2)) self.assertEqual((block, metadata), truthify_block(o, block, metadata)) return d def test_not_gate(self): """ NOT gates should work. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i, o in ((True, False), (False, True)): # Reset the hook. self.hook.asic = Asic() # The tableau. chunk.set_block((2, 1, 1), blocks["sand"].slot) chunk.set_block((3, 1, 1), blocks["redstone-torch"].slot) # Attach the lever to the sand block, and throw it. For sanity # purposes, grab the orientation metadata from the block # definition. orientation = blocks["lever"].orientation("-x") iblock, imetadata = truthify_block(i, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 1), iblock) chunk.set_metadata((1, 1, 1), imetadata) # Attach the torch to the sand block too. orientation = blocks["redstone-torch"].orientation("+x") chunk.set_metadata((3, 1, 1), orientation) # Run the circuit, starting at the switch. self.hook.feed((1, 1, 1)) # Lever, torch, sand. self.hook.process() self.hook.process() self.hook.process() block = chunk.get_block((3, 1, 1)) metadata = chunk.get_metadata((3, 1, 1)) self.assertEqual((block, metadata), truthify_block(o, block, metadata)) return d
config.set("world mapgen", "url", target) config.set("world mapgen", "serializer", "beta") world = World(config, "mapgen") world.connect() world.pipeline = pipeline world.season = None world.saving = True counts = [1, 2, 4, 5, 8] count = 0 total = size**2 cpu = 0 before = time.time() for i, j in product(xrange(size), repeat=2): start = time.time() d = world.request_chunk(i, j) cpu += (time.time() - start) d.addCallback(lambda chunk: world.save_chunk(chunk)) count += 1 if count >= counts[0]: print "Status: %d/%d (%.2f%%)" % (count, total, count * 100 / total) counts.append(counts.pop(0) * 10) taken = time.time() - before print "Finished!" print "Took %.2f seconds to generate (%dms/chunk)" % (taken, taken * 1000 / size) print "Spent %.2f seconds on CPU (%dms/chunk)" % (cpu, cpu * 1000 / size)
class TestGrass(unittest.TestCase): def setUp(self): plugins = retrieve_plugins(IAutomaton) if "grass" not in plugins: raise unittest.SkipTest("Plugin not present") self.hook = plugins["grass"] self.d = tempfile.mkdtemp() configuration.add_section("world unittest") configuration.set("world unittest", "url", "file://%s" % self.d) configuration.set("world unittest", "serializer", "alpha") self.w = World("unittest") self.w.pipeline = [] self.f = GrassMockFactory() self.f.world = self.w def tearDown(self): if self.w.chunk_management_loop.running: self.w.chunk_management_loop.stop() del self.w shutil.rmtree(self.d) configuration.remove_section("world unittest") def test_trivial(self): pass @inlineCallbacks def test_not_dirt(self): """ Blocks which aren't dirt by the time they're processed will be ignored. """ chunk = yield self.w.request_chunk(0, 0) chunk.set_block((0, 0, 0), blocks["bedrock"].slot) # Run the loop once. self.hook.feed(self.f, (0, 0, 0)) self.hook.process() # We shouldn't have any pending blocks now. self.assertFalse(self.hook.tracked) @inlineCallbacks def test_surrounding(self): """ When surrounded by eight grassy neighbors, dirt should turn into grass immediately. """ chunk = yield self.w.request_chunk(0, 0) # Set up grassy surroundings. for x, z in product(xrange(0, 3), repeat=2): chunk.set_block((x, 0, z), blocks["grass"].slot) # Our lone Cinderella. chunk.set_block((1, 0, 1), blocks["dirt"].slot) # Do the actual hook run. This should take exactly one run. self.hook.feed(self.f, (1, 0, 1)) self.hook.process() self.assertFalse(self.hook.tracked) self.assertEqual(chunk.get_block((1, 0, 1)), blocks["grass"].slot) @inlineCallbacks def test_surrounding_obstructed(self): """ Grass can't grow on blocks which have other blocks on top of them. """ chunk = yield self.w.request_chunk(0, 0) # Set up grassy surroundings. for x, z in product(xrange(0, 3), repeat=2): chunk.set_block((x, 0, z), blocks["grass"].slot) # Put an obstruction on top. chunk.set_block((1, 1, 1), blocks["stone"].slot) # Our lone Cinderella. chunk.set_block((1, 0, 1), blocks["dirt"].slot) # Do the actual hook run. This should take exactly one run. self.hook.feed(self.f, (1, 0, 1)) self.hook.process() self.assertFalse(self.hook.tracked) self.assertEqual(chunk.get_block((1, 0, 1)), blocks["dirt"].slot)
class TestWorldChunks(unittest.TestCase): def setUp(self): self.name = "unittest" self.d = tempfile.mkdtemp() bravo.config.configuration.add_section("world unittest") bravo.config.configuration.set("world unittest", "url", "file://%s" % self.d) bravo.config.configuration.set("world unittest", "serializer", "alpha") self.w = World(self.name) self.w.pipeline = [] self.w.start() def tearDown(self): self.w.stop() del self.w shutil.rmtree(self.d) bravo.config.configuration.remove_section("world unittest") def test_trivial(self): pass @inlineCallbacks def test_get_block(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = yield self.w.get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_metadata(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.metadata = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.metadata.shape = (16, 16, 128) for x, y, z in product(xrange(2), xrange(2), xrange(2)): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. metadata = yield self.w.get_metadata((x, y, z)) self.assertEqual(metadata, chunk.get_metadata((x, y, z))) @inlineCallbacks def test_get_block_readback(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) # Evict the chunk and grab it again. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(0, 0) for x, y, z in product(xrange(2), xrange(2), xrange(2)): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = yield self.w.get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_block_readback_negative(self): chunk = yield self.w.request_chunk(-1, -1) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) # Evict the chunk and grab it again. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(-1, -1) for x, y, z in product(xrange(2), xrange(2), xrange(2)): block = yield self.w.get_block((x - 16, y, z - 16)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_metadata_readback(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.metadata = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.metadata.shape = (16, 16, 128) # Evict the chunk and grab it again. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(0, 0) for x, y, z in product(xrange(2), xrange(2), xrange(2)): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. metadata = yield self.w.get_metadata((x, y, z)) self.assertEqual(metadata, chunk.get_metadata((x, y, z))) @inlineCallbacks def test_world_level_mark_chunk_dirty(self): chunk = yield self.w.request_chunk(0, 0) # Reload chunk. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(0, 0) self.assertFalse(chunk.dirty) self.w.mark_dirty((12, 64, 4)) chunk = yield self.w.request_chunk(0, 0) self.assertTrue(chunk.dirty) @inlineCallbacks def test_world_level_mark_chunk_dirty_offset(self): chunk = yield self.w.request_chunk(1, 2) # Reload chunk. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(1, 2) self.assertFalse(chunk.dirty) self.w.mark_dirty((29, 64, 43)) chunk = yield self.w.request_chunk(1, 2) self.assertTrue(chunk.dirty) @inlineCallbacks def test_sync_get_block(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = self.w.sync_get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) def test_sync_get_block_unloaded(self): self.assertRaises(ChunkNotLoaded, self.w.sync_get_block, (0, 0, 0)) def test_sync_get_metadata_neighboring(self): """ Even if a neighboring chunk is loaded, the target chunk could still be unloaded. Test with sync_get_metadata() to increase test coverage. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertRaises(ChunkNotLoaded, self.w.sync_get_metadata, (16, 0, 0)) return d
class TestRedstone(unittest.TestCase): def setUp(self): # Set up world. self.name = "unittest" self.d = tempfile.mkdtemp() bravo.config.configuration.add_section("world unittest") bravo.config.configuration.set("world unittest", "url", "file://%s" % self.d) bravo.config.configuration.set("world unittest", "serializer", "alpha") self.w = World(self.name) self.w.pipeline = [] self.w.start() # And finally the mock factory. self.f = RedstoneMockFactory() self.f.world = self.w pp = {"factory": self.f} self.p = retrieve_plugins(IDigHook, parameters=pp) if "redstone" not in self.p: raise unittest.SkipTest("Plugin not present") self.hook = self.p["redstone"] def tearDown(self): self.w.stop() shutil.rmtree(self.d) bravo.config.configuration.remove_section("world unittest") def test_trivial(self): pass def test_update_wires_enable(self): """ update_wires() should correctly light up a wire. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i in range(1, 15): chunk.set_block((i, 1, 1), blocks["redstone-wire"].slot) chunk.set_metadata((i, 1, 1), 0x0) # Enable wires. self.hook.update_wires(1, 1, 1, True) for i in range(1, 15): metadata = chunk.get_metadata((i, 1, 1)) self.assertEqual(metadata, 0xf - i + 1) return d def test_update_wires_disable(self): """ update_wires() should correctly drain a wire. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i in range(1, 15): chunk.set_block((i, 1, 1), blocks["redstone-wire"].slot) chunk.set_metadata((i, 1, 1), i) # Enable wires. self.hook.update_wires(1, 1, 1, False) for i in range(1, 15): metadata = chunk.get_metadata((i, 1, 1)) self.assertEqual(metadata, 0x0) return d def test_switch(self): """ Levers should work. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((1, 1, 1), blocks["lever"].slot) chunk.set_block((2, 1, 1), blocks["sand"].slot) chunk.set_block((3, 1, 1), blocks["redstone-wire"].slot) # Attach the lever to the sand block, and throw it. For sanity # purposes, grab the orientation metadata from the block # definition. orientation = blocks["lever"].orientation("+x") chunk.set_metadata((1, 1, 1), orientation | 0x8) # Run the circuit, starting at the switch. circuit = list(self.hook.run_circuit(1, 1, 1))[0] self.hook.run_circuit(*circuit) metadata = chunk.get_metadata((3, 1, 1)) self.assertEqual(metadata, 0xf) return d def test_or_gate(self): """ OR gates should work. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i1, i2, o in ( (False, False, False), (True, False, True), (False, True, True), (True, True, True), ): # The tableau. chunk.set_block((1, 1, 2), blocks["sand"].slot) chunk.set_block((2, 1, 2), blocks["redstone-wire"].slot) # Attach the levers to the sand block. orientation = blocks["lever"].orientation("+z") iblock, imetadata = truthify_block(i1, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 1), iblock) chunk.set_metadata((1, 1, 1), imetadata) orientation = blocks["lever"].orientation("-z") iblock, imetadata = truthify_block(i2, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 3), iblock) chunk.set_metadata((1, 1, 3), imetadata) # Run the circuit, starting at the switches. circuit = list(self.hook.run_circuit(1, 1, 1))[0] self.hook.run_circuit(*circuit) circuit = list(self.hook.run_circuit(1, 1, 3))[0] self.hook.run_circuit(*circuit) block = chunk.get_block((2, 1, 2)) metadata = chunk.get_metadata((2, 1, 2)) self.assertEqual((block, metadata), truthify_block(o, block, metadata)) return d test_or_gate.todo = "Doesn't work yet." def test_not_gate(self): """ NOT gates should work. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i, o in ((True, False), (False, True)): # The tableau. chunk.set_block((2, 1, 1), blocks["sand"].slot) chunk.set_block((3, 1, 1), blocks["redstone-torch"].slot) # Attach the lever to the sand block, and throw it. For sanity # purposes, grab the orientation metadata from the block # definition. orientation = blocks["lever"].orientation("+x") iblock, imetadata = truthify_block(i, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 1), iblock) chunk.set_metadata((1, 1, 1), imetadata) # Attach the torch to the sand block too. orientation = blocks["redstone-torch"].orientation("-x") chunk.set_metadata((3, 1, 1), orientation) # Run the circuit, starting at the switch. circuit = list(self.hook.run_circuit(1, 1, 1))[0] self.hook.run_circuit(*circuit) block = chunk.get_block((3, 1, 1)) metadata = chunk.get_metadata((3, 1, 1)) self.assertEqual((block, metadata), truthify_block(o, block, metadata)) return d def test_nor_gate(self): """ NOR gates should work. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): for i1, i2, o in ( (False, False, True), (True, False, False), (False, True, False), (True, True, False), ): # The tableau. chunk.set_block((1, 1, 2), blocks["sand"].slot) chunk.set_block((2, 1, 2), blocks["redstone-torch"].slot) # Attach the levers to the sand block. orientation = blocks["lever"].orientation("+z") iblock, imetadata = truthify_block(i1, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 1), iblock) chunk.set_metadata((1, 1, 1), imetadata) orientation = blocks["lever"].orientation("-z") iblock, imetadata = truthify_block(i2, blocks["lever"].slot, orientation) chunk.set_block((1, 1, 3), iblock) chunk.set_metadata((1, 1, 3), imetadata) # Attach the torch to the sand block too. orientation = blocks["redstone-torch"].orientation("-x") chunk.set_metadata((2, 1, 2), orientation) # Run the circuit, starting at the switches. circuit = list(self.hook.run_circuit(1, 1, 1))[0] self.hook.run_circuit(*circuit) circuit = list(self.hook.run_circuit(1, 1, 3))[0] self.hook.run_circuit(*circuit) block = chunk.get_block((2, 1, 2)) metadata = chunk.get_metadata((2, 1, 2)) self.assertEqual((block, metadata), truthify_block(o, block, metadata)) return d
class TestWorldChunks(unittest.TestCase): def setUp(self): self.name = "unittest" self.d = tempfile.mkdtemp() self.bcp = BravoConfigParser() self.bcp.add_section("world unittest") self.bcp.set("world unittest", "url", "file://%s" % self.d) self.bcp.set("world unittest", "serializer", "alpha") self.w = World(self.bcp, self.name) self.w.pipeline = [] self.w.start() def tearDown(self): self.w.stop() shutil.rmtree(self.d) def test_trivial(self): pass @inlineCallbacks def test_get_block(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = yield self.w.get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_metadata(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.metadata = numpy.fromstring(numpy.random.bytes( chunk.blocks.size), dtype=numpy.uint8) chunk.metadata.shape = (16, 16, 128) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. metadata = yield self.w.get_metadata((x, y, z)) self.assertEqual(metadata, chunk.get_metadata((x, y, z))) @inlineCallbacks def test_get_block_readback(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) # Evict the chunk and grab it again. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(0, 0) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = yield self.w.get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_block_readback_negative(self): chunk = yield self.w.request_chunk(-1, -1) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) # Evict the chunk and grab it again. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(-1, -1) for x, y, z in product(xrange(2), repeat=3): block = yield self.w.get_block((x - 16, y, z - 16)) self.assertEqual(block, chunk.get_block((x, y, z))) @inlineCallbacks def test_get_metadata_readback(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.metadata = numpy.fromstring(numpy.random.bytes( chunk.blocks.size), dtype=numpy.uint8) chunk.metadata.shape = (16, 16, 128) # Evict the chunk and grab it again. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(0, 0) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. metadata = yield self.w.get_metadata((x, y, z)) self.assertEqual(metadata, chunk.get_metadata((x, y, z))) @inlineCallbacks def test_world_level_mark_chunk_dirty(self): chunk = yield self.w.request_chunk(0, 0) # Reload chunk. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(0, 0) self.assertFalse(chunk.dirty) self.w.mark_dirty((12, 64, 4)) chunk = yield self.w.request_chunk(0, 0) self.assertTrue(chunk.dirty) @inlineCallbacks def test_world_level_mark_chunk_dirty_offset(self): chunk = yield self.w.request_chunk(1, 2) # Reload chunk. self.w.save_chunk(chunk) del chunk self.w.chunk_cache.clear() self.w.dirty_chunk_cache.clear() chunk = yield self.w.request_chunk(1, 2) self.assertFalse(chunk.dirty) self.w.mark_dirty((29, 64, 43)) chunk = yield self.w.request_chunk(1, 2) self.assertTrue(chunk.dirty) @inlineCallbacks def test_sync_get_block(self): chunk = yield self.w.request_chunk(0, 0) # Fill the chunk with random stuff. chunk.blocks = numpy.fromstring(numpy.random.bytes(chunk.blocks.size), dtype=numpy.uint8) chunk.blocks.shape = (16, 16, 128) for x, y, z in product(xrange(2), repeat=3): # This works because the chunk is at (0, 0) so the coords don't # need to be adjusted. block = self.w.sync_get_block((x, y, z)) self.assertEqual(block, chunk.get_block((x, y, z))) def test_sync_get_block_unloaded(self): self.assertRaises(ChunkNotLoaded, self.w.sync_get_block, (0, 0, 0)) def test_sync_get_metadata_neighboring(self): """ Even if a neighboring chunk is loaded, the target chunk could still be unloaded. Test with sync_get_metadata() to increase test coverage. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertRaises(ChunkNotLoaded, self.w.sync_get_metadata, (16, 0, 0)) return d
class TestWater(unittest.TestCase): def setUp(self): # Set up world. self.name = "unittest" self.d = tempfile.mkdtemp() self.bcp = BravoConfigParser() self.bcp.add_section("world unittest") self.bcp.set("world unittest", "url", "file://%s" % self.d) self.bcp.set("world unittest", "serializer", "alpha") self.w = World(self.bcp, self.name) self.w.pipeline = [] self.w.start() # And finally the mock factory. self.f = PhysicsMockFactory() self.f.world = self.w # Using dig hook to grab the plugin since the build hook was nuked in # favor of the automaton interface. pp = {"factory": self.f} self.p = bravo.plugin.retrieve_plugins(IDigHook, parameters=pp) if "water" not in self.p: raise unittest.SkipTest("Plugin not present") self.hook = self.p["water"] def tearDown(self): self.w.stop() self.hook.stop() shutil.rmtree(self.d) def test_trivial(self): pass def test_update_fluid_negative(self): """ update_fluid() should always return False for Y at the bottom of the world. """ self.assertFalse(self.hook.update_fluid(self.w, (0, -1, 0), False)) def test_update_fluid_unloaded(self): self.assertRaises(ChunkNotLoaded, self.hook.update_fluid, self.w, (0, 0, 0), False) def test_update_fluid(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertTrue(self.hook.update_fluid(self.w, (0, 0, 0), False)) self.assertEqual(self.w.sync_get_block((0, 0, 0)), blocks["water"].slot) self.assertEqual(self.w.sync_get_metadata((0, 0, 0)), 0) return d def test_update_fluid_metadata(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertTrue(self.hook.update_fluid(self.w, (0, 0, 0), False, 1)) self.assertEqual(self.w.sync_get_metadata((0, 0, 0)), 1) return d def test_update_fluid_falling(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): self.assertTrue(self.hook.update_fluid(self.w, (0, 0, 0), True)) self.assertEqual(self.w.sync_get_metadata((0, 0, 0)), 8) return d def test_zero_y(self): """ Double-check that water placed on the very bottom of the world doesn't cause internal errors. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium; if any exceptions happen, # they will bubble up. while self.hook.tracked: self.hook.process() def test_spring_spread(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((1, 0, 1), blocks["spring"].slot) self.hook.tracked.add((1, 0, 1)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((2, 0, 1), (1, 0, 2), (0, 0, 1), (1, 0, 0)): self.assertEqual(chunk.get_block(coords), blocks["water"].slot) self.assertEqual(chunk.get_metadata(coords), 0x0) return d def test_spring_spread_edge(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((1, 0, 0), (0, 0, 1)): self.assertEqual(chunk.get_block(coords), blocks["water"].slot) self.assertEqual(chunk.get_metadata(coords), 0x0) return d def test_fluid_spread_edge(self): d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((2, 0, 0), (1, 0, 1), (0, 0, 2)): self.assertEqual(chunk.get_block(coords), blocks["water"].slot) self.assertEqual(chunk.get_metadata(coords), 0x1) return d @inlineCallbacks def test_spring_fall(self): """ Falling water should appear below springs. """ self.w.set_block((0, 1, 0), blocks["spring"].slot) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() block = yield self.w.get_block((0, 0, 0)) metadata = yield self.w.get_metadata((0, 0, 0)) self.assertEqual(block, blocks["water"].slot) self.assertEqual(metadata, 0x8) @inlineCallbacks def test_spring_fall_dig(self): """ Destroying ground underneath spring should allow water to continue falling downwards. """ self.w.set_block((0, 1, 0), blocks["spring"].slot) self.w.set_block((0, 0, 0), blocks["dirt"].slot) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() #dig away dirt under spring self.w.destroy((0, 0, 0)) self.hook.tracked.add((0, 1, 0)) while self.hook.tracked: self.hook.process() block = yield self.w.get_block((0, 0, 0)) self.assertEqual(block, blocks["water"].slot) def test_spring_fall_dig_offset(self): """ Destroying ground next to a spring should cause a waterfall effect. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((1, 1, 0), blocks["spring"].slot) chunk.set_block((1, 0, 0), blocks["dirt"].slot) chunk.set_block((1, 0, 1), blocks["dirt"].slot) self.hook.tracked.add((1, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Dig away the dirt next to the dirt under the spring, and simulate # the dig hook by adding the block above it. chunk.destroy((1, 0, 1)) self.hook.tracked.add((1, 1, 1)) while self.hook.tracked: self.hook.process() self.assertEqual(chunk.get_block((1, 0, 1)), blocks["water"].slot) return d def test_trench(self): """ Fluid should not spread across the top of existing fluid. This test is for a specific kind of trench-digging pattern. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 2, 0), blocks["spring"].slot) chunk.set_block((0, 1, 0), blocks["dirt"].slot) self.hook.tracked.add((0, 2, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Dig the dirt. self.w.destroy((0, 1, 0)) self.hook.tracked.add((0, 1, 1)) self.hook.tracked.add((0, 2, 0)) self.hook.tracked.add((1, 1, 0)) while self.hook.tracked: self.hook.process() block = chunk.get_block((0, 2, 2)) self.assertEqual(block, blocks["air"].slot) @inlineCallbacks def test_obstacle(self): """ Test that obstacles are flowed around correctly. """ yield self.w.set_block((0, 0, 0), blocks["spring"].slot) yield self.w.set_block((1, 0, 0), blocks["stone"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Make sure that the water level behind the stone is 0x3, not 0x0. metadata = yield self.w.get_metadata((2, 0, 0)) self.assertEqual(metadata, 0x3) @inlineCallbacks def test_sponge(self): """ Test that sponges prevent water from spreading near them. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.w.set_block((3, 0, 0), blocks["sponge"].slot) self.hook.tracked.add((0, 0, 0)) self.hook.tracked.add((3, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Make sure that water did not spread near the sponge. block = yield self.w.get_block((1, 0, 0)) self.assertNotEqual(block, blocks["water"].slot) def test_sponge_absorb_spring(self): """ Test that sponges can absorb springs and will cause all of the surrounding water to dry up. """ d = self.w.request_chunk(0, 0) @d.addCallback def cb(chunk): chunk.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() self.w.set_block((1, 0, 0), blocks["sponge"].slot) self.hook.tracked.add((1, 0, 0)) while self.hook.tracked: self.hook.process() for coords in ((0, 0, 0), (0, 0, 1)): block = yield self.w.get_block(coords) self.assertEqual(block, blocks["air"].slot) # Make sure that water did not spread near the sponge. block = yield self.w.get_block((1, 0, 0)) self.assertNotEqual(block, blocks["water"].slot) return d @inlineCallbacks def test_sponge_salt(self): """ Test that sponges don't "salt the earth" or have any kind of lasting effects after destruction. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Take a snapshot. chunk = yield self.w.request_chunk(0, 0) before = chunk.blocks[:, :, 0], chunk.metadata[:, :, 0] self.w.set_block((3, 0, 0), blocks["sponge"].slot) self.hook.tracked.add((3, 0, 0)) while self.hook.tracked: self.hook.process() self.w.destroy((3, 0, 0)) self.hook.tracked.add((3, 0, 0)) while self.hook.tracked: self.hook.process() after = chunk.blocks[:, :, 0], chunk.metadata[:, :, 0] # Make sure that the sponge didn't permanently change anything. assert_array_equal(before, after) @inlineCallbacks def test_spring_remove(self): """ Test that water dries up if no spring is providing it. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Remove the spring. self.w.destroy((0, 0, 0)) self.hook.tracked.add((0, 0, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() for coords in ((1, 0, 0), (-1, 0, 0), (0, 0, 1), (0, 0, -1)): block = yield self.w.get_block(coords) self.assertEqual(block, blocks["air"].slot) @inlineCallbacks def test_spring_underneath_keepalive(self): """ Test that springs located at a lower altitude than stray water do not keep that stray water alive. """ self.w.set_block((0, 0, 0), blocks["spring"].slot) self.w.set_block((0, 1, 0), blocks["spring"].slot) self.hook.tracked.add((0, 0, 0)) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Remove the upper spring. self.w.destroy((0, 1, 0)) self.hook.tracked.add((0, 1, 0)) # Tight-loop run the hook to equilibrium. while self.hook.tracked: self.hook.process() # Check that the upper water blocks dried out. Don't care about the # lower ones in this test. for coords in ((1, 1, 0), (-1, 1, 0), (0, 1, 1), (0, 1, -1)): block = yield self.w.get_block(coords) self.assertEqual(block, blocks["air"].slot)