view gamelib/savegame.py @ 547:1456bf2ff02e

Allow cancelling of save game dialog with ESC.
author Simon Cross <hodgestar@gmail.com>
date Sat, 28 Nov 2009 11:12:45 +0000
parents 882b582da618
children a8dde729000a
line wrap: on
line source

"""Utilities and widgets for saving and restoring games."""

import xmlrpclib
import os
import StringIO
import base64
import zlib
import datetime

from pgu import gui
import pygame
from pygame.locals import KEYDOWN, K_ESCAPE

import config
import version
import gameboard
import serializer

TIMESTAMP_FORMAT = "%Y-%m-%dT%H:%M:%S"

def read_savegame(fullpath):
    """Open a save game file."""
    xml = zlib.decompress(open(fullpath, "rb").read())
    params, methodname = xmlrpclib.loads(xml)
    if methodname != "foxassault":
        raise SaveGameError("File does not appear to be a "
            "Fox Assault save game.")
    save_version = params[0]
    if save_version != version.SAVE_GAME_VERSION:
        raise SaveGameError("Incompatible save game version.")

    data = params[1]

    try:
        snapshot = decode_snapshot(params[2])
    except Exception, e:
        snapshot = None

    try:
        level_name = params[3]
    except Exception, e:
        level_name = None

    try:
        timestamp = datetime.datetime.strptime(params[4], TIMESTAMP_FORMAT)
    except Exception, e:
        timestamp = None

    return data, snapshot, level_name, timestamp

def write_savegame(fullpath, data, snapshot, level_name, timestamp):
    """Write a save game file."""
    snapshot_data = encode_snapshot(snapshot)
    timestamp_str = timestamp.strftime(TIMESTAMP_FORMAT)
    params = (version.SAVE_GAME_VERSION, data, snapshot_data, level_name, timestamp_str)
    xml = xmlrpclib.dumps(params, "foxassault")
    open(fullpath, "wb").write(zlib.compress(xml))

def encode_snapshot(snapshot):
    """Encode a snapshot."""
    snapshot_file = StringIO.StringIO()
    pygame.image.save(snapshot, snapshot_file)
    data = snapshot_file.getvalue()
    data = base64.standard_b64encode(data)
    return data

def decode_snapshot(data):
    """Decode a snapshot."""
    data = base64.standard_b64decode(data)
    snapshot_file = StringIO.StringIO(data)
    snapshot = pygame.image.load(snapshot_file, "snapshot.tga")
    return snapshot


class SaveGameError(Exception):
    pass


