diff --git a/AssetStudio.Utility/ACL/ACLExtensions.cs b/AssetStudio.Utility/ACL/ACLExtensions.cs index 9feb908..e99ff4b 100644 --- a/AssetStudio.Utility/ACL/ACLExtensions.cs +++ b/AssetStudio.Utility/ACL/ACLExtensions.cs @@ -1,4 +1,5 @@ -using ACLLibs; +using System; +using ACLLibs; namespace AssetStudio { @@ -8,17 +9,23 @@ namespace AssetStudio { if (game.Type.IsSRGroup()) { - SRACL.DecompressAll(m_ACLClip.m_ClipData, out values, out times); + var aclClip = m_ACLClip as MHYACLClip; + SRACL.DecompressAll(aclClip.m_ClipData, out values, out times); } else { - if (!m_ACLClip.m_DatabaseData.IsNullOrEmpty()) + switch (m_ACLClip) { - DBACL.DecompressTracks(m_ACLClip.m_ClipData, m_ACLClip.m_DatabaseData, out values, out times); - } - else - { - ACL.DecompressAll(m_ACLClip.m_ClipData, out values, out times); + case GIACLClip giaclClip: + DBACL.DecompressTracks(giaclClip.m_ClipData, giaclClip.m_DatabaseData, out values, out times); + break; + case MHYACLClip mhyaclClip: + ACL.DecompressAll(mhyaclClip.m_ClipData, out values, out times); + break; + default: + values = Array.Empty(); + times = Array.Empty(); + break; } } } diff --git a/AssetStudio.Utility/ModelConverter.cs b/AssetStudio.Utility/ModelConverter.cs index d3b8896..9d8c3b4 100644 --- a/AssetStudio.Utility/ModelConverter.cs +++ b/AssetStudio.Utility/ModelConverter.cs @@ -718,6 +718,8 @@ namespace AssetStudio dest = 1; else if (Game.Type.IsSRGroup() && texEnv.Key.Contains("Pack")) dest = 0; + else if (Game.Type.IsArknightsEndfield() && texEnv.Key == "_BaseMap") + dest = 0; texture.Dest = dest; @@ -896,15 +898,15 @@ namespace AssetStudio var streamedFrames = m_Clip.m_StreamedClip.ReadData(); var m_ClipBindingConstant = animationClip.m_ClipBindingConstant ?? m_Clip.ConvertValueArrayToGenericBinding(); var m_ACLClip = m_Clip.m_ACLClip; - var aclCount = m_ACLClip.m_CurveCount; - if (!m_ACLClip.m_ClipData.IsNullOrEmpty() && !Game.Type.IsSRGroup()) + var aclCount = m_ACLClip.CurveCount; + if (m_ACLClip.IsSet && !Game.Type.IsSRGroup()) { m_ACLClip.Process(Game, out var values, out var times); for (int frameIndex = 0; frameIndex < times.Length; frameIndex++) { var time = times[frameIndex]; - var frameOffset = frameIndex * m_ACLClip.m_CurveCount; - for (int curveIndex = 0; curveIndex < m_ACLClip.m_CurveCount;) + var frameOffset = frameIndex * m_ACLClip.CurveCount; + for (int curveIndex = 0; curveIndex < m_ACLClip.CurveCount;) { var index = curveIndex; ReadCurveData(iAnim, m_ClipBindingConstant, index, time, values, (int)frameOffset, ref curveIndex); @@ -938,14 +940,14 @@ namespace AssetStudio ReadCurveData(iAnim, m_ClipBindingConstant, (int)index, time, m_DenseClip.m_SampleArray, (int)frameOffset, ref curveIndex); } } - if (!m_ACLClip.m_ClipData.IsNullOrEmpty() && Game.Type.IsSRGroup()) + if (m_ACLClip.IsSet && Game.Type.IsSRGroup()) { m_ACLClip.Process(Game, out var values, out var times); for (int frameIndex = 0; frameIndex < times.Length; frameIndex++) { var time = times[frameIndex]; - var frameOffset = frameIndex * m_ACLClip.m_CurveCount; - for (int curveIndex = 0; curveIndex < m_ACLClip.m_CurveCount;) + var frameOffset = frameIndex * m_ACLClip.CurveCount; + for (int curveIndex = 0; curveIndex < m_ACLClip.CurveCount;) { var index = (int)(curveIndex + m_DenseClip.m_CurveCount + streamCount); ReadCurveData(iAnim, m_ClipBindingConstant, index, time, values, (int)frameOffset, ref curveIndex); diff --git a/AssetStudio.Utility/YAML/AnimationClipConverter.cs b/AssetStudio.Utility/YAML/AnimationClipConverter.cs index 942c0ba..8afee60 100644 --- a/AssetStudio.Utility/YAML/AnimationClipConverter.cs +++ b/AssetStudio.Utility/YAML/AnimationClipConverter.cs @@ -57,7 +57,7 @@ namespace AssetStudio var lastSampleFrame = streamedFrames.Count > 1 ? streamedFrames[streamedFrames.Count - 2].time : 0.0f; var lastFrame = Math.Max(lastDenseFrame, lastSampleFrame); - if (!m_Clip.m_ACLClip.m_ClipData.IsNullOrEmpty() && !game.Type.IsSRGroup()) + if (m_Clip.m_ACLClip.IsSet && !game.Type.IsSRGroup()) { var lastACLFrame = ProcessACLClip(m_Clip, bindings, tos); lastFrame = Math.Max(lastFrame, lastACLFrame); @@ -65,7 +65,7 @@ namespace AssetStudio } ProcessStreams(streamedFrames, bindings, tos, m_Clip.m_DenseClip.m_SampleRate); ProcessDenses(m_Clip, bindings, tos); - if (!m_Clip.m_ACLClip.m_ClipData.IsNullOrEmpty() && game.Type.IsSRGroup()) + if (m_Clip.m_ACLClip.IsSet && game.Type.IsSRGroup()) { var lastACLFrame = ProcessACLClip(m_Clip, bindings, tos); lastFrame = Math.Max(lastFrame, lastACLFrame); @@ -112,7 +112,7 @@ namespace AssetStudio var curve = frame.keyList[curveIndex]; var index = curve.index; if (!game.Type.IsSRGroup()) - index += (int)animationClip.m_MuscleClip.m_Clip.m_ACLClip.m_CurveCount; + index += (int)animationClip.m_MuscleClip.m_Clip.m_ACLClip.CurveCount; var binding = bindings.FindBinding(index); var path = GetCurvePath(tos, binding.path); @@ -161,7 +161,7 @@ namespace AssetStudio { var index = (int)streamCount + curveIndex; if (!game.Type.IsSRGroup()) - index += (int)clip.m_ACLClip.m_CurveCount; + index += (int)clip.m_ACLClip.CurveCount; var binding = bindings.FindBinding(index); var path = GetCurvePath(tos, binding.path); var framePosition = frameOffset + curveIndex; @@ -193,8 +193,8 @@ namespace AssetStudio for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { float time = times[frameIndex]; - int frameOffset = frameIndex * (int)acl.m_CurveCount; - for (int curveIndex = 0; curveIndex < acl.m_CurveCount;) + int frameOffset = frameIndex * (int)acl.CurveCount; + for (int curveIndex = 0; curveIndex < acl.CurveCount;) { var index = curveIndex; if (game.Type.IsSRGroup()) @@ -236,8 +236,8 @@ namespace AssetStudio for (var curveIndex = 0; curveIndex < constant.data.Length;) { var index = (int)(streamCount + denseCount + curveIndex); - if (!clip.m_ACLClip.m_ClipData.IsNullOrEmpty()) - index += (int)clip.m_ACLClip.m_CurveCount; + if (clip.m_ACLClip.IsSet) + index += (int)clip.m_ACLClip.CurveCount; GenericBinding binding = bindings.FindBinding(index); string path = GetCurvePath(tos, binding.path); if (binding.typeID == ClassIDType.Transform) diff --git a/AssetStudio.Utility/YAML/CustomCurveResolver.cs b/AssetStudio.Utility/YAML/CustomCurveResolver.cs index ef398c5..5d3fa66 100644 --- a/AssetStudio.Utility/YAML/CustomCurveResolver.cs +++ b/AssetStudio.Utility/YAML/CustomCurveResolver.cs @@ -634,36 +634,36 @@ namespace AssetStudio public static string FindPropertyNameByCRC28(this Material material, uint crc) { - foreach (var property in material.m_SavedProperties.m_TexEnvs.Keys) + foreach (var property in material.m_SavedProperties.m_TexEnvs) { - string hdrName = $"{property}_HDR"; + string hdrName = $"{property.Key}_HDR"; if (CRC.Verify28DigestUTF8(hdrName, crc)) { return hdrName; } - string stName = $"{property}_ST"; + string stName = $"{property.Key}_ST"; if (CRC.Verify28DigestUTF8(stName, crc)) { return stName; } - string texelName = $"{property}_TexelSize"; + string texelName = $"{property.Key}_TexelSize"; if (CRC.Verify28DigestUTF8(texelName, crc)) { return texelName; } } - foreach (var property in material.m_SavedProperties.m_Floats.Keys) + foreach (var property in material.m_SavedProperties.m_Floats) { - if (CRC.Verify28DigestUTF8(property, crc)) + if (CRC.Verify28DigestUTF8(property.Key, crc)) { - return property; + return property.Key; } } - foreach (var property in material.m_SavedProperties.m_Colors.Keys) + foreach (var property in material.m_SavedProperties.m_Colors) { - if (CRC.Verify28DigestUTF8(property, crc)) + if (CRC.Verify28DigestUTF8(property.Key, crc)) { - return property; + return property.Key; } } return null; diff --git a/AssetStudio/BundleFile.cs b/AssetStudio/BundleFile.cs index 353853a..c59c897 100644 --- a/AssetStudio/BundleFile.cs +++ b/AssetStudio/BundleFile.cs @@ -35,6 +35,7 @@ namespace AssetStudio Lz4HC, Lzham, Lz4Mr0k, + Lz4Inv = 5, Zstd = 5 } @@ -542,8 +543,12 @@ namespace AssetStudio { NetEaseUtils.Decrypt(compressedBytesSpan); } - if (Game.Type.IsOPFP()) + if (Game.Type.IsArknightsEndfield() && i == 0) { + FairGuardUtils.Decrypt(compressedBytesSpan); + } + if (Game.Type.IsOPFP()) + { OPFPUtils.Decrypt(compressedBytesSpan, reader.FullPath); } var uncompressedSize = (int)blockInfo.uncompressedSize; @@ -559,6 +564,26 @@ namespace AssetStudio BigArrayPool.Shared.Return(uncompressedBytes); break; } + case CompressionType.Lz4Inv when Game.Type.IsArknightsEndfield(): + { + var compressedSize = (int)blockInfo.compressedSize; + var compressedBytes = BigArrayPool.Shared.Rent(compressedSize); + reader.Read(compressedBytes, 0, compressedSize); + var compressedBytesSpan = compressedBytes.AsSpan(0, compressedSize); + if (i == 0) + { + FairGuardUtils.Decrypt(compressedBytesSpan); + + } + var uncompressedSize = (int)blockInfo.uncompressedSize; + var uncompressedBytes = BigArrayPool.Shared.Rent(uncompressedSize); + var uncompressedBytesSpan = uncompressedBytes.AsSpan(0, uncompressedSize); + FairGuardUtils.Lz4.Decompress(compressedBytesSpan, uncompressedBytesSpan); + blocksStream.Write(uncompressedBytes, 0, uncompressedSize); + BigArrayPool.Shared.Return(compressedBytes); + BigArrayPool.Shared.Return(uncompressedBytes); + break; + } case CompressionType.Zstd when !Game.Type.IsMhyGroup(): //Zstd { var compressedSize = (int)blockInfo.compressedSize; diff --git a/AssetStudio/Classes/AnimationClip.cs b/AssetStudio/Classes/AnimationClip.cs index f2e14d9..b31d15b 100644 --- a/AssetStudio/Classes/AnimationClip.cs +++ b/AssetStudio/Classes/AnimationClip.cs @@ -783,22 +783,71 @@ namespace AssetStudio } } - public class ACLClip + public abstract class ACLClip { - public byte[] m_ClipData; - public byte[] m_DatabaseData; + public virtual bool IsSet => false; + public virtual uint CurveCount => 0; + public abstract void Read(ObjectReader reader); + } + public class EmptyACLClip : ACLClip + { + public override void Read(ObjectReader reader) { } + } + + public class ArkACLClip : ACLClip + { + public int m_ACLType; + public byte[] m_ACLArray; + public float m_PositionFactor; + public float m_EulerFactor; + public float m_ScaleFactor; + public float m_FloatFactor; + public float m_nPositionCurves; + public float m_nRotationCurves; + public float m_nEulerCurves; + public float m_nScaleCurves; + + public override bool IsSet => !m_ACLArray.IsNullOrEmpty(); + + public ArkACLClip() + { + m_ACLArray = Array.Empty(); + } + + public override void Read(ObjectReader reader) + { + m_ACLType = reader.ReadInt32(); + m_ACLArray = reader.ReadUInt8Array(); + reader.AlignStream(); + m_PositionFactor = reader.ReadSingle(); + m_EulerFactor = reader.ReadSingle(); + m_ScaleFactor = reader.ReadSingle(); + m_FloatFactor = reader.ReadSingle(); + m_nPositionCurves = reader.ReadSingle(); + m_nRotationCurves = reader.ReadSingle(); + m_nEulerCurves = reader.ReadSingle(); + m_nScaleCurves = reader.ReadSingle(); + } + } + + public class MHYACLClip : ACLClip + { public uint m_CurveCount; public uint m_ConstCurveCount; - - public ACLClip() + + public byte[] m_ClipData; + + public override bool IsSet => !m_ClipData.IsNullOrEmpty(); + public override uint CurveCount => m_CurveCount; + + public MHYACLClip() { m_CurveCount = 0; m_ConstCurveCount = 0; m_ClipData = Array.Empty(); - m_DatabaseData = Array.Empty(); } - public void Read(ObjectReader reader) + public override void Read(ObjectReader reader) { var byteCount = reader.ReadInt32(); @@ -817,7 +866,28 @@ namespace AssetStudio m_ConstCurveCount = reader.ReadUInt32(); } } - public void ParseGI(ObjectReader reader) + } + + public class GIACLClip : ACLClip + { + public uint m_CurveCount; + public uint m_ConstCurveCount; + + public byte[] m_ClipData; + public byte[] m_DatabaseData; + + public override bool IsSet => !m_ClipData.IsNullOrEmpty() && !m_DatabaseData.IsNullOrEmpty(); + public override uint CurveCount => m_CurveCount; + + public GIACLClip() + { + m_CurveCount = 0; + m_ConstCurveCount = 0; + m_ClipData = Array.Empty(); + m_DatabaseData = Array.Empty(); + } + + public override void Read(ObjectReader reader) { var aclTracksCount = (int)reader.ReadUInt64(); var aclTracksOffset = reader.Position + reader.ReadInt64(); @@ -1099,7 +1169,7 @@ namespace AssetStudio public class Clip { - public ACLClip m_ACLClip = new(); + public ACLClip m_ACLClip = new EmptyACLClip(); public StreamedClip m_StreamedClip; public DenseClip m_DenseClip; public ConstantClip m_ConstantClip; @@ -1113,6 +1183,12 @@ namespace AssetStudio m_DenseClip = new DenseClip(reader); if (reader.Game.Type.IsSRGroup()) { + m_ACLClip = new MHYACLClip(); + m_ACLClip.Read(reader); + } + if (reader.Game.Type.IsArknightsEndfield()) + { + m_ACLClip = new ArkACLClip(); m_ACLClip.Read(reader); } if (version[0] > 4 || (version[0] == 4 && version[1] >= 3)) //4.3 and up @@ -1121,6 +1197,7 @@ namespace AssetStudio } if (reader.Game.Type.IsGIGroup() || reader.Game.Type.IsBH3Group() || reader.Game.Type.IsZZZCB1()) { + m_ACLClip = new MHYACLClip(); m_ACLClip.Read(reader); } if (version[0] < 2018 || (version[0] == 2018 && version[1] < 3)) //2018.3 down @@ -1143,7 +1220,8 @@ namespace AssetStudio clip.m_StreamedClip = StreamedClip.ParseGI(reader); clip.m_DenseClip = DenseClip.ParseGI(reader); clip.m_ConstantClip = ConstantClip.ParseGI(reader); - clip.m_ACLClip.ParseGI(reader); + clip.m_ACLClip = new GIACLClip(); + clip.m_ACLClip.Read(reader); reader.Position = pos; @@ -1683,6 +1761,10 @@ namespace AssetStudio m_SampleRate = reader.ReadSingle(); m_WrapMode = reader.ReadInt32(); + if (reader.Game.Type.IsArknightsEndfield()) + { + var m_aclType = reader.ReadInt32(); + } if (version[0] > 3 || (version[0] == 3 && version[1] >= 4)) //3.4 and up { m_Bounds = new AABB(reader); @@ -1746,14 +1828,16 @@ namespace AssetStudio m_StreamData = new StreamingInfo(reader); if (!string.IsNullOrEmpty(m_StreamData?.path)) { + var aclClip = m_MuscleClip.m_Clip.m_ACLClip as GIACLClip; + var resourceReader = new ResourceReader(m_StreamData.path, assetsFile, m_StreamData.offset, m_StreamData.size); var ms = new MemoryStream(); - ms.Write(m_MuscleClip.m_Clip.m_ACLClip.m_DatabaseData); + ms.Write(aclClip.m_DatabaseData); ms.Write(resourceReader.GetData()); ms.AlignStream(); - m_MuscleClip.m_Clip.m_ACLClip.m_DatabaseData = ms.ToArray(); + aclClip.m_DatabaseData = ms.ToArray(); } } } diff --git a/AssetStudio/Classes/AnimatorController.cs b/AssetStudio/Classes/AnimatorController.cs index 296456f..719e57f 100644 --- a/AssetStudio/Classes/AnimatorController.cs +++ b/AssetStudio/Classes/AnimatorController.cs @@ -291,6 +291,10 @@ namespace AssetStudio || (version[0] == 4 && version[1] == 1 && version[2] >= 3)) //4.1.3 and up { m_CycleOffset = reader.ReadSingle(); + if (reader.Game.Type.IsArknightsEndfield()) + { + var m_StateNameHash = reader.ReadUInt32(); + } m_Mirror = reader.ReadBoolean(); reader.AlignStream(); } @@ -411,6 +415,12 @@ namespace AssetStudio m_Mirror = reader.ReadBoolean(); } + if (reader.Game.Type.IsArknightsEndfield()) + { + var m_SyncGroupID = reader.ReadUInt32(); + var m_SyncGroupRole = reader.ReadUInt32(); + } + reader.AlignStream(); } } diff --git a/AssetStudio/Classes/Material.cs b/AssetStudio/Classes/Material.cs index 5d94fc6..124a24d 100644 --- a/AssetStudio/Classes/Material.cs +++ b/AssetStudio/Classes/Material.cs @@ -13,49 +13,53 @@ namespace AssetStudio m_Texture = new PPtr(reader); m_Scale = reader.ReadVector2(); m_Offset = reader.ReadVector2(); + if (reader.Game.Type.IsArknightsEndfield()) + { + var m_UVSetIndex = reader.ReadInt32(); + } } } public class UnityPropertySheet { - public Dictionary m_TexEnvs; - public Dictionary m_Ints; - public Dictionary m_Floats; - public Dictionary m_Colors; + public KeyValuePair[] m_TexEnvs; + public KeyValuePair[] m_Ints; + public KeyValuePair[] m_Floats; + public KeyValuePair[] m_Colors; public UnityPropertySheet(ObjectReader reader) { var version = reader.version; int m_TexEnvsSize = reader.ReadInt32(); - m_TexEnvs = new Dictionary(m_TexEnvsSize); + m_TexEnvs = new KeyValuePair[m_TexEnvsSize]; for (int i = 0; i < m_TexEnvsSize; i++) { - m_TexEnvs.Add(reader.ReadAlignedString(), new UnityTexEnv(reader)); + m_TexEnvs[i] = new(reader.ReadAlignedString(), new UnityTexEnv(reader)); } if (version[0] >= 2021) //2021.1 and up { int m_IntsSize = reader.ReadInt32(); - m_Ints = new Dictionary(m_IntsSize); + m_Ints = new KeyValuePair[m_IntsSize]; for (int i = 0; i < m_IntsSize; i++) { - m_Ints.Add(reader.ReadAlignedString(), reader.ReadInt32()); + m_Ints[i] = new(reader.ReadAlignedString(), reader.ReadInt32()); } } int m_FloatsSize = reader.ReadInt32(); - m_Floats = new Dictionary(m_FloatsSize); + m_Floats = new KeyValuePair[m_FloatsSize]; for (int i = 0; i < m_FloatsSize; i++) { - m_Floats.Add(reader.ReadAlignedString(), reader.ReadSingle()); + m_Floats[i] = new(reader.ReadAlignedString(), reader.ReadSingle()); } int m_ColorsSize = reader.ReadInt32(); - m_Colors = new Dictionary(m_ColorsSize); + m_Colors = new KeyValuePair[m_ColorsSize]; for (int i = 0; i < m_ColorsSize; i++) { - m_Colors.Add(reader.ReadAlignedString(), reader.ReadColor4()); + m_Colors[i] = new(reader.ReadAlignedString(), reader.ReadColor4()); } } } diff --git a/AssetStudio/Classes/Mesh.cs b/AssetStudio/Classes/Mesh.cs index ec3a34e..451a034 100644 --- a/AssetStudio/Classes/Mesh.cs +++ b/AssetStudio/Classes/Mesh.cs @@ -471,6 +471,7 @@ namespace AssetStudio private VertexData m_VertexData; private CompressedMesh m_CompressedMesh; private StreamingInfo m_StreamData; + private bool m_CollisionMeshBaked = false; public List m_Indices = new List(); @@ -549,6 +550,12 @@ namespace AssetStudio } var m_KeepVertices = reader.ReadBoolean(); var m_KeepIndices = reader.ReadBoolean(); + if (reader.Game.Type.IsArknightsEndfield()) + { + var m_CollisionMeshOnly = reader.ReadBoolean(); + m_CollisionMeshBaked = reader.ReadBoolean(); + var m_CollisionMeshConvex = reader.ReadBoolean(); + } } reader.AlignStream(); if (reader.Game.Type.IsGISubGroup()) @@ -641,7 +648,7 @@ namespace AssetStudio m_VertexData = new VertexData(reader); } - if (version[0] > 2 || (version[0] == 2 && version[1] >= 6)) //2.6.0 and later + if ((version[0] > 2 || (version[0] == 2 && version[1] >= 6)) && !m_CollisionMeshBaked) //2.6.0 and later { m_CompressedMesh = new CompressedMesh(reader); } @@ -696,6 +703,10 @@ namespace AssetStudio var m_MeshMetrics = new float[2]; m_MeshMetrics[0] = reader.ReadSingle(); m_MeshMetrics[1] = reader.ReadSingle(); + if (reader.Game.Type.IsArknightsEndfield()) + { + var m_MeshMetrics2 = reader.ReadSingle(); + } } if (reader.Game.Type.IsGIGroup()) @@ -735,7 +746,12 @@ namespace AssetStudio ReadVertexData(); } - if (version[0] > 2 || (version[0] == 2 && version[1] >= 6)) //2.6.0 and later + if (m_CollisionMeshBaked) + { + return; + } + + if ((version[0] > 2 || (version[0] == 2 && version[1] >= 6))) //2.6.0 and later { DecompressCompressedMesh(); } diff --git a/AssetStudio/Classes/Renderer.cs b/AssetStudio/Classes/Renderer.cs index 484d682..3f0ce2e 100644 --- a/AssetStudio/Classes/Renderer.cs +++ b/AssetStudio/Classes/Renderer.cs @@ -98,6 +98,12 @@ namespace AssetStudio if (version[0] >= 2021) //2021.1 and up { var m_StaticShadowCaster = reader.ReadByte(); + if (reader.Game.Type.IsArknightsEndfield()) + { + var m_RealtimeShadowCaster = reader.ReadByte(); + var m_SubMeshRenderMode = reader.ReadByte(); + var m_CharacterIndex = reader.ReadByte(); + } } var m_MotionVectors = reader.ReadByte(); var m_LightProbeUsage = reader.ReadByte(); diff --git a/AssetStudio/Crypto/FairGuardUtils.cs b/AssetStudio/Crypto/FairGuardUtils.cs new file mode 100644 index 0000000..b8fcbbe --- /dev/null +++ b/AssetStudio/Crypto/FairGuardUtils.cs @@ -0,0 +1,305 @@ +using System; +using System.Runtime.InteropServices; + +namespace AssetStudio +{ + //Special thanks to LukeFZ#4035. + public static class FairGuardUtils + { + public static void Decrypt(Span bytes) + { + Logger.Verbose($"Attempting to decrypt block with FairGuard encryption..."); + + var encryptedOffset = 0; + var encryptedSize = bytes.Length > 0x500 ? 0x500 : bytes.Length; + + if (encryptedSize < 0x20) + { + Logger.Verbose($"block size is less that minimum, skipping..."); + return; + } + + var encrypted = bytes.Slice(encryptedOffset, encryptedSize); + var encryptedInts = MemoryMarshal.Cast(encrypted); + + for (int i = 0; i < 0x20; i++) + { + encrypted[i] ^= 0xA6; + } + + var seedPart0 = (uint)(encryptedInts[2] ^ 0x1274CBEC ^ encryptedInts[6] ^ 0x3F72EAF3); + var seedPart1 = (uint)(encryptedInts[3] ^ 0xBE482704 ^ encryptedInts[0] ^ encryptedSize); + var seedPart2 = (uint)(encryptedInts[1] ^ encryptedSize ^ encryptedInts[5] ^ 0x753BDCAA); + var seedPart3 = (uint)(encryptedInts[0] ^ 0x82C57E3C ^ encryptedInts[7] ^ 0xE3D947D3); + var seedPart4 = (uint)(encryptedInts[4] ^ 0x6F2A7347 ^ encryptedInts[7] ^ 0x4736C714); + + var seedInts = new uint[] { seedPart0, seedPart1, seedPart2, seedPart3, seedPart4 }; + var seedBytes = MemoryMarshal.AsBytes(seedInts); + + var seed = GenerateSeed(seedBytes); + var seedBuffer = BitConverter.GetBytes(seed); + seed = CRC.CalculateDigest(seedBuffer, 0, (uint)seedBuffer.Length); + + var key = seedInts[0] ^ seedInts[1] ^ seedInts[2] ^ seedInts[3] ^ seedInts[4] ^ (uint)encryptedSize; + + RC4(seedBytes, key); + var keySeed = CRC.CalculateDigest(seedBytes.ToArray(), 0, (uint)seedBytes.Length); + var keySeedBytes = BitConverter.GetBytes(keySeed); + keySeed = GenerateSeed(keySeedBytes); + + var keyPart0 = (seedInts[3] - 0x1C26B82D) ^ keySeed; + var keyPart1 = (seedInts[2] + 0x3F72EAF3) ^ seed; + var keyPart2 = seedInts[0] ^ 0x82C57E3C ^ keySeed; + var keyPart3 = (seedInts[1] + 0x6F2A7347) ^ seed; + var keyVector = new uint[] { keyPart0, keyPart1, keyPart2, keyPart3 }; + + var block = encrypted[0x20..]; + if (block.Length >= 0x80) + { + RC4(block[..0x60], seed); + for (int i = 0; i < 0x60; i++) + { + block[i] ^= (byte)(seed ^ 0x6E); + } + + block = block[0x60..]; + var blockSize = (encryptedSize - 0x80) / 4; + for (int i = 0; i < 4; i++) + { + var blockOffset = i * blockSize; + var blockKey = i switch + { + 0 => 0x6142756Eu, + 1 => 0x62496E66u, + 2 => 0x1304B000u, + 3 => 0x6E8E30ECu, + _ => throw new NotImplementedException() + }; + RC4(block.Slice(blockOffset, blockSize), seed); + var blockInts = MemoryMarshal.Cast(block[blockOffset..]); + for (int j = 0; j < blockSize / 4; j++) + { + blockInts[j] ^= seed ^ keyVector[i] ^ blockKey; + } + } + } + else + { + RC4(block, seed); + } + } + + private static uint GenerateSeed(Span bytes) + { + var state = new uint[] { 0xC1646153, 0x78DA0550, 0x2947E56B }; + for (int i = 0; i < bytes.Length; i++) + { + state[0] = 0x21 * state[0] + bytes[i]; + if ((state[0] & 0xF) >= 0xB) + { + state[0] = (state[0] ^ RotateIsSet(state[2], 6)) - 0x2CD86315; + } + else if ((state[0] & 0xF0) >> 4 > 0xE) + { + state[0] = (state[1] ^ 0xAB4A010B) + (state[0] ^ RotateIsSet(state[2], 9)); + } + else if ((state[0] & 0xF00) >> 8 < 2) + { + state[1] = ((state[2] >> 3) - 0x55EEAB7B) ^ state[0]; + } + else if (state[1] + 0x567A >= 0xAB5489E4) + { + state[1] = (state[1] >> 16) ^ state[0]; + } + else if ((state[1] ^ 0x738766FA) <= state[2]) + { + state[1] = (state[1] >> 8) ^ state[2]; + } + else if (state[1] == 0x68F53AA6) + { + if ((state[1] ^ (state[0] + state[2])) > 0x594AF86E) + { + state[1] -= 0x8CA292E; + } + else + { + state[2] -= 0x760A1649; + } + } + else + { + if (state[0] > 0x865703AF) + { + state[1] = state[2] ^ (state[0] - 0x564389D7); + } + else + { + state[1] = (state[1] - 0x12B9DD92) ^ state[0]; + } + + state[0] ^= RotateIsSet(state[1], 8); + } + } + + return state[0]; + } + + private static uint RotateIsSet(uint value, int count) => (((value >> count) != 0) || ((value << (32 - count))) != 0) ? 1u : 0u; + + public class CRC + { + private static readonly uint[] Table; + + static CRC() + { + Table = new uint[256]; + const uint kPoly = 0xD35E417E; + for (uint i = 0; i < 256; i++) + { + uint r = i; + for (int j = 0; j < 8; j++) + { + if ((r & 1) != 0) + r = (r >> 1) ^ kPoly; + else + r >>= 1; + } + Table[i] = r; + } + } + + uint _value = 0xFFFFFFFF; + + public void Update(byte[] data, uint offset, uint size) + { + for (uint i = 0; i < size; i++) + _value = (Table[(byte)_value ^ data[offset + i]] ^ (_value >> 9)) + 0x5B; + } + + public uint GetDigest() { return ~_value - 0x41607A3D; } + + public static uint CalculateDigest(byte[] data, uint offset, uint size) + { + var crc = new CRC(); + crc.Update(data, offset, size); + return crc.GetDigest(); + } + } + + public static void RC4(Span data, uint key) => RC4(data, BitConverter.GetBytes(key)); + + public static void RC4(Span data, byte[] key) + { + int[] S = new int[0x100]; + for (int _ = 0; _ < 0x100; _++) + { + S[_] = _; + } + + int[] T = new int[0x100]; + + if (key.Length == 0x100) + { + Buffer.BlockCopy(key, 0, T, 0, key.Length); + } + else + { + for (int _ = 0; _ < 0x100; _++) + { + T[_] = key[_ % key.Length]; + } + } + + int i = 0; + int j = 0; + for (i = 0; i < 0x100; i++) + { + j = (j + S[i] + T[i]) % 0x100; + + (S[j], S[i]) = (S[i], S[j]); + } + + i = j = 0; + for (int iteration = 0; iteration < data.Length; iteration++) + { + i = (i + 1) % 0x100; + j = (j + S[i]) % 0x100; + + (S[j], S[i]) = (S[i], S[j]); + var K = (uint)S[(S[j] + S[i]) % 0x100]; + + var k = (byte)(K << 1) | (K >> 7); + data[iteration] ^= (byte)(k - 0x61); + } + } + + public static class Lz4 + { + public static void Decompress(ReadOnlySpan cmp, Span dec) + { + int cmpPos = 0; + int decPos = 0; + + // ReSharper disable once VariableHidesOuterVariable + int GetLength(int length, ReadOnlySpan cmp) + { + byte sum; + + if (length == 0xf) + { + do + { + length += sum = cmp[cmpPos++]; + } while (sum == 0xff); + } + + return length; + } + + do + { + byte token = cmp[cmpPos++]; + + int encCount = (token >> 4) & 0xf; + int litCount = (token >> 0) & 0xf; + + //Copy literal chunk + litCount = GetLength(litCount, cmp); + + cmp.Slice(cmpPos, litCount).CopyTo(dec.Slice(decPos)); + + cmpPos += litCount; + decPos += litCount; + + if (cmpPos >= cmp.Length) + { + break; + } + + //Copy compressed chunk + int back = cmp[cmpPos++] << 8 | + cmp[cmpPos++] << 0; + + encCount = GetLength(encCount, cmp) + 4; + + int encPos = decPos - back; + + if (encCount <= back) + { + dec.Slice(encPos, encCount).CopyTo(dec.Slice(decPos)); + + decPos += encCount; + } + else + { + while (encCount-- > 0) + { + dec[decPos++] = dec[encPos++]; + } + } + } while (cmpPos < cmp.Length && + decPos < dec.Length); + } + } + } +} \ No newline at end of file diff --git a/AssetStudio/GameManager.cs b/AssetStudio/GameManager.cs index 1ef2214..6826c03 100644 --- a/AssetStudio/GameManager.cs +++ b/AssetStudio/GameManager.cs @@ -41,6 +41,7 @@ namespace AssetStudio Games.Add(index++, new Game(GameType.CodenameJump)); Games.Add(index++, new Game(GameType.GirlsFrontline)); Games.Add(index++, new Game(GameType.Reverse1999)); + Games.Add(index++, new Game(GameType.ArknightsEndfield)); } public static Game GetGame(GameType gameType) => GetGame((int)gameType); public static Game GetGame(int index) @@ -152,7 +153,8 @@ namespace AssetStudio ProjectSekai, CodenameJump, GirlsFrontline, - Reverse1999 + Reverse1999, + ArknightsEndfield } public static class GameTypes @@ -174,6 +176,7 @@ namespace AssetStudio public static bool IsNaraka(this GameType type) => type == GameType.Naraka; public static bool IsOPFP(this GameType type) => type == GameType.OPFP; public static bool IsNetEase(this GameType type) => type == GameType.NetEase; + public static bool IsArknightsEndfield(this GameType type) => type == GameType.ArknightsEndfield; public static bool IsGIGroup(this GameType type) => type switch { GameType.GI or GameType.GI_Pack or GameType.GI_CB1 or GameType.GI_CB2 or GameType.GI_CB3 or GameType.GI_CB3Pre => true, @@ -200,7 +203,7 @@ namespace AssetStudio public static bool IsBlockFile(this GameType type) => type switch { - GameType.BH3 or GameType.BH3Pre or GameType.SR or GameType.GI_Pack or GameType.TOT => true, + GameType.BH3 or GameType.BH3Pre or GameType.SR or GameType.GI_Pack or GameType.TOT or GameType.ArknightsEndfield => true, _ => false, };