Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions Documentation/command-dump.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ UnityDataTool dump <path> [options]
| `--stdout` | Write the dump to stdout (status and errors go to stderr). Mutually exclusive with `-o`. | `false` |
| `-f, --output-format <format>` | Output format | `text` |
| `-a, --show-large-arrays` | Dump the full content of large arrays of basic data types, instead of summarizing them with a hash | `false` |
| `-x, --hexfloat` | Print the bit-exact hexadecimal representation after each float and double value | `false` |
| `-i, --objectid <id>` | Only dump object with this ID | All objects |
| `-t, --type <type>` | Filter by object type (ClassID number or type name) | All objects |
| `-d, --typetree-data <file>` | Load an external TypeTree data file before processing (Unity 6.5+) | — |
Expand All @@ -41,6 +42,15 @@ Dump the full content of large arrays (e.g. mesh or texture data):
UnityDataTool dump /path/to/file -a
```

Include the bit-exact hexadecimal representation of float and double values, useful for seeing tiny differences that get lost when converting to decimal (similar to the `-hexfloat` option of Unity's `binary2text` tool):
```bash
UnityDataTool dump /path/to/file -x
```
```
floatValue (float) 3.1415927(0x40490fdb)
doubleValue (double) 2.718281828459045(0x4005bf0a8b145769)
```

Dump only MonoBehaviour objects by type name:
```bash
UnityDataTool dump /path/to/file -t MonoBehaviour
Expand Down
31 changes: 27 additions & 4 deletions TextDumper/TextDumperTool.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.IO;
using System.Text;
using UnityDataTools.BinaryFormat;
Expand Down Expand Up @@ -34,6 +35,7 @@ public class DumpOptions
public string Path { get; init; }
public string OutputPath { get; init; }
public bool ShowLargeArrays { get; init; }
public bool HexFloat { get; init; }
public long ObjectId { get; init; }
public string TypeFilter { get; init; }
public bool ToStdout { get; init; }
Expand Down Expand Up @@ -382,11 +384,11 @@ void DumpArray(TypeTreeNode node, ref long offset, int level)
var array = ReadBasicTypeArray(dataNode, offset, arraySize);
offset += dataNode.Size * arraySize;

m_StringBuilder.Append(array.GetValue(0));
m_StringBuilder.Append(FormatArrayElement(array.GetValue(0)));
for (int i = 1; i < arraySize; ++i)
{
m_StringBuilder.Append(", ");
m_StringBuilder.Append(array.GetValue(i));
m_StringBuilder.Append(FormatArrayElement(array.GetValue(i)));
}
}

Expand Down Expand Up @@ -608,10 +610,10 @@ string ReadValue(TypeTreeNode node, long offset)
return m_Reader.ReadUInt32(offset).ToString();

case TypeCode.Single:
return m_Reader.ReadFloat(offset).ToString();
return FormatFloat(m_Reader.ReadFloat(offset));

case TypeCode.Double:
return m_Reader.ReadDouble(offset).ToString();
return FormatDouble(m_Reader.ReadDouble(offset));

case TypeCode.Int16:
return m_Reader.ReadInt16(offset).ToString();
Expand Down Expand Up @@ -640,6 +642,27 @@ string ReadValue(TypeTreeNode node, long offset)
}
}

// With the HexFloat option, floating point values are followed by their bit-exact hexadecimal
// representation (like binary2text -hexfloat), because tiny differences between two values can
// be lost when they are converted to decimal. The decimal part always uses the invariant
// culture so that dumps are identical (and diffable) across locales.
string FormatFloat(float value) =>
m_Options.HexFloat
? string.Create(CultureInfo.InvariantCulture, $"{value}(0x{BitConverter.SingleToUInt32Bits(value):x8})")
: value.ToString(CultureInfo.InvariantCulture);

string FormatDouble(double value) =>
m_Options.HexFloat
? string.Create(CultureInfo.InvariantCulture, $"{value}(0x{BitConverter.DoubleToUInt64Bits(value):x16})")
: value.ToString(CultureInfo.InvariantCulture);

string FormatArrayElement(object value) => value switch
{
float f => FormatFloat(f),
double d => FormatDouble(d),
_ => value.ToString(),
};

