## vim:ts=4:et:nowrap
##
##---------------------------------------------------------------------------##
##
## PySol -- a Python Solitaire game
##
## Copyright (C) 2003 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2002 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2001 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 2000 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 1999 Markus Franz Xaver Johannes Oberhumer
## Copyright (C) 1998 Markus Franz Xaver Johannes Oberhumer
## All Rights Reserved.
##
## This program 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 2 of the License, or
## (at your option) any later version.
##
## This program 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 this program; see the file COPYING.
## If not, write to the Free Software Foundation, Inc.,
## 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
##
## Markus F.X.J. Oberhumer
## <markus@oberhumer.com>
## http://www.oberhumer.com/pysol
##
##---------------------------------------------------------------------------##


# imports
import sys, imp, re, os, string, types

# PySol imports
from mfxtools import *
from mfxutil import Struct, latin1_to_ascii
from util import PACKAGE, VERSION, bundle
from resource import CSI


# /***********************************************************************
# // constants
# ************************************************************************/

# GameInfo constants
class GI:
    # game category - these *must* match the cardset CSI.TYPE_xxx
    GC_FRENCH        = CSI.TYPE_FRENCH
    GC_HANAFUDA      = CSI.TYPE_HANAFUDA
    GC_TAROCK        = CSI.TYPE_TAROCK
    GC_MAHJONGG      = CSI.TYPE_MAHJONGG
    GC_HEXADECK      = CSI.TYPE_HEXADECK
    GC_MUGHAL_GANJIFA = CSI.TYPE_MUGHAL_GANJIFA
    GC_NAVAGRAHA_GANJIFA = CSI.TYPE_NAVAGRAHA_GANJIFA
    GC_DASHAVATARA_GANJIFA = CSI.TYPE_DASHAVATARA_GANJIFA
    GC_TRUMP_ONLY    = CSI.TYPE_TRUMP_ONLY

    # game type
    GT_1DECK_TYPE   = 0
    GT_2DECK_TYPE   = 1
    GT_3DECK_TYPE   = 2
    GT_4DECK_TYPE   = 3
    GT_BAKERS_DOZEN = 4
    GT_BELEAGUERED_CASTLE = 5
    GT_CANFIELD     = 6
    GT_DASHAVATARA_GANJIFA = 7
    GT_FAN_TYPE     = 8
    GT_FORTY_THIEVES = 9
    GT_FREECELL     = 10
    GT_GOLF         = 11
    GT_GYPSY        = 12
    GT_HANAFUDA     = 13
    GT_HEXADECK     = 14
    GT_KLONDIKE     = 15
    GT_MAHJONGG     = 16
    GT_MATRIX       = 17
    GT_MEMORY       = 18
    GT_MONTANA      = 19
    GT_MUGHAL_GANJIFA = 20
    GT_NAPOLEON     = 21
    GT_NAVAGRAHA_GANJIFA = 22
    GT_NUMERICA     = 23
    GT_PAIRING_TYPE = 24
    GT_POKER_TYPE   = 25
    GT_PUZZLE_TYPE  = 26
    GT_RAGLAN       = 27
    GT_ROW_TYPE     = 28
    GT_SIMPLE_TYPE  = 29
    GT_SPIDER       = 30
    GT_TAROCK       = 31
    GT_TERRACE      = 32
    GT_YUKON        = 33
    # extra flags
    GT_BETA          = 1 << 12      # beta version of game driver
    GT_CHILDREN      = 1 << 13
    GT_CONTRIB       = 1 << 14      # contributed games under the GNU GPL
    GT_HIDDEN        = 1 << 15      # not visible in menus, but games can be loaded
    GT_OPEN          = 1 << 16
    GT_ORIGINAL      = 1 << 17
    GT_POPULAR       = 1 << 18
    GT_RELAXED       = 1 << 19
    GT_SCORE         = 1 << 20      # game has some type of scoring
    GT_SEPARATE_DECKS = 1 << 21
    GT_XORIGINAL     = 1 << 22      # original games by other people, not playable

    SELECT_GAME_BY_TYPE = (
        ("Baker's Dozen type",  lambda gi, gt=GT_BAKERS_DOZEN: gi.si.game_type == gt),
        ("Beleaguered Castle type", lambda gi, gt=GT_BELEAGUERED_CASTLE: gi.si.game_type == gt),
        ("Canfield type",       lambda gi, gt=GT_CANFIELD: gi.si.game_type == gt),
        ("Fan type",            lambda gi, gt=GT_FAN_TYPE: gi.si.game_type == gt),
        ("Forty Thieves type",  lambda gi, gt=GT_FORTY_THIEVES: gi.si.game_type == gt),
        ("FreeCell type",       lambda gi, gt=GT_FREECELL: gi.si.game_type == gt),
        ("Golf type",           lambda gi, gt=GT_GOLF: gi.si.game_type == gt),
        ("Gypsy type",          lambda gi, gt=GT_GYPSY: gi.si.game_type == gt),
        ("Klondike type",       lambda gi, gt=GT_KLONDIKE: gi.si.game_type == gt),
        ("Montana type",        lambda gi, gt=GT_MONTANA: gi.si.game_type == gt),
        ("Napoleon type",       lambda gi, gt=GT_NAPOLEON: gi.si.game_type == gt),
        ("Numerica type",       lambda gi, gt=GT_NUMERICA: gi.si.game_type == gt),
        ("Pairing type",        lambda gi, gt=GT_PAIRING_TYPE: gi.si.game_type == gt),
        ("Raglan type",         lambda gi, gt=GT_RAGLAN: gi.si.game_type == gt),
        ("Simple games",        lambda gi, gt=GT_SIMPLE_TYPE: gi.si.game_type == gt),
        ("Spider type",         lambda gi, gt=GT_SPIDER: gi.si.game_type == gt),
        ("Terrace type",        lambda gi, gt=GT_TERRACE: gi.si.game_type == gt),
        ("Yukon type",          lambda gi, gt=GT_YUKON: gi.si.game_type == gt),
        ("One-Deck games",      lambda gi, gt=GT_1DECK_TYPE: gi.si.game_type == gt),
        ("Two-Deck games",      lambda gi, gt=GT_2DECK_TYPE: gi.si.game_type == gt),
        ("Three-Deck games",    lambda gi, gt=GT_3DECK_TYPE: gi.si.game_type == gt),
        ("Four-Deck games",     lambda gi, gt=GT_4DECK_TYPE: gi.si.game_type == gt),
    )

    SELECT_SPECIAL_GAME_BY_TYPE = (
        ("Dashavatara Ganjifa type", lambda gi, gt=GT_DASHAVATARA_GANJIFA: gi.si.game_type == gt),
##        ("Ganjifa type",        lambda gi, gt=(GT_MUGHAL_GANJIFA, GT_NAVAGRAHA_GANJIFA, GT_DASHAVATARA_GANJIFA,): gi.si.game_type in gt),
        ("Hanafuda type",       lambda gi, gt=GT_HANAFUDA: gi.si.game_type == gt),
        ("Hex A Deck type",     lambda gi, gt=GT_HEXADECK: gi.si.game_type == gt),
##        ("Mahjongg type",       lambda gi, gt=GT_MAHJONGG: gi.si.game_type == gt),
        ("Matrix type",         lambda gi, gt=GT_MATRIX: gi.si.game_type == gt),
        ("Mughal Ganjifa type", lambda gi, gt=GT_MUGHAL_GANJIFA: gi.si.game_type == gt),
        ("Navagraha Ganjifa type", lambda gi, gt=GT_NAVAGRAHA_GANJIFA: gi.si.game_type == gt),
        ("Memory type",         lambda gi, gt=GT_MEMORY: gi.si.game_type == gt),
        ("Poker type",          lambda gi, gt=GT_POKER_TYPE: gi.si.game_type == gt),
        ("Puzzle type",         lambda gi, gt=GT_PUZZLE_TYPE: gi.si.game_type == gt),
        ("Tarock type",         lambda gi, gt=GT_TAROCK: gi.si.game_type == gt),
    )

    SELECT_ORIGINAL_GAME_BY_TYPE = (
        ("French type",         lambda gi, gf=GT_ORIGINAL, gt=(GT_HANAFUDA, GT_HEXADECK, GT_MUGHAL_GANJIFA, GT_NAVAGRAHA_GANJIFA, GT_DASHAVATARA_GANJIFA, GT_TAROCK,): gi.si.game_flags & gf and gi.si.game_type not in gt),
        ("Ganjifa type",        lambda gi, gf=GT_ORIGINAL, gt=(GT_MUGHAL_GANJIFA, GT_NAVAGRAHA_GANJIFA, GT_DASHAVATARA_GANJIFA,): gi.si.game_flags & gf and gi.si.game_type in gt),
        ("Hanafuda type",       lambda gi, gf=GT_ORIGINAL, gt=GT_HANAFUDA: gi.si.game_flags & gf and gi.si.game_type == gt),
        ("Hex A Deck type",     lambda gi, gf=GT_ORIGINAL, gt=GT_HEXADECK: gi.si.game_flags & gf and gi.si.game_type == gt),
        ("Tarock type",         lambda gi, gf=GT_ORIGINAL, gt=GT_TAROCK: gi.si.game_flags & gf and gi.si.game_type == gt),
    )

    SELECT_CONTRIB_GAME_BY_TYPE = (
        ("French type",         lambda gi, gf=GT_CONTRIB, gt=(GT_HANAFUDA, GT_HEXADECK, GT_MUGHAL_GANJIFA, GT_NAVAGRAHA_GANJIFA, GT_DASHAVATARA_GANJIFA, GT_TAROCK,): gi.si.game_flags & gf and gi.si.game_type not in gt),
        ("Ganjifa type",        lambda gi, gf=GT_CONTRIB, gt=(GT_MUGHAL_GANJIFA, GT_NAVAGRAHA_GANJIFA, GT_DASHAVATARA_GANJIFA,): gi.si.game_flags & gf and gi.si.game_type in gt),
        ("Hanafuda type",       lambda gi, gf=GT_CONTRIB, gt=GT_HANAFUDA: gi.si.game_flags & gf and gi.si.game_type == gt),
        ("Hex A Deck type",     lambda gi, gf=GT_CONTRIB, gt=GT_HEXADECK: gi.si.game_flags & gf and gi.si.game_type == gt),
        ("Tarock type",         lambda gi, gf=GT_CONTRIB, gt=GT_TAROCK: gi.si.game_flags & gf and gi.si.game_type == gt),
    )

    # These obsolete gameids have been used in previous versions of
    # PySol and are no longer supported because of internal changes
    # (mainly rule changes). The game has been assigned a new id.
    PROTECTED_GAMES = {
         22:  106,              # Double Canfield
         32:  901,              # La Belle Lucie (Midnight Oil)
         52:  903,              # Aces Up
         72:  115,              # Little Forty
         75:  126,              # Red and Black
         82:  901,              # La Belle Lucie (Midnight Oil)
##        155: 5034,              # Mahjongg - Flying Dragon
##        156: 5035,              # Mahjongg - Fortress Towers
        262:  105,              # Canfield
        902:   88,              # Trefoil
        904:   68,              # Lexington Harp
    }

    GAMES_BY_COMPATIBILITY = (
        # Atari ST Patience game v2.13 (we have 10 out of 10 games)
        ("Atari ST Patience", (1, 3, 4, 7, 12, 14, 15, 16, 17, 39,)),

        ### Gnome AisleRiot 1.0.51 (we have 28 out of 32 games)
        ###   still missing: Camelot, Clock, Thieves, Thirteen
        ##("Gnome AisleRiot 1.0.51", (
        ##    2, 8, 11, 19, 27, 29, 33, 34, 35, 40,
        ##    41, 42, 43, 58, 59, 92, 93, 94, 95, 96,
        ##    100, 105, 111, 112, 113, 130, 200, 201,
        ##)),

        # Gnome AisleRiot 1.4.0.1 (we have XX out of XX games)
        #   still missing: ???
        ("Gnome AisleRiot", (
            1, 2, 8, 11, 19, 27, 29, 33, 34, 35, 40,
            41, 42, 43, 58, 59, 92, 93, 94, 95, 96,
            100, 105, 111, 112, 113, 130, 200, 201,
        )),

        ### KDE Patience 0.7.3 from KDE 1.1.2 (we have 6 out of 9 games)
        ##("KDE Patience 0.7.3", (2, 7, 8, 18, 256, 903,)),

        # KDE Patience 2.0 from KDE 2.1.2 (we have 11 out of 13 games)
        #   still missing: ???
        ("KDE Patience", (1, 2, 7, 8, 18, 19, 23, 50, 256, 261, 903,)),

        ### KDE Patience 2.0 from KDE 2.2beta1 (we have 12 out of 14 games)
        ###   still missing: ???
        ##("KDE Patience", (1, 2, 7, 8, 18, 19, 23, 36, 50, 256, 261, 903,)),

        # xpat2 1.06 (we have 14 out of 16 games)
        #   still missing: ???
        ("xpat2", (
            1, 2, 8, 9, 11, 31, 54, 63, 89, 105, 901, 256, 345, 903,
        )),
    )

    GAMES_BY_PYSOL_VERSION = (
        ("1.00", (1, 2, 3, 4)),
        ("1.01", (5, 6)),
        ("1.02", (7, 8, 9)),
        ("1.03", (10, 11, 12, 13)),
        ("1.10", (14,)),
        ("1.11", (15, 16, 17)),
        ("2.00", (256, 257)),
        ("2.01", (258, 259, 260, 261)),
        ("2.02", (105,)),
        ("2.90", (18, 19, 20, 21, 106, 23, 24, 25, 26, 27,
                  28, 29, 30, 31, 901, 33, 34, 35, 36)),
        ("2.99", (37,)),
        ("3.00", (38, 39,
                  40, 41, 42, 43,     45, 46, 47, 48, 49,
                  50, 51,903, 53, 54, 55, 56, 57, 58, 59,
                  60, 61, 62, 63, 64, 65, 66, 67, 68, 69,
                  70, 71,115, 73, 74,126, 76, 77, 78, 79,
                  80, 81,     83, 84, 85, 86, 87, 88, 89,
                  90, 91, 92, 93, 94, 95, 96, 97, 98, 99,
                  100, 101, 102, 103, 104, 107, 108,)),
        ("3.10", (109, 110, 111, 112, 113, 114, 116, 117, 118, 119,
                  120,-121,-122, 123, 124, 125, 127)),
        ("3.20", (128, 129, 130, 131, 132, 133, 134, 135, 136, 137,
                  138, 139, 140, 141, 142,
                  345, 346, 347, 348, 349, 350, 351, 352)),
        ("3.21", (143, 144)),
        ("3.30", (145, 146, 147, 148, 149, 150, 151)),
        ("3.40", (152, 153, 154)),
        ("4.00", (          157, 158, 159, 160, 161, 162, 163, 164)),
        ("4.20", (165, 166, 167, 168, 169, 170, 171, 172, 173, 174,
                  175, 176, 177, 178)),
        ("4.30", (179, 180, 181, 182, 183, 184)),
        ("4.41", (185, 186,-187,-188,-189,-190,-191,-192, 193,-194,
                  195, 196,-197,-198, 199)),
        ("4.60", (200, 201, 202, 203, 204, 205)),
##                  206, 207, 208, 209,
##                  210, 211, 212, 213, 214, 215, 216, 217, 218, 219,
##                  220, 221, 222, 223, 224, 225, 226, 227, 228, 229,
##                  230, 231, 232, 233, 234, 235, 236)
##                  ## + tuple(range(353, 370)),
##                  ),
        ("4.70", (237,)),
    )

    # deprecated - the correct way is to or a GI.GT_XXX flag
    # in the registerGame() call
    _CHILDREN_GAMES = [16, 33, 55, 90, 91, 96, 97, 176, 903,]
    _OPEN_GAMES = [
        5, 6, 8, 9, 26, 31, 45, 46, 50, 53, 63, 64,
        77, 85, 86, 96, 116, 117, 118, 258,
    ]
    _POPULAR_GAMES = [
        1, 2, 7, 8, 11, 12, 13, 14, 19, 31, 36, 38, 105, 158,
        345, 903,
    ]


    #
    # sanity check
    #

    def assertGI(self, manager=None):
        for key, games in self.GAMES_BY_COMPATIBILITY:
            games = filter(lambda id: id >= 0, games)
            print "%-20s: %d games" % (key, len(games))
            for id in games[:]:
                assert type(id) is types.IntType
                if self.PROTECTED_GAMES.get(id):
                    print id; assert 0, id
                if manager and not manager.get(id):
                    print id; assert 0, id
        #
        all_games = []
        for key, games in self.GAMES_BY_PYSOL_VERSION:
            games = filter(lambda id: id >= 0, games)
            print "%s: %3d games, %3d total" % (key, len(games), len(all_games) + len(games))
            for id in games[:]:
                assert type(id) is types.IntType
                if self.PROTECTED_GAMES.get(id):
                    print id; assert 0, id
                if id in all_games:
                    print id; assert 0, id
                if manager and not manager.get(id):
                    print id; assert 0, id
            all_games.extend(list(games))
        print "total:", len(all_games), "games"
        if manager:
            g = manager.getGamesIdSortedById()
            g = filter(lambda id, m=manager: m.get(id).plugin == 0, g)
            g = list(g)
            for id in all_games:
                g.remove(id)
            if g:
                print "WARNING: games without version:", g
                ##assert 0


