diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 79bc706..c16194a 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -19,14 +19,6 @@ jobs: distribution: 'corretto' java-version: '21' cache: 'gradle' - - uses: actions/cache@v4 - with: - path: | - build/gradle-jvm - ~/.nuget/packages - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-Build-${{ hashFiles('gradlew.bat', 'src/dotnet/*/*.csproj', './*.props', 'gradle-wrapper.properties') }} - run: ./gradlew :buildPlugin --no-daemon - run: ./gradlew :buildResharperPlugin --no-daemon - uses: actions/upload-artifact@v4 @@ -46,13 +38,4 @@ jobs: distribution: 'corretto' java-version: '17' cache: 'gradle' - - uses: actions/cache@v4 - with: - path: | - build/gradle-jvm - packages - ~/.nuget/packages - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-Test-${{ hashFiles('gradlew.bat', 'src/dotnet/*/*.csproj', './*.props', 'gradle-wrapper.properties') }} - run: ./gradlew :testDotNet --no-daemon \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f246f..7a6fe60 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## 2024.2 + * Adds support for [Parent=""] attributes + * Makes the New Mod template modular, allowing you to select the components you want included in your mod + * Bundled in Zetrith's Remodder code into this plugin + * Update the Rimworld template for 1.6 + * Automatically detect and use the user-installed Harmony version for debugging, if available + ## 2024.1.7 * Solved some incompatibilities for ReSharper diff --git a/Directory.Build.props b/Directory.Build.props index 20c468f..066f197 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -39,6 +39,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/README.md b/README.md index 2a5257b..49d2279 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 @@ -38,6 +40,7 @@ into the definitions on which the XML sits. * The ability to perform Find Usages on XML Defs * A "Generate" menu option to generate properties for a given XML Def * Includes a Rimworld Dictionary so that Rimworld terms don't get flagged as not real words by Rider + * Transpilation Explorer, curtesy of [Zetrith](https://github.com/Zetrith/Remodder) ## Quick Architecture For Developers @@ -75,6 +78,10 @@ our own custom context provider which grabs the solution from the XML File and a The other is the [`RimworldXmlReference`](./src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs) which is just a class that we have to define to hold our reference from the XML to the C#. +## Acknowledgements +The feature of exploring transpiled code is taken from Zetrith's open source plugin, [Remodder](https://github.com/Zetrith/Remodder). +The feature would not have happened without them. + ## FAQ **VS Code Plugin When?** diff --git a/build.gradle.kts b/build.gradle.kts index 407f416..263698c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ +import com.jetbrains.plugin.structure.base.utils.isFile import groovy.ant.FileNameFinder import org.apache.tools.ant.taskdefs.condition.Os +import org.jetbrains.intellij.platform.gradle.Constants import org.jetbrains.intellij.platform.gradle.IntelliJPlatformType import java.io.ByteArrayOutputStream @@ -7,7 +9,6 @@ plugins { id("java") alias(libs.plugins.kotlinJvm) id("org.jetbrains.intellij.platform") version "2.5.0" // https://github.com/JetBrains/gradle-intellij-plugin/releases -// id("com.jetbrains.rdgen") version libs.versions.rdGen // https://www.myget.org/feed/rd-snapshots/package/maven/com.jetbrains.rd/rd-gen id("me.filippov.gradle.jvm.wrapper") version "0.14.0" } @@ -196,6 +197,14 @@ tasks.prepareSandbox { val dllFiles = listOf( "$outputFolder/${DotnetPluginId}.dll", "$outputFolder/${DotnetPluginId}.pdb", + + // Not 100% sure why, but we manually need to include these dependencies for Remodder to work + "$outputFolder/0Harmony.dll", + "$outputFolder/AsmResolver.dll", + "$outputFolder/AsmResolver.DotNet.dll", + "$outputFolder/AsmResolver.PE.dll", + "$outputFolder/AsmResolver.PE.File.dll", + "$outputFolder/ICSharpCode.Decompiler.dll" ) dllFiles.forEach({ f -> @@ -243,4 +252,21 @@ tasks.patchPluginXml { pluginVersion.set(PluginVersion) changeNotes.set("
    \r\n$changelogText\r\n
