diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cba12eb..c9c3a2a 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,7 +24,7 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout
- uses: actions/checkout@v4
+ uses: actions/checkout@v6
with:
fetch-depth: 0
@@ -40,8 +40,7 @@ jobs:
exit 1
fi
- - name: Setup Resonite environment
- id: resonite
+ - name: Setup current Resonite environment
uses: resonite-modding-group/setup-resonite-env-action@v0.1.0
with:
steam-user: ${{ secrets.STEAMUSER }}
@@ -65,9 +64,9 @@ jobs:
https://github.com/resonite-modding-group/ResoniteModLoader/releases/latest/download/0Harmony.dll
- name: Setup .NET SDK
- uses: actions/setup-dotnet@v4
+ uses: actions/setup-dotnet@v5
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Cache NuGet packages
uses: actions/cache@v4
@@ -112,14 +111,12 @@ jobs:
set -euo pipefail
artifacts_dir=release-artifacts
mkdir -p "$artifacts_dir"
- cp src/ReferenceReplacement/bin/Release/net9.0/ReferenceReplacement.dll "$artifacts_dir/ReferenceReplacement.dll"
- if [ -f src/ReferenceReplacement/bin/Release/net9.0/ReferenceReplacement.pdb ]; then
- cp src/ReferenceReplacement/bin/Release/net9.0/ReferenceReplacement.pdb "$artifacts_dir/ReferenceReplacement.pdb"
- fi
+ cp src/ReferenceReplacement/bin/Release/net10.0/ReferenceReplacement.dll "$artifacts_dir/ReferenceReplacement.dll"
+ cp src/ReferenceReplacement/bin/Release/net10.0/ReferenceReplacement.pdb "$artifacts_dir/ReferenceReplacement.pdb"
- name: Upload release artifacts
if: startsWith(github.ref, 'refs/tags/v')
- uses: actions/upload-artifact@v4
+ uses: actions/upload-artifact@v7
with:
name: reference-replacement
path: release-artifacts
@@ -129,11 +126,12 @@ jobs:
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/v')
+ timeout-minutes: 10
permissions:
contents: write
steps:
- name: Download build artifacts
- uses: actions/download-artifact@v4
+ uses: actions/download-artifact@v7
with:
name: reference-replacement
path: release-artifacts
diff --git a/Directory.Build.props b/Directory.Build.props
index 92915b9..c301b98 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,9 +1,15 @@
+ net10.0
+ enable
+ latest
true
latest-all
enable
true
+ false
+ false
+ false
@@ -16,4 +22,22 @@
$([System.IO.Path]::GetFullPath('$(ResonitePath)'))
+
+
+
+ $([MSBuild]::ValueOrDefault('$(IsTestProject)','false'))
+
+
+
+
+
+ $(ResolvedResonitePath)/FrooxEngine.dll
+
+
+ $(ResolvedResonitePath)/Elements.Core.dll
+
+
+ $(ResolvedResonitePath)/Renderite.Shared.dll
+
+
diff --git a/README.md b/README.md
index 41c5c48..af17476 100644
--- a/README.md
+++ b/README.md
@@ -4,6 +4,8 @@ A [ResoniteModLoader](https://github.com/resonite-modding-group/ResoniteModLoade
Launch the tool via `Create New > Editor > Reference Replacement (Mod)` in the Dev Create menu.
+Reference discovery traverses nested sync members, lists, dictionaries, slot components, and child slots while avoiding duplicate visits. Matches with an incompatible target type are reported but never replaced.
+
## Installation
1. Install [ResoniteModLoader](https://github.com/resonite-modding-group/ResoniteModLoader).
@@ -12,7 +14,9 @@ Launch the tool via `Create New > Editor > Reference Replacement (Mod)` in the D
## Build & Hot Reload
-1. Install the .NET 9 SDK.
+1. Install the .NET 10 SDK.
2. `dotnet build ReferenceReplacement.sln` auto-detects the Resonite install next to this repo, the default Steam Windows path, then the default Steam Linux path. If the game lives elsewhere, pass `-p:ResonitePath="/absolute/path/to/Resonite"` so the build can find `FrooxEngine.dll`, `Elements.Core.dll`, `Libraries/ResoniteModLoader.dll`, and `rml_libs/0Harmony.dll`.
3. Set `CopyToMods=true` when invoking `dotnet build` to copy the compiled DLL into `$(ResonitePath)/rml_mods` after each build.
4. Drop `ResoniteHotReloadLib.dll` (and `ResoniteHotReloadLibCore.dll`) into `$(ResonitePath)/rml_libs` and build with `-p:EnableResoniteHotReloadLib=true` if you want the Dev Tool’s **Hot Reload Mods** panel to reload this mod without restarting Resonite. Leave the property unset on machines without the DLL.
+
+CI provisions the current Resonite assembly set with `setup-resonite-env`, then runs restore, formatting, build, and tests against those runtime assemblies before publishing a tagged release.
diff --git a/src/ReferenceReplacement/Logic/ReferenceMatchCollector.cs b/src/ReferenceReplacement/Logic/ReferenceMatchCollector.cs
new file mode 100644
index 0000000..31b826f
--- /dev/null
+++ b/src/ReferenceReplacement/Logic/ReferenceMatchCollector.cs
@@ -0,0 +1,57 @@
+using System;
+using System.Collections.Generic;
+
+using FrooxEngine;
+
+namespace ReferenceReplacement.Logic;
+
+internal sealed class ReferenceMatchCollector
+{
+ private readonly IWorldElement _source;
+ private readonly IWorldElement _target;
+ private readonly HashSet _visitedRefs = new();
+ private readonly List _matches = new();
+
+ internal ReferenceMatchCollector(IWorldElement source, IWorldElement target)
+ {
+ _source = source ?? throw new ArgumentNullException(nameof(source));
+ _target = target ?? throw new ArgumentNullException(nameof(target));
+ }
+
+ public int IncompatibleCount { get; private set; }
+ public string? LastHitPath { get; private set; }
+
+ public void TryCapture(ISyncRef candidate, string path)
+ {
+ if (!_visitedRefs.Add(candidate))
+ {
+ return;
+ }
+
+ IWorldElement? current = candidate.Target;
+ if (current == null)
+ {
+ return;
+ }
+
+ if (!ReferenceEquals(current, _source) && current.ReferenceID != _source.ReferenceID)
+ {
+ return;
+ }
+
+ Type? requiredType = candidate.TargetType;
+ if (requiredType != null && !requiredType.IsInstanceOfType(_target))
+ {
+ IncompatibleCount++;
+ return;
+ }
+
+ LastHitPath = path;
+ _matches.Add(new SyncReferenceMatch(candidate, path));
+ }
+
+ public ReferenceScanResult BuildResult(int visitedMembers)
+ {
+ return new ReferenceScanResult(_matches.ToArray(), IncompatibleCount, visitedMembers, LastHitPath);
+ }
+}
diff --git a/src/ReferenceReplacement/Logic/ReferenceScanner.cs b/src/ReferenceReplacement/Logic/ReferenceScanner.cs
index b5ecf10..09dd71f 100644
--- a/src/ReferenceReplacement/Logic/ReferenceScanner.cs
+++ b/src/ReferenceReplacement/Logic/ReferenceScanner.cs
@@ -1,7 +1,5 @@
using System;
-using System.Collections;
using System.Collections.Generic;
-using System.Reflection;
using System.Runtime.CompilerServices;
using FrooxEngine;
@@ -10,327 +8,37 @@ namespace ReferenceReplacement.Logic;
internal static class ReferenceScanner
{
- public static ReferenceScanResult Scan(Slot root, IWorldElement source, IWorldElement target, Slot? excludedSlot = null)
+ public static ReferenceScanResult Scan(
+ Slot root,
+ IWorldElement source,
+ IWorldElement target,
+ Slot? excludedSlot = null,
+ ITraversalExceptionFilter? exceptionFilter = null)
{
ArgumentNullException.ThrowIfNull(root);
- ReferenceScanSession session = new(source, target, excludedSlot);
- session.VisitSlot(root, TraversalPath.FromSlot(root));
- return session.BuildResult();
- }
-
- internal static ReferenceScanResult Scan(HierarchyBlueprint blueprintRoot, IWorldElement source, IWorldElement target)
- {
- ArgumentNullException.ThrowIfNull(blueprintRoot);
+ TraversalCursor cursor = TraversalCursor.FromSlot(root);
+ ReferenceMatchCollector collector = new(source, target);
+ ReferenceTraversal traversal = new(exceptionFilter ?? DefaultTraversalExceptionFilter.Instance, collector);
+ traversal.TraverseSlot(root, cursor, excludedSlot);
- ReferenceScanSession session = new(source, target, excludedSlot: null);
- session.VisitBlueprint(blueprintRoot, new(blueprintRoot.Label));
- return session.BuildResult();
+ return collector.BuildResult(traversal.VisitedMembers);
}
- private sealed class ReferenceScanSession
+ internal static ReferenceScanResult Scan(
+ HierarchyBlueprint blueprintRoot,
+ IWorldElement source,
+ IWorldElement target,
+ ITraversalExceptionFilter? exceptionFilter = null)
{
- private readonly IWorldElement _source;
- private readonly IWorldElement _target;
- private readonly Slot? _excludedSlot;
- private readonly List _matches = new();
- private readonly HashSet _visitedRefs = new();
- private readonly HashSet