# /***********************************************************************
# // core games database
# ************************************************************************/

class GameInfoException(Exception):
    pass

class GameInfo(Struct):
    def __init__(self, id, gameclass, name,
                 game_type, decks, redeals,
                 # keyword arguments:
                 si={}, category=0, short_name=None, altnames=None,
                 suits=range(4), ranks=range(13), trumps=(),
                 rules_filename=None):
        #
        ncards = decks * (len(suits) * len(ranks) + len(trumps))
        game_flags = game_type & ~1023
        game_type = game_type & 1023
        if os.name == "mac":
            name = latin1_to_ascii(name)
        if not short_name:
            short_name = name
        if type(altnames) is types.StringType:
            altnames = (altnames,)
        if not altnames:
            altnames = ()
        #
        if not (1 <= id <= 999999):
            raise GameInfoException, name + ": invalid game ID " + str(id)
        if not (1 <= decks <= 4):
            raise GameInfoException, name + ": invalid number of decks " + str(id)
        if not name or not (2 <= len(short_name) <= 25):
            raise GameInfoException, name + ": invalid game name"
        if GI.PROTECTED_GAMES.get(id):
            raise GameInfoException, name + ": protected game ID " + str(id)
        #
        for f, l in ((GI.GT_CHILDREN, GI._CHILDREN_GAMES),
                     (GI.GT_OPEN, GI._OPEN_GAMES),
                     (GI.GT_POPULAR, GI._POPULAR_GAMES)):
            if (game_flags & f) and (id not in l):
                l.append(id)
            elif not (game_flags & f) and (id in l):
                game_flags = game_flags | f
        #
        if not (1 <= category <= 9):
            if game_type == GI.GT_HANAFUDA:
                category = GI.GC_HANAFUDA
            elif game_type == GI.GT_TAROCK:
                category = GI.GC_TAROCK
            elif game_type == GI.GT_MAHJONGG:
                category = GI.GC_MAHJONGG
            elif game_type == GI.GT_HEXADECK:
                category = GI.GC_HEXADECK
            elif game_type == GI.GT_MUGHAL_GANJIFA:
                category = GI.GC_MUGHAL_GANJIFA
            elif game_type == GI.GT_NAVAGRAHA_GANJIFA:
                category = GI.GC_NAVAGRAHA_GANJIFA
            elif game_type == GI.GT_DASHAVATARA_GANJIFA:
                category = GI.GC_DASHAVATARA_GANJIFA
            else:
                category = GI.GC_FRENCH
        # si is the SelectionInfo struct that will be queried by
        # the "select game" dialogs. It can be freely modified.
        gi_si = Struct(game_type=game_type, game_flags=game_flags,
                       decks=decks, redeals=redeals, ncards=ncards)
        gi_si.update(si)
        #
        Struct.__init__(self, id=id, gameclass=gameclass,
                        name=name, short_name=short_name,
                        altnames=tuple(altnames),
                        decks=decks, redeals=redeals, ncards=ncards,
                        category=category,
                        suits=tuple(suits), ranks=tuple(ranks), trumps=tuple(trumps),
                        si=gi_si, rules_filename=rules_filename, plugin=0)


