A Python package for generating custom Magic The Gathering cards
from . import config

from os import remove,rename,mkdir,chdir,remove
from os.path import isfile,isdir,basename
from pathlib import Path
from subprocess import Popen,DEVNULL
from shutil import copy,rmtree

cfg = config.config

class Card:
    
    __fieldNames = ['name','text','type','super_type','casting_cost','pt','card_color','rarity','illustrator','set_code']
        
    def __init__(self,image=None,name="[name]",text="[text]",type="[type]",superType="[superType]",castingCost=1,power=1,toughness=1,rarity="Common",color="Blue",illustrator="[illustrator]",setCode="[setCode]"):
        
        self.name = name
        self.text = text
        self.type = type
        self.superType = superType
        self.castingCost = castingCost
        self.power = power
        self.toughness = toughness
        self.rarity = rarity
        self.color = color
        self.illustrator = illustrator
        self.setCode = setCode
        self.image = image
        
        self.__formattedFields = {}
        
        for fieldName in Card.__fieldNames:
            self.__formattedFields.setdefault(fieldName)
            
    def __formatFields(self):
        self.__formattedFields['name'] = f"\"{self.name}\""
        self.__formattedFields['text'] = f"\"{self.text}\""
        self.__formattedFields['type'] = f"\"{self.type}\""
        self.__formattedFields['super_type'] = f"\"{self.superType[0].upper()}{self.superType[1:]}\""
        self.__formattedFields['casting_cost'] = f"\"{self.castingCost}\""
        self.__formattedFields['pt'] = f"\"{self.power}/{self.toughness}\""
        self.__formattedFields['card_color'] = f"\"{self.color.lower()}\""
        self.__formattedFields['rarity'] = f"\"{self.rarity.lower()}\""
        self.__formattedFields['illustrator'] = f"\"{self.illustrator}\""
        self.__formattedFields['set_code'] = f"\"{self.setCode}\""
        
    def __generateNewCardParamsString(self):
        formattedParams = [f"{fieldName}: {value}" for fieldName, value in self.__formattedFields.items()]
        return "[" + ", ".join(formattedParams) + "]"
    
    def __checkImageValidity(self):
        return self.image is not None and type(self.image) is str and isfile(self.image) and ( self.image.endswith(".jpg") or self.image.endswith(".png") )
    
    def __getImageInfo(self):
        imagePath = Path(self.image)
        imageName = basename(self.image)
        imageFileExtension = imageName[-3:]
        return (imagePath,imageName,imageFileExtension)

    def export(self,fileName="card.jpg"):
        
        self.__formatFields()
        paramsString = self.__generateNewCardParamsString()
        cardWriteCommand = f":load set.mse-set\nmy_card := new_card({paramsString})\nwrite_image_file(my_card, file: \"{fileName}\")"
        mseFolderPath = cfg['file-locations']['mse-folder']
        
        if isdir(mseFolderPath):
            
            mseFolderPath = Path(mseFolderPath)
            tempDirectory = Path(mseFolderPath / 'temp')
            isValidImage = self.__checkImageValidity()
            defaultImageScriptReplaced = False

            mkdir(tempDirectory)
            copy( config.rootDirectory / 'include' / 'set.mse-set', mseFolderPath )
            
            if isfile(mseFolderPath / 'data' / 'magic-default-image.mse-include' / 'scripts') and isValidImage:
                imagePath,imageName,imageFileExtension = self.__getImageInfo()
                if isfile(mseFolderPath / 'data' / 'magic-default-image.mse-include' / f'custom.{imageFileExtension}'):
                    remove(mseFolderPath / 'data' / 'magic-default-image.mse-include' / f'custom.{imageFileExtension}')
                copy(imagePath, mseFolderPath / 'data' / 'magic-default-image.mse-include')
                chdir(mseFolderPath / 'data' / 'magic-default-image.mse-include')
                rename(mseFolderPath / 'data' / 'magic-default-image.mse-include' / imageName, f'custom.{imageFileExtension}')
                copy(mseFolderPath / 'data' / 'magic-default-image.mse-include' / 'scripts', tempDirectory)
                remove(mseFolderPath / 'data' / 'magic-default-image.mse-include' / 'scripts')
                copy(config.rootDirectory / 'include' / f'custom-image-script-{imageFileExtension}', mseFolderPath / 'data' / 'magic-default-image.mse-include')
                rename(mseFolderPath / 'data' / 'magic-default-image.mse-include' / f'custom-image-script-{imageFileExtension}', 'scripts')
                chdir(mseFolderPath)
                defaultImageScriptReplaced = True
                
        else:
            mseFolderPath = None
            
        if mseFolderPath:
            with open(tempDirectory / 'ezmse-in.txt','w') as f:
                f.writelines(iter(cardWriteCommand))

            with open(tempDirectory / 'ezmse-in.txt','r') as f:
                with Popen([str(mseFolderPath / 'magicseteditor.com'),'--cli'],stdin=f,stdout=DEVNULL):
                    pass
            if defaultImageScriptReplaced:
                remove(mseFolderPath / 'data' / 'magic-default-image.mse-include' / 'scripts')
                copy(tempDirectory / 'scripts', mseFolderPath / 'data' / 'magic-default-image.mse-include')
                
            rmtree(tempDirectory)
            remove(mseFolderPath / 'set.mse-set')
        else:
            print("no path found to Magic Set Editor :(")
