class World(Base): """ A gaming world with a specific game. This groups game objects together. """ # Textual name of this world name = DB.Column(DB.String(32), unique=True, nullable=False) # Whether this world is enabled or not enabled = DB.Column(DB.Boolean(), default=True, nullable=False) # Relationship to the game played in this world game_id = DB.Column(DB.Integer, DB.ForeignKey('game.id')) game = DB.relationship('Game', backref=DB.backref('worlds', lazy='dynamic')) @api_method(authenticated=True) def player_join(self): """ Make the current player join this world. Called as an API method from a client. """ # Check whether a user is logged in if g.user is None: # FIXME proper error return None # Check whether the user has a player in this world player = Player.query.filter_by(user=g.user, world=self).scalar() if player is None: # Create a new player in this world player = self.game.module.Player() player.name = g.user.username player.world = self player.user = g.user DB.session.add(player) DB.session.commit() # Update current_player g.user.current_player = player DB.session.add(g.user) DB.session.commit() # Redirect to new player object return redirect(url_for(player.__class__, resource_id=player.id)) return redirect("/api/gameobject_player/%i" % player.id)
class Game(Base): """ A game known to the server. This model/table is automatically filled with all games known to the server from the _sync_games() initialisation function. It is used to keep a mapping to game modules within the data model. """ # Base name of the game's Python package package = DB.Column(DB.String(128), nullable=False) # Filled from the respective game constants name = DB.Column(DB.String(32), nullable=False) version = DB.Column(DB.String(16), nullable=False) description = DB.Column(DB.String(1024)) author = DB.Column(DB.String(32)) license = DB.Column(DB.String(32)) # The triple package,name,version needs to be unique __table_args__ = (DB.UniqueConstraint('package', 'name', 'version', name='_name_version_uc'), ) @property def module(self): """ Attribute pointing to the real Python module loaded for the game. """ # Determine the game module from the package name return get_game_by_name(self.package) @api_method(authenticated=True) def world_create(self, name=None): """ Create a world with this game. Called as an API method from a client. """ # Create a new World object and store in database world = World() if name is None: world.name = self.name else: world.name = name world.game = self DB.session.add(world) DB.session.commit() # Redirect to new World object return redirect("/api/world/%i" % world.id)
class User(Base): """ A user account in the Veripeditus webserver. This is not a player object, it is only used for authentication and to link players to it. """ # Login credentials # Password is automatically maintained in encrypted form through the PasswordType username = DB.Column(DB.String(32), unique=True, nullable=False) password = DB.Column(PasswordType(schemes=APP.config['PASSWORD_SCHEMES']), nullable=False) # Relationship to the currently active player object for this account current_player_id = DB.Column(DB.Integer(), DB.ForeignKey("gameobject_player.id")) current_player = DB.relationship("veripeditus.framework.model.Player", foreign_keys=[current_player_id]) # The role of this account in the server # role = DB.Column(DB.Enum(Roles), default=Roles.player, nullable=False) role = DB.Column(DB.Unicode(32), default="PLAYER", nullable=False) @staticmethod def get_authenticated(username, password): """ Return a User object if username and password match, or None otherwise. """ # Filter for username first user = User.query.filter_by(username=username).first() # Compare password if a user was found if user and user.password == password: # Return found user return user else: # Fallback to None return None
class GameObject(Base, metaclass=_GameObjectMeta): __tablename__ = "gameobject" _api_includes = ["world", "attributes"] id = DB.Column(DB.Integer(), primary_key=True) # Columns and relationships name = DB.Column(DB.String(32)) image = DB.Column(DB.String(32), default="dummy", nullable=False) world_id = DB.Column(DB.Integer(), DB.ForeignKey("world.id")) world = DB.relationship("World", backref=DB.backref("gameobjects", lazy="dynamic"), foreign_keys=[world_id]) longitude = DB.Column(DB.Float(), default=0.0, nullable=False) latitude = DB.Column(DB.Float(), default=0.0, nullable=False) osm_element_id = DB.Column(DB.Integer(), DB.ForeignKey("osm_elements.id")) osm_element = DB.relationship(OA.element, backref=DB.backref("osm_elements", lazy="dynamic"), foreign_keys=[osm_element_id]) type = DB.Column(DB.Unicode(256)) attributes = association_proxy("gameobjects_to_attributes", "value", creator=lambda k, v: GameObjectsToAttributes( attribute=Attribute(key=k, value=v))) distance_max = None available_images_pattern = ["*.svg", "*.png"] @property def gameobject_type(self): # Return type of gameobject return self.__tablename__ def distance_to(self, obj): # Return distance to another gamobject return get_gameobject_distance(self, obj) @property def image_path(self): # Return path of image file return get_image_path(self.world.game.module, self.image) @hybrid_property def isonmap(self): return True @property def distance_to_current_player(self): # Return distance to current player if g.user is None or g.user.current_player is None: return None return self.distance_to(g.user.current_player) @api_method(authenticated=False) def image_raw(self, name=None): # Take path of current image if name is not given # If name is given take its path instead if name is None: image_path = self.image_path elif name in self.available_images(): image_path = get_image_path(self.world.game.module, name) else: # FIXME correct error return None with open(image_path, "rb") as file: return file.read() @api_method(authenticated=True) def set_image(self, name): # Check if image is available if name in self.available_images(): # Update image self.image = name self.commit() else: # FIXME correct error return None # Redirect to new image return redirect("/api/v2/gameobject/%d/image_raw" % self.id) @api_method(authenticated=False) def available_images(self): res = [] if self.available_images_pattern is not None: # Make patterns a list if isinstance(self.available_images_pattern, list): patterns = self.available_images_pattern else: patterns = [self.available_images_pattern] # Get data path of this object's module data_path_game = get_data_path(self.world.game.module) # Get data path of the framework module data_path_framework = get_data_path() for data_path in (data_path_game, data_path_framework): for pattern in patterns: # Get images in data path matching the pattern res += glob(os.path.join(data_path, pattern)) # Get basenames of every file without extension basenames = [os.path.extsep.join(os.path.basename(r).split(os.path.extsep)[:-1]) for r in res] # Return files in json format return json.dumps(basenames) @classmethod def spawn(cls, world=None): if world is None: # Iterate over all defined GameObject classes for go in cls.__subclasses__(): # Iterate over all worlds using the game worlds = World.query.filter(World.game.has(package=go.__module__.split(".")[2])).all() for world in worlds: if "spawn" in vars(go): # Call spawn for each world go.spawn(world) else: # Call parameterised default spawn code go.spawn_default(world) @classmethod def spawn_default(cls, world): # Get current player current_player = None if g.user is None else g.user.current_player # Determine spawn location if "spawn_latlon" in vars(cls): latlon = cls.spawn_latlon if isinstance(latlon, Sequence): # We got one of: # (lat, lon) # ((lat, lon), (lat, lon),…) # ((lat, lon), radius) if isinstance(latlon[0], Sequence) and isinstance(latlon[1], Sequence): if len(latlon) == 2: # We got a rect like ((lat, lon), (lat, lon)) # Randomise coordinates within that rect latlon = (random.uniform(latlon[0][0], latlon[1][0]), random.uniform(latlon[0][1], latlon[1][1])) else: # We got a polygon, randomise coordinates within it latlon = random_point_in_polygon(latlon) elif isinstance(latlon[0], Sequence) and isinstance(latlon[1], Real): # We got a circle like ((lat, lon), radius) # FIXME implement raise RuntimeError("Not implemented.") elif isinstance(latlon[0], Real) and isinstance(latlon[1], Real): # We got a single point like (lat, lon) # Nothing to do, we can use that as is pass else: raise TypeError("Unknown value for spawn_latlon.") # Define a single spawn point with no linked OSM element spawn_points = {latlon: None} elif "spawn_osm" in vars(cls): # Skip if no current player or current player not in this world if current_player is None or current_player.world is not world: return # Define bounding box around current player # FIXME do something more intelligent here lat_min = current_player.latitude - 0.001 lat_max = current_player.latitude + 0.001 lon_min = current_player.longitude - 0.001 lon_max = current_player.longitude + 0.001 bbox_queries = [OA.node.latitude>lat_min, OA.node.latitude<lat_max, OA.node.longitude>lon_min, OA.node.longitude<lon_max] # Build list of tag values using OSMAlchemy has_queries = [OA.node.tags.any(key=k, value=v) for k, v in cls.spawn_osm.items()] and_query = sa_and(*bbox_queries, *has_queries) # Do query # FIXME support more than plain nodes nodes = DB.session.query(OA.node).filter(and_query).all() # Extract latitude and longitude information and build spawn_points latlons = [(node.latitude, node.longitude) for node in nodes] spawn_points = dict(zip(latlons, nodes)) else: # Do nothing if we cannot determine a location return for latlon, osm_element in spawn_points.items(): # Determine existing number of objects on map existing = cls.query.filter_by(world=world, osm_element=osm_element, isonmap=True).count() if "spawn_min" in vars(cls) and "spawn_max" in vars(cls) and existing < cls.spawn_min: to_spawn = cls.spawn_max - existing elif existing == 0: to_spawn = 1 else: to_spawn = 0 # Spawn the determined number of objects for i in range(0, to_spawn): # Create a new object obj = cls() obj.world = world obj.latitude = latlon[0] obj.longitude = latlon[1] obj.osm_element = osm_element # Determine any defaults for k in vars(cls): if k.startswith("default_"): setattr(obj, k[8:], getattr(cls, k)) # Derive missing defaults from class name if obj.image is None: obj.image = cls.__name__.lower() if obj.name is None: obj.name = cls.__name__.lower() # Add to session obj.commit() def commit(self): """ Commit this object to the database. """ DB.session.add(self) DB.session.commit()