Array ReadBasicTypeArray(TypeTreeNode node, long offset, int arraySize)
{
// bool isn't blittable into Array.CreateInstance(typeof(bool), ...) the way other basic types
Expand Down
29 changes: 29 additions & 0 deletions UnityDataTool.Tests/DumpTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Globalization;
using System.IO;
using System.Threading.Tasks;
using NUnit.Framework;
Expand Down Expand Up @@ -314,6 +315,34 @@ public async Task Dump_Stdout_AssetBundle_SerializationDemo_ContainsExpectedFiel
Assert.That(output, Does.Not.Contain("293, 294, 295, 296,"));
}

// The expected bit patterns are the well-known IEEE 754 representations of the
// SerializationDemo field values (also verified against python struct.pack).
// Runs under a comma-decimal locale to confirm the output is culture-invariant.
[Test]
public async Task Dump_Stdout_HexFloat_PrintsBitExactRepresentation(
[Values("-x", "--hexfloat")] string options)
{
using var sw = new StringWriter();
var currentOut = Console.Out;
var currentCulture = CultureInfo.CurrentCulture;
try
{
Console.SetOut(sw);
CultureInfo.CurrentCulture = new CultureInfo("de-DE");
Assert.AreEqual(0, await Program.Main(new string[] { "dump", m_SerializationDemoBundlePath, "--stdout", "--type", "MonoBehaviour", options }));
}
finally
{
Console.SetOut(currentOut);
CultureInfo.CurrentCulture = currentCulture;
}

var output = sw.ToString();

Assert.That(output, Does.Contain("floatValue (float) 3.1415927(0x40490fdb)"));
Assert.That(output, Does.Contain("doubleValue (double) 2.718281828459045(0x4005bf0a8b145769)"));
}

[Test]
public async Task Dump_Stdout_ShowLargeArrays_PrintsFullArrayContent()
{
Expand Down
7 changes: 5 additions & 2 deletions UnityDataTool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ static Command BuildDumpCommand()
var pathArg = new Argument<FileInfo>("filename", "The path of the file to dump").ExistingOnly();
var fOpt = new Option<TextDumperTool.DumpFormat>(aliases: new[] { "--output-format", "-f" }, description: "Output format", getDefaultValue: () => TextDumperTool.DumpFormat.Text);
var aOpt = new Option<bool>(aliases: new[] { "--show-large-arrays", "-a" }, description: "Dump the full content of large arrays of basic data types, instead of summarizing them with a hash");
var xOpt = new Option<bool>(aliases: new[] { "--hexfloat", "-x" }, description: "Print the bit-exact hexadecimal representation after each float and double value, e.g. 0.25(0x3e800000)");
// Former option, kept so that existing scripts don't break. Ignored because skipping
// large arrays (with a hash) is now the default behavior.
var sOpt = new Option<bool>(aliases: new[] { "--skip-large-arrays", "-s" }) { IsHidden = true };
Expand All @@ -182,6 +183,7 @@ static Command BuildDumpCommand()
pathArg,
fOpt,
aOpt,
xOpt,
sOpt,
oOpt,
objectIdOpt,
Expand All @@ -201,7 +203,7 @@ static Command BuildDumpCommand()
}
});
dumpCommand.SetHandler(
(FileInfo fi, TextDumperTool.DumpFormat f, bool a, DirectoryInfo o, long objectId, string type, FileInfo d, bool toStdout) =>
(FileInfo fi, TextDumperTool.DumpFormat f, bool a, bool x, DirectoryInfo o, long objectId, string type, FileInfo d, bool toStdout) =>
{
var ttResult = LoadTypeTreeDataFile(d);
if (ttResult != 0) return Task.FromResult(ttResult);
Expand All @@ -211,13 +213,14 @@ static Command BuildDumpCommand()
Path = fi.FullName,
OutputPath = o.FullName,
ShowLargeArrays = a,
HexFloat = x,
ObjectId = objectId,
TypeFilter = type,
ToStdout = toStdout,
};
return Task.FromResult(HandleDump(options));
},
pathArg, fOpt, aOpt, oOpt, objectIdOpt, typeOpt, dOpt, stdoutOpt);
pathArg, fOpt, aOpt, xOpt, oOpt, objectIdOpt, typeOpt, dOpt, stdoutOpt);

return dumpCommand;
}
Expand Down
Loading