Skip to content

Instantly share code, notes, and snippets.

@robert-nix
Last active September 27, 2016 16:51
Show Gist options
  • Save robert-nix/5782651 to your computer and use it in GitHub Desktop.
Save robert-nix/5782651 to your computer and use it in GitHub Desktop.
Hack tool to rewrite Hearthstone's Unity3d asset files with a custom assembly/monobehaviour
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()
}
}
using System;
using System.IO;
using System.Threading;
using UnityEngine;
public class OurMono : MonoBehaviour
{
StreamWriter log;
Timer t;
bool doIt;
public void Awake()
{
doIt = false;
log = File.AppendText("our.log");
Blizzard.Log.SayToFile(log, "It works!");
t = new Timer((_) => doIt = true, null, 1000, Timeout.Infinite);
}
public void Update()
{
if (doIt)
{
doIt = false;
Blizzard.Log.SayToFile(log, "Did it!");
}
}
public void Start()
{
}
}

Make copies of the current version's assets

cp ../Hearthstone_Data/sharedassets0.assets ../Hearthstone_data/sharedassets0.assets-latest
cp ../Hearthstone_Data/mainData ../Hearthstone_data/mainData-latest

Patch the copies

go run assetwriter.go -file="../Hearthstone_Data/sharedassets0.assets-latest" -patch="sharedassets"
go run assetwriter.go -file="../Hearthstone_Data/mainData-latest" -patch="mainData"

Results will have a "-1" appended because why not (e.g. sharedassets0.assets-latest-1). I suggest copying those into a subdirectory of Hearthstone_Data such as ../Hearthstone_Data/modded, renaming them to sharedassets0.assets and mainData, and finally overwriting the corresponding files in Hearthstone_Data.

As the script is currently configured, it will load the class ::OurMono as a MonoBehaviour from Our.dll in the Managed folder. You can compile this dll with mono or csc; there's a sample .cs file below.

@Trumpie
Copy link

Trumpie commented Jan 10, 2016

The background mumbles in hearthstone are incredibly annoying. Can this be used to decompile the unity3d sound files (~/hearthstone/data/win) and perhaps remove certain tracks?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment