K0lb3 / UnityPy

UnityPy is python module that makes it possible to extract/unpack and edit Unity assets
MIT License
761 stars 113 forks source link

Edit Texture2D make file corrupted #230

Closed AXiX-official closed 4 months ago

AXiX-official commented 4 months ago

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

AXiX-official commented 4 months ago

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?

JunkBeat commented 4 months ago

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

AXiX-official commented 4 months ago

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

JunkBeat commented 4 months ago

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

AXiX-official commented 4 months ago

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

AXiX-official commented 4 months ago

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

JunkBeat commented 4 months ago

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"))
AXiX-official commented 4 months ago

thanks for your reply

AXiX-official commented 3 months ago

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