view gamelib/savegame.py @ 510:3e4bb2c9556c

Lazy loading of save game snapshots.
author Simon Cross <hodgestar@gmail.com>
date Thu, 26 Nov 2009 23:15:04 +0000
parents 77a3f7f1c856
children b112bcf4d435
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

import config
import version

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

def read_savegame(fullpath):
    """Open a save game file."""
    try:
        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

    except Exception, e:
        raise SaveGameError("Failed to load game: %s" % (e,))

    return data, snapshot, level_name, timestamp

def write_savegame(fullpath, data, snapshot, level_name, timestamp):
    """Write a save game file."""
    try:
        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))
    except Exception, e:
        raise SaveGameError("Failed to save game: %s" % (e,))

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 SaveGameError:
            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()


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, gameboard):
        BaseSaveRestoreDialog.__init__(self, "Load Game ...", "Load", allow_new=False)
        self.connect(gui.CHANGE, self._restore, gameboard)

    def _restore(self, gameboard):
        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

        gameboard.restore_game(data)