class GameManager:
    def __init__(self):
        self.__selected_key = -1
        self.__games = {}
        self.__gamenames = {}
        self.__games_by_id = None
        self.__games_by_name = None
        self.__games_by_short_name = None
        self.__games_by_altname = None
        self.__all_games = {}           # includes hidden games
        self.__all_gamenames = {}       # includes hidden games
        self.loading_plugin = 0
        self.registered_game_types = {}

    def getSelected(self):
        return self.__selected_key

    def setSelected(self, gameid):
        assert self.__all_games.has_key(gameid)
        self.__selected_key = gameid

    def get(self, key):
        return self.__all_games.get(key)

    def register(self, gi):
        if not isinstance(gi, GameInfo):
            raise GameInfoException, "wrong GameInfo class"
        gi.plugin = self.loading_plugin
        if self.__all_games.has_key(gi.id):
            raise GameInfoException, "duplicate game ID " + str(gi.id)
        if self.__all_gamenames.has_key(gi.name):
            raise GameInfoException, "duplicate game name " + str(gi.id) + ": " + game.name
        if 1:
            for id, game in self.__all_games.items():
                if gi.gameclass is game.gameclass:
                    raise GameInfoException, "duplicate game class " + str(gi.id)
        for n in gi.altnames:
            if self.__all_gamenames.has_key(n):
                raise GameInfoException, "duplicate altgame name " + str(gi.id) + ": " + n
        if gi.si.game_flags & GI.GT_XORIGINAL:
            return
        if 1 and (206 <= gi.id <= 236): ### and bundle & 1:
            ##print gi.id
            return
        self.__all_games[gi.id] = gi
        self.__all_gamenames[gi.name] = gi
        for n in gi.altnames:
            self.__all_gamenames[n] = gi
        if not (gi.si.game_flags & GI.GT_HIDDEN):
            self.__games[gi.id] = gi
            self.__gamenames[gi.name] = gi
            for n in gi.altnames:
                self.__gamenames[n] = gi
            # invalidate sorted lists
            self.__games_by_id = None
            self.__games_by_name = None
            # update registry
            k = gi.si.game_type
            self.registered_game_types[k] = self.registered_game_types.get(k, 0) + 1


    #
    # access games database - we do not expose hidden games
    #

    def getGamesIdSortedById(self):
        if self.__games_by_id is None:
            l = self.__games.keys()
            l.sort()
            self.__games_by_id = tuple(l)
        return self.__games_by_id

    def getGamesIdSortedByName(self):
        if self.__games_by_name is None:
            l1, l2, l3  = [], [], []
            for id, gi in self.__games.items():
                name = string.lower(latin1_to_ascii(gi.name))
                l1.append((name, id))
                if gi.name != gi.short_name:
                    name = string.lower(latin1_to_ascii(gi.short_name))
                l2.append((name, id))
                for n in gi.altnames:
                    name = string.lower(latin1_to_ascii(n))
                    l3.append((name, id, n))
            l1.sort()
            l2.sort()
            l3.sort()
            self.__games_by_name = tuple(map(lambda item: item[1], l1))
            self.__games_by_short_name = tuple(map(lambda item: item[1], l2))
            self.__games_by_altname = tuple(map(lambda item: item[1:], l3))
        return self.__games_by_name

    def getGamesIdSortedByShortName(self):
        if self.__games_by_name is None:
            self.getGamesIdSortedByName()
        return self.__games_by_short_name

    # note: this contains tuples as entries
    def getGamesTuplesSortedByAlternateName(self):
        if self.__games_by_name is None:
            self.getGamesIdSortedByName()
        return self.__games_by_altname


# /***********************************************************************
# //
# ************************************************************************/

# the global game database (the single instance of class GameManager)
GAME_DB = GameManager()


def registerGame(gameinfo):
    GAME_DB.register(gameinfo)
    return gameinfo


def loadGame(modname, filename, plugin=1):
    ##print "load game", modname, filename
    GAME_DB.loading_plugin = plugin
    if sys.modules.has_key("gamedb"):
        # unbundled
        module = imp.load_source(modname, filename)
    else:
        # bundled
        execfile(filename, globals(), globals())

