from random import uniform
from twisted.internet.task import LoopingCall
from twisted.python import log
from bravo.inventory import Inventory
from bravo.inventory.slots import ChestStorage, FurnaceStorage
from bravo.location import Location
from bravo.beta.packets import make_packet, Speed, Slot
from bravo.utilities.geometry import gen_close_point
from bravo.utilities.maths import clamp
from bravo.utilities.furnace import (furnace_recipes, furnace_on_off,
update_all_windows_slot, update_all_windows_progress)
from bravo.blocks import furnace_fuel, unstackable
[docs]class Entity(object):
"""
Class representing an entity.
Entities are simply dynamic in-game objects. Plain entities are not very
interesting.
"""
name = "Entity"
def __init__(self, location=None, eid=0, **kwargs):
"""
Create an entity.
This method calls super().
"""
super(Entity, self).__init__()
self.eid = eid
if location is None:
self.location = Location()
else:
self.location = location
def __repr__(self):
return "%s(eid=%d, location=%s)" % (self.name, self.eid, self.location)
__str__ = __repr__
[docs]class Player(Entity):
"""
A player entity.
"""
name = "Player"
def __init__(self, username="", **kwargs):
"""
Create a player.
This method calls super().
"""
super(Player, self).__init__(**kwargs)
self.username = username
self.inventory = Inventory()
self.equipped = 0
def __repr__(self):
return ("%s(eid=%d, location=%s, username=%s)" %
(self.name, self.eid, self.location, self.username))
__str__ = __repr__
[docs] def save_to_packet(self):
"""
Create a "player" packet representing this entity.
"""
yaw, pitch = self.location.ori.to_fracs()
x, y, z = self.location.pos
item = self.inventory.holdables[self.equipped]
if item is None:
item = 0
else:
item = item[0]
packet = make_packet("player", eid=self.eid, username=self.username,
x=x, y=y, z=z, yaw=yaw, pitch=pitch, item=item,
# http://www.wiki.vg/Entities#Objects
metadata={
0: ('byte', 0), # Flags
1: ('short', 300), # Drowning counter
8: ('int', 0), # Color of the bubbling effects
})
return packet
[docs] def save_equipment_to_packet(self):
"""
Creates packets that include the equipment of the player. Equipment
is the item the player holds and all 4 armor parts.
"""
packet = ""
slots = (self.inventory.holdables[self.equipped],
self.inventory.armor[3], self.inventory.armor[2],
self.inventory.armor[1], self.inventory.armor[0])
for slot, item in enumerate(slots):
if item is None:
continue
primary, secondary, count = item
packet += make_packet("entity-equipment", eid=self.eid, slot=slot,
primary=primary, secondary=secondary,
count=1)
return packet
[docs]class Painting(Entity):
"""
A painting on a wall.
"""
name = "Painting"
def __init__(self, face="+x", motive="", **kwargs):
"""
Create a painting.
This method calls super().
"""
super(Painting, self).__init__(**kwargs)
self.face = face
self.motive = motive
[docs] def save_to_packet(self):
"""
Create a "painting" packet representing this entity.
"""
x, y, z = self.location.pos
return make_packet("painting", eid=self.eid, title=self.motive, x=x,
y=y, z=z, face=self.face)
[docs]class Pickup(Entity):
"""
Class representing a dropped block or item.
For historical and sanity reasons, this class is called Pickup, even
though its entity name is "Item."
"""
name = "Item"
def __init__(self, item=(0, 0), quantity=1, **kwargs):
"""
Create a pickup.
This method calls super().
"""
super(Pickup, self).__init__(**kwargs)
self.item = item
self.quantity = quantity
[docs] def save_to_packet(self):
"""
Create a "pickup" packet representing this entity.
"""
x, y, z = self.location.pos
packets = make_packet('object', eid=self.eid, type='item_stack',
x=x, y=y, z=z, yaw=0, pitch=0, data=1,
speed=Speed(0, 0, 0))
packets += make_packet('metadata', eid=self.eid,
# See http://www.wiki.vg/Entities#Objects
metadata={
0: ('byte', 0), # Flags
1: ('short', 300), # Drowning counter
10: ('slot', Slot.fromItem(self.item, self.quantity))
})
return packets
[docs]class Mob(Entity):
"""
A creature.
"""
name = "Mob"
"""
The name of this mob.
Names are used to identify mobs during serialization, just like for all
other entities.
This mob might not be serialized if this name is not overriden.
"""
metadata = {0: ("byte", 0)}
def __init__(self, **kwargs):
"""
Create a mob.
This method calls super().
"""
self.loop = None
super(Mob, self).__init__(**kwargs)
self.manager = None
[docs] def run(self):
"""
Start this mob's update loop.
"""
# Save the current chunk coordinates of this mob. They will be used to
# track which chunk this mob belongs to.
self.chunk_coords = self.location.pos
self.loop = LoopingCall(self.update)
self.loop.start(.2)
[docs] def save_to_packet(self):
"""
Create a "mob" packet representing this entity.
"""
x, y, z = self.location.pos
yaw, pitch = self.location.ori.to_fracs()
# Update metadata from instance variables.
self.update_metadata()
return make_packet("mob", eid=self.eid, type=self.name, x=x, y=y, z=z,
yaw=yaw, pitch=pitch, head_yaw=yaw, vx=0, vy=0, vz=0,
metadata=self.metadata)
def save_location_to_packet(self):
x, y, z = self.location.pos
yaw, pitch = self.location.ori.to_fracs()
return make_packet("teleport", eid=self.eid, x=x, y=y, z=z, yaw=yaw,
pitch=pitch)
[docs] def update(self):
"""
Update this mob's location with respect to a factory.
"""
# XXX Discuss appropriate style with MAD
# XXX remarkably untested
player = self.manager.closest_player(self.location.pos, 16)
if player is None:
vector = (uniform(-.4,.4),
uniform(-.4,.4),
uniform(-.4,.4))
target = self.location.pos + vector
else:
target = player.location.pos
self_pos = self.location.pos
vector = gen_close_point(self_pos, target)
vector = (
clamp(vector[0], -0.4, 0.4),
clamp(vector[1], -0.4, 0.4),
clamp(vector[2], -0.4, 0.4),
)
new_position = self.location.pos + vector
new_theta = self.location.pos.heading(new_position)
self.location.ori = self.location.ori._replace(theta=new_theta)
# XXX explain these magic numbers please
can_go = self.manager.check_block_collision(self.location.pos,
(-10, 0, -10), (16, 32, 16))
if can_go:
self.slide = False
self.location.pos = new_position
self.manager.correct_origin_chunk(self)
self.manager.broadcast(self.save_location_to_packet())
else:
self.slide = self.manager.slide_vector(vector)
self.manager.broadcast(self.save_location_to_packet())
[docs]class Chuck(Mob):
"""
A cross between a duck and a chicken.
"""
name = "Chicken"
offsetlist = ((.5, 0, .5),
(-.5, 0, .5),
(.5, 0, -.5),
(-.5, 0, -.5))
[docs]class Cow(Mob):
"""
Large, four-legged milk containers.
"""
name = "Cow"
[docs]class Creeper(Mob):
"""
A creeper.
"""
name = "Creeper"
def __init__(self, aura=False, **kwargs):
"""
Create a creeper.
This method calls super()
"""
super(Creeper, self).__init__(**kwargs)
self.aura = aura
def update_metadata(self):
self.metadata = {
0: ("byte", 0),
17: ("byte", int(self.aura)),
}
[docs]class Ghast(Mob):
"""
A very melancholy ghost.
"""
name = "Ghast"
[docs]class GiantZombie(Mob):
"""
Like a regular zombie, but far larger.
"""
name = "GiantZombie"
[docs]class Pig(Mob):
"""
A provider of bacon and piggyback rides.
"""
name = "Pig"
def __init__(self, saddle=False, **kwargs):
"""
Create a pig.
This method calls super().
"""
super(Pig, self).__init__(**kwargs)
self.saddle = saddle
def update_metadata(self):
self.metadata = {
0: ("byte", 0),
16: ("byte", int(self.saddle)),
}
[docs]class ZombiePigman(Mob):
"""
A zombie pigman.
"""
name = "PigZombie"
[docs]class Sheep(Mob):
"""
A woolly mob.
"""
name = "Sheep"
def __init__(self, sheared=False, color=0, **kwargs):
"""
Create a sheep.
This method calls super().
"""
super(Sheep, self).__init__(**kwargs)
self.sheared = sheared
self.color = color
def update_metadata(self):
color = self.color
if self.sheared:
color |= 0x10
self.metadata = {
0: ("byte", 0),
16: ("byte", color),
}
[docs]class Skeleton(Mob):
"""
An archer skeleton.
"""
name = "Skeleton"
[docs]class Slime(Mob):
"""
A gelatinous blob.
"""
name = "Slime"
def __init__(self, size=1, **kwargs):
"""
Create a slime.
This method calls super().
"""
super(Slime, self).__init__(**kwargs)
self.size = size
def update_metadata(self):
self.metadata = {
0: ("byte", 0),
16: ("byte", self.size),
}
[docs]class Spider(Mob):
"""
A spider.
"""
name = "Spider"
[docs]class Squid(Mob):
"""
An aquatic source of ink.
"""
name = "Squid"
[docs]class Wolf(Mob):
"""
A wolf.
"""
name = "Wolf"
def __init__(self, owner=None, angry=False, sitting=False, **kwargs):
"""
Create a wolf.
This method calls super().
"""
super(Wolf, self).__init__(**kwargs)
self.owner = owner
self.angry = angry
self.sitting = sitting
def update_metadata(self):
flags = 0
if self.sitting:
flags |= 0x1
if self.angry:
flags |= 0x2
if self.owner:
flags |= 0x4
self.metadata = {
0: ("byte", 0),
16: ("byte", flags),
}
[docs]class Zombie(Mob):
"""
A zombie.
"""
name = "Zombie"
offsetlist = ((-.5,0,-.5), (-.5,0,.5), (.5,0,-.5), (.5,0,.5), (-.5,1,-.5), (-.5,1,.5), (.5,1,-.5), (.5,1,.5),)
entities = dict((entity.name, entity)
for entity in (
Chuck,
Cow,
Creeper,
Ghast,
GiantZombie,
Painting,
Pickup,
Pig,
Player,
Sheep,
Skeleton,
Slime,
Spider,
Squid,
Wolf,
Zombie,
ZombiePigman,
)
)
[docs]class Tile(object):
"""
An entity that is also a block.
Or, perhaps more correctly, a block that is also an entity.
"""
name = "GenericTile"
def __init__(self, x, y, z):
self.x = x
self.y = y
self.z = z
def load_from_packet(self, container):
log.msg("%s doesn't know how to load from a packet!" % self.name)
def save_to_packet(self):
log.msg("%s doesn't know how to save to a packet!" % self.name)
return ""
[docs]class Chest(Tile):
"""
A tile that holds items.
"""
name = "Chest"
def __init__(self, *args, **kwargs):
super(Chest, self).__init__(*args, **kwargs)
self.inventory = ChestStorage()
[docs]class Furnace(Tile):
"""
A tile that converts items to other items, using specific items as fuel.
"""
name = "Furnace"
burntime = 0
cooktime = 0
running = False
def __init__(self, *args, **kwargs):
super(Furnace, self).__init__(*args, **kwargs)
self.inventory = FurnaceStorage()
self.burning = LoopingCall.withCount(self.burn)
[docs] def changed(self, factory, coords):
'''
Called from outside by event handler to inform the tile
that the content was changed. If the furnace meet the requirements
the method starts ``burn`` process. The ``burn`` stops the
looping call when it's out of fuel or no need to burn more.
We get furnace coords from outer side as the tile does not know
about own chunk. If self.chunk is implemented the parameter
can be removed and self.coords will be:
>>> self.coords = self.chunk.x, self.x, self.chunk.z, self.z, self.y
:param `BravoFactory` factory: The factory
:param tuple coords: (bigx, smallx, bigz, smallz, y) - coords of this furnace
'''
self.coords = coords
self.factory = factory
if not self.running:
if self.burntime != 0:
# This furnace was already burning, but not started. This
# usually means that the furnace was serialized while burning.
self.running = True
self.burn_max = self.burntime
self.burning.start(0.5)
elif self.has_fuel() and self.can_craft():
# This furnace could be burning, but isn't. Let's start it!
self.burntime = 0
self.cooktime = 0
self.burning.start(0.5)
[docs] def burn(self, ticks):
'''
The main furnace loop.
:param int ticks: number of furnace iterations to perform
'''
# Usually it's only one iteration but if something blocks the server
# for long period we shall process skipped ticks.
# Note: progress bars will lag anyway.
if ticks > 1:
log.msg("Lag detected; skipping %d furnace ticks" % (ticks - 1))
for iteration in xrange(ticks):
# Craft items, if we can craft them.
if self.can_craft():
self.cooktime += 1
# Notchian time is ~9.25-9.50 sec.
if self.cooktime == 20:
# Looks like things were successfully crafted.
source = self.inventory.crafting[0]
product = furnace_recipes[source.primary]
self.inventory.crafting[0] = source.decrement()
if self.inventory.crafted[0] is None:
self.inventory.crafted[0] = product
else:
item = self.inventory.crafted[0]
self.inventory.crafted[0] = item.increment(product.quantity)
update_all_windows_slot(self.factory, self.coords, 0, self.inventory.crafting[0])
update_all_windows_slot(self.factory, self.coords, 2, self.inventory.crafted[0])
self.cooktime = 0
else:
self.cooktime = 0
# Consume fuel, if applicable.
if self.burntime == 0:
if self.has_fuel() and self.can_craft():
# We have fuel and stuff to craft, so burn a bit of fuel
# and craft some stuff.
fuel = self.inventory.fuel[0]
self.burntime = self.burn_max = furnace_fuel[fuel.primary]
self.inventory.fuel[0] = fuel.decrement()
if not self.running:
self.running = True
furnace_on_off(self.factory, self.coords, True)
update_all_windows_slot(self.factory, self.coords, 1, self.inventory.fuel[0])
else:
# We're finished burning. Turn ourselves off.
self.burning.stop()
self.running = False
furnace_on_off(self.factory, self.coords, False)
# Reset the cooking time, just because.
self.cooktime = 0
update_all_windows_progress(self.factory, self.coords, 0, 0)
return
self.burntime -= 1
# Update progress bars for the window.
# XXX magic numbers
cook_progress = 185 * self.cooktime / 19
burn_progress = 250 * self.burntime / self.burn_max
update_all_windows_progress(self.factory, self.coords, 0, cook_progress)
update_all_windows_progress(self.factory, self.coords, 1, burn_progress)
[docs] def has_fuel(self):
'''
Determine whether this furnace is fueled.
:returns: bool
'''
return (self.inventory.fuel[0] is not None and
self.inventory.fuel[0].primary in furnace_fuel)
[docs] def can_craft(self):
'''
Determine whether this furnace is capable of outputting items.
Note that this is independent of whether the furnace is fueled.
:returns: bool
'''
crafting = self.inventory.crafting[0]
crafted = self.inventory.crafted[0]
# Nothing to craft?
if crafting is None:
return False
# No matching recipe?
if crafting.primary not in furnace_recipes:
return False
# Something to craft and no current output? This is a success
# condition.
if crafted is None:
return True
# Unstackable output?
if crafted.primary in unstackable:
return False
recipe = furnace_recipes[crafting.primary]
# Recipe doesn't match current output?
if recipe[0] != crafted.primary:
return False
# Crafting would overflow current output?
if crafted.quantity + recipe.quantity > 64:
return False
# By default, yes, you can craft.
return True
[docs]class MobSpawner(Tile):
"""
A tile that spawns mobs.
"""
name = "MobSpawner"
[docs]class Music(Tile):
"""
A tile which produces a pitch when whacked.
"""
name = "Music"
[docs]class Sign(Tile):
"""
A tile that stores text.
"""
name = "Sign"
def __init__(self, *args, **kwargs):
super(Sign, self).__init__(*args, **kwargs)
self.text1 = ""
self.text2 = ""
self.text3 = ""
self.text4 = ""
def load_from_packet(self, container):
self.x = container.x
self.y = container.y
self.z = container.z
self.text1 = container.line1
self.text2 = container.line2
self.text3 = container.line3
self.text4 = container.line4
def save_to_packet(self):
packet = make_packet("sign", x=self.x, y=self.y, z=self.z,
line1=self.text1, line2=self.text2, line3=self.text3,
line4=self.text4)
return packet
tiles = dict((tile.name, tile)
for tile in (
Chest,
Furnace,
MobSpawner,
Music,
Sign,
)
)