// Design inventory only. Parses source; never loads or executes game code. using System.Text.Json; using System.Security.Cryptography; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp; using Microsoft.CodeAnalysis.CSharp.Syntax; var root = Path.GetFullPath(args[0]); var output = Path.GetFullPath(args[1]); var configs = new Dictionary { ["editor-standalone"] = new[]{"UNITY_EDITOR","UNITY_STANDALONE_WIN"}, ["editor-android-agg"] = new[]{"UNITY_EDITOR","UNITY_ANDROID","AGG"}, ["player-android-agg"] = new[]{"UNITY_ANDROID","AGG"}, ["player-ios-agg"] = new[]{"UNITY_IOS","AGG"}, ["player-standalone"] = new[]{"UNITY_STANDALONE_WIN"} }; var calls = new Dictionary(); var refs = new Dictionary>(); var files = new Dictionary(); foreach (var path in Directory.EnumerateFiles(Path.Combine(root,"Client/Assets/Scripts"),"*.cs",SearchOption.AllDirectories).Order()) { var rel = Path.GetRelativePath(root,path).Replace('\\','/'); var source = File.ReadAllText(path); files[rel] = Convert.ToHexString(SHA256.HashData(File.ReadAllBytes(path))); foreach (var config in configs) { var tree = CSharpSyntaxTree.ParseText(source,new CSharpParseOptions(preprocessorSymbols:config.Value)); var node = tree.GetRoot(); foreach (var inv in node.DescendantNodes().OfType()) { string name = inv.Expression switch { MemberAccessExpressionSyntax m => m.Name.Identifier.ValueText, MemberBindingExpressionSyntax m => m.Name.Identifier.ValueText, SimpleNameSyntax s => s.Identifier.ValueText, _ => "" }; int line=tree.GetLineSpan(inv.Span).StartLinePosition.Line+1; if(!refs.TryGetValue(name,out var refsForName)) refs[name]=refsForName=new(); refsForName.Add(rel+":"+line); var full = inv.ToString(); var conditional=inv.Ancestors().OfType().FirstOrDefault(); var receiver = inv.Expression.ToString()+" "+conditional?.Expression.ToString(); bool facade=receiver.Contains("PlayFabMgr") && new[]{"GetLocalData","UpdateUserDataValue","SyncDataImmidiate","CompareDatas"}.Contains(name); bool sdk=receiver.Contains("PlayFabClient") && (name.Contains("UserData")); // Include unqualified internal facade calls, which the old text scan omitted. bool internalFacade=rel.EndsWith("/Core/PlayFabMgr.cs") && new[]{"GetLocalData","UpdateUserDataValue","SyncDataImmidiate","CompareDatas","SyncData"}.Contains(name); if(!facade && !sdk && !internalFacade) continue; string key=rel+":"+line+":"+inv.SpanStart; if(calls.TryGetValue(key,out var previous)){previous.Configs.Add(config.Key);continue;} var member=inv.Ancestors().FirstOrDefault(n=>n is BaseMethodDeclarationSyntax || n is PropertyDeclarationSyntax); string memberName=member switch { MethodDeclarationSyntax m => m.Identifier.ValueText, ConstructorDeclarationSyntax c => c.Identifier.ValueText, PropertyDeclarationSyntax p => p.Identifier.ValueText, _=>"" }; var type=inv.Ancestors().OfType().FirstOrDefault()?.Identifier.ValueText; var e=new Entry { Path=rel,Line=line,Member=type+"."+memberName,MemberName=memberName, MemberLine=member==null?line:tree.GetLineSpan(member.Span).StartLinePosition.Line+1, Kind=sdk?"sdk":internalFacade?"facade-internal":"facade-call",Method=name, Arguments=inv.ArgumentList.Arguments.Select(x=>x.ToString()).ToArray(), CallbackAncestor=inv.Ancestors().Any(x=>x is AnonymousFunctionExpressionSyntax), AwaitLines=member?.DescendantNodes().OfType().Select(x=>tree.GetLineSpan(x.Span).StartLinePosition.Line+1).ToArray()??Array.Empty(), MemberSource=member?.ToString()??full, Configs=new(){config.Key}, Source=full }; calls[key]=e; } } } var ordered=calls.Values.OrderBy(x=>x.Path).ThenBy(x=>x.Line).ToArray(); int i=0; foreach(var entry in ordered){entry.Id="B3-C"+(++i).ToString("D3");entry.CallerNameCandidates=refs.GetValueOrDefault(entry.MemberName)?.Order().ToArray()??Array.Empty();} Directory.CreateDirectory(output); File.WriteAllText(Path.Combine(output,"calls.json"),JsonSerializer.Serialize(ordered,new JsonSerializerOptions{WriteIndented=true})); File.WriteAllText(Path.Combine(output,"source-hashes.json"),JsonSerializer.Serialize(files,new JsonSerializerOptions{WriteIndented=true})); Console.WriteLine(JsonSerializer.Serialize(new {calls=ordered.Length,files=ordered.Select(x=>x.Path).Distinct().Count(),sourceFiles=files.Count,groups=ordered.GroupBy(x=>x.Kind).ToDictionary(g=>g.Key,g=>g.Count())})); class Entry { public string Id{get;set;} public string Path{get;set;} public int Line{get;set;} public string Member{get;set;} public string MemberName{get;set;} public int MemberLine{get;set;} public string Kind{get;set;} public string Method{get;set;} public string[] Arguments{get;set;} public bool CallbackAncestor{get;set;} public int[] AwaitLines{get;set;} public List Configs{get;set;} public string[] CallerNameCandidates{get;set;} public string MemberSource{get;set;} public string Source{get;set;} }