class BaseSaveRestoreDialog(gui.Dialog):
    """Save game dialog."""

    TIMESTAMP_DISPLAY = "%H:%M, %d %b %Y"

    def __init__(self, title_txt, button_txt, allow_new, cls="dialog"):
        self.value = None
        self.save_folder = config.config.save_folder

        self.save_games = {}
        self._populate_save_games()

        if allow_new:
            self.name_input = gui.Input()
        else:
            self.name_input = None

        td_style = {
            'padding_left': 4,
            'padding_right': 4,
            'padding_top': 2,
            'padding_bottom': 2,
        }

        self.save_list = gui.List(width=350, height=250)
        games = self.save_games.keys()
        games.sort()
        for name in games:
            self.save_list.add(name, value=name)
        self.save_list.set_vertical_scroll(0)
        self.save_list.connect(gui.CHANGE, self._save_list_change)

        self.image_container = gui.Container()

        button_ok = gui.Button(button_txt)
        button_ok.connect(gui.CLICK, self._click_ok)

        button_cancel = gui.Button("Cancel")
        button_cancel.connect(gui.CLICK, self._click_cancel)

        body = gui.Table()
        body.tr()
        body.td(self.save_list, style=td_style, colspan=2)
        body.td(self.image_container, style=td_style, colspan=2)
        body.tr()
        if self.name_input:
            body.td(gui.Label("Save as:"), style=td_style, align=1)
            body.td(self.name_input, style=td_style)
        else:
            body.td(gui.Spacer(0, 0), style=td_style, colspan=2)
        body.td(button_ok, style=td_style, align=1)
        body.td(button_cancel, style=td_style, align=1)

        title = gui.Label(title_txt, cls=cls + ".title.label")
        gui.Dialog.__init__(self, title, body)

        if games:
            self.save_list.group.value = games[0]

    def get_fullpath(self):
        """Return the fullpath of the select save game file or None."""
        if self.value is None:
            return None
        return os.path.join(self.save_folder, self.value + ".xml")

    def _populate_save_games(self):
        """Read list of save games."""
        for filename in os.listdir(self.save_folder):
            fullpath = os.path.join(self.save_folder, filename)
            root, ext = os.path.splitext(filename)
            if not os.path.isfile(fullpath):
                continue
            if ext != ".xml":
                continue
            self.save_games[root] = (fullpath, None)

    def _create_image_widget(self, fullpath):
        """Create an image showing the contents of a save game file."""
        try:
            data, screenshot, level_name, timestamp = read_savegame(fullpath)
        except Exception:
            return gui.Label("Bad Save Game")

        tbl = gui.Table()

        tbl.tr()
        if screenshot is None:
            tbl.td(gui.Label("No screenshot"))
        else:
            tbl.td(gui.Image(screenshot))

        tbl.tr()
        if level_name is None:
            tbl.td(gui.Label("Level: ???"))
        else:
            tbl.td(gui.Label("Level: %s" % (level_name,)))

        if timestamp is not None:
            tbl.tr()
            tbl.td(gui.Label(timestamp.strftime(self.TIMESTAMP_DISPLAY)))

        return tbl

    def _save_list_change(self):
        if self.name_input:
            self.name_input.value = self.save_list.value

        for w in self.image_container.widgets:
            self.image_container.remove(w)

        name = self.save_list.value
        fullpath, widget = self.save_games[name]
        if widget is None:
            widget = self._create_image_widget(fullpath)
            self.save_games[name] = (fullpath, widget)

        self.image_container.add(widget, 0, 0)

    def _click_ok(self):
        if self.name_input:
            self.value = self.name_input.value
        else:
            self.value = self.save_list.value
        if self.value:
            self.send(gui.CHANGE)
            self.close()

    def _click_cancel(self):
        self.value = None
        self.send(gui.CHANGE)
        self.close()

    def event(self, e):
        if e.type == KEYDOWN and e.key == K_ESCAPE:
            self._click_cancel()
            return True
        return gui.Dialog.event(self, e)

class SaveDialog(BaseSaveRestoreDialog):
    """Save game dialog."""

    def __init__(self, gameboard):
        BaseSaveRestoreDialog.__init__(self, "Save Game ...", "Save", allow_new=True)
        self.connect(gui.CHANGE, self._save, gameboard)

    def _save(self, gameboard):
        filename = self.get_fullpath()
        if filename is None:
            return

        data = gameboard.save_game()
        snapshot = gameboard.snapshot()
        level_name = gameboard.level.level_name
        timestamp = datetime.datetime.now()

        try:
            write_savegame(filename, data, snapshot, level_name, timestamp)
        except Exception, e:
            print "Failed to save game: %s" % (e,)


class RestoreDialog(BaseSaveRestoreDialog):
    """Restore game dialog."""

    def __init__(self, restore_func):
        BaseSaveRestoreDialog.__init__(self, "Load Game ...", "Load", allow_new=False)
        self.connect(gui.CHANGE, self._restore, restore_func)

    def _restore(self, restore_func):
        filename = self.get_fullpath()
        if filename is None:
            return

        try:
            data, screenshot, level_name, timestamp = read_savegame(filename)
        except Exception, e:
            print "Failed to load game: %s" % (e,)
            return

        if 'refid' not in data or 'class' not in data or data['class'] != gameboard.GameBoard.__name__:
            print "Failed to load game: Invalid game data."
            return

        try:
            new_gameboard = serializer.unsimplify(data)
        except Exception, e:
            print "Failed to load game: %s" % (e,)
            return

        restore_func(new_gameboard)