view src/parpg/gamemodel.py @ 110:5feab6555bf9

Added pc_has and npc_has as functions to the dialogue engine. These replace calls to pc.has_item and ncp.has_item.
author KarstenBock@gmx.net
date Fri, 23 Sep 2011 14:11:49 +0200
parents 2f928c913c78
children 7b0846025412
line wrap: on
line source

#   This file is part of PARPG.

#   PARPG is free software: you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation, either version 3 of the License, or
#   (at your option) any later version.

#   PARPG is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.

#   You should have received a copy of the GNU General Public License
#   along with PARPG.  If not, see <http://www.gnu.org/licenses/>.

# there should be NO references to FIFE here!
import sys
import os.path
import logging
from copy import deepcopy

from fife import fife
from fife.extensions.serializers.xmlobject import XMLObjectLoader 
from parpg.bGrease.geometry import Vec2d

from parpg import vfs
from gamestate import GameState
from gamemap import GameMap
from common.utils import locateFiles
from common.utils import parseBool
from parpg.dialogueparsers import YamlDialogueParser, DialogueFormatError
from parpg.entities import createEntity
from parpg import behaviours
from parpg.components import fifeagent, container, equip

try:
    import xml.etree.cElementTree as ElementTree
except ImportError:
    import xml.etree.ElementTree as ElementTree

import yaml

logger = logging.getLogger('gamemodel')

