Last active
October 6, 2020 17:40
-
-
Save amis92/760757b559e4b070d6c4b037192637cd to your computer and use it in GitHub Desktop.
BSData duplicate ID analysis script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env dotnet-script | |
#r "nuget: System.Linq.Async, 4.1.1" | |
#r "nuget: Optional, 4.0.0" | |
#r "nuget: WarHub.ArmouryModel.Workspaces.BattleScribe, 0.11.0" | |
#r "./script/bin/netcoreapp3.1/WarHub.GodMode.SourceAnalysis.dll" | |
using WarHub.ArmouryModel.ProjectModel; | |
using WarHub.ArmouryModel.Source; | |
using WarHub.ArmouryModel.Source.BattleScribe; | |
using WarHub.ArmouryModel.Workspaces.BattleScribe; | |
using WarHub.GodMode.SourceAnalysis; | |
if (Args.Count < 2) | |
throw new ArgumentException("You need to provide at least 2 arguments: base and target catalogue names."); | |
var baseName = Args[0]; | |
var targetName = Args[1]; | |
WriteLine($"Analyzing '{targetName}' (target) based on '{baseName}' (base)."); | |
WriteLine("Starting. Loading workspace..."); | |
var workspace = XmlWorkspace.CreateFromDirectory("."); | |
WriteLine("Creating analysis context..."); | |
var ctx = (await GamesystemContext.CreateAsync(workspace)).Single(); | |
var baseCatalogue = ctx.Catalogues.SingleOrDefault(x => x.Name == baseName) | |
?? throw new ArgumentException($"Catalogue '{baseName}' not found"); | |
var targetCatalogue = ctx.Catalogues.Single(x => x.Name == targetName) | |
?? throw new ArgumentException($"Catalogue '{targetName}' not found"); | |
if (ctx.Diagnostics.Length > 0) | |
Print(ctx.Diagnostics); | |
WriteLine("Indexing catalogues..."); | |
Dictionary<string, List<SourceNode>> MapById(SourceNode root) | |
{ | |
return root | |
.Descendants() | |
.Where(x => !x.IsList && x is IIdentifiableNode) | |
.GroupBy(x => ((IIdentifiableNode)x).Id) | |
.ToDictionary(x => x.Key, x => x.ToList()); | |
} | |
var baseMap = MapById(baseCatalogue); | |
var targetMap = MapById(targetCatalogue); | |
bool internalDuplicates = false; | |
if (baseMap.Values.Any(x => x.Count > 1)) | |
{ | |
internalDuplicates = true; | |
WriteLine("Duplicate IDs found in base:"); | |
foreach (var list in baseMap.Values.Where(x => x.Count > 0)) | |
{ | |
WriteLine("- ID: " + (list[0] as IIdentifiableNode).Id); | |
Print(list); | |
} | |
} | |
if (targetMap.Values.Any(x => x.Count > 1)) | |
{ | |
internalDuplicates = true; | |
WriteLine("Duplicate IDs found in target:"); | |
foreach (var list in targetMap.Values.Where(x => x.Count > 0)) | |
{ | |
WriteLine("- ID: " + (list[0] as IIdentifiableNode).Id); | |
Print(list); | |
} | |
} | |
if (internalDuplicates) | |
{ | |
WriteLine("Please fix duplicate IDs within catalogues first."); | |
return 0; | |
} | |
WriteLine("Indexed."); | |
WriteLine("Finding duplicate IDs across catalogues..."); | |
string GetAncestryString(SourceNode node) | |
{ | |
var ancestry = | |
(from anc in node.AncestorsAndSelf().Reverse().Skip(1) | |
select anc switch | |
{ | |
INameableNode nameable => nameable.Name, | |
IIdentifiableNode ident => $"{anc.Kind} {ident.Id}", | |
{ Parent: { IsList: true } } => $"{anc.Kind}[{anc.IndexInParent + 1}]", | |
{ Parent: { } } => $"{anc.GetChildInfoFromParent()?.Name}", | |
_ => anc.Kind.ToString() | |
}); | |
return string.Join(" -> ", ancestry); | |
} | |
foreach (var key in baseMap.Keys) | |
{ | |
if (targetMap.ContainsKey(key)) | |
{ | |
WriteLine("### Duplicate key " + key); | |
WriteLine("* Base: " + GetAncestryString(baseMap[key].Single())); | |
WriteLine("* Trgt: " + GetAncestryString(targetMap[key].Single())); | |
} | |
} | |
if (Args.Count > 2 && Args[2] == "dedupe") | |
{ | |
WriteLine("Deduplication (removing target duplicates)..."); | |
} | |
else | |
{ | |
WriteLine("Done."); | |
return 0; | |
} | |
internal class LambdaRewriter : SourceRewriter | |
{ | |
private readonly Func<SourceNode, SourceNode> selector; | |
public LambdaRewriter(Func<SourceNode, SourceNode> selector) | |
{ | |
this.selector = selector; | |
} | |
public static TNode Visit<TNode>(TNode node, Func<SourceNode, SourceNode> selector) | |
where TNode : SourceNode | |
{ | |
var visitor = new LambdaRewriter(selector); | |
return (TNode)visitor.Visit(node); | |
} | |
public override SourceNode Visit(SourceNode node) | |
{ | |
return base.Visit(selector(node))!; | |
} | |
} | |
var targetDocument = await workspace.Documents | |
.ToAsyncEnumerable() | |
.WhereAwait(async x => (await x.GetRootAsync()) == targetCatalogue) | |
.SingleAsync(); | |
var nodesToRemove = baseMap.Keys | |
.Select(key => | |
{ | |
return targetMap.TryGetValue(key, out var nodes) ? nodes.Single() : null; | |
}) | |
.Where(x => x != null) | |
.ToHashSet(); | |
foreach (var node in nodesToRemove) | |
{ | |
WriteLine("### Removing node for duplicate key " + ((IIdentifiableNode)node).Id); | |
WriteLine("* Target node removed: " + GetAncestryString(node)); | |
} | |
var strippedCatalogue = LambdaRewriter.Visit(targetCatalogue, x => nodesToRemove.Contains(x) ? null : x); | |
WriteLine("Saving target catalogue after nodes removed..."); | |
using (var stream = File.Open(targetDocument.Filepath, FileMode.Create)) | |
{ | |
strippedCatalogue.Serialize(stream); | |
} | |
WriteLine("Done."); | |
return 0; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment