diff --git a/README.md b/README.md
index 9d4b01b..ba6619e 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,6 @@
# Xenoblade Research
Repo for scratch work from reversing all 3 Xenoblade games, tools for reading their files, and information discovered by reverse engineering them.
+
+## Includes: Xenoblade X/2/DE Bdat Editor+
+Modding tool for binary data files. (Modified by threethan to support all three HD-engine games, as well as array and flag values)
diff --git a/XbTool/BdatEditor/BdatEditor.csproj b/XbTool/BdatEditor/BdatEditor.csproj
index 29fee08..fa0dbd3 100644
--- a/XbTool/BdatEditor/BdatEditor.csproj
+++ b/XbTool/BdatEditor/BdatEditor.csproj
@@ -33,6 +33,9 @@
prompt
4
+
+ Icon.ico
+
@@ -116,5 +119,8 @@
+
+
+
\ No newline at end of file
diff --git a/XbTool/BdatEditor/Icon.ico b/XbTool/BdatEditor/Icon.ico
new file mode 100644
index 0000000..11bedf8
Binary files /dev/null and b/XbTool/BdatEditor/Icon.ico differ
diff --git a/XbTool/BdatEditor/MainWindow.xaml b/XbTool/BdatEditor/MainWindow.xaml
index 2cd7d74..f0b8a61 100644
--- a/XbTool/BdatEditor/MainWindow.xaml
+++ b/XbTool/BdatEditor/MainWindow.xaml
@@ -5,19 +5,12 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
DataContext="{Binding Main, Source={StaticResource Locator}}"
mc:Ignorable="d"
- Title="Xenoblade 2 BDAT Editor" Height="450" Width="800">
+ Title="Xenoblade Games Bdat Editor+" Height="550" Width="1000">
-
-
-
@@ -26,21 +19,37 @@
+
+
+
+
-
+
+
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
+
+
+ ItemsSource="{Binding Path=EditingTable, Mode=TwoWay, IsAsync=True}" Grid.RowSpan="2" Margin="-7,37,0,0"/>
diff --git a/XbTool/BdatEditor/MainWindow.xaml.cs b/XbTool/BdatEditor/MainWindow.xaml.cs
index 2034167..828213a 100644
--- a/XbTool/BdatEditor/MainWindow.xaml.cs
+++ b/XbTool/BdatEditor/MainWindow.xaml.cs
@@ -1,4 +1,7 @@
-namespace BdatEditor
+using BdatEditor.ViewModel;
+using System.Windows.Data;
+
+namespace BdatEditor
{
///
/// Interaction logic for MainWindow.xaml
diff --git a/XbTool/BdatEditor/ViewModel/MainViewModel.cs b/XbTool/BdatEditor/ViewModel/MainViewModel.cs
index b1cf9fa..17f73f3 100644
--- a/XbTool/BdatEditor/ViewModel/MainViewModel.cs
+++ b/XbTool/BdatEditor/ViewModel/MainViewModel.cs
@@ -3,6 +3,7 @@
using System.Data;
using System.IO;
using System.Linq;
+using System.Text;
using System.Windows;
using System.Windows.Input;
using BdatEditor.Bdat;
@@ -19,15 +20,21 @@ public class MainViewModel : ViewModelBase
public ICommand OpenBdatCommand { get; set; }
public ICommand ViewTableCommand { get; set; }
public ICommand SaveTableCommand { get; set; }
+ public ICommand RefreshTableCommand { get; set; }
+
+ public ICommand StringExportCommand { get; set; }
+ public ICommand StringImportCommand { get; set; }
+
private bool IsFileOpened { get; set; }
private bool IsTableOpened { get; set; }
public BdatTables BdatTables { get; private set; }
private string Filename { get; set; }
+ public string FileDisplayName { get; set; } = "Select a file";
public List TableNames { get; private set; }
public int SelectedTable { get; set; }
- private BdatTable CurrentTable { get; set; }
+ public BdatTable CurrentTable { get; set; }
public DataTable EditingTable { get; set; }
public MainViewModel()
@@ -35,12 +42,120 @@ public MainViewModel()
OpenBdatCommand = new RelayCommand(OpenBdatViaFileBrowser);
ViewTableCommand = new RelayCommand(ViewTable);
SaveTableCommand = new RelayCommand(SaveTable);
+ RefreshTableCommand = new RelayCommand(RefreshTable);
+ StringExportCommand = new RelayCommand(StringExport);
+ StringImportCommand = new RelayCommand(StringImport);
+ }
+
+ private void tables_list_DoubleClick(object sender, System.EventArgs e)
+ {
+ ViewTable();
}
+
public void OpenBdatViaFileBrowser()
{
Filename = OpenViaFileBrowser(".bdat", "BDAT Files (*.bdat)|*.bdat|All Files|*.*");
+ if (Filename == null)
+ return;
OpenBdat(Filename);
+ FileDisplayName = Filename.Split('\\')[Filename.Split('\\').Length - 1];
+ CurrentTable = null;
+ EditingTable = new DataTable();
+ EditingTable.AcceptChanges();
+ }
+
+
+ public void StringExport()
+ {
+ var Files = OpenViaFileBrowser(".bdat", "BDAT Files (*.bdat)|*.bdat|All Files|*.*", true);
+ if (Files == null)
+ return;
+
+ foreach (var File in Files) {
+ List Lines = new List();
+ var Info = LoadBdat(File);
+ foreach (var Table in Info.BDAT.Tables) {
+ var CurrentTable = new EditTable(Table);
+ foreach (var Row in CurrentTable.Items)
+ {
+ for (int Col = 1; Col < Row.Length; Col++)
+ {
+ var ColumnValue = Row[Col];
+ var ColumnInfo = CurrentTable.Columns[Col - 1];
+ if (ColumnInfo.ValType != BdatValueType.String)
+ continue;
+
+ Lines.Add(Escape((string)ColumnValue));
+ }
+ }
+ }
+
+ string OutFile = File + ".txt";
+
+ using (StreamWriter Writer = System.IO.File.CreateText(OutFile)) {
+ foreach (var Line in Lines)
+ Writer.WriteLine(Line);
+ }
+ }
+ MessageBox.Show("Exported!");
+ }
+
+ public void StringImport()
+ {
+ var Files = OpenViaFileBrowser(".bdat", "BDAT Files (*.bdat)|*.bdat|All Files|*.*", true);
+ if (Files == null)
+ return;
+
+ bool IgnoreLimit = false;
+
+ foreach (var File in Files)
+ {
+ string TxtFile = File + ".txt";
+ string OutFile = File + ".new";
+
+ if (!System.IO.File.Exists(TxtFile))
+ continue;
+
+ string[] Lines = System.IO.File.ReadAllLines(TxtFile);
+ int i = 0;
+
+ var Info = LoadBdat(File);
+ foreach (var Table in Info.BDAT.Tables)
+ {
+ var CurrentTable = new EditTable(Table);
+ foreach (var Row in CurrentTable.Items)
+ {
+ for (int Col = 1; Col < Row.Length; Col++)
+ {
+ var ColumnValue = Row[Col];
+ var ColumnInfo = CurrentTable.Columns[Col - 1];
+ if (ColumnInfo.ValType != BdatValueType.String)
+ continue;
+
+ var OriLine = Table.ReadValue((int)Row.First(), ColumnInfo.Name);
+ var NewLine = Unescape(Lines[i++]);
+ if (!IgnoreLimit && NewLine.Length > OriLine.Length) {
+ IgnoreLimit = MessageBoxResult.Yes == MessageBox.Show($"The line {i - 1} has more character than the original line. Ignore?", "Error at " + Path.GetFileName(File), MessageBoxButton.YesNo, MessageBoxImage.Error); ;
+ if (!IgnoreLimit)
+ return;
+ }
+ if (NewLine.Length > OriLine.Length) {
+ NewLine = NewLine.Substring(0, OriLine.Length);
+
+ //Remove Possible Open Tags after the cut
+ if (NewLine.LastIndexOf(']') < NewLine.LastIndexOf('['))
+ NewLine = NewLine.Substring(0, NewLine.LastIndexOf('['));
+ }
+
+ Table.WriteValue((int)Row.First(), ColumnInfo.Name, NewLine);
+ }
+ }
+ }
+
+ System.IO.File.WriteAllBytes(OutFile, Info.BDAT.FileData);
+ }
+ MessageBox.Show("Imported!");
}
public void ViewTable()
@@ -48,18 +163,22 @@ public void ViewTable()
if (!IsFileOpened) return;
BdatTable table = BdatTables.Tables[SelectedTable];
- var editTable = new EditTable(table);
- EditingTable = new DataTable();
CurrentTable = table;
+ RefreshTable();
+ }
+ public void RefreshTable() {
+ if (CurrentTable == null) return;
+ var editTable = new EditTable(CurrentTable);
+ EditingTable = new DataTable();
EditingTable.Columns.Add(new DataColumn("ID") { ReadOnly = true });
foreach (BdatMember col in editTable.Columns)
{
var column = new DataColumn();
- if (col.Type != BdatMemberType.Scalar)
+ /*if (col.Type != BdatMemberType.Scalar)
{
column.ReadOnly = true;
- }
+ }*/
column.ColumnName = $"{col.Name}\n({col.ValType})";
EditingTable.Columns.Add(column);
@@ -79,7 +198,6 @@ public void ViewTable()
EditingTable = null;
EditingTable = temp;
}
-
public void SaveTable()
{
if (!IsTableOpened) return;
@@ -93,7 +211,7 @@ public void SaveTable()
int itemId = int.Parse((string)row.ItemArray[0]);
for (int m = 0; m < members.Length; m++)
{
- if (members[m].Type != BdatMemberType.Scalar) continue;
+ //if (members[m].Type != BdatMemberType.Scalar) continue;
string value = (string)row.ItemArray[m + 1];
try
{
@@ -104,7 +222,7 @@ public void SaveTable()
string caption = null;
if (ex is ArgumentOutOfRangeException)
{
- caption = "Replacement string was too large to fit in the original space.";
+ caption = "Error: " + ex.Message;
}
if (ex is FormatException || ex is OverflowException)
@@ -112,10 +230,13 @@ public void SaveTable()
caption = $"Error parsing \"{value}\" as a {members[m].ValType} " +
$"from Item \"{itemId}\" Column \"{members[m].Name}\".";
}
+ else
+ {
- if (caption == null) throw;
+ if (caption == null) throw;
- MessageBox.Show(caption, "Format Error");
+ MessageBox.Show(caption, "Format Error");
+ }
}
}
}
@@ -125,10 +246,104 @@ public void SaveTable()
EditingTable.AcceptChanges();
}
+ public string Escape(string Str) {
+ StringBuilder Result = new StringBuilder();
+ foreach (var Char in Str) {
+ switch (Char) {
+ case '\n':
+ Result.Append("\\n");
+ break;
+ case '\r':
+ Result.Append("\\r");
+ break;
+ case '\\':
+ Result.Append("\\\\");
+ break;
+ default:
+ Result.Append(Char);
+ break;
+ }
+ }
+ return Result.ToString();
+ }
+ public string Unescape(string Str)
+ {
+ StringBuilder Result = new StringBuilder();
+ bool InEscape = false;
+ foreach (var Char in Str)
+ {
+ switch (Char)
+ {
+ case 'n':
+ if (!InEscape)
+ goto default;
+
+ Result.Append("\n");
+ InEscape = false;
+ break;
+ case 'r':
+ if (!InEscape)
+ goto default;
+
+ Result.Append("\r");
+ InEscape = false;
+ break;
+ case '\\':
+ if (InEscape)
+ goto default;
+
+ InEscape = true;
+ break;
+ default:
+ InEscape = false;
+ Result.Append(Char);
+ break;
+ }
+ }
+ return Result.ToString();
+ }
+
+ public (BdatTables BDAT, List Names) LoadBdat(string filename) {
+ BdatTables Tables;
+ List Names;
+ try
+ {
+ Tables = new BdatTables(filename, Game.XB2, false);
+ Names = Tables.Tables.Select(x => x.Name).ToList();
+ }
+ catch
+ {
+ try
+ {
+ Tables = new BdatTables(filename, Game.XBX, false);
+ Names = Tables.Tables.Select(x => x.Name).ToList();
+ }
+ catch
+ {
+ try
+ {
+ Tables = new BdatTables(filename, Game.XB1DE, false);
+ Names = Tables.Tables.Select(x => x.Name).ToList();
+ }
+ catch
+ {
+ return (null, null);
+ }
+ }
+ }
+
+ return (Tables, Names);
+ }
public void OpenBdat(string filename)
{
- BdatTables = new BdatTables(filename, Game.XB2, false);
- TableNames = BdatTables.Tables.Select(x => x.Name).ToList();
+ var Info = LoadBdat(filename);
+ if (Info.BDAT == null) {
+ MessageBox.Show("Failed to open file. Is it a valid bdat?");
+ return;
+ }
+
+ BdatTables = Info.BDAT;
+ TableNames = Info.Names;
IsFileOpened = true;
}
@@ -144,5 +359,18 @@ private static string OpenViaFileBrowser(string extension, string filter)
return openDialog.FileName;
}
+ private static string[] OpenViaFileBrowser(string extension, string filter, bool Multi)
+ {
+ var openDialog = new OpenFileDialog
+ {
+ DefaultExt = extension,
+ Filter = filter,
+ Multiselect = Multi
+ };
+
+ if (openDialog.ShowDialog() != true) return null;
+
+ return openDialog.FileNames;
+ }
}
}
diff --git a/XbTool/XbTool/Bdat/BdatTable.cs b/XbTool/XbTool/Bdat/BdatTable.cs
index 0fb950c..e00d14f 100644
--- a/XbTool/XbTool/Bdat/BdatTable.cs
+++ b/XbTool/XbTool/Bdat/BdatTable.cs
@@ -3,7 +3,11 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Linq.Expressions;
using System.Text;
+using XbTool.Bdat;
+using XbTool.BdatString;
+using XbTool.CodeGen;
using XbTool.Common;
namespace XbTool.Bdat
@@ -74,6 +78,13 @@ public BdatTable(DataBuffer table)
MembersDict = Members.ToDictionary(x => x.Name, x => x);
}
+ private static string ReadFlag(DataBuffer table, int itemOffset, BdatMember member, BdatMember flagsMember)
+ {
+ uint flags = table[itemOffset + flagsMember.MemberPos];
+ return ((flags & member.FlagMask) != 0).ToString();
+ //return (Convert.ToString(flags, 2).PadLeft(8, '0') + " & "+ Convert.ToString(member.FlagMask, 2).PadLeft(8,'0') + " -> " + ((flags & member.FlagMask) != 0).ToString());
+ }
+
public string ReadValue(int itemId, string memberName)
{
BdatMember member = MembersDict[memberName];
@@ -81,8 +92,51 @@ public string ReadValue(int itemId, string memberName)
int itemOffset = ItemTableOffset + itemIndex * ItemSize;
int valueOffset = itemOffset + member.MemberPos;
- if (member.Type == BdatMemberType.Array) return "Array";
- if (member.Type == BdatMemberType.Flag) return "Flag";
+ if (member.Type == BdatMemberType.Array) //return "Array";
+ {
+ String outString = "";
+ //Set data length by type
+ int bytes = GetTypeBytes(member.ValType);
+ //Add each item
+ for (int i = 0; i < member.ArrayCount; i++)
+ {
+ outString = string.Concat(outString, ReadIndividualValue(member, valueOffset + i * bytes).ToString());
+ if (i < member.ArrayCount - 1)
+ outString = string.Concat(outString, ":");
+ }
+ return outString;
+ }
+ if (member.Type == BdatMemberType.Flag)
+ {
+ BdatMember flagsMember = Members[member.FlagVarIndex];
+ return ReadFlag(Data, itemOffset, member, flagsMember);
+ }
+ return ReadIndividualValue(member, valueOffset);
+ }
+ private int GetTypeBytes (BdatValueType type)
+ {
+ //also for arrays
+ switch (type)
+ {
+ case BdatValueType.Int8:
+ case BdatValueType.UInt8:
+ return 1;
+ case BdatValueType.UInt16:
+ case BdatValueType.Int16:
+ return 2;
+ case BdatValueType.Int32:
+ case BdatValueType.UInt32:
+ case BdatValueType.String:
+ case BdatValueType.FP32:
+ return 4;
+ default:
+ return 1;
+ }
+ }
+
+ private string ReadIndividualValue(BdatMember member, int valueOffset)
+ //Custom, only used for arrays.
+ {
switch (member.ValType)
{
case BdatValueType.UInt8:
@@ -113,14 +167,46 @@ public string ReadValue(int itemId, string memberName)
public void WriteValue(int itemId, string memberName, string value)
{
+
BdatMember member = MembersDict[memberName];
int itemIndex = itemId - BaseId;
int itemOffset = ItemTableOffset + itemIndex * ItemSize;
int valueOffset = itemOffset + member.MemberPos;
- if (member.Type != BdatMemberType.Scalar)
- return;
+ if (member.Type == BdatMemberType.Array)
+ {
+ int i = 0;
+ int bytes = GetTypeBytes(member.ValType);
+ string[] array_items = value.Split(':');
+ foreach (String array_item in array_items)
+ {
+ WriteIndividualValue(member, valueOffset + i*bytes, array_item);
+ i++;
+ }
+ }
+ if (member.Type == BdatMemberType.Flag)
+ {
+ if (value == "True" || value == "False")
+ {
+ BdatMember flagsMember = Members[member.FlagVarIndex];
+ uint flags = Data[itemOffset + flagsMember.MemberPos];
+ uint newVal = 0;
+ if (value == "True")
+ newVal = flags | member.FlagMask;
+ else
+ newVal = flags ^ (flags & member.FlagMask);
+ Data.WriteUInt8(Convert.ToByte(newVal), itemOffset + flagsMember.MemberPos);
+ return;
+ }
+ else
+ throw new ArgumentOutOfRangeException(nameof(value), "Flags must be True or False exactly!");
+ }
+
+ WriteIndividualValue(member, valueOffset, value);
+ }
+ private void WriteIndividualValue(BdatMember member, int valueOffset, string value)
+ {
switch (member.ValType)
{
case BdatValueType.UInt8:
@@ -149,7 +235,7 @@ public void WriteValue(int itemId, string memberName, string value)
string oldValue = Data.ReadUTF8Z(offset);
int oldLength = Encoding.UTF8.GetByteCount(oldValue);
int length = Encoding.UTF8.GetByteCount(value);
- if (length > oldLength) throw new ArgumentOutOfRangeException(nameof(value), "String is too long");
+ if (length > oldLength) throw new ArgumentOutOfRangeException(nameof(value), "String is too long! Must be shorter or equal to previous.");
Data.WriteUTF8Z(value, offset);
break;
default: