Mercurial > parpg-source
diff gamemodel.py @ 0:7a89ea5404b1
Initial commit of parpg-core.
author | M. George Hansen <technopolitica@gmail.com> |
---|---|
date | Sat, 14 May 2011 01:12:35 -0700 |
parents | |
children | 06145a6ee387 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/gamemodel.py Sat May 14 01:12:35 2011 -0700 @@ -0,0 +1,775 @@ +# 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 gamestate import GameState +from objects import createObject +from objects.composed import CarryableItem, CarryableContainer +from gamemap import GameMap +from common.utils import locateFiles +from common.utils import parseBool +from inventory import Inventory +from parpg.dialogueparsers import YamlDialogueParser, DialogueFormatError + +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 = os.path.join(self.settings.system_path, + self.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_file = os.path.join(self.settings.system_path, + self.settings.parpg.MapsPath, + self.settings.parpg.MapsFile) + self.game_state.maps_file = maps_file + all_agents_file = os.path.join(self.settings.system_path, + self.settings.parpg.MapsPath, + self.settings.parpg.AllAgentsFile) + self.all_agents_file = all_agents_file + objects_dir = os.path.join(self.settings.system_path, + self.settings.parpg.ObjectsPath) + self.objects_directory = objects_dir + object_db_file = os.path.join(self.objects_directory, + self.settings.parpg.ObjectDatabaseFile) + self.object_db_file = object_db_file + dialogues_dir = os.path.join(self.settings.system_path, + self.settings.parpg.DialoguesPath) + self.dialogues_directory = dialogues_dir + 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): + """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 + @return: The modified attributes""" + if attributes.has_key("object_type"): + class_name = attributes.pop("object_type") + else: + class_name = attributes["type"] + if not attributes.has_key("type"): + attributes["type"] = class_name + if self.object_db.has_key(class_name): + db_attributes = deepcopy(self.object_db[class_name]) + 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 createContainerItems(self, container_objs): + """Create the items of a container from a dictionary + @param container_objs: Dictionary containing the items + @type container_objs: dict""" + items = [] + for container_obj in container_objs: + items.append(self.createContainerObject(container_obj)) + + return items + + def createContainerObject(self, attributes): + """Create an object that can be stored in + an container and return it + @param attributes: Dictionary of all object attributes + @type attributes: Dictionary + @return: The created object """ + # create the extra data + extra = {} + extra['controller'] = self + attributes = self.checkAttributes(attributes) + + info = {} + info.update(attributes) + info.update(extra) + ID = info.pop("id") if info.has_key("id") else info.pop("ID") + if not info.has_key("item_type"): + info["item_type"] = info["type"] + ID = self.createUniqueID(ID) + if info.has_key("attributes"): + attributes = info["attributes"] + if "Container" in attributes: + info["actions"]["Open"] = "" + if info.has_key("Items"): + inventory_objs = info["Items"] + info["items"] = self.createContainerItems(inventory_objs) + + new_item = CarryableContainer(ID = ID, **info) + else: + new_item = CarryableItem(ID = ID, **info) + else: + new_item = CarryableItem(ID = ID, **info) + self.game_state.addObject(None, new_item) + return new_item + + def createInventoryObject(self, container, attributes): + """Create an inventory object and place it into a container + @type container: base.Container + @param container: Container where the item is on + @type attributes: Dictionary + @param attributes: Dictionary of all object attributes + @return: None""" + index = attributes.pop("index") if attributes.has_key("index") else None + slot = attributes.pop("slot") if attributes.has_key("slot") else None + obj = self.createContainerObject(attributes) + #obj = createObject(attributes, extra) + if slot: + container.moveItemToSlot(obj, slot) + else: + container.placeItem(obj, index) + + 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.player_character + 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_data = file(self.game_state.maps_file) + self.map_files = yaml.load(maps_data)["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.has_key("ObjectModel"): + object_model = agent_values["ObjectModel"] + elif agent_values["ObjectType"] == "MapItem": + object_data = self.object_db[agent_values["ItemType"]] + object_model = object_data["gfx"] if object_data.has_key("gfx") \ + else "generic_item" + else: + object_model = self.object_db[agent_values["ObjectType"]]["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 = file(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_data = file(self.all_agents_file) + agents = yaml.load_all(agents_data) + for agent in agents: + if not agent == 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): + object_type = agent["ObjectType"] + object_id = agent["ObjectModel"] \ + if agent.has_key("ObjectModel") \ + else None + if object_id == None: + if object_type == "MapItem": + object_data = self.object_db[agent["ItemType"]] + object_id = object_data["gfx"] if object_data.has_key("gfx") \ + else "generic_item" + else: + object_id = self.object_db[object_type]["gfx"] + 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.1 if object_type == "MapItem" \ + 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) + + inst_dict = {} + inst_dict["id"] = inst_id + inst_dict["type"] = object_type + inst_dict["xpos"] = x_pos + inst_dict["ypos"] = y_pos + inst_dict["gfx"] = object_id + inst_dict["is_open"] = parseBool(agent["Open"]) \ + if agent.has_key("Open") \ + else False + inst_dict["locked"] = parseBool(agent["Locked"]) \ + if agent.has_key("Locked") \ + else False + inst_dict["name"] = agent["ViewName"] + inst_dict["real_name"] = agent["RealName"] \ + if agent.has_key("RealName") \ + else agent["ViewName"] + inst_dict["text"] = agent["Text"] \ + if agent.has_key("Text") \ + else None + if self.dialogues.has_key(inst_id): + inst_dict["dialogue"] = self.dialogues[inst_id] + inst_dict["target_map_name"] = agent["TargetMap"] \ + if agent.\ + has_key("TargetMap") \ + else None + inst_dict["target_x"] = agent["TargetPosition"][0] \ + if agent.\ + has_key("TargetPosition") \ + else None + inst_dict["target_y"] = agent["TargetPosition"][1] \ + if agent.\ + has_key("TargetPosition") \ + else None + if agent.has_key("Inventory"): + inventory = Inventory() + inventory_objs = agent["Inventory"] + for inventory_obj in inventory_objs: + self.createInventoryObject(inventory, + inventory_obj + ) + inst_dict["inventory"] = inventory + + if agent.has_key("Items"): + container_objs = agent["Items"] + items = self.createContainerItems(container_objs) + inst_dict["items"] = items + + if agent.has_key("ItemType"): + if not agent.has_key("item"): + item_data = {} + item_data["type"] = agent["ItemType"] + item_data["ID"] = inst_id + item_data = self.createContainerObject(item_data) + else: + item_data = agent["item"] + inst_dict["item"] = item_data + inst_dict["item_type"] = agent["ItemType"] + + self.createMapObject(self.active_map.agent_layer, inst_dict) + + def placeAgents(self): + """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) + + def placePC(self): + """Places the PlayerCharacter on the map""" + agent = self.agents[self.ALL_AGENTS_KEY]["PlayerCharacter"] + inst_id = "PlayerCharacter" + self.createAgent(agent, inst_id) + + # create the PlayerCharacter agent + self.active_map.addPC() + self.game_state.player_character.start() + if agent.has_key("PeopleKnown"): + self.game_state.player_character.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): + """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: None""" + # create the extra data + extra = {} + if layer is not None: + extra['agent_layer'] = layer + attributes = self.checkAttributes(attributes) + + obj = createObject(attributes, extra) + + if obj.trueAttr("PC"): + self.addPC(layer, obj) + else: + self.addObject(layer, 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.player_character = player_char + self.game_state.player_character.setup() + self.game_state.player_character.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.ID, \ + self.game_state.current_map_name) + if ref is None: + # no, add it to the game state + self.game_state.addObject(self.game_state.current_map_name, obj) + else: + # yes, use the current game state data + obj.X = ref.X + obj.Y = ref.Y + obj.gfx = ref.gfx + + if obj.trueAttr("NPC"): + # create the agent + obj.setup() + obj.behaviour.speed = self.settings.parpg.PCSpeed - 1 + # 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.ID == 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.player_character.run(position) + else: + self.game_state.player_character.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 = file(self.object_db_file, "r") + 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 """ + files = locateFiles("*.xml", self.objects_directory) + for xml_file in files: + xml_file = os.path.relpath(xml_file).replace("\\", "/") + try: + root = ElementTree.parse(xml_file).getroot() + if root.tag == "object": + self.agent_import_files[root.attrib["id"]] = xml_file + except SyntaxError as error: + assert(isinstance(error, SyntaxError)) + logging.critical("Error parsing file {0}: " + "{1}".format(xml_file, error.msg)) + sys.exit(1) + + def getDialogues(self): + """Searches the dialogue directory for dialogues """ + files = locateFiles("*.yaml", self.dialogues_directory) + dialogue_parser = YamlDialogueParser() + for dialogue_filepath in files: + dialogue_filepath = os.path.relpath(dialogue_filepath) \ + .replace("\\", "/") + # Note Technomage 2010-11-13: the new DialogueEngine uses its own + # parser now, YamlDialogueParser. +# dialogues = yaml.load_all(file(dialogue_file, "r")) + with file(dialogue_filepath, 'r') as dialogue_file: + 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