ezmse/__init__.py
TomTkacz/EasyMSE
from configparser import ConfigParser
from os.path import isfile,isdir
from os import getcwd,listdir
from pathlib import Path

rootDirectory = Path(__file__).parent.resolve()
config = ConfigParser()
config.read(rootDirectory / 'include' / 'config.ini')

def update():
    with open(rootDirectory / 'include' / 'config.ini', 'w') as f:
        config.write(f)
    config.read(rootDirectory / 'include' / 'config.ini')
    
def wipe(section=None,option=None):
    if not section:
        for section in config.sections():
            for option in config.options(section):
                config[section][option] = ''
    elif not option:
        for option in config.options(section):
                config[section][option] = ''
    config[section][option] = ''
    update()
    
def setSetLocation(path):
    if type(path) is str:
        path = Path(path)
    if isdir(path):
        for file in listdir(path):
            if file.endswith(".mse-set"):
                path = path / file
                break
    elif not (isfile(path) and str(path).endswith(".mse-set")):
        return
    config['file-locations']['mse-set'] = str(path)
    update()

def setMSEFolder(path):
    if type(path) is str:
        path = Path(path)
    if isdir(path/'data') and isdir(path/'resource') and isfile(path/'magicseteditor.com') and isfile(path/'magicseteditor.exe'):
        config['file-locations']['mse-folder'] = str(path)
        config['file-locations']['mse-exe'] = str(path / 'magicseteditor.exe')
        config['file-locations']['mse-com'] = str(path / 'magicseteditor.com')
        update()
    else:
        print("folder must contain the files 'magicseteditor.exe' 'magicseteditor.com' and the directories 'data' 'resource'")

if config['file-locations']['mse-folder'] == '':
    setMSEFolder(Path(getcwd()))
     
if config['file-locations']['mse-set'] == '' and isfile(rootDirectory / 'include' / 'set.mse-set'):
    config['file-locations']['mse-set'] = str(rootDirectory / 'include' / 'set.mse-set')
    update()
ezmse/config.py
TomTkacz/EasyMSE
from pathlib import Path
from os import getcwd
import zipfile

class SetHandler():
    
    def __init__(self,setPath):
        self.path = Path(setPath) if type(setPath) is str else setPath
            
    def getStylePath(self):
        archive = zipfile.ZipFile(str(self.path),'r')
        setData = archive.read("set")
        return Path(getcwd()) / 'data' / f'magic-{parse(setData)["stylesheet"]}.mse-style'
    
