Differences between revisions 10 and 11
Revision 10 as of 2010-11-08 12:15:51
Size: 1809
Editor: PaulBoddie
Comment: Revert spam.
Revision 11 as of 2011-02-05 22:32:45
Size: 62347
Editor: 125
Comment:
Deletions are marked like this. Additions are marked like this.
Line 1: Line 1:
= 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 [[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.

== Code Fragments ==

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

 * [[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

== 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.

 * [[CodingProjectIdeas]] - a list of ideas for projects of various sizes

== Statistics on Projects ==

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

 * LargePythonProjects
 * MostPopularPythonProjects
# -*- 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.
##
################################################################

################################################################
## 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"

# 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]))

sMOR_HEADER="GFF V4.0PC MORPV0.1"

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

################################################################
## 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

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

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

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

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

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

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

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

# 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

# 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"

# 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]))

sMOR_HEADER="GFF V4.0PC MORPV0.1"

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

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: # write to file out_file=open(daspath,"wb") out_file.write(dasdata) out_file.close() return True

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

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

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

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

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

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 ""

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

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

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

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

  • 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): def ShowMOR(self): 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):
    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=""): 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):

        if self.dst and self.DST_MOR.data=="": # fill dst mor with source if empty

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

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)]

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

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

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