"); untilBuild.set(provider { null }) +} + +val riderModel: Configuration by configurations.creating { + isCanBeConsumed = true + isCanBeResolved = false +} + +artifacts { + add(riderModel.name, provider { + intellijPlatform.platformPath.resolve("lib/rd/rider-model.jar").also { + check(it.isFile) { + "rider-model.jar is not found at $riderModel" + } + } + }) { + builtBy(Constants.Tasks.INITIALIZE_INTELLIJ_PLATFORM_PLUGIN) + } } \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index d6599b7..a32a40b 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 @@ -25,4 +25,7 @@ rdVersion=2025.1 rdKotlinVersion=2.1.0 intellijPlatformGradlePluginVersion=2.2.1 gradleJvmWrapperVersion=0.14.0 -riderBaseVersion=2025.1 \ No newline at end of file +riderBaseVersion=2025.1 + +# Required to download Rider artifacts from Maven (and not "binary" releases from CDN). +org.jetbrains.intellij.platform.buildFeature.useBinaryReleases=false \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c52f4c0..ec205ef 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin = "2.1.20" # https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library +kotlin = "2.1.10" # https://plugins.jetbrains.com/docs/intellij/using-kotlin.html#kotlin-standard-library rdGen = "2025.1.1" # https://github.com/JetBrains/rd/releases [libraries] diff --git a/protocol/build.gradle.kts b/protocol/build.gradle.kts new file mode 100644 index 0000000..2ee7928 --- /dev/null +++ b/protocol/build.gradle.kts @@ -0,0 +1,52 @@ +import com.jetbrains.rd.generator.gradle.RdGenTask + +plugins { + id("org.jetbrains.kotlin.jvm") + id("com.jetbrains.rdgen") version libs.versions.rdGen +} + +dependencies { + implementation(libs.kotlinStdLib) + implementation(libs.rdGen) + implementation( + project( + mapOf( + "path" to ":", + "configuration" to "riderModel" + ) + ) + ) +} + +val DotnetPluginId: String by rootProject +val RiderPluginId: String by rootProject + +rdgen { + val csOutput = File(rootDir, "src/dotnet/${DotnetPluginId}") + val ktOutput = File(rootDir, "src/rider/main/kotlin/remodder") + + verbose = true + packages = "model.rider" + + generator { + language = "kotlin" + transform = "asis" + root = "com.jetbrains.rider.model.nova.ide.IdeRoot" + namespace = "com.jetbrains.rider.model" + directory = "$ktOutput" + } + + generator { + language = "csharp" + transform = "reversed" + root = "com.jetbrains.rider.model.nova.ide.IdeRoot" + namespace = "JetBrains.Rider.Model" + directory = "$csOutput" + } +} + +tasks.withType { + val classPath = sourceSets["main"].runtimeClasspath + dependsOn(classPath) + classpath(classPath) +} \ No newline at end of file diff --git a/protocol/src/main/kotlin/model/rider/Model.kt b/protocol/src/main/kotlin/model/rider/Model.kt new file mode 100644 index 0000000..877d28d --- /dev/null +++ b/protocol/src/main/kotlin/model/rider/Model.kt @@ -0,0 +1,18 @@ +package model.rider + +import com.jetbrains.rd.generator.nova.Ext +import com.jetbrains.rd.generator.nova.csharp.CSharp50Generator +import com.jetbrains.rd.generator.nova.kotlin.Kotlin11Generator +import com.jetbrains.rd.generator.nova.* +import com.jetbrains.rd.generator.nova.PredefinedType.* +import com.jetbrains.rider.model.nova.ide.SolutionModel + +object RemodderProtocolModel : Ext(SolutionModel.Solution) { + init { + setting(CSharp50Generator.Namespace, "ReSharperPlugin.RdProtocol") + setting(Kotlin11Generator.Namespace, "com.jetbrains.rider.plugins.rdprotocol") + + // Remote procedure on backend + call("decompile", array(string), array(string)).async + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 024b8ab..4a6834b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -35,11 +35,11 @@ pluginManagement { resolutionStrategy { eachPlugin { - when (requested.id.name) { - // This required to correctly rd-gen plugin resolution. May be we should switch our naming to match Gradle plugin naming convention. - "rdgen" -> { - useModule("com.jetbrains.rd:rd-gen:${rdVersion}") - } + // Gradle has to map a plugin dependency to Maven coordinates - '{groupId}:{artifactId}:{version}'. It tries + // to do use '{plugin.id}:{plugin.id}.gradle.plugin:version'. + // This doesn't work for rdgen, so we provide some help + if (requested.id.id == "com.jetbrains.rdgen") { + useModule("com.jetbrains.rd:rd-gen:${requested.version}") } } } @@ -54,4 +54,6 @@ dependencyResolutionManagement { maven("https://cache-redirector.jetbrains.com/dl.bintray.com/kotlin/kotlin-eap") maven("https://cache-redirector.jetbrains.com/myget.org.rd-snapshots.maven") } -} \ No newline at end of file +} + +include(":protocol") \ No newline at end of file diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj index 5088e5a..79c9f7b 100644 --- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj +++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj @@ -1,7 +1,7 @@  - net472 + net6.0 false 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": [ " + + AssemblyDir\System.dll + False + + + AssemblyDir\System.Core.dll + False + + + AssemblyDir\System.Xml.dll + False + Assembly-CSharp.dll False @@ -52,7 +58,7 @@ If that reference does not exist, it will add Krafs Rimworld Ref to the project. If it does exist, Krafs won't be added as a reference. This basically means that Krafs is treated as a fallback if Assembly-CSharp is not found --> - + + 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/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/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