class GameModel(object):
    """GameModel holds the logic for the game.
       Since some data (object position and so forth) is held in the
       fife, and would be pointless to replicate, we hold a instance of
       the fife view here. This also prevents us from just having a
       function heavy controller."""
    ALL_AGENTS_KEY = "All"
    MAX_ID_NUMBER = 1000
    
    def __init__(self, engine, settings):
        """Initialize the instance.
        @param engine: A fife.Engine object
        @type emgome: fife.Engine 
        @param setting: The applications settigns
        @type setting: parpg.settings.Settings object
        @return: None"""
        self.settings = settings

        self.map_change = False
        self.load_saver = False
        self.savegame = None
        quests_directory = settings.parpg.QuestsPath
        self.game_state = GameState(quests_dir=quests_directory)
        #self.game_state.quest_engine = 
        #self.game_state.quest_engine.readQuests()
        self.pc_run = 1
        self.target_position = None
        self.target_map_name = None
        self.object_db = {}
        self.active_map = None
        self.map_files = {}
        self.agents = {}
        self.agents[self.ALL_AGENTS_KEY] = {}
        self.engine = engine
        self.fife_model = engine.getModel()

        # set values from settings
        maps_directory = settings.parpg.MapsPath
        self.game_state.maps_file = '/'.join([maps_directory,
                                              settings.parpg.MapsFile])
        self.all_agents_file = '/'.join([maps_directory,
                                         settings.parpg.AllAgentsFile])
        objects_directory = self.settings.parpg.ObjectsPath
        self.objects_directory = objects_directory
        self.object_db_file = '/'.join([objects_directory,
                                        settings.parpg.ObjectDatabaseFile])
        self.dialogue_directory = settings.parpg.DialoguesPath
        self.dialogues = {}
        self.agent_import_files = {}
        self.obj_loader = XMLObjectLoader(
                                          self.engine.getImagePool(), 
                                          self.engine.getAnimationPool(), 
                                          self.engine.getModel(),
                                          self.engine.getVFS() 
                                          )

    def checkAttributes(self, attributes, template):
        """Checks for attributes that where not given in the map file
        and fills them with values from the object database
        @param attributes: attributes to check        
        @type attributes: Dictionary
        @param template: Template from which the values will be used
        @return: The modified attributes""" 
        if self.object_db.has_key(template):
            db_attributes = deepcopy(self.object_db[template])
            for key in db_attributes.keys():
                if attributes.has_key(key):
                    attributes[key] = attributes[key] or db_attributes[key]
                else:
                    attributes[key] = db_attributes[key]
        return attributes
    
    def isIDUsed(self, ID):
        if self.game_state.hasObject(ID):
            return True
        for namespace in self.agents:
            if ID in self.agents[namespace]:
                return True
        return False
    
    def createUniqueID(self, ID):
        if self.isIDUsed(ID):
            id_number = 1
            while self.isIDUsed(ID + "_" + str(id_number)):
                id_number += 1
                if id_number > self.MAX_ID_NUMBER:
                    raise ValueError(
                        "Number exceeds MAX_ID_NUMBER:" + str(self.MAX_ID_NUMBER))
            
            ID = ID + "_" + str(id_number)
        return ID
    
    def deleteObject(self, object_id):
        """Removes an object from the game
        @param object_id: ID of the object
        @type object_id: str """
        del self.agents["All"][object_id]
        self.game_state.deleteObject(object_id)
        
    def save(self, path, filename):
        """Writes the saver to a file.
           @type filename: string
           @param filename: the name of the file to write to
           @return: None"""
        fname = '/'.join([path, filename])
        try:
            save_file = open(fname, 'w')
        except(IOError):
            sys.stderr.write("Error: Can't create save game: " + fname + "\n")
            return
        save_state = {}
        save_state["Agents"] = {}
        for map_name in self.agents:
            if map_name == self.ALL_AGENTS_KEY:
                continue
            agents_dict = {}
            for agent in self.agents[map_name]:
                agent_obj = self.game_state.getObjectById(agent, map_name)
                agent_inst = self.game_state.maps[map_name].\
                                    agent_layer.getInstance(agent)
                agent_dict = self.agents[map_name][agent]
                agent_dict.update(agent_obj.getStateForSaving())
                agent_dict["Rotation"] = agent_inst.getRotation()
                agents_dict[agent] = agent_dict
            save_state["Agents"][map_name] = agents_dict
        agents_dict = {}
        for agent in self.agents["All"]:
            map_name = self.agents["All"][agent]["Map"]
            agent_dict = self.agents["All"][agent]
            agent_obj = None
            if agent == "PlayerCharacter":
                agent_obj = self.game_state.getObjectById("PlayerCharacter").fifeagent
            else:
                agent_obj = self.game_state.getObjectById(agent, map_name)
            if agent_obj:
                agent_inst = self.game_state.maps[map_name].\
                                    agent_layer.getInstance(agent)
                agent_dict.update(agent_obj.getStateForSaving())
                agent_dict["Rotation"] = agent_inst.getRotation()
                agent_dict["MapName"] = map_name
            agents_dict[agent] = agent_dict
        save_state["Agents"]["All"] = agents_dict
        save_state["GameState"] = self.game_state.getStateForSaving()
        yaml.dump(save_state, save_file)
        
        save_file.close()       

    def load(self, path, filename):
        """Loads a saver from a file.
           @type filename: string
           @param filename: the name of the file (including path) to load from
           @return: None"""
        fname = '/'.join([path, filename])

        try:
            load_file = open(fname, 'r')
        except(IOError):
            sys.stderr.write("Error: Can't find save game file\n")
            return        
        self.deleteMaps()
        self.clearAgents()
        
        save_state = yaml.load(load_file)
        self.game_state.restoreFromState(save_state["GameState"])
        maps = save_state["Agents"]
        for map_name in maps:
            for agent_name in maps[map_name]:
                agent = {agent_name:maps[map_name][agent_name]}
                self.addAgent(map_name, agent)
                
        # Load the current map
        if self.game_state.current_map_name:
            self.loadMap(self.game_state.current_map_name)         
        load_file.close()
        

        # Recreate all the behaviours. These can't be saved because FIFE
        # objects cannot be pickled
        
        self.placeAgents()
        self.placePC()
      
        # In most maps we'll create the PlayerCharacter Instance internally. 
        # In these cases we need a target position
         
    def teleport(self, agent, position):
        """Called when a an agent is moved instantly to a new position. 
        The setting of position may wan to be created as its own method down the road.
        @type position: String Tuple
        @param position: X,Y coordinates passed from engine.changeMap
        @return: fife.Location"""
        logging.debug(position)
        coord = fife.DoublePoint3D(float(position[0]), float(position[1]), 0)
        location = fife.Location(self.active_map.agent_layer)
        location.setMapCoordinates(coord)
        agent.teleport(location)         
               
    def getObjectAtCoords(self, coords):
        """Get the object which is at the given coords
        @type coords: fife.Screenpoint
        @param coords: Coordinates where to check for an object
        @rtype: fife.Object
        @return: An object or None"""
        instances = self.active_map.cameras[
                                            self.active_map.my_cam_id].\
            getMatchingInstances(coords, self.active_map.agent_layer)
        # no object returns an empty tuple
        if(instances != ()):
            front_y = 0
            

            for obj in instances:
                # check to see if this in our list at all
                if(self.objectActive(obj.getId())):
                    # check if the object is on the foreground
                    obj_map_coords = \
                                      obj.getLocation().getMapCoordinates()
                    obj_screen_coords = self.active_map.\
                        cameras[self.active_map.my_cam_id]\
                        .toScreenCoordinates(obj_map_coords)

                    if obj_screen_coords.y > front_y:
                        #Object on the foreground
                        front_y = obj_screen_coords.y
                        return obj
                    else:
                        return None
        else:
            return None

    def getCoords(self, click):
        """Get the map location x, y coordinates from the screen coordinates
           @type click: fife.ScreenPoint
           @param click: Screen coordinates
           @rtype: fife.Location
           @return: The map coordinates"""
        coord = self.active_map.cameras[self.active_map.my_cam_id].\
                    toMapCoordinates(click, False)
        coord.z = 0
        location = fife.Location(self.active_map.agent_layer)
        location.setMapCoordinates(coord)
        return location

    def pause(self, paused):
        """ Pause/Unpause the game
        @return: nothing"""
        if self.active_map:
            self.active_map.pause(paused)
    
    def togglePause(self):
        """ Toggle paused state.
        @return: nothing"""
        self.active_map.togglePause()
        
    def isPaused(self):
        """Returns wheter the game is paused or not"""
        return self.active_map.isPaused()
    
    def readMapFiles(self):
        """Read all a available map-files and store them"""
        maps_file = vfs.VFS.open(self.game_state.maps_file)
        self.map_files = yaml.load(maps_file)["Maps"]
    
    def addAgent(self, namespace, agent):
        """Adds an agent to the agents dictionary
        @param namespace: the namespace where the agent is to be added to
        @type namespace: str
        @param agent: The agent to be added
        @type agent: dict """
        from fife.extensions.serializers.xml_loader_tools import loadImportFile
        if not self.agents.has_key(namespace):
            self.agents[namespace] = {}
            
        agent_values = agent.values()[0]
        unique_agent_id = self.createUniqueID(agent.keys()[0])
        del agent[agent.keys()[0]]
        agent[unique_agent_id] = agent_values
        self.agents[namespace].update(agent)
        object_model = ""
        if agent_values["Entity"].has_key("fifeagent") \
           and agent_values["Entity"]["fifeagent"].has_key("gfx"): 
            object_model = agent_values["Entity"]["fifeagent"]["gfx"]
        else:
            object_model = self.object_db[agent_values["Template"]]["fifeagent"]["gfx"]
        import_file = self.agent_import_files[object_model]
        loadImportFile(self.obj_loader, import_file, self.engine)
        
    def readAgentsOfMap(self, map_name):
        """Read the agents of the map
        @param map_name: Name of the map
        @type map_name: str """
        #Get the agents of the map        
        map_agents_file = self.map_files[map_name].\
                            replace(".xml", "_agents.yaml")   
        agents_data = vfs.VFS.open(map_agents_file)
        agents = yaml.load_all(agents_data)
        for agent in agents:
            if not agent == None:
                self.addAgent(map_name, agent)  
    
    def readAllAgents(self):
        """Read the agents of the all_agents_file and store them"""
        agents_file = vfs.VFS.open(self.all_agents_file)
        agents = yaml.load_all(agents_file)
        for agent in agents:
            if agent is not None:
                self.addAgent(self.ALL_AGENTS_KEY, agent)  
                
    def getAgentsOfMap(self, map_name):
        """Returns the agents that are on the given map
        @param map_name: Name of the map
        @type map_name: str
        @return: A dictionary with the agents of the map"""
        if not self.agents.has_key(map_name):
            return {}
        ret_dict = self.agents[map_name].copy()
        for agent_name, agent_value in self.agents[self.ALL_AGENTS_KEY]\
                                                .iteritems():
            if agent_value["Map"] == map_name:
                ret_dict[agent_name] = agent_value
        return ret_dict
                
    def getAgentsOfActiveMap(self):
        """Returns the agents that are on active map
        @return: A dictionary with the agents of the map """
        return self.getAgentsOfMap(self.active_map.map.getId())

    def clearAgents(self):
        """Resets the agents dictionary"""
        self.agents = {}
        self.agents[self.ALL_AGENTS_KEY] = {}
    
    def loadMap(self, map_name):
        """Load a new map.
           @type map_name: string
           @param map_name: Name of the map to load
           @return: None"""
        if not map_name in self.game_state.maps:  
            map_file = self.map_files[map_name]
            new_map = GameMap(self.engine, self)
            self.game_state.maps[map_name] = new_map
            new_map.load(map_file)    

    def createAgent(self, agent, inst_id, world):
        entity_data = agent["Entity"]
        if agent.has_key("Template"):
            entity_data = self.checkAttributes(entity_data, agent["Template"])
        object_id = agent["Entity"]["fifeagent"]["gfx"] \
                                if agent["Entity"]["fifeagent"].has_key("gfx") \
                                else "generic_item"
        map_obj = self.fife_model.getObject(str(object_id), "PARPG")
        if not map_obj:
            logging.warning("Object with inst_id={0}, ns=PARPG, "
                                  "could not be found. "
                                  "Omitting...".format(str(obj_id)))

        x_pos = agent["Position"][0]
        y_pos = agent["Position"][1]
        z_pos = agent["Position"][2] if len(agent["Position"]) == 3 \
                                        else 0.0  
        stack_pos = agent["Stackposition"] if \
                        agent.has_key("StackPosition") \
                        else None
        inst = self.active_map.agent_layer.\
                        createInstance(map_obj,
                                       fife.ExactModelCoordinate(x_pos, 
                                                                 y_pos, 
                                                                 z_pos),
                                       inst_id)
        inst.setId(inst_id)

        rotation = agent["Rotation"]
        inst.setRotation(rotation)

        fife.InstanceVisual.create(inst)
        if (stack_pos):
            inst.get2dGfxVisual().setStackPosition(int(stack_pos))

        if (map_obj.getAction('default')):
            target = fife.Location(self.active_map.agent_layer)
            inst.act('default', target, True)        

            
        entity_data["fifeagent"]["identifier"] = inst_id
        if entity_data["fifeagent"].has_key("behaviour"):
            entity_data["fifeagent"]["behaviour"] = getattr(behaviours, entity_data["fifeagent"]["behaviour"])()
        else:
            entity_data["fifeagent"]["behaviour"] = behaviours.Base()
        if self.dialogues.has_key(inst_id):
            entity_data["dialogue"] = {}
            entity_data["dialogue"]["dialogue"] = self.dialogues[inst_id]           
                       
        obj = self.createMapObject(self.active_map.agent_layer, entity_data, world)

        if agent.has_key("Inventory"):
            inv = agent["Inventory"]
            slots = inv["Slots"]
            obj.container.children = list()
            for x in xrange(slots):
                obj.container.children.append(None)
            items = inv["Items"] if inv.has_key("Items") else list()
            for data in items:
                item_type = data["type"]
                item_data = {}
                item_data = self.checkAttributes(item_data, item_type)
                if item_data.has_key("containable"):
                    item = self.create_item(item_data, world, item_type, data)
                    container.put_item(obj.container, item.containable)
                else:
                    raise Exception("Item %s is not containable." % item_type)

        if agent.has_key("Equipment"):
            for slot, data in agent["Equipment"].iteritems():
                item_type = data["type"]
                item_data = {}
                item_data = self.checkAttributes(item_data, item_type)
                if item_data.has_key("containable") and item_data.has_key("equipable"):
                    item = self.create_item(item_data, world, item_type, data)
                    equip.equip(obj.equip, item.equipable, slot)
                else:
                    raise Exception("Item %s is not containable or equipable." % item_type)

    def create_item(self, item_data, world, item_type, data):
        item = createEntity(item_data, world, None)
        item.containable.item_type = item_type
        self.game_state.addObject(self.createUniqueID(data["ID"]), None, item)
        return item

    def placeAgents(self, world):
        """Places the current maps agents """
        if not self.active_map:
            return
        agents = self.getAgentsOfMap(self.game_state.current_map_name)
        for agent in agents:
            if agent == "PlayerCharacter":
                continue
            if self.active_map.agent_layer.getInstances(agent):
                continue
            self.createAgent(agents[agent], agent, world)

    def placePC(self, world):
        """Places the PlayerCharacter on the map"""
        agent = self.agents[self.ALL_AGENTS_KEY]["PlayerCharacter"]
        inst_id = "PlayerCharacter"
        self.createAgent(agent, inst_id, world)
        
        # create the PlayerCharacter agent
        self.active_map.addPC()
        #self.game_state.getObjectById("PlayerCharacter").fifeagent.start()
        if agent.has_key("PeopleKnown"):
            self.game_state.getObjectById("PlayerCharacter").fifeagent.people_i_know = agent["PeopleKnown"]
                      
    def changeMap(self, map_name, target_position = None):
        """Registers for a map change on the next pump().
           @type map_name: String
           @param map_name: Id of the map to teleport to
           @type map_file: String
           @param map_file: Filename of the map to teleport to
           @type target_position: Tuple
           @param target_position: Position of PlayerCharacter on target map.
           @return None"""
        # set the parameters for the map change if moving to a new map
        if map_name != self.game_state.current_map_name:
            self.target_map_name = map_name
            self.target_position = target_position
            # issue the map change
            self.map_change = True

    def deleteMaps(self):
        """Clear all currently loaded maps from FIFE as well as clear our
            local map cache
            @return: nothing"""
        self.engine.getModel().deleteMaps()
        self.engine.getModel().deleteObjects()
        self.game_state.clearObjects()
        self.game_state.maps = {}
        
    def setActiveMap(self, map_name):
        """Sets the active map that is to be rendered.
           @type map_name: String
           @param map_name: The name of the map to load
           @return: None"""
        # Turn off the camera on the old map before we turn on the camera
        # on the new map.
        self.active_map.cameras[self.active_map.my_cam_id].setEnabled(False)
        # Make the new map active.
        self.active_map = self.game_state.maps[map_name]
        self.active_map.makeActive()
        self.game_state.current_map_name = map_name

    def createMapObject (self, layer, attributes, world):
        """Create an object and add it to the current map.
           @type layer: fife.Layer
           @param layer: FIFE layer object exists in
           @type attributes: Dictionary
           @param attributes: Dictionary of all object attributes
           @type instance: fife.Instance
           @param instance: FIFE instance corresponding to the object
           @return: The created object"""
        # create the extra data
        extra = {}
        if layer is not None:
            extra['fifeagent'] = {}
            extra['fifeagent']['layer'] = layer
        
        obj = createEntity(attributes, world, extra)
        if obj:
            self.addObject(layer, obj)
        return obj

    def addPC(self, layer, player_char):
        """Add the PlayerCharacter to the map
           @type layer: fife.Layer
           @param layer: FIFE layer object exists in
           @type player_char: PlayerCharacter
           @param player_char: PlayerCharacter object
           @type instance: fife.Instance
           @param instance: FIFE instance of PlayerCharacter
           @return: None"""
        # For now we copy the PlayerCharacter, 
        # in the future we will need to copy
        # PlayerCharacter specifics between the different PlayerCharacter's
        self.game_state.getObjectById("PlayerCharacter").fifeagent = player_char
        self.game_state.getObjectById("PlayerCharacter").fifeagent.setup()        
        self.game_state.getObjectById("PlayerCharacter").fifeagent.behaviour.speed = self.settings.parpg.PCSpeed


    def addObject(self, layer, obj):
        """Adds an object to the map.
           @type layer: fife.Layer
           @param layer: FIFE layer object exists in
           @type obj: GameObject
           @param obj: corresponding object class
           @type instance: fife.Instance
           @param instance: FIFE instance of object
           @return: None"""
        ref = self.game_state.getObjectById(obj.fifeagent.identifier,
                                            self.game_state.current_map_name) 
        if ref is None:
            # no, add it to the game state
            self.game_state.addObject(obj.fifeagent.identifier, self.game_state.current_map_name, obj)
        else:
            # yes, use the current game state data
            obj.fifeagent.pos.X = ref.X
            obj.fifeagent.pos.Y = ref.Y
            obj.fifeagent.gfx = ref.gfx  
             
        if obj.fifeagent.behaviour:
            obj.fifeagent.behaviour.parent = obj
            fifeagent.setup_behaviour(obj.fifeagent)
            obj.fifeagent.behaviour.speed = self.settings.parpg.PCSpeed
            #Start the behaviour            
            obj.fifeagent.behaviour.idle()
            # create the agent
            #obj.setup()
            #obj.behaviour.speed = self.settings.parpg.PCSpeed
            # create the PlayerCharacter agent
            #obj.start()
        #if obj.trueAttr("AnimatedContainer"):
            # create the agent
            #obj.setup()

    def objectActive(self, ident):
        """Given the objects ID, pass back the object if it is active,
           False if it doesn't exist or not displayed
           @type ident: string
           @param ident: ID of object
           @rtype: boolean
           @return: Status of result (True/False)"""
        for game_object in \
           self.game_state.getObjectsFromMap(self.game_state.current_map_name):
            if (game_object.fifeagent.identifier == ident):
                # we found a match
                return game_object
        # no match
        return False    

    def movePlayer(self, position):
        """Code called when the player should move to another location
           @type position: fife.ScreenPoint
           @param position: Screen position to move to
           @return: None"""
        if(self.pc_run == 1):
            self.game_state.getObjectById("PlayerCharacter").fifeagent.behaviour.run(position)
        else:
            self.game_state.getObjectById("PlayerCharacter").fifeagent.behaviour.walk(position)
        
    def teleportAgent(self, agent, position):
        """Code called when an agent should teleport to another location
           @type position: fife.ScreenPoint
           @param position: Screen position to teleport to
           @return: None"""
        agent.teleport(position)
        self.agents[agent.ID]["Position"] = position

    def readObjectDB(self):
        """Reads the Object Information Database from a file. """
        database_file = vfs.VFS.open(self.object_db_file)
        database = yaml.load_all(database_file)
        for object_info in database:
            self.object_db.update(object_info)

    def getAgentImportFiles(self):
        """Searches the agents directory for import files """
        filepaths = locateFiles("*.xml", self.objects_directory)
        for filepath in filepaths:
            try:
                xml_file = vfs.VFS.open(filepath)
                root = ElementTree.parse(xml_file).getroot()
                if root.tag == "object":
                    self.agent_import_files[root.attrib["id"]] = filepath
            except SyntaxError as error:
                logging.error("Error parsing file {0}: {1}".format(filepath,
                                                                   error))
    
    def getDialogues(self):
        """Searches the dialogue directory for dialogues """
        files = locateFiles("*.yaml", self.dialogue_directory)
        dialogue_parser = YamlDialogueParser()
        for dialogue_filepath in files:
            # Note Technomage 2010-11-13: the new DialogueEngine uses its own
            #     parser now, YamlDialogueParser.
#            dialogues = yaml.load_all(file(dialogue_file, "r"))
            dialogue_file = vfs.VFS.open(dialogue_filepath)
            try:
                dialogue = dialogue_parser.load(dialogue_file)
            except DialogueFormatError as error:
                logging.error('unable to load dialogue file {0}: {1}'
                              .format(dialogue_filepath, error))
            else:
                self.dialogues[dialogue.npc_name] = dialogue
            # Note Technomage 2010-11-13: the below code is used to load
            #     multiple dialogues from a single file. Is this functionality
            #     used/necessary?
#            for dialogue in dialogues:
#                self.dialogues[dialogue["NPC"]] = dialogue