Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
64854c2
Acroarts Format (Read)
Dec 5, 2025
f84ab67
Improved reading API and preliminary writing support (WIP)
hyperbx Mar 14, 2026
d21c4b0
Implement MaterialColorGoal
hyperbx Mar 15, 2026
ce6d96d
Fix ParticlePlay string alignment
hyperbx Mar 15, 2026
5a78463
Fix PlaceFanShaped writing
hyperbx Mar 15, 2026
8863c53
Add warnings for unknown fields
hyperbx Mar 15, 2026
12afcaa
Fix ABDA and ABRS chunk length writing
hyperbx Mar 15, 2026
6685c6b
Fix offsets
hyperbx Mar 15, 2026
8d21e9e
Fix chunk alignment and sizing
hyperbx Mar 15, 2026
e7a1910
Implement Subtitle
hyperbx Mar 15, 2026
8826c40
Implement DirectionalLight
hyperbx Mar 15, 2026
002306a
Implement ShadowOn
hyperbx Mar 15, 2026
5c433b5
Implement SoundPlay and Sound3DPlay
hyperbx Mar 15, 2026
f6dbfb8
DataChunk/ResourceChunk: inherit List<T>
hyperbx Mar 16, 2026
0ffdd21
ResourcePathChunk: use wildcard expression for getting FourCC from ex…
hyperbx Mar 16, 2026
3ccd364
Identified various unknown fields
hyperbx Mar 16, 2026
0d29296
Clean-up
hyperbx Mar 16, 2026
9a3f92f
Fix MotionSet reading and writing
hyperbx Mar 16, 2026
a99598e
Implement FilterColorCorrection
hyperbx Mar 16, 2026
8ba83eb
Implement TranslateNormal
hyperbx Mar 16, 2026
805a719
Implement reader/writer-level offset origin, fix string reading and w…
hyperbx Mar 19, 2026
dc09c2e
FixedString -> MomentumString
hyperbx Mar 19, 2026
1e49b2a
Branch: implement coord node name
hyperbx Mar 19, 2026
7a91a0d
Implement TranslateAccel
hyperbx Mar 19, 2026
f0a6f75
Implement ScaleRandomNormal
hyperbx Mar 19, 2026
628bc8d
Implement ScaleGoal
hyperbx Mar 19, 2026
c8f9816
TranslateNormal/TranslateRandomAdd: rename Position to Vector
hyperbx Mar 19, 2026
6cea0d0
Implement DetachCoordinate
hyperbx Mar 19, 2026
fcc93d4
Implement TranslateAdd
hyperbx Mar 19, 2026
b113134
Implement TranslateRandomNormal
hyperbx Mar 19, 2026
33d30a8
Implement ClipPlane
hyperbx Mar 19, 2026
f40e2a5
Implement BlurBelt
hyperbx Mar 19, 2026
1790412
Implement CellSpriteSceneSet
hyperbx Mar 19, 2026
e1100ee
Implement ModelJoin
hyperbx Mar 19, 2026
974a6b4
Implement PointLight
hyperbx Mar 19, 2026
bb45f89
Implement TranslateGoal
hyperbx Mar 19, 2026
dd40789
Implement RotateGoal
hyperbx Mar 19, 2026
ce93b30
Implement AmbientLight
hyperbx Mar 19, 2026
9a2b359
Implement TranslateRandomSin
hyperbx Mar 19, 2026
e3a00ef
Implement MaterialColorSin
hyperbx Mar 19, 2026
1721b50
Implement MaterialColorRandomNormal
hyperbx Mar 19, 2026
3637511
Implement MaterialColorRandomGoal
hyperbx Mar 19, 2026
b9ad331
Implement PlaceLineShaped
hyperbx Mar 19, 2026
70f5f0c
Implement ScaleAddGoal
hyperbx Mar 19, 2026
f69fa33
Implement ScaleRandomGoal
hyperbx Mar 19, 2026
237a309
Leaf: store unknown fields
hyperbx Mar 19, 2026
f3007d8
ResourcePathChunk: add warning for undetermined resource IDs
hyperbx Mar 19, 2026
0f31a87
CellSpriteSceneSet: mapped SceneMotionName field
hyperbx Mar 19, 2026
d047050
MomentumString: check for null string pointer
hyperbx Mar 19, 2026
4d98ed9
Leaf: fix ModelAttachIndex writing
hyperbx Mar 19, 2026
408082d
DataChunk: match erroneous chunk alignment
hyperbx Mar 19, 2026
fc684bc
RelocationTableChunk: fix table length
hyperbx Mar 19, 2026
1c31918
ResourceChunk: reference ResourcePathChunk directly in ResourceChunkP…
hyperbx Mar 19, 2026
80c0572
AckResource: match erroneous lack of BINA signature
hyperbx Mar 19, 2026
2ba0838
Implement ParticleBillboardPV
hyperbx Mar 20, 2026
a0a74cd
Rename "Sets" in Goal types to "Steps"
hyperbx Mar 20, 2026
0b8244f
ParticleBillboardPV: mapped Angle
hyperbx Mar 20, 2026
caa6a74
ParticleBillboardPV: rename unknown fields
hyperbx Mar 20, 2026
aa9aee0
Leaf: use Vector2 for primitive min/max
hyperbx Mar 20, 2026
30b274f
English (Simplified)
hyperbx Mar 21, 2026
d3b58d2
Added Rectangle type for Leaf Primitive
hyperbx Mar 21, 2026
1dc49da
Implement SparklingTail
hyperbx Mar 21, 2026
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
20 changes: 20 additions & 0 deletions Marathon.Tests/Formats/Acroarts/AckResourceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
using Marathon.Formats.Acroarts;
using Marathon.Tests.Helpers;

namespace Marathon.Tests.Formats.Acroarts
{
internal class AckResourceTests : ITest
{
private Func<bool>[] _tests = [BinaryIdenticalTest];

private static bool BinaryIdenticalTest()
{
return TestHelper.CheckAllBinaries<AckResource>("*.mab");
}

public bool Run()
{
return TestHelper.RunSubTests(_tests);
}
}
}
126 changes: 126 additions & 0 deletions Marathon/Formats/Acroarts/AckResource.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
using Amicitia.IO.Streams;
using Marathon.Formats.Acroarts.Chunks;
using Marathon.IO;
using Marathon.IO.Extensions;
using Marathon.IO.Types.BINA;
using Marathon.IO.Types.FileSystem;
using System.IO;

// Format names: Acroarts Resource
// Format references: Sonicteam::Spanverse::AckResource
// Format designers: Sonic Team
// Format researchers: Hyper, Rei-san

namespace Marathon.Formats.Acroarts
{
/// <summary>
/// Support for *.mab files; used for Acroarts data.
/// </summary>
public class AckResource : FileBase
{
private const string _extension = ".mab"; // "Merged Acroarts Binary" (speculatory)
private const string _signature = "MRAB"; // "MeRged Acroarts Binary" (speculatory)

private bool _binaHeaderHasSignature = true;

public const uint Version = 2006020901; // 2006 February 9th, Revision 1

public DataChunk Data { get; set; }

public ResourceChunk Resources { get; set; }

public override string Extension => _extension;

public AckResource() { }

public AckResource(string in_path) : base(in_path) { }

public AckResource(Stream in_stream) : base(in_stream) { }

public AckResource(IFile in_file) : base(in_file) { }

public override void Read(Stream in_stream)
{
var mrabReader = new BinaryObjectReaderEx(in_stream, StreamOwnership.Retain, Endianness);

mrabReader.CheckSignature(_signature);

var binaOffset = mrabReader.Read<uint>();
var binaLength = mrabReader.Read<uint>();

mrabReader.JumpAhead(4); // Reserved.

var binaReader = new BINAReader(in_stream, binaOffset);

Endianness = binaReader.Endianness;

_binaHeaderHasSignature = binaReader.Header.HasSignature;

var abdaOffset = binaReader.Read<uint>();
var abrsOffset = binaReader.Read<uint>();

if (abdaOffset != 0)
{
binaReader.JumpTo(binaReader.CalculateOffset(abdaOffset));
Data = new DataChunk(binaReader);
}

if (abrsOffset != 0)
{
binaReader.JumpTo(binaReader.CalculateOffset(abrsOffset));
Resources = new ResourceChunk(binaReader);
}
}

public override void Write(Stream in_stream)
{
var mrabWriter = new BinaryObjectWriterEx(in_stream, StreamOwnership.Retain, Endianness);

mrabWriter.WriteSignature(_signature);

var binaOffset = mrabWriter.Reserve<uint>(true);
var binaLength = mrabWriter.Reserve<uint>(true);

mrabWriter.WriteZero<int>(); // Reserved.
mrabWriter.WriteReserved(binaOffset, (uint)mrabWriter.Position);

var binaWriter = new BINAWriter(in_stream, mrabWriter.Position, mrabWriter.Endianness);
{
binaWriter.Header.HasSignature = _binaHeaderHasSignature;
}

var abdaOffset = binaWriter.Reserve<uint>(true);
var abrsOffset = binaWriter.Reserve<uint>(true);

binaWriter.WriteZero<byte>(0x18); // Reserved.

var abdaWriter = new BinaryObjectWriterEx(in_stream, StreamOwnership.Retain, binaWriter.Endianness);

abdaWriter.JumpTo(binaWriter.Position);

if (Data != null)
{
binaWriter.WriteReserved(abdaOffset, (uint)(binaWriter.Position - abdaOffset));
Data.Write(abdaWriter);
}

var abrsWriter = new BinaryObjectWriterEx(in_stream, StreamOwnership.Retain, binaWriter.Endianness);

abrsWriter.JumpTo(binaWriter.Position);

if (Resources != null)
{
binaWriter.WriteReserved(abrsOffset, (uint)(binaWriter.Position - abrsOffset) + sizeof(uint));
Resources.Write(abrsWriter);
}

// BINA is exclusively used for these.
binaWriter.AddOffset(abdaOffset);
binaWriter.AddOffset(abrsOffset);

binaWriter.FinishWrite();

mrabWriter.WriteReserved(binaLength, binaWriter.Header.Length);
}
}
}
111 changes: 111 additions & 0 deletions Marathon/Formats/Acroarts/Chunks/ChunkHeader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
using Amicitia.IO;
using Amicitia.IO.Binary;
using Marathon.IO;
using Marathon.IO.Types;

namespace Marathon.Formats.Acroarts.Chunks
{
public class ChunkHeader : IBinarySerializable
{
public const uint DefaultHeaderSize = 16;

private long _lengthOffset;
private long _chunkStart;
private long _headerSizeOffset;
private long _systemFlagsOffset;
private long _userFlagsOffset;

public FourCC ID { get; set; }

public uint Length { get; set; }

public uint HeaderSize { get; set; } = DefaultHeaderSize;

public ushort SystemFlags { get; set; }

public ushort UserFlags { get; set; }

public ChunkHeader() { }

public ChunkHeader(FourCC in_id, uint in_length, uint in_headerSize, ushort in_systemFlags, ushort in_userFlags)
{
ID = in_id;
Length = in_length;
HeaderSize = in_headerSize;
SystemFlags = in_systemFlags;
UserFlags = in_userFlags;
}

public ChunkHeader(string in_id, uint in_length, uint in_headerSize, ushort in_systemFlags, ushort in_userFlags)
: this(new FourCC(in_id), in_length, in_headerSize, in_systemFlags, in_userFlags) { }

public ChunkHeader(BinaryObjectWriterEx in_writer, FourCC in_id)
{
ID = in_id;

Reserve(in_writer);
}

public ChunkHeader(BinaryObjectWriterEx in_writer, string in_id)
: this(in_writer, new FourCC(in_id)) { }

public void Read(BinaryObjectReader in_reader)
{
ID = in_reader.ReadObject<FourCC>();
Length = in_reader.Read<uint>();
HeaderSize = in_reader.Read<uint>();
SystemFlags = in_reader.Read<ushort>();
UserFlags = in_reader.Read<ushort>();
}

public void Write(BinaryObjectWriter in_writer)
{
in_writer.WriteObject(ID);
in_writer.Write(Length);
in_writer.Write(HeaderSize);
in_writer.Write(SystemFlags);
in_writer.Write(UserFlags);
}

public void Reserve(BinaryObjectWriterEx in_writer)
{
in_writer.WriteObject(ID);
_lengthOffset = in_writer.Reserve<uint>(true);
_headerSizeOffset = in_writer.Reserve<uint>(true);
_systemFlagsOffset = in_writer.Reserve<ushort>(true);
_userFlagsOffset = in_writer.Reserve<ushort>(true);
_chunkStart = (uint)in_writer.Position;
}

public void FinishWrite(BinaryObjectWriterEx in_writer)
{
in_writer.WriteReserved(_lengthOffset, (uint)AlignmentHelper.Align(in_writer.Position - _chunkStart, 16));
in_writer.WriteReserved(_headerSizeOffset, HeaderSize);
in_writer.WriteReserved(_systemFlagsOffset, SystemFlags);
in_writer.WriteReserved(_userFlagsOffset, UserFlags);
}

public void FinishWrite(BinaryObjectWriterEx in_writer, uint in_length, uint in_headerSize, ushort in_systemFlags, ushort in_userFlags)
{
in_writer.WriteReserved(_lengthOffset, in_length);
in_writer.WriteReserved(_headerSizeOffset, in_headerSize);
in_writer.WriteReserved(_systemFlagsOffset, in_systemFlags);
in_writer.WriteReserved(_userFlagsOffset, in_userFlags);
}

public void FinishWrite(BinaryObjectWriterEx in_writer, uint in_length, uint in_headerSize)
{
FinishWrite(in_writer, in_length, in_headerSize, SystemFlags, UserFlags);
}

public void FinishWrite(BinaryObjectWriterEx in_writer, uint in_length)
{
FinishWrite(in_writer, in_length, HeaderSize, SystemFlags, UserFlags);
}

public long GetChunkStart()
{
return _chunkStart;
}
}
}
131 changes: 131 additions & 0 deletions Marathon/Formats/Acroarts/Chunks/DataChunk.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
using Amicitia.IO.Binary;
using Marathon.Exceptions;
using Marathon.IO;
using Marathon.IO.Extensions;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;

namespace Marathon.Formats.Acroarts.Chunks
{
public class DataChunk : List<TrunkChunkParam>, IBinarySerializableEx
{
private bool _isChunkAligned = false;

public const string ID = "ABDA"; // "Acroarts Binary DAta"

[JsonIgnore]
public RelocationTableChunk RelocationTableChunk { get; set; }

public DataChunk() { }

public DataChunk(BinaryObjectReaderEx in_reader)
{
Read(in_reader);
}

public void Read(BinaryObjectReaderEx in_reader)
{
in_reader.PushOffsetOrigin(in_reader.Position);

var chunkHeader = in_reader.ReadObject<ChunkHeader>();

if (!chunkHeader.ID.Equals(ID))
throw new InvalidSignatureException(ID, chunkHeader.ID);

var version = in_reader.Read<uint>();

if (version != AckResource.Version)
throw new InvalidSignatureException(AckResource.Version, version);

var trunkCount = in_reader.Read<uint>();
var relocTableOffset = in_reader.Read<uint>();

in_reader.JumpAhead(4); // Reserved.

for (uint i = 0; i < trunkCount; i++)
{
var chunkOffset = in_reader.Read<uint>();
var param = in_reader.Read<uint>();

in_reader.ReadAtOffset(in_reader.CalculateOffset(chunkOffset), () =>
{
Add(new(new TrunkChunk(in_reader), param));
});
}

in_reader.JumpTo(in_reader.CalculateOffset(relocTableOffset));

RelocationTableChunk = new RelocationTableChunk(in_reader);
new EndOfChunk().Read(in_reader);

_isChunkAligned = in_reader.ReadArray<byte>(0x10).Sum(x => x) == 0;

in_reader.PopOffsetOrigin();
}

public void Write(BinaryObjectWriterEx in_writer)
{
in_writer.PushOffsetOrigin(in_writer.Position);

var chunkHeader = new ChunkHeader(in_writer, ID)
{
HeaderSize = 0x30
};

in_writer.Write(AckResource.Version);
in_writer.Write(Count);

var relocTableOffset = in_writer.Reserve<uint>(true);

in_writer.WriteZero<int>(); // Reserved.

if (Count <= 0)
{
// Empty chunk array.
in_writer.WriteZero<long>();
in_writer.Align(16);
}
else
{
var trunkOffsets = new List<long>();

foreach (var trunk in this)
{
trunkOffsets.Add(in_writer.Reserve<uint>());
in_writer.Write(trunk.Parameter);
}

in_writer.Align(16);

chunkHeader.HeaderSize = (uint)(in_writer.Position - in_writer.OffsetOrigin);

for (int i = 0; i < Count; i++)
{
in_writer.WriteReserved(trunkOffsets[i], (uint)in_writer.CalculateOffset(in_writer.Position, OffsetType.Relative), false);
this[i].Trunk.Write(in_writer);
}
}

in_writer.Align(16);
in_writer.WriteReserved(relocTableOffset, (uint)in_writer.CalculateOffset(in_writer.Position, OffsetType.Relative));

new RelocationTableChunk().Write(in_writer);

chunkHeader.FinishWrite(in_writer, (uint)(in_writer.Position - in_writer.OffsetOrigin - chunkHeader.HeaderSize));

new EndOfChunk().Write(in_writer);

if (_isChunkAligned)
in_writer.WriteZero<byte>(0x10);

in_writer.PopOffsetOrigin();
}
}

public struct TrunkChunkParam(TrunkChunk in_trunk, uint in_parameter)
{
public TrunkChunk Trunk = in_trunk;
public uint Parameter = in_parameter;
}
}
Loading