From 31c3da28b57864665702dea6b18d5507415469fd Mon Sep 17 00:00:00 2001 From: Gareth Parker Date: Fri, 10 May 2024 18:31:40 +0100 Subject: [PATCH 1/5] Add support for Parent="" (#45) --- CHANGELOG.md | 3 + README.md | 2 + .../References/RimworldReferenceProvider.cs | 22 ++++-- .../RimworldXMLItemProvider.cs | 56 ++++++++++--- .../SymbolScope/RimworldSymbolScope.cs | 79 ++++++++++++++++--- 5 files changed, 136 insertions(+), 26 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f246f..3bf051c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # Changelog +## 2024.2 +* Adds support for [Parent=""] attributes + ## 2024.1.7 * Solved some incompatibilities for ReSharper diff --git a/README.md b/README.md index 2a5257b..b44ead2 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,14 @@ into the definitions on which the XML sits. * Autocomplete DefNames when using `DefDatabase.GetNamed()` * Autocomplete DefNames when creating fields in `[DefOf]` classes * Autocomplete certain values for properties with fixed options (Such as Altitude Layer, boolean and directions) + * Autocompletion for `Parent=""` attributes * Use `Ctrl+Click` to go references * When using them on DefTypes, just to the C# class for that Def * When using them on XML Properties, jump to the C# definition for that property * When using them on DefName references, jump to the XML definition for that DefName * When using them on certain XML values, jump to the C# definition for that value * When using them on `[DefOf]` fields, or `DefDatabase.GetNamed()` calls, jump to the XML definition for that DefName + * When using them on `Parent=""` attributes, jump to the XML definition for that parent * Read the values in `Class=""` attributes to fetch the correct class to autocomplete from, such as in comps * Support for Custom Def Classes (Such as ``) * A Rimworld Run Configuration diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs index f3f43bc..d24c37e 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs @@ -50,6 +50,12 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection element.Parent.GetText().StartsWith("")) return GetReferenceForDeclaredElement(element, oldReferences); + if (element.Parent != null && element.NodeType.ToString() == "STRING" && + (element.Parent.GetText().StartsWith("Name") || element.Parent.GetText().StartsWith("ParentName"))) + { + return GetReferenceForDeclaredElement(element, oldReferences); + } + if (element.NodeType.ToString() == "TEXT") return GetReferencesForText(element, oldReferences); if (element is not XmlIdentifier identifier) return new ReferenceCollection(); if (element.GetSourceFile() is not { } sourceFile) return new ReferenceCollection(); @@ -129,7 +135,6 @@ private ReferenceCollection GetReferencesForText(ITreeNode element, ReferenceCol if (classContext == null) return new ReferenceCollection(); var rimworldSymbolScope = ScopeHelper.RimworldScope; - var allSymbolScopes = ScopeHelper.AllScopes; if (classContext.GetType().Name == "Enum") { @@ -137,7 +142,6 @@ private ReferenceCollection GetReferencesForText(ITreeNode element, ReferenceCol var col = new ReferenceCollection(new RimworldXmlReference(@class, element)); return col; - // return new ReferenceCollection(); } if (!ScopeHelper.ExtendsFromVerseDef(classContext.GetClrName().FullName)) @@ -169,15 +173,21 @@ private ReferenceCollection GetReferencesForText(ITreeNode element, ReferenceCol private ReferenceCollection GetReferenceForDeclaredElement(ITreeNode element, ReferenceCollection oldReferences) { // We're currently in a text node inside a inside another ThingDef node. We want to get that node - var defTypeName = element.Parent?.Parent? + // Alternatively, we may be in a string node inside a Name Attribute, so we need to go a step further + var parentTag = element.Parent?.Parent; + if (parentTag is not XmlTag && parentTag?.Parent is XmlTag) parentTag = parentTag.Parent; + + if (parentTag is null) return new ReferenceCollection(); + + var defTypeName = parentTag // And then get the TagHeader () of that node .Children().FirstOrDefault(childElement => childElement is XmlTagHeaderNode)? // And then get the text that provides the ID of that node (ThingDef) .Children().FirstOrDefault(childElement => childElement is XmlIdentifier)?.GetText(); - if (defTypeName is null) new ReferenceCollection(); - - var defName = element.GetText(); + if (defTypeName is null) return new ReferenceCollection(); + + var defName = element.GetText().Trim('"'); var xmlSymbolTable = element.GetSolution().GetComponent(); diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs index 97f58e3..d6e935f 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs @@ -48,6 +48,11 @@ protected override bool IsAvailable(RimworldXmlCodeCompletionContext context) if (context.TreeNode is XmlTagEndToken && context.TreeNode.PrevSibling is XmlIdentifier && context.TreeNode.PrevSibling.PrevSibling?.GetText() != " * <{CARET HERE @@ -125,6 +138,33 @@ protected override bool AddLookupItems(RimworldXmlCodeCompletionContext context, return base.AddLookupItems(context, collector); } + protected void AddParentNameItems(RimworldXmlCodeCompletionContext context, IItemsCollector collector) + { + if (context.TreeNode?.Parent is not XmlAttribute attribute) return; + if (attribute.Parent is not XmlTagHeaderNode defTag) return; + + var defClassName = defTag.ContainerName; + var defClass = ScopeHelper.GetScopeForClass(defClassName); + + var xmlSymbolTable = context.TreeNode!.GetSolution().GetSolution().GetComponent(); + + var keys = xmlSymbolTable.GetDefsByType(defClassName); + + foreach (var key in keys) + { + if (!xmlSymbolTable.IsDefAbstract(key)) continue; + + var defType = key.Split('/').First(); + var defName = key.Split('/').Last(); + + var item = xmlSymbolTable.GetTagByDef(defType, defName); + + var lookup = LookupFactory.CreateDeclaredElementLookupItem(context, defName, + new DeclaredElementInstance(new XMLTagDeclaredElement(item, defType, defName, false))); + collector.Add(lookup); + } + } + protected void AddTextLookupItems(RimworldXmlCodeCompletionContext context, IItemsCollector collector) { var hierarchy = GetHierarchy(context.TreeNode); @@ -197,8 +237,6 @@ protected void AddTextLookupItems(RimworldXmlCodeCompletionContext context, IIte new DeclaredElementInstance(new XMLTagDeclaredElement(item, defType, defName, false))); collector.Add(lookup); } - - return; } protected void AddThingDefClasses(RimworldXmlCodeCompletionContext context, IItemsCollector collector, @@ -296,7 +334,7 @@ public static List GetAllPublicFields(ITypeElement desiredClass, ISymbol .FirstOrDefault(attribute => attribute.GetClrName().FullName == "Verse.LoadAliasAttribute"); if (loadAliasAttributes != null) return true; - + return false; }) .Select(member => member.Member) @@ -499,18 +537,18 @@ attribute is MetadataAttributeInstance ); if (loadAliasAttribute == null) continue; - + var writer = new StringWriter(new StringBuilder()); - ((MetadataAttributeInstance) loadAliasAttribute).Dump(writer, ""); + ((MetadataAttributeInstance)loadAliasAttribute).Dump(writer, ""); writer.Close(); var match = Regex.Match(writer.ToString(), "Arguments: \"(.*?)\""); if (match.Groups.Count < 2) continue; - - + + collector.Add(LookupFactory.CreateDeclaredElementLookupItem(context, match.Groups[1].Value, new DeclaredElementInstance(field), true, false, QualifierKind.NONE)); - + continue; } diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs index d63eed7..f3b434a 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs +++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs @@ -3,6 +3,7 @@ using JetBrains; using JetBrains.Annotations; using JetBrains.Application.Parts; +using JetBrains.Application.Parts; using JetBrains.Application.Threading; using JetBrains.Collections; using JetBrains.Lifetimes; @@ -15,15 +16,28 @@ using JetBrains.ReSharper.Psi.Resolve; using JetBrains.ReSharper.Psi.Tree; using JetBrains.ReSharper.Psi.Util; +using JetBrains.ReSharper.Psi.Xml.Impl.Tree; using JetBrains.ReSharper.Psi.Xml.Tree; using ReSharperPlugin.RimworldDev.TypeDeclaration; namespace ReSharperPlugin.RimworldDev.SymbolScope; +public struct DefTag +{ + public DefTag(ITreeNode treeNode, bool isAbstract = false) + { + TreeNode = treeNode; + IsAbstract = isAbstract; + } + + public ITreeNode TreeNode { get; } + public bool IsAbstract { get; } +} + [PsiComponent(Instantiation.ContainerAsyncPrimaryThread)] public class RimworldSymbolScope : SimpleICache> { - private Dictionary DefTags = new(); + private Dictionary DefTags = new(); private Dictionary ExtraDefTagNames = new(); private Dictionary _declaredElements = new(); private SymbolTable _symbolTable; @@ -58,7 +72,12 @@ public ITreeNode GetTagByDef(string defId) if (!DefTags.ContainsKey(defId)) return null; - return DefTags[defId]; + return DefTags[defId].TreeNode; + } + + public bool IsDefAbstract(string defId) + { + return DefTags.ContainsKey(defId) && DefTags[defId].IsAbstract; } public DefNameValue GetDefName(DefNameValue value) => @@ -88,15 +107,33 @@ public override object Build(IPsiSourceFile sourceFile, bool isStartup) var tags = xmlFile.GetNestedTags("Defs/*").Where(tag => { var defNameTag = tag.GetNestedTags("defName").FirstOrDefault(); - return defNameTag is not null; + if (defNameTag is not null) return true; + + var nameAttribute = tag.GetAttribute("Name"); + return nameAttribute is not null; }); List defs = new(); foreach (var tag in tags) { - var defName = tag.GetNestedTags("defName").FirstOrDefault()?.InnerText; - var defNameTag = tag.GetNestedTags("defName").FirstOrDefault().Children().ElementAt(1); + var defName = tag + .GetNestedTags("defName") + .FirstOrDefault()?.InnerText ?? + tag + .GetAttribute("Name")? + .Children() + .FirstOrDefault(element => element is IXmlValueToken)? + .GetUnquotedText(); + + var defNameTag = tag.GetNestedTags("defName"). + FirstOrDefault()?. + Children(). + ElementAt(1) ?? + tag.GetAttribute("Name")?. + Children(). + FirstOrDefault(element => element is IXmlValueToken); + if (defName is null) continue; defs.Add(new RimworldXmlDefSymbol(defNameTag, defName, tag.GetTagName())); @@ -132,12 +169,27 @@ private void AddToLocalCache(IPsiSourceFile sourceFile, [CanBeNull] List { var matchingDefTag = xmlFile - .GetNestedTags("Defs/*/defName").FirstOrDefault(tag => - tag.Children().ElementAt(1).GetTreeStartOffset().Offset == item.DocumentOffset); - + .GetNestedTags("Defs/*/defName").FirstOrDefault(tag => + tag.Children().ElementAt(1).GetTreeStartOffset().Offset == + item.DocumentOffset) ?? + xmlFile + .GetNestedTags("Defs/*") + .FirstOrDefault(tag => + tag.GetAttribute("Name")? + .Children() + .FirstOrDefault(element => element is IXmlValueToken)? + .GetTreeStartOffset().Offset == item.DocumentOffset + )? + .GetAttribute("Name")? + .Children() + .FirstOrDefault(element => element is IXmlValueToken); + if (matchingDefTag is null) return; - var xmlTag = matchingDefTag.Children().ElementAt(1); + // If the DefName is in a [Name=""] Attribute, it'll be matched to a XmlValueToken, which doesn't have any + // children. Otherwise, it'll be matched to the XmlTag for , where we want the first child as the + // string value + var xmlTag = matchingDefTag is IXmlValueToken ? matchingDefTag : matchingDefTag.Children().ElementAt(1); AddDefTagToList(item, xmlTag); }); @@ -168,10 +220,15 @@ void AddDefTagToList(RimworldXmlDefSymbol item, ITreeNode xmlTag) } } + var isAbstract = xmlTag is IXmlValueToken && + xmlTag.Parent?.Parent is XmlTagHeaderNode defTypeTag && + defTypeTag.GetAttribute("Abstract") is {} attribute && + attribute.UnquotedValue.ToLower() == "true"; + if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}")) - DefTags.Add($"{item.DefType}/{item.DefName}", xmlTag); + DefTags.Add($"{item.DefType}/{item.DefName}", new DefTag(xmlTag, isAbstract)); else - DefTags[$"{item.DefType}/{item.DefName}"] = xmlTag; + DefTags[$"{item.DefType}/{item.DefName}"] = new DefTag(xmlTag, isAbstract); } } } From 2591d70d128310608bb23120839e8d4ec726cc4d Mon Sep 17 00:00:00 2001 From: Gareth Date: Fri, 10 May 2024 18:33:12 +0100 Subject: [PATCH 2/5] Change the version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index d6599b7..a24c4ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ DotnetPluginId=ReSharperPlugin.RimworldDev DotnetSolution=ReSharperPlugin.RimworldDev.sln RiderPluginId=com.jetbrains.rider.plugins.rimworlddev -PluginVersion=2024.1.7 +PluginVersion=2024.2 BuildConfiguration=Release From 56a54b061629ea19526a1b434393b0316b1b5fab Mon Sep 17 00:00:00 2001 From: Gareth Parker Date: Fri, 9 May 2025 21:50:43 +0100 Subject: [PATCH 3/5] Make the template more modular (#47) * Make the template more modular * Adds the feature to the changelog * Let the AssemblyPath be auto-filled again and fetch it from our centralized ScopeHelper --- CHANGELOG.md | 3 +- .../.template.config/template.json | 83 ++++++++++-- .../TemplateParameters/ModAuthorParameter.cs | 38 ------ .../RimworldDLLParameter.cs | 127 +++++------------- 4 files changed, 111 insertions(+), 140 deletions(-) delete mode 100644 src/dotnet/ReSharperPlugin.RimworldDev/TemplateParameters/ModAuthorParameter.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3bf051c..04c312e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,8 @@ # Changelog ## 2024.2 -* Adds support for [Parent=""] attributes + * Adds support for [Parent=""] attributes + * Makes the New Mod template modular, allowing you to select the components you want included in your mod ## 2024.1.7 * Solved some incompatibilities for ReSharper diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ProjectTemplates/RimworldProjectTemplate/.template.config/template.json b/src/dotnet/ReSharperPlugin.RimworldDev/ProjectTemplates/RimworldProjectTemplate/.template.config/template.json index 99bcb8d..3e4e45e 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ProjectTemplates/RimworldProjectTemplate/.template.config/template.json +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ProjectTemplates/RimworldProjectTemplate/.template.config/template.json @@ -41,6 +41,38 @@ "isRequired": "false", "defaultValue": "" }, + "PublisherPlus": { + "type": "parameter", + "datatype": "bool", + "displayName": "Include a PublisherPlus config", + "description": "If enabled, a PublisherPlus config file will be included in the mod", + "isRequired": "false", + "defaultValue": "true" + }, + "CSharp": { + "type": "parameter", + "datatype": "bool", + "displayName": "Add a C# Project and Sources", + "description": "If enabled, this mod will be created with a .sln file, Sources folder and a csproj file", + "isRequired": "false", + "defaultValue": "true" + }, + "XML": { + "type": "parameter", + "datatype": "bool", + "displayName": "Add a Defs folder with a starting XML file", + "description": "If enabled, this mod will be created with a Defs folder and a starting XML file", + "isRequired": "false", + "defaultValue": "true" + }, + "Languages": { + "type": "parameter", + "datatype": "bool", + "displayName": "Add a Languages folder", + "description": "If enabled, this mod will be created with a Languages folder with a starting English.xml file", + "isRequired": "false", + "defaultValue": "true" + }, "AssemblyDir": { "type": "derived", "valueSource": "RimworldDLL", @@ -64,11 +96,22 @@ { "type": "conditional", "configuration": { - "if": [ " + true @@ -20,8 +23,15 @@ true + + + + + + + @@ -42,4 +52,14 @@ + + + + + + + JetResourceGenerator + + + \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj index bd8468d..edf475a 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj @@ -8,7 +8,7 @@ - net472 + net6.0 True $(DefineConstants);RESHARPER false @@ -42,6 +42,9 @@ + + + diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Remodder/AssemblyHelper.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Remodder/AssemblyHelper.cs new file mode 100644 index 0000000..28d37ae --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Remodder/AssemblyHelper.cs @@ -0,0 +1,47 @@ +using System.IO; +using AsmResolver; +using AsmResolver.PE; +using AsmResolver.PE.DotNet.Builder; +using AsmResolver.PE.DotNet.Metadata.Strings; +using AsmResolver.PE.DotNet.Metadata.Tables; +using AsmResolver.PE.DotNet.Metadata.Tables.Rows; + +namespace ReSharperPlugin.RimworldDev.Remodder; + +public static class AssemblyHelper +{ + public static byte[] RemoveReferenceAssemblyAttribute(byte[] input) + { + var peImage = PEImage.FromBytes(input); + var tables = peImage.DotNetDirectory.Metadata.GetStream(); + var customAttrsTable = tables.GetTable(); + var memberRefTable = tables.GetTable(); + var typeRefTable = tables.GetTable(); + var stringsStream = peImage.DotNetDirectory.Metadata.GetStream(); + + for (int i = customAttrsTable.Count - 1; i >= 0; i--) + { + var r = customAttrsTable[i]; + if ((r.Type & 0b111) == 3) + { + var ctor = memberRefTable.GetByRid(r.Type >> 3); + if ((ctor.Parent & 0b111) == 1) + { + var type = typeRefTable.GetByRid(ctor.Parent >> 3); + if (stringsStream.GetStringByIndex(type.Name) == "ReferenceAssemblyAttribute") + { + customAttrsTable.Remove(r); + } + } + } + } + + var fileBuilder = new ManagedPEFileBuilder(EmptyErrorListener.Instance); + var file = fileBuilder.CreateFile(peImage); + + var stream = new MemoryStream(); + file.Write(stream); + + return stream.ToArray(); + } +} \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Remodder/Decompiler.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Remodder/Decompiler.cs new file mode 100644 index 0000000..a17d50d --- /dev/null +++ b/src/dotnet/ReSharperPlugin.RimworldDev/Remodder/Decompiler.cs @@ -0,0 +1,200 @@ +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Reflection.Emit; +using System.Threading; +using HarmonyLib; +using ICSharpCode.Decompiler; +using ICSharpCode.Decompiler.CSharp; +using ICSharpCode.Decompiler.DebugInfo; +using ICSharpCode.Decompiler.Disassembler; +using ICSharpCode.Decompiler.Metadata; +using ICSharpCode.Decompiler.TypeSystem; +using Mono.Cecil; +using Mono.Cecil.Cil; +using MonoMod.Utils; + +// Taken from my old TranspilerExplorer project +// Badly needs a rework... +namespace ReSharperPlugin.RimworldDev.Remodder; + +public static class Decompiler +{ + const string OrigType = "OrigType"; + const string DummyDll = "decomp.dll"; + + public static AttributePatch? GetTranspiler(Assembly asm, string typeName) + { + var type = asm.GetType(typeName); + if (type == null) + return null; + var transpiler = new Harmony("dummy"). + CreateClassProcessor(type).patchMethods?. + FirstOrDefault(p => p.type == HarmonyPatchType.Transpiler); + return transpiler; + } + + public static string Decompile(MethodBase orig, MethodInfo? transpiler, string[] userAsms, IDebugInfoProvider? debugInfo) + { + using var stream = new MemoryStream(); + WriteAssembly(stream, orig, transpiler); + stream.Position = 0; + + using var peFile = new PEFile(DummyDll, stream); + using var writer = new StringWriter(); + + var assemblyResolver = new UniversalAssemblyResolver( + userAsms.FirstOrDefault(), + false, + peFile.DetectTargetFrameworkId(), + peFile.DetectRuntimePack() + ); + + foreach (var userAsm in userAsms.Skip(1)) + { + var dir = Path.GetDirectoryName(userAsm); + if (!string.IsNullOrEmpty(dir) && !assemblyResolver.GetSearchDirectories().Contains(dir)) + assemblyResolver.AddSearchDirectory(Path.GetDirectoryName(userAsm)); + } + + var settings = new DecompilerSettings + { + ThrowOnAssemblyResolveErrors = false, + AnonymousMethods = false, + UseDebugSymbols = debugInfo != null + }; + + var decompiler = new CSharpDecompiler(peFile, assemblyResolver, settings) + { + DebugInfoProvider = debugInfo, + }; + + var code = decompiler.DecompileTypeAsString(new FullTypeName(orig.DeclaringType?.Name ?? OrigType)); + + return code; + } + + public static string Disasm(MethodBase orig, MethodInfo transpiler) + { + using var stream = new MemoryStream(); + WriteAssembly(stream, orig, transpiler); + stream.Position = 0; + + using var peFile = new PEFile(DummyDll, stream); + using var writer = new StringWriter(); + + var output = new PlainTextOutput(writer); + ReflectionDisassembler rd = new ReflectionDisassembler(output, CancellationToken.None); + rd.DetectControlStructure = false; + rd.DisassembleType(peFile, peFile.GetTypeDefinition(new TopLevelTypeName(orig.DeclaringType?.Name ?? OrigType))); + + return writer.ToString(); + } + + static void WriteAssembly(Stream stream, MethodBase orig, MethodInfo? transpiler) + { + var patch = MethodPatcher.CreateDynamicMethod(orig, "", false); + var il = patch.GetILGenerator(); + var originalVariables = MethodPatcher.DeclareOriginalLocalVariables(il, orig); + var copier = new MethodCopier(orig, il, originalVariables); + var emitter = new Emitter(il, false); + + copier.AddTranspiler(transpiler ?? identityTranspiler); + + var endLabels = new List