K0lb3 / UnityPy

UnityPy is python module that makes it possible to extract/unpack and edit Unity assets

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Edit Texture2D make file corrupted

AXiX-official opened this issue · comments

commented

Code
I use example code to edit Texture2D

from PIL import Image
for obj in env.objects:
    if obj.type.name == "Texture2D":
        # export texture
        data = obj.read()
        data.image.save(path)
        # edit texture
        fp = os.path.join(replace_dir, data.name)
        pil_img = Image.open(fp)
        data.image = pil_img
        data.save()

and save like this

with open(dst, "wb") as f:
    f.write(env.file.save(packer="original"))

Error
I got

The file 'archive:/CAB-3a309dcafc425e59c83a625cb055748f/CAB-3a309dcafc425e59c83a625cb055748f' is corrupted! Remove it and launch unity again!
[Position out of bounds!]
UnityEngine.AssetBundle:LoadAsset(String)
ResLoaderBundle:GetRes(String, Object)
Seven.UIBase:DynamicLoadRes(String, String, Boolean)
Seven.UISpine:SetData(String)
XLua.CSObjectWrap.SevenUISpineWrap:_m_SetData(IntPtr)

mabye because the game implemented its own dynamic resource loading strategy

Bug
however,using assetstudio to see dump information about Texture2D before and after edit, something broken

before

Texture2D Base
	string m_Name = "UI_jiazai"
	int m_ForcedFallbackFormat = 4
	bool m_DownscaleFallback = False
	int m_Width = 2048
	int m_Height = 2048
	int m_CompleteImageSize = 4194304
	int m_TextureFormat = 47
	int m_MipCount = 1
	bool m_IsReadable = False
	bool m_IgnoreMasterTextureLimit = False
	bool m_IsPreProcessed = False
	bool m_StreamingMipmaps = False
	int m_StreamingMipmapsPriority = 0
	int m_ImageCount = 1
	int m_TextureDimension = 2
	GLTextureSettings m_TextureSettings
		int m_FilterMode = 1
		int m_Aniso = 1
		float m_MipBias = 0
		int m_WrapU = 1
		int m_WrapV = 1
		int m_WrapW = 1
	int m_LightmapFormat = 6
	int m_ColorSpace = 1
	TypelessData image data
	int size = 0
	StreamingInfo m_StreamData
		unsigned int offset = 0
		unsigned int size = 4194304
		string path = "archive:/CAB-d7da4949770a0f0599e3534deda1589a/CAB-d7da4949770a0f0599e3534deda1589a.resS"
	int m_OriginalWidth = 0
	int m_OriginalHeight = 0
	GUID m_OriginalAssetGuid
		unsigned int data[0] = 0
		unsigned int data[1] = 0
		unsigned int data[2] = 0
		unsigned int data[3] = 0

after

Texture2D Base
	string m_Name = "UI_jiazai"
	int m_ForcedFallbackFormat = 4
	bool m_DownscaleFallback = False
	int m_Width = 2048
	int m_Height = 2048
	int m_CompleteImageSize = 4194304
	int m_TextureFormat = 47
	int m_MipCount = 1
	bool m_IsReadable = False
	bool m_IgnoreMasterTextureLimit = False
	bool m_IsPreProcessed = False
	bool m_StreamingMipmaps = False
	int m_StreamingMipmapsPriority = 0
	int m_ImageCount = 1
	int m_TextureDimension = 2
	GLTextureSettings m_TextureSettings
		int m_FilterMode = 1
		int m_Aniso = 1
		float m_MipBias = 0
		int m_WrapU = 1
		int m_WrapV = 1
		int m_WrapW = 1
	int m_LightmapFormat = 6
	int m_ColorSpace = 1
	TypelessData image data
	int size = 4194304
	StreamingInfo m_StreamData
		unsigned int offset = 0
		unsigned int size = 0
		string path = ""
	int m_OriginalWidth = 0
	int m_OriginalHeight = 9
	GUID m_OriginalAssetGuid
		unsigned int data[0] = 1784629589
		unsigned int data[1] = 1635410281
		unsigned int data[2] = 105
		unsigned int data[3] = 0

i think that's the problem,i tried uabea and got the same result

To Reproduce

  • following data:
    • Python 3.8
    • UnityPy 1.10.7
    • UnityVersion 2019.4.40f1c1、2019.4.40f1、2018.4.34f1
commented

I fixed code in Texture2D.py tring to edit an existed cab and get error.

in Texture2D.py

    @image_data.setter
    def image_data(self, data: bytes):
        self._image_data = data
        # ignore writing to cab for now until it's more stable
        if self.version >= (5, 3) and self.m_StreamData.path:
            path = self.m_StreamData.path
            cab = self.assets_file.get_writeable_cab(path.split("/")[-1])
            if cab:
                self.m_StreamData.offset = cab.Position
                cab.write(data)
                self.m_StreamData.size = len(data)
                self.m_StreamData.path = cab.path

and i got this

