diff --git a/CHANGELOG.md b/CHANGELOG.md
index c338468..6906898 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,13 @@
# Changelog
+## 2023.4
+ * Added support for Custom Def Classes
+ * Implemented XML Def Find Usages, so you can just Right-Click a defName and see all usages of that def in XML
+ * Add a new "Generate" menu feature to add props to your XML
+ * Fixed an issue where some classes (like `ThingDef`) wouldn't resolve correctly, meaning that references weren't made to them
+ * Fixed an issue where some elements in lists weren't resolved correctly
+
+
## 2023.3.3
* Fixed some missed API Changes in Rider 2023.3.3
diff --git a/Find Usages.md b/Find Usages.md
new file mode 100644
index 0000000..934e911
--- /dev/null
+++ b/Find Usages.md
@@ -0,0 +1,45 @@
+## Find Usages
+
+This is a trail of thoughts/possible solutions and references that I went down for Find Usages. They mostly exist to
+document what I went through, and as a reference if I want to try and understand my thought process later down the line
+when I try to expand the Find Usages functionality to finding XML Defs from C# (think DefOf usage)
+
+### Reference Points
+ * `SearchProcessorBase` appears to be where the we can look to see if nodes are being visited
+ * `NamedThingsSearchSourceFileProcessor::GetReferences` is where I think our reference is being filtered out
+ * `FomderSearchDomainVisitor::Add` and `AsyncSearchResult::Add` are where the references are being added to the result set
+ * `CSharpReferenceSearcher::ProcessElement` is where we're seeing some of the C# references being processed?
+
+### Thoughts/Notes
+So far we've been abusing the `ShortName` of our `XmlTagDeclaredElement` to store both the defName and the defType, but
+that appears to be one of the things limiting us from being able to find usages. `ShortName` should probably refer only
+to the `defName` of our def, but trying to do that quickly just results in references not existing, presumably because
+we can't grab them from the symbol table.
+
+In this branch I've done work to actually move over to a singleton Symbol Table, which is good work, however since I think
+it's almost a pre-requisite for the rest of our work, we really need to separate it from this branch and put it into main.
+This branch is getting pretty messy because we're trying to do a whole number of things. Likewise, the ShortName migration
+should probably be done separately as well so that we're not just throwing a bunch of shit against the wall to see what sticks.
+
+I'm not sure we need/want to implement a separate `IFindUsagesContextSearch` ourselves. I think what we want to be looking
+at is possibly just our own `SearcherFactory` and `DomainSearcher`. I think what we've got so far is a start however the
+ShortName issue has caused us to make hacky workarounds to swap the ShortName on demand rather than just construct it properly.
+With that kind of approach, there's no real way to say what we do or do not need. Still, with a propper ShortName we were
+actually hitting `RimworldReferenceProvider::hasReference` for the first time ever. We were visiting nodes, collecting references
+it's just that those references were being filtered out at some point, so we need to investigate that.
+
+One problem though is that we're still trying to do a "Find Usages" on an actual usage of the def. If we try to do it on the
+defName itself, we don't get what we need. Is that because we've attached the declaredElement to the xmlTag instead of the
+string? No clue. Could be something else. I think once we get the ShortName sorted and the SymbolTable sorted, we can either
+reference the Unity/YAML plugin to see what they've got or we can post to slack for help. Once we've cleaned up that is.
+
+-----
+
+It looks like we may be looking in the wrong place. We implemented a SearchFactory for LateBoundReferences, but we're
+not serving up a LateBoundReference (I think), we're serving up a regular reference.
+
+-----
+
+That was definitely the wrong place. Implementing a DeclaredElementReferenceSearcher works well, but we need to do a
+`SwapName` on the `RimworldXmlDefReference` object in the middle of the call inside Rider, which obviously isn't going to
+work for us. This just goes back to the idea of having to rework the ShortName to be the defName and not the defType.
\ No newline at end of file
diff --git a/README.md b/README.md
index b3a214f..cc3ef91 100644
--- a/README.md
+++ b/README.md
@@ -21,21 +21,9 @@ into the definitions on which the XML sits.
* Rimworld Mod support in New Solution
* Custom Rimworld XML Projects
* Basic validation for some XML Values
-
-## Roadmap
-
-The major feature for the next version of this plugin (Version 1.1) is custom error reporting for your XML. For example
-if you try to use `NorthSouth` it will point out that's an invalid option. Likewise, if you pass in
-`2.5` it will tell you that you need to pass in a range of numbers, not a single number.
-
-Additionally, here are some other features I intend to include at some point. These features are not slated for the 1.1
-release and are a list of things to do "at some point in time". In no particular order, they are:
-
- * Autocompleting and referencing classes in `Class=""` attributes in XML Tags
- * Autocompleting and providing references when referring to C# classes in XML Values like in `` or ``
- * Handle Def tags with custom classes instead of just Rimworld classes (such as ``)
- * Supporting Patches
-
+ * Support for Custom Def Classes (Such as ``)
+ * The ability to perform Find Usages on XML Defs
+ * A "Generate" menu option to generate properties for a given XML Def
## Quick Architecture For Developers
diff --git a/TODO.md b/TODO.md
index 6500698..8145c97 100644
--- a/TODO.md
+++ b/TODO.md
@@ -3,8 +3,6 @@
* General features:
* Support LookupItems for Class="" attributes
* Support LookupItems for things like or
- * If we know a bit of XML should be an enum like Gender, we should be able to check that it's a valid enum value
- * Handle Defs with custom classes instead of Rimworld classes
* We need to be able to support "LoadAlias", such as "StorageSettings.priority"
* \ handling
@@ -12,8 +10,6 @@
* XML Autocomplete
* Make auto completing to other XMLTags look nicer and work faster
- * When linking to other def (`DefNameHere`) also include defs where the tag is a custom class
- that extends from the def we're looking for
`
* Documentation
* Re-read and document References.RimworldXmlReference
@@ -27,4 +23,7 @@
* Investigate the possibility of tying a version number to our types in the SymbolScope so that we can don't cross reference between versions
* Right now we only try to load the latest version of the XML, since we aren't built for loading multiple copies of the same def
- * Refactor all reading of `About.xml` into a single class that holds that information for that file
\ No newline at end of file
+ * Refactor all reading of `About.xml` into a single class that holds that information for that file
+ * Look at refactoring RimworldSymbolScope to see if we can make it cleaner
+ * Refactor the fetching of classes to a central location. We're all over the place, using different scopes from `ScopeHelper`
+ when we really should just let `ScopeHelper` deal with it all internally as well as the scope swapping
\ No newline at end of file
diff --git a/buildPlugin.ps1 b/buildPlugin.ps1
index 1229a42..77156c2 100644
--- a/buildPlugin.ps1
+++ b/buildPlugin.ps1
@@ -1,5 +1,5 @@
Param(
- $Version = "2023.3.2"
+ $Version = "2023.4"
)
Set-StrictMode -Version Latest
diff --git a/gradle.properties b/gradle.properties
index 89d0680..fac652b 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=2023.3.3
+PluginVersion=2023.4
BuildConfiguration=Release
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj
index 7b8e73e..2dae672 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj
+++ b/src/dotnet/ReSharperPlugin.RimworldDev.Tests/ReSharperPlugin.RimworldDev.Tests.csproj
@@ -6,7 +6,8 @@
-
+
+ all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageChecker.cs b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageChecker.cs
new file mode 100644
index 0000000..52fada5
--- /dev/null
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageChecker.cs
@@ -0,0 +1,103 @@
+// using System.Collections.Generic;
+// using System.Linq;
+// using JetBrains.Annotations;
+// using JetBrains.Application.Threading;
+// using JetBrains.Lifetimes;
+// using JetBrains.ReSharper.Psi;
+// using JetBrains.ReSharper.Psi.Caches;
+// using JetBrains.ReSharper.Psi.Files;
+// using JetBrains.ReSharper.Psi.Xml.Tree;
+// using JetBrains.Util;
+//
+// namespace ReSharperPlugin.RimworldDev.DevTools;
+//
+// [PsiComponent]
+// public class PropertyUsageChecker : SimpleICache>
+// {
+// private static readonly Dictionary UsageCount = new();
+//
+// public static string GetUsageOutput(int minUsages = 100)
+// {
+// var usages = UsageCount;
+// var itemsToOutput = (from entry in usages orderby entry.Value descending select entry).ToList()
+// .Where(item => item.Value > minUsages)
+// .Select(usage => $"{{\"{usage.Key}\", {usage.Value}}}")
+// .ToList();
+//
+// return string.Join(",\r\n", itemsToOutput);
+// }
+//
+// public PropertyUsageChecker
+// (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager,
+// long? version = null)
+// : base(lifetime, locks, persistentIndexManager, PropertyUsageItem.Marshaller, version)
+// {
+// }
+//
+// protected override bool IsApplicable(IPsiSourceFile sourceFile)
+// {
+// return base.IsApplicable(sourceFile) && sourceFile.LanguageType.Name == "XML";
+// }
+//
+// public override object Build(IPsiSourceFile sourceFile, bool isStartup)
+// {
+// ScopeHelper.UpdateScopes(sourceFile.GetSolution());
+//
+// if (!IsApplicable(sourceFile))
+// return null;
+//
+// if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return null;
+//
+// List usages = new();
+//
+// var tags = xmlFile.GetNestedTags("Defs/*").Where(tag =>
+// {
+// var defNameTag = tag.GetNestedTags("defName").FirstOrDefault();
+// return defNameTag is not null;
+// });
+//
+// foreach (var tag in tags)
+// {
+// BuildFromTag(tag, false);
+// }
+//
+// return usages;
+// }
+//
+// private void BuildFromTag(IXmlTag tag, bool isNested = true)
+// {
+// if (isNested)
+// {
+// var hierachy = RimworldXMLItemProvider.GetHierarchy(tag);
+// var fieldName = hierachy.Pop();
+//
+// var context =
+// RimworldXMLItemProvider.GetContextFromHierachy(hierachy, ScopeHelper.RimworldScope,
+// ScopeHelper.AllScopes);
+//
+//
+// if (context is not null)
+// {
+// var fields = RimworldXMLItemProvider.GetAllPublicFields(context,
+// ScopeHelper.GetScopeForClass(context.GetClrName().FullName));
+// var field = fields.FirstOrDefault(field => field.ShortName == fieldName);
+//
+// if (field is not null)
+// {
+// var className = field.GetContainingType().GetClrName().FullName;
+// var fullName = $"{className}::{field.ShortName}";
+//
+// UsageCount.TryAdd(fullName, 0);
+// UsageCount[fullName]++;
+// }
+// }
+// }
+//
+// var nested = tag.GetNestedTags("*");
+//
+// foreach (var nestedTag in nested)
+// {
+// BuildFromTag(nestedTag);
+// }
+// }
+// }
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageItem.cs b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageItem.cs
new file mode 100644
index 0000000..50ed45e
--- /dev/null
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/DevTools/PropertyUsageItem.cs
@@ -0,0 +1,30 @@
+using System.Collections.Generic;
+using JetBrains.Serialization;
+using JetBrains.Util.PersistentMap;
+
+namespace ReSharperPlugin.RimworldDev.DevTools;
+
+public class PropertyUsageItem
+{
+ public readonly string FullName;
+
+ public PropertyUsageItem(string fullName)
+ {
+ FullName = fullName;
+ }
+
+ public static readonly IUnsafeMarshaller> Marshaller =
+ UnsafeMarshallers.GetCollectionMarshaller(new UniversalMarshaller(Read, Write), _ => new List());
+
+ private static PropertyUsageItem Read(UnsafeReader reader)
+ {
+ var fullName = reader.ReadString();
+
+ return new PropertyUsageItem(fullName);
+ }
+
+ private static void Write(UnsafeWriter writer, PropertyUsageItem value)
+ {
+ writer.Write(value.FullName);
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Generator/DefGenerator.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/DefGenerator.cs
new file mode 100644
index 0000000..4185279
--- /dev/null
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/DefGenerator.cs
@@ -0,0 +1,161 @@
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Application.DataContext;
+using JetBrains.ProjectModel.DataContext;
+using JetBrains.ReSharper.Feature.Services.Generate;
+using JetBrains.ReSharper.Feature.Services.Generate.Actions;
+using JetBrains.ReSharper.Feature.Services.Generate.Workflows;
+using JetBrains.ReSharper.Psi;
+using JetBrains.ReSharper.Psi.DataContext;
+using JetBrains.ReSharper.Psi.ExtensionsAPI.Tree;
+using JetBrains.ReSharper.Psi.Tree;
+using JetBrains.ReSharper.Psi.Xml;
+using JetBrains.ReSharper.Psi.Xml.Impl.Tree;
+using JetBrains.ReSharper.Psi.Xml.Parsing;
+using JetBrains.ReSharper.Psi.Xml.Resources;
+using JetBrains.ReSharper.Psi.Xml.Tree;
+
+namespace ReSharperPlugin.RimworldDev.Generator;
+
+[GenerateProvider]
+public class DefPropertyGeneratorItemProvider : IGenerateWorkflowProvider
+{
+ public IEnumerable CreateWorkflow(IDataContext dataContext)
+ {
+ yield return new GenerateDefPropertiesWorkflowXml();
+ }
+}
+
+public class GenerateDefPropertiesWorkflowXml : GenerateCodeWorkflowBase
+{
+ public GenerateDefPropertiesWorkflowXml()
+ : base(kind: "RimworldPropertyGenerator",
+ icon: PsiXmlThemedIcons.XmlNode.Id,
+ title: "&Property Generator",
+ actionGroup: GenerateActionGroup.CLR_LANGUAGE,
+ windowTitle: "Def Properties",
+ description: "Add Properties to your Def",
+ actionId: "Generate.DefProperties")
+ {
+ }
+
+ public override double Order => 100;
+
+ public override bool IsAvailable(IDataContext dataContext)
+ {
+ var solution = dataContext.GetData(ProjectModelDataConstants.SOLUTION);
+ if (solution == null)
+ return false;
+
+ var sourceFile = dataContext.GetData(PsiDataConstants.SOURCE_FILE);
+ if (sourceFile == null)
+ return false;
+
+ if (!sourceFile.PrimaryPsiLanguage.IsLanguage(XmlLanguage.Instance)) return false;
+
+ return true;
+ }
+ public override bool IsEnabled(ITreeNode context)
+ {
+ return true;
+ }
+
+ public override bool IsEmptyInputAllowed(IGeneratorContext context)
+ {
+ return true;
+ }
+}
+
+[GeneratorBuilder("RimworldPropertyGenerator", typeof(XmlLanguage))]
+public class DefPropertiesGeneratorBuilderXml : GeneratorBuilderBase
+{
+ protected override bool IsAvailable(GeneratorContextBase context)
+ {
+ var anchor = context.Anchor;
+
+ if (anchor is not XmlWhitespaceToken) return false;
+ if (anchor.Parent is XmlTagHeaderNode) return false;
+ if (anchor.Parent?.Parent is null or IXmlFile) return false;
+
+ return true;
+ }
+
+ protected override bool HasProcessableElements(GeneratorContextBase context,
+ IEnumerable elements)
+ {
+ return true;
+ }
+
+ protected override void Process(GeneratorContextBase context)
+ {
+ var anchor = context.Anchor;
+ if (anchor is not ITreeNode) return;
+ if (anchor?.Parent is not XmlTag parentTag) return;
+
+ var factory = XmlElementFactory.GetInstance(context.Anchor);
+
+ foreach (var inputElement in context.InputElements)
+ {
+ if (inputElement is not GeneratorDeclaredElement declaredElement) continue;
+ var name = declaredElement.DeclaredElement.ShortName;
+ var newTag = factory.CreateTagForTag(parentTag, $"<{name}>{name}>");
+
+ anchor = ModificationUtil.AddChildAfter(anchor, newTag);
+
+ context.OutputElements.Add(inputElement);
+ }
+ }
+}
+
+[GeneratorElementProvider("RimworldPropertyGenerator", typeof(XmlLanguage))]
+public class DefGeneratorElementProvider : GeneratorProviderBase
+{
+ public override void Populate(GeneratorContextBase context)
+ {
+ var anchor = context.Anchor;
+
+ if (ScopeHelper.RimworldScope is null) return;
+ if (anchor is not XmlWhitespaceToken) return;
+ if (anchor.Parent is XmlTagHeaderNode) return;
+ if (anchor.Parent?.Parent is null or IXmlFile) return;
+
+ var hierarchy = RimworldXMLItemProvider.GetHierarchy(context.Anchor);
+ var currentClass =
+ RimworldXMLItemProvider.GetContextFromHierachy(hierarchy, ScopeHelper.RimworldScope, ScopeHelper.AllScopes);
+
+ var fields = RimworldXMLItemProvider.GetAllPublicFields(currentClass, ScopeHelper.RimworldScope);
+
+ var alreadyDefinedTags = anchor.Parent
+ .Children()
+ .Where(child => child is XmlTag)
+ .Select(tag => (tag.Children()
+ .First(child => child is XmlTagHeaderNode) as XmlTagHeaderNode)?
+ .ContainerName
+ )
+ .Where(name => name is not null)
+ .ToList();
+
+ var publicFields = fields.Where(
+ field =>
+ field.IsField
+ && field.AccessibilityDomain.DomainType == AccessibilityDomain.AccessibilityDomainType.PUBLIC
+ && !alreadyDefinedTags.Contains(field.ShortName)
+ && !field.GetAttributeInstances(AttributesSource.All)
+ .Select(attribute => attribute.GetAttributeShortName()).Contains("UnsavedAttribute")
+ ).ToList().OrderByDescending(field =>
+ {
+ var className = field.ContainingType?.GetClrName().FullName;
+ if (className is null) return 0;
+
+ var fullName = $"{className}::{field.ShortName}";
+
+ if (!PropertyOrdering.PropertyUsageCount.ContainsKey(fullName)) return 0;
+ return PropertyOrdering.PropertyUsageCount[fullName];
+ });
+
+ foreach (var field in publicFields)
+ {
+ context.ProvidedElements.Add(new GeneratorDeclaredElement(field));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Generator/PropertyOrdering.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/PropertyOrdering.cs
new file mode 100644
index 0000000..1bf3812
--- /dev/null
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/PropertyOrdering.cs
@@ -0,0 +1,521 @@
+using System.Collections.Generic;
+
+namespace ReSharperPlugin.RimworldDev.Generator;
+
+public class PropertyOrdering
+{
+ public static Dictionary PropertyUsageCount = new()
+ {
+ { "Verse.Def::defName", 9979 },
+ { "Verse.Def::label", 4765 },
+ { "Verse.Def::description", 2720 },
+ { "Verse.GraphicData::texPath", 1910 },
+ { "Verse.GraphicData::drawSize", 1186 },
+ { "RimWorld.ThoughtStage::label", 1160 },
+ { "Verse.ThingDef::graphicData", 1146 },
+ { "Verse.GraphicData::graphicClass", 1078 },
+ { "RimWorld.ThoughtStage::description", 977 },
+ { "RimWorld.ThoughtStage::baseMoodEffect", 969 },
+ { "Verse.BuildableDef::statBases", 956 },
+ { "Verse.Sound.SubSoundDef::grains", 917 },
+ { "Verse.SoundDef::subSounds", 904 },
+ { "Verse.Sound.SubSoundDef::volumeRange", 891 },
+ { "RimWorld.BackstoryDef::titleShort", 829 },
+ { "RimWorld.BackstoryDef::baseDesc", 829 },
+ { "RimWorld.BackstoryDef::title", 828 },
+ { "RimWorld.BackstoryDef::slot", 827 },
+ { "RimWorld.ThoughtDef::stages", 824 },
+ { "RimWorld.BackstoryDef::skillGains", 820 },
+ { "Verse.SoundDef::context", 770 },
+ { "RimWorld.BackstoryDef::requiredWorkTags", 747 },
+ { "Verse.BodyPartRecord::def", 722 },
+ { "Verse.BodyPartRecord::coverage", 694 },
+ { "RimWorld.BackstoryDef::workDisables", 685 },
+ { "Verse.Def::ignoreIllegalLabelCharacterConfigError", 605 },
+ { "RimWorld.BackstoryDef::shuffleable", 603 },
+ { "RimWorld.BackstoryDef::identifier", 602 },
+ { "Verse.GeneSymbolPack+WeightedSymbol::symbol", 584 },
+ { "Verse.Sound.AudioGrain_Folder::clipFolderPath", 569 },
+ { "RimWorld.BackstoryDef::spawnCategories", 536 },
+ { "Verse.GraphicData::shadowData", 523 },
+ { "Verse.ShadowData::volume", 522 },
+ { "Verse.SoundDef::maxSimultaneous", 492 },
+ { "Verse.Sound.SubSoundDef::pitchRange", 474 },
+ { "RimWorld.BackstoryDef::bodyTypeMale", 465 },
+ { "RimWorld.BackstoryDef::bodyTypeFemale", 465 },
+ { "Verse.BodyPartRecord::customLabel", 436 },
+ { "RimWorld.ThoughtDef::durationDays", 424 },
+ { "RimWorld.PreceptComp_Thought::thought", 418 },
+ { "Verse.AI.ThinkNode::subNodes", 403 },
+ { "Verse.Sound.SubSoundDef::distRange", 401 },
+ { "Verse.GraphicData::shaderType", 399 },
+ { "Verse.ThingDef::comps", 380 },
+ { "Verse.SubEffecterDef::subEffecterClass", 374 },
+ { "RimWorld.ThoughtDef::thoughtClass", 373 },
+ { "Verse.BuildableDef::costList", 334 },
+ { "Verse.Sound.AudioGrain_Clip::clipPath", 334 },
+ { "Verse.Def::descriptionHyperlinks", 330 },
+ { "Verse.Tool::capacities", 327 },
+ { "Verse.Tool::cooldownTime", 327 },
+ { "RimWorld.ThoughtDef::stackLimit", 326 },
+ { "Verse.Tool::power", 319 },
+ { "Verse.BuildableDef::altitudeLayer", 310 },
+ { "Verse.Sound.SubSoundDef::sustainLoop", 302 },
+ { "Verse.PawnCapacityModifier::capacity", 299 },
+ { "RimWorld.BackstoryDef::bodyTypeGlobal", 298 },
+ { "Verse.ThingDef::size", 296 },
+ { "Verse.GraphicData::color", 293 },
+ { "Verse.Tool::label", 291 },
+ { "RimWorld.ThoughtDef::workerClass", 289 },
+ { "Verse.ThingDef::thingClass", 270 },
+ { "Verse.ThingDef::fillPercent", 262 },
+ { "Verse.BuildableDef::pathCost", 261 },
+ { "RimWorld.StockGenerator::countRange", 260 },
+ { "Verse.JobDef::driverClass", 256 },
+ { "Verse.PawnKindLifeStage::bodyGraphicData", 251 },
+ { "Verse.SubEffecterDef::spawnLocType", 250 },
+ { "Verse.ShadowData::offset", 250 },
+ { "Verse.BuildableDef::passability", 249 },
+ { "Verse.JobDef::reportString", 242 },
+ { "Verse.ThingDef::building", 241 },
+ { "Verse.SoundDef::priorityMode", 239 },
+ { "Verse.BodyPartRecord::depth", 237 },
+ { "Verse.Tool::linkedBodyPartsGroup", 214 },
+ { "Verse.SubEffecterDef::scale", 211 },
+ { "Verse.PawnKindLifeStage::dessicatedBodyGraphicData", 209 },
+ { "Verse.LifeStageAge::def", 207 },
+ { "Verse.BodyPartRecord::parts", 207 },
+ { "Verse.PawnCapacityModifier::offset", 200 },
+ { "RimWorld.RitualRoleBehavior::roleId", 199 },
+ { "RimWorld.RitualRoleBehavior::dutyDef", 199 },
+ { "Verse.Sound.SubSoundDef::muteWhenPaused", 199 },
+ { "RimWorld.ThoughtStage::baseOpinionOffset", 190 },
+ { "Verse.SubEffecterDef::fleckDef", 189 },
+ { "Verse.IngredientCount::filter", 188 },
+ { "Verse.LifeStageAge::minAge", 185 },
+ { "Verse.ThingDef::recipeMaker", 184 },
+ { "Verse.BodyPartRecord::groups", 183 },
+ { "Verse.SoundDef::sustain", 182 },
+ { "RimWorld.PreceptComp_KnowsMemoryThought::eventDef", 179 },
+ { "Verse.HediffDef::stages", 177 },
+ { "RimWorld.PreceptDef::issue", 176 },
+ { "RimWorld.StatDef::displayPriorityInCategory", 176 },
+ { "Verse.BuildableDef::costStuffCount", 174 },
+ { "RimWorld.StatDef::defaultBaseValue", 173 },
+ { "Verse.EffecterDef::children", 171 },
+ { "RimWorld.PreceptDef::impact", 170 },
+ { "Verse.ThingDef::rotatable", 167 },
+ { "Verse.CompProperties::compClass", 167 },
+ { "RimWorld.StatDef::category", 166 },
+ { "Verse.HediffStage::capMods", 165 },
+ { "Verse.RecipeDef::jobString", 165 },
+ { "RimWorld.StatDef::toStringStyle", 162 },
+ { "Verse.ThingDef::tickerType", 158 },
+ { "Verse.FleckDef::graphicData", 156 },
+ { "Verse.HediffStage::label", 155 },
+ { "RimWorld.StatDef::minValue", 155 },
+ { "Verse.BuildableDef::stuffCategories", 153 },
+ { "Verse.BodyPartRecord::height", 153 },
+ { "Verse.HediffDef::comps", 152 },
+ { "Verse.ThingDef::thingCategories", 152 },
+ { "Verse.SubEffecterDef::positionRadius", 151 },
+ { "Verse.HediffStage::minSeverity", 151 },
+ { "Verse.FleckDef::fadeOutTime", 149 },
+ { "Verse.FleckDef::solidTime", 149 },
+ { "Verse.Sound.SubSoundDef::onCamera", 149 },
+ { "Verse.FleckDef::altitudeLayer", 147 },
+ { "RimWorld.ThoughtDef::nullifyingTraits", 147 },
+ { "Verse.SubEffecterDef::burstCount", 146 },
+ { "Verse.JobDef::allowOpportunisticPrefix", 142 },
+ { "RimWorld.BackstoryDef::forcedTraits", 141 },
+ { "Verse.RecipeDef::ingredients", 139 },
+ { "RimWorld.PreceptDef::displayOrderInIssue", 138 },
+ { "RimWorld.PreceptDef::comps", 136 },
+ { "RimWorld.ThoughtDef::validWhileDespawned", 135 },
+ { "Verse.GraphicData::damageData", 135 },
+ { "Verse.RecipeMakerProperties::researchPrerequisite", 133 },
+ { "Verse.VerbProperties::warmupTime", 132 },
+ { "RimWorld.StyleItemDef::texPath", 132 },
+ { "Verse.PawnKindDef::combatPower", 132 },
+ { "RimWorld.IdeoSymbolPack::adjective", 132 },
+ { "RimWorld.IdeoSymbolPack::member", 132 },
+ { "RimWorld.WorkGiverDef::giverClass", 132 },
+ { "Verse.ThingDef::tradeTags", 130 },
+ { "Verse.GeneDef::displayOrderInCategory", 130 },
+ { "Verse.VerbProperties::range", 129 },
+ { "Verse.RulePackDef::include", 129 },
+ { "RimWorld.RitualStage::defaultDuty", 129 },
+ { "RimWorld.RitualStage::endTriggers", 129 },
+ { "Verse.BuildableDef::designationCategory", 129 },
+ { "RimWorld.WorkGiverDef::verb", 126 },
+ { "RimWorld.WorkGiverDef::gerund", 126 },
+ { "RimWorld.BodyTypeDef+WoundAnchor::rotation", 126 },
+ { "RimWorld.BodyTypeDef+WoundAnchor::offset", 126 },
+ { "RimWorld.BodyTypeDef+WoundAnchor::range", 126 },
+ { "RimWorld.BodyTypeDef+WoundAnchor::debugColor", 126 },
+ { "RimWorld.WorkGiverDef::workType", 125 },
+ { "Verse.GeneDef::iconPath", 125 },
+ { "RimWorld.StyleItemDef::styleTags", 124 },
+ { "Verse.PawnKindDef::race", 124 },
+ { "RimWorld.WorkGiverDef::priorityInType", 124 },
+ { "RimWorld.IdeoSymbolPack::ideoName", 122 },
+ { "RimWorld.WorkGiverDef::requiredCapacities", 119 },
+ { "Verse.ThingDef::mote", 118 },
+ { "Verse.MoteProperties::solidTime", 118 },
+ { "Verse.ThingDef::tools", 118 },
+ { "Verse.RecipeDef::fixedIngredientFilter", 117 },
+ { "RimWorld.ThoughtDef::developmentalStageFilter", 117 },
+ { "Verse.BuildableDef::researchPrerequisites", 117 },
+ { "Verse.HediffDef::labelNoun", 116 },
+ { "Verse.SubEffecterDef::speed", 116 },
+ { "Verse.ThingDef::uiIconScale", 115 },
+ { "RimWorld.PreceptDef::displayOrderInImpact", 115 },
+ { "Verse.FleckDef::fadeInTime", 114 },
+ { "RimWorld.ThoughtDef::stackLimitForSameOtherPawn", 114 },
+ { "Verse.ResearchProjectDef::researchViewX", 113 },
+ { "Verse.ResearchProjectDef::researchViewY", 113 },
+ { "Verse.BuildableDef::uiOrder", 112 },
+ { "RimWorld.PreceptComp::description", 112 },
+ { "Verse.LifeStageAge::soundWounded", 111 },
+ { "Verse.BuildableDef::terrainAffordanceNeeded", 110 },
+ { "Verse.RecipeMakerProperties::displayPriority", 110 },
+ { "Verse.LifeStageAge::soundCall", 110 },
+ { "RimWorld.QuestScriptDef::root", 108 },
+ { "RimWorld.RuleDef::symbol", 108 },
+ { "RimWorld.RuleDef::resolvers", 108 },
+ { "RimWorld.ColorDef::color", 106 },
+ { "RimWorld.ThoughtDef::doNotApplyToQuestLodgers", 105 },
+ { "Verse.ThingDef::race", 105 },
+ { "Verse.PawnKindDef::lifeStages", 105 },
+ { "Verse.BuildableDef::placeWorkers", 104 },
+ { "Verse.ThingStyleDef::graphicData", 103 },
+ { "Verse.ResearchProjectDef::baseCost", 102 },
+ { "Verse.GeneDef::symbolPack", 102 },
+ { "Verse.VerbProperties::verbClass", 101 },
+ { "Verse.GeneSymbolPack::prefixSymbols", 101 },
+ { "Verse.MoteProperties::fadeInTime", 100 },
+ { "Verse.VerbProperties::targetParams", 100 },
+ { "Verse.ThingDef::apparel", 100 },
+ { "Verse.MoteProperties::fadeOutTime", 99 },
+ { "Verse.ThingDef::thingSetMakerTags", 99 },
+ { "Verse.ResearchProjectDef::techLevel", 99 },
+ { "Verse.ResearchProjectDef::prerequisites", 99 },
+ { "RimWorld.RitualStage::roleBehaviors", 99 },
+ { "Verse.PawnKindDef::initialResistanceRange", 98 },
+ { "Verse.BuildableDef::designationHotKey", 98 },
+ { "Verse.BodyPartDef::hitPoints", 98 },
+ { "RimWorld.BodyTypeDef+WoundAnchor::tag", 98 },
+ { "RimWorld.BiomeDiseaseRecord::diseaseInc", 98 },
+ { "RimWorld.BiomeDiseaseRecord::commonality", 98 },
+ { "Verse.GraphicData::shaderParameters", 97 },
+ { "Verse.SubEffecterDef::moteDef", 97 },
+ { "RimWorld.IdeoIconDef::iconPath", 94 },
+ { "RimWorld.PreceptComp_SelfTookMemoryThought::eventDef", 94 },
+ { "RimWorld.StyleItemDef::styleGender", 92 },
+ { "Verse.Tool::chanceFactor", 92 },
+ { "Verse.HediffDef::hediffClass", 91 },
+ { "Verse.RaceProperties::lifeStageAges", 90 },
+ { "RimWorld.TaleDef::taleClass", 89 },
+ { "RimWorld.TaleDef::type", 89 },
+ { "Verse.LifeStageAge::soundDeath", 89 },
+ { "Verse.SubEffecterDef::soundDef", 88 },
+ { "RimWorld.ScenPart::def", 88 },
+ { "RimWorld.ApparelProperties::wornGraphicPath", 86 },
+ { "RimWorld.PreceptDef::associatedMemes", 86 },
+ { "Verse.DamageGraphicData::rect", 85 },
+ { "Verse.BuildableDef::constructionSkillPrerequisite", 85 },
+ { "RimWorld.AbilityDef::iconPath", 84 },
+ { "Verse.GeneDef::biostatMet", 84 },
+ { "RimWorld.AbilityDef::comps", 83 },
+ { "RimWorld.TaleDef::baseInterest", 83 },
+ { "RimWorld.TaleDef::rulePack", 83 },
+ { "Verse.ThingDef::drawerType", 83 },
+ { "Verse.AI.DutyDef::thinkNode", 81 },
+ { "Verse.PawnKindDef::weaponTags", 81 },
+ { "RimWorld.QuestScriptDef::questDescriptionRules", 81 },
+ { "Verse.HediffStage::statOffsets", 80 },
+ { "RimWorld.StageEndTrigger_DurationPercentage::percentage", 80 },
+ { "RimWorld.IdeoSymbolPack::theme", 80 },
+ { "Verse.SoundDef::maxVoices", 80 },
+ { "RimWorld.AbilityDef::verbProperties", 79 },
+ { "Verse.GenStepDef::genStep", 79 },
+ { "Verse.ShaderTypeDef::shaderPath", 79 },
+ { "Verse.ThingDef::techLevel", 79 },
+ { "Verse.ThingDef::ingestible", 79 },
+ { "Verse.ThingDef::possessionCount", 78 },
+ { "Verse.ThingDef::killedLeavings", 78 },
+ { "Verse.ThingDef::plant", 78 },
+ { "Verse.GenStepDef::order", 77 },
+ { "Verse.RaceProperties::headPosPerRotation", 76 },
+ { "RimWorld.ApparelProperties::bodyPartGroups", 75 },
+ { "Verse.ThingDef::castEdgeShadows", 75 },
+ { "Verse.ThingDef::selectable", 74 },
+ { "Verse.ThingDef::minifiedDef", 74 },
+ { "RimWorld.ThoughtDef::stackedEffectMultiplier", 74 },
+ { "Verse.Sound.SubSoundDef::repeatMode", 74 },
+ { "RimWorld.PlantProperties::growDays", 73 },
+ { "RimWorld.IdeoSymbolPartDef::memes", 73 },
+ { "Verse.RecipeDef::addsHediff", 72 },
+ { "RimWorld.BodyTypeDef+WoundAnchor::canMirror", 70 },
+ { "Verse.HediffStage::painOffset", 69 },
+ { "Verse.RecipeDef::appliedOnFixedBodyParts", 69 },
+ { "Verse.SubEffecterDef::ticksBetweenMotes", 69 },
+ { "Verse.SubEffecterDef::chancePerTick", 69 },
+ { "RimWorld.PawnGroupMaker::kindDef", 68 },
+ { "Verse.ThingDef::interactionCellOffset", 68 },
+ { "RimWorld.ApparelProperties::layers", 68 },
+ { "RimWorld.PawnCapacityFactor::capacity", 68 },
+ { "RimWorld.PawnCapacityFactor::weight", 68 },
+ { "RimWorld.RecordDef::type", 68 },
+ { "Verse.ThingCategoryDef::parent", 68 },
+ { "Verse.HediffDef::spawnThingOnRemoved", 67 },
+ { "Verse.SubEffecterDef::angle", 67 },
+ { "Verse.VerbProperties::soundCast", 67 },
+ { "RimWorld.TraitDegreeData::label", 67 },
+ { "RimWorld.TraitDegreeData::description", 67 },
+ { "RimWorld.QuestScriptDef::questNameRules", 66 },
+ { "Verse.ThingDef::useHitPoints", 66 },
+ { "Verse.RaceProperties::baseHealthScale", 66 },
+ { "RimWorld.BackstoryDef::possessions", 65 },
+ { "RimWorld.BackstoryThingDefCountClass::key", 65 },
+ { "RimWorld.BackstoryThingDefCountClass::value", 65 },
+ { "Verse.Grammar.RulePack::include", 65 },
+ { "Verse.Tool::ensureLinkedBodyPartsGroupAlwaysUsable", 65 },
+ { "RimWorld.PawnGroupMaker::options", 64 },
+ { "Verse.GeneDef::labelShortAdj", 63 },
+ { "RimWorld.ColorDef::displayOrder", 63 },
+ { "RimWorld.InteractionDef::logRulesInitiator", 62 },
+ { "RimWorld.RitualRole::id", 62 },
+ { "RimWorld.RitualRole::maxCount", 62 },
+ { "Verse.Sound.SubSoundDef::sustainIntervalRange", 62 },
+ { "Verse.PawnKindDef::initialWillRange", 61 },
+ { "RimWorld.BuildingProperties::paintable", 61 },
+ { "RimWorld.AbilityDef::hotKey", 60 },
+ { "Verse.RaceProperties::baseBodySize", 60 },
+ { "Verse.HediffDef::isBad", 59 },
+ { "Verse.PawnKindDef::apparelMoney", 59 },
+ { "Verse.JobDef::casualInterruptible", 59 },
+ { "RimWorld.IncidentDef::category", 58 },
+ { "RimWorld.IncidentDef::targetTags", 58 },
+ { "RimWorld.IncidentDef::workerClass", 58 },
+ { "Verse.PawnCapacityModifier::postFactor", 58 },
+ { "RimWorld.StatDef::showIfUndefined", 58 },
+ { "RimWorld.BuildingProperties::destroySound", 58 },
+ { "Verse.RaceProperties::body", 58 },
+ { "RimWorld.RitualBehaviorDef::roles", 57 },
+ { "RimWorld.RitualBehaviorDef::stages", 57 },
+ { "RimWorld.PlantProperties::visualSizeRange", 57 },
+ { "RimWorld.IssueDef::iconPath", 57 },
+ { "Verse.BodyPartDef::tags", 57 },
+ { "RimWorld.TerrainThreshold::terrain", 57 },
+ { "RimWorld.TerrainThreshold::min", 57 },
+ { "RimWorld.TerrainThreshold::max", 57 },
+ { "RimWorld.AbilityDef::statBases", 56 },
+ { "RimWorld.IncidentDef::baseChance", 56 },
+ { "RimWorld.BaseGen.SymbolResolver::minRectSize", 56 },
+ { "Verse.RecipeMakerProperties::skillRequirements", 56 },
+ { "Verse.VerbProperties::hasStandardCommand", 56 },
+ { "RimWorld.StockGenerator_BuyTradeTag::tag", 56 },
+ { "RimWorld.PreceptComp_UnwillingToDo::eventDef", 56 },
+ { "RimWorld.BodyTypeDef+WoundAnchor::layer", 56 },
+ { "Verse.LifeStageAge::soundAngry", 56 },
+ { "RimWorld.RitualRole::required", 55 },
+ { "Verse.ThingDef::hasInteractionCell", 55 },
+ { "Verse.KeyBindingDef::defaultKeyCodeA", 55 },
+ { "Verse.PawnKindDef::techHediffsMoney", 54 },
+ { "RimWorld.StatDef::hideAtValue", 54 },
+ { "Verse.ThingDef::equippedStatOffsets", 54 },
+ { "RimWorld.RitualRole::countsAsParticipant", 54 },
+ { "Verse.BodyPartDef::permanentInjuryChanceFactor", 54 },
+ { "RimWorld.StatDef::parts", 53 },
+ { "Verse.ThingDef::projectile", 53 },
+ { "RimWorld.StageFailTrigger::desc", 53 },
+ { "RimWorld.PlantProperties::harvestYield", 53 },
+ { "Verse.RecipeDef::products", 53 },
+ { "Verse.RaceProperties::lifeExpectancy", 53 },
+ { "Verse.ThingDef::colorGenerator", 52 },
+ { "Verse.ThingDef::weaponTags", 52 },
+ { "RimWorld.IdeoSymbolPack::prefix", 52 },
+ { "Verse.BodyPartDef::bleedRate", 52 },
+ { "Verse.GeneSymbolPack::suffixSymbols", 52 },
+ { "Verse.RaceProperties::soundMeleeHitPawn", 52 },
+ { "Verse.RaceProperties::soundMeleeHitBuilding", 52 },
+ { "Verse.RaceProperties::soundMeleeMiss", 52 },
+ { "Verse.RaceProperties::wildness", 52 },
+ { "RimWorld.TargetingParameters::canTargetAnimals", 51 },
+ { "RimWorld.TargetingParameters::canTargetLocations", 51 },
+ { "RimWorld.PawnGroupMaker::commonality", 51 },
+ { "Verse.ProjectileProperties::speed", 51 },
+ { "RimWorld.ConceptDef::priority", 51 },
+ { "Verse.FleckDef::growthRate", 50 },
+ { "Verse.PawnKindDef::weaponMoney", 50 },
+ { "Verse.VerbProperties::defaultProjectile", 50 },
+ { "RimWorld.WornGraphicBodyTypeData::scale", 50 },
+ { "Verse.SubEffecterDef::positionLerpFactor", 50 },
+ { "RimWorld.PlantProperties::topWindExposure", 50 },
+ { "Verse.RaceProperties::baseHungerRate", 50 },
+ { "RimWorld.AbilityDef::level", 49 },
+ { "Verse.GenStepDef::linkWithSite", 49 },
+ { "Verse.ProjectileProperties::damageDef", 49 },
+ { "RimWorld.ThoughtDef::showBubble", 49 },
+ { "Verse.Sound.SoundParameterMapping::inParam", 49 },
+ { "Verse.Sound.SoundParameterMapping::outParam", 49 },
+ { "Verse.SubEffecterDef::rotation", 48 },
+ { "RimWorld.RoomRequirement::labelKey", 48 },
+ { "Verse.ThingDef::soundInteract", 48 },
+ { "RimWorld.ApparelProperties::tags", 48 },
+ { "Verse.RaceProperties::foodType", 48 },
+ { "Verse.AbilityCompProperties::compClass", 47 },
+ { "Verse.PawnKindDef::ecoSystemWeight", 47 },
+ { "RimWorld.IncidentDef::letterLabel", 46 },
+ { "RimWorld.QuestScriptDef::rootSelectionWeight", 46 },
+ { "RimWorld.RitualBehaviorDef::durationTicks", 46 },
+ { "RimWorld.OutcomeChance::label", 46 },
+ { "RimWorld.OutcomeChance::chance", 46 },
+ { "RimWorld.OutcomeChance::positivityIndex", 46 },
+ { "RimWorld.BuildingProperties::ai_chillDestination", 46 },
+ { "RimWorld.CompProperties_Explosive::explosiveRadius", 46 },
+ { "RimWorld.CompProperties_Explosive::explosiveDamageType", 46 },
+ { "Verse.ThingDef::devNote", 46 },
+ { "Verse.PawnKindDef::techHediffsTags", 45 },
+ { "Verse.Sound.SubSoundDef::tempoAffectedByGameSpeed", 45 },
+ { "Verse.RecipeMakerProperties::recipeUsers", 45 },
+ { "RimWorld.PawnColumnDef::workerClass", 45 },
+ { "Verse.DamageGraphicData::cornerTR", 45 },
+ { "RimWorld.TraitDef::degreeDatas", 45 },
+ { "Verse.MoteProperties::needsMaintenance", 44 },
+ { "Verse.SubEffecterDef::positionOffset", 44 },
+ { "Verse.BuildableDef::defaultPlacingRot", 44 },
+ { "RimWorld.SkillNeed::skill", 44 },
+ { "Verse.HediffGiver::hediff", 43 },
+ { "Verse.PawnKindDef::techHediffsChance", 43 },
+ { "RimWorld.RitualStage::spectateDistanceOverride", 43 },
+ { "Verse.ThingDef::blockWind", 43 },
+ { "RimWorld.CompProperties_Glower::glowRadius", 43 },
+ { "RimWorld.CompProperties_Glower::glowColor", 43 },
+ { "Verse.VerbProperties::muzzleFlashScale", 43 },
+ { "Verse.Sound.SubSoundDef::sustainAttack", 43 },
+ { "Verse.PawnCapacityModifier::setMax", 42 },
+ { "RimWorld.BuildingProperties::buildingTags", 42 },
+ { "Verse.ThingDef::weaponClasses", 42 },
+ { "RimWorld.PreceptDef::requiredMemes", 42 },
+ { "Verse.RaceProperties::trainability", 42 },
+ { "Verse.GraphicData::maskPath", 42 },
+ { "Verse.RaceProperties::leatherDef", 42 },
+ { "Verse.RecipeDef::removesHediff", 41 },
+ { "Verse.PawnKindDef::apparelTags", 41 },
+ { "RimWorld.ApparelRequirement::bodyPartGroupsMatchAny", 41 },
+ { "Verse.VerbProperties::soundCastTail", 41 },
+ { "RimWorld.StockGenerator_MarketValue::tradeTag", 41 },
+ { "RimWorld.PreceptDef::conflictingMemes", 41 },
+ { "RimWorld.PawnCapacityFactor::max", 41 },
+ { "RimWorld.WorkGiverDef::prioritizeSustains", 41 },
+ { "Verse.ThingDef::stuffProps", 41 },
+ { "Verse.StuffProperties::color", 41 },
+ { "Verse.RecipeDef::workAmount", 40 },
+ { "Verse.TerrainDef::color", 40 },
+ { "RimWorld.ApparelProperties::defaultOutfitTags", 40 },
+ { "RimWorld.StockGenerator_Tag::tradeTag", 40 },
+ { "Verse.DamageGraphicData::cornerTL", 40 },
+ { "Verse.DamageGraphicData::cornerBR", 40 },
+ { "Verse.ThingDef::drawPlaceWorkersWhileSelected", 39 },
+ { "RimWorld.ApparelProperties::countsAsClothingForNudity", 39 },
+ { "Verse.SubEffecterDef::fleckUsesAngleForVelocity", 39 },
+ { "RimWorld.GoodwillSituationDef::workerClass", 39 },
+ { "Verse.DamageGraphicData::cornerBL", 39 },
+ { "Verse.GeneDef::displayCategory", 39 },
+ { "Verse.RoomStatScoreStage::label", 39 },
+ { "Verse.HediffDef::addedPartProps", 38 },
+ { "Verse.ThingDef::allowedArchonexusCount", 38 },
+ { "Verse.SubEffecterDef::rotationRate", 38 },
+ { "RimWorld.OutcomeChance::description", 38 },
+ { "RimWorld.StatDef::scenarioRandomizable", 38 },
+ { "Verse.HediffDef::defaultLabelColor", 38 },
+ { "Verse.RaceProperties::gestationPeriodDays", 38 },
+ { "RimWorld.TargetingParameters::canTargetSelf", 37 },
+ { "Verse.HediffDef::initialSeverity", 37 },
+ { "RimWorld.OutcomeChance::memory", 37 },
+ { "RimWorld.TaleDef::firstPawnSymbol", 37 },
+ { "RimWorld.PlantProperties::fertilitySensitivity", 37 },
+ { "RimWorld.ConceptDef::highlightTags", 37 },
+ { "RimWorld.StatDef::skillNeedFactors", 37 },
+ { "Verse.ThingDef::inspectorTabs", 37 },
+ { "Verse.Sound.SoundParameterMapping::paramUpdateMode", 37 },
+ { "Verse.Sound.SoundParameterMapping::curve", 37 },
+ { "RimWorld.InstructionDef::text", 37 },
+ { "RimWorld.InstructionDef::eventTagInitiate", 37 },
+ { "Verse.HediffStage::vomitMtbDays", 36 },
+ { "Verse.HediffCompProperties_SeverityPerDay::severityPerDay", 36 },
+ { "Verse.PawnKindDef::defaultFactionType", 36 },
+ { "Verse.ResearchProjectDef::requiredResearchBuilding", 36 },
+ { "RimWorld.RitualBehaviorDef::workerClass", 36 },
+ { "RimWorld.TaleDef::secondPawnSymbol", 36 },
+ { "RimWorld.GoodwillSituationDef::naturalGoodwillOffset", 36 },
+ { "Verse.BuildableDef::uiIconPath", 36 },
+ { "RimWorld.PlantProperties::wildClusterWeight", 36 },
+ { "Verse.AddedBodyPartProps::solid", 35 },
+ { "Verse.ThingDef::leaveResourcesWhenKilled", 35 },
+ { "Verse.ThingDef::category", 35 },
+ { "Verse.VerbProperties::burstShotCount", 35 },
+ { "RimWorld.GoodwillSituationDef::meme", 35 },
+ { "RimWorld.StatDef::capacityFactors", 35 },
+ { "Verse.TerrainDef::renderPrecedence", 35 },
+ { "RimWorld.ThoughtDef::requiredTraits", 35 },
+ { "RimWorld.InteractionDef::ignoreTimeSinceLastInteraction", 34 },
+ { "RimWorld.CompProperties_Explosive::wickTicks", 34 },
+ { "RimWorld.TargetingParameters::canTargetBuildings", 34 },
+ { "RimWorld.PlantProperties::harvestWork", 34 },
+ { "RimWorld.PreceptDef::defaultSelectionWeight", 34 },
+ { "Verse.TerrainDef::texturePath", 34 },
+ { "Verse.RoomStatScoreStage::minScore", 34 },
+ { "RimWorld.AbilityDef::writeCombatLog", 33 },
+ { "Verse.PawnKindDef::gearHealthRange", 33 },
+ { "Verse.PawnKindDef::apparelAllowHeadgearChance", 33 },
+ { "RimWorld.QuestScriptDef::rootMinPoints", 33 },
+ { "RimWorld.RitualRole::allowChild", 33 },
+ { "RimWorld.IdeoColorDef::colorDef", 33 },
+ { "RimWorld.MemeDef::iconPath", 33 },
+ { "RimWorld.MemeDef::generalRules", 33 },
+ { "RimWorld.MemeDef::descriptionMaker", 33 },
+ { "Verse.ThingDef::staticSunShadowHeight", 33 },
+ { "Verse.BodyPartDef::alive", 33 },
+ { "Verse.GeneDef::biostatCpx", 33 },
+ { "Verse.SkyColorSet::sky", 33 },
+ { "Verse.SkyColorSet::shadow", 33 },
+ { "Verse.SkyColorSet::overlay", 33 },
+ { "Verse.SkyColorSet::saturation", 33 },
+ { "Verse.PawnKindDef::xenotypeSet", 32 },
+ { "Verse.HediffStage::partEfficiencyOffset", 32 },
+ { "RimWorld.DeityNameType::name", 32 },
+ { "RimWorld.DeityNameType::type", 32 },
+ { "RimWorld.RitualRoleBehavior::speakerInteraction", 32 },
+ { "RimWorld.StatCategoryDef::displayOrder", 32 },
+ { "Verse.StuffProperties::commonality", 32 },
+ { "Verse.StuffProperties::statFactors", 32 },
+ { "RimWorld.TraitDegreeData::degree", 32 },
+ { "Verse.AI.DutyDef::hook", 31 },
+ { "RimWorld.PlantProperties::fertilityMin", 31 },
+ { "RimWorld.GoodwillSituationDef::otherMeme", 31 },
+ { "Verse.HediffGiver::partsToAffect", 31 },
+ { "Verse.MentalStateDef::stateClass", 31 },
+ { "RimWorld.TattooDef::tattooType", 31 },
+ { "RimWorld.TraitDegreeData::statOffsets", 31 },
+ { "RimWorld.InstructionDef::actionTagsAllowed", 31 },
+ { "RimWorld.InstructionDef::highlightTags", 31 },
+ { "RimWorld.InstructionDef::rejectInputMessage", 31 },
+ { "Verse.HediffCompProperties_Disappears::showRemainingTime", 30 },
+ { "Verse.HediffDef::maxSeverity", 30 },
+ { "Verse.PawnKindDef::apparelRequired", 30 },
+ { "Verse.ResearchProjectDef::hiddenPrerequisites", 30 },
+ { "RimWorld.RitualBehaviorDef::spectatorsLabel", 30 },
+ { "RimWorld.RitualBehaviorDef::spectatorGerund", 30 },
+ { "RimWorld.PlantProperties::sowTags", 30 },
+ { "RimWorld.StockGenerator::totalPriceRange", 30 },
+ { "RimWorld.PlantProperties::wildOrder", 30 },
+ { "Verse.MentalStateDef::baseInspectLine", 30 },
+ { "RimWorld.PlantProperties::harvestTag", 30 },
+ { "Verse.RaceProperties::useMeatFrom", 30 },
+ { "RimWorld.WorkGiverDef::canBeDoneByMechs", 30 },
+ { "RimWorld.StorytellerCompProperties::allowedTargetTags", 30 }
+ };
+}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Generator/XmlGeneratorContext.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/XmlGeneratorContext.cs
new file mode 100644
index 0000000..b379568
--- /dev/null
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/Generator/XmlGeneratorContext.cs
@@ -0,0 +1,118 @@
+using System.Collections.Generic;
+using JetBrains.Annotations;
+using JetBrains.Application.Parts;
+using JetBrains.ProjectModel;
+using JetBrains.ReSharper.Feature.Services.Generate;
+using JetBrains.ReSharper.Psi;
+using JetBrains.ReSharper.Psi.DataContext;
+using JetBrains.ReSharper.Psi.Modules;
+using JetBrains.ReSharper.Psi.Pointers;
+using JetBrains.ReSharper.Psi.Tree;
+using JetBrains.ReSharper.Psi.Xml;
+using JetBrains.ReSharper.Psi.Xml.Tree;
+using JetBrains.Util;
+
+namespace ReSharperPlugin.RimworldDev.Generator;
+
+[Language(typeof (XmlLanguage), Instantiation.DemandAnyThread)]
+public class XamlGeneratorContextFactory : IGeneratorContextFactory
+{
+ public IGeneratorContext TryCreate(string kind, IPsiDocumentRangeView psiDocumentRangeView)
+ {
+ return XmlGeneratorContext.CreateContext(kind, psiDocumentRangeView);
+ }
+
+ public IGeneratorContext TryCreate(string kind, IDeclaredElement contextElement)
+ {
+ return null;
+ }
+
+ public IGeneratorContext TryCreate(string kind, ITreeNode targetContext, ITreeNode anchor)
+ {
+ return null;
+ }
+}
+
+
+ public class XmlGeneratorContext : GeneratorContextBase
+ {
+ private XmlGeneratorContext([NotNull] string kind, [NotNull] IXmlFile file, [CanBeNull] ITreeNode anchor)
+ : base(kind)
+ {
+ XmlFile = file;
+ Anchor = anchor;
+ if (anchor != null)
+ CodebehindDeclarations = EmptyList.InstanceList;
+ }
+
+ [CanBeNull]
+ public static XmlGeneratorContext CreateContext(
+ string kind,
+ [NotNull] IPsiDocumentRangeView psiDocumentRangeView)
+ {
+ ITreeNode anchor;
+ IXmlFile selectedTreeNode = psiDocumentRangeView.View().GetSelectedTreeNode(out anchor);
+ return selectedTreeNode == null ? null : new XmlGeneratorContext(kind, selectedTreeNode, anchor);
+ }
+
+ public IXmlFile XmlFile { get; set; }
+
+ public IList CodebehindDeclarations { get; }
+
+ public override sealed ITreeNode Anchor { get; set; }
+
+ public override ITreeNode Root => XmlFile;
+
+ public override ISolution Solution => XmlFile.GetSolution();
+
+ public override IPsiModule PsiModule => XmlFile.GetPsiModule();
+
+ public override PsiLanguageType Language => XmlFile.Language;
+
+ public override PsiLanguageType PresentationLanguage
+ {
+ get
+ {
+ using (IEnumerator enumerator = CodebehindDeclarations.GetEnumerator())
+ {
+ if (enumerator.MoveNext())
+ return enumerator.Current.Language;
+ }
+ return XmlFile.Language;
+ }
+ }
+
+ public override TreeTextRange GetSelectionTreeRange() => TreeTextRange.InvalidRange;
+
+ public override IGeneratorContextPointer CreatePointer()
+ {
+ return new XmlGeneratorWorkflowPointer(this);
+ }
+
+ private sealed class XmlGeneratorWorkflowPointer : GeneratorWorkflowPointer
+ {
+ [NotNull]
+ private readonly ITreeNodePointer myClassPointer;
+ [CanBeNull]
+ private readonly ITreeNodePointer myAnchorPointer;
+
+ public XmlGeneratorWorkflowPointer([NotNull] XmlGeneratorContext context)
+ : base(context)
+ {
+ myClassPointer = context.XmlFile.CreateTreeElementPointer();
+ ITreeNode anchor = context.Anchor;
+ if (anchor == null)
+ return;
+ myAnchorPointer = anchor.CreateTreeElementPointer();
+ }
+
+ protected override GeneratorContextBase CreateWorkflow()
+ {
+ IXmlFile treeNode1 = myClassPointer.GetTreeNode();
+ if (treeNode1 == null)
+ return null;
+ ITreeNode treeNode2 = myAnchorPointer != null ? myAnchorPointer.GetTreeNode() : null;
+ return new XmlGeneratorContext(Kind, treeNode1, treeNode2);
+ }
+ }
+ }
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/CustomSearcher.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/CustomSearcher.cs
new file mode 100644
index 0000000..38c45cd
--- /dev/null
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/CustomSearcher.cs
@@ -0,0 +1,82 @@
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.Annotations;
+using JetBrains.Diagnostics;
+using JetBrains.ReSharper.Psi;
+using JetBrains.ReSharper.Psi.Caches;
+using JetBrains.ReSharper.Psi.ExtensionsAPI;
+using JetBrains.ReSharper.Psi.ExtensionsAPI.Finder;
+using JetBrains.ReSharper.Psi.Files;
+using JetBrains.ReSharper.Psi.Search;
+using JetBrains.ReSharper.Psi.Tree;
+
+namespace ReSharperPlugin.RimworldDev.Navigation;
+
+public class CustomSearcher : IDomainSpecificSearcher where TLanguage : KnownLanguage
+{
+ private readonly JetHashSet myNames;
+ private readonly JetHashSet myWordsInText;
+ private readonly IDeclaredElementsSet myElements;
+ private readonly ReferenceSearcherParameters myReferenceSearcherParameters;
+ private readonly bool mySearchForLateBound;
+ private readonly IWordIndex myWordIndex;
+
+ public CustomSearcher(
+ IDomainSpecificSearcherFactory factory,
+ IDeclaredElementsSet elements,
+ ReferenceSearcherParameters referenceSearcherParameters,
+ bool searchForLateBound)
+ {
+ mySearchForLateBound = searchForLateBound;
+ myElements = elements;
+ myReferenceSearcherParameters = referenceSearcherParameters;
+ myNames = [];
+ myWordsInText = [];
+
+ // The element shortNames are {defType}/{defName} to make them unique references, however in the text they're
+ // just {defName} so we split up their shortName for seraching
+ foreach (var element in myElements)
+ {
+ myNames.Add(element.ShortName.Split('/').Last());
+ myWordsInText.UnionWith(factory.GetAllPossibleWordsInFile(element));
+ }
+
+ myWordIndex = myElements.FirstOrDefault()?.GetPsiServices().WordIndex;
+ }
+
+ public bool ProcessProjectItem(
+ IPsiSourceFile sourceFile,
+ IFindResultConsumer consumer)
+ {
+ if (!CanContainWord(sourceFile)) return false;
+
+ foreach (ITreeNode psiFile in sourceFile.GetPsiFiles())
+ {
+ if (ProcessElement(psiFile, consumer))
+ return true;
+ }
+
+ return false;
+ }
+
+ private bool CanContainWord([NotNull] IPsiSourceFile sourceFile)
+ {
+ return myWordsInText.Any(word => myWordIndex?.CanContainWord(sourceFile, word) == true);
+ }
+
+ public bool ProcessElement(ITreeNode element, IFindResultConsumer consumer)
+ {
+ Assertion.Assert(element != null);
+ JetHashSet referenceNames = [];
+ foreach (var myElement in myElements)
+ {
+ myNames.Add(myElement.ShortName);
+ }
+
+ return (!mySearchForLateBound
+ ? new ReferenceSearchSourceFileProcessor(element, myReferenceSearcherParameters, consumer,
+ myElements, myWordsInText, referenceNames)
+ : (NamedThingsSearchSourceFileProcessor)new LateBoundReferenceSourceFileProcessor(element,
+ consumer, myElements, myWordsInText, referenceNames)).Run() == FindExecution.Stop;
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs
new file mode 100644
index 0000000..af10669
--- /dev/null
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/Navigation/RimworldSearcherFactory.cs
@@ -0,0 +1,35 @@
+using System.Collections.Generic;
+using System.Linq;
+using JetBrains.ReSharper.Psi;
+using JetBrains.ReSharper.Psi.ExtensionsAPI;
+using JetBrains.ReSharper.Psi.Search;
+using JetBrains.ReSharper.Psi.Xml;
+
+namespace ReSharperPlugin.RimworldDev.Navigation;
+
+[PsiSharedComponent]
+public class RimworldSearcherFactory(SearchDomainFactory searchDomainFactory) : DomainSpecificSearcherFactoryBase
+{
+ public override bool IsCompatibleWithLanguage(PsiLanguageType languageType) => languageType.Is();
+
+ public override ISearchDomain GetDeclaredElementSearchDomain(IDeclaredElement declaredElement)
+ {
+ return searchDomainFactory.CreateSearchDomain(declaredElement.GetSolution(), false);
+ }
+
+ public override IDomainSpecificSearcher CreateReferenceSearcher(
+ IDeclaredElementsSet elements,
+ ReferenceSearcherParameters referenceSearcherParameters)
+ {
+ elements = new DeclaredElementsSet(elements.Where(element =>
+ IsCompatibleWithLanguage(element.PresentationLanguage)));
+
+ return new CustomSearcher(this,
+ elements, referenceSearcherParameters, false);
+ }
+
+ public override IEnumerable GetAllPossibleWordsInFile(IDeclaredElement element)
+ {
+ return new List { element.ShortName.Split('/').Last() };
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs
index 76e56dd..98ee07c 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/ProblemAnalyzers/CustomXmlAnalysisStageProcess.cs
@@ -100,6 +100,7 @@ void IRecursiveElementProcessor.ProcessAfterInterior(ITreeNode element)
return;
case "System.String":
break;
+ case "Verse.CurvePoint":
case "UnityEngine.Vector2":
ProcessVector2(fullText, range);
break;
@@ -143,14 +144,14 @@ private void ProcessBoolean(string textValue, DocumentRange range)
private void ProcessInt(string textValue, DocumentRange range)
{
- if (int.TryParse(textValue, out _)) return;
+ if (IsValidInt(textValue)) return;
AddError("Value must be a whole number with no decimal points", range);
}
private void ProcessFloat(string textValue, DocumentRange range)
{
- if (float.TryParse(textValue, out _)) return;
+ if (IsValidFloat(textValue)) return;
AddError("Value must be a valid number", range);
}
@@ -160,7 +161,7 @@ private void ProcessIntRange(string textValue, DocumentRange range)
var strArray = textValue.Split('~');
// For some reason IntRange also just accepts a single number instead of a range
- if (strArray.Length == 1 && int.TryParse(strArray[0], out _)) return;
+ if (strArray.Length == 1 && IsValidInt(strArray[0])) return;
if (strArray.Length != 2)
{
@@ -168,13 +169,13 @@ private void ProcessIntRange(string textValue, DocumentRange range)
return;
}
- if (!int.TryParse(strArray[0], out _))
+ if (!IsValidInt(strArray[0]))
{
AddError($"\"{strArray[0]}\" is not a valid integer", range);
return;
}
- if (!int.TryParse(strArray[1], out _))
+ if (!IsValidInt(strArray[1]))
{
AddError($"\"{strArray[1]}\" is not a valid integer", range);
return;
@@ -185,7 +186,7 @@ private void ProcessFloatRange(string textValue, DocumentRange range)
{
var strArray = textValue.Split('~');
// For some reason FloatRange also just accepts a single number instead of a range
- if (strArray.Length == 1 && float.TryParse(strArray[0], out _)) return;
+ if (strArray.Length == 1 && IsValidFloat(strArray[0])) return;
if (strArray.Length != 2)
{
@@ -193,13 +194,13 @@ private void ProcessFloatRange(string textValue, DocumentRange range)
return;
}
- if (!float.TryParse(strArray[0], out _))
+ if (!IsValidFloat(strArray[0]))
{
AddError($"\"{strArray[0]}\" is not a valid float", range);
return;
}
- if (!float.TryParse(strArray[1], out _))
+ if (!IsValidFloat(strArray[1]))
{
AddError($"\"{strArray[1]}\" is not a valid float", range);
return;
@@ -208,11 +209,11 @@ private void ProcessFloatRange(string textValue, DocumentRange range)
private void ProcessVector2(string textValue, DocumentRange range)
{
- var match = Regex.Match(textValue.Trim(), @"^\((.*?),(.*?)\)$");
+ var match = Regex.Match(textValue.Trim(), @"^\(?(.*?),(.*?)\)?$");
if (!match.Success)
{
// For some reason Vector2 allows us to pass in just a single number instead of a vector?
- if (float.TryParse(textValue, out _)) return;
+ if (IsValidFloat(textValue)) return;
AddError("Your value must be in a format similar to (1,2)", range);
return;
@@ -221,13 +222,13 @@ private void ProcessVector2(string textValue, DocumentRange range)
var firstValue = match.Groups[1].Value;
var secondValue = match.Groups[2].Value;
- if (!float.TryParse(firstValue, out _))
+ if (!IsValidFloat(firstValue))
{
AddError($"\"{firstValue}\" is not a valid number", range);
return;
}
- if (!float.TryParse(secondValue, out _))
+ if (!IsValidFloat(secondValue))
{
AddError($"\"{secondValue}\" is not a valid number", range);
return;
@@ -236,7 +237,7 @@ private void ProcessVector2(string textValue, DocumentRange range)
private void ProcessVector3(string textValue, DocumentRange range)
{
- var match = Regex.Match(textValue.Trim(), @"^\((.*?),(.*?),(.*?)\)$");
+ var match = Regex.Match(textValue.Trim(), @"^\(?(.*?),(.*?),(.*?)\)?$");
if (!match.Success)
{
AddError("Your value must be in a format similar to (1,2,3)", range);
@@ -247,19 +248,19 @@ private void ProcessVector3(string textValue, DocumentRange range)
var secondValue = match.Groups[2].Value;
var thirdValue = match.Groups[3].Value;
- if (!float.TryParse(firstValue, out _))
+ if (!IsValidFloat(firstValue))
{
AddError($"\"{firstValue}\" is not a valid number", range);
return;
}
- if (!float.TryParse(secondValue, out _))
+ if (!IsValidFloat(secondValue))
{
AddError($"\"{secondValue}\" is not a valid number", range);
return;
}
- if (!float.TryParse(thirdValue, out _))
+ if (!IsValidFloat(thirdValue))
{
AddError($"\"{thirdValue}\" is not a valid number", range);
return;
@@ -287,6 +288,22 @@ private void ProcessStruct(string textValue, DocumentRange range, ITypeElement c
AddError($"\"{textValue}\" is not a valid value for {context.ShortName}", range);
}
}
+
+ private bool IsValidFloat(string value)
+ {
+ if (float.TryParse(value, out _)) return true;
+ if (value.Trim() == "Infinity") return true;
+
+ return false;
+ }
+
+ private bool IsValidInt(string value)
+ {
+ if (int.TryParse(value, out _)) return true;
+ if (value.Trim() == "Infinity") return true;
+
+ return false;
+ }
private void AddError(string errorText, DocumentRange range)
{
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj
index cae39f7..5541a8e 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/ReSharperPlugin.RimworldDev.csproj
@@ -2,7 +2,7 @@
$(MSBuildToolsPath)\Microsoft.CSharp.targets
- 10
+ latest
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs
index 048e135..9350f87 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldReferenceProvider.cs
@@ -43,6 +43,10 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection
{
if (!ScopeHelper.UpdateScopes(element.GetSolution())) return new ReferenceCollection();
+ if (element.Parent != null && element.NodeType.ToString() == "TEXT" &&
+ element.Parent.GetText().StartsWith(""))
+ 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();
@@ -54,7 +58,16 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection
if (hierarchy.Count == 0)
{
- var @class = rimworldSymbolScope.GetElementsByShortName(identifier.GetText()).FirstOrDefault();
+ var className = identifier.GetText();
+
+ var allWorkingScopes =
+ ScopeHelper.AllScopes.Where(scope => scope.GetTypeElementByCLRName(className) is not null);
+
+ var @class = className.Contains(".")
+ ? ScopeHelper.GetScopeForClass(className).GetTypeElementByCLRName(className)
+ : rimworldSymbolScope.GetElementsByShortName(className).FirstOrDefault();
+
+ if (@class == null) return new ReferenceCollection();
return new ReferenceCollection(new RimworldXmlReference(@class, identifier));
}
@@ -62,7 +75,7 @@ public ReferenceCollection GetReferences(ITreeNode element, ReferenceCollection
RimworldXMLItemProvider.GetContextFromHierachy(hierarchy, rimworldSymbolScope, allSymbolScopes);
if (classContext == null) return new ReferenceCollection();
- var field = RimworldXMLItemProvider.GetAllPublicFields(classContext, rimworldSymbolScope)
+ var field = RimworldXMLItemProvider.GetAllFields(classContext, rimworldSymbolScope)
.FirstOrDefault(field => field.ShortName == identifier.GetText());
if (field == null) return new ReferenceCollection();
@@ -92,22 +105,63 @@ private ReferenceCollection GetReferencesForText(ITreeNode element, ReferenceCol
// return new ReferenceCollection();
}
- if (!classContext.GetAllSuperClasses().Any(superClass => superClass.GetClrName().FullName == "Verse.Def") &&
- !classContext.GetAllSuperTypes().Any(superType => superType.GetClrName().FullName == "Verse.Def"))
+ if (!ScopeHelper.ExtendsFromVerseDef(classContext.GetClrName().FullName))
return new ReferenceCollection();
var xmlSymbolTable = element.GetSolution().GetComponent();
- var tagId = $"{classContext.ShortName}/{element.GetText()}";
- if (xmlSymbolTable.GetTagByDef(classContext.ShortName, element.GetText()) is not { } tag)
+ var defType = classContext.ShortName;
+ var defName = element.GetText();
+
+ if (xmlSymbolTable.ExtraDefTagNames.ContainsKey($"{defType}/{defName}"))
+ {
+ var newDef = xmlSymbolTable.ExtraDefTagNames[$"{defType}/{defName}"];
+ defType = newDef.Split('/').First();
+ defName = newDef.Split('/').Last();
+ }
+
+ if (xmlSymbolTable.GetTagByDef(defType, defName) is not { } tag)
+ return new ReferenceCollection();
+
+ return new ReferenceCollection(new RimworldXmlDefReference(element, tag, defType, defName));
+ }
+
+ /**
+ * This is a bit of a hacky workaround. Since we're not constructing our own Custom Language, we don't have control
+ * over the Psi Tree. Unfortunately, Find Usages expects to be able to find an `IDeclaration` which is a `ITreeNode`
+ * in the Psi Tree, which would be created on the Tree Construction.
+ *
+ * Since we can't do that, in order to be able to invoke Find Usages on the declared element itself, we're going to
+ * make a hacky workaround and just give it a reference... to itself
+ */
+ 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?
+ // 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();
+
+ var xmlSymbolTable = element.GetSolution().GetComponent();
+
+ var tagId = $"{defTypeName}/{defName}";
+ if (!xmlSymbolTable.DefTags.ContainsKey(tagId)) return new ReferenceCollection();
+
+ if (xmlSymbolTable.GetTagByDef(defTypeName, defName) is not { } tag)
return new ReferenceCollection();
- return new ReferenceCollection(new RimworldXmlDefReference(element,
- tag.GetNestedTags("defName").FirstOrDefault() ?? tag, tagId));
+ return new ReferenceCollection(new RimworldXmlDefReference(element, tag, defTypeName,
+ element.GetText()));
}
public bool HasReference(ITreeNode element, IReferenceNameContainer names)
{
- return true;
+ if (element.NodeType.ToString() != "TEXT") return false;
+ return !element.Parent.GetText().Contains("defName") && names.Contains(element.GetText());
}
}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlDefReference.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlDefReference.cs
index 6c5aaa4..8342a37 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlDefReference.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlDefReference.cs
@@ -1,52 +1,51 @@
-using System.Linq;
using JetBrains.Annotations;
using JetBrains.ProjectModel;
using JetBrains.ReSharper.Psi;
-using JetBrains.ReSharper.Psi.CSharp.Conversions;
using JetBrains.ReSharper.Psi.ExtensionsAPI.Resolve;
-using JetBrains.ReSharper.Psi.Modules;
using JetBrains.ReSharper.Psi.Resolve;
using JetBrains.ReSharper.Psi.Tree;
-using JetBrains.ReSharper.Psi.Xml.Impl.Tree;
-using JetBrains.ReSharper.Psi.Xml.Impl.Tree.References;
-using JetBrains.ReSharper.Psi.Xml.Tree;
-using JetBrains.Util;
-using ReSharperPlugin.RimworldDev.TypeDeclaration;
+using ReSharperPlugin.RimworldDev.SymbolScope;
namespace ReSharperPlugin.RimworldDev.TypeDeclaration;
public class RimworldXmlDefReference :
TreeReferenceBase
{
- private readonly IXmlTag myTypeElement;
+ private readonly ITreeNode myTypeElement;
- private readonly string myName;
+ private string myName;
+ private string defName;
+ private string defType;
- public RimworldXmlDefReference([NotNull] ITreeNode owner, IXmlTag typeElement, string name) : base(owner)
+ public RimworldXmlDefReference([NotNull] ITreeNode owner, ITreeNode typeElement, string defType, string defName) : base(owner)
{
myTypeElement = typeElement;
- myName = name;
+ myName = defName;
+ myName = $"{defType}/{defName}";
+ this.defName = defName;
+ this.defType = defType;
}
public override ISymbolTable GetReferenceSymbolTable(bool useReferenceName)
{
- var symbolTable = new SymbolTable(myOwner.GetPsiServices());
+ var symbolScope = myOwner.GetSolution().GetComponent();
- symbolTable.AddSymbol(
- new XMLTagDeclaredElement(
- myTypeElement,
- myName,
- false
- )
+ symbolScope.AddDeclaredElement(
+ myOwner.GetSolution(),
+ myTypeElement,
+ defType,
+ defName,
+ false
);
-
- return symbolTable;
+
+ return symbolScope.GetSymbolTable(myOwner.GetSolution());
}
public override ResolveResultWithInfo ResolveWithoutCache()
{
- return GetReferenceSymbolTable(true).GetResolveResult(GetName());
+ return GetReferenceSymbolTable(true).GetResolveResult(GetName());
}
+
public override string GetName() => myName;
public override IReference BindTo(IDeclaredElement element) =>
@@ -62,4 +61,4 @@ public override IReference BindTo(
public override TreeTextRange GetTreeTextRange() => myOwner.GetTreeTextRange();
public override IAccessContext GetAccessContext() => new ElementAccessContext(myOwner);
-}
+}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs
index 06617f9..f5056bd 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/References/RimworldXmlReference.cs
@@ -1,3 +1,4 @@
+using System.Linq;
using JetBrains.Annotations;
using JetBrains.Metadata.Reader.API;
using JetBrains.ReSharper.Psi;
@@ -50,7 +51,8 @@ private static bool FilterToApplicableProperties([NotNull] ISymbolInfo symbolInf
}
public override string GetName() => myOwner.GetText();
-
+ private string GetShortName() => GetName().Split('.').Last();
+
public override TreeTextRange GetTreeTextRange() => myOwner.GetTreeTextRange();
public override IAccessContext GetAccessContext() => new ElementAccessContext(myOwner);
@@ -83,12 +85,7 @@ public override ISymbolTable GetReferenceSymbolTable(bool useReferenceName)
if (!useReferenceName)
return table;
- return table.Filter(GetName(), new AllFilter(myOwner.GetText()));
-
- // ISymbolTable table = this.myOwner.GetSolution().GetComponent().GetAllTagsSymbolTable(this.myOwner.GetSourceFile()).Distinct(SymbolInfoComparer.OrdinalIgnoreCase);
- // if (useReferenceName)
- // table = table.Filter(this.GetName());
- // return table;
+ return table.Filter(GetShortName(), new AllFilter(GetShortName()));
}
public ISymbolTable GetCompletionSymbolTable() => GetReferenceSymbolTable(false);
@@ -104,9 +101,8 @@ public override IReference BindTo(
public override ResolveResultWithInfo ResolveWithoutCache()
{
- ResolveResultWithInfo resolveResult = GetReferenceSymbolTable(true).GetResolveResult(GetName());
+ ResolveResultWithInfo resolveResult = GetReferenceSymbolTable(true).GetResolveResult(GetShortName());
return resolveResult;
- // return new ResolveResultWithInfo(resolveResult.Result, resolveResult.Info.CheckResolveInfo((ResolveErrorType) HtmlResolveErrorType.UNKNOWN_HTML_TAG));
}
public sealed class AllFilter : SimpleSymbolInfoFilter
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs
index 1506100..e80d5a0 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/RimworldXMLItemProvider.cs
@@ -12,6 +12,7 @@
using JetBrains.ReSharper.Psi.Caches;
using JetBrains.ReSharper.Psi.CSharp;
using JetBrains.ReSharper.Psi.Impl.reflection2.elements.Compiled;
+using JetBrains.ReSharper.Psi.Impl.Types;
using JetBrains.ReSharper.Psi.Modules;
using JetBrains.ReSharper.Psi.Resolve;
using JetBrains.ReSharper.Psi.Tree;
@@ -180,16 +181,17 @@ protected void AddTextLookupItems(RimworldXmlCodeCompletionContext context, IIte
var xmlSymbolTable = context.TreeNode!.GetSolution().GetSolution().GetComponent();
- var keys = xmlSymbolTable.DefTags.Keys
- .Where(key => key.StartsWith($"{className}/"))
- .Select(key => key.Substring(className.Length + 1));
-
+ var keys = xmlSymbolTable.GetDefsByType(className);
+
foreach (var key in keys)
{
- var item = xmlSymbolTable.GetTagByDef(className, key);
-
- var lookup = LookupFactory.CreateDeclaredElementLookupItem(context, key,
- new DeclaredElementInstance(new XMLTagDeclaredElement(item, key, false)));
+ 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);
}
@@ -278,11 +280,22 @@ public static List GetHierarchy(ITreeNode treeNode)
public static List GetAllPublicFields(ITypeElement desiredClass, ISymbolScope symbolScope)
{
return desiredClass.GetAllClassMembers()
+ .Where(field => !field.Member.GetAttributeInstances(AttributesSource.All).Select(attribute => attribute.GetAttributeShortName()).Contains("UnsavedAttribute"))
.Where(member => member.Member.AccessibilityDomain.DomainType == AccessibilityDomain.AccessibilityDomainType.PUBLIC)
.Select(member => member.Member)
.ToList();
}
+ // Grabs the fields that we can use for a class by looking at that classes fields as well as the fields for all the
+ // classes that it inherits from
+ public static List GetAllFields(ITypeElement desiredClass, ISymbolScope symbolScope)
+ {
+ return desiredClass.GetAllClassMembers()
+ .Where(field => !field.Member.GetAttributeInstances(AttributesSource.All).Select(attribute => attribute.GetAttributeShortName()).Contains("UnsavedAttribute"))
+ .Select(member => member.Member)
+ .ToList();
+ }
+
/**
* This is where a lot of our heavy lifting is going to be. We're taking the a list of strings as a hierarchy
* (See the docblock for GetHierarchy) and getting the Class for that last item in that list. So for example, if we
@@ -310,20 +323,34 @@ public static ITypeElement GetContextFromHierachy(List hierarchy, ISymbo
{
// If we know we're in an li but can't pull the expected class from the C# typing for some reason, just
// return null so that we don't throw an error
- if (previousField == null ||
- !Regex.Match(
- previousField.Type.GetLongPresentableName(CSharpLanguage.Instance),
- @"^System.Collections.Generic.List<.*?>$"
- ).Success)
+ if (previousField == null)
{
return null;
}
- // Use regex to grab the className and then fetch it from the symbol scope
- var classValue = Regex.Match(previousField.Type.GetLongPresentableName(CSharpLanguage.Instance),
- @"^System.Collections.Generic.List<(.*?)>$").Groups[1].Value;
-
- currentContext = symbolScope.GetTypeElementByCLRName(classValue);
+ string classValue = null;
+ if (previousField.Type is ISimplifiedIdTypeInfo simpleTypeInfo)
+ {
+ classValue = simpleTypeInfo.GetTypeArguments()?.FirstOrDefault()?
+ .GetLongPresentableName(CSharpLanguage.Instance);
+ }
+
+ if (classValue == null)
+ {
+ if (!Regex.Match(
+ previousField.Type.GetLongPresentableName(CSharpLanguage.Instance),
+ @"^System.Collections.Generic.List<.*?>$"
+ ).Success)
+ {
+ return null;
+ }
+
+ // Use regex to grab the className and then fetch it from the symbol scope
+ classValue = Regex.Match(previousField.Type.GetLongPresentableName(CSharpLanguage.Instance),
+ @"^System.Collections.Generic.List<(.*?)>$").Groups[1].Value;
+ };
+
+ currentContext = ScopeHelper.GetScopeForClass(classValue).GetTypeElementByCLRName(classValue);
continue;
}
@@ -336,12 +363,13 @@ public static ITypeElement GetContextFromHierachy(List hierarchy, ISymbo
if (classValue == "") return null;
+ var scopeToUse = ScopeHelper.GetScopeForClass(classValue);
// First we try to look it up as a short name from the Rimworld DLL
- currentContext = symbolScope.GetElementsByShortName(classValue).FirstOrDefault() as ITypeElement;
+ currentContext = scopeToUse.GetElementsByShortName(classValue).FirstOrDefault() as ITypeElement;
if (currentContext != null) continue;
// Then we try to look it up as a fully qualified name from Rimworld
- currentContext = symbolScope.GetTypeElementByCLRName(classValue);
+ currentContext = scopeToUse.GetTypeElementByCLRName(classValue);
if (currentContext != null) continue;
// If it's not a Rimworld class, let's assume that it's a custom class in our own C#. In that case, let's
@@ -362,17 +390,19 @@ public static ITypeElement GetContextFromHierachy(List hierarchy, ISymbo
// current context to be diving into
if (!currentContext.IsClass())
{
- currentContext = symbolScope.GetElementsByShortName(currentNode).FirstOrDefault() as Class;
+ currentContext = currentNode.Contains(".") ? ScopeHelper.GetScopeForClass(currentNode)?.GetTypeElementByCLRName(currentNode) : symbolScope?.GetElementsByShortName(currentNode).FirstOrDefault() as Class;
+ if (currentContext is null) return null;
+
continue;
}
- var fields = GetAllPublicFields(currentContext, symbolScope);
+ var fields = GetAllFields(currentContext, symbolScope);
var field = fields.FirstOrDefault(field => field.ShortName == currentNode);
if (field == null) return null;
previousField = field;
var clrName = field.Type.GetLongPresentableName(CSharpLanguage.Instance);
- currentContext = symbolScope.GetTypeElementByCLRName(clrName);
+ currentContext = ScopeHelper.GetScopeForClass(clrName).GetTypeElementByCLRName(clrName);
switch (clrName)
{
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs
index a0b60aa..007cc7f 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/ScopeHelper.cs
@@ -9,6 +9,7 @@
using JetBrains.ProjectModel.model2.Assemblies.Interfaces;
using JetBrains.ReSharper.Psi.Caches;
using JetBrains.ReSharper.Psi.Modules;
+using JetBrains.ReSharper.Psi.Util;
using JetBrains.Util;
using JetBrains.Util.Threading.Tasks;
using ReSharperPlugin.RimworldDev.Settings;
@@ -18,6 +19,7 @@ namespace ReSharperPlugin.RimworldDev;
public class ScopeHelper
{
private static List allScopes = new();
+ private static List knownCustomScopes = new();
private static ISymbolScope rimworldScope;
private static IPsiModule rimworldModule;
private static List usedScopes;
@@ -56,6 +58,22 @@ public static bool UpdateScopes(ISolution solution)
return true;
}
+ public static ISymbolScope GetScopeForClass(string className)
+ {
+ if (!className.Contains(".")) return rimworldScope;
+ if (knownCustomScopes.FirstOrDefault(scope => scope.GetTypeElementByCLRName(className) is not null) is
+ { } foundScope) return foundScope;
+
+ if (knownCustomScopes.FirstOrDefault(scope => scope.GetTypeElementByCLRName(className) is not null) is
+ { } newCustomScope)
+ {
+ knownCustomScopes.Add(newCustomScope);
+ return newCustomScope;
+ }
+
+ return rimworldScope;
+ }
+
private static async void AddRef(ISolution solution)
{
if (adding) return;
@@ -85,7 +103,7 @@ public static FileSystemPath FindRimworldDirectory(string currentPath)
{
return customPath;
}
-
+
var locations = new List();
locations.AddRange(GetSteamLocations()
@@ -135,7 +153,7 @@ public static FileSystemPath FindRimworldDll(string currentPath)
FileSystemPath.ParseRelativelyTo(path, rimworldLocation).ExistsFile);
if (location == null) return null;
-
+
var path = FileSystemPath.ParseRelativelyTo(location, rimworldLocation);
return path.ExistsFile ? path : null;
@@ -154,7 +172,7 @@ public static List FindModDirectories(string currentPath)
if (dataDirectory.ExistsDirectory) modDirectories.Add(dataDirectory.FullPath);
if (modsDirectory.ExistsDirectory) modDirectories.Add(modsDirectory.FullPath);
}
-
+
modDirectories.AddRange(GetSteamLocations()
.Select(location => FileSystemPath.TryParse($"{location}/workshop/content/294100/").FullPath)
.Where(location => FileSystemPath.TryParse(location).ExistsDirectory)
@@ -209,12 +227,12 @@ public static Dictionary GetModLocations(string basePath, List GetModLocations(string basePath, List GetAllSuperClasses(string clrName)
+ {
+ if (clrName.StartsWith("System")) return new List();
+
+ var items = new List();
+ var supers = RimworldScope.GetTypeElementByCLRName(clrName)?.GetAllSuperClasses().ToList();
+ supers?.ForEach(super =>
+ {
+ var clr = super.GetClrName().FullName;
+ items.Add(clr);
+ items.AddRange(GetAllSuperClasses(clr));
+ });
+
+ return items;
+ }
+
+ public static List GetAllSuperTypeElements(string clrName)
+ {
+ if (clrName.StartsWith("System")) return new List();
+
+ var items = new List();
+ var supers = RimworldScope.GetTypeElementByCLRName(clrName).GetAllSuperTypeElements().ToList();
+ supers.ForEach(super =>
+ {
+ var clr = super.GetClrName().FullName;
+ items.Add(clr);
+ items.AddRange(GetAllSuperTypeElements(clr));
+ });
+
+ return items;
+ }
+
+ public static List GetAllSuperTypes(string clrName)
+ {
+ if (clrName.StartsWith("System")) return new List();
+
+ var items = new List();
+ var supers = RimworldScope.GetTypeElementByCLRName(clrName).GetAllSuperTypes().ToList();
+ supers.ForEach(super =>
+ {
+ var clr = super.GetClrName().FullName;
+ items.Add(clr);
+ items.AddRange(GetAllSuperTypes(clr));
+ });
+
+ return items;
+ }
+
+ public static bool ExtendsFromVerseDef(string clrName)
+ {
+ if (RimworldScope is null) return false;
+
+ return
+ GetAllSuperClasses(clrName).Any(super => super == "Verse.Def") ||
+ GetAllSuperTypes(clrName).Any(super => super == "Verse.Def");
+ }
}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs
index c0bc0ed..564814a 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldSymbolScope.cs
@@ -5,20 +5,31 @@
using JetBrains.Application.Threading;
using JetBrains.Collections;
using JetBrains.Lifetimes;
+using JetBrains.ProjectModel;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Caches;
+using JetBrains.ReSharper.Psi.ExtensionsAPI.Resolve;
using JetBrains.ReSharper.Psi.Files;
+using JetBrains.ReSharper.Psi.Resolve;
+using JetBrains.ReSharper.Psi.Tree;
+using JetBrains.ReSharper.Psi.Util;
using JetBrains.ReSharper.Psi.Xml.Tree;
+using ReSharperPlugin.RimworldDev.TypeDeclaration;
namespace ReSharperPlugin.RimworldDev.SymbolScope;
[PsiComponent]
public class RimworldSymbolScope : SimpleICache>
{
- public Dictionary DefTags = new();
-
+ public Dictionary DefTags = new();
+
+ public Dictionary ExtraDefTagNames = new();
+ private Dictionary _declaredElements = new();
+ private SymbolTable _symbolTable;
+
public RimworldSymbolScope
- (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager, long? version = null)
+ (Lifetime lifetime, [NotNull] IShellLocks locks, [NotNull] IPersistentIndexManager persistentIndexManager,
+ long? version = null)
: base(lifetime, locks, persistentIndexManager, RimworldXmlDefSymbol.Marshaller, version)
{
}
@@ -29,13 +40,13 @@ protected override bool IsApplicable(IPsiSourceFile sourceFile)
}
[CanBeNull]
- public IXmlTag GetTagByDef(string defType, string defName)
+ public ITreeNode GetTagByDef(string defType, string defName)
{
return GetTagByDef($"{defType}/{defName}");
}
-
+
[CanBeNull]
- public IXmlTag GetTagByDef(string defId)
+ public ITreeNode GetTagByDef(string defId)
{
if (!DefTags.ContainsKey(defId))
return null;
@@ -43,6 +54,20 @@ public IXmlTag GetTagByDef(string defId)
return DefTags[defId];
}
+ public List GetDefsByType(string defType)
+ {
+ return DefTags
+ .Keys
+ .Where(key => key.StartsWith($"{defType}/"))
+ .Select(defId => ExtraDefTagNames.ContainsKey(defId) ? ExtraDefTagNames[defId] : defId)
+ .ToList()
+ .Concat(
+ ExtraDefTagNames.Keys
+ .Where(key => key.StartsWith($"{defType}/"))
+ .Select(key => ExtraDefTagNames[key])
+ ).ToList();
+ }
+
public override object Build(IPsiSourceFile sourceFile, bool isStartup)
{
if (!IsApplicable(sourceFile))
@@ -61,9 +86,10 @@ public override object Build(IPsiSourceFile sourceFile, bool isStartup)
foreach (var tag in tags)
{
var defName = tag.GetNestedTags("defName").FirstOrDefault()?.InnerText;
+ var defNameTag = tag.GetNestedTags("defName").FirstOrDefault().Children().ElementAt(1);
if (defName is null) continue;
- defs.Add(new RimworldXmlDefSymbol(tag, defName, tag.GetTagName()));
+ defs.Add(new RimworldXmlDefSymbol(defNameTag, defName, tag.GetTagName()));
}
return defs;
@@ -75,7 +101,7 @@ public override void Merge(IPsiSourceFile sourceFile, object builtPart)
AddToLocalCache(sourceFile, builtPart as List);
base.Merge(sourceFile, builtPart);
}
-
+
public override void MergeLoaded(object data)
{
PopulateLocalCache();
@@ -87,32 +113,100 @@ public override void Drop(IPsiSourceFile sourceFile)
RemoveFromLocalCache(sourceFile);
base.Drop(sourceFile);
}
-
+
private void AddToLocalCache(IPsiSourceFile sourceFile, [CanBeNull] List cacheItem)
{
+ ScopeHelper.UpdateScopes(sourceFile.GetSolution());
if (sourceFile.GetPrimaryPsiFile() is not IXmlFile xmlFile) return;
-
+
cacheItem?.ForEach(item =>
{
- if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}"))
- DefTags.Add($"{item.DefType}/{item.DefName}", xmlFile.GetNestedTags("Defs/*").FirstOrDefault(tag => tag.GetTreeStartOffset().Offset == item.DocumentOffset));
+ var matchingDefTag = xmlFile
+ .GetNestedTags("Defs/*/defName").FirstOrDefault(tag =>
+ tag.Children().ElementAt(1).GetTreeStartOffset().Offset == item.DocumentOffset);
+
+ if (matchingDefTag is null) return;
+
+ var xmlTag = matchingDefTag.Children().ElementAt(1);
+
+ AddDefTagToList(item, xmlTag);
});
+
+ void AddDefTagToList(RimworldXmlDefSymbol item, ITreeNode xmlTag)
+ {
+ if (item.DefType.Contains(".") && ScopeHelper.RimworldScope is not null)
+ {
+ var superClasses = ScopeHelper.GetScopeForClass(item.DefType)?
+ .GetTypeElementByCLRName(item.DefType)?
+ .GetAllSuperClasses().ToList() ?? new();
+
+ foreach (var superClass in superClasses)
+ {
+ if (superClass.GetClrName().FullName == "Verse.Def") break;
+
+ var subDefType = superClass.GetClrName().ShortName;
+ if (!ExtraDefTagNames.ContainsKey($"{subDefType}/{item.DefName}"))
+ {
+ ExtraDefTagNames.Add($"{subDefType}/{item.DefName}", $"{item.DefType}/{item.DefName}");
+ }
+ else
+ {
+ ExtraDefTagNames[$"{subDefType}/{item.DefName}"] = $"{item.DefType}/{item.DefName}";
+ }
+ }
+ }
+
+ if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}"))
+ DefTags.Add($"{item.DefType}/{item.DefName}", xmlTag);
+ else
+ DefTags[$"{item.DefType}/{item.DefName}"] = xmlTag;
+ }
}
private void RemoveFromLocalCache(IPsiSourceFile sourceFile)
{
var items = Map!.GetValueSafe(sourceFile);
-
+
items?.ForEach(item =>
{
- if (!DefTags.ContainsKey($"{item.DefType}/{item.DefName}"))
+ if (DefTags.ContainsKey($"{item.DefType}/{item.DefName}"))
DefTags.Remove($"{item.DefType}/{item.DefName}");
});
}
-
+
private void PopulateLocalCache()
{
foreach (var (sourceFile, cacheItem) in Map)
AddToLocalCache(sourceFile, cacheItem);
}
+
+ public void AddDeclaredElement(ISolution solution, ITreeNode owner, string defType, string defName,
+ bool caseSensitiveName)
+ {
+ if (_symbolTable == null) _symbolTable = new SymbolTable(solution.GetPsiServices());
+
+ if (_declaredElements.ContainsKey($"{defType}/{defName}"))
+ {
+ _declaredElements[$"{defType}/{defName}"].Update(owner);
+ return;
+ }
+
+ var declaredElement = new XMLTagDeclaredElement(
+ owner,
+ defType,
+ defName,
+ caseSensitiveName
+ );
+
+ // @TODO: We seem to get "Key Already Exists" errors. Race condition?
+ _declaredElements.Add($"{defType}/{defName}", declaredElement);
+ _symbolTable.AddSymbol(declaredElement);
+ }
+
+ public ISymbolTable GetSymbolTable(ISolution solution)
+ {
+ if (_symbolTable == null) _symbolTable = new SymbolTable(solution.GetPsiServices());
+
+ return _symbolTable;
+ }
}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs
index 9bf0b60..fd58ba0 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/SymbolScope/RimworldXmlDefSymbol.cs
@@ -1,5 +1,5 @@
using System.Collections.Generic;
-using JetBrains.ReSharper.Psi.Xml.Tree;
+using JetBrains.ReSharper.Psi.Tree;
using JetBrains.Serialization;
using JetBrains.Util.PersistentMap;
@@ -17,7 +17,7 @@ public class RimworldXmlDefSymbol
// public IXmlTag Tag { get; }
- public RimworldXmlDefSymbol(IXmlTag tag, string defName, string defType)
+ public RimworldXmlDefSymbol(ITreeNode tag, string defName, string defType)
{
DefName = defName;
DefType = defType;
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs
index 8768264..63c9121 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XMLTagDeclaredElement.cs
@@ -1,8 +1,8 @@
using System.Collections.Generic;
+using System.Linq;
using System.Xml;
using JetBrains.ReSharper.Psi;
using JetBrains.ReSharper.Psi.Tree;
-using JetBrains.ReSharper.Psi.Xml.Tree;
using JetBrains.Util;
using JetBrains.Util.DataStructures;
@@ -11,31 +11,37 @@ namespace ReSharperPlugin.RimworldDev.TypeDeclaration;
/**
* This class allows us to create an IDeclaredElement out of an IXmlTag, allowing us to use it as an IReference
*/
-public class XMLTagDeclaredElement: IDeclaredElement
+public class XMLTagDeclaredElement : IDeclaredElement
{
- public XMLTagDeclaredElement(IXmlTag owner, string shortName, bool caseSensitiveName)
+ public XMLTagDeclaredElement(ITreeNode owner, string defType, string defName, bool caseSensitiveName)
{
this.owner = owner;
myPsiServices = owner.GetPsiServices();
- ShortName = shortName;
+ ShortName = $"{defType}/{defName}";
CaseSensitiveName = caseSensitiveName;
PresentationLanguage = owner.Language;
}
- private readonly IXmlTag owner;
+ public void Update(ITreeNode newOwner)
+ {
+ owner = newOwner;
+ myPsiServices = newOwner.GetPsiServices();
+ }
+
+ private ITreeNode owner;
public string ShortName { get; }
public bool CaseSensitiveName { get; }
public PsiLanguageType PresentationLanguage { get; }
-
- private readonly IPsiServices myPsiServices;
-
+
+ private IPsiServices myPsiServices;
+
public DeclaredElementType GetElementType() => XmlTagDeclaredElemntType.Instance;
public bool IsValid() => true;
-
+
public bool IsSynthetic() => false;
public IList GetDeclarations()
@@ -47,7 +53,7 @@ public IList GetDeclarationsIn(IPsiSourceFile sourceFile)
{
if (!HasDeclarationsIn(sourceFile)) return EmptyList.Instance;
- return new List {new XmlTagDeclaration(owner, this, ShortName)};
+ return new List { new XmlTagDeclaration(owner, this, ShortName) };
}
public HybridCollection GetSourceFiles()
@@ -64,8 +70,7 @@ public bool HasDeclarationsIn(IPsiSourceFile sourceFile)
public IPsiServices GetPsiServices() => myPsiServices;
- public XmlNode GetXMLDoc(bool inherit) => (XmlNode) null;
-
- public XmlNode GetXMLDescriptionSummary(bool inherit) => (XmlNode) null;
+ public XmlNode GetXMLDoc(bool inherit) => (XmlNode)null;
+ public XmlNode GetXMLDescriptionSummary(bool inherit) => (XmlNode)null;
}
\ No newline at end of file
diff --git a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs
index 9245178..3929b4b 100644
--- a/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs
+++ b/src/dotnet/ReSharperPlugin.RimworldDev/TypeDeclaration/XmlTagDeclaration.cs
@@ -15,13 +15,13 @@ namespace ReSharperPlugin.RimworldDev.TypeDeclaration;
/**
* In order to have an IDeclaredElement for XMLTags, we need a IDeclaration implementation for it. For the most part this
- * is just a wrapper around the original IXmlTag
+ * is just a wrapper around the original ITreeNode
*/
-public class XmlTagDeclaration: IXmlTag, IDeclaration
+public class XmlTagDeclaration: IDeclaration
{
- private readonly IXmlTag owner;
+ private readonly ITreeNode owner;
- public XmlTagDeclaration(IXmlTag owner, IDeclaredElement declaredElement, string declaredName)
+ public XmlTagDeclaration(ITreeNode owner, IDeclaredElement declaredElement, string declaredName)
{
this.owner = owner;
DeclaredElement = declaredElement;
@@ -87,57 +87,6 @@ public IReadOnlyCollection FindNodesAt(TreeOffset treeOffset) =>
public NodeUserData PersistentUserData => owner.PersistentUserData;
- public TReturn AcceptVisitor(IXmlTreeVisitor visitor, TContext context) =>
- owner.AcceptVisitor(visitor, context);
-
- public XmlTokenTypes XmlTokenTypes => owner.XmlTokenTypes;
-
- public IXmlTag GetTag(Predicate predicate) => owner.GetTag(predicate);
-
- public TreeNodeEnumerable GetTags() where T : class, IXmlTag => owner.GetTags();
-
- public TreeNodeCollection GetTags2() where T : class, IXmlTag => owner.GetTags2();
-
- public IList GetNestedTags(string xpath) where T : class, IXmlTag => owner.GetNestedTags(xpath);
-
- public TXmlTag AddTagBefore(TXmlTag tag, IXmlTag anchor) where TXmlTag : class, IXmlTag =>
- owner.AddTagBefore(tag, anchor);
-
- public TXmlTag AddTagAfter(TXmlTag tag, IXmlTag anchor) where TXmlTag : class, IXmlTag =>
- owner.AddTagAfter(tag, anchor);
-
- public void RemoveTag(IXmlTag tag) => owner.RemoveTag(tag);
-
- public TreeNodeCollection InnerTags => owner.InnerTags;
-
- public TXmlAttribute AddAttributeBefore(TXmlAttribute attribute, IXmlAttribute anchor)
- where TXmlAttribute : class, IXmlAttribute =>
- owner.AddAttributeBefore(attribute, anchor);
-
- public TXmlAttribute AddAttributeAfter(TXmlAttribute attribute, IXmlAttribute anchor)
- where TXmlAttribute : class, IXmlAttribute =>
- owner.AddAttributeAfter(attribute, anchor);
-
- public void RemoveAttribute(IXmlAttribute attribute) => owner.RemoveAttribute(attribute);
-
- public IXmlTagHeader Header => owner.Header;
-
- public IXmlTagFooter Footer => owner.Footer;
-
- public bool IsEmptyTag => owner.IsEmptyTag;
-
- public ITreeRange InnerXml => owner.InnerXml;
-
- public TreeNodeCollection InnerTextTokens => owner.InnerTextTokens;
-
- public string InnerText => owner.InnerText;
-
- public string InnerValue => owner.InnerValue;
-
- public IXmlTagHeader HeaderNode => owner.HeaderNode;
-
- public IXmlTagFooter FooterNode => owner.FooterNode;
-
// IDeclaration
public XmlNode GetXMLDoc(bool inherit) => null;
diff --git a/src/rider/main/resources/META-INF/plugin.xml b/src/rider/main/resources/META-INF/plugin.xml
index 54aec81..fb6bed4 100644
--- a/src/rider/main/resources/META-INF/plugin.xml
+++ b/src/rider/main/resources/META-INF/plugin.xml
@@ -1,7 +1,7 @@
com.jetbrains.rider.plugins.rimworlddev
Rimworld Development Environment
- 2023.3.3
+ 2023.4
Garethp
com.intellij.modules.rider
@@ -18,7 +18,10 @@ in your mods!
-- Fixed some missed API Changes in Rider 2023.3.3
+- Added a Find Usages for XML Defs
+- Added support for Custom Def Classes
+- Add a new "Generate" menu feature to add props to your XML
+- Improved detection of various properties that weren't being picked up as references before
]]>