├── libogg.dll ├── etcpack.exe ├── libvorbis.dll ├── README.md ├── LICENSE ├── .gitignore └── extract.py /libogg.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moesoha/unity-game-resource-unpacker/HEAD/libogg.dll -------------------------------------------------------------------------------- /etcpack.exe: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moesoha/unity-game-resource-unpacker/HEAD/etcpack.exe -------------------------------------------------------------------------------- /libvorbis.dll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moesoha/unity-game-resource-unpacker/HEAD/libvorbis.dll -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This project is tested with Xamarin Unity game which Android package name is `com.papegames.evol`. 2 | 3 | ## Requirements 4 | 5 | Python 3 6 | 7 | pip install pillow unitypack lz4 fsb5 8 | 9 | `etcpack.exe`, `libogg.dll`, `libvorbis.dll` are for Windows. 10 | 11 | ## Usage 12 | 13 | python ./extract.py -i -o 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Soha Jin 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/linux,macos,python,windows,visualstudiocode 3 | 4 | ### Linux ### 5 | *~ 6 | 7 | # temporary files which can be created if a process still has a handle open of a deleted file 8 | .fuse_hidden* 9 | 10 | # KDE directory preferences 11 | .directory 12 | 13 | # Linux trash folder which might appear on any partition or disk 14 | .Trash-* 15 | 16 | # .nfs files are created when an open file is removed but is still being accessed 17 | .nfs* 18 | 19 | ### macOS ### 20 | *.DS_Store 21 | .AppleDouble 22 | .LSOverride 23 | 24 | # Icon must end with two \r 25 | Icon 26 | 27 | # Thumbnails 28 | ._* 29 | 30 | # Files that might appear in the root of a volume 31 | .DocumentRevisions-V100 32 | .fseventsd 33 | .Spotlight-V100 34 | .TemporaryItems 35 | .Trashes 36 | .VolumeIcon.icns 37 | .com.apple.timemachine.donotpresent 38 | 39 | # Directories potentially created on remote AFP share 40 | .AppleDB 41 | .AppleDesktop 42 | Network Trash Folder 43 | Temporary Items 44 | .apdisk 45 | 46 | ### Python ### 47 | # Byte-compiled / optimized / DLL files 48 | __pycache__/ 49 | *.py[cod] 50 | *$py.class 51 | 52 | # C extensions 53 | *.so 54 | 55 | # Distribution / packaging 56 | .Python 57 | build/ 58 | develop-eggs/ 59 | dist/ 60 | downloads/ 61 | eggs/ 62 | .eggs/ 63 | lib/ 64 | lib64/ 65 | parts/ 66 | sdist/ 67 | var/ 68 | wheels/ 69 | *.egg-info/ 70 | .installed.cfg 71 | *.egg 72 | 73 | # PyInstaller 74 | # Usually these files are written by a python script from a template 75 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 76 | *.manifest 77 | *.spec 78 | 79 | # Installer logs 80 | pip-log.txt 81 | pip-delete-this-directory.txt 82 | 83 | # Unit test / coverage reports 84 | htmlcov/ 85 | .tox/ 86 | .coverage 87 | .coverage.* 88 | .cache 89 | nosetests.xml 90 | coverage.xml 91 | *.cover 92 | .hypothesis/ 93 | 94 | # Translations 95 | *.mo 96 | *.pot 97 | 98 | # Django stuff: 99 | *.log 100 | local_settings.py 101 | 102 | # Flask stuff: 103 | instance/ 104 | .webassets-cache 105 | 106 | # Scrapy stuff: 107 | .scrapy 108 | 109 | # Sphinx documentation 110 | docs/_build/ 111 | 112 | # PyBuilder 113 | target/ 114 | 115 | # Jupyter Notebook 116 | .ipynb_checkpoints 117 | 118 | # pyenv 119 | .python-version 120 | 121 | # celery beat schedule file 122 | celerybeat-schedule.* 123 | 124 | # SageMath parsed files 125 | *.sage.py 126 | 127 | # Environments 128 | .env 129 | .venv 130 | env/ 131 | venv/ 132 | ENV/ 133 | env.bak/ 134 | venv.bak/ 135 | 136 | # Spyder project settings 137 | .spyderproject 138 | .spyproject 139 | 140 | # Rope project settings 141 | .ropeproject 142 | 143 | # mkdocs documentation 144 | /site 145 | 146 | # mypy 147 | .mypy_cache/ 148 | 149 | ### VisualStudioCode ### 150 | .vscode/* 151 | !.vscode/settings.json 152 | !.vscode/tasks.json 153 | !.vscode/launch.json 154 | !.vscode/extensions.json 155 | .history 156 | 157 | ### Windows ### 158 | # Windows thumbnail cache files 159 | Thumbs.db 160 | ehthumbs.db 161 | ehthumbs_vista.db 162 | 163 | # Folder config file 164 | Desktop.ini 165 | 166 | # Recycle Bin used on file shares 167 | $RECYCLE.BIN/ 168 | 169 | # Windows Installer files 170 | *.cab 171 | *.msi 172 | *.msm 173 | *.msp 174 | 175 | # Windows shortcuts 176 | *.lnk 177 | 178 | 179 | # End of https://www.gitignore.io/api/linux,macos,python,windows,visualstudiocode 180 | 181 | temp* 182 | assets 183 | .vscode 184 | -------------------------------------------------------------------------------- /extract.py: -------------------------------------------------------------------------------- 1 | import unitypack 2 | import fsb5 3 | from PIL import ImageOps,Image 4 | import os,pickle,io,sys,locale 5 | from unitypack.engine.texture import TextureFormat 6 | from argparse import ArgumentParser 7 | import subprocess 8 | 9 | currentWorkDir=os.getcwd() 10 | 11 | ETC_SERIES=( 12 | TextureFormat.ETC_RGB4, 13 | TextureFormat.ETC2_RGB, 14 | TextureFormat.ETC2_RGBA8, 15 | TextureFormat.ETC2_RGBA1, 16 | TextureFormat.EAC_R, 17 | TextureFormat.EAC_RG, 18 | TextureFormat.EAC_R_SIGNED, 19 | TextureFormat.EAC_RG_SIGNED, 20 | ) 21 | 22 | def putTextFile(filename,directory,content,mode="wb",encoding="utf-8"): 23 | if(not(os.path.exists(directory))): 24 | os.makedirs(directory) 25 | 26 | path=os.path.join(directory,filename) 27 | 28 | with open(path,mode=mode,encoding=encoding) as file: 29 | print("Writing contents to "+path) 30 | file.write(content) 31 | return path 32 | 33 | def putFile(filename,directory,content,mode="wb"): 34 | if(not(os.path.exists(directory))): 35 | os.makedirs(directory) 36 | 37 | path=os.path.join(directory,filename) 38 | 39 | with open(path,mode=mode) as file: 40 | print("Writing contents to "+path) 41 | file.write(content) 42 | return path 43 | 44 | def readSamplesFromFSB5(fsb): 45 | for sample in fsb.samples: 46 | try: 47 | yield sample.name,fsb.rebuild_sample(sample) 48 | except ValueError as e: 49 | print('FAILED to extract %r: %s'%(sample.name,e)) 50 | 51 | def getPKMHeader(width,height,tformat): 52 | header=b"\x50\x4B\x4D\x20" 53 | 54 | version=b"20" 55 | if tformat==TextureFormat.ETC_RGB4: 56 | version=b"10" 57 | formatD=0 58 | elif tformat==TextureFormat.ETC2_RGB: 59 | formatD=1 60 | elif tformat==TextureFormat.ETC2_RGBA8: 61 | formatD=3 62 | elif tformat==TextureFormat.ETC2_RGBA1: 63 | formatD=4 64 | elif tformat==TextureFormat.EAC_R: 65 | formatD=5 66 | elif tformat==TextureFormat.EAC_RG: 67 | formatD=6 68 | elif tformat==TextureFormat.EAC_R_SIGNED: 69 | formatD=7 70 | elif tformat==TextureFormat.EAC_RG_SIGNED: 71 | formatD=8 72 | else: 73 | formatD=0 74 | 75 | formatB=formatD.to_bytes(2,byteorder="big") 76 | widthB=width.to_bytes(2,byteorder="big") 77 | heightB=height.to_bytes(2,byteorder="big") 78 | 79 | return(header+version+formatB+widthB+heightB+widthB+heightB) 80 | 81 | class UnityGameResUnpack: 82 | 83 | apkExtractedPath="./orz/" 84 | assetsExtractTo="./assets/" 85 | 86 | def __init__(self,args): 87 | self.parseCmdArgs(args) 88 | 89 | def parseCmdArgs(self,args): 90 | parser=ArgumentParser() 91 | parser.add_argument("-o", "--outdir",nargs="?",default="",help="Directory where extracted files will be put",required=True) 92 | parser.add_argument("-i", "--indir",nargs="?",default="",help="Directory to parse",required=True) 93 | args=parser.parse_args(args) 94 | if(args.indir=='' or args.outdir==''): 95 | parser.print_help() 96 | exit() 97 | else: 98 | self.apkExtractedPath=args.indir 99 | self.assetsExtractTo=args.outdir 100 | print(args.indir,args.outdir) 101 | 102 | 103 | def handleFile(self,filepath,filedir): 104 | subdirname=filepath.replace(self.apkExtractedPath,"") 105 | filedirpath=filedir.replace(self.apkExtractedPath,"") 106 | while(subdirname[0]=="/" or subdirname[0]=="\\"): 107 | subdirname=subdirname[1:] 108 | if(filedirpath==""): 109 | filedirpath="." 110 | while(filedirpath[0]=="/" or filedirpath[0]=="\\"): 111 | filedirpath=filedirpath[1:] 112 | savepath=os.path.join(self.assetsExtractTo,subdirname) 113 | os.makedirs(savepath,exist_ok=True) 114 | filedirpath=os.path.join(self.assetsExtractTo,filedirpath) 115 | os.makedirs(filedirpath,exist_ok=True) 116 | # savepath=os.path.dirname(filepath); 117 | with open(filepath,'rb') as file: 118 | bundle=unitypack.load(file) 119 | for asset in bundle.assets: 120 | for id,obj in asset.objects.items(): 121 | try: 122 | data=obj.read() 123 | 124 | if obj.type=="AudioClip": 125 | print(obj.type,":",data.name) 126 | # extract samples 127 | bindata=data.data 128 | index=0 129 | while bindata: 130 | fsb=fsb5.load(bindata) 131 | ext=fsb.get_sample_extension() 132 | bindata=bindata[fsb.raw_size:] 133 | for sampleName,sampleData in readSamplesFromFSB5(fsb): 134 | filenameWrite=data.name+"--"+sampleName+'.'+ext 135 | putFile(filenameWrite,savepath,sampleData) 136 | index+=1 137 | 138 | elif obj.type=="Texture2D": 139 | print(obj.type+"["+str(data.format)+"]:",data.name) 140 | filename=data.name+".png" 141 | if data.format in ETC_SERIES: 142 | bindata=getPKMHeader(data.width,data.height,data.format)+data.image_data 143 | putFile('temp.pkm',currentWorkDir,bindata) 144 | subprocess.call(["./etcpack","./temp.pkm",currentWorkDir]) 145 | image=Image.open('./temp.ppm') 146 | else: 147 | image=data.image 148 | if image is None: 149 | print("WARNING: %s is an empty image"%(filename)) 150 | continue 151 | img=ImageOps.flip(image) 152 | output=io.BytesIO() 153 | img.save(output,format="png") 154 | putFile(filename,savepath,output.getvalue()) 155 | 156 | elif obj.type=="MovieTexture": 157 | print(obj.type,":",data.name) 158 | filename=data.name+".ogv" 159 | putFile(filename,savepath,data.movie_data) 160 | 161 | # elif obj.type=="Shader": 162 | # print(obj.type,":",data.name) 163 | # filename=data.name+".cg" 164 | # putFile(filename,savepath,data.script) 165 | 166 | elif obj.type=="Mesh": 167 | print(obj.type,":",data.name) 168 | try: 169 | meshdata=unitypack.export.OBJMesh(d).export() 170 | filename=data.name+".obj" 171 | putFile(filename,savepath,meshdata) 172 | except NotImplementedError as e: 173 | print("WARNING: Could not extract %r (%s)"%(d,e)) 174 | meshdata=pickle.dumps(data._obj) 175 | filename=data.name+".Mesh.pickle" 176 | putFile(filename,savepath,meshdata) 177 | 178 | elif obj.type=="Font": 179 | print(obj.type,":",data.name) 180 | filename=data.name+".ttf" 181 | putFile(filename,savepath,data.data) 182 | 183 | elif obj.type=="TextAsset": 184 | print(obj.type,":",data.name) 185 | # toSavePath=savepath 186 | toSavePath=filedirpath 187 | if isinstance(data.script,bytes): 188 | filename,mode=data.name,"wb" 189 | putFile(filename,toSavePath,data.script,mode=mode) 190 | else: 191 | filename,mode=data.name,"w" 192 | putTextFile(filename,toSavePath,data.script,mode=mode,encoding="utf-8") 193 | except Exception as e: 194 | print("WARNING: Error while processing %r (%s)"%(filepath,e)) 195 | 196 | def main(): 197 | ugru=UnityGameResUnpack(sys.argv[1:]) 198 | for currentPath,dirs,files in os.walk(ugru.apkExtractedPath): 199 | for nowFile in files: 200 | try: 201 | nowFilePath=os.path.join(currentPath,nowFile) 202 | with open(nowFilePath,'rb') as file: 203 | bundle=unitypack.load(file) 204 | except NotImplementedError as e: 205 | pass 206 | else: 207 | ugru.handleFile(nowFilePath,currentPath) 208 | 209 | 210 | if __name__=="__main__": 211 | main() 212 | --------------------------------------------------------------------------------