# TODO: convert dictionaries with duplicate keys to lists... somehow
def parse(text):
        
    result = {}
    keyStack = []
    lastIndent = -1
    plainText = False
    scriptIndent = -1
    key = None
    value = None
    currentDict = None
    
    try:
        lines = bytes.decode(text,encoding="utf-8-sig").rstrip().split('\n')
    except:
        lines = text.splitlines(True)
    
    for line in lines:
        
        if line.strip() == '':
            continue
        
        lineStripped = line.strip()
        if lineStripped.startswith('#'):
            continue
                    
        indent = len(line)-len(line.lstrip())
        
        if indent <= scriptIndent:
            plainText = False
            scriptIndent = -1
        
        if currentDict and not plainText:
            if indent < lastIndent:
                for _ in range(lastIndent-indent):
                    if not len(keyStack)>0:
                        break
                    keyStack.pop()
            elif type(currentDict.get(key)) is dict and indent == lastIndent and len(keyStack)>0:
                keyStack.pop()
        
        if indent == 0:
            currentDict = result
            keyStack = []
        else:
            d = result
            for k in keyStack:
                child = d.get(k)
                if type(child) is dict:
                    d = d.get(k)
                else:
                    break
            currentDict = d

        if not plainText:
            keyValueList = lineStripped.split(':', 1)
            if len(keyValueList) == 1:
                key = keyValueList[0][:-1].strip()
                value = None
            elif len(keyValueList) == 2:
                key,value = tuple(keyValueList)
                key = key.strip()
                value = value.strip()
                    
            if not value:
                currentDict.setdefault(key,{})
                keyStack.append(key)
            else:
                currentDict.setdefault(key,value)
        else:
            try:
                currentDict['code'].append(lineStripped)
            except:
                currentDict.setdefault('code',[lineStripped])
                
        if 'script:' in line and not plainText:
            plainText = True
            scriptIndent = indent

        lastIndent = indent
                    
    return result
ezmse/file.py
TomTkacz/EasyMSE
from setuptools import setup
from setuptools import find_packages
from configparser import ConfigParser
from pathlib import Path

this_directory = Path(__file__).parent
long_description = (this_directory / "README.md").read_text()

setup(
    name='ezmse',
    version='0.1.2',
    description='A Python package for generating custom Magic The Gathering cards',
    author='Tom Tkacz',
    author_email='thomasatkacz@gmail.com',
    url='https://github.com/TomTkacz/EasyMSE',
    packages=find_packages(),
    long_description=long_description,
    long_description_content_type='text/markdown',
    package_data={
        'ezmse': ['include/*'],
    },
    install_requires=[
        'pyfakefs>=5.2'
    ],
    extras_require={
        'dev': [
            'pytest>=7.4',
        ]
    }
)

# easy way to ensure config.ini is cleared before deployment (for development)
try:
    pathToConfig = 'ezmse/include/config.ini'
    config = ConfigParser()
    config.read(pathToConfig)

    for section in config.sections():
        for option in config.options(section):
            config[section][option] = ''

    with open(pathToConfig, 'w') as f:
        config.write(f)
except:
    pass
setup.py
TomTkacz/EasyMSE
from ezmse import Card
from pytest import fixture
from pyfakefs.fake_filesystem_unittest import Patcher

@fixture
def fs():
    with Patcher() as patcher:
        yield patcher.fs

@fixture()
def card():
    return Card()
    
def test_generateNewCardParamsString(card):
    card.name = "TestName"
    card.text = "TestText"
    paramsString = card._Card__generateNewCardParamsString()
    assert paramsString.find('name: "TestName"') and paramsString.find('text: "TestText"')
    
def test_checkImageValidity(card,fs):
    fs.create_file("myimage.jpg")
    card.image = "myimage.jpg"
    assert card._Card__checkImageValidity()
    
def test_checkImageValidity_subDirectory(card,fs):
    fs.create_file("src/myimage.jpg")
    card.image = "src/myimage.jpg"
    assert card._Card__checkImageValidity()
tests/test_card.py
TomTkacz/EasyMSE