AssetRipper_AssetRipper/Source/AssetRipper.AssemblyDumper/Passes/Pass007_ExtractSubclasses.SubclassCandidate.cs
2025-09-17 16:40:02 -07:00

371 lines
12 KiB
C#

using AssetRipper.AssemblyDumper.Utils;
using AssetRipper.Numerics;
using System.Diagnostics;
namespace AssetRipper.AssemblyDumper.Passes;
internal static partial class Pass007_ExtractSubclasses
{
private readonly struct SubclassCandidate
{
public readonly UniversalNode? ReleaseNode;
public readonly UniversalNode? EditorNode;
public readonly string Name;
public readonly DiscontinuousRange<UnityVersion> VersionRange;
public readonly UniversalNode[] NodesToBeAltered;
public SubclassCandidate(UniversalNode? releaseNode, UniversalNode? editorNode, Range<UnityVersion> versionRange) : this()
{
if (releaseNode is null && editorNode is null)
{
throw new Exception("Release and editor can't both be null");
}
ReleaseNode = releaseNode;
EditorNode = editorNode;
VersionRange = versionRange;
Name = releaseNode?.TypeName ?? editorNode?.TypeName ?? throw new Exception("All type names were null");
if (releaseNode is null)
{
Debug.Assert(editorNode is not null);
NodesToBeAltered = [editorNode];
}
else if (editorNode is null)
{
NodesToBeAltered = [releaseNode];
}
else
{
NodesToBeAltered = [releaseNode, editorNode];
}
}
private SubclassCandidate(UniversalNode? releaseNode, UniversalNode? editorNode, string name, DiscontinuousRange<UnityVersion> versionRange, UniversalNode[] nodesToBeAltered)
{
if (releaseNode is null && editorNode is null)
{
throw new ArgumentException("Release and editor can't both be null");
}
ReleaseNode = releaseNode;
EditorNode = editorNode;
Name = name;
VersionRange = versionRange;
NodesToBeAltered = nodesToBeAltered;
}
public bool Contains(SubclassCandidate candidate)
{
return Name == candidate.Name &&
VersionRange.Contains(candidate.VersionRange) &&
(candidate.ReleaseNode is null || UniversalNodeComparer.Equals(ReleaseNode, candidate.ReleaseNode, true)) &&
(candidate.EditorNode is null || UniversalNodeComparer.Equals(EditorNode, candidate.EditorNode, true));
}
public bool CanMerge(SubclassCandidate candidate)
{
return Name == candidate.Name &&
UniversalNodeComparer.Equals(ReleaseNode, candidate.ReleaseNode, true) &&
UniversalNodeComparer.Equals(EditorNode, candidate.EditorNode, true);
}
public bool CanMergeRelaxed(SubclassCandidate candidate)
{
return Name == candidate.Name &&
(candidate.ReleaseNode is null || ReleaseNode is null || UniversalNodeComparer.Equals(ReleaseNode, candidate.ReleaseNode, true)) &&
(candidate.EditorNode is null || EditorNode is null || UniversalNodeComparer.Equals(EditorNode, candidate.EditorNode, true));
}
public SubclassCandidate Merge(SubclassCandidate candidate)
{
UniversalNode[] nodes = new UniversalNode[NodesToBeAltered.Length + candidate.NodesToBeAltered.Length];
Array.Copy(NodesToBeAltered, nodes, NodesToBeAltered.Length);
Array.Copy(candidate.NodesToBeAltered, 0, nodes, NodesToBeAltered.Length, candidate.NodesToBeAltered.Length);
DiscontinuousRange<UnityVersion> range = VersionRange.Union(candidate.VersionRange);
return new SubclassCandidate(ReleaseNode ?? candidate.ReleaseNode, EditorNode ?? candidate.EditorNode, Name, range, nodes);
}
internal UniversalClass ToUniversalClass()
{
return new UniversalClass(ReleaseNode?.ShallowCloneAsRootNode(), EditorNode?.ShallowCloneAsRootNode());
}
public override string ToString()
{
return $"{Name} ({VersionRange})";
}
}
private static void AddClassesToSharedStateSubclasses(List<SubclassCandidate> unprocessedList)
{
List<SubclassCandidate> consolidatedCandidates = ProcessList(unprocessedList);
if (consolidatedCandidates.Count == 0)
{
throw new Exception("Candidate count can't be zero");
}
else if (consolidatedCandidates.Count == 1)
{
//Single class, don't change the name
SubclassCandidate candidate = consolidatedCandidates[0];
VersionedList<UniversalClass> classList = new();
SharedState.Instance.SubclassInformation.Add(candidate.Name, classList);
DiscontinuousRange<UnityVersion> versionRange = candidate.VersionRange;
classList.Add(versionRange[0].Start, candidate.ToUniversalClass());
UnityVersion end = versionRange[versionRange.Count - 1].End;
if (end != UnityVersion.MaxVersion)
{
classList.Add(end, null);
}
}
else if (AnyIntersections(consolidatedCandidates))
{
Console.WriteLine($"Using conflict naming for {consolidatedCandidates[0].Name}");
//Use _2 naming
for (int i = 0; i < consolidatedCandidates.Count; i++)
{
SubclassCandidate candidate = consolidatedCandidates[i];
string typeName = $"{candidate.Name}_{i}";
VersionedList<UniversalClass> classList = new();
SharedState.Instance.SubclassInformation.Add(typeName, classList);
foreach (UniversalNode node in candidate.NodesToBeAltered)
{
node.TypeName = typeName;
}
DiscontinuousRange<UnityVersion> versionRange = candidate.VersionRange;
classList.Add(versionRange[0].Start, candidate.ToUniversalClass());
UnityVersion end = versionRange[versionRange.Count - 1].End;
if (end != UnityVersion.MaxVersion)
{
classList.Add(end, null);
}
else if (candidate.Name.StartsWith("PPtr"))
{
Console.WriteLine($"{candidate.Name} has unresolved conflicts and has no end version");
}
}
}
else
{
//Use _3_4_0f5 naming
VersionedList<UniversalClass> classList = new();
SharedState.Instance.SubclassInformation.Add(consolidatedCandidates[0].Name, classList);
HashSet<UnityVersion> startSet = new();
HashSet<UnityVersion> combinedSet = new();
foreach (SubclassCandidate candidate in consolidatedCandidates)
{
foreach (UnityVersionRange range in candidate.VersionRange)
{
startSet.Add(range.Start);
combinedSet.Add(range.Start);
combinedSet.Add(range.End);
}
}
List<(UnityVersion, SubclassCandidate?)> orderedVersions = new(combinedSet.Count);
foreach (UnityVersion version in combinedSet.Order())
{
if (startSet.Contains(version))
{
SubclassCandidate? candidate = consolidatedCandidates.First(c => c.VersionRange.Contains(version));
orderedVersions.Add((version, candidate));
}
else if (version != UnityVersion.MaxVersion)
{
orderedVersions.Add((version, null));
}
}
SubclassCandidate? previousCandidate = default;//Always not null
for (int i = 0; i < orderedVersions.Count; i++)
{
(UnityVersion version, SubclassCandidate? candidate) = orderedVersions[i];
bool shouldAdd;
if (i == 0)
{
//This is the first candidate.
Debug.Assert(previousCandidate is null);
shouldAdd = true;
previousCandidate = candidate;
}
else if (candidate is null)
{
Debug.Assert(previousCandidate is not null);
if (i < orderedVersions.Count - 1)
{
//This is null and not the end. Therefore, it's between two not null candidates.
//If those candidates are the same, we don't add this.
//The array is used for an efficient reference equality comparison.
shouldAdd = orderedVersions[i + 1].Item2?.NodesToBeAltered != previousCandidate?.NodesToBeAltered;
}
else
{
//This is the last version.
shouldAdd = true;
}
}
else
{
//We only add if this candidate is different from the previous one.
//The array is used for an efficient reference equality comparison.
Debug.Assert(previousCandidate is not null);
shouldAdd = candidate?.NodesToBeAltered != previousCandidate?.NodesToBeAltered;
previousCandidate = candidate;
}
if (shouldAdd)
{
classList.Add(version, candidate?.ToUniversalClass());
}
}
}
}
private static bool AnyIntersections(List<SubclassCandidate> consolidatedCandidates)
{
for (int i = 0; i < consolidatedCandidates.Count; i++)
{
for (int j = i + 1; j < consolidatedCandidates.Count; j++)
{
if (consolidatedCandidates[i].VersionRange.Intersects(consolidatedCandidates[j].VersionRange))
{
Console.WriteLine($"{consolidatedCandidates[i].VersionRange} intersects with {consolidatedCandidates[j].VersionRange}");
return true;
}
}
}
return false;
}
private static List<SubclassCandidate> ProcessList(List<SubclassCandidate> unprocessedList)
{
SplitByNullability(unprocessedList, out List<SubclassCandidate> bothList, out List<SubclassCandidate> releaseList, out List<SubclassCandidate> editorList);
bothList = GetConsolidatedList(bothList);
MergeIntoBothList(bothList, releaseList);
MergeIntoBothList(bothList, editorList);
releaseList = GetConsolidatedList(releaseList);
editorList = GetConsolidatedList(editorList);
FinalMergeAttempts(bothList, releaseList, editorList);
List<SubclassCandidate> unifiedList = new List<SubclassCandidate>(bothList.Count + releaseList.Count + editorList.Count);
unifiedList.AddRange(bothList);
unifiedList.AddRange(releaseList);
unifiedList.AddRange(editorList);
return unifiedList;
}
private static void SplitByNullability(List<SubclassCandidate> inputList,
out List<SubclassCandidate> bothList,
out List<SubclassCandidate> releaseList,
out List<SubclassCandidate> editorList)
{
bothList = inputList.Where(c => c.EditorNode is not null && c.ReleaseNode is not null).ToList();
releaseList = inputList.Where(c => c.EditorNode is null && c.ReleaseNode is not null).ToList();
editorList = inputList.Where(c => c.EditorNode is not null && c.ReleaseNode is null).ToList();
}
private static bool TryMergeClasses(List<SubclassCandidate> inputList, out List<SubclassCandidate> outputList)
{
outputList = new();
bool result = false;
foreach (SubclassCandidate candidate in inputList)
{
bool merged = false;
for (int i = 0; i < outputList.Count; i++)
{
if (outputList[i].CanMerge(candidate))
{
result = true;
merged = true;
outputList[i] = outputList[i].Merge(candidate);
break;
}
}
if (!merged)
{
outputList.Add(candidate);
}
}
return result;
}
private static List<SubclassCandidate> GetConsolidatedList(List<SubclassCandidate> inputList)
{
List<SubclassCandidate> outputList = inputList;
bool shouldTryAgain = true;
while (shouldTryAgain)
{
shouldTryAgain = TryMergeClasses(outputList, out List<SubclassCandidate> newOutput);
outputList = newOutput;
}
return outputList;
}
private static void MergeIntoBothList(List<SubclassCandidate> bothList, List<SubclassCandidate> singleList)
{
List<SubclassCandidate> leftovers = new();
foreach (SubclassCandidate singleCandidate in singleList)
{
bool successful = false;
for (int i = 0; i < bothList.Count; i++)
{
if (bothList[i].Contains(singleCandidate))
{
successful = true;
bothList[i] = bothList[i].Merge(singleCandidate);
break;
}
}
if (!successful)
{
leftovers.Add(singleCandidate);
}
}
singleList.Clear();
singleList.Capacity = leftovers.Count;
singleList.AddRange(leftovers);
}
private static void FinalMergeAttempts(List<SubclassCandidate> bothList, List<SubclassCandidate> releaseList, List<SubclassCandidate> editorList)
{
if (bothList.Count == 0)
{
if (releaseList.Count == 1 && editorList.Count == 1)
{
SubclassCandidate releaseCandidate = releaseList[0];
SubclassCandidate editorCandidate = editorList[0];
if (releaseCandidate.Name == editorCandidate.Name
&& AreCompatible(releaseCandidate.ReleaseNode!, editorCandidate.EditorNode!, true))
{
SubclassCandidate mergedCandidate = releaseCandidate.Merge(editorCandidate);
releaseList.Clear();
editorList.Clear();
bothList.Add(mergedCandidate);
}
}
}
else if (bothList.Count == 1)
{
if (releaseList.Count == 1 && editorList.Count == 0)
{
SubclassCandidate releaseCandidate = releaseList[0];
SubclassCandidate bothCandidate = bothList[0];
if (bothCandidate.CanMergeRelaxed(releaseCandidate))
{
SubclassCandidate mergedCandidate = bothCandidate.Merge(releaseCandidate);
releaseList.Clear();
bothList[0] = mergedCandidate;
}
}
else if (releaseList.Count == 0 && editorList.Count == 1)
{
SubclassCandidate editorCandidate = editorList[0];
SubclassCandidate bothCandidate = bothList[0];
if (bothCandidate.CanMergeRelaxed(editorCandidate))
{
SubclassCandidate mergedCandidate = bothCandidate.Merge(editorCandidate);
editorList.Clear();
bothList[0] = mergedCandidate;
}
}
}
}
}