Traceback (most recent call last):
  File "F:\blhx\lsns-decryptor\main.py", line 19, in <module>
    data.image = pil_img
  File "F:\blhx\lsns-decryptor\venv\lib\site-packages\UnityPy\classes\Texture2D.py", line 40, in image
    self.image_data = img_data
  File "F:\blhx\lsns-decryptor\venv\lib\site-packages\UnityPy\classes\Texture2D.py", line 72, in image_data
    cab = self.assets_file.get_writeable_cab(path.split("/")[-1])
  File "F:\blhx\lsns-decryptor\venv\lib\site-packages\UnityPy\files\SerializedFile.py", line 417, in get_writeable_cab
    cab = self.parent.get_writeable_cab(name)
  File "F:\blhx\lsns-decryptor\venv\lib\site-packages\UnityPy\files\File.py", line 106, in get_writeable_cab
    raise ValueError(
ValueError: This cab already exists and isn't an EndianBinaryWriter

can i remove this cab and add modified cab myself?

Hello. Can you provide the bundle file? I'm just curious, maybe I can help.

commented

ui_jiazai.zip
the origin file uses unityCN encrypted,I made a decrypt version according to the RazTools/Studio
but the problem i met has nothing to do with this,because it's a widely existed problem.the source code said writing to cab for is not stable.
I tested two other games in version 2019.4.40f1、2018.4.34f1(one is azur lane)and got the same,though in those games this dosent matter.
use UABEA can remove cab record and use unitypy add a new modfied one,but it changed file struct a low makes my encrypt application strike

It's strange but UnityPy thinks the file is encrypted
LookupError: The BundleFile is encrypted, but no key was provided!

commented

haha,it's becauese in that version of decryptor i forgot fix header's flags and blocks' flag so UnityPy thinks the file is encrypted.
however,i said it's not file's problem.editing cab is a not fully supported feature in both unitypy and uabea
ui_jiazai.zip

commented

I'm happy to tell you that I managed to solve this problem.
In fact if you simply remove the cab-****.resS file via uabea's remove and then add the cab using unitypy it does corrupt the file. The problem is that when uabea removes the .resS file, it does not remove the corresponding reference in the cab that references it, and when it adds the cab later using unitypy, it will add additional records to it. (You can verify this yourself by looking at the contents of the modified file).
So my solution to this is, in cases where the .resS file I need to work with is just Texture2D data, to somehow convert the modified.png that I need to replace to the textual2D binary that unity uses, e.g. by using the unitypy

img_data, tex_format = Texture2DConverter.image_to_texture2d(
            img, self.m_TextureFormat
        )

Then find the corresponding data in the unpacked bundlefile and replace it directly before repacking.
This completely solves the problem I encountered above.
After that I might write a program to do it? Maybe

You can write an embedded texture using typetree changing just a few values (not in resS).

from PIL import Image
for obj in env.objects:
    if obj.type.name == "Texture2D":
        data = obj.read()
        fp = os.path.join(replace_dir, data.name)
        pil_img = Image.open(fp)
    
        # UnityPy converts to original format by default
        data.set_image(pil_img)
        data.save_via_tree()

classes\Texture2D.py

def save_via_tree(self):
    tree = self.read_typetree()

    if "m_MipMap" in tree:
        tree["m_MipMap"] = self.m_MipMap
    else:
        tree["m_MipCount"] = self.m_MipCount

    tree["m_TextureFormat"] = self.m_TextureFormat
    tree["m_CompleteImageSize"] = len(self.image_data)
    tree["image data"] = self.image_data

    # Reset StreamData
    tree["m_StreamData"] = {
        "offset": 0,
        "size": 0,
        "path": ""
    }
    self.save_typetree_dump(tree)

classes\Object.py

def save_typetree_dump(self, tree: dict, nodes: list = None, writer: EndianBinaryWriter = None):
  return self.reader.save_typetree(tree, nodes, writer)

Saving

for name, file in env.files.items():
  if hasattr(file, "is_changed") and file.is_changed:
      file_name = os.path.basename(name)
      with open(os.path.join(dest, file_name), "wb") as f:
          f.write(file.save(packer="original"))
commented

thanks for your reply

commented

my solution be like this

import os
from PIL import Image
import UnityPy
from UnityPy.export import Texture2DConverter

dir = r"work"
src = r"test.asset"
dst = r"work\test.asset.fix"

# set unityCN key for unityCN
# bundle_key = ""
# UnityPy.set_assetbundle_decrypt_key(bundle_key)

# load file
env = UnityPy.load(os.path.join(dir, src))

# view Texture2D info
# some file has more than one texture2D data store in .resS
textureList = {}
totalSize = 0
for obj in env.objects:
    if obj.type.name == "Texture2D":
        data = obj.read()
        # print(data.name)
        # print(f"offset:{data.m_StreamData.offset}")
        # print(f"size:{data.m_StreamData.size}")
        # print(f"path:{data.m_StreamData.path}")
        # print(f"TextureFormat:{data.m_TextureFormat}")
        textureList[data.name] = {
            "offset": data.m_StreamData.offset,
            "size": data.m_StreamData.size,
            "path": data.m_StreamData.path,
            "TextureFormat": data.m_TextureFormat
        }
        totalSize += data.m_StreamData.size

# convert data
# toLoadImg should be the same wide/height as toEditTexture
# so that img_data has the same size as data.m_StreamData.size
toEditTextureName = "example"
toLoadImg = os.path.join(dir, "example.fix.png")
img = Image.open(toLoadImg)
toEditTexture = textureList[toEditTextureName]
img_data, tex_format = Texture2DConverter.image_to_texture2d(
    img, toEditTexture["TextureFormat"]
)
# replace data with different size is possible
# when needed i will add support
if len(img_data) != toEditTexture["size"]:
    raise ValueError(f"data size missmatch,except {toEditTexture['size']} but recieved {len(img_data)}")

# save file with no compression
newFileData = bytearray(env.file.save(packer="none"))
size = len(newFileData)
begin = size - totalSize + toEditTexture["offset"]
newFileData[begin:begin + toEditTexture["size"]] = img_data
with open(dst, 'wb') as f:
    f.write(newFileData)

env = UnityPy.load(dst)
with open(dst, 'wb') as f:
    f.write(env.file.save(packer="lz4"))