diff --git a/hactoolnet/Program.cs b/hactoolnet/Program.cs
index 67a87da8..23e8384d 100644
--- a/hactoolnet/Program.cs
+++ b/hactoolnet/Program.cs
@@ -214,38 +214,34 @@ namespace hactoolnet
{
using (var file = new FileStream(ctx.Options.InFile, FileMode.Open, FileAccess.Read))
{
- var save = new Savefile(file);
+ var save = new Savefile(file, ctx.Logger);
var layout = save.Header.Layout;
- save.FileRemap.Position = layout.DuplexL1OffsetB;
- using (var outFile = new FileStream("0_DuplexL1A", FileMode.Create, FileAccess.Write))
- {
- save.FileRemap.CopyStream(outFile, layout.DuplexDataSize);
- }
+ File.WriteAllBytes("d0_JournalTable", save.JournalTable);
+ File.WriteAllBytes("d1_JournalBitmapUpdatedPhysical", save.JournalBitmapUpdatedPhysical);
+ File.WriteAllBytes("d2_JournalBitmapUpdatedVirtual", save.JournalBitmapUpdatedVirtual);
+ File.WriteAllBytes("d3_JournalBitmapUnassigned", save.JournalBitmapUnassigned);
+ File.WriteAllBytes("d4_Layer1Hash", save.JournalLayer1Hash);
+ File.WriteAllBytes("d5_Layer2Hash", save.JournalLayer2Hash);
+ File.WriteAllBytes("d6_Layer3Hash", save.JournalLayer3Hash);
+ File.WriteAllBytes("d7_Stuff", save.JournalStuff);
- save.FileRemap.Position = layout.DuplexL1OffsetB;
- using (var outFile = new FileStream("1_DuplexL1B", FileMode.Create, FileAccess.Write))
- {
- save.FileRemap.CopyStream(outFile, layout.DuplexDataSize);
- }
-
- save.FileRemap.Position = layout.DuplexDataOffsetA;
- using (var outFile = new FileStream("2_DuplexDataA", FileMode.Create, FileAccess.Write))
- {
- save.FileRemap.CopyStream(outFile, layout.DuplexDataSize);
- }
-
- save.FileRemap.Position = layout.DuplexDataOffsetB;
- using (var outFile = new FileStream("3_DuplexDataB", FileMode.Create, FileAccess.Write))
- {
- save.FileRemap.CopyStream(outFile, layout.DuplexDataSize);
- }
+ File.WriteAllBytes("0_DuplexL1A", save.DuplexL1A);
+ File.WriteAllBytes("1_DuplexL1B", save.DuplexL1B);
+ File.WriteAllBytes("2_DuplexDataA", save.DuplexDataA);
+ File.WriteAllBytes("3_DuplexDataB", save.DuplexDataB);
save.FileRemap.Position = layout.JournalDataOffset;
using (var outFile = new FileStream("4_JournalData", FileMode.Create, FileAccess.Write))
{
save.FileRemap.CopyStream(outFile, layout.JournalDataSizeB + layout.SizeReservedArea);
}
+
+ save.JournalStream.Position = 0;
+ using (var outFile = new FileStream("j0_Data", FileMode.Create, FileAccess.Write))
+ {
+ save.JournalStream.CopyStream(outFile, save.JournalStream.Length);
+ }
}
}
diff --git a/libhac/BitReader.cs b/libhac/BitReader.cs
new file mode 100644
index 00000000..9f0a12a2
--- /dev/null
+++ b/libhac/BitReader.cs
@@ -0,0 +1,133 @@
+using System;
+using System.Diagnostics;
+
+namespace libhac
+{
+ public class BitReader
+ {
+ public byte[] Buffer { get; private set; }
+ public int LengthBits { get; private set; }
+ public int Position { get; set; }
+ public int Remaining => LengthBits - Position;
+
+ public BitReader(byte[] buffer) => SetBuffer(buffer);
+
+ public void SetBuffer(byte[] buffer)
+ {
+ Buffer = buffer;
+ LengthBits = Buffer?.Length * 8 ?? 0;
+ Position = 0;
+ }
+
+ public int ReadInt(int bitCount)
+ {
+ int value = PeekInt(bitCount);
+ Position += bitCount;
+ return value;
+ }
+
+ //public int ReadSignedInt(int bitCount)
+ //{
+ // int value = PeekInt(bitCount);
+ // Position += bitCount;
+ // return Bit.SignExtend32(value, bitCount);
+ //}
+
+ public bool ReadBool() => ReadInt(1) == 1;
+
+ public int ReadOffsetBinary(int bitCount, OffsetBias bias)
+ {
+ int offset = (1 << (bitCount - 1)) - (int)bias;
+ int value = PeekInt(bitCount) - offset;
+ Position += bitCount;
+ return value;
+ }
+
+ //public void AlignPosition(int multiple)
+ //{
+ // Position = Helpers.GetNextMultiple(Position, multiple);
+ //}
+
+ public int PeekInt(int bitCount)
+ {
+ Debug.Assert(bitCount >= 0 && bitCount <= 32);
+
+ if (bitCount > Remaining)
+ {
+ if (Position >= LengthBits) return 0;
+
+ int extraBits = bitCount - Remaining;
+ return PeekIntFallback(Remaining) << extraBits;
+ }
+
+ int byteIndex = Position / 8;
+ int bitIndex = Position % 8;
+
+ if (bitCount <= 9 && Remaining >= 16)
+ {
+ int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1];
+ value &= 0xFFFF >> bitIndex;
+ value >>= 16 - bitCount - bitIndex;
+ return value;
+ }
+
+ if (bitCount <= 17 && Remaining >= 24)
+ {
+ int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2];
+ value &= 0xFFFFFF >> bitIndex;
+ value >>= 24 - bitCount - bitIndex;
+ return value;
+ }
+
+ if (bitCount <= 25 && Remaining >= 32)
+ {
+ int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3];
+ value &= (int)(0xFFFFFFFF >> bitIndex);
+ value >>= 32 - bitCount - bitIndex;
+ return value;
+ }
+ return PeekIntFallback(bitCount);
+ }
+
+ private int PeekIntFallback(int bitCount)
+ {
+ int value = 0;
+ int byteIndex = Position / 8;
+ int bitIndex = Position % 8;
+
+ while (bitCount > 0)
+ {
+ if (bitIndex >= 8)
+ {
+ bitIndex = 0;
+ byteIndex++;
+ }
+
+ int bitsToRead = Math.Min(bitCount, 8 - bitIndex);
+ int mask = 0xFF >> bitIndex;
+ int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead);
+
+ value = (value << bitsToRead) | currentByte;
+ bitIndex += bitsToRead;
+ bitCount -= bitsToRead;
+ }
+ return value;
+ }
+
+ ///
+ /// Specifies the bias of an offset binary value. A positive bias can represent one more
+ /// positive value than negative value, and a negative bias can represent one more
+ /// negative value than positive value.
+ ///
+ /// Example:
+ /// A 4-bit offset binary value with a positive bias can store
+ /// the values 8 through -7 inclusive.
+ /// A 4-bit offset binary value with a positive bias can store
+ /// the values 7 through -8 inclusive.
+ public enum OffsetBias
+ {
+ Positive = 1,
+ Negative = 0
+ }
+ }
+}
diff --git a/libhac/Savefile/Header.cs b/libhac/Savefile/Header.cs
index 6b79c096..703c4637 100644
--- a/libhac/Savefile/Header.cs
+++ b/libhac/Savefile/Header.cs
@@ -7,6 +7,7 @@ namespace libhac.Savefile
{
public byte[] Cmac { get; set; }
public FsLayout Layout { get; set; }
+ public JournalHeader Journal{ get; set; }
public RemapHeader FileRemap { get; set; }
public RemapHeader MetaRemap { get; set; }
@@ -27,6 +28,9 @@ namespace libhac.Savefile
reader.BaseStream.Position = 0x100;
Layout = new FsLayout(reader);
+ reader.BaseStream.Position = 0x408;
+ Journal = new JournalHeader(reader);
+
reader.BaseStream.Position = 0x650;
FileRemap = new RemapHeader(reader);
reader.BaseStream.Position = 0x690;
@@ -87,8 +91,8 @@ namespace libhac.Savefile
public long MasterHashOffset0 { get; set; }
public long MasterHashOffset1 { get; set; }
public long MasterHashSize { get; set; }
- public long OffsetJournalTable { get; set; }
- public long SizeJournalTable { get; set; }
+ public long JournalTableOffset { get; set; }
+ public long JournalTableSize { get; set; }
public long JournalBitmapUpdatedPhysicalOffset { get; set; }
public long JournalBitmapUpdatedPhysicalSize { get; set; }
public long JournalBitmapUpdatedVirtualOffset { get; set; }
@@ -132,8 +136,8 @@ namespace libhac.Savefile
MasterHashOffset0 = reader.ReadInt64();
MasterHashOffset1 = reader.ReadInt64();
MasterHashSize = reader.ReadInt64();
- OffsetJournalTable = reader.ReadInt64();
- SizeJournalTable = reader.ReadInt64();
+ JournalTableOffset = reader.ReadInt64();
+ JournalTableSize = reader.ReadInt64();
JournalBitmapUpdatedPhysicalOffset = reader.ReadInt64();
JournalBitmapUpdatedPhysicalSize = reader.ReadInt64();
JournalBitmapUpdatedVirtualOffset = reader.ReadInt64();
@@ -170,6 +174,32 @@ namespace libhac.Savefile
}
}
+ public class JournalHeader
+ {
+ public string Magic { get; }
+ public uint MagicNum { get; }
+ public long Field8 { get; }
+ public long Field10 { get; }
+ public long BlockSize { get; }
+ public int Field20 { get; }
+ public int MappingEntryCount { get; }
+ public int Field28 { get; }
+ public int Field2C { get; }
+
+ public JournalHeader(BinaryReader reader)
+ {
+ Magic = reader.ReadAscii(4);
+ MagicNum = reader.ReadUInt32();
+ Field8 = reader.ReadInt64();
+ Field10 = reader.ReadInt64();
+ BlockSize = reader.ReadInt64();
+ Field20 = reader.ReadInt32();
+ MappingEntryCount = reader.ReadInt32();
+ Field28 = reader.ReadInt32();
+ Field2C = reader.ReadInt32();
+ }
+ }
+
public class MapEntry
{
public long VirtualOffset { get; }
diff --git a/libhac/Savefile/Journal.cs b/libhac/Savefile/Journal.cs
new file mode 100644
index 00000000..a68359e7
--- /dev/null
+++ b/libhac/Savefile/Journal.cs
@@ -0,0 +1,120 @@
+using System;
+using System.IO;
+
+namespace libhac.Savefile
+{
+ public class JournalStream : Stream
+ {
+ private long _position;
+ private Stream BaseStream { get; }
+ public MappingEntry[] Map { get; }
+ public int BlockSize { get; }
+ private MappingEntry CurrentMapEntry { get; set; }
+
+ public JournalStream(Stream baseStream, MappingEntry[] map, int blockSize)
+ {
+ BaseStream = baseStream;
+ Map = map;
+ BlockSize = blockSize;
+ Length = map.Length * BlockSize;
+ }
+
+ public override int Read(byte[] buffer, int offset, int count)
+ {
+ long remaining = Length - Position;
+ if (remaining <= 0) return 0;
+ if (remaining < count) count = (int)remaining;
+
+ var toOutput = count;
+ int outPos = offset;
+
+ while (toOutput > 0)
+ {
+ var remainInEntry = BlockSize - Position % BlockSize;
+ int toRead = (int)Math.Min(toOutput, remainInEntry);
+ BaseStream.Read(buffer, outPos, toRead);
+
+ outPos += toRead;
+ toOutput -= toRead;
+ Position += toRead;
+ }
+
+ return count;
+ }
+
+ public override long Seek(long offset, SeekOrigin origin)
+ {
+ switch (origin)
+ {
+ case SeekOrigin.Begin:
+ Position = offset;
+ break;
+ case SeekOrigin.Current:
+ Position += offset;
+ break;
+ case SeekOrigin.End:
+ Position = Length - offset;
+ break;
+ }
+
+ return Position;
+ }
+
+ public override void SetLength(long value) => throw new NotSupportedException();
+ public override void Write(byte[] buffer, int offset, int count) => throw new NotSupportedException();
+ public override void Flush() => throw new NotSupportedException();
+ public override bool CanRead => true;
+ public override bool CanSeek => true;
+ public override bool CanWrite => false;
+ public override long Length { get; }
+ public override long Position
+ {
+ get => _position;
+ set
+ {
+ _position = value;
+ if (value >= Length) return;
+ var currentBlock = value / BlockSize;
+ var blockPos = value % BlockSize;
+ CurrentMapEntry = Map[currentBlock];
+ BaseStream.Position = CurrentMapEntry.PhysicalIndex * BlockSize + blockPos;
+ }
+ }
+
+ public static MappingEntry[] ReadMappingEntries(byte[] mapTable, byte[] bitmapUpdatedPhysical,
+ byte[] bitmapUpdatedVirtual, byte[] bitmapUnassigned, int count)
+ {
+ var physicalBits = new BitReader(bitmapUpdatedPhysical);
+ var virtualBits = new BitReader(bitmapUpdatedVirtual);
+ var unassignedBits = new BitReader(bitmapUnassigned);
+ var tableReader = new BinaryReader(new MemoryStream(mapTable));
+ var map = new MappingEntry[count];
+
+ for (int i = 0; i < count; i++)
+ {
+ var entry = new MappingEntry
+ {
+ VirtualIndex = i,
+ PhysicalIndex = tableReader.ReadInt32() & 0x7FFFFFFF,
+ //UpdatedPhysical = physicalBits.ReadBool(),
+ //UpdatedVirtual = virtualBits.ReadBool(),
+ //Unassigned = unassignedBits.ReadBool()
+ };
+
+ map[i] = entry;
+ tableReader.BaseStream.Position += 4;
+ }
+
+ return map;
+ }
+ }
+
+ public class MappingEntry
+ {
+ public int PhysicalIndex { get; set; }
+ public int VirtualIndex { get; set; }
+ public bool UpdatedPhysical { get; set; }
+ public bool UpdatedVirtual { get; set; }
+ public bool Unassigned { get; set; }
+ }
+}
diff --git a/libhac/Savefile/Savefile.cs b/libhac/Savefile/Savefile.cs
index 5fb656a6..59da839a 100644
--- a/libhac/Savefile/Savefile.cs
+++ b/libhac/Savefile/Savefile.cs
@@ -7,15 +7,85 @@ namespace libhac.Savefile
{
public Header Header { get; }
public RemapStream FileRemap { get; }
+ public RemapStream MetaRemap { get; }
+ private Stream FileStream { get; }
+ public JournalStream JournalStream { get; }
- public Savefile(Stream file)
+ public byte[] DuplexL1A { get; }
+ public byte[] DuplexL1B { get; }
+ public byte[] DuplexDataA { get; }
+ public byte[] DuplexDataB { get; }
+
+ public byte[] JournalTable { get; }
+ public byte[] JournalBitmapUpdatedPhysical { get; }
+ public byte[] JournalBitmapUpdatedVirtual { get; }
+ public byte[] JournalBitmapUnassigned { get; }
+ public byte[] JournalLayer1Hash { get; }
+ public byte[] JournalLayer2Hash { get; }
+ public byte[] JournalLayer3Hash { get; }
+ public byte[] JournalStuff { get; }
+
+ public Savefile(Stream file, IProgressReport logger = null)
{
+ FileStream = file;
using (var reader = new BinaryReader(file, Encoding.Default, true))
{
- Header = new Header(reader);
+ Header = new Header(reader, logger);
+ var layout = Header.Layout;
FileRemap = new RemapStream(
- new SubStream(file, Header.Layout.FileMapDataOffset, Header.Layout.FileMapDataSize),
+ new SubStream(file, layout.FileMapDataOffset, layout.FileMapDataSize),
Header.FileMapEntries, Header.FileRemap.MapSegmentCount);
+
+ DuplexL1A = new byte[layout.DuplexL1Size];
+ DuplexL1B = new byte[layout.DuplexL1Size];
+ DuplexDataA = new byte[layout.DuplexDataSize];
+ DuplexDataB = new byte[layout.DuplexDataSize];
+
+ FileRemap.Position = layout.DuplexL1OffsetA;
+ FileRemap.Read(DuplexL1A, 0, DuplexL1A.Length);
+ FileRemap.Position = layout.DuplexL1OffsetB;
+ FileRemap.Read(DuplexL1B, 0, DuplexL1B.Length);
+ FileRemap.Position = layout.DuplexDataOffsetA;
+ FileRemap.Read(DuplexDataA, 0, DuplexDataA.Length);
+ FileRemap.Position = layout.DuplexDataOffsetB;
+ FileRemap.Read(DuplexDataB, 0, DuplexDataB.Length);
+
+ var duplexData = new SubStream(FileRemap, layout.DuplexDataOffsetB, layout.DuplexDataSize);
+ MetaRemap = new RemapStream(duplexData, Header.MetaMapEntries, Header.MetaRemap.MapSegmentCount);
+
+ JournalTable = new byte[layout.JournalTableSize];
+ JournalBitmapUpdatedPhysical = new byte[layout.JournalBitmapUpdatedPhysicalSize];
+ JournalBitmapUpdatedVirtual = new byte[layout.JournalBitmapUpdatedVirtualSize];
+ JournalBitmapUnassigned = new byte[layout.JournalBitmapUnassignedSize];
+ JournalLayer1Hash = new byte[layout.Layer1HashSize];
+ JournalLayer2Hash = new byte[layout.Layer2HashSize];
+ JournalLayer3Hash = new byte[layout.Layer3HashSize];
+ JournalStuff = new byte[layout.Field150];
+
+ MetaRemap.Position = layout.JournalTableOffset;
+ MetaRemap.Read(JournalTable, 0, JournalTable.Length);
+ MetaRemap.Position = layout.JournalBitmapUpdatedPhysicalOffset;
+ MetaRemap.Read(JournalBitmapUpdatedPhysical, 0, JournalBitmapUpdatedPhysical.Length);
+ MetaRemap.Position = layout.JournalBitmapUpdatedVirtualOffset;
+ MetaRemap.Read(JournalBitmapUpdatedVirtual, 0, JournalBitmapUpdatedVirtual.Length);
+ MetaRemap.Position = layout.JournalBitmapUnassignedOffset;
+ MetaRemap.Read(JournalBitmapUnassigned, 0, JournalBitmapUnassigned.Length);
+ MetaRemap.Position = layout.Layer1HashOffset;
+ MetaRemap.Read(JournalLayer1Hash, 0, JournalLayer1Hash.Length);
+ MetaRemap.Position = layout.Layer2HashOffset;
+ MetaRemap.Read(JournalLayer2Hash, 0, JournalLayer2Hash.Length);
+ MetaRemap.Position = layout.Layer3HashOffset;
+ MetaRemap.Read(JournalLayer3Hash, 0, JournalLayer3Hash.Length);
+ MetaRemap.Position = layout.Field148;
+ MetaRemap.Read(JournalStuff, 0, JournalStuff.Length);
+
+ var journalMap = JournalStream.ReadMappingEntries(JournalTable, JournalBitmapUpdatedPhysical,
+ JournalBitmapUpdatedVirtual, JournalBitmapUnassigned, Header.Journal.MappingEntryCount);
+
+ var journalData = new SubStream(FileRemap, layout.JournalDataOffset,
+ layout.JournalDataSizeB + layout.SizeReservedArea);
+ JournalStream = new JournalStream(journalData, journalMap, (int) Header.Journal.BlockSize)
+ ;
}
}
}