diff --git a/CommunityBugFixCollection/CommunityBugFixCollection.csproj b/CommunityBugFixCollection/CommunityBugFixCollection.csproj
index b067fac..2ce7bc0 100644
--- a/CommunityBugFixCollection/CommunityBugFixCollection.csproj
+++ b/CommunityBugFixCollection/CommunityBugFixCollection.csproj
@@ -10,7 +10,7 @@
True
CommunityBugFixCollection
Component Selector Additions
- Banane9; Nytra
+ Banane9; Nytra; art0007i
0.7.0-beta
This MonkeyLoader mod for Resonite overhauls the Component Selector / Protoflux Node Selector to have a search, as well as favorites and recents categories.
README.md
diff --git a/CommunityBugFixCollection/Contributors.cs b/CommunityBugFixCollection/Contributors.cs
index 949e3e1..f4fad1f 100644
--- a/CommunityBugFixCollection/Contributors.cs
+++ b/CommunityBugFixCollection/Contributors.cs
@@ -7,6 +7,8 @@ namespace CommunityBugFixCollection
{
internal static class Contributors
{
+ public static string[] Art0007i { get; } = ["art0007i"];
+
public static string[] Banane9 { get; } = ["Banane9"];
public static string[] Nytra { get; } = ["Nytra"];
diff --git a/CommunityBugFixCollection/DuplicateAndMoveMultipleGrabbedItems.cs b/CommunityBugFixCollection/DuplicateAndMoveMultipleGrabbedItems.cs
new file mode 100644
index 0000000..a204e2e
--- /dev/null
+++ b/CommunityBugFixCollection/DuplicateAndMoveMultipleGrabbedItems.cs
@@ -0,0 +1,237 @@
+using Elements.Core;
+using FrooxEngine;
+using FrooxEngine.Undo;
+using HarmonyLib;
+using MonkeyLoader.Resonite;
+using System;
+using System.Collections.Generic;
+using System.Text;
+using static FrooxEngine.Worker;
+
+// This code is mainly decompiled Resonite source code,
+// but the fix was devised by art0007i.
+// Originally released under MIT-0 here:
+// https://github.com/art0007i/DuplicateFix
+
+namespace CommunityBugFixCollection
+{
+ internal static class DuplicateExtensions
+ {
+ // Literally just a copy paste of Slot.Duplicate but with a tiny snippet added to the middle
+ // would probably do this using a transpiler but it would require some kind of additional function argument to whether it should create undo steps
+ // or I could check stack traces to see if it's called from within my code but that's a little jank
+
+ public static Slot UndoableChildrenDuplicate(this Slot toDuplicate, Slot? duplicateRoot = null, bool keepGlobalTransform = true, DuplicationSettings? settings = null)
+ {
+ if (toDuplicate.IsRootSlot)
+ throw new Exception("Cannot duplicate root slot");
+
+ duplicateRoot ??= toDuplicate.Parent ?? toDuplicate.World.RootSlot;
+
+ if (duplicateRoot.IsChildOf(toDuplicate))
+ throw new Exception("Target for the duplicate hierarchy cannot be within the hierarchy of the source");
+
+ using var internalReferences = new InternalReferences();
+ var syncRefs = Pool.BorrowHashSet();
+ var slots = Pool.BorrowHashSet();
+ var postDuplication = Pool.BorrowList();
+
+ void DuplicationHandler(IDuplicationHandler handler)
+ {
+ handler.OnBeforeDuplicate(toDuplicate, out var onDuplicated);
+
+ if (onDuplicated is not null)
+ postDuplication.Add(onDuplicated);
+ }
+
+ toDuplicate.ForeachComponentInChildren(DuplicationHandler,
+ includeLocal: false, cacheItems: true);
+
+ toDuplicate.GenerateHierarchy(slots);
+ toDuplicate.CollectInternalReferences(toDuplicate, internalReferences, syncRefs, slots);
+ var duplicated = toDuplicate.InternalDuplicate(duplicateRoot, internalReferences, syncRefs, settings!);
+
+ if (keepGlobalTransform)
+ duplicated.CopyTransform(toDuplicate);
+
+ internalReferences.TransferReferences(false);
+
+ // arti stuff begin
+ foreach (var child in duplicated.Children)
+ child.CreateSpawnUndoPoint();
+ // arti stuff end
+
+ var duplicatedComponents = Pool.BorrowList();
+ duplicated.GetComponentsInChildren(duplicatedComponents);
+
+ foreach (var component in duplicatedComponents)
+ component.RunDuplicate();
+
+ Pool.Return(ref duplicatedComponents);
+ Pool.Return(ref syncRefs);
+
+ foreach (var postDuplicationAction in postDuplication)
+ postDuplicationAction();
+
+ Pool.Return(ref postDuplication);
+
+ return duplicated;
+ }
+ }
+
+ [HarmonyPatch]
+ [HarmonyPatchCategory(nameof(DuplicateAndMoveMultipleGrabbedItems))]
+ internal sealed class DuplicateAndMoveMultipleGrabbedItems : ResoniteMonkey
+ {
+ public override IEnumerable Authors => Contributors.Art0007i;
+ public override bool CanBeDisabled => true;
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(InteractionHandler), "DuplicateGrabbed", [])]
+ public static bool DuplicateGrabbedPrefix(InteractionHandler __instance)
+ {
+ if (!Enabled)
+ return false;
+
+ Slot? tempHolder = null;
+ Slot? dupeHolder = null;
+
+ __instance.World.BeginUndoBatch("Undo.DuplicateGrabbed".AsLocaleKey());
+
+ try
+ {
+ tempHolder = __instance.Grabber.Slot.AddSlot("Holder");
+
+ foreach (var grabbedObject in __instance.Grabber.GrabbedObjects)
+ {
+ if (__instance.Grabber.GrabbableGetComponentInParents(grabbedObject.Slot, excludeDisabled: true) == null)
+ continue;
+
+ grabbedObject.Slot.SetParent(tempHolder, false);
+ }
+
+ // by the time the duplication is done the children will have already escaped using their Grabbable.OnDuplicate function
+ // so I just copied the entire duplication sequence and made it create undo steps at the correct time.. kinda jank but works
+ /*dupeHolder = __instance.Grabber.HolderSlot.Duplicate();
+
+ foreach (var child in dupeHolder.Children)
+ {
+ child.CreateSpawnUndoPoint();
+ }*/
+
+ dupeHolder = __instance.Grabber.HolderSlot.UndoableChildrenDuplicate();
+
+ dupeHolder.GetComponentsInChildren().ForEach(static grabbable =>
+ {
+ if (grabbable.IsGrabbed)
+ {
+ grabbable.Release(grabbable.Grabber);
+ }
+ });
+
+ tempHolder.Destroy(__instance.Grabber.HolderSlot, false);
+ }
+ catch (Exception ex)
+ {
+ __instance.Debug.Error("Exception duplicating items!\n" + ex);
+ }
+
+ dupeHolder!.FilterWorldElement()?.Destroy(false);
+ tempHolder!.FilterWorldElement()?.Destroy(false);
+
+ __instance.World.EndUndoBatch();
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Grabber), nameof(Grabber.OnFocusChanged))]
+ public static bool OnFocusChangedPrefix(Grabber __instance, World.WorldFocus focus)
+ {
+ if (!Enabled)
+ return true;
+
+ if (!__instance.World.CanTransferObjectsOut())
+ return false;
+
+ __instance.BeforeUserspaceTransfer?.Invoke();
+ __instance.CleanupGrabbed();
+
+ if (focus != 0 || !__instance.IsHoldingObjects)
+ return false;
+
+ foreach (var grabbedObject in __instance.GrabbedObjects)
+ {
+ if (grabbedObject.Slot.GetComponentInChildren(static (IItemPermissions p) => !p.CanSave) is not null)
+ return false;
+ }
+
+ if (__instance.HolderSlot.LocalPosition != float3.Zero)
+ {
+ var holderOffset = __instance.HolderSlot.LocalPosition;
+ __instance.HolderSlot.LocalPosition = float3.Zero;
+
+ foreach (var child in __instance.HolderSlot.Children)
+ child.LocalPosition += holderOffset;
+ }
+
+ if (Userspace.TryTransferToUserspaceGrabber(__instance.HolderSlot, __instance.LinkingKey))
+ __instance.DestroyGrabbed();
+
+ return false;
+ }
+
+ [HarmonyPrefix]
+ [HarmonyPatch(typeof(Userspace), nameof(Userspace.Paste))]
+ public static bool PastePrefix(ref Job __result, SavedGraph data, Slot source, float3 userspacePos, floatQ userspaceRot, float3 userspaceScale, World targetWorld)
+ {
+ if (!Enabled)
+ return true;
+
+ var task = new Job();
+ var world = targetWorld ?? Engine.Current.WorldManager.FocusedWorld;
+
+ world.RunSynchronously(() =>
+ {
+ Slot? slot = null;
+
+ if (world.CanSpawnObjects())
+ {
+ var globalPosition = WorldManager.TransferPoint(userspacePos, Userspace.Current.World, world);
+ var globalRotation = WorldManager.TransferRotation(userspaceRot, Userspace.Current.World, world);
+ var globalScale = WorldManager.TransferScale(userspaceScale, Userspace.Current.World, world);
+
+ if (source?.World == world && !source.IsDestroyed)
+ {
+ source.ActiveSelf = true;
+ source.GlobalPosition = globalPosition;
+ source.GlobalRotation = globalRotation;
+ source.GlobalScale = globalScale;
+ task.SetResultAndFinish(source);
+ }
+ else
+ {
+ slot = world.AddSlot("Paste");
+ slot.LoadObject(data.Root, null!);
+ slot.GlobalPosition = globalPosition;
+ slot.GlobalRotation = globalRotation;
+ slot.GlobalScale = globalScale;
+
+ slot.RunOnPaste();
+
+ if (slot.Name == "Holder")
+ slot.Destroy(slot.Parent, false);
+ }
+ }
+
+ if (source!.FilterWorldElement() is not null)
+ source!.World.RunSynchronously(source.Destroy);
+
+ task.SetResultAndFinish(slot!);
+ });
+
+ __result = task;
+ return false;
+ }
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 7865f52..866642a 100644
--- a/README.md
+++ b/README.md
@@ -33,5 +33,6 @@ just disable them in the settings in the meantime.
* Tools derived from `BrushTool` not firing *OnDequipped* events (https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/723)
* It not being possible to import multiple audio clips at once (https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/737)
* URLs to text files or Resonite Packages failing to import instead of appearing as a hyperlink (https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/785)
+* References in multiple duplicated or transferred-between-worlds items breaking (https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/984)
* UserInspectors not listing existing users in the session for non-host users (https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/1964)
* Animators updating all associated fields every frame while enabled but not playing (https://github.com/Yellow-Dog-Man/Resonite-Issues/issues/3480)
\ No newline at end of file