|
package main |
|
|
|
import ( |
|
"bufio" |
|
"bytes" |
|
"encoding/binary" |
|
"flag" |
|
"fmt" |
|
"io" |
|
"os" |
|
) |
|
|
|
var ( |
|
fileName = flag.String("file", "", "Path to file for dump/modification") |
|
patchName = flag.String("patch", "", "Patch mode (mainData|sharedassets|test)") |
|
typeMap = make(map[int32]Dumper, 200) |
|
) |
|
|
|
type Dumper interface { |
|
fromBytes(r *bytes.Reader, endianness binary.ByteOrder) |
|
} |
|
|
|
type Header struct { |
|
HeaderSize uint32 |
|
FileSize uint32 |
|
Version uint32 |
|
DataStart uint32 |
|
Endianness uint8 |
|
Reserved [0x17]uint8 |
|
} |
|
|
|
type ObjectInfo struct { |
|
Index uint32 |
|
DataOffset uint32 |
|
DataSize uint32 |
|
TypeId int32 |
|
ClassId int32 |
|
} |
|
|
|
type ExternalInfo struct { |
|
TypeId int32 |
|
Guid [4]uint32 |
|
Name string |
|
} |
|
|
|
type ObjectDatum struct { |
|
TypeId int32 |
|
ClassId int32 |
|
Data []byte |
|
} |
|
|
|
type MonoScript struct { |
|
Name string |
|
ClassName string |
|
Namespace string |
|
AssemblyName string |
|
PropertiesHash uint32 |
|
ExecutionOrder int32 |
|
IsEditorScript uint8 |
|
} |
|
|
|
type PPtr struct { |
|
FileIndex uint32 |
|
LocalIndex uint32 |
|
} |
|
|
|
type MonoBehaviour struct { |
|
GameObject PPtr |
|
Enabled int32 |
|
MonoScript PPtr |
|
Name string |
|
Data []byte // Preloaded instance data |
|
} |
|
|
|
type MonoManager struct { |
|
Scripts []PPtr |
|
AssemblyNames []string |
|
} |
|
|
|
type ScriptMapper struct { |
|
Shaders map[string]PPtr |
|
} |
|
|
|
type GOComponent struct { |
|
Index int32 |
|
Component PPtr |
|
} |
|
|
|
type GameObject struct { |
|
Components []GOComponent |
|
Layer uint32 |
|
Name string |
|
Tag uint16 |
|
Enabled bool |
|
} |
|
|
|
type BuildSettings struct { |
|
Levels []string |
|
// The rest doesn't matter yet |
|
} |
|
|
|
type PreloadData struct { |
|
Name string |
|
Assets []PPtr |
|
} |
|
|
|
func readExternalInfo(file *bufio.Reader, endianness binary.ByteOrder) ExternalInfo { |
|
var theExternal ExternalInfo |
|
binary.Read(file, endianness, &theExternal.TypeId) |
|
binary.Read(file, endianness, &theExternal.Guid) |
|
file.ReadByte() |
|
bufReader := bufio.NewReader(file) |
|
line, _ := bufReader.ReadBytes(0) |
|
theExternal.Name = string(line[:len(line)-1]) |
|
return theExternal |
|
} |
|
|
|
func writeExternalInfo(file io.Writer, endianness binary.ByteOrder, info ExternalInfo) { |
|
binary.Write(file, endianness, info.TypeId) |
|
binary.Write(file, endianness, info.Guid) |
|
file.Write([]byte{0}) |
|
file.Write([]byte(info.Name)) |
|
file.Write([]byte{0}) |
|
} |
|
|
|
func align(offset, alignment uint32) uint32 { |
|
return (offset + alignment - 1) & ^(alignment - 1) |
|
} |
|
|
|
func readUnityStr(r io.ReadSeeker, endianness binary.ByteOrder) string { |
|
var length uint32 |
|
binary.Read(r, endianness, &length) |
|
if length > 0 { |
|
lengthAligned := align(length, 4) |
|
resultBuf := make([]byte, length) |
|
extraBytes := make([]byte, lengthAligned-length) |
|
_, err := r.Read(resultBuf) |
|
if err != nil { |
|
panic(err) |
|
} |
|
r.Read(extraBytes) |
|
return string(resultBuf) |
|
} else { |
|
return "" |
|
} |
|
} |
|
|
|
func writeUnityStr(w io.Writer, endianness binary.ByteOrder, s string) { |
|
stringLength := uint32(len(s)) |
|
binary.Write(w, endianness, stringLength) |
|
if stringLength > 0 { |
|
strBytes := []byte(s) |
|
w.Write(strBytes) |
|
extraBytes := make([]byte, align(stringLength, 4)-stringLength) |
|
w.Write(extraBytes) |
|
} |
|
} |
|
|
|
func (o *MonoScript) fromBytes(r *bytes.Reader, endianness binary.ByteOrder) { |
|
o.Name = readUnityStr(r, endianness) |
|
binary.Read(r, endianness, &o.ExecutionOrder) |
|
binary.Read(r, endianness, &o.PropertiesHash) |
|
o.ClassName = readUnityStr(r, endianness) |
|
o.Namespace = readUnityStr(r, endianness) |
|
o.AssemblyName = readUnityStr(r, endianness) |
|
binary.Read(r, endianness, &o.IsEditorScript) |
|
} |
|
|
|
func (o *MonoScript) toBytes(w *bytes.Buffer, endianness binary.ByteOrder) { |
|
writeUnityStr(w, endianness, o.Name) |
|
binary.Write(w, endianness, o.ExecutionOrder) |
|
binary.Write(w, endianness, o.PropertiesHash) |
|
writeUnityStr(w, endianness, o.ClassName) |
|
writeUnityStr(w, endianness, o.Namespace) |
|
writeUnityStr(w, endianness, o.AssemblyName) |
|
binary.Write(w, endianness, o.IsEditorScript) |
|
} |
|
|
|
func (o *MonoBehaviour) fromBytes(r *bytes.Reader, endianness binary.ByteOrder) { |
|
binary.Read(r, endianness, &o.GameObject) |
|
binary.Read(r, endianness, &o.Enabled) |
|
binary.Read(r, endianness, &o.MonoScript) |
|
o.Name = readUnityStr(r, endianness) |
|
theRest := make([]byte, r.Len()) |
|
r.Read(theRest) |
|
o.Data = theRest |
|
} |
|
|
|
func (o *MonoBehaviour) toBytes(w *bytes.Buffer, endianness binary.ByteOrder) { |
|
binary.Write(w, endianness, o.GameObject) |
|
binary.Write(w, endianness, o.Enabled) |
|
binary.Write(w, endianness, o.MonoScript) |
|
writeUnityStr(w, endianness, o.Name) |
|
w.Write(o.Data) |
|
} |
|
|
|
func (o *MonoManager) fromBytes(r *bytes.Reader, endianness binary.ByteOrder) { |
|
var scriptsLength uint32 |
|
binary.Read(r, endianness, &scriptsLength) |
|
o.Scripts = make([]PPtr, scriptsLength) |
|
for i := uint32(0); i < scriptsLength; i++ { |
|
binary.Read(r, endianness, &o.Scripts[i]) |
|
} |
|
|
|
var assembliesLength uint32 |
|
binary.Read(r, endianness, &assembliesLength) |
|
o.AssemblyNames = make([]string, assembliesLength) |
|
for i := uint32(0); i < assembliesLength; i++ { |
|
o.AssemblyNames[i] = readUnityStr(r, endianness) |
|
} |
|
} |
|
|
|
func (o *MonoManager) toBytes(w *bytes.Buffer, endianness binary.ByteOrder) { |
|
binary.Write(w, endianness, uint32(len(o.Scripts))) |
|
for i := range o.Scripts { |
|
binary.Write(w, endianness, o.Scripts[i]) |
|
} |
|
binary.Write(w, endianness, uint32(len(o.AssemblyNames))) |
|
for i := range o.AssemblyNames { |
|
writeUnityStr(w, endianness, o.AssemblyNames[i]) |
|
} |
|
} |
|
|
|
func (o *ScriptMapper) fromBytes(r *bytes.Reader, endianness binary.ByteOrder) { |
|
var shadersLength uint32 |
|
binary.Read(r, endianness, &shadersLength) |
|
o.Shaders = make(map[string]PPtr, shadersLength) |
|
for i := uint32(0); i < shadersLength; i++ { |
|
var script PPtr |
|
binary.Read(r, endianness, &script) |
|
name := readUnityStr(r, endianness) |
|
o.Shaders[name] = script |
|
} |
|
} |
|
|
|
func (o *BuildSettings) fromBytes(r *bytes.Reader, endianness binary.ByteOrder) { |
|
var levelsLength uint32 |
|
binary.Read(r, endianness, &levelsLength) |
|
o.Levels = make([]string, levelsLength) |
|
for i := uint32(0); i < levelsLength; i++ { |
|
o.Levels[i] = readUnityStr(r, endianness) |
|
} |
|
} |
|
|
|
func (o *GameObject) fromBytes(r *bytes.Reader, endianness binary.ByteOrder) { |
|
var componentsLength uint32 |
|
binary.Read(r, endianness, &componentsLength) |
|
o.Components = make([]GOComponent, componentsLength) |
|
for i := uint32(0); i < componentsLength; i++ { |
|
binary.Read(r, endianness, &o.Components[i]) |
|
} |
|
binary.Read(r, endianness, &o.Layer) |
|
o.Name = readUnityStr(r, endianness) |
|
binary.Read(r, endianness, &o.Tag) |
|
binary.Read(r, endianness, &o.Enabled) |
|
} |
|
|
|
func (o *PreloadData) fromBytes(r *bytes.Reader, endianness binary.ByteOrder) { |
|
o.Name = readUnityStr(r, endianness) |
|
var assetsLength uint32 |
|
binary.Read(r, endianness, &assetsLength) |
|
o.Assets = make([]PPtr, assetsLength) |
|
for i := uint32(0); i < assetsLength; i++ { |
|
binary.Read(r, endianness, &o.Assets[i]) |
|
} |
|
} |
|
|
|
type dumpMonoScript struct{} |
|
type dumpMonoBehaviour struct{} |
|
|
|
func dumpFormatter(info *ObjectInfo, data interface{}) string { |
|
return fmt.Sprintf("%d: <%d>[0x%x] %v", |
|
info.Index, |
|
info.ClassId, |
|
info.DataOffset, |
|
data) |
|
} |
|
|
|
func initTypeMap() { |
|
typeMap[1] = &GameObject{} |
|
typeMap[94] = &ScriptMapper{} |
|
typeMap[114] = &MonoBehaviour{} |
|
typeMap[115] = &MonoScript{} |
|
typeMap[116] = &MonoManager{} |
|
typeMap[141] = &BuildSettings{} |
|
typeMap[150] = &PreloadData{} |
|
} |
|
|
|
func main() { |
|
flag.Parse() |
|
initTypeMap() |
|
fileRaw, err := os.Open(*fileName) |
|
if err != nil { |
|
panic(err) |
|
} |
|
file := bufio.NewReader(fileRaw) |
|
|
|
var header Header |
|
binary.Read(file, binary.BigEndian, &header) |
|
|
|
var endianness binary.ByteOrder |
|
endianness = binary.BigEndian |
|
if header.Endianness == 0 { |
|
endianness = binary.LittleEndian |
|
} |
|
|
|
var numObjectInfos uint32 |
|
var numExternals uint32 |
|
binary.Read(file, endianness, &numObjectInfos) |
|
objectInfos := make([]ObjectInfo, numObjectInfos) |
|
for i := uint32(0); i < numObjectInfos; i++ { |
|
var theObjectInfo ObjectInfo |
|
binary.Read(file, endianness, &theObjectInfo) |
|
objectInfos[i] = theObjectInfo |
|
} |
|
binary.Read(file, endianness, &numExternals) |
|
externals := make([]ExternalInfo, numExternals) |
|
for i := uint32(0); i < numExternals; i++ { |
|
externals[i] = readExternalInfo(file, endianness) |
|
} |
|
|
|
objectData := make([]ObjectDatum, numObjectInfos) |
|
for i := uint32(0); i < numObjectInfos; i++ { |
|
objectData[i].Data = make([]byte, objectInfos[i].DataSize) |
|
fileRaw.ReadAt(objectData[i].Data, int64(header.DataStart+objectInfos[i].DataOffset)) |
|
objectData[i].TypeId = objectInfos[i].TypeId |
|
objectData[i].ClassId = objectInfos[i].ClassId |
|
} |
|
|
|
fmt.Println(externals) |
|
fmt.Println(header) |
|
for i := uint32(0); i < numObjectInfos; i++ { |
|
dumper, ok := typeMap[objectInfos[i].ClassId] |
|
if ok { |
|
dumper.fromBytes(bytes.NewReader(objectData[i].Data), endianness) |
|
fmt.Println(dumpFormatter(&objectInfos[i], dumper)) |
|
} else { |
|
fmt.Printf("%d: <%d>[0x%x] %x\n", |
|
objectInfos[i].Index, |
|
objectData[i].ClassId, |
|
objectInfos[i].DataOffset, |
|
objectData[i].Data) |
|
} |
|
} |
|
|
|
fileRaw.Close() |
|
if *patchName == "mainData" || |
|
*patchName == "sharedassets" || |
|
*patchName == "test" { |
|
fileWrite, err := os.Create(fmt.Sprintf("%s-1", *fileName)) |
|
if err != nil { |
|
panic(err) |
|
} |
|
fileWriter := bufio.NewWriter(fileWrite) |
|
// Writing back -- offsets align to &-8 |
|
// Rebuild metadata, adjust header sizes if necessary (only data should be?) |
|
// Data section seems to align &-0xf && > 0x1000 |
|
// |
|
// To load: |
|
// Create assembly with monobehaviours |
|
// place assembly in managed folder |
|
// add assembly path to MonoManager in mainData |
|
// add monoscripts to the end of mainData |
|
// add monobehaviours to the end of {2} = sharedassets0.assets |
|
// pray. |
|
|
|
// Do hacky stuff |
|
if *patchName == "sharedassets" { |
|
// Append a MonoScript |
|
var monoScript MonoScript |
|
monoScript.AssemblyName = "Our.dll" |
|
monoScript.Namespace = "" |
|
monoScript.ClassName = "OurMono" |
|
monoScript.PropertiesHash = 0 |
|
monoScript.ExecutionOrder = 1200 |
|
monoScript.Name = "OurMono" |
|
var outBuf bytes.Buffer |
|
monoScript.toBytes(&outBuf, endianness) |
|
var newObj ObjectDatum |
|
newObj.ClassId = 115 |
|
newObj.TypeId = 115 |
|
newObj.Data = outBuf.Bytes() |
|
objectData = append(objectData, newObj) |
|
} |
|
|
|
if *patchName == "mainData" { |
|
// Append MonoManager |
|
obj := &objectData[5] // manual index, but the MonoManager is usually here |
|
var monoManager MonoManager |
|
monoManager.fromBytes(bytes.NewReader(obj.Data), endianness) |
|
monoManager.AssemblyNames = append(monoManager.AssemblyNames, "Our.dll") |
|
// This PPtr is manually generated. 2 is from the ExternalInfo files on |
|
// mainData (2 = sharedassets0 in this case), and 1360 is from the index |
|
// that the sharedassets patch writes to--it will change as classes are |
|
// added/removed from the Unity project. |
|
monoManager.Scripts = append(monoManager.Scripts, PPtr{2, 1360}) |
|
fmt.Println(dumpFormatter(&objectInfos[5], monoManager)) |
|
var outBuf bytes.Buffer |
|
monoManager.toBytes(&outBuf, endianness) |
|
fmt.Println(outBuf) |
|
obj.Data = outBuf.Bytes() |
|
|
|
// Append a MonoBehaviour |
|
var monoBehaviour MonoBehaviour |
|
monoBehaviour.Enabled = 1 |
|
monoBehaviour.GameObject = PPtr{0, 18} |
|
monoBehaviour.MonoScript = PPtr{2, 1360} // See above |
|
monoBehaviour.Name = "" |
|
monoBehaviour.Data = []byte{} |
|
var outBuf2 bytes.Buffer |
|
monoBehaviour.toBytes(&outBuf2, endianness) |
|
var newObj ObjectDatum |
|
newObj.ClassId = 114 |
|
newObj.TypeId = 114 |
|
newObj.Data = outBuf2.Bytes() |
|
objectData = append(objectData, newObj) |
|
} |
|
// End hackiest stuff |
|
|
|
// 1. Write externals to determine length |
|
var externalsBuf bytes.Buffer |
|
binary.Write(&externalsBuf, endianness, uint32(len(externals))) |
|
for i := range externals { |
|
writeExternalInfo(&externalsBuf, endianness, externals[i]) |
|
} |
|
fmt.Println(externalsBuf, externalsBuf.Len()) |
|
// Pre object-data size is sizeof(ObjectInfo) = 0x14 * len(objectData) + |
|
// sizeof(int32) + sizeof(externalsBuf) + sizeof(Header) = 0x28 |
|
// Data offset should be max(this size aligned, 0x1000), with alignment 0x10 |
|
// After that is calculated, metadata can be generated for all object data |
|
// (index, offset, size). |
|
// Append only, changing indices will ruin PPtr references. Ruining pptr refs |
|
// requires the ability to rebuild assetbundles. |
|
headerSize := uint32(0x14*len(objectData) + 0x4 + externalsBuf.Len() + 0x28) |
|
dataStart := align(headerSize, 0x10) |
|
if dataStart < 0x1000 { |
|
dataStart = 0x1000 |
|
} |
|
objectInfosNew := make([]ObjectInfo, len(objectData)) |
|
dataOffset := uint32(0) |
|
for i := range objectData { |
|
o := &objectInfosNew[i] |
|
o.DataOffset = dataOffset |
|
o.DataSize = uint32(len(objectData[i].Data)) |
|
o.Index = uint32(i + 1) |
|
o.ClassId = objectData[i].ClassId |
|
o.TypeId = objectData[i].TypeId |
|
dataOffset = align(dataOffset+o.DataSize, 8) |
|
} |
|
// Fill in header data |
|
header.HeaderSize = headerSize - 0x14 |
|
header.FileSize = dataStart + dataOffset |
|
header.DataStart = dataStart |
|
// Write header |
|
binary.Write(fileWriter, binary.BigEndian, header) |
|
// Write metadata |
|
binary.Write(fileWriter, endianness, uint32(len(objectInfosNew))) |
|
for i := range objectInfosNew { |
|
binary.Write(fileWriter, endianness, objectInfosNew[i]) |
|
} |
|
// Write externals + padding |
|
fileWriter.Write(externalsBuf.Bytes()) |
|
fileWriter.Write(make([]byte, dataStart-headerSize)) |
|
// Write data |
|
for i := range objectData { |
|
binary.Write(fileWriter, endianness, objectData[i].Data) |
|
padding := align(uint32(len(objectData[i].Data)), 0x8) - uint32(len(objectData[i].Data)) |
|
fileWriter.Write(make([]byte, padding)) |
|
} |
|
fileWriter.Flush() |
|
fileWrite.Close() |
|
} |
|
} |
Hey man, what does this allow one to do?