using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Text.RegularExpressions; using Microsoft.Build.Construction; using Microsoft.Xna.Framework.Content.Pipeline; using Newtonsoft.Json; namespace Contentless; public static class Program { public static void Main(string[] args) { if (args.Length < 1) { Console.WriteLine("Please specify the location of the content file you want to use"); return; } var contentFile = new FileInfo(Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, args[0]))); if (!contentFile.Exists) { Console.WriteLine($"Unable to find content file {contentFile}"); return; } Console.WriteLine($"Using content file {contentFile}"); var content = Program.ReadContent(contentFile); // load config var config = new Config(); var configFile = new FileInfo(Path.Combine(contentFile.DirectoryName!, "Contentless.json")); if (configFile.Exists) { using var stream = configFile.OpenText(); try { config = JsonConvert.DeserializeObject(stream.ReadToEnd()); Console.WriteLine($"Using config from {configFile}"); } catch (Exception e) { Console.WriteLine($"Error loading config from {configFile}: {e}"); } } else { Console.WriteLine("Using default config"); } var excluded = config.ExcludedFiles.Select(Program.MakeFileRegex).ToArray(); var overrides = Program.GetOverrides(config.Overrides).ToArray(); var referencesVersions = config.References.ToDictionary(x => x, x => (string)null, StringComparer.OrdinalIgnoreCase); if (config.References.Length > 0) { if (args.Length > 1) { ExtractVersions(args[1], referencesVersions); _nuGetHelper = new NuGetHelper(Path.GetDirectoryName(args[1])); } else Console.Error.WriteLine("You supplied references but there is no project file, this isn't compatible. Please specify the full path of project file, if you want to sync references"); } const string ReferenceHeader = "/reference:"; var changed = false; var referencesSyncs = new HashSet(StringComparer.OrdinalIgnoreCase); // load any references to be able to include custom content types as well for (int i = 0; i < content.Count; i++) { var line = content[i]; if (!line.StartsWith(ReferenceHeader)) continue; var reference = line.Substring(ReferenceHeader.Length); var libraryName = Path.GetFileName(reference)[..^4]; if (referencesVersions.TryGetValue(libraryName, out var version) && version is not null) { var fullLibraryPath = CalculateFullPathToLibrary(libraryName, version); if (reference != fullLibraryPath) { Console.WriteLine($"Changing library reference from {reference} to {fullLibraryPath}"); reference = fullLibraryPath; content[i] = ReferenceHeader + fullLibraryPath; changed = true; } else Console.WriteLine($"Skipping library reference {fullLibraryPath} (success sync)"); referencesSyncs.Add(libraryName); } var refPath = Path.GetFullPath(Path.Combine(contentFile.DirectoryName, reference)); SafeAssemblyLoad(refPath); } // check references not in .mgcb now var referencesLastIndex = 0; // find place where I can add new reference for (int i = 0; i < content.Count; i++) { var line = content[i]; if (line.StartsWith(ReferenceHeader)) referencesLastIndex = i + 1; else if (line.StartsWith("/importer:") || line.StartsWith("/processor:") || line.StartsWith("/build:") || line.Contains("-- Content --")) { if (referencesLastIndex == 0) referencesLastIndex = i; break; } } foreach (var reference in referencesVersions) if (!referencesSyncs.Contains(reference.Key) && reference.Value is not null) { try { var path = CalculateFullPathToLibrary(reference.Key, reference.Value); content.Insert(referencesLastIndex++, ReferenceHeader + path); changed = true; SafeAssemblyLoad(path); Console.WriteLine($"Adding reference for {path} in .mgcb"); } catch (Exception e) { Console.Error.WriteLine($"Error adding library {reference.Key} in .mgcb: {e}"); } } // load content importers var (importers, processors) = Program.GetContentData(); Console.WriteLine($"Found possible importer types {string.Join(", ", importers)}"); Console.WriteLine($"Found possible processor types {string.Join(", ", processors)}"); foreach (var file in contentFile.Directory.EnumerateFiles("*", SearchOption.AllDirectories)) { // is the file the content or config file? if (file.Name == contentFile.Name || file.Name == configFile.Name) continue; var relative = Program.GetRelativePath(contentFile.DirectoryName, file.FullName).Replace("\\", "/"); // is the file in an excluded directory? if (excluded.Any(e => e.IsMatch(relative))) { if (config.LogSkipped) Console.WriteLine($"Skipping excluded file {relative}"); continue; } // is the file already in the content file? if (Program.HasEntry(content, relative)) { if (config.LogSkipped) Console.WriteLine($"Skipping file {relative} as it is already part of the content file"); continue; } ImporterInfo importer = null; string processor = null; Dictionary processorParams = null; // override importers var over = Program.GetOverrideFor(relative, overrides); if (over != null) { processorParams = over.Override.ProcessorParams; // copy special case if (over.Override.Copy) { Program.CopyFile(content, relative); changed = true; continue; } if (!string.IsNullOrEmpty(over.Override.Importer)) { importer = importers.Find(i => i.Type.Name == over.Override.Importer); if (importer == null) { Console.WriteLine($"Override importer {over.Override.Importer} not found for file {relative}"); continue; } } if (!string.IsNullOrEmpty(over.Override.Processor)) { processor = processors.Find(p => p == over.Override.Processor); if (processor == null) { Console.WriteLine($"Override processor {over.Override.Processor} not found for file {relative}"); continue; } } } // normal importers importer ??= Program.GetImporterFor(relative, importers); if (importer != null && processor == null) processor = processors.Find(p => p == importer.Importer.DefaultProcessor); // no importer found :( if (importer == null || processor == null) { Console.WriteLine($"No importer or processor found for file {relative}"); continue; } Program.AddFile(content, relative, importer.Type.Name, processor, processorParams); changed = true; } if (changed) { contentFile.Delete(); using (var stream = contentFile.CreateText()) { foreach (var line in content) stream.WriteLine(line); } Console.WriteLine("Wrote changes to content file"); } Console.Write("Done"); } private static void SafeAssemblyLoad(string refPath) { try { Assembly.LoadFrom(refPath); Console.WriteLine($"Using reference {refPath}"); } catch (Exception e) { Console.WriteLine($"Error loading reference {refPath}: {e}"); } } private static void ExtractVersions(string csprojPath, Dictionary referencesVersions) { Console.WriteLine($"Using project file {csprojPath}"); var projectRootElement = ProjectRootElement.Open(csprojPath); foreach (var property in projectRootElement.AllChildren.Where(x => x.ElementName == "PackageReference").Select(x => x as ProjectItemElement)) { var libraryName = property.Include; if (property.Children.FirstOrDefault(x => x.ElementName == "Version") is not ProjectMetadataElement versionElement) continue; var version = versionElement.Value; if (referencesVersions.Keys.Contains(libraryName)) { referencesVersions[libraryName] = version; Console.WriteLine($"Found library version for sync: {libraryName}, {version}"); } } foreach (var library in referencesVersions) if (library.Value is null) Console.Error.WriteLine($"Unable to find library {library.Key} in .csproj"); } private static NuGetHelper _nuGetHelper; private static string CalculateFullPathToLibrary(string libraryName, string referencesVersion) { return Path.Combine(_nuGetHelper.PackageFolder, libraryName.ToLower(), referencesVersion, "tools", libraryName + ".dll"); } private static (List, List) GetContentData() { var importers = new List(); var processors = new List(); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { try { foreach (var type in assembly.GetTypes()) { var importer = (ContentImporterAttribute) type.GetCustomAttribute(typeof(ContentImporterAttribute), true); if (importer != null) importers.Add(new ImporterInfo(importer, type)); var processor = type.GetCustomAttribute(typeof(ContentProcessorAttribute), true); if (processor != null) processors.Add(type.Name); } } catch (Exception e) { Console.WriteLine($"Error gathering types in reference {assembly}: {e}"); } } return (importers, processors); } private static IEnumerable GetOverrides(Dictionary config) { foreach (var entry in config) yield return new OverrideInfo(Program.MakeFileRegex(entry.Key), entry.Value); } private static OverrideInfo GetOverrideFor(string file, IEnumerable overrides) { foreach (var over in overrides) { if (over.Regex.IsMatch(file)) return over; } return null; } private static ImporterInfo GetImporterFor(string file, IEnumerable importers) { var extension = Path.GetExtension(file); foreach (var importer in importers) { if (importer.Importer.FileExtensions.Contains(extension)) return importer; } return null; } private static bool HasEntry(IEnumerable content, string relativeFile) { foreach (var line in content) { if (line.StartsWith($"#begin {relativeFile}")) return true; } return false; } private static List ReadContent(FileInfo file) { var content = new List(); using var stream = file.OpenText(); while (stream.ReadLine() is {} line) content.Add(line); return content; } private static void AddFile(ICollection content, string relative, string importer, string processor, Dictionary processorParams) { content.Add($"#begin {relative}"); content.Add($"/importer:{importer}"); content.Add($"/processor:{processor}"); if (processorParams != null) { foreach (var kv in processorParams) content.Add($"/processorParam:{kv.Key}={kv.Value}"); } content.Add($"/build:{relative}"); content.Add(""); Console.WriteLine($"Adding file {relative} with importer {importer} and processor {processor}"); } private static void CopyFile(ICollection content, string relative) { content.Add($"#begin {relative}"); content.Add($"/copy:{relative}"); content.Add(""); Console.WriteLine($"Adding file {relative} with the Copy build action"); } private static string GetRelativePath(string relativeTo, string path) { if (!relativeTo.EndsWith(Path.DirectorySeparatorChar.ToString())) relativeTo += Path.DirectorySeparatorChar; return path.Replace(relativeTo, ""); } private static Regex MakeFileRegex(string s) { return new Regex(s.Replace(".", "[.]").Replace("*", ".*").Replace("?", ".")); } }