Differences between revisions 12 and 13
Revision 12 as of 2011-02-05 22:33:38
Size: 62347
Editor: 125
Comment:
Revision 13 as of 2011-02-06 19:59:14
Size: 1809
Editor: PaulBoddie
Comment: Revert vandalism.
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
# -*- coding: utf-8 -*-
################################################################
## LICENCING / LEGAL
################################################################
## Disclaimers:
## - This script is provided as is, without any warranty.
## - If you think your sVERSION isn't an original sVERSION, contact the authors.
## - Authors of this script are not responsible of its usages and results.
## - The script licence can be modified at any time without warnings.
## - Source code is intellectual property of its authors.
##
## Distribution:
## - This script cannot be distributed, shared, packed, ... in any manner without authors' agreement.
## - Doing links to authorized hosts are allowed, as long as you send this information to the authors
## ( in order them to be warned of bugs, thanks and suggestions )
## - Authorized hosts list ( on Apr. 25 2010 )
## - www.dragonagenexus.com
## - social.bioware.com
## Usage:
## - This script is free to use for non-commercial purposes.
## - Any commercial use needs authors' agreement.
## - Importing elements of source code is allowed for non-commercial purpose.
##
## Modification:
## - Script modifications are allowed for personal use only.
## - Modified sVERSIONs cannot be distributed without agreement of authors.
## - If you've made a better sVERSION and want to share it, contact the authors.
##
## Involvment:
## - Testers and coders are welcome to participate to this script. Contact authors for more informations.
##
## Help and questions:
## - Contact the authors for help about using this script.
## - Contact the authors for any other questions about this script or this licence.
##
################################################################
= Python Projects =
Line 38: Line 3:
################################################################
## History
################################################################
sAPPNAME="DragonAge Face Replacer"
sVERSION = "2.08"
# 0.10 : Very first version
# 0.11 : Fixed a bug on file mismatch, added some infos, changing processus method
# 0.12 : added a Tkinter UI for file selection, old method is still valuable set UITOOL to False to enable
# 0.20 : Now able to exchange with different filesizes ( MOR is written at the end of the file )
# 0.21 : Able to import from mor file instead of savegame
# 0.22 : 0.21 bugfix and eyecandy
# 1.00 : Final version, now able to retrieve MOR files from ERF resources
# 1.01 : Adding some tweaking for destination savegames ( name ), as XunAmarox suggested
# 1.02 : Some bugfix and eyecandy (list scrollbar), added inventory size tweaking ( always suggested by XunAmarox )
# 1.03 : Fixed some errors appearing with 1.02 and the separation between tweak and exchange face
# 1.04 : Added compatibilty with Awakening and some save formats
# 1.10 : Now save the path of the source and destination file, fixed the non-acsii names and paths
# 1.11 : Fix 1.10 problem when trying to launch with bad paths and weird name problems
# 1.20 : Now handle characters and saves in one window. UI improvements. Fixes problem with non-ascii paths
# 1.30 : Now you can edit the face : model parts & tints. Log is less verbose
# 2.00 : 1.30 version with reworked UI and handling
# 2.01 : Fixes major bug in 2.00 with autocheck for characters and resource files
# 2.02 : Fixes 2.01 problems in exchange of features, add more clearer paths and a "reset face" button
# 2.03 : Fixes a script encoding error to save path, name exchange problems, features missing
# 2.04 : Allowing to see source features values, limit inventory size, reworked code to handle a CheckList, more reliable log
# 2.05 : Last bugfix of 2.0x serie, now things are 100% OK
# 2.06 : Fixes a write error on savegames that prevent faces to be updated in some cases. Now faces are only appended, not replaced.
# Fixes also an error in getting the last modified face
# 2.07 : Simple fix about name problem: now if the name found is greater than nMAX_NAME, name cannot be changed (errors)
# sAUTO_SELECT savegame is auto selected in character folder (based on a suggestion by setiweb)
# Tint 11 & 12 is now eyebrow texture (thanks to setiweb)
# 2.08 : 2.07 bugfix and code rewriting. It seems that some features (tatooes, age map) are not into features list
# Also, now edited files can be put in another folder, use nSAVE_METHOD to achieve this
################################################################
## EXTERNAL MODULES
################################################################
# Python modules
import struct,os
import os.path as OP
import struct
import shutil
# Tkinter modules
from Tkinter import * # Tkinter main
import tkMessageBox as TKMB # Message box
import tkFileDialog as TKFD # File dialog
import tkFont
import Tix # Tkinter extension
from Tix import CheckList # Checklist
################################################################
## USER PARAMETERS & CONSTANTS
################################################################
# Log level (0: errors only, 1: with informations, 2: with program flow)
nLOG_LEVEL=0
# Save method : Set to
# -1 to replace save witout backup
# 0 to replace with making a backup
# 1 to create a new slot
nSAVE_METHOD=1
# previous paths for inputs
# Default ERF resource may be "C:\MyGames\Dragon Age\packages\core\data\face.erf"
pLAST_CHR=""
pLAST_DAS=""
pLAST_MOR=""
pLAST_ERF=""
# set to the directory of the characters in "(My Documents)\Bioware\Dragon Age\Characters"
pCHARACTERS_DIR=""
# Maximum inventory size allowed
nMAX_INVSIZE=100000
# Max name size (only for file reading)
nMAX_NAME=32
# select this savegame rather than first alphabetical save
sAUTO_SELECT="QuickSave_1"
As a programming language, Python is the foundation of many software projects producing things like applications, libraries, modules and packages. Since so many projects exist, it can be difficult to find one which is concentrating on a particular topic of interest, but there are several starting points:
Line 111: Line 5:
# Do not modify these lines
nREL_OFFSET_INVSIZE=168
sPYTHON_FILE="DAFaceReplacer%s.py"%sVERSION.replace(".","")
aMOR_FEATURES_PARTS=["P_head","P_eyes","P_hair","P_beard","P_part5","P_lashes"]
aMOR_FEATURES_TINTS=["T_skin","T_lips","T_eyes","T_hair","T_eyelids","T_blush",
                    "T_tatoo1","T_tatoo2","T_tatoo3","T_tatoo4","T_tint11","T_tint12"]
# Lines commented are features wanting a color mask editing
aMOR_EDIT=(("Hair model",aMOR_FEATURES_PARTS[2]),
          ("Beard model",aMOR_FEATURES_PARTS[3]),
          ("Eye model",aMOR_FEATURES_PARTS[1]),
          ("Lashes model",aMOR_FEATURES_PARTS[5]),
          ("Head model",aMOR_FEATURES_PARTS[0]),
          ("Skin color",aMOR_FEATURES_TINTS[0]),
          ("Hair color",aMOR_FEATURES_TINTS[3]),
          ("Eyes color",aMOR_FEATURES_TINTS[2]),
          ("Eye Make-up",aMOR_FEATURES_TINTS[4]),
          ("Lips color",aMOR_FEATURES_TINTS[1]),
          ("Blush color",aMOR_FEATURES_TINTS[5]),
          ("Eyebrow color",aMOR_FEATURES_TINTS[10]),
          ("Tatoo color 1",aMOR_FEATURES_TINTS[6]),
          ("Tatoo color 2",aMOR_FEATURES_TINTS[7]),
          ("Tatoo color 3",aMOR_FEATURES_TINTS[8]),
          ("Tatoo color 4",aMOR_FEATURES_TINTS[9]),
          ("?Model #5?",aMOR_FEATURES_PARTS[4]),
          ("?Tint #12?",aMOR_FEATURES_TINTS[11]))
 * [[Applications]] - a list of Python-based applications
 * [[UsefulModules]] - libraries, modules and packages
 * [[PublishingPythonModules]] - places where modules are often publicised
 * [[PythonMed]] - Python Med (along the lines of [[http://wiki.debian.org/DebianMed|DebianMed]]) presents packages that are associated with medicine, pre-clinical research, life science and bio-informatics.
 * [[http://www.sourceforge.net/|SourceForge]] hosts open source Python-based software projects:
   * Browse for projects written on [[http://sourceforge.net/softwaremap/trove_list.php?form_cat=178|Python]]
 * [[http://pythonsource.com|Python Source]] is a directory of open source python projects.
Line 137: Line 13:
sMOR_HEADER="GFF V4.0PC MORPV0.1" == Code Fragments ==
Line 139: Line 15:
if not OP.exists(pLAST_DAS): pLAST_DAS=""
if not OP.exists(pLAST_MOR): pLAST_MOR=""
if not OP.exists(pLAST_ERF): pLAST_ERF=""
if not OP.exists(pCHARACTERS_DIR): pCHARACTERS_DIR=os.getcwd()
Useful code does not always exist in the context of a project - smaller fragments may be published in various places:
Line 144: Line 17:
################################################################
## DAS Savegame Editing
################################################################
def ChangeSaveData(daspath,new_name,new_invsize,morface):
    Log("1INFO: Changing DAS savegame data of %s"%daspath)
    # Open the DAS file
    dasfile=open(daspath,"rb")
    dasdata=dasfile.read()
    dasfile.close()
    # Backup it
    if nSAVE_METHOD==0:
        bak_file=open(daspath+".bak","wb")
        bak_file.write(dasdata)
        bak_file.close()
        Log("2 DAS file backup: %s.bak"%daspath)
    if morface:
        # morface MUST be Update() before to register new data
        dasdata=ChangeFaceData(dasdata,morface.data)
    # try to change name
    dasdata,changed=SetName(dasdata,new_name)
    # replace inv size
    if int(new_invsize)>nMAX_INVSIZE: # limit inventory size
        Log("0ERROR: Inventory size exceed maximum, DAFR limits to %s)"%nMAX_INVSIZE)
        new_invsize=nMAX_INVSIZE
    if int(new_invsize)<0:
        Log("0ERROR: Inventory size below 0, passing")
    else:
        dasdata=SetInvSize(dasdata,new_invsize)
    # write to file
    out_file=open(daspath,"wb")
    out_file.write(dasdata)
    out_file.close()
    return True
 * [[Code]] - a list of small code fragments
 * [[http://aspn.activestate.com/ASPN/Python/Cookbook/|The Python Cookbook]] - a site with pieces of code, rated and commented
Line 178: Line 20:
def ChangeFaceData(dasdata,mordata):
    # TODO : remove old unecessary faces
    Log("1INFO: Changing face in DAS data")
    # get the content offset
    das_content_offset=struct.unpack("I",dasdata[24:28])[0]
    Log("2 DAS content offset=%s"%das_content_offset)
    # find the mor (binary) offset declaration
    offset=dasdata.find("d\x00e\x00f\x00a\x00u\x00l\x00t\x00_\x00p\x00l\x00a\x00y\x00e\x00r\x00")-40
    Log("2 MOR declaration offset @%s"%offset)
    # check if it is the correct offset
    start=struct.unpack("I",dasdata[offset:offset+4])[0]+das_content_offset
    Log("2 Checking MOR face data @%s"%start)
    if dasdata[start+4:start+24]==sMOR_HEADER:
        Log("2 Found MOR face data offset=%s @%s"%(start,offset))
        # change the offset declaration
        new_start=len(dasdata)-das_content_offset
        dasdata=dasdata[0:offset]+struct.pack("I",new_start)+dasdata[offset+4:]
        Log("2 Changed MOR face data offset=%s @%s"%(new_start,offset))
        # append binary data to destination data
        dasdata=dasdata+struct.pack("I",len(mordata))+mordata
        Log("2 MOR face data appended to DAS file")
        Log("2 DAS size changed : %s -> %s)"%(len(dasdata)-len(mordata),len(dasdata)))
        return dasdata
    else:
        Log("0ERROR: MOR offset not found ! Face not changed.")
        return dasdata
== Ideas for New Projects ==
Line 205: Line 22:
def GetName(daspath,log=True):
    # open the file
    if daspath=="": return ""
    if log:Log("1INFO: Get character name in DAS file: %s"%daspath)
    dasfile=open(daspath,"rb")
    dasdata=dasfile.read()
    dasfile.close()
    # get the content offset
    das_content_offset=struct.unpack("I",dasdata[24:28])[0]
    # find the name offset declaration
    offset=dasdata.find("\x07\x00\x00\x00p\x00l\x00a\x00y\x00e\x00r\x00\x00\x00")+20
    name=""
    if offset>=0:
        # get the original name
        mode=0
        length=struct.unpack("I",dasdata[offset:offset+4])[0]
        name=dasdata[offset+4:offset+4+length*2]
        # with python 2.5 and over, use decode function to have name from unicode
        try:
            name=name.decode("u16").strip("\x00")
        except:
            name=name.replace("\x00","")
        if dasdata.count(struct.pack("I",offset-das_content_offset))>1:
            mode=1
            if log:
                Log("2 Found %s possible offsets for name"%dasdata.count(struct.pack("I",offset-das_content_offset)))
                Log("0ERROR: Name cannot be changed in this savegame ! Unable to get the name offset")
            name=""
        if len(name)>nMAX_NAME:
            mode=1
            if log:
                Log("0ERROR: Name cannot be retrieved ! Found name of length %s @ %s : too long, maybe offset error"%(len(name),struct.pack("I",offset)))
            name=""
    if name and log: Log('2 Found character name="%s" @%s'%(name,offset))
    elif log: Log("2 Character name has errors !")
    return name,mode
Sometimes there is no project addressing a certain need, or perhaps a project has been started and requires help to reach its objectives.
Line 242: Line 24:
def SetName(dasdata,newname):
    Log('1INFO: Set character name "%s" in DAS file'%newname)
    # get the content offset
    das_content_offset=struct.unpack("I",dasdata[24:28])[0]
    # find the name of character
    offset=dasdata.find("\x07\x00\x00\x00p\x00l\x00a\x00y\x00e\x00r\x00\x00\x00")+20
    # get the original name
    length=struct.unpack("I",dasdata[offset:offset+4])[0]
    oldname=dasdata[offset+4:offset+4+length*2].replace("\x00","")
    Log("2 Old name : %s"%oldname)
    if dasdata.count(struct.pack("I",offset-das_content_offset))>1:
        Log("2 Found %s possible offsets"%dasdata.count(struct.pack("I",offset-das_content_offset)))
        Message(None,"Unable to change name this for this savegame\nTry with another savegame for this character.","e")
        return dasdata,False
    if oldname!=newname:
        Log('2 New character name : "%s"'%newname)
        # find the name offset declaration
        decl=dasdata.find(struct.pack("I",offset-das_content_offset))
        # new offset at the end
        dasdata=dasdata[0:decl]+struct.pack("I",len(dasdata)-das_content_offset)+dasdata[decl+4:]
        # with python 2.5 and over, use encode function to have unicode name
        try:
            name_u16=newname.encode("u16")[2:]+"\x00\x00"
        except:
            name_u16=""
            for c in newname+"\x00": name_u16+=c+"\x00"
        # append name to dst_data
        dasdata+=struct.pack("I",len(newname)+1)+name_u16
        return dasdata,True
    else:
        Log("2 Character name not changed")
        return dasdata,False
 * [[CodingProjectIdeas]] - a list of ideas for projects of various sizes
Line 275: Line 26:
def GetInvSize(daspath,log=True):
    if log: Log("1INFO: Get Inventory Size in DAS file: %s"%daspath)
    # open the file
    if daspath=="": return ""
    dasfile=open(daspath,"rb")
    dasfile.seek(24)
    # get the content offset
    das_content_offset=struct.unpack("I",dasfile.read(4))[0]
    invsize_offset=das_content_offset+nREL_OFFSET_INVSIZE
    dasfile.seek(invsize_offset)
    invsize=struct.unpack("I",dasfile.read(4))[0]
    dasfile.close()
    if log: Log("2 Inventory Size=%s @%s"%(invsize,invsize_offset))
    return invsize
== Statistics on Projects ==
Line 290: Line 28:
def SetInvSize(dasdata,invsize):
    Log("1INFO: Set Inventory Size %s in DAS file"%invsize)
    # get the content offset
    das_content_offset=struct.unpack("I",dasdata[24:28])[0]
    invsize_offset=das_content_offset+nREL_OFFSET_INVSIZE
    Log("2 Inventory Size @%s"%invsize_offset)
    isd=struct.pack("I",invsize)
    dasdata=dasdata[0:invsize_offset]+isd+dasdata[invsize_offset+4:]
    return dasdata
For those who like statistics, some resources exist cataloguing the size and popularity of some Python projects:
Line 300: Line 30:
def GetMORData(daspath):
    # recover dasdata
    if daspath=="": return ""
    dasfile=open(daspath,"rb")
    Log("1INFO: Open DAS file to get MOR face data = %s"%daspath)
    dasdata=dasfile.read()
    dasfile.close()
    # find morph data
    cnt=dasdata.count(sMOR_HEADER)
    if cnt>1: Log("2 Multiple MOR face data, taking the last")
    start=0
    for i in range(cnt):
        start=dasdata.find(sMOR_HEADER,start+len(sMOR_HEADER))
    if start>=0:
        length=struct.unpack("I",dasdata[start-4:start])[0]
        Log("2 MOR face data @%s length=%s"%(start,length))
        # return morph data
        return dasdata[start:start+length]
    Log("0ERROR: No MOR face data in DAS file")
    return ""

################################################################
## ERF HANDLER
################################################################
class ERFFile():
    def __init__(self,path):
        self.infos={"type":u"",
                   "files":0}
        self.registry={}
        self.path=path
        self.file=None
        if OP.exists(path):
            self.file=open(path,"rb")
            # Fill the parser
            self.file.seek(0)
            head=self.file.read(16)
            head=head.replace("\x00","")
            self.infos["type"]=head
            self.file.seek(16)
            nfiles=struct.unpack("I",self.file.read(4))[0]
            self.infos["files"]=nfiles
            Log("1INFO: Loaded: %s (%s, %s files)"%(path,head,nfiles))
            # Files registry
            self.file.seek(32)
            for i in range(0,nfiles):
                name=self.file.read(64)
                name=name.replace("\x00","")
                offset,lenght=struct.unpack("II",self.file.read(8))
                self.registry[name]=(offset,lenght)
                
    def Search(self,match="",log=False,ext=""):
        if match: Log('1INFO: Searching in ERF "%s"'%match)
        rtn=[]
        if match:
            for f in self.registry.keys():
                if match.lower() in f.lower():
                    if log: Log("2 Match %s"%f)
                    if ext and f.endswith(ext): rtn.append(f)
                    else: rtn.append(f)
        else:
            rtn=self.registry.keys()
        return rtn
    
    def GetFileData(self,name):
        o,l=self.registry[name]
        self.file.seek(o)
        return self.file.read(l)
    
    def Close(self):
        self.file.close()

    # Will come in 2.1x maybe
    def AddFile(self,name,filedata):
        return

    def RemoveFile(self,name):
        return

    def SaveERF(self,path):
        return
################################################################
## MOR HANDLER
################################################################
class MORFile():
    MARK="DAFR"
    ids={2:"NAME",
         23000:"MORPH_PARTS",
         23001:"MORPH_TINTFILENAMES",
         23002:"MORPH_NODES"}
    
    def __init__(self,raw_data="",name="<none>"):
        self.name=name
        self.data=raw_data
        self.mod=False # modified flag
        self.nodes={}
        self.MP={}
        self.MT={}
        if raw_data:
            self._parse()

    def _parse(self):
        # build nodes definition
        ncount=self._read(20,"I")[0]
        for i in range(ncount):
            no,nn,nc,nd=self._read(24+16*i,"I4sII")
            self.nodes[nn]=(no,nc,nd)
# print nodes
        # get the morp node
        mo,mc,md=self.nodes["morp"]
        morp={}
        for i in range(mc):
            mii,mit,mif,mio=self._read(md+12*i,"IHHI")
            morp[self.ids[mii]]=(mit,mif,mio)
# print morp
        # MORPH_PARTS
        # offset of the string list declaration
        offset=self._read(mo+morp["MORPH_PARTS"][2],"I")[0]+mo
        # get the stringlist count & locations
        count=self._read(offset,"I")[0]
        for i in range(count):
            p=aMOR_FEATURES_PARTS[i]
            o=self._read(offset+4*i+4,"I")[0]
            if o!=0xffffffff:
                l=self._read(o+mo,"I")[0]
                s=self._read(o+mo+4,"%ss"%(l*2))[0]
                # s have only ascii chars, so use a replace() instead of decode():
                s=s.replace("\x00","")
            else:
                s=""
            self.MP[p]=s
        self.MP["_off"]=offset
        self.MP["_cnt"]=count
# print self.MP
        # MORPH_TINTFILENAMES
        # offset of the string list declaration
        offset=self._read(mo+morp["MORPH_TINTFILENAMES"][2],"I")[0]+mo
        # get the stringlist count & location
        count=self._read(offset,"I")[0]
        for i in range(count):
            p=aMOR_FEATURES_TINTS[i]
            o=self._read(offset+4*i+4,"I")[0]
            if o!=0xffffffff:
                l=self._read(o+mo,"I")[0]
                s=self._read(o+mo+4,"%ss"%(l*2))[0]
                # s have only ascii chars, so use a replace() instead of decode():
                s=s.replace("\x00","")
            else:
                s=""
            self.MT[p]=s
        self.MT["_off"]=offset
        self.MT["_cnt"]=count
# print self.MT
        
    def _read(self,start,fmt):
        # read %fmt at %start
        length=struct.calcsize(fmt)
        rtn=self.data[start:start+length]
        rtn=struct.unpack(fmt,rtn)
        return rtn
    
    def _replace(self,start,string):
        self.data=self.data[0:start]+string+self.data[start+len(string):]
        
    def Update(self):
        # do not proceed if file not modified
        if self.mod==False:
            return
        Log("1INFO: Updating MOR file")
        # update the data
        mo,mc,md=self.nodes["morp"]
        morp={}
        for i in range(mc):
            mii,mit,mif,mio=self._read(md+12*i,"IHHI")
            morp[self.ids[mii]]=(mit,mif,mio)
        
        # MORPH_PARTS
        start=len(self.data)-mo
        self._replace(mo+morp["MORPH_PARTS"][2],struct.pack("I",start))
        # build string list : declaration
        sl=struct.pack("I",6)
        start+=4+4*6
        tlst=""
        for p in aMOR_FEATURES_PARTS:
# print p
            s=self.MP[p]+"\x00"
            if s!="\x00": # string is defined
# print s,start
                l=len(s)
                ns=""
                for c in s: ns+=c+"\x00"
                sl+=struct.pack("I",start)
                tlst+=struct.pack("I",l)+ns
                start+=4+len(ns)
            else: # string not defined
                sl+=struct.pack("I",0xffffffff)
        # append list to string list declaration
        sl+=tlst
        # append string list to data
        self.data+=sl
        # MORPH_TINTFILENAMES
        # new offset: at the end of file:
        start=len(self.data)-mo
        self._replace(mo+morp["MORPH_TINTFILENAMES"][2],struct.pack("I",start))
        # build string list : declaration
        sl=struct.pack("I",12)
        start+=4+4*12
        tlst=""
        for t in aMOR_FEATURES_TINTS:
# print t
            s=self.MT[t]+"\x00"
            if s!="\x00": # string is defined
# print s,start
                l=len(s)
                ns=""
                for c in s: ns+=c+"\x00"
                sl+=struct.pack("I",start)
                tlst+=struct.pack("I",l)+ns
                start+=4+len(ns)
            else: # string not defined
                sl+=struct.pack("I",0xffffffff)
        # append list to string list declaration
        sl+=tlst
        # append string list to data
        self.data+=sl

        # append a mark to show that MOR is edited
        self.data+=self.MARK
        Log("1INFO: MOR file size=%s"%len(self.data))

    def Save(self,path):
        # Save MORFile.data to path
        if OP.exists(path):
            Log("1INFO: Backing up MOR file")
            fin=open(path,"rb")
            txt=fin.read()
            fin.close()
            fout=open(path+".bak","wb")
            fout.write(txt)
            fout.close()
        fout=open(path,"wb")
        fout.write(self.data)
        fout.close()
        
    def GetString(self,part):
        # Get String for part
        if part in self.MP.keys(): return self.MP[part]
        if part in self.MT.keys(): return self.MT[part]
        return ""

    def SetString(self,part,string):
        # Set String for part
        self.mod=True
        if part in self.MP.keys(): self.MP[part]=string
        elif part in self.MT.keys(): self.MT[part]=string
        else: self.mod=False
        
################################################################
## INTERFACE
################################################################
class DAFR(Frame):
    HELP='''Source\t-> Destination:
-----------------------------
None\t-> *.das
-Edit face features, name, and inventory size

*.das\t-> *.das
*.mor\t-> *.das
*.erf\t-> *.das
-Change face & edit face features, name and inventory size

None\t-> *.mor
-Modify face in a exchangeable *.mor face file

*.mor\t-> *.mor
-Edit *.mor face with another

*.das\t-> *.mor
*.erf\t-> *.mor
-Extract & modify face in a exchangeable *.mor face file
-Edit *.mor face with another mor in savegame or resource

 For more deep changes of face (colors for example):
- extract the face from *.das or *.erf
- edit the *.mor file in the Toolset
- use as source for your savegame'''
    
    def __init__(self,master=None):
        Frame.__init__(self, master)
        self.sel="" # selected path
        self.src="" # source path
        self.dst="" # destination path
        # source and destination MOR files
        self.SRC_MOR=MORFile("","<source>")
        self.DST_MOR=MORFile("","<destination>")
        # resource file and facename
        self.res_file=ERFFile(pLAST_ERF)
        self.res_facename=""
        # key for face editing
        self.entry_key=""
        # copy flags for src to dest
        self.copy_face=True
        self.copy_name=True

        # check if there is chars in CHARACTER_DIR
        self.ScanForChars(pCHARACTERS_DIR)
        
        # start UI
        self.CreateWidgets()

    def CreateWidgets(self):
        self.master.title("%s v%s"%(sAPPNAME,sVERSION))
        self.BuildWidgets()
        self.PackWidgets()
        self.BindWidgets()
        self.InitVars()
        
    def BuildWidgets(self):
        # self.f_buttons
        self.f_buttons=Frame(self,bg="darkgrey")
        self.fB_credits=Label(self.f_buttons,text="(c)2010 by NewByPower",fg="white",bg="darkgrey")
        self.fB_bQuit=Button(self.f_buttons,text='QUIT',fg="white",bg='darkred',width=10,command=self.quit)
        self.fB_bHelp=Button(self.f_buttons,text='?',fg="white",bg='darkblue',width=1,command=self.ShowHelp)
        
        self.f_main=Frame(self)
        # self.f_files : all means to open files
        self.f_files=Frame(self.f_main)
        self.fF_title=Label(self.f_files,text="FILE SELECTED (None)")
        self.fF_path=Label(self.f_files,text="...\n...",fg="white",bg="black",width=40,justify=RIGHT)
        self.fF_buttons=Frame(self.f_files)
        self.fFB_bSetSRC=Button(self.fF_buttons,text="Set as Source",width=20,command=self.SetSRC)
        self.fFB_bSetDST=Button(self.fF_buttons,text="Set as Destination",width=20,command=self.SetDST)
        self.fF_infos=Label(self.f_files,text="1- Select a file and set it as source or destination\n2- Copy what you want from source\n3- Save the destination file",justify=LEFT)
        
        # File type tabs
        self.fF_tabs=Frame(self.f_files)
        self.fFT_bChar=Button(self.fF_tabs,text="Character",width=10,command=self.ShowCHR)
        self.fFT_bSave=Button(self.fF_tabs,text="Savegame",width=10,command=self.ShowDAS)
        self.fFT_bFace=Button(self.fF_tabs,text="Face",width=10,command=self.ShowMOR)
        self.fFT_bResF=Button(self.fF_tabs,text="Resource",width=10,command=self.ShowERF)
        # CHR tab
        self.fF_chr=Frame(self.f_files)
        self.fFC_path=Label(self.fF_chr,text="...\n...",fg="white",bg="black",justify=RIGHT)
        self.fFC_chardir=Button(self.fF_chr,text="%process character folder",command=self.SetCharDir,width=28)
        self.fFC_browser=Frame(self.fF_chr)
        self.fFCB_chars=Frame(self.fFC_browser)
        self.fFCBC_list=Listbox(self.fFCB_chars,width=26,height=4,activestyle=DOTBOX)
        self.fFCBC_sblist=Scrollbar(self.fFCB_chars,orient=VERTICAL)
        self.fFCB_label=Label(self.fFC_browser,text="View: %char",width=28)
        self.fFCB_saves=Frame(self.fFC_browser)
        self.fFCBS_list=Listbox(self.fFCB_saves,width=26,height=5,activestyle=DOTBOX)
        self.fFCBS_sblist=Scrollbar(self.fFCB_saves,orient=VERTICAL)
        # DAS tab
        self.fF_das=Frame(self.f_files)
        self.fFD_path=Label(self.fF_das,text="...\n...",fg="white",bg="black",justify=RIGHT)
        self.fFD_bOpenDAS=Button(self.fF_das,text="Open Savegame",width=28,command=self.OpenDAS)
        # MOR tab
        self.fF_mor=Frame(self.f_files)
        self.fFM_path=Label(self.fF_mor,text="...\n...",fg="white",bg="black",justify=RIGHT)
        self.fFM_bNewMOR=Button(self.fF_mor,text="New Face file",width=28,command=self.NewMOR)
        self.fFM_bOpenMOR=Button(self.fF_mor,text="Open Face file",width=28,command=self.OpenMOR)
        # ERF tab
        self.fF_res=Frame(self.f_files)
        self.fFR_path=Label(self.fF_res,text="...\n...",fg="white",bg="black",justify=RIGHT)
        self.fFR_bOpenERF=Button(self.fF_res,text='Open Resource file',width=28,command=self.OpenERF)
        self.fFR_browser=Frame(self.fF_res)
        self.fFRB_infos=Label(self.fFR_browser,text="Faces files (%filter/%total)")
        self.fFRB_filter=Frame(self.fFR_browser)
        self.fFRBF_label=Label(self.fFRB_filter,text="Filter:",width=5)
        self.fFRBF_filter=Entry(self.fFRB_filter,width=22)
        self.fFRB_list=Frame(self.fFR_browser)
        self.fFRBL_list=Listbox(self.fFRB_list,width=26,height=8,activestyle=DOTBOX)
        self.fFRBL_sblist=Scrollbar(self.fFRB_list,orient=VERTICAL)
        
        # self.f_src : source editing
        self.f_src=Frame(self.f_main)
        self.fS_title=Label(self.f_src,text="SOURCE FILE (None)",width=40)
        self.fS_path=Label(self.f_src,text="...\n...",fg="white",bg="black",justify=RIGHT)
        self.fS_bCopyToDst=Button(self.f_src,text="Copy selection to Destination file",command=self.CopyData)
        # Face features
        self.fSF_cMorph=Checkbutton(self.f_src,text="Face shape (with tatooes, wrinkles, scars, ...)")
        self.fS_face=Frame(self.f_src)
        self.fSF_header=Frame(self.fS_face)
        self.fSFH_info=Label(self.fSF_header,text="Face features:",width=20)
        self.fSFH_bToggle=Button(self.fSF_header,text="Select all / none",command=self.Toggle,width=20)
# self.fSF={}
        self.fSF_clFeatures=CheckList(self.fS_face)
        self.fSF_clFeatures.hlist.delete_all()
        self.fSF_clFeatures.hlist.configure(bg="white")
        for i,ff in enumerate(aMOR_EDIT):
# self.fSF[ff[1]]=Checkbutton(self.fS_face,text="%s (%s)"%(ff[0],self.SRC_MOR.GetString(ff[1])))
            self.fSF_clFeatures.hlist.add(i,text="%s (%s)"%(ff[0],self.SRC_MOR.GetString(ff[1])))
            self.fSF_clFeatures.setstatus(i,"on")
            i+=1
        # Other features
        self.fS_cName=Checkbutton(self.f_src,text="Character name (%name)")

        # self.f_dst : destination editing
        self.f_dst=Frame(self.f_main)
        self.fD_title=Label(self.f_dst,text="DESTINATION FILE (None)",width=40)
        self.fD_path=Label(self.f_dst,text="...\n...",fg="white",bg="black",justify=RIGHT)
        # Face Editing
        self.fD_face=Frame(self.f_dst)
        self.fDF_label=Label(self.fD_face,text="Manual face editing:")
        self.fDF_list=Frame(self.fD_face)
        self.fDFL_list=Listbox(self.fDF_list,width=38,height=6,activestyle=DOTBOX)
        self.fDFL_sblist=Scrollbar(self.fDF_list,orient=VERTICAL)
        self.fDF_edit=Frame(self.fD_face)
        self.fDFE_label=Label(self.fDF_edit,text="%entry",width=15)
        self.fDFE_eValue=Entry(self.fDF_edit,width=20)
        self.fDFE_bSetValue=Button(self.fDF_edit,text="Set",width=5,command=self.SetValToDest)
        self.fDF_bResetFace=Button(self.fD_face,text="Reset destination face data",width=30,command=self.ResetFace)
        # DAS editing
        self.fD_savegame=Frame(self.f_dst)
        self.fDS_label=Label(self.fD_savegame,text="Savegame Editing",width=40)
        self.fDS_name=Frame(self.fD_savegame)
        self.fDSN_label=Label(self.fDS_name,text="Name",width=20,anchor=W)
        self.fDSN_eName=Entry(self.fDS_name,width=20)
        self.fDS_inventory=Frame(self.fD_savegame)
        self.fDSI_label=Label(self.fDS_inventory,text="Inventory size",width=20,anchor=W)
        self.fDSI_eInvsize=Entry(self.fDS_inventory,width=20)
        # Save
        bfont=tkFont.Font (family="Helvetica", size=8, weight="bold" )
        self.fD_bSave=Button(self.f_dst,text='SAVE DESTINATION FILE',font=bfont,width=28,command=self.Save)
        
# # ERF editing - forget in 2.00, mean to add faces into an ERF package
# self.fD_resource=Frame(self.f_dst)
# self.fDR_label=Label(self.fD_resource,text="Add to Resource",width=20)
# self.fDR_name=Frame(self.fD_resource)
# self.fDRN_label=Label(self.fDR_name,text="Filename",width=6)
# self.fDRN_eName=Entry(self.fDR_name,width=14)
        
    def PackWidgets(self):
        # buttons
        self.fB_credits.pack(side=LEFT,fill=X)
        self.fB_bHelp.pack(side=RIGHT)
        self.fB_bQuit.pack(side=RIGHT)
        self.f_buttons.pack(fill=X)
        # files
        self.fF_title.pack(fill=X)
        self.fF_path.pack(fill=X,pady=2)
        self.fFB_bSetSRC.pack(side=LEFT,fill=X)
        self.fFB_bSetDST.pack(side=RIGHT,fill=X)
        self.fF_buttons.pack(fill=X,pady=2)
        self.fF_infos.pack()
        self.fFT_bChar.pack(pady=2)
        self.fFT_bSave.pack(pady=2)
        self.fFT_bFace.pack(pady=2)
        self.fFT_bResF.pack(pady=2)
        self.fF_tabs.pack(side=LEFT,fill=Y,padx=2)
        spacer1=Frame(self.f_files,height=1,bg="darkgrey")
        spacer1.pack(side=LEFT,fill=Y,padx=1)
# self.fFC_path.pack(fill=X)
        self.fFC_chardir.pack(pady=2,padx=2)
        self.fFCBC_list.pack(side=LEFT)
        self.fFCBC_sblist.pack(side=LEFT,fill=Y)
        self.fFCBS_list.pack(side=LEFT)
        self.fFCBS_sblist.pack(side=LEFT,fill=Y)
        self.fFCB_chars.pack()
        self.fFCB_label.pack(anchor=W)
        self.fFCB_saves.pack()
        self.fFC_browser.pack()
# self.fF_chr.pack()
# self.fFD_path.pack(fill=X)
        self.fFD_bOpenDAS.pack(pady=2,padx=2)
# self.fF_das.pack()
# self.fFM_path.pack(fill=X)
        self.fFM_bNewMOR.pack(pady=2,padx=2)
        self.fFM_bOpenMOR.pack(pady=2,padx=2)
# self.fF_mor.pack()
# self.fFR_path.pack(fill=X)
        self.fFR_bOpenERF.pack(pady=2,padx=2)
        self.fFRB_infos.pack(side=TOP)
        self.fFRBF_label.pack(side=LEFT)
        self.fFRBF_filter.pack(side=LEFT)
        self.fFRB_filter.pack()
        self.fFRBL_list.pack(side=LEFT)
        self.fFRBL_sblist.pack(side=LEFT,fill=Y)
        self.fFRB_list.pack()
        self.fFR_browser.pack()
# self.fF_res.pack()
        self.f_files.pack(side=LEFT,fill=Y,padx=2)
        # spacer
        spacer2=Frame(self.f_main,height=1,bg="darkgrey")
        spacer2.pack(side=LEFT,fill=Y,padx=1)
        # source
        self.fS_title.pack(fill=X)
        self.fS_path.pack(fill=X,pady=2)
        self.fS_bCopyToDst.pack(pady=2,fill=X)
# self.fSF_info.grid(row=0,column=0,sticky=W)
# self.fSF_bToggle.grid(row=0,column=1)
# self.fSF_cMorph.grid(row=1,column=0,sticky=W)
        self.fSF_cMorph.pack(anchor=W)
        # spacer
        spacer4=Frame(self.f_src,height=1,bg="darkgrey")
        spacer4.pack(fill=X,pady=1)
        self.fSFH_info.pack(side=LEFT,anchor=W)
        self.fSFH_bToggle.pack(side=LEFT,fill=X)
        self.fSF_header.pack()
        self.fSF_clFeatures.pack(fill=X)
# for col in range(2):
# for row in range(9):
# if col==0 and row==0: continue
# else:
# ff=aMOR_EDIT[9*col+row-1]
# self.fSF[ff[1]].grid(row=row+1,column=col,sticky=W)
        self.fS_face.pack(pady=2)
        # spacer
        spacer5=Frame(self.f_src,height=1,bg="darkgrey")
        spacer5.pack(fill=X,pady=1)
        self.f_src.pack(side=LEFT,fill=Y,padx=2)
        # spacer
        spacer3=Frame(self.f_main,height=1,bg="darkgrey")
        spacer3.pack(side=LEFT,fill=Y,padx=1)
        # destination
        self.fD_title.pack(fill=X)
        self.fD_path.pack(fill=X,pady=2)
        self.fDF_label.pack()
        self.fDFL_list.pack(side=LEFT)
        self.fDFL_sblist.pack(side=LEFT,fill=Y)
        self.fDF_list.pack()
        self.fDFE_label.pack(side=LEFT)
        self.fDFE_eValue.pack(side=LEFT)
        self.fDFE_bSetValue.pack(side=LEFT)
        self.fDF_edit.pack()
        self.fDF_bResetFace.pack(pady=2,fill=X)
        self.fD_face.pack()
        # spacer
        spacer6=Frame(self.f_dst,height=1,bg="darkgrey")
        spacer6.pack(fill=X,pady=1)
        self.fDS_label.pack()
        self.fDSN_label.pack(side=LEFT)
        self.fDSN_eName.pack(side=LEFT)
        self.fDS_name.pack()
        self.fDSI_label.pack(side=LEFT)
        self.fDSI_eInvsize.pack(side=LEFT)
        self.fDS_inventory.pack()
        self.fD_bSave.pack(side=BOTTOM,fill=X)
        
# self.fDR_label.pack()
# self.fDRN_label.pack(side=LEFT)
# self.fDRN_eName.pack(side=LEFT)
# self.fDR_name.pack()
# self.fD_resource.pack()
        
        self.f_dst.pack(side=LEFT,fill=Y,padx=2)
        self.f_main.pack()

        self.pack()

    def BindWidgets(self):
        # binding
        self.fFRBF_filter.bind('<Key-Return>',self.Filter)
        
        self.fFRBL_list.bind('<Button1-ButtonRelease>',self.SelectFace)
        self.fFRBL_list.configure(yscrollcommand=self.fFRBL_sblist.set)
        self.fFRBL_sblist.configure(command=self.fFRBL_list.yview)
        
        self.fFCBC_list.bind('<Button1-ButtonRelease>',self.SelectChar)
        self.fFCBC_list.configure(yscrollcommand=self.fFCBC_sblist.set)
        self.fFCBC_sblist.configure(command=self.fFCBC_list.yview)
        
        self.fFCBS_list.bind('<Button1-ButtonRelease>',self.SelectSave)
        self.fFCBS_list.configure(yscrollcommand=self.fFCBS_sblist.set)
        self.fFCBS_sblist.configure(command=self.fFCBS_list.yview)
            
        self.fDFL_list.bind('<Button1-ButtonRelease>',self.SelectEntry)
        self.fDFL_sblist.configure(command=self.fDFL_list.yview)
        self.fDFL_list.configure(yscrollcommand=self.fDFL_sblist.set)

    def InitVars(self):
        # Init vars
        self.filter_string=StringVar()
        self.files_list=Variable()
        self.chars_list=Variable()
        self.saves_list=Variable()
        self.dst_name=StringVar()
        self.dst_invsize=IntVar()
        self.entries_list=Variable()
        self.entry_value=StringVar()
        
        self.fFRBF_filter["textvariable"]=self.filter_string
        self.fFRBL_list["listvariable"]=self.files_list
        self.fFCBC_list["listvariable"]=self.chars_list
        self.fFCBS_list["listvariable"]=self.saves_list
        self.fDFL_list["listvariable"]=self.entries_list
        self.fDSN_eName["textvariable"]=self.dst_name
        self.fDSI_eInvsize["textvariable"]=self.dst_invsize
        self.fDFE_eValue["textvariable"]=self.entry_value
# self.fDRN_eName["textvariable"]=self.res_filename

# self.copy_mask=[BooleanVar()]
# self.fSF_cMorph["variable"]=self.copy_mask[0]
# for x in range (len(aMOR_EDIT)):
# v=BooleanVar()
# self.copy_mask.append(v)
# self.fSF[aMOR_EDIT[x][1]]["variable"]=self.copy_mask[x+1]
# self.copy_mask.append(BooleanVar())
# self.fS_cName["variable"]=self.copy_mask[len(aMOR_EDIT)+1]
        self.copy_face=BooleanVar()
        self.fSF_cMorph["variable"]=self.copy_face
        self.copy_name=BooleanVar()
        self.fS_cName["variable"]=self.copy_name
        
        self.entries_list.set(tuple(map(lambda me: me[0],aMOR_EDIT)))
        self.chars_list.set(tuple(map(lambda c: c[0],self.chars)))
        self.fFCBC_list.selection_set(self.cs[0],self.cs[0])

        self.Filter()
        self.SelectEntry(None)
        self.SelectFace(None)
        self.SelectChar(None)
        
# for ff in aMOR_EDIT: self.fSF[ff[1]].select()
        self.fSF_cMorph.select()
        self.fS_cName.select()
        self.RebuildFeaturesList()
        self.Show("C")
        
    def ShowCHR(self):
        self.SetSelected(pLAST_CHR)
        self.fFC_path["text"]=TruncatePath(pLAST_DAS,"",22)
        charname=self.chars_list.get()[self.cs[0]]
        savename=self.saves_list.get()[self.cs[1]]
        if self.chars_list.get():
            self.fFCB_label["text"]="%s : %s"%(charname,savename)
        self.SetCharDirText()
        self.Show("C")
        
    def ShowDAS(self):
        self.SetSelected(pLAST_DAS)
        self.fFD_path["text"]=TruncatePath(pLAST_DAS,"",22)
        self.Show("S")
        
    def ShowMOR(self):
        self.SetSelected(pLAST_MOR,False)
        self.fFM_path["text"]=TruncatePath(pLAST_MOR,"",22)
        self.Show("F")
        
    def ShowERF(self):
        self.SetSelected(pLAST_ERF)
        if pLAST_ERF:
            self.fF_path["text"]=TruncatePath(pLAST_ERF,"",45)+"\n"+self.res_facename
            self.fFR_path["text"]=TruncatePath(pLAST_ERF,"",22)
        self.Filter(self.filter_string.get())
        self.Show("R")

    def Show(self,what=""):
        pairs={"C":(self.fF_chr,self.fFT_bChar),
               "S":(self.fF_das,self.fFT_bSave),
               "F":(self.fF_mor,self.fFT_bFace),
               "R":(self.fF_res,self.fFT_bResF)}
        for v in pairs.values():
            v[0].forget()
            v[1].config(relief="raised")
        for c in what:
            if c in pairs.keys():
                pairs[c][0].pack()
                pairs[c][1].config(relief="sunken")
                
    def ShowHelp(self):
        Help=TKMB.Message(self,message=self.HELP,icon=TKMB.INFO,title="%s : Help"%sAPPNAME)
        Help.show()

# Source and destination loading
    def OpenDAS(self):
        global pLAST_DAS
        Dialog=TKFD.Open(filetypes=(("DragonAge Savegame",".das"),),
                         initialdir=OP.dirname(pLAST_DAS))
        path=Dialog.show()
        if GetType(path)==1:
            pLAST_DAS=path
            Log("1INFO: Open savegame: %s"%pLAST_DAS)
            self.ShowDAS()
        
    def NewMOR(self):
        global pLAST_MOR
        Dialog=TKFD.SaveAs(filetypes=(("Face file",".mor"),),
                           initialdir=OP.dirname(pLAST_MOR),
                           defaultextension=".mor",
                           initialfile=self.dst_name.get())
        path=Dialog.show()
        if GetType(path)==2:
            pLAST_MOR=path
            Log("1INFO: New face: %s"%pLAST_MOR)
            self.ShowMOR()
        
    def OpenMOR(self):
        global pLAST_MOR
        Dialog=TKFD.Open(filetypes=(("Face file",".mor"),),
                         initialdir=OP.dirname(pLAST_MOR))
        path=Dialog.show()
        if GetType(path)==2:
            pLAST_MOR=path
            Log("1INFO: Open face: %s"%pLAST_MOR)
            self.ShowMOR()
        
    def OpenERF(self):
        global pLAST_ERF
        Dialog=TKFD.Open(filetypes=(("DragonAge Resource",".erf .rim"),),
                         initialdir=OP.dirname(pLAST_ERF))
        path=Dialog.show()
        if GetType(path)==3:
            pLAST_ERF=path
            Log("1INFO: Open resource: %s"%pLAST_ERF)
            self.res_file=ERFFile(pLAST_ERF)
            self.Filter()
            if self.files_list.get():
                self.fFRBL_list.selection_set(0)
                self.res_facename=self.files_list.get()[0]
            self.ShowERF()
        
    def SetSelected(self,path,checkpath=True):
        path=OP.normpath(path)
        self.sel=path
        if checkpath and not OP.exists(path) or path in ("",".","/."): SetPathText(self.fF_path,"","",45)
        else: SetPathText(self.fF_path,path,"",45)
        self.fF_title["text"]="FILE SELECTED (%s)"%GetTypeName(path)
        self.fF_title["fg"]=GetTypeColor(path)

    def SetSRC(self,path=""):
        if path: self.sel=path
        if OP.exists(self.sel):
            self.SetSrcFile(self.sel)
            self.CheckSRC()
        
    def CheckSRC(self):
        self.fS_cName.forget()
        if GetType(self.src)==1 and GetType(self.dst)==1: #DAS 2 DAS
            self.fS_cName.pack(pady=2,anchor=W)
            name,mode=GetName(self.src,log=False)
            if name:
                self.fS_cName["text"]="Character name (%s)"%name
                self.fS_cName.select()
        else:
            self.fS_cName.forget()
        self.fSF_clFeatures.hlist.delete_all()
        for i,ff in enumerate(aMOR_EDIT):
# self.fSF[ff[1]]["text"]="%s (%s)"%(ff[0],self.SRC_MOR.GetString(ff[1]))
# self.fSF[ff[1]].select()
            self.fSF_clFeatures.hlist.add(i,text="%s (%s)"%(ff[0],self.SRC_MOR.GetString(ff[1])))
            self.fSF_clFeatures.setstatus(i)
            
        self.fSF_cMorph.select()
        
    def SetDST(self,path=""):
        if path: self.sel=path
        if GetType(self.sel)==1 or GetType(self.sel)==2:
            self.SetDstFile(self.sel)
            self.CheckDST()
        
    def CheckDST(self):
        entries=map(lambda me: me[0]+" (%s)"%self.DST_MOR.GetString(me[1]),aMOR_EDIT)
        self.entries_list.set(tuple(entries))
        self.SelectEntry()
        self.fD_savegame.forget()
        if GetType(self.dst)==1: #DAS
            self.fD_savegame.pack()
            name,mode=GetName(self.dst,log=False)
            if mode==1: self.fDSN_eName["bg"]="grey" # not change possible
            elif mode==2: self.fDSN_eName["bg"]="yellow" # name error
            else: self.fDSN_eName["bg"]="white"
            self.dst_name.set(name)
            self.dst_invsize.set(GetInvSize(self.dst,log=False))
        if GetType(self.src)==1 and GetType(self.dst)==1: #DAS 2 DAS
            self.fS_cName.pack(pady=2,anchor=W)
            name,mode=GetName(self.src,log=False)
            if name:
                self.fS_cName["text"]="Character name (%s)"%name
                self.fS_cName.select()
        
# Character selection
    def SetCharDir(self):
        global pCHARACTERS_DIR
        Dialog=TKFD.Directory(initialdir=OP.dirname(pCHARACTERS_DIR))
        char_dir=OP.normpath(Dialog.show()+"/")
        self.ScanForChars(char_dir)
        if self.chars:
            Log("1INFO: New Characters directory : %s"%char_dir)
            pCHARACTERS_DIR=char_dir
            self.chars_list.set(tuple(map(lambda c: c[0],self.chars)))
            self.fFCBC_list.selection_set(self.cs[0],self.cs[0])
            self.SelectChar(None)
            self.SetCharDirText()
            
    def SetCharDirText(self):
        if self.chars: self.fFC_chardir.config(text="Change character folder")
        else: self.fFC_chardir.config(text="Set character folder")
        
    def ScanForChars(self,char_dir=pCHARACTERS_DIR):
        self.chars=[]
        if OP.exists(char_dir):
            for p in os.listdir(char_dir):
                sp=OP.normpath(OP.join(char_dir,p,"Saves"))
                if OP.isdir(sp): # path is a directory
                    saves=[]
                    for pp in os.listdir(sp):
                        spp=OP.normpath(OP.join(char_dir,p,"Saves",pp))
                        if not OP.exists(spp): continue
                        if OP.isdir(spp):
                            for f in os.listdir(spp):
                                if OP.splitext(f)[1]==".das":
                                    saves.append(pp)
                    if saves: self.chars.append((p,saves))
        self.cs=[0,0]
        self.sel=""
        Log("1INFO: Found %s characters"%len(self.chars))

    def SelectChar(self,event):
        try:
            ci=int(self.fFCBC_list.curselection()[0])
        except:
            if pLAST_CHR:
                charname=pLAST_CHR[pLAST_CHR.lower().index("characters")+11:].split(os.sep)[0]
                names=self.chars_list.get()
                if charname in names: ci=names.index(charname)
            else: ci=0
        self.fFCBS_list.selection_clear(0)
        self.fFCBC_list.selection_set(ci)
        self.cs=[ci,0]
        if self.chars:
            self.saves_list.set(tuple(map(lambda s: s,self.chars[self.cs[0]][1])))
            self.fFCBS_list.selection_clear(0)
            self.fFCBS_list.selection_set(0)
            self.SelectSave(None,False)
    
    def SelectSave(self,event,log=True):
        global pLAST_CHR
        try:
            si=int(self.fFCBS_list.curselection()[0])
        except:
            if pLAST_CHR:
                charsave=pLAST_CHR[pLAST_CHR.lower().index("saves")+6:].split(os.sep)[0]
                saves=self.saves_list.get()
                if charsave in saves: si=saves.index(savename)
            else: si=self.cs[1]
        self.fFCBS_list.selection_clear(0)
        self.fFCBS_list.selection_set(si)
        if sAUTO_SELECT and not event:
            lst=map(lambda s:s.lower(),self.saves_list.get())
            if sAUTO_SELECT.lower() in lst:
                si=lst.index(sAUTO_SELECT.lower())
                self.fFCBS_list.selection_clear(0)
                self.fFCBS_list.selection_set(si)
                self.fFCBS_list.see(si)
        self.cs[1]=si
        charname=self.chars_list.get()[self.cs[0]]
        savename=self.saves_list.get()[self.cs[1]]
        savedir=OP.normpath(OP.join(pCHARACTERS_DIR,charname,"Saves",savename))
        for f in os.listdir(savedir):
            if OP.splitext(f)[1]==".das":
                pLAST_CHR=OP.normpath(OP.join(savedir,f))
        Log("1INFO: Selected character savegame: %s"%pLAST_CHR)
        self.ShowCHR()
        
# Resource File selection
    def Filter(self,match=""):
        faces=[]
        if self.res_file:
            match=self.filter_string.get()
            faces=self.res_file.Search(match,False,".mor")
            lst=[]
            for f in faces:
                if match in f: lst.append(f)
            self.files_list.set(tuple(lst))
        self.fFRB_infos["text"]="Faces Files (%s/%s)"%(len(lst),len(faces))
    
    def SelectFace(self,event):
        if self.res_file:
            try:
                selection=int(self.fFRBL_list.curselection()[0])
            except:
                selection=0
                self.fFRBL_list.selection_set(0)
            if self.files_list.get():
                self.res_facename=self.files_list.get()[selection]
                Log("1INFO: Selected Face file: %s (in %s)"%(self.res_facename,self.res_file.path))
            self.ShowERF()
            
# Source Edition
    def CopyData(self,mask=""):
        if self.SRC_MOR.data=="" or self.DST_MOR.data=="":
            Message(self,"Set a source and a destination first !")
            return
        Log("1INFO: Copying Data")
        features=self.fSF_clFeatures.getselection()
        
        # face morph : build new mor with source or dest
        if self.copy_face.get():
            NEW_MOR=MORFile(self.SRC_MOR.data,"<new>")
            Log("2 Get Morph from SRC_MOR")
        else:
            NEW_MOR=MORFile(self.DST_MOR.data,"<new>")
            Log("2 Get Morph from DST_MOR")
        # At this point, all the morph has been replaced
        if len(features)==len(aMOR_EDIT) and self.copy_face.get():
            # useless to copy features if all selected and new morph is src morph
            Log("2 All features are already sets")
        else:
            # face features : replace values in new mor by src mor or dst mor values
            for i in range(len(aMOR_EDIT)):
                key=aMOR_EDIT[int(i)][1]
                valS=self.SRC_MOR.GetString(key)
                valD=self.DST_MOR.GetString(key)
                if str(i) in features:
                    Log("2 Get %s from SRC_MOR (%s)"%(key,valS))
                    NEW_MOR.SetString(key,valS)
                else:
                    Log("2 Get %s from DST_MOR (%s)"%(key,valD))
                    NEW_MOR.SetString(key,valD)
        # Set charname in UI
        if self.copy_name.get() and GetType(self.src)==1 and GetType(self.dst)==1:
            dstname,mode=GetName(self.dst,log=False)
            srcname,mode=GetName(self.src,log=False)
            if srcname:
                self.dst_name.set(srcname)
                Log("2 Get Name from SRC : (%s)"%(srcname))
        # Copy new mor to dest mor
        self.DST_MOR=NEW_MOR
        self.RebuildFeaturesList()
        self.SelectEntry()
        
    def Toggle(self):
        features=self.fSF_clFeatures.getselection()
        if features:
            for i,ff in enumerate(aMOR_EDIT):
# self.fSF[ff[1]].deselect()
                self.fSF_clFeatures.setstatus(i,"off")
            #self.fSF_cMorph.deselect()
            #self.fS_cName.deselect()
        else:
            for i,ff in enumerate(aMOR_EDIT):
# self.fSF[ff[1]].select()
                self.fSF_clFeatures.setstatus(i,"on")
            #self.fSF_cMorph.select()
            #self.fS_cName.select()
        
# Destination face editing
    def SelectEntry(self,event=None):
        try:
            entry=int(self.fDFL_list.curselection()[0])
        except:
            entry=0
            self.fDFL_list.selection_set(entry)
        self.edit_key=aMOR_EDIT[entry][1]
        self.fDFE_label["text"]=aMOR_EDIT[entry][0]
        self.entry_value.set(self.DST_MOR.GetString(self.edit_key))
        
    def SetValToDest(self):
        if self.DST_MOR.data=="":
            Message(self,"Set a destination first !")
            return
        key=self.edit_key
        val=self.fDFE_eValue.get()
        self.DST_MOR.SetString(key,val)
        self.RebuildFeaturesList()
        Log('1INFO: Changed feature <%s> to "%s" in destination face'%(key,val))
        
    def ResetFace(self):
        if GetType(self.dst)==1:
            self.DST_MOR=MORFile(GetMORData(self.dst),"<destination savegame>")
        if GetType(self.dst)==2:
            if OP.exists(self.dst):
                fin=open(self.dst,"rb")
                txt=fin.read()
                fin.close()
                self.DST_MOR=MORFile(txt)
            else: # new file
                self.DST_MOR=MORFile(self.SRC_MOR.data,"<mor file>")
                if not self.SRC_MOR.data: Log("1INFO: New mor file waiting to be filled with any source")
        self.RebuildFeaturesList()
        
    def RebuildFeaturesList(self):
        entries=map(lambda me: me[0]+" (%s)"%self.DST_MOR.GetString(me[1]),aMOR_EDIT)
        self.entries_list.set(tuple(entries))
        self.SelectEntry()
        
# Saving process
    def Save(self):
        if not self.dst:
            Message(self,"Set a destination first !")
            return
        if GetType(self.dst)==1 and OP.exists(self.dst) and nSAVE_METHOD>=1:
            # list files in the save folder
            folder=OP.dirname(self.dst)
            if nSAVE_METHOD==1: # copy to Slot_n+1
                inc=1
                while OP.exists(OP.dirname(folder)+os.sep+"Slot_%d"%inc): inc+=1
                newfolder=OP.dirname(folder)+os.sep+"Slot_%d"%inc
            # create folder if necessary and copy files
            if not OP.exists(newfolder): os.mkdir(newfolder)
            for f in os.listdir(folder): shutil.copy(OP.join(folder,f),OP.join(newfolder,f))
            # set self.dst as the new das file
            destpath=OP.join(newfolder,OP.basename(self.dst))
        else:
            destpath=self.dst
        if destpath and self.DST_MOR.data!="":
            Log("1INFO: Saving to destination file : %s"%destpath)
            ok=False
            # First, update destination face with the new strings (build the raw data)
            self.DST_MOR.Update()
            # Dest is a DAS, proceed with tweaks
            if GetType(destpath)==1:
                ok=ChangeSaveData(destpath,self.dst_name.get(),self.dst_invsize.get(),self.DST_MOR)
                if ok:
                    if nSAVE_METHOD<0: Message(self,"Savegame modified (without backup):\n%s"%destpath)
                    elif nSAVE_METHOD==0: Message(self,"Savegame modified (with backup):\n%s"%destpath)
                    elif nSAVE_METHOD==1: Message(self,"Savegame created (in new slot):\n%s"%(destpath))
            # Dest is a MOR, proceed with writing mor face to file
            elif GetType(destpath)==2:
                self.DST_MOR.Save(destpath)
                Message(self,"Face file created:\n%s"%destpath)
        
    def SetSrcFile(self,path):
        path=OP.normpath(path)
        self.SRC_MOR=MORFile("","<source>")
        if path:
            if GetType(path)==1:
                Log("1INFO: Selected DAS Source file: %s"%path)
                self.src=path
                self.SRC_MOR=MORFile(GetMORData(path),"<source savegame>")
                SetPathText(self.fS_path,path,"",45)
            elif GetType(path)==2:
                Log("1INFO: Selected MOR Source file: %s"%path)
                self.src=path
                fin=open(path,"rb")
                txt=fin.read()
                fin.close()
                self.SRC_MOR=MORFile(txt)
                SetPathText(self.fS_path,path,"",45)
            elif GetType(path)==3:
                Log("1INFO: Selected ERF Face Source file: %s in %s"%(self.res_facename,path))
                self.src=path
                facefile=self.res_file.GetFileData(self.res_facename)
                self.SRC_MOR=MORFile(facefile,"<%s>"%self.res_facename)
                SetPathText(self.fS_path,path,"",45)
                self.fS_path["text"]=TruncatePath(path,"",45)+"\n"+self.res_facename
            if GetType(path) in (1,2,3):
                self.fS_title["text"]="SOURCE FILE (%s)"%GetTypeName(path)
                self.fS_title["fg"]=GetTypeColor(path)
            if self.dst and self.DST_MOR.data=="": # fill dst mor with source if empty
                self.DST_MOR=MORFile(self.SRC_MOR.data,"<mor file>")
                self.RebuildFeaturesList()
                
    def SetDstFile(self,path):
        path=OP.normpath(path)
        self.DST_MOR=MORFile("","<destination>")
        if path:
            if GetType(path)==1:
                Log("1INFO: Selected DAS Destination file: %s"%path)
                self.dst=path
                dstname,mode=GetName(path)
                self.dst_name.set(dstname)
                self.dst_invsize.set(GetInvSize(path))
                self.DST_MOR=MORFile(GetMORData(path),"<%s savegame>"%dstname)
                SetPathText(self.fD_path,path,"",45)
            elif GetType(path)==2:
                Log("1INFO: Selected MOR Destination file: %s"%path)
                self.dst=path
                if OP.exists(path): # opening file
                    fin=open(path,"rb")
                    txt=fin.read()
                    fin.close()
                    self.DST_MOR=MORFile(txt)
                else: # new file
                    self.DST_MOR=MORFile(self.SRC_MOR.data,"<mor file>")
                    if not self.SRC_MOR.data: Log("1INFO: New mor file waiting to be filled with any source")
                SetPathText(self.fD_path,path,"",45)
            if GetType(path) in (1,2):
                self.fD_title["text"]="DESTINATION FILE (%s)"%GetTypeName(path)
                self.fD_title["fg"]=GetTypeColor(path)
        
################################################################
## FUNCTIONS
################################################################
def Log(text,write=True):
    log=2
    if text[0].isdigit():
        log=int(text[0])
        text=log*"."+text[1:]
    # only show some messages, based on nLOG_LEVEL
    try: # Python v2.5 and upper
        if log<=nLOG_LEVEL: print(text)
    except: # Python v2.4 and lower
        if log<=nLOG_LEVEL: print text
    # always write all flow into logfile
    if write:
        try: LOG.write(text+"\n")
        except: LOG.write("ERROR: Log Error\n")
        LOG.flush()
    
def Message(frame,msg="A message",msgtype="i",log=True):
    if log and msgtype in ("e","i"): Log({"e":"0ERROR: ","i":"1INFO: "}[msgtype]+msg)
    if msgtype=="e": icotype=TKMB.ERROR
    else: icotype=TKMB.INFO
    Dialog=TKMB.Message(frame,message=msg,icon=icotype,title=sAPPNAME)
    Dialog.show()

def UpdateScript(src,dst,text):
    chardir=pCHARACTERS_DIR.strip("\ /")
    Log("1INFO: Updating script: %s"%sPYTHON_FILE)
    for k,v in zip(("pCHARACTERS_DIR=","pLAST_CHR=","pLAST_DAS=","pLAST_MOR=","pLAST_ERF=","nSAVE_METHOD="),
                   (chardir,pLAST_CHR,pLAST_DAS,pLAST_MOR,pLAST_ERF,nSAVE_METHOD)):
        # replace the first occurence of data
        start=text.find(k,0)
        Log("3 Updating : %s%s"%(k,v))
        end=text.find("\n",start)
        if k.startswith("p"):
            text=text[0:start]+k+'"'+OP.normpath(v).strip(".")+'"'+text[end:]
        elif k.startswith("b"):
            text=text[0:start]+k+("False","True")[v]+text[end:]
    return text

def SetPathText(control,path,prefix="",length=35):
    # set the path and colorize
    if not path: control["text"]="...\n..."
    else:
        control["text"]=TruncatePath(OP.dirname(path),prefix,length)+"\\\n"+OP.basename(path)
    control["bg"]=GetTypeColor(path)
    
def TruncatePath(path,prefix,length=35):
    if not path: return ""
    if len(path)>length: path="..."+path[-(length-3):]
    return prefix+path

def GetType(path):
    if path.endswith(".das"): return 1
    if path.endswith(".mor"): return 2
    if path.endswith(".erf"): return 3
    if path.endswith(".rim"): return 3
    return 0
def GetTypeName(path): return ("None","Savegame","Face","Resource")[GetType(path)]
def GetTypeColor(path): return ("black","darkgreen","darkblue","darkred")[GetType(path)]

################################################################
## MAIN ROUTINE
################################################################
LOG=open("DAFR_%s.log"%sVERSION,"w")
Log("0Starting DragonAge Face Replacer v%s"%sVERSION)
Log("0 Code is (c) 2010 NewByPower. See licence information in the script.")
Log("0 Homepage: http://www.dragonagenexus.com/downloads/file.php?id=428")
Log("0 Use [?] button to see a short help. See the readme for more explanations.")

root=Tix.Tk()
UI=DAFR(root)
UI.mainloop()
root.destroy()

# Updating script globals
# load python script
pyfile=open(sPYTHON_FILE,"rb")
txt=pyfile.read()
pyfile.close()
# update code
txt=UpdateScript(UI.src,UI.dst,txt).encode("utf8")
# rewrite python script
pyfile=open(sPYTHON_FILE,"wb")
pyfile.write(txt)
pyfile.close()

Log("0Ending DragonAge Face Replacer")
LOG.close()
 * LargePythonProjects
 * MostPopularPythonProjects

Python Projects

As a programming language, Python is the foundation of many software projects producing things like applications, libraries, modules and packages. Since so many projects exist, it can be difficult to find one which is concentrating on a particular topic of interest, but there are several starting points:

  • Applications - a list of Python-based applications

  • UsefulModules - libraries, modules and packages

  • PublishingPythonModules - places where modules are often publicised

  • PythonMed - Python Med (along the lines of DebianMed) presents packages that are associated with medicine, pre-clinical research, life science and bio-informatics.

  • SourceForge hosts open source Python-based software projects:

    • Browse for projects written on Python

  • Python Source is a directory of open source python projects.

Code Fragments

Useful code does not always exist in the context of a project - smaller fragments may be published in various places:

Ideas for New Projects

Sometimes there is no project addressing a certain need, or perhaps a project has been started and requires help to reach its objectives.

Statistics on Projects

For those who like statistics, some resources exist cataloguing the size and popularity of some Python projects:

PythonProjects (last edited 2015-01-08 11:09:16 by WolfgangMaier)

Unable to edit the page? See the FrontPage for instructions.