diff --git a/Assets/Gothic-Tests/PlayMode/Gothic.Tests.PlayMode.asmdef b/Assets/Gothic-Tests/PlayMode/Gothic.Tests.PlayMode.asmdef
index f77ed9518..00df3fc5a 100644
--- a/Assets/Gothic-Tests/PlayMode/Gothic.Tests.PlayMode.asmdef
+++ b/Assets/Gothic-Tests/PlayMode/Gothic.Tests.PlayMode.asmdef
@@ -5,6 +5,8 @@
"UnityEngine.TestRunner",
"UnityEditor.TestRunner",
"Gothic.Core",
+ "Gothic.VR",
+ "Unity.XR.Interaction.Toolkit",
"Unity.InputSystem",
"Unity.InputSystem.TestFramework"
],
diff --git a/Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs b/Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs
new file mode 100644
index 000000000..942045e57
--- /dev/null
+++ b/Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs
@@ -0,0 +1,235 @@
+using System.Collections;
+using System.Collections.Generic;
+using System.Reflection;
+using Gothic.Core.Adapters.Vob;
+using Gothic.VR.Interaction.Backends.Xri;
+using Gothic.VR.Interaction.Components;
+using Gothic.VR.Interaction.Models;
+using Gothic.VR.Interaction.Ports;
+using NUnit.Framework;
+using UnityEngine;
+using UnityEngine.TestTools;
+using UnityEngine.XR.Interaction.Toolkit;
+using UnityEngine.XR.Interaction.Toolkit.Interactables;
+using UnityEngine.XR.Interaction.Toolkit.Interactors;
+
+namespace Gothic.Tests.PlayMode
+{
+ ///
+ /// Checks the VobLoader hierarchy of items put into and taken out of a VrSocketSlot (HVR phase 3a).
+ /// Runs without a Gothic installation: the items are plain GameObjects with VobLoader -> grabbable -> cube
+ /// (no VobContainer, therefore no category rotation and no item filter).
+ ///
+ public class VrSocketSlotTest
+ {
+ private const float CubeSize = 0.4f;
+ private const float Tolerance = 0.001f;
+
+ private readonly List _created = new();
+
+ [TearDown]
+ public void TearDown()
+ {
+ foreach (var go in _created)
+ {
+ if (go != null)
+ Object.Destroy(go);
+ }
+
+ _created.Clear();
+ }
+
+ [UnityTest]
+ public IEnumerator PlaceAndRemoveWithoutBackend()
+ {
+ var world = Create("World");
+ var slot = Create("Slot").AddComponent();
+ slot.transform.position = new Vector3(0, 1, 0);
+ var item = CreateItem(world.transform, withXri: false);
+ item.Grabbable.transform.localPosition = new Vector3(0.2f, 0, 0); // As if a hand moved it.
+
+ var placed = 0;
+ var removed = 0;
+ slot.Grabbed += args => placed += args.Kind == VrGrabberKind.Socket ? 1 : 100;
+ slot.Released += _ => removed++;
+ yield return null;
+
+ Assert.IsTrue(slot.TryPlace(item.Interactable, true));
+ AssertInSlot(slot, item);
+ Assert.AreEqual(1, placed);
+ Assert.IsFalse(slot.TryPlace(item.Interactable, true), "An occupied slot must not take a second item.");
+
+ slot.ForceRelease();
+ AssertRemoved(slot, item, world.transform);
+ Assert.AreEqual(1, removed);
+ }
+
+ [UnityTest]
+ public IEnumerator PlaceAndRemoveOnXri()
+ {
+ Create("XR Interaction Manager").AddComponent();
+ var world = Create("World");
+ var slot = CreateXriSlot(new Vector3(0, 1, 0));
+ var item = CreateItem(world.transform, withXri: true);
+ yield return null;
+ yield return null;
+
+ Assert.IsTrue(slot.TryPlace(item.Interactable, true));
+ for (var i = 0; i < 5; i++)
+ yield return new WaitForFixedUpdate();
+ yield return null;
+
+ Assert.IsTrue(item.GrabInteractable.isSelected, "XRI didn't select the item in the socket.");
+ Assert.IsTrue(item.GrabInteractable.IsSocketed);
+ AssertInSlot(slot, item);
+
+ // The item follows the slot, XRI doesn't move the grabbable child.
+ slot.transform.position += new Vector3(1, 0, 0);
+ yield return new WaitForFixedUpdate();
+ yield return null;
+ AssertInSlot(slot, item);
+
+ slot.ForceRelease();
+ yield return null;
+ Assert.IsFalse(item.GrabInteractable.isSelected);
+ AssertRemoved(slot, item, world.transform);
+ }
+
+ [UnityTest]
+ public IEnumerator HandTakesItemOutOnXri()
+ {
+ var manager = Create("XR Interaction Manager").AddComponent();
+ var world = Create("World");
+ var slot = CreateXriSlot(new Vector3(0, 1, 0));
+ var item = CreateItem(world.transform, withXri: true);
+ var hand = Create("Hand").AddComponent();
+ yield return null;
+ yield return null;
+
+ slot.TryPlace(item.Interactable, true);
+ yield return null;
+ Assert.IsTrue(item.GrabInteractable.IsSocketed);
+
+ var removedWhileHandHeld = true;
+ slot.Released += _ => removedWhileHandHeld = item.GrabInteractable.isSelected;
+ manager.SelectEnter((IXRSelectInteractor)hand, item.GrabInteractable);
+ yield return null;
+
+ Assert.IsFalse(removedWhileHandHeld, "The socket must release the item before the hand's grab starts.");
+ Assert.IsFalse(slot.IsOccupied);
+ Assert.IsTrue(hand.IsSelecting(item.GrabInteractable));
+ Assert.AreSame(item.Loader.transform, item.Grabbable.transform.parent,
+ "The grabbable must stay below its VobLoader.");
+ Assert.AreSame(world.transform, item.Loader.transform.parent);
+ Assert.That(item.Loader.transform.localScale.x, Is.EqualTo(1f).Within(Tolerance));
+ }
+
+ private void AssertInSlot(VrSocketSlot slot, TestItem item)
+ {
+ Assert.IsTrue(slot.IsOccupied);
+ Assert.AreSame(item.Interactable, slot.Held);
+ Assert.AreSame(slot.AttachPoint, item.Loader.transform.parent, "The VobLoader must be in the slot.");
+ Assert.AreSame(item.Loader.transform, item.Grabbable.transform.parent,
+ "The grabbable must stay below its VobLoader.");
+ Assert.That(item.Grabbable.transform.localPosition.magnitude, Is.LessThan(Tolerance));
+ Assert.IsTrue(item.Rigidbody.isKinematic);
+
+ // Scale-to-fit: half of the largest side becomes Size.
+ var expectedScale = slot.Size / (CubeSize / 2);
+ Assert.That(item.Loader.transform.localScale.x, Is.EqualTo(expectedScale).Within(Tolerance));
+ Assert.That(Vector3.Distance(item.Cube.bounds.center, slot.AttachPoint.position),
+ Is.LessThan(Tolerance), "The item must be centered in the slot.");
+ }
+
+ private static void AssertRemoved(VrSocketSlot slot, TestItem item, Transform world)
+ {
+ Assert.IsFalse(slot.IsOccupied);
+ Assert.IsNull(slot.Held);
+ Assert.AreSame(world, item.Loader.transform.parent, "The VobLoader must be back at its previous parent.");
+ Assert.AreSame(item.Loader.transform, item.Grabbable.transform.parent);
+ Assert.That(item.Loader.transform.localScale.x, Is.EqualTo(1f).Within(Tolerance));
+ Assert.IsFalse(item.Rigidbody.isKinematic, "The kinematic state before the slot must be restored.");
+ }
+
+ private VrSocketSlot CreateXriSlot(Vector3 position)
+ {
+ var go = Create("Slot");
+ go.SetActive(false);
+ go.transform.position = position;
+ go.AddComponent().isTrigger = true;
+ var slot = go.AddComponent();
+ var bridge = go.AddComponent();
+ bridge.enabled = false;
+ InjectXriBackend(bridge);
+ go.SetActive(true);
+ return slot;
+ }
+
+ private TestItem CreateItem(Transform parent, bool withXri)
+ {
+ var loaderGo = Create("Item");
+ loaderGo.transform.SetParent(parent, false);
+ loaderGo.transform.position = new Vector3(2, 0, 0);
+ var loader = loaderGo.AddComponent();
+
+ var grabbableGo = new GameObject("Grabbable");
+ grabbableGo.SetActive(false);
+ grabbableGo.transform.SetParent(loaderGo.transform, false);
+ var body = grabbableGo.AddComponent();
+ body.useGravity = false;
+ var interactable = grabbableGo.AddComponent();
+
+ var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
+ cube.transform.SetParent(grabbableGo.transform, false);
+ cube.transform.localScale = Vector3.one * CubeSize;
+ cube.transform.localPosition = new Vector3(0, 0.3f, 0); // Pivot not in the center, like many items.
+
+ GothicGrabInteractable grab = null;
+ if (withXri)
+ {
+ InjectXriBackend(grabbableGo.AddComponent());
+ grab = grabbableGo.AddComponent();
+ grab.enabled = false;
+ grab.unparentTransformOnGrab = false;
+ }
+
+ grabbableGo.SetActive(true);
+ return new TestItem(loader, grabbableGo, interactable, body, cube.GetComponent(), grab);
+ }
+
+ private static void InjectXriBackend(Component component)
+ {
+ var field = component.GetType().GetField("_vrBackend", BindingFlags.Instance | BindingFlags.NonPublic);
+ Assert.IsNotNull(field, $"{component.GetType().Name} has no _vrBackend field.");
+ field.SetValue(component, new XriBackend());
+ }
+
+ private GameObject Create(string goName)
+ {
+ var go = new GameObject(goName);
+ _created.Add(go);
+ return go;
+ }
+
+ private readonly struct TestItem
+ {
+ public readonly VobLoader Loader;
+ public readonly GameObject Grabbable;
+ public readonly VrInteractable Interactable;
+ public readonly Rigidbody Rigidbody;
+ public readonly Renderer Cube;
+ public readonly GothicGrabInteractable GrabInteractable;
+
+ public TestItem(VobLoader loader, GameObject grabbable, VrInteractable interactable, Rigidbody rigidbody,
+ Renderer cube, GothicGrabInteractable grabInteractable)
+ {
+ Loader = loader;
+ Grabbable = grabbable;
+ Interactable = interactable;
+ Rigidbody = rigidbody;
+ Cube = cube;
+ GrabInteractable = grabInteractable;
+ }
+ }
+ }
+}
diff --git a/Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs.meta b/Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs.meta
new file mode 100644
index 000000000..f1a0f66a6
--- /dev/null
+++ b/Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 4d9aec3fcbf7e814c8b4e774efe1b0e3
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Materials/SocketRing-Invalid.mat b/Assets/Gothic-VR/Materials/SocketRing-Invalid.mat
new file mode 100644
index 000000000..298c385e7
--- /dev/null
+++ b/Assets/Gothic-VR/Materials/SocketRing-Invalid.mat
@@ -0,0 +1,74 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: SocketRing-Invalid
+ m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords:
+ - _SURFACE_TYPE_TRANSPARENT
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: 3000
+ stringTagMap:
+ RenderType: Transparent
+ disabledShaderPasses:
+ - DepthOnly
+ - SHADOWCASTER
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendOp: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DstBlend: 10
+ - _DstBlendAlpha: 10
+ - _QueueOffset: 0
+ - _SampleGI: 0
+ - _SrcBlend: 5
+ - _SrcBlendAlpha: 1
+ - _Surface: 1
+ - _XRMotionVectorsPass: 1
+ - _ZWrite: 0
+ m_Colors:
+ - _BaseColor: {r: 0.95, g: 0.25, b: 0.2, a: 0.8}
+ - _Color: {r: 0.95, g: 0.24999997, b: 0.19999996, a: 0.8}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
+--- !u!114 &1898626942210310402
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
+ version: 10
diff --git a/Assets/Gothic-VR/Materials/SocketRing-Invalid.mat.meta b/Assets/Gothic-VR/Materials/SocketRing-Invalid.mat.meta
new file mode 100644
index 000000000..8e5945ed7
--- /dev/null
+++ b/Assets/Gothic-VR/Materials/SocketRing-Invalid.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 52dc35718064d9c43ab35b777254779c
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Gothic-VR/Materials/SocketRing-Valid.mat b/Assets/Gothic-VR/Materials/SocketRing-Valid.mat
new file mode 100644
index 000000000..b16c7e100
--- /dev/null
+++ b/Assets/Gothic-VR/Materials/SocketRing-Valid.mat
@@ -0,0 +1,74 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: SocketRing-Valid
+ m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords:
+ - _SURFACE_TYPE_TRANSPARENT
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: 3000
+ stringTagMap:
+ RenderType: Transparent
+ disabledShaderPasses:
+ - DepthOnly
+ - SHADOWCASTER
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendOp: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DstBlend: 10
+ - _DstBlendAlpha: 10
+ - _QueueOffset: 0
+ - _SampleGI: 0
+ - _SrcBlend: 5
+ - _SrcBlendAlpha: 1
+ - _Surface: 1
+ - _XRMotionVectorsPass: 1
+ - _ZWrite: 0
+ m_Colors:
+ - _BaseColor: {r: 0.3, g: 0.9, b: 0.35, a: 0.8}
+ - _Color: {r: 0.29999998, g: 0.9, b: 0.34999996, a: 0.8}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
+--- !u!114 &9069054574587120345
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
+ version: 10
diff --git a/Assets/Gothic-VR/Materials/SocketRing-Valid.mat.meta b/Assets/Gothic-VR/Materials/SocketRing-Valid.mat.meta
new file mode 100644
index 000000000..e2cd9d0d7
--- /dev/null
+++ b/Assets/Gothic-VR/Materials/SocketRing-Valid.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 086e8cac1c176f14982891c8610dd9d7
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Gothic-VR/Materials/SocketRing.mat b/Assets/Gothic-VR/Materials/SocketRing.mat
new file mode 100644
index 000000000..70f555311
--- /dev/null
+++ b/Assets/Gothic-VR/Materials/SocketRing.mat
@@ -0,0 +1,74 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!21 &2100000
+Material:
+ serializedVersion: 8
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: SocketRing
+ m_Shader: {fileID: 4800000, guid: 650dd9526735d5b46b79224bc6e94025, type: 3}
+ m_Parent: {fileID: 0}
+ m_ModifiedSerializedProperties: 0
+ m_ValidKeywords:
+ - _SURFACE_TYPE_TRANSPARENT
+ m_InvalidKeywords: []
+ m_LightmapFlags: 4
+ m_EnableInstancingVariants: 0
+ m_DoubleSidedGI: 0
+ m_CustomRenderQueue: 3000
+ stringTagMap:
+ RenderType: Transparent
+ disabledShaderPasses:
+ - DepthOnly
+ - SHADOWCASTER
+ - MOTIONVECTORS
+ m_LockedProperties:
+ m_SavedProperties:
+ serializedVersion: 3
+ m_TexEnvs:
+ - _BaseMap:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ - _MainTex:
+ m_Texture: {fileID: 0}
+ m_Scale: {x: 1, y: 1}
+ m_Offset: {x: 0, y: 0}
+ m_Ints: []
+ m_Floats:
+ - _AddPrecomputedVelocity: 0
+ - _AlphaClip: 0
+ - _AlphaToMask: 0
+ - _Blend: 0
+ - _BlendOp: 0
+ - _Cull: 2
+ - _Cutoff: 0.5
+ - _DstBlend: 10
+ - _DstBlendAlpha: 10
+ - _QueueOffset: 0
+ - _SampleGI: 0
+ - _SrcBlend: 5
+ - _SrcBlendAlpha: 1
+ - _Surface: 1
+ - _XRMotionVectorsPass: 1
+ - _ZWrite: 0
+ m_Colors:
+ - _BaseColor: {r: 0.85, g: 0.8, b: 0.65, a: 0.55}
+ - _Color: {r: 0.85, g: 0.8, b: 0.65, a: 0.55}
+ m_BuildTextureStacks: []
+ m_AllowLocking: 1
+--- !u!114 &8790264341158592662
+MonoBehaviour:
+ m_ObjectHideFlags: 11
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 0}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: d0353a89b1f911e48b9e16bdc9f2e058, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Unity.RenderPipelines.Universal.Editor::UnityEditor.Rendering.Universal.AssetVersion
+ version: 10
diff --git a/Assets/Gothic-VR/Materials/SocketRing.mat.meta b/Assets/Gothic-VR/Materials/SocketRing.mat.meta
new file mode 100644
index 000000000..6c495ed92
--- /dev/null
+++ b/Assets/Gothic-VR/Materials/SocketRing.mat.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: a3dcff690c3412844a33e16319700e44
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 2100000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Gothic-VR/Meshes.meta b/Assets/Gothic-VR/Meshes.meta
new file mode 100644
index 000000000..b3c09a548
--- /dev/null
+++ b/Assets/Gothic-VR/Meshes.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: 6377ff1fd9527ec43ba907ea65980050
+folderAsset: yes
+DefaultImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Gothic-VR/Meshes/SocketRing.asset b/Assets/Gothic-VR/Meshes/SocketRing.asset
new file mode 100644
index 000000000..3e61e3081
--- /dev/null
+++ b/Assets/Gothic-VR/Meshes/SocketRing.asset
@@ -0,0 +1,180 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!43 &4300000
+Mesh:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_Name: SocketRing
+ serializedVersion: 12
+ m_SubMeshes:
+ - serializedVersion: 2
+ firstByte: 0
+ indexCount: 2880
+ topology: 0
+ baseVertex: 0
+ firstVertex: 0
+ vertexCount: 539
+ localAABB:
+ m_Center: {x: 0, y: 0, z: 1.1641532e-10}
+ m_Extent: {x: 0.063999996, y: 0.063999996, z: 0.0038042264}
+ m_Shapes:
+ vertices: []
+ shapes: []
+ channels: []
+ fullWeights: []
+ m_BindPose: []
+ m_BoneNameHashes:
+ m_RootBoneNameHash: 0
+ m_BonesAABB: []
+ m_VariableBoneCountWeights:
+ m_Data:
+ m_MeshCompression: 0
+ m_IsReadable: 1
+ m_KeepVertices: 0
+ m_KeepIndices: 0
+ m_IndexFormat: 0
+ m_IndexBuffer: 00000b0001000b000c00010001000c0002000c000d00020002000d0003000d000e00030003000e0004000e000f00040004000f0005000f0010000500050010000600100011000600060011000700110012000700070012000800120013000800080013000900130014000900090014000a00140015000a000b0016000c00160017000c000c0017000d00170018000d000d0018000e00180019000e000e0019000f0019001a000f000f001a0010001a001b00100010001b0011001b001c00110011001c0012001c001d00120012001d0013001d001e00130013001e0014001e001f00140014001f0015001f0020001500160021001700210022001700170022001800220023001800180023001900230024001900190024001a00240025001a001a0025001b00250026001b001b0026001c00260027001c001c0027001d00270028001d001d0028001e00280029001e001e0029001f0029002a001f001f002a0020002a002b00200021002c0022002c002d00220022002d0023002d002e00230023002e0024002e002f00240024002f0025002f0030002500250030002600300031002600260031002700310032002700270032002800320033002800280033002900330034002900290034002a00340035002a002a0035002b00350036002b002c0037002d00370038002d002d0038002e00380039002e002e0039002f0039003a002f002f003a0030003a003b00300030003b0031003b003c00310031003c0032003c003d00320032003d0033003d003e00330033003e0034003e003f00340034003f0035003f0040003500350040003600400041003600370042003800420043003800380043003900430044003900390044003a00440045003a003a0045003b00450046003b003b0046003c00460047003c003c0047003d00470048003d003d0048003e00480049003e003e0049003f0049004a003f003f004a0040004a004b00400040004b0041004b004c00410042004d0043004d004e00430043004e0044004e004f00440044004f0045004f0050004500450050004600500051004600460051004700510052004700470052004800520053004800480053004900530054004900490054004a00540055004a004a0055004b00550056004b004b0056004c00560057004c004d0058004e00580059004e004e0059004f0059005a004f004f005a0050005a005b00500050005b0051005b005c00510051005c0052005c005d00520052005d0053005d005e00530053005e0054005e005f00540054005f0055005f0060005500550060005600600061005600560061005700610062005700580063005900630064005900590064005a00640065005a005a0065005b00650066005b005b0066005c00660067005c005c0067005d00670068005d005d0068005e00680069005e005e0069005f0069006a005f005f006a0060006a006b00600060006b0061006b006c00610061006c0062006c006d00620063006e0064006e006f00640064006f0065006f0070006500650070006600700071006600660071006700710072006700670072006800720073006800680073006900730074006900690074006a00740075006a006a0075006b00750076006b006b0076006c00760077006c006c0077006d00770078006d006e0079006f0079007a006f006f007a0070007a007b00700070007b0071007b007c00710071007c0072007c007d00720072007d0073007d007e00730073007e0074007e007f00740074007f0075007f0080007500750080007600800081007600760081007700810082007700770082007800820083007800790084007a00840085007a007a0085007b00850086007b007b0086007c00860087007c007c0087007d00870088007d007d0088007e00880089007e007e0089007f0089008a007f007f008a0080008a008b00800080008b0081008b008c00810081008c0082008c008d00820082008d0083008d008e00830084008f0085008f0090008500850090008600900091008600860091008700910092008700870092008800920093008800880093008900930094008900890094008a00940095008a008a0095008b00950096008b008b0096008c00960097008c008c0097008d00970098008d008d0098008e00980099008e008f009a0090009a009b00900090009b0091009b009c00910091009c0092009c009d00920092009d0093009d009e00930093009e0094009e009f00940094009f0095009f00a00095009500a0009600a000a10096009600a1009700a100a20097009700a2009800a200a30098009800a3009900a300a40099009a00a5009b00a500a6009b009b00a6009c00a600a7009c009c00a7009d00a700a8009d009d00a8009e00a800a9009e009e00a9009f00a900aa009f009f00aa00a000aa00ab00a000a000ab00a100ab00ac00a100a100ac00a200ac00ad00a200a200ad00a300ad00ae00a300a300ae00a400ae00af00a400a500b000a600b000b100a600a600b100a700b100b200a700a700b200a800b200b300a800a800b300a900b300b400a900a900b400aa00b400b500aa00aa00b500ab00b500b600ab00ab00b600ac00b600b700ac00ac00b700ad00b700b800ad00ad00b800ae00b800b900ae00ae00b900af00b900ba00af00b000bb00b100bb00bc00b100b100bc00b200bc00bd00b200b200bd00b300bd00be00b300b300be00b400be00bf00b400b400bf00b500bf00c000b500b500c000b600c000c100b600b600c100b700c100c200b700b700c200b800c200c300b800b800c300b900c300c400b900b900c400ba00c400c500ba00bb00c600bc00c600c700bc00bc00c700bd00c700c800bd00bd00c800be00c800c900be00be00c900bf00c900ca00bf00bf00ca00c000ca00cb00c000c000cb00c100cb00cc00c100c100cc00c200cc00cd00c200c200cd00c300cd00ce00c300c300ce00c400ce00cf00c400c400cf00c500cf00d000c500c600d100c700d100d200c700c700d200c800d200d300c800c800d300c900d300d400c900c900d400ca00d400d500ca00ca00d500cb00d500d600cb00cb00d600cc00d600d700cc00cc00d700cd00d700d800cd00cd00d800ce00d800d900ce00ce00d900cf00d900da00cf00cf00da00d000da00db00d000d100dc00d200dc00dd00d200d200dd00d300dd00de00d300d300de00d400de00df00d400d400df00d500df00e000d500d500e000d600e000e100d600d600e100d700e100e200d700d700e200d800e200e300d800d800e300d900e300e400d900d900e400da00e400e500da00da00e500db00e500e600db00dc00e700dd00e700e800dd00dd00e800de00e800e900de00de00e900df00e900ea00df00df00ea00e000ea00eb00e000e000eb00e100eb00ec00e100e100ec00e200ec00ed00e200e200ed00e300ed00ee00e300e300ee00e400ee00ef00e400e400ef00e500ef00f000e500e500f000e600f000f100e600e700f200e800f200f300e800e800f300e900f300f400e900e900f400ea00f400f500ea00ea00f500eb00f500f600eb00eb00f600ec00f600f700ec00ec00f700ed00f700f800ed00ed00f800ee00f800f900ee00ee00f900ef00f900fa00ef00ef00fa00f000fa00fb00f000f000fb00f100fb00fc00f100f200fd00f300fd00fe00f300f300fe00f400fe00ff00f400f400ff00f500ff000001f500f5000001f60000010101f600f6000101f70001010201f700f7000201f80002010301f800f8000301f90003010401f900f9000401fa0004010501fa00fa000501fb0005010601fb00fb000601fc0006010701fc00fd000801fe0008010901fe00fe000901ff0009010a01ff00ff000a0100010a010b01000100010b0101010b010c01010101010c0102010c010d01020102010d0103010d010e01030103010e0104010e010f01040104010f0105010f0110010501050110010601100111010601060111010701110112010701080113010901130114010901090114010a01140115010a010a0115010b01150116010b010b0116010c01160117010c010c0117010d01170118010d010d0118010e01180119010e010e0119010f0119011a010f010f011a0110011a011b01100110011b0111011b011c01110111011c0112011c011d01120113011e0114011e011f01140114011f0115011f0120011501150120011601200121011601160121011701210122011701170122011801220123011801180123011901230124011901190124011a01240125011a011a0125011b01250126011b011b0126011c01260127011c011c0127011d01270128011d011e0129011f0129012a011f011f012a0120012a012b01200120012b0121012b012c01210121012c0122012c012d01220122012d0123012d012e01230123012e0124012e012f01240124012f0125012f0130012501250130012601300131012601260131012701310132012701270132012801320133012801290134012a01340135012a012a0135012b01350136012b012b0136012c01360137012c012c0137012d01370138012d012d0138012e01380139012e012e0139012f0139013a012f012f013a0130013a013b01300130013b0131013b013c01310131013c0132013c013d01320132013d0133013d013e01330134013f0135013f0140013501350140013601400141013601360141013701410142013701370142013801420143013801380143013901430144013901390144013a01440145013a013a0145013b01450146013b013b0146013c01460147013c013c0147013d01470148013d013d0148013e01480149013e013f014a0140014a014b01400140014b0141014b014c01410141014c0142014c014d01420142014d0143014d014e01430143014e0144014e014f01440144014f0145014f01500145014501500146015001510146014601510147015101520147014701520148015201530148014801530149015301540149014a0155014b01550156014b014b0156014c01560157014c014c0157014d01570158014d014d0158014e01580159014e014e0159014f0159015a014f014f015a0150015a015b01500150015b0151015b015c01510151015c0152015c015d01520152015d0153015d015e01530153015e0154015e015f015401550160015601600161015601560161015701610162015701570162015801620163015801580163015901630164015901590164015a01640165015a015a0165015b01650166015b015b0166015c01660167015c015c0167015d01670168015d015d0168015e01680169015e015e0169015f0169016a015f0160016b0161016b016c01610161016c0162016c016d01620162016d0163016d016e01630163016e0164016e016f01640164016f0165016f0170016501650170016601700171016601660171016701710172016701670172016801720173016801680173016901730174016901690174016a01740175016a016b0176016c01760177016c016c0177016d01770178016d016d0178016e01780179016e016e0179016f0179017a016f016f017a0170017a017b01700170017b0171017b017c01710171017c0172017c017d01720172017d0173017d017e01730173017e0174017e017f01740174017f0175017f0180017501760181017701810182017701770182017801820183017801780183017901830184017901790184017a01840185017a017a0185017b01850186017b017b0186017c01860187017c017c0187017d01870188017d017d0188017e01880189017e017e0189017f0189018a017f017f018a0180018a018b01800181018c0182018c018d01820182018d0183018d018e01830183018e0184018e018f01840184018f0185018f0190018501850190018601900191018601860191018701910192018701870192018801920193018801880193018901930194018901890194018a01940195018a018a0195018b01950196018b018c0197018d01970198018d018d0198018e01980199018e018e0199018f0199019a018f018f019a0190019a019b01900190019b0191019b019c01910191019c0192019c019d01920192019d0193019d019e01930193019e0194019e019f01940194019f0195019f01a00195019501a0019601a001a10196019701a2019801a201a30198019801a3019901a301a40199019901a4019a01a401a5019a019a01a5019b01a501a6019b019b01a6019c01a601a7019c019c01a7019d01a701a8019d019d01a8019e01a801a9019e019e01a9019f01a901aa019f019f01aa01a001aa01ab01a001a001ab01a101ab01ac01a101a201ad01a301ad01ae01a301a301ae01a401ae01af01a401a401af01a501af01b001a501a501b001a601b001b101a601a601b101a701b101b201a701a701b201a801b201b301a801a801b301a901b301b401a901a901b401aa01b401b501aa01aa01b501ab01b501b601ab01ab01b601ac01b601b701ac01ad01b801ae01b801b901ae01ae01b901af01b901ba01af01af01ba01b001ba01bb01b001b001bb01b101bb01bc01b101b101bc01b201bc01bd01b201b201bd01b301bd01be01b301b301be01b401be01bf01b401b401bf01b501bf01c001b501b501c001b601c001c101b601b601c101b701c101c201b701b801c301b901c301c401b901b901c401ba01c401c501ba01ba01c501bb01c501c601bb01bb01c601bc01c601c701bc01bc01c701bd01c701c801bd01bd01c801be01c801c901be01be01c901bf01c901ca01bf01bf01ca01c001ca01cb01c001c001cb01c101cb01cc01c101c101cc01c201cc01cd01c201c301ce01c401ce01cf01c401c401cf01c501cf01d001c501c501d001c601d001d101c601c601d101c701d101d201c701c701d201c801d201d301c801c801d301c901d301d401c901c901d401ca01d401d501ca01ca01d501cb01d501d601cb01cb01d601cc01d601d701cc01cc01d701cd01d701d801cd01ce01d901cf01d901da01cf01cf01da01d001da01db01d001d001db01d101db01dc01d101d101dc01d201dc01dd01d201d201dd01d301dd01de01d301d301de01d401de01df01d401d401df01d501df01e001d501d501e001d601e001e101d601d601e101d701e101e201d701d701e201d801e201e301d801d901e401da01e401e501da01da01e501db01e501e601db01db01e601dc01e601e701dc01dc01e701dd01e701e801dd01dd01e801de01e801e901de01de01e901df01e901ea01df01df01ea01e001ea01eb01e001e001eb01e101eb01ec01e101e101ec01e201ec01ed01e201e201ed01e301ed01ee01e301e401ef01e501ef01f001e501e501f001e601f001f101e601e601f101e701f101f201e701e701f201e801f201f301e801e801f301e901f301f401e901e901f401ea01f401f501ea01ea01f501eb01f501f601eb01eb01f601ec01f601f701ec01ec01f701ed01f701f801ed01ed01f801ee01f801f901ee01ef01fa01f001fa01fb01f001f001fb01f101fb01fc01f101f101fc01f201fc01fd01f201f201fd01f301fd01fe01f301f301fe01f401fe01ff01f401f401ff01f501ff010002f501f5010002f60100020102f601f6010102f70101020202f701f7010202f80102020302f801f8010302f90103020402f901fa010502fb0105020602fb01fb010602fc0106020702fc01fc010702fd0107020802fd01fd010802fe0108020902fe01fe010902ff0109020a02ff01ff010a0200020a020b02000200020b0201020b020c02010201020c0202020c020d02020202020d0203020d020e02030203020e0204020e020f020402050210020602100211020602060211020702110212020702070212020802120213020802080213020902130214020902090214020a02140215020a020a0215020b02150216020b020b0216020c02160217020c020c0217020d02170218020d020d0218020e02180219020e020e0219020f0219021a020f02
+ m_VertexData:
+ serializedVersion: 3
+ m_VertexCount: 539
+ m_Channels:
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 3
+ - stream: 0
+ offset: 12
+ format: 0
+ dimension: 3
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 24
+ format: 0
+ dimension: 2
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ - stream: 0
+ offset: 0
+ format: 0
+ dimension: 0
+ m_DataSize: 17248
+ _typelessdata: 6e12833d00000000000000000000803f00000000000000000000000000000000e981813d000000009a151a3bbd1b4f3f000000001879163f00000000cdcccc3dabd27a3d000000005450793b79379e3e000000007178733f00000000cdcc4c3e72b2703d000000005350793b7b379ebe000000007078733f000000009a99993e4c81683d0000000099151a3bbe1b4fbf000000001779163f00000000cdcccc3e4160653d00000000a63ec0af000080bf000000802ebdbbb3000000000000003f4c81683d000000009c151abbbc1b4fbf000000801a7916bf000000009a99193f72b2703d00000000535079bb7d379ebe00000080707873bf000000003333333facd27a3d00000000535079bb7e379e3e00000000707873bf00000000cdcc4c3fe981813d0000000095151abbc11b4f3f00000000137916bf000000006666663f6e12833d00000000a63e40300000803f000000002ebd3b34000000000000803f5ef3813ddcdd083c0000000055cf7d3fa9a8053e00000000abaaaa3c000000004666803da23b073c9a151a3b26564d3fb843d83d1879163fabaaaa3ccdcccc3d57ad783db3f4023c5450793bf6dc9c3e1d36253d7178733fabaaaa3ccdcc4c3e4ba36e3d9256fb3b5350793bf8dc9cbe203625bd7078733fabaaaa3c9a99993e1584663db5c8f23b99151a3b27564dbfb943d8bd1779163fabaaaa3ccdcccc3ee569633d4084ef3ba63ec0af55cf7dbfa9a805be2ebdbbb3abaaaa3c0000003f1584663db5c8f23b9c151abb25564dbfb743d8bd1a7916bfabaaaa3c9a99193f4ba36e3d9256fb3b535079bbfadc9cbe223625bd707873bfabaaaa3c3333333f57ad783db3f4023c535079bbfbdc9c3e2336253d707873bfabaaaa3ccdcc4c3f4766803da23b073c95151abb2a564d3fbc43d83d137916bfabaaaa3c6666663f5ef3813ddcdd083ca63e403055cf7d3fa9a8053e2ebd3b34abaaaa3c0000803f2f367d3d1bb2873c00000000ea46773fee83843e00000000abaa2a3d0000000070307a3d7413863c9a151a3b220d483f126a563e1879163fabaa2a3dcdcccc3dbe46723de4d5813c5450793b5ad3983e48cca33d7178733fabaa2a3dcdcc4c3ed97e683d1d30793c5350793b5bd398be4acca3bd7078733fabaa2a3d9a99993e2895603dfbb4703c99151a3b230d48bf136a56be1779163fabaa2a3dcdcccc3e698f5d3daf776d3ca63ec0afea4677bfee8384be2ebdbbb3abaa2a3d0000003f2895603dfbb4703c9c151abb210d48bf116a56be1a7916bfabaa2a3d9a99193fd97e683d1d30793c535079bb5dd398be4ccca3bd707873bfabaa2a3d3333333fbf46723de4d5813c535079bb5ed3983e4dcca33d707873bfabaa2a3dcdcc4c3f70307a3d7513863c95151abb260d483f176a563e137916bfabaa2a3d6666663f2f367d3d1bb2873ca63e4030ea46773fee83843e2ebd3b34abaa2a3d0000803f8130723de8a2c83c000000005e836c3f16efc33e000000000000803d00000000704c6f3dd03dc63c9a151a3bd9573f3f8e839e3e1879163f0000803dcdcccc3decba673db9f8bf3c5450793b542c923e0930f23d7178733f0000803dcdcc4c3e06605e3db938b83c5350793b562c92be0c30f2bd7078733f0000803d9a99993e82ce563da2f3b13c99151a3bda573fbf8e839ebe1779163f0000803dcdcccc3e71ea533d8a8eaf3ca63ec0af5e836cbf16efc3be2ebdbbb30000803d0000003f82ce563da2f3b13c9c151abbd9573fbf8d839ebe1a7916bf0000803d9a99193f06605e3db938b83c535079bb582c92be0f30f2bd707873bf0000803d3333333fecba673db9f8bf3c535079bb592c923e1130f23d707873bf0000803dcdcc4c3f714c6f3dd13dc63c95151abbdd573f3f91839e3e137916bf0000803d6666663f8130723de8a2c83ca63e40305e836c3f16efc33e2ebd3b340000803d0000803ffb05633d6e12033d00000000d7b35d3f0000003f00000000abaaaa3d000000004250603de981013d9a151a3b705c333fbd1bcf3e1879163fabaaaa3dcdcccc3d1238593dabd2fa3c5450793b0905893e79371e3e7178733fabaaaa3dcdcc4c3e2473503d72b2f03c5350793b0b0589be7b371ebe7078733fabaaaa3d9a99993ef45a493d4c81e83c99151a3b715c33bfbe1bcfbe1779163fabaaaa3dcdcccc3e3ba5463d4160e53ca63ec0afd7b35dbf000000bf2ebdbbb3abaaaa3d0000003ff45a493d4c81e83c9c151abb705c33bfbc1bcfbe1a7916bfabaaaa3d9a99193f2473503d72b2f03c535079bb0d0589be7d371ebe707873bfabaaaa3d3333333f1238593dacd2fa3c535079bb0d05893e7e371e3e707873bfabaaaa3dcdcc4c3f4250603de981013d95151abb745c333fc11bcf3e137916bfabaaaa3d6666663ffb05633d6e12033da63e4030d7b35d3f0000003f2ebd3b34abaaaa3d0000803f0af94f3d4a951f3d0000000034194b3fcad71b3f000000005555d53d00000000887d4d3da5ad1d3d9a151a3b634f243fbe28fc3e1879163f5555d53dcdcccc3dbffd463dfbb0183d5450793b210b7b3ee7a1403e7178733f5555d53dcdcc4c3e33f53e3def86123d5350793b240b7bbeeaa140be7078733f5555d53d9a99993e6a75383d458a0d3d99151a3b634f24bfc028fcbe1779163f5555d53dcdcccc3ee8f9353da0a20b3da63ec0af34194bbfcad71bbf2ebdbbb35555d53d0000003f6a75383d458a0d3d9c151abb624f24bfbd28fcbe1a7916bf5555d53d9a99193f33f53e3def86123d535079bb270b7bbeeca140be707873bf5555d53d3333333fbffd463dfbb0183d535079bb290b7b3eeda1403e707873bf5555d53dcdcc4c3f887d4d3da5ad1d3d95151abb664f243fc328fc3e137916bf5555d53d6666663f0af94f3d4a951f3da63e403034194b3fcad71b3f2ebd3b345555d53d0000803f215d393d215d393d00000000f304353ff304353f000000000000003e00000000b526373db526373d9a151a3b9e72123f9e72123f1879163f0000003ecdcccc3dcc5b313dcc5b313d5450793b8fc05f3e8fc05f3e7178733f0000003ecdcc4c3ed2322a3dd2322a3d5350793b92c05fbe92c05fbe7078733f0000003e9a99993ee967243de967243d99151a3b9e7212bf9e7212bf1779163f0000003ecdcccc3e7d31223d7d31223da63ec0aff30435bff30435bf2ebdbbb30000003e0000003fe967243de967243d9c151abb9d7212bf9d7212bf1a7916bf0000003e9a99193fd2322a3dd2322a3d535079bb95c05fbe95c05fbe707873bf0000003e3333333fcc5b313dcc5b313d535079bb96c05f3e96c05f3e707873bf0000003ecdcc4c3fb526373db526373d95151abba072123fa072123f137916bf0000003e6666663f215d393d215d393da63e4030f304353ff304353f2ebd3b340000003e0000803f49951f3d0bf94f3d00000000c9d71b3f35194b3f000000005555153e00000000a4ad1d3d897d4d3d9a151a3bbd28fc3e634f243f1879163f5555153ecdcccc3dfab0183dc0fd463d5450793be6a1403e220b7b3e7178733f5555153ecdcc4c3eee86123d34f53e3d5350793be9a140be250b7bbe7078733f5555153e9a99993e448a0d3d6b75383d99151a3bbe28fcbe644f24bf1779163f5555153ecdcccc3e9fa20b3de9f9353da63ec0afc9d71bbf35194bbf2ebdbbb35555153e0000003f448a0d3d6b75383d9c151abbbb28fcbe634f24bf1a7916bf5555153e9a99193fee86123d34f53e3d535079bbeba140be290b7bbe707873bf5555153e3333333ffab0183dc0fd463d535079bbeca1403e2a0b7b3e707873bf5555153ecdcc4c3fa4ad1d3d897d4d3d95151abbc228fc3e674f243f137916bf5555153e6666663f49951f3d0bf94f3da63e4030c9d71b3f35194b3f2ebd3b345555153e0000803f6e12033dfc05633d00000000ffffff3ed8b35d3f00000000abaa2a3e00000000e981013d4350603d9a151a3bbc1bcf3e715c333f1879163fabaa2a3ecdcccc3daad2fa3c1338593d5450793b78371e3e0a05893e7178733fabaa2a3ecdcc4c3e72b2f03c2573503d5350793b7a371ebe0b0589be7078733fabaa2a3e9a99993e4b81e83cf55a493d99151a3bbd1bcfbe725c33bf1779163fabaa2a3ecdcccc3e4060e53c3ca5463da63ec0afffffffbed8b35dbf2ebdbbb3abaa2a3e0000003f4b81e83cf55a493d9c151abbbb1bcfbe705c33bf1a7916bfabaa2a3e9a99193f71b2f03c2573503d535079bb7c371ebe0d0589be707873bfabaa2a3e3333333fabd2fa3c1338593d535079bb7d371e3e0e05893e707873bfabaa2a3ecdcc4c3fe981013d4350603d95151abbc01bcf3e755c333f137916bfabaa2a3e6666663f6e12033dfc05633da63e4030ffffff3ed8b35d3f2ebd3b34abaa2a3e0000803fe6a2c83c8130723d0000000015efc33e5e836c3f000000000000403e00000000cf3dc63c704c6f3d9a151a3b8d839e3ed9573f3f1879163f0000403ecdcccc3db8f8bf3cecba673d5450793b0830f23d542c923e7178733f0000403ecdcc4c3eb838b83c06605e3d5350793b0b30f2bd562c92be7078733f0000403e9a99993ea1f3b13c82ce563d99151a3b8e839ebeda573fbf1779163f0000403ecdcccc3e8a8eaf3c71ea533da63ec0af15efc3be5e836cbf2ebdbbb30000403e0000003fa1f3b13c82ce563d9c151abb8c839ebed9573fbf1a7916bf0000403e9a99193fb838b83c06605e3d535079bb0e30f2bd582c92be707873bf0000403e3333333fb8f8bf3cecba673d535079bb1030f23d592c923e707873bf0000403ecdcc4c3fd03dc63c714c6f3d95151abb90839e3edd573f3f137916bf0000403e6666663fe6a2c83c8130723da63e403015efc33e5e836c3f2ebd3b340000403e0000803f1bb2873c2f367d3d00000000ef83843eea46773f000000005555553e000000007513863c70307a3d9a151a3b146a563e220d483f1879163f5555553ecdcccc3de4d5813cbe46723d5450793b49cca33d5ad3983e7178733f5555553ecdcc4c3e1e30793cd97e683d5350793b4bcca3bd5bd398be7078733f5555553e9a99993efcb4703c2895603d99151a3b156a56be230d48bf1779163f5555553ecdcccc3eb0776d3c698f5d3da63ec0afef8384beea4677bf2ebdbbb35555553e0000003ffcb4703c2895603d9c151abb136a56be210d48bf1a7916bf5555553e9a99193f1e30793cd97e683d535079bb4dcca3bd5dd398be707873bf5555553e3333333fe4d5813cbf46723d535079bb4ecca33d5ed3983e707873bf5555553ecdcc4c3f7513863c70307a3d95151abb186a563e260d483f137916bf5555553e6666663f1bb2873c2f367d3da63e4030ef83843eea46773f2ebd3b345555553e0000803fd6dd083c5ef3813d00000000a4a8053e55cf7d3f00000000abaa6a3e000000009c3b073c4666803d9a151a3bb043d83d26564d3f1879163fabaa6a3ecdcccc3daef4023c57ad783d5450793b1736253df6dc9c3e7178733fabaa6a3ecdcc4c3e8956fb3b4ba36e3d5350793b193625bdf8dc9cbe7078733fabaa6a3e9a99993eabc8f23b1584663d99151a3bb143d8bd27564dbf1779163fabaa6a3ecdcccc3e3784ef3be569633da63ec0afa4a805be55cf7dbf2ebdbbb3abaa6a3e0000003fabc8f23b1584663d9c151abbaf43d8bd25564dbf1a7916bfabaa6a3e9a99193f8956fb3b4ba36e3d535079bb1b3625bdfadc9cbe707873bfabaa6a3e3333333faef4023c57ad783d535079bb1c36253dfbdc9c3e707873bfabaa6a3ecdcc4c3f9c3b073c4766803d95151abbb443d83d2a564d3f137916bfabaa6a3e6666663fd6dd083c5ef3813da63e4030a4a8053e55cf7d3f2ebd3b34abaa6a3e0000803fa53e40b16e12833d000000002ebd3bb30000803f000000000000803e0000000033f33db1e981813d9a151a3b50e217b3bd1b4f3f1879163f0000803ecdcccc3d3ef137b1abd27a3d5450793be20e68b279379e3e7178733f0000803ecdcc4c3e388430b172b2703d5350793be40e68327b379ebe7078733f0000803e9a99993e43822ab14c81683d99151a3b50e21733be1b4fbf1779163f0000803ecdcccc3ed13628b14160653da63ec0af2ebd3b33000080bf2ebdbbb30000803e0000003f43822ab14c81683d9c151abb4fe21733bc1b4fbf1a7916bf0000803e9a99193f388430b172b2703d535079bbe70e68327d379ebe707873bf0000803e3333333f3ef137b1acd27a3d535079bbe90e68b27e379e3e707873bf0000803ecdcc4c3f33f33db1e981813d95151abb53e217b3c11b4f3f137916bf0000803e6666663fa53e40b16e12833da63e40302ebd3bb30000803f2ebd3b340000803e0000803fdddd08bc5ef3813d00000000aaa805be55cf7d3f00000000abaa8a3e00000000a33b07bc4666803d9a151a3bba43d8bd26564d3f1879163fabaa8a3ecdcccc3db4f402bc57ad783d5450793b1f3625bdf6dc9c3e7178733fabaa8a3ecdcc4c3e9456fbbb4ba36e3d5350793b2136253df8dc9cbe7078733fabaa8a3e9a99993eb7c8f2bb1584663d99151a3bbb43d83d27564dbf1779163fabaa8a3ecdcccc3e4284efbbe569633da63ec0afaaa8053e55cf7dbf2ebdbbb3abaa8a3e0000003fb7c8f2bb1584663d9c151abbb943d83d25564dbf1a7916bfabaa8a3e9a99193f9456fbbb4ba36e3d535079bb2336253dfadc9cbe707873bfabaa8a3e3333333fb4f402bc57ad783d535079bb243625bdfbdc9c3e707873bfabaa8a3ecdcc4c3fa33b07bc4766803d95151abbbe43d8bd2a564d3f137916bfabaa8a3e6666663fdddd08bc5ef3813da63e4030aaa805be55cf7d3f2ebd3b34abaa8a3e0000803f1eb287bc2f367d3d00000000f28384beea46773f000000005555953e00000000781386bc70307a3d9a151a3b196a56be220d483f1879163f5555953ecdcccc3de7d581bcbe46723d5450793b4dcca3bd5ad3983e7178733f5555953ecdcc4c3e243079bcd97e683d5350793b4fcca33d5bd398be7078733f5555953e9a99993e02b570bc2895603d99151a3b1a6a563e230d48bf1779163f5555953ecdcccc3eb5776dbc698f5d3da63ec0aff283843eea4677bf2ebdbbb35555953e0000003f02b570bc2895603d9c151abb186a563e210d48bf1a7916bf5555953e9a99193f233079bcd97e683d535079bb51cca33d5dd398be707873bf5555953e3333333fe7d581bcbf46723d535079bb52cca3bd5ed3983e707873bf5555953ecdcc4c3f781386bc70307a3d95151abb1d6a56be260d483f137916bf5555953e6666663f1eb287bc2f367d3da63e4030f28384beea46773f2ebd3b345555953e0000803feaa2c8bc8130723d0000000018efc3be5e836c3f000000000000a03e00000000d23dc6bc704c6f3d9a151a3b8f839ebed9573f3f1879163f0000a03ecdcccc3dbbf8bfbcecba673d5450793b0c30f2bd542c923e7178733f0000a03ecdcc4c3ebb38b8bc06605e3d5350793b0f30f23d562c92be7078733f0000a03e9a99993ea3f3b1bc82ce563d99151a3b90839e3eda573fbf1779163f0000a03ecdcccc3e8c8eafbc71ea533da63ec0af18efc33e5e836cbf2ebdbbb30000a03e0000003fa4f3b1bc82ce563d9c151abb8f839e3ed9573fbf1a7916bf0000a03e9a99193fbb38b8bc06605e3d535079bb1230f23d582c92be707873bf0000a03e3333333fbbf8bfbcecba673d535079bb1330f2bd592c923e707873bf0000a03ecdcc4c3fd33dc6bc714c6f3d95151abb92839ebedd573f3f137916bf0000a03e6666663feaa2c8bc8130723da63e403018efc3be5e836c3f2ebd3b340000a03e0000803f701203bdfb05633d00000000010000bfd7b35d3f00000000abaaaa3e00000000ea8101bd4250603d9a151a3bbf1bcfbe705c333f1879163fabaaaa3ecdcccc3daed2fabc1238593d5450793b7a371ebe0905893e7178733fabaaaa3ecdcc4c3e74b2f0bc2473503d5350793b7c371e3e0b0589be7078733fabaaaa3e9a99993e4d81e8bcf45a493d99151a3bc01bcf3e715c33bf1779163fabaaaa3ecdcccc3e4360e5bc3ba5463da63ec0af0100003fd7b35dbf2ebdbbb3abaaaa3e0000003f4e81e8bcf45a493d9c151abbbe1bcf3e705c33bf1a7916bfabaaaa3e9a99193f74b2f0bc2473503d535079bb7e371e3e0d0589be707873bfabaaaa3e3333333faed2fabc1238593d535079bb7f371ebe0d05893e707873bfabaaaa3ecdcc4c3fea8101bd4250603d95151abbc31bcfbe745c333f137916bfabaaaa3e6666663f701203bdfb05633da63e4030010000bfd7b35d3f2ebd3b34abaaaa3e0000803f4a951fbd0af94f3d00000000cad71bbf34194b3f000000005555b53e00000000a5ad1dbd887d4d3d9a151a3bbe28fcbe634f243f1879163f5555b53ecdcccc3dfbb018bdbffd463d5450793be7a140be210b7b3e7178733f5555b53ecdcc4c3eef8612bd33f53e3d5350793beaa1403e240b7bbe7078733f5555b53e9a99993e458a0dbd6a75383d99151a3bc028fc3e634f24bf1779163f5555b53ecdcccc3ea0a20bbde8f9353da63ec0afcad71b3f34194bbf2ebdbbb35555b53e0000003f458a0dbd6a75383d9c151abbbd28fc3e624f24bf1a7916bf5555b53e9a99193fef8612bd33f53e3d535079bbeca1403e270b7bbe707873bf5555b53e3333333ffbb018bdbffd463d535079bbeda140be290b7b3e707873bf5555b53ecdcc4c3fa5ad1dbd887d4d3d95151abbc328fcbe664f243f137916bf5555b53e6666663f4a951fbd0af94f3da63e4030cad71bbf34194b3f2ebd3b345555b53e0000803f215d39bd215d393d00000000f30435bff304353f000000000000c03e00000000b52637bdb526373d9a151a3b9e7212bf9e72123f1879163f0000c03ecdcccc3dcc5b31bdcc5b313d5450793b8fc05fbe8fc05f3e7178733f0000c03ecdcc4c3ed2322abdd2322a3d5350793b92c05f3e92c05fbe7078733f0000c03e9a99993ee96724bde967243d99151a3b9e72123f9e7212bf1779163f0000c03ecdcccc3e7d3122bd7d31223da63ec0aff304353ff30435bf2ebdbbb30000c03e0000003fe96724bde967243d9c151abb9d72123f9d7212bf1a7916bf0000c03e9a99193fd2322abdd2322a3d535079bb95c05f3e95c05fbe707873bf0000c03e3333333fcc5b31bdcc5b313d535079bb96c05fbe96c05f3e707873bf0000c03ecdcc4c3fb52637bdb526373d95151abba07212bfa072123f137916bf0000c03e6666663f215d39bd215d393da63e4030f30435bff304353f2ebd3b340000c03e0000803f0af94fbd4a951f3d0000000034194bbfcad71b3f00000000abaaca3e00000000887d4dbda5ad1d3d9a151a3b634f24bfbe28fc3e1879163fabaaca3ecdcccc3dbffd46bdfbb0183d5450793b210b7bbee7a1403e7178733fabaaca3ecdcc4c3e33f53ebdef86123d5350793b240b7b3eeaa140be7078733fabaaca3e9a99993e6a7538bd458a0d3d99151a3b634f243fc028fcbe1779163fabaaca3ecdcccc3ee8f935bda0a20b3da63ec0af34194b3fcad71bbf2ebdbbb3abaaca3e0000003f6a7538bd458a0d3d9c151abb624f243fbd28fcbe1a7916bfabaaca3e9a99193f33f53ebdef86123d535079bb270b7b3eeca140be707873bfabaaca3e3333333fbffd46bdfbb0183d535079bb290b7bbeeda1403e707873bfabaaca3ecdcc4c3f887d4dbda5ad1d3d95151abb664f24bfc328fc3e137916bfabaaca3e6666663f0af94fbd4a951f3da63e403034194bbfcad71b3f2ebd3b34abaaca3e0000803ffb0563bd7012033d00000000d7b35dbf0100003f000000005555d53e00000000425060bdea81013d9a151a3b705c33bfbf1bcf3e1879163f5555d53ecdcccc3d123859bdaed2fa3c5450793b090589be7a371e3e7178733f5555d53ecdcc4c3e247350bd74b2f03c5350793b0b05893e7c371ebe7078733f5555d53e9a99993ef45a49bd4d81e83c99151a3b715c333fc01bcfbe1779163f5555d53ecdcccc3e3ba546bd4360e53ca63ec0afd7b35d3f010000bf2ebdbbb35555d53e0000003ff45a49bd4e81e83c9c151abb705c333fbe1bcfbe1a7916bf5555d53e9a99193f247350bd74b2f03c535079bb0d05893e7e371ebe707873bf5555d53e3333333f123859bdaed2fa3c535079bb0d0589be7f371e3e707873bf5555d53ecdcc4c3f425060bdea81013d95151abb745c33bfc31bcf3e137916bf5555d53e6666663ffb0563bd7012033da63e4030d7b35dbf0100003f2ebd3b345555d53e0000803f833072bde1a2c83c0000000060836cbf10efc33e000000000000e03e00000000724c6fbdca3dc63c9a151a3bdb573fbf89839e3e1879163f0000e03ecdcccc3deeba67bdb3f8bf3c5450793b552c92be0230f23d7178733f0000e03ecdcc4c3e08605ebdb338b83c5350793b572c923e0530f2bd7078733f0000e03e9a99993e83ce56bd9cf3b13c99151a3bdc573f3f8a839ebe1779163f0000e03ecdcccc3e73ea53bd858eaf3ca63ec0af60836c3f10efc3be2ebdbbb30000e03e0000003f84ce56bd9cf3b13c9c151abbda573f3f88839ebe1a7916bf0000e03e9a99193f08605ebdb338b83c535079bb592c923e0830f2bd707873bf0000e03e3333333fefba67bdb3f8bf3c535079bb5a2c92be0930f23d707873bf0000e03ecdcc4c3f734c6fbdca3dc63c95151abbdf573fbf8c839e3e137916bf0000e03e6666663f833072bde1a2c83ca63e403060836cbf10efc33e2ebd3b340000e03e0000803f30367dbd16b2873c00000000eb4677bfea83843e00000000abaaea3e0000000071307abd7013863c9a151a3b230d48bf0c6a563e1879163fabaaea3ecdcccc3dc04672bde0d5813c5450793b5ad398be43cca33d7178733fabaaea3ecdcc4c3eda7e68bd1530793c5350793b5cd3983e45cca3bd7078733fabaaea3e9a99993e299560bdf3b4703c99151a3b240d483f0d6a56be1779163fabaaea3ecdcccc3e6a8f5dbda7776d3ca63ec0afeb46773fea8384be2ebdbbb3abaaea3e0000003f299560bdf4b4703c9c151abb220d483f0b6a56be1a7916bfabaaea3e9a99193fda7e68bd1530793c535079bb5ed3983e47cca3bd707873bfabaaea3e3333333fc04672bde0d5813c535079bb5fd398be48cca33d707873bfabaaea3ecdcc4c3f71307abd7013863c95151abb270d48bf106a563e137916bfabaaea3e6666663f30367dbd16b2873ca63e4030eb4677bfea83843e2ebd3b34abaaea3e0000803f5ef381bdd4dd083c0000000055cf7dbfa1a8053e000000005555f53e00000000466680bd9a3b073c9a151a3b26564dbfab43d83d1879163f5555f53ecdcccc3d57ad78bdacf4023c5450793bf6dc9cbe1436253d7178733f5555f53ecdcc4c3e4ba36ebd8556fb3b5350793bf8dc9c3e163625bd7078733f5555f53e9a99993e158466bda8c8f23b99151a3b27564d3fac43d8bd1779163f5555f53ecdcccc3ee56963bd3484ef3ba63ec0af55cf7d3fa1a805be2ebdbbb35555f53e0000003f158466bda8c8f23b9c151abb25564d3faa43d8bd1a7916bf5555f53e9a99193f4ba36ebd8556fb3b535079bbfadc9c3e183625bd707873bf5555f53e3333333f57ad78bdacf4023c535079bbfbdc9cbe1936253d707873bf5555f53ecdcc4c3f476680bd9a3b073c95151abb2a564dbfaf43d83d137916bf5555f53e6666663f5ef381bdd4dd083ca63e403055cf7dbfa1a8053e2ebd3b345555f53e0000803f6e1283bda53ec0b100000000000080bf2ebdbbb3000000000000003f00000000e98181bd33f3bdb19a151a3bbd1b4fbf50e297b31879163f0000003fcdcccc3dabd27abd3ef1b7b15450793b79379ebee20ee8b27178733f0000003fcdcc4c3e72b270bd3884b0b15350793b7b379e3ee40ee8327078733f0000003f9a99993e4c8168bd4382aab199151a3bbe1b4f3f50e297331779163f0000003fcdcccc3e416065bdd136a8b1a63ec0af0000803f2ebdbb332ebdbbb30000003f0000003f4c8168bd4382aab19c151abbbc1b4f3f4fe297331a7916bf0000003f9a99193f72b270bd3884b0b1535079bb7d379e3ee70ee832707873bf0000003f3333333facd27abd3ef1b7b1535079bb7e379ebee90ee8b2707873bf0000003fcdcc4c3fe98181bd33f3bdb195151abbc11b4fbf53e297b3137916bf0000003f6666663f6e1283bda53ec0b1a63e4030000080bf2ebdbbb32ebd3b340000003f0000803f5ef381bde0dd08bc0000000054cf7dbfada805be000000005555053f00000000466680bda63b07bc9a151a3b25564dbfbf43d8bd1879163f5555053fcdcccc3d56ad78bdb7f402bc5450793bf5dc9cbe223625bd7178733f5555053fcdcc4c3e4aa36ebd9a56fbbb5350793bf7dc9c3e2436253d7078733f5555053f9a99993e148466bdbcc8f2bb99151a3b26564d3fc043d83d1779163f5555053fcdcccc3ee46963bd4884efbba63ec0af54cf7d3fada8053e2ebdbbb35555053f0000003f148466bdbcc8f2bb9c151abb24564d3fbe43d83d1a7916bf5555053f9a99193f4aa36ebd9a56fbbb535079bbf9dc9c3e2736253d707873bf5555053f3333333f56ad78bdb7f402bc535079bbfadc9cbe283625bd707873bf5555053fcdcc4c3f466680bda63b07bc95151abb29564dbfc343d8bd137916bf5555053f6666663f5ef381bde0dd08bca63e403054cf7dbfada805be2ebd3b345555053f0000803f2f367dbd1bb287bc00000000ea4677bfef8384be00000000abaa0a3f0000000070307abd751386bc9a151a3b220d48bf146a56be1879163fabaa0a3fcdcccc3dbe4672bde4d581bc5450793b5ad398be49cca3bd7178733fabaa0a3fcdcc4c3ed97e68bd1e3079bc5350793b5bd3983e4bcca33d7078733fabaa0a3f9a99993e289560bdfcb470bc99151a3b230d483f156a563e1779163fabaa0a3fcdcccc3e698f5dbdb0776dbca63ec0afea46773fef83843e2ebdbbb3abaa0a3f0000003f289560bdfcb470bc9c151abb210d483f136a563e1a7916bfabaa0a3f9a99193fd97e68bd1e3079bc535079bb5dd3983e4dcca33d707873bfabaa0a3f3333333fbf4672bde4d581bc535079bb5ed398be4ecca3bd707873bfabaa0a3fcdcc4c3f70307abd751386bc95151abb260d48bf186a56be137916bfabaa0a3f6666663f2f367dbd1bb287bca63e4030ea4677bfef8384be2ebd3b34abaa0a3f0000803f813072bde6a2c8bc000000005e836cbf15efc3be000000000000103f00000000704c6fbdcf3dc6bc9a151a3bd9573fbf8d839ebe1879163f0000103fcdcccc3decba67bdb8f8bfbc5450793b542c92be0830f2bd7178733f0000103fcdcc4c3e06605ebdb838b8bc5350793b562c923e0b30f23d7078733f0000103f9a99993e82ce56bda1f3b1bc99151a3bda573f3f8e839e3e1779163f0000103fcdcccc3e71ea53bd8a8eafbca63ec0af5e836c3f15efc33e2ebdbbb30000103f0000003f82ce56bda1f3b1bc9c151abbd9573f3f8c839e3e1a7916bf0000103f9a99193f06605ebdb838b8bc535079bb582c923e0e30f23d707873bf0000103f3333333fecba67bdb8f8bfbc535079bb592c92be1030f2bd707873bf0000103fcdcc4c3f714c6fbdd03dc6bc95151abbdd573fbf90839ebe137916bf0000103f6666663f813072bde6a2c8bca63e40305e836cbf15efc3be2ebd3b340000103f0000803ff90563bd721203bd00000000d5b35dbf030000bf000000005555153f00000000405060bdec8101bd9a151a3b6f5c33bfc21bcfbe1879163f5555153fcdcccc3d103859bdb2d2fabc5450793b080589be7d371ebe7178733f5555153fcdcc4c3e227350bd78b2f0bc5350793b0a05893e7f371e3e7078733f5555153f9a99993ef25a49bd5181e8bc99151a3b705c333fc31bcf3e1779163f5555153fcdcccc3e39a546bd4760e5bca63ec0afd5b35d3f0300003f2ebdbbb35555153f0000003ff25a49bd5181e8bc9c151abb6e5c333fc11bcf3e1a7916bf5555153f9a99193f227350bd78b2f0bc535079bb0b05893e81371e3e707873bf5555153f3333333f103859bdb2d2fabc535079bb0c0589be82371ebe707873bf5555153fcdcc4c3f405060bdec8101bd95151abb725c33bfc61bcfbe137916bf5555153f6666663ff90563bd721203bda63e4030d5b35dbf030000bf2ebd3b345555153f0000803f08f94fbd4d951fbd0000000032194bbfcdd71bbf00000000abaa1a3f00000000857d4dbda8ad1dbd9a151a3b614f24bfc328fcbe1879163fabaa1a3fcdcccc3dbdfd46bdfeb018bd5450793b1f0b7bbeeba140be7178733fabaa1a3fcdcc4c3e31f53ebdf28612bd5350793b220b7b3eeea1403e7078733fabaa1a3f9a99993e687538bd488a0dbd99151a3b624f243fc428fc3e1779163fabaa1a3fcdcccc3ee6f935bda3a20bbda63ec0af32194b3fcdd71b3f2ebdbbb3abaa1a3f0000003f697538bd488a0dbd9c151abb604f243fc228fc3e1a7916bfabaa1a3f9a99193f31f53ebdf28612bd535079bb250b7b3ef0a1403e707873bfabaa1a3f3333333fbdfd46bdfeb018bd535079bb260b7bbef1a140be707873bfabaa1a3fcdcc4c3f867d4dbda8ad1dbd95151abb644f24bfc828fcbe137916bfabaa1a3f6666663f08f94fbd4d951fbda63e403032194bbfcdd71bbf2ebd3b34abaa1a3f0000803f205d39bd235d39bd00000000f10435bff50435bf000000000000203f00000000b42637bdb72637bd9a151a3b9c7212bf9f7212bf1879163f0000203fcdcccc3dcb5b31bdce5b31bd5450793b8dc05fbe92c05fbe7178733f0000203fcdcc4c3ed1322abdd4322abd5350793b90c05f3e95c05f3e7078733f0000203f9a99993ee86724bdeb6724bd99151a3b9d72123fa072123f1779163f0000203fcdcccc3e7c3122bd7f3122bda63ec0aff104353ff504353f2ebdbbb30000203f0000003fe86724bdeb6724bd9c151abb9b72123f9f72123f1a7916bf0000203f9a99193fd1322abdd4322abd535079bb92c05f3e97c05f3e707873bf0000203f3333333fcb5b31bdce5b31bd535079bb94c05fbe99c05fbe707873bf0000203fcdcc4c3fb42637bdb82637bd95151abb9f7212bfa27212bf137916bf0000203f6666663f205d39bd235d39bda63e4030f10435bff50435bf2ebd3b340000203f0000803f4b951fbd09f94fbd00000000cbd71bbf33194bbf000000005555253f00000000a6ad1dbd867d4dbd9a151a3bc028fcbe624f24bf1879163f5555253fcdcccc3dfcb018bdbefd46bd5450793be9a140be200b7bbe7178733f5555253fcdcc4c3ef08612bd32f53ebd5350793beba1403e230b7b3e7078733f5555253f9a99993e468a0dbd697538bd99151a3bc128fc3e634f243f1779163f5555253fcdcccc3ea1a20bbde7f935bda63ec0afcbd71b3f33194b3f2ebdbbb35555253f0000003f468a0dbd6a7538bd9c151abbbf28fc3e614f243f1a7916bf5555253f9a99193ff08612bd32f53ebd535079bbeda1403e260b7b3e707873bf5555253f3333333ffcb018bdbefd46bd535079bbefa140be280b7bbe707873bf5555253fcdcc4c3fa6ad1dbd877d4dbd95151abbc528fcbe654f24bf137916bf5555253f6666663f4b951fbd09f94fbda63e4030cbd71bbf33194bbf2ebd3b345555253f0000803f6d1203bdfc0563bd00000000fdffffbed8b35dbf00000000abaa2a3f00000000e88101bd435060bd9a151a3bbb1bcfbe715c33bf1879163fabaa2a3fcdcccc3da8d2fabc133859bd5450793b77371ebe0a0589be7178733fabaa2a3fcdcc4c3e70b2f0bc257350bd5350793b79371e3e0b05893e7078733fabaa2a3f9a99993e4981e8bcf55a49bd99151a3bbc1bcf3e725c333f1779163fabaa2a3fcdcccc3e3e60e5bc3ca546bda63ec0affdffff3ed8b35d3f2ebdbbb3abaa2a3f0000003f4981e8bcf55a49bd9c151abbba1bcf3e705c333f1a7916bfabaa2a3f9a99193f6fb2f0bc257350bd535079bb7b371e3e0d05893e707873bfabaa2a3f3333333fa9d2fabc133859bd535079bb7c371ebe0e0589be707873bfabaa2a3fcdcc4c3fe88101bd435060bd95151abbbf1bcfbe755c33bf137916bfabaa2a3f6666663f6d1203bdfc0563bda63e4030fdffffbed8b35dbf2ebd3b34abaa2a3f0000803fdca2c8bc843072bd000000000befc3be61836cbf000000000000303f00000000c53dc6bc744c6fbd9a151a3b85839ebedc573fbf1879163f0000303fcdcccc3daef8bfbcefba67bd5450793bfc2ff2bd562c92be7178733f0000303fcdcc4c3eae38b8bc09605ebd5350793bff2ff23d582c923e7078733f0000303f9a99993e97f3b1bc84ce56bd99151a3b86839e3edd573f3f1779163f0000303fcdcccc3e808eafbc74ea53bda63ec0af0befc33e61836c3f2ebdbbb30000303f0000003f97f3b1bc84ce56bd9c151abb84839e3edb573f3f1a7916bf0000303f9a99193fae38b8bc08605ebd535079bb0230f23d5a2c923e707873bf0000303f3333333faef8bfbcf0ba67bd535079bb0330f2bd5b2c92be707873bf0000303fcdcc4c3fc53dc6bc744c6fbd95151abb88839ebee0573fbf137916bf0000303f6666663fdca2c8bc843072bda63e40300befc3be61836cbf2ebd3b340000303f0000803f19b287bc30367dbd00000000ec8384beeb4677bf000000005555353f00000000721386bc71307abd9a151a3b0f6a56be230d48bf1879163f5555353fcdcccc3de2d581bcc04672bd5450793b45cca3bd5ad398be7178733f5555353fcdcc4c3e193079bcda7e68bd5350793b47cca33d5cd3983e7078733f5555353f9a99993ef7b470bc299560bd99151a3b106a563e240d483f1779163f5555353fcdcccc3eab776dbc6a8f5dbda63ec0afec83843eeb46773f2ebdbbb35555353f0000003ff7b470bc299560bd9c151abb0e6a563e220d483f1a7916bf5555353f9a99193f193079bcda7e68bd535079bb49cca33d5ed3983e707873bf5555353f3333333fe2d581bcc04672bd535079bb4acca3bd5fd398be707873bf5555353fcdcc4c3f721386bc71307abd95151abb136a56be270d48bf137916bf5555353f6666663f19b287bc30367dbda63e4030ec8384beeb4677bf2ebd3b345555353f0000803fc9dd08bc5ef381bd0000000096a805be55cf7dbf00000000abaa3a3f000000008f3b07bc466680bd9a151a3b9943d8bd26564dbf1879163fabaa3a3fcdcccc3da0f402bc57ad78bd5450793b063625bdf6dc9cbe7178733fabaa3a3fcdcc4c3e6f56fbbb4ba36ebd5350793b0836253df8dc9c3e7078733fabaa3a3f9a99993e93c8f2bb158466bd99151a3b9b43d83d27564d3f1779163fabaa3a3fcdcccc3e1f84efbbe56963bda63ec0af96a8053e55cf7d3f2ebdbbb3abaa3a3f0000003f93c8f2bb158466bd9c151abb9843d83d25564d3f1a7916bfabaa3a3f9a99193f6f56fbbb4ba36ebd535079bb0a36253dfadc9c3e707873bfabaa3a3f3333333fa0f402bc57ad78bd535079bb0b3625bdfbdc9cbe707873bfabaa3a3fcdcc4c3f8f3b07bc476680bd95151abb9e43d8bd2a564dbf137916bfabaa3a3f6666663fc9dd08bc5ef381bda63e403096a805be55cf7dbf2ebd3b34abaa3a3f0000803fe3c851306e1283bd000000002ede4c32000080bf000000000000403f00000000d8474f30e98181bd9a151a3bdabd2532bd1b4fbf1879163f0000403fcdcccc3d91b94830abd27abd5450793b0a3b7d3179379ebe7178733f0000403fcdcc4c3e199f403072b270bd5350793b0d3b7db17b379e3e7078733f0000403f9a99993ed2103a304c8168bd99151a3bdbbd25b2be1b4f3f1779163f0000403fcdcccc3ec78f3730416065bda63ec0af2ede4cb20000803f2ebdbbb30000403f0000003fd2103a304c8168bd9c151abbd9bd25b2bc1b4f3f1a7916bf0000403f9a99193f199f403072b270bd535079bb103b7db17d379e3e707873bf0000403f3333333f91b94830acd27abd535079bb123b7d317e379ebe707873bf0000403fcdcc4c3fd8474f30e98181bd95151abbddbd2532c11b4fbf137916bf0000403f6666663fe3c851306e1283bda63e40302ede4c32000080bf2ebd3b340000403f0000803febdd083c5ef381bd00000000b7a8053e54cf7dbf000000005555453f00000000b03b073c466680bd9a151a3bcf43d83d25564dbf1879163f5555453fcdcccc3dc1f4023c56ad78bd5450793b2f36253df5dc9cbe7178733f5555453fcdcc4c3eae56fb3b4aa36ebd5350793b313625bdf7dc9c3e7078733f5555453f9a99993ecfc8f23b148466bd99151a3bd043d8bd26564d3f1779163f5555453fcdcccc3e5b84ef3be46963bda63ec0afb7a805be54cf7d3f2ebdbbb35555453f0000003fcfc8f23b148466bd9c151abbce43d8bd24564d3f1a7916bf5555453f9a99193fae56fb3b4aa36ebd535079bb333625bdf9dc9c3e707873bf5555453f3333333fc1f4023c56ad78bd535079bb3436253dfadc9cbe707873bf5555453fcdcc4c3fb03b073c466680bd95151abbd343d83d29564dbf137916bf5555453f6666663febdd083c5ef381bda63e4030b7a8053e54cf7dbf2ebd3b345555453f0000803f1ab2873c2f367dbd00000000ed83843eea4677bf00000000abaa4a3f000000007313863c70307abd9a151a3b116a563e220d48bf1879163fabaa4a3fcdcccc3de3d5813cbe4672bd5450793b47cca33d5ad398be7178733fabaa4a3fcdcc4c3e1b30793cd97e68bd5350793b49cca3bd5bd3983e7078733fabaa4a3f9a99993ef9b4703c289560bd99151a3b126a56be230d483f1779163fabaa4a3fcdcccc3ead776d3c698f5dbda63ec0afed8384beea46773f2ebdbbb3abaa4a3f0000003ff9b4703c289560bd9c151abb106a56be210d483f1a7916bfabaa4a3f9a99193f1b30793cd97e68bd535079bb4bcca3bd5dd3983e707873bfabaa4a3f3333333fe3d5813cbf4672bd535079bb4ccca33d5ed398be707873bfabaa4a3fcdcc4c3f7413863c70307abd95151abb156a563e260d48bf137916bfabaa4a3f6666663f1ab2873c2f367dbda63e4030ed83843eea4677bf2ebd3b34abaa4a3f0000803feca2c83c803072bd000000001befc33e5d836cbf000000000000503f00000000d53dc63c6f4c6fbd9a151a3b92839e3ed9573fbf1879163f0000503fcdcccc3dbdf8bf3cebba67bd5450793b0f30f23d532c92be7178733f0000503fcdcc4c3ebd38b83c05605ebd5350793b1230f2bd552c923e7078733f0000503f9a99993ea5f3b13c81ce56bd99151a3b92839ebeda573f3f1779163f0000503fcdcccc3e8e8eaf3c70ea53bda63ec0af1befc3be5d836c3f2ebdbbb30000503f0000003fa5f3b13c81ce56bd9c151abb91839ebed8573f3f1a7916bf0000503f9a99193fbd38b83c05605ebd535079bb1530f2bd572c923e707873bf0000503f3333333fbdf8bf3cebba67bd535079bb1730f23d582c92be707873bf0000503fcdcc4c3fd53dc63c704c6fbd95151abb95839e3edc573fbf137916bf0000503f6666663feca2c83c803072bda63e40301befc33e5d836cbf2ebd3b340000503f0000803f6d12033dfc0563bd00000000fdffff3ed8b35dbf000000005555553f00000000e881013d435060bd9a151a3bbb1bcf3e715c33bf1879163f5555553fcdcccc3da8d2fa3c133859bd5450793b77371e3e0a0589be7178733f5555553fcdcc4c3e70b2f03c257350bd5350793b79371ebe0b05893e7078733f5555553f9a99993e4981e83cf55a49bd99151a3bbc1bcfbe725c333f1779163f5555553fcdcccc3e3e60e53c3ca546bda63ec0affdffffbed8b35d3f2ebdbbb35555553f0000003f4981e83cf55a49bd9c151abbba1bcfbe705c333f1a7916bf5555553f9a99193f6fb2f03c257350bd535079bb7b371ebe0d05893e707873bf5555553f3333333fa9d2fa3c133859bd535079bb7c371e3e0e0589be707873bf5555553fcdcc4c3fe881013d435060bd95151abbbf1bcf3e755c33bf137916bf5555553f6666663f6d12033dfc0563bda63e4030fdffff3ed8b35dbf2ebd3b345555553f0000803f4c951f3d09f94fbd00000000ccd71b3f33194bbf00000000abaa5a3f00000000a7ad1d3d867d4dbd9a151a3bc228fc3e624f24bf1879163fabaa5a3fcdcccc3dfdb0183dbefd46bd5450793beaa1403e200b7bbe7178733fabaa5a3fcdcc4c3ef186123d32f53ebd5350793beca140be230b7b3e7078733fabaa5a3f9a99993e478a0d3d697538bd99151a3bc328fcbe634f243f1779163fabaa5a3fcdcccc3ea2a20b3de7f935bda63ec0afccd71bbf33194b3f2ebdbbb3abaa5a3f0000003f478a0d3d6a7538bd9c151abbc028fcbe614f243f1a7916bfabaa5a3f9a99193ff186123d32f53ebd535079bbefa140be260b7b3e707873bfabaa5a3f3333333ffdb0183dbefd46bd535079bbf0a1403e280b7bbe707873bfabaa5a3fcdcc4c3fa7ad1d3d877d4dbd95151abbc628fc3e654f24bf137916bfabaa5a3f6666663f4c951f3d09f94fbda63e4030ccd71b3f33194bbf2ebd3b34abaa5a3f0000803f255d393d1e5d39bd00000000f704353fef0435bf000000000000603f00000000ba26373db22637bd9a151a3ba172123f9a7212bf1879163f0000603fcdcccc3dd05b313dc95b31bd5450793b94c05f3e8ac05fbe7178733f0000603fcdcc4c3ed6322a3dcf322abd5350793b97c05fbe8dc05f3e7078733f0000603f9a99993eec67243de66724bd99151a3ba27212bf9b72123f1779163f0000603fcdcccc3e8131223d7a3122bda63ec0aff70435bfef04353f2ebdbbb30000603f0000003fed67243de66724bd9c151abba07212bf9a72123f1a7916bf0000603f9a99193fd6322a3dcf322abd535079bb9ac05fbe90c05f3e707873bf0000603f3333333fd05b313dc95b31bd535079bb9bc05f3e91c05fbe707873bf0000603fcdcc4c3fba26373db22637bd95151abba472123f9d7212bf137916bf0000603f6666663f255d393d1e5d39bda63e4030f704353fef0435bf2ebd3b340000603f0000803f0bf94f3d49951fbd0000000035194b3fc9d71bbf000000005555653f00000000897d4d3da4ad1dbd9a151a3b634f243fbd28fcbe1879163f5555653fcdcccc3dc0fd463dfab018bd5450793b220b7b3ee6a140be7178733f5555653fcdcc4c3e34f53e3dee8612bd5350793b250b7bbee9a1403e7078733f5555653f9a99993e6b75383d448a0dbd99151a3b644f24bfbe28fc3e1779163f5555653fcdcccc3ee9f9353d9fa20bbda63ec0af35194bbfc9d71b3f2ebdbbb35555653f0000003f6b75383d448a0dbd9c151abb634f24bfbb28fc3e1a7916bf5555653f9a99193f34f53e3dee8612bd535079bb290b7bbeeba1403e707873bf5555653f3333333fc0fd463dfab018bd535079bb2a0b7b3eeca140be707873bf5555653fcdcc4c3f897d4d3da4ad1dbd95151abb674f243fc228fcbe137916bf5555653f6666663f0bf94f3d49951fbda63e403035194b3fc9d71bbf2ebd3b345555653f0000803ffe05633d6a1203bd00000000dab35d3ff8ffffbe00000000abaa6a3f000000004550603de58101bd9a151a3b735c333fb71bcfbe1879163fabaa6a3fcdcccc3d1538593da3d2fabc5450793b0b05893e74371ebe7178733fabaa6a3fcdcc4c3e2773503d6bb2f0bc5350793b0d0589be76371e3e7078733fabaa6a3f9a99993ef75a493d4481e8bc99151a3b745c33bfb81bcf3e1779163fabaa6a3fcdcccc3e3ea5463d3a60e5bca63ec0afdab35dbff8ffff3e2ebdbbb3abaa6a3f0000003ff75a493d4481e8bc9c151abb725c33bfb61bcf3e1a7916bfabaa6a3f9a99193f2773503d6bb2f0bc535079bb0e0589be78371e3e707873bfabaa6a3f3333333f1538593da3d2fabc535079bb0f05893e79371ebe707873bfabaa6a3fcdcc4c3f4550603de58101bd95151abb765c333fbb1bcfbe137916bfabaa6a3f6666663ffe05633d6a1203bda63e4030dab35d3ff8ffffbe2ebd3b34abaa6a3f0000803f8230723de6a2c8bc000000005f836c3f15efc3be000000000000703f00000000714c6f3dcf3dc6bc9a151a3bda573f3f8d839ebe1879163f0000703fcdcccc3dedba673db8f8bfbc5450793b552c923e0830f2bd7178733f0000703fcdcc4c3e07605e3db838b8bc5350793b562c92be0b30f23d7078733f0000703f9a99993e82ce563da1f3b1bc99151a3bdb573fbf8e839e3e1779163f0000703fcdcccc3e72ea533d8a8eafbca63ec0af5f836cbf15efc33e2ebdbbb30000703f0000003f83ce563da1f3b1bc9c151abbd9573fbf8c839e3e1a7916bf0000703f9a99193f07605e3db838b8bc535079bb582c92be0e30f23d707873bf0000703f3333333feeba673db8f8bfbc535079bb592c923e1030f2bd707873bf0000703fcdcc4c3f724c6f3dd03dc6bc95151abbde573f3f90839ebe137916bf0000703f6666663f8230723de6a2c8bca63e40305f836c3f15efc3be2ebd3b340000703f0000803f30367d3d13b287bc00000000eb46773fe78384be000000005555753f0000000071307a3d6d1386bc9a151a3b230d483f076a56be1879163f5555753fcdcccc3dc046723ddcd581bc5450793b5ad3983e3fcca3bd7178733f5555753fcdcc4c3eda7e683d0f3079bc5350793b5cd398be41cca33d7078733f5555753f9a99993e2995603deeb470bc99151a3b240d48bf086a563e1779163f5555753fcdcccc3e6a8f5d3da2776dbca63ec0afeb4677bfe783843e2ebdbbb35555753f0000003f2995603deeb470bc9c151abb220d48bf066a563e1a7916bf5555753f9a99193fda7e683d0f3079bc535079bb5ed398be43cca33d707873bf5555753f3333333fc046723dddd581bc535079bb5fd3983e44cca3bd707873bf5555753fcdcc4c3f71307a3d6d1386bc95151abb270d483f0b6a56be137916bf5555753f6666663f30367d3d13b287bca63e4030eb46773fe78384be2ebd3b345555753f0000803f5ef3813ddedd08bc0000000055cf7d3faba805be00000000abaa7a3f000000004666803da43b07bc9a151a3b26564d3fbb43d8bd1879163fabaa7a3fcdcccc3d57ad783db5f402bc5450793bf6dc9c3e203625bd7178733fabaa7a3fcdcc4c3e4ba36e3d9656fbbb5350793bf8dc9cbe2236253d7078733fabaa7a3f9a99993e1584663db8c8f2bb99151a3b27564dbfbd43d83d1779163fabaa7a3fcdcccc3ee569633d4484efbba63ec0af55cf7dbfaba8053e2ebdbbb3abaa7a3f0000003f1584663db9c8f2bb9c151abb25564dbfba43d83d1a7916bfabaa7a3f9a99193f4ba36e3d9656fbbb535079bbfadc9cbe2436253d707873bfabaa7a3f3333333f57ad783db5f402bc535079bbfbdc9c3e253625bd707873bfabaa7a3fcdcc4c3f4766803da43b07bc95151abb2a564d3fc043d8bd137916bfabaa7a3f6666663f5ef3813ddedd08bca63e403055cf7d3faba805be2ebd3b34abaa7a3f0000803f6e12833da53e4032000000000000803f2ebd3b34000000000000803f00000000e981813d33f33d329a151a3bbd1b4f3f50e217341879163f0000803fcdcccc3dabd27a3d3ef137325450793b79379e3ee20e68337178733f0000803fcdcc4c3e72b2703d388430325350793b7b379ebee40e68b37078733f0000803f9a99993e4c81683d43822a3299151a3bbe1b4fbf50e217b41779163f0000803fcdcccc3e4160653dd1362832a63ec0af000080bf2ebd3bb42ebdbbb30000803f0000003f4c81683d43822a329c151abbbc1b4fbf4fe217b41a7916bf0000803f9a99193f72b2703d38843032535079bb7d379ebee70e68b3707873bf0000803f3333333facd27a3d3ef13732535079bb7e379e3ee90e6833707873bf0000803fcdcc4c3fe981813d33f33d3295151abbc11b4f3f53e21734137916bf0000803f6666663f6e12833da53e4032a63e40300000803f2ebd3b342ebd3b340000803f0000803f
+ m_CompressedMesh:
+ m_Vertices:
+ m_NumItems: 0
+ m_Range: 0
+ m_Start: 0
+ m_Data:
+ m_BitSize: 0
+ m_UV:
+ m_NumItems: 0
+ m_Range: 0
+ m_Start: 0
+ m_Data:
+ m_BitSize: 0
+ m_Normals:
+ m_NumItems: 0
+ m_Range: 0
+ m_Start: 0
+ m_Data:
+ m_BitSize: 0
+ m_Tangents:
+ m_NumItems: 0
+ m_Range: 0
+ m_Start: 0
+ m_Data:
+ m_BitSize: 0
+ m_Weights:
+ m_NumItems: 0
+ m_Data:
+ m_BitSize: 0
+ m_NormalSigns:
+ m_NumItems: 0
+ m_Data:
+ m_BitSize: 0
+ m_TangentSigns:
+ m_NumItems: 0
+ m_Data:
+ m_BitSize: 0
+ m_FloatColors:
+ m_NumItems: 0
+ m_Range: 0
+ m_Start: 0
+ m_Data:
+ m_BitSize: 0
+ m_BoneIndices:
+ m_NumItems: 0
+ m_Data:
+ m_BitSize: 0
+ m_Triangles:
+ m_NumItems: 0
+ m_Data:
+ m_BitSize: 0
+ m_UVInfo: 0
+ m_LocalAABB:
+ m_Center: {x: 0, y: 0, z: 1.1641532e-10}
+ m_Extent: {x: 0.063999996, y: 0.063999996, z: 0.0038042264}
+ m_MeshUsageFlags: 0
+ m_CookingOptions: 30
+ m_BakedConvexCollisionMesh:
+ m_BakedTriangleCollisionMesh:
+ 'm_MeshMetrics[0]': 1
+ 'm_MeshMetrics[1]': 1
+ m_MeshOptimizationFlags: 1
+ m_StreamData:
+ serializedVersion: 2
+ offset: 0
+ size: 0
+ path:
+ m_MeshLodInfo:
+ serializedVersion: 2
+ m_LodSelectionCurve:
+ serializedVersion: 1
+ m_LodSlope: 0
+ m_LodBias: 0
+ m_NumLevels: 1
+ m_SubMeshes:
+ - serializedVersion: 2
+ m_Levels:
+ - serializedVersion: 1
+ m_IndexStart: 0
+ m_IndexCount: 0
diff --git a/Assets/Gothic-VR/Meshes/SocketRing.asset.meta b/Assets/Gothic-VR/Meshes/SocketRing.asset.meta
new file mode 100644
index 000000000..8cb502bd6
--- /dev/null
+++ b/Assets/Gothic-VR/Meshes/SocketRing.asset.meta
@@ -0,0 +1,8 @@
+fileFormatVersion: 2
+guid: c734a1f14df34a445912fa9ce2346315
+NativeFormatImporter:
+ externalObjects: {}
+ mainObjectFileID: 4300000
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab
new file mode 100644
index 000000000..1f513ff8e
--- /dev/null
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab
@@ -0,0 +1,251 @@
+%YAML 1.1
+%TAG !u! tag:unity3d.com,2011:
+--- !u!1 &7034454508457674463
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 7165789852774304286}
+ - component: {fileID: 3389816361291436549}
+ - component: {fileID: 2951849760368368205}
+ - component: {fileID: 6523535316780004576}
+ - component: {fileID: 2601360332808143820}
+ - component: {fileID: 3550217527804890868}
+ m_Layer: 0
+ m_Name: VR Socket Slot
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &7165789852774304286
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7034454508457674463}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children:
+ - {fileID: 6494516477512632141}
+ m_Father: {fileID: 0}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!114 &3389816361291436549
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7034454508457674463}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 20620ae30fdd0164c9bb5bdc717d3ada, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Reflex::Reflex.Components.GameObjectSelfInjector
+ _injectionStrategy: 2
+--- !u!135 &2951849760368368205
+SphereCollider:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7034454508457674463}
+ m_Material: {fileID: 0}
+ m_IncludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_ExcludeLayers:
+ serializedVersion: 2
+ m_Bits: 0
+ m_LayerOverridePriority: 0
+ m_IsTrigger: 1
+ m_ProvidesContacts: 0
+ m_Enabled: 1
+ serializedVersion: 3
+ m_Radius: 0.07
+ m_Center: {x: 0, y: 0, z: 0}
+--- !u!114 &6523535316780004576
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7034454508457674463}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 471f1450115357944ad35a0a2e0a45e0, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Components.VrSocketSlot
+ _attachPoint: {fileID: 0}
+ _shouldScaleToFit: 1
+ _size: 0.05
+ _shouldRotateByCategory: 1
+ _shouldCenterItem: 1
+ _isShoulder: 0
+ _placedSoundFallback: {fileID: 0}
+ _removedSoundFallback: {fileID: 0}
+--- !u!114 &2601360332808143820
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7034454508457674463}
+ m_Enabled: 0
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: bf2e310a0e2e45642afdccbcb8ff797c, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Backends.Xri.XriSocketBridge
+ m_InteractionManager: {fileID: 0}
+ m_InteractionLayers:
+ m_Bits: 4294967295
+ m_Handedness: 0
+ m_AttachTransform: {fileID: 0}
+ m_KeepSelectedTargetValid: 1
+ m_DisableVisualsWhenBlockedInGroup: 1
+ m_StartingSelectedInteractable: {fileID: 0}
+ m_StartingTargetFilter: {fileID: 0}
+ m_AutoFindParentInteractableInHierarchy: 0
+ m_ParentInteractableObject: {fileID: 0}
+ m_HoverEntered:
+ m_PersistentCalls:
+ m_Calls: []
+ m_HoverExited:
+ m_PersistentCalls:
+ m_Calls: []
+ m_SelectEntered:
+ m_PersistentCalls:
+ m_Calls: []
+ m_SelectExited:
+ m_PersistentCalls:
+ m_Calls: []
+ m_StartingHoverFilters: []
+ m_StartingSelectFilters: []
+ m_ShowInteractableHoverMeshes: 0
+ m_InteractableHoverMeshMaterial: {fileID: 2100000, guid: 086e8cac1c176f14982891c8610dd9d7, type: 2}
+ m_InteractableCantHoverMeshMaterial: {fileID: 2100000, guid: 52dc35718064d9c43ab35b777254779c, type: 2}
+ m_SocketActive: 1
+ m_InteractableHoverScale: 1
+ m_RecycleDelayTime: 1
+ m_HoverSocketSnapping: 0
+ m_SocketSnappingRadius: 0.1
+ m_SocketScaleMode: 0
+ m_FixedScale: {x: 1, y: 1, z: 1}
+ m_TargetBoundsSize: {x: 1, y: 1, z: 1}
+ _handReleaseWindow: 0.5
+--- !u!114 &3550217527804890868
+MonoBehaviour:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 7034454508457674463}
+ m_Enabled: 1
+ m_EditorHideFlags: 0
+ m_Script: {fileID: 11500000, guid: 875ac271adb28194bbf02de59244807a, type: 3}
+ m_Name:
+ m_EditorClassIdentifier: Gothic.VR::Gothic.VR.Interaction.Components.VrSocketHoverFeedback
+ _slot: {fileID: 6523535316780004576}
+ _ringRenderer: {fileID: 8774205800838183018}
+ _scaleTarget: {fileID: 0}
+ _hoverScale: 1.25
+ _normalMaterial: {fileID: 2100000, guid: a3dcff690c3412844a33e16319700e44, type: 2}
+ _validMaterial: {fileID: 2100000, guid: 086e8cac1c176f14982891c8610dd9d7, type: 2}
+ _invalidMaterial: {fileID: 2100000, guid: 52dc35718064d9c43ab35b777254779c, type: 2}
+--- !u!1 &8594113158379179365
+GameObject:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ serializedVersion: 6
+ m_Component:
+ - component: {fileID: 6494516477512632141}
+ - component: {fileID: 8826080630609865501}
+ - component: {fileID: 8774205800838183018}
+ m_Layer: 0
+ m_Name: Ring
+ m_TagString: Untagged
+ m_Icon: {fileID: 0}
+ m_NavMeshLayer: 0
+ m_StaticEditorFlags: 0
+ m_IsActive: 1
+--- !u!4 &6494516477512632141
+Transform:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8594113158379179365}
+ serializedVersion: 2
+ m_LocalRotation: {x: 0, y: 0, z: 0, w: 1}
+ m_LocalPosition: {x: 0, y: 0, z: 0}
+ m_LocalScale: {x: 1, y: 1, z: 1}
+ m_ConstrainProportionsScale: 0
+ m_Children: []
+ m_Father: {fileID: 7165789852774304286}
+ m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0}
+--- !u!33 &8826080630609865501
+MeshFilter:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8594113158379179365}
+ m_Mesh: {fileID: 4300000, guid: c734a1f14df34a445912fa9ce2346315, type: 2}
+--- !u!23 &8774205800838183018
+MeshRenderer:
+ m_ObjectHideFlags: 0
+ m_CorrespondingSourceObject: {fileID: 0}
+ m_PrefabInstance: {fileID: 0}
+ m_PrefabAsset: {fileID: 0}
+ m_GameObject: {fileID: 8594113158379179365}
+ m_Enabled: 1
+ m_CastShadows: 0
+ m_ReceiveShadows: 0
+ m_DynamicOccludee: 1
+ m_StaticShadowCaster: 0
+ m_MotionVectors: 1
+ m_LightProbeUsage: 1
+ m_ReflectionProbeUsage: 1
+ m_RayTracingMode: 2
+ m_RayTraceProcedural: 0
+ m_RayTracingAccelStructBuildFlagsOverride: 0
+ m_RayTracingAccelStructBuildFlags: 1
+ m_SmallMeshCulling: 1
+ m_ForceMeshLod: -1
+ m_MeshLodSelectionBias: 0
+ m_RenderingLayerMask: 1
+ m_RendererPriority: 0
+ m_Materials:
+ - {fileID: 2100000, guid: a3dcff690c3412844a33e16319700e44, type: 2}
+ m_StaticBatchInfo:
+ firstSubMesh: 0
+ subMeshCount: 0
+ m_StaticBatchRoot: {fileID: 0}
+ m_ProbeAnchor: {fileID: 0}
+ m_LightProbeVolumeOverride: {fileID: 0}
+ m_ScaleInLightmap: 1
+ m_ReceiveGI: 1
+ m_PreserveUVs: 0
+ m_IgnoreNormalsForChartDetection: 0
+ m_ImportantGI: 0
+ m_StitchLightmapSeams: 1
+ m_SelectedEditorRenderState: 3
+ m_MinimumChartSize: 4
+ m_AutoUVMaxDistance: 0.5
+ m_AutoUVMaxAngle: 89
+ m_LightmapParameters: {fileID: 0}
+ m_GlobalIlluminationMeshLod: 0
+ m_SortingLayerID: 0
+ m_SortingLayer: 0
+ m_SortingOrder: 0
+ m_MaskInteraction: 0
+ m_AdditionalVertexStreams: {fileID: 0}
diff --git a/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab.meta b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab.meta
new file mode 100644
index 000000000..cd194247f
--- /dev/null
+++ b/Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab.meta
@@ -0,0 +1,7 @@
+fileFormatVersion: 2
+guid: e970795bd5895ed4d8a7686f050c5832
+PrefabImporter:
+ externalObjects: {}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs
index acec0ec87..40effaaf7 100644
--- a/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs
+++ b/Assets/Gothic-VR/Scripts/Adapters/Player/VRBackpack.cs
@@ -329,7 +329,7 @@ private void RefillSockets(List inventory)
});
vobContainer.Go.GetComponentInChildren().isKinematic = false; // Get rid of isKinematic warnings as the Grab is done "physically".
- _socketContainer.TryAdd(vobContainer.Go.GetComponentInChildren());
+ _socketContainer.TryPlace(vobContainer.Go.GetComponentInChildren());
}
}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocket.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocket.cs
index 6a3566ffd..07f93b3e3 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocket.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocket.cs
@@ -1,8 +1,8 @@
#if GOTHIC_HVR_INSTALLED
using System;
using Gothic.Core.Adapters.Vob;
-using Gothic.Core.Models.Vm;
using Gothic.Core.Extensions;
+using Gothic.VR.Interaction.Components;
using HurricaneVR.Framework.Core;
using HurricaneVR.Framework.Core.Grabbers;
using UnityEngine;
@@ -86,40 +86,8 @@ protected override void AttachGrabbable(HVRGrabbable grabbable)
private void RotateItemForCategorySlot(GameObject rootGo, ItemInstance item)
{
- switch ((VmGothicEnums.ItemFlags)item.MainFlag)
- {
- case VmGothicEnums.ItemFlags.ItemKatNf:
- case VmGothicEnums.ItemFlags.ItemKatMun:
- rootGo.transform.localRotation = Quaternion.Euler(-45, 0, -90);
- break;
- case VmGothicEnums.ItemFlags.ItemKatFf:
- rootGo.transform.localRotation = Quaternion.Euler(45, 180, -90);
- break;
- case VmGothicEnums.ItemFlags.ItemKatArmor:
- rootGo.transform.localRotation = Quaternion.Euler(0, 90, 0);
- break;
- case VmGothicEnums.ItemFlags.ItemKatFood:
- rootGo.transform.localRotation = Quaternion.Euler(0, 0, 22.5f);
- break;
- case VmGothicEnums.ItemFlags.ItemKatDocs:
- rootGo.transform.localRotation = Quaternion.Euler(90, 0, 90);
- break;
- case VmGothicEnums.ItemFlags.ItemKatRune:
- // Runes and magic scrolls
- rootGo.transform.localRotation = Quaternion.Euler(-90, 0, 90);
- break;
- case VmGothicEnums.ItemFlags.ItemKatMagic:
- // Rings and amulets
- rootGo.transform.localRotation = Quaternion.Euler(0, 0, 45);
- break;
- case VmGothicEnums.ItemFlags.ItemKatPotions:
- // 0, 0, 0 - nothing to rotate.
- break;
- default:
- // Misc and all other we missed.
- rootGo.transform.localRotation = Quaternion.Euler(-45, 135, -45);
- break;
- }
+ // Same table as the backend-neutral VrSocketSlot.
+ rootGo.transform.localRotation = VrSocketSlot.GetCategoryRotation(item);
}
}
}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocketContainerBridge.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocketContainerBridge.cs
index dbcce81a4..ab571cff9 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocketContainerBridge.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Hvr/HvrSocketContainerBridge.cs
@@ -16,6 +16,8 @@ namespace Gothic.VR.Interaction.Backends.Hvr
public class HvrSocketContainerBridge : MonoBehaviour, IVrSocketContainer
{
private HVRSocketContainer _container;
+ private readonly Dictionary _savedSounds = new();
+ private bool _isSilent;
private HVRSocketContainer Container
{
@@ -55,12 +57,42 @@ public bool TryFindFree(VrInteractable interactable, out IVrSocket socket)
return socket != null;
}
- public bool TryAdd(VrInteractable interactable)
+ public bool TryPlace(VrInteractable interactable, bool silent = false)
{
if (interactable == null || !interactable.TryGetComponent(out HVRGrabbable grabbable))
return false;
+ if (!silent)
+ return Container.TryAddGrabbable(grabbable);
- return Container.TryAddGrabbable(grabbable);
+ return TryFindFree(interactable, out var socket) && socket.TryPlace(interactable, true);
+ }
+
+ ///
+ /// HVR plays the sockets' fallback clips, so they're removed while silent and restored afterwards.
+ ///
+ public void SetSilent(bool isSilent)
+ {
+ if (_isSilent == isSilent)
+ return;
+
+ _isSilent = isSilent;
+ foreach (var socket in Sockets)
+ {
+ if (isSilent)
+ {
+ _savedSounds[socket] = (socket.PlacedSoundFallback, socket.RemovedSoundFallback);
+ socket.PlacedSoundFallback = null;
+ socket.RemovedSoundFallback = null;
+ }
+ else if (_savedSounds.TryGetValue(socket, out var sounds))
+ {
+ socket.PlacedSoundFallback = sounds.Placed;
+ socket.RemovedSoundFallback = sounds.Removed;
+ }
+ }
+
+ if (!isSilent)
+ _savedSounds.Clear();
}
private static IVrSocket GetSocket(Component hvrSocket)
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs
index d0d30d6c2..4a1af248b 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/GothicGrabInteractable.cs
@@ -1,6 +1,7 @@
using UnityEngine;
using UnityEngine.XR.Interaction.Toolkit;
using UnityEngine.XR.Interaction.Toolkit.Interactables;
+using UnityEngine.XR.Interaction.Toolkit.Interactors;
namespace Gothic.VR.Interaction.Backends.Xri
{
@@ -9,6 +10,9 @@ namespace Gothic.VR.Interaction.Backends.Xri
/// - BeforeGrabbed: raised (via XriInteractableBridge) before XRI captures the Rigidbody state. Gothic items lie
/// kinematic, the game turns physics on there, so that VelocityTracking moves them and they fall when dropped.
/// - Interpolation while held, against jitter of velocity tracked items while walking.
+ /// - Sockets (XriSocketBridge): while only a socket selects the item, XRI neither unparents nor moves it, as
+ /// VrSocketSlot re-parents its VobLoader. A hand grabbing it releases it from the socket first, so that the
+ /// socket restores the hierarchy before the hand's grab starts (HVR order: socket released, then grabbed).
///
/// Disabled in the prefab. XriInteractableBridge enables it on the XRI backend once the runtime colliders exist.
///
@@ -20,6 +24,12 @@ public class GothicGrabInteractable : XRGrabInteractable
private XriInteractableBridge _bridge;
private RigidbodyInterpolation _interpolationBeforeGrab;
private bool _isInterpolationOverridden;
+ private bool _isSocketDetach;
+
+ ///
+ /// Selected by a socket only (no hand).
+ ///
+ public bool IsSocketed => interactorsSelecting.Count == 1 && interactorsSelecting[0] is XriSocketBridge;
protected override void Awake()
{
@@ -27,8 +37,20 @@ protected override void Awake()
_bridge = GetComponent();
}
+ public override void ProcessInteractable(XRInteractionUpdateOrder.UpdatePhase updatePhase)
+ {
+ // The socket's VobLoader holds the item. Late is kept: it finishes a detach after the socket released it.
+ if (IsSocketed && updatePhase != XRInteractionUpdateOrder.UpdatePhase.Late)
+ return;
+
+ base.ProcessInteractable(updatePhase);
+ }
+
protected override void OnSelectEntering(SelectEnterEventArgs args)
{
+ if (args.interactorObject is not XriSocketBridge)
+ ReleaseFromSockets();
+
// Before base: XRGrabInteractable.Grab() stores isKinematic, which it restores on drop.
if (_bridge != null)
_bridge.OnSelectEntering(args);
@@ -36,6 +58,38 @@ protected override void OnSelectEntering(SelectEnterEventArgs args)
base.OnSelectEntering(args);
}
+ protected override void OnSelectExiting(SelectExitEventArgs args)
+ {
+ _isSocketDetach = args.interactorObject is XriSocketBridge;
+ base.OnSelectExiting(args);
+ }
+
+ protected override void Grab()
+ {
+ if (!IsSocketed)
+ {
+ base.Grab();
+ return;
+ }
+
+ var isUnparenting = unparentTransformOnGrab;
+ unparentTransformOnGrab = false;
+ base.Grab();
+ unparentTransformOnGrab = isUnparenting;
+ }
+
+ protected override void Detach()
+ {
+ // No throw out of a socket (and no warning for kinematic items).
+ if (_isSocketDetach)
+ {
+ _isSocketDetach = false;
+ return;
+ }
+
+ base.Detach();
+ }
+
protected override void SetupRigidbodyGrab(Rigidbody rigidbody)
{
// XRI calls it again when the movement type changes while held.
@@ -58,5 +112,17 @@ protected override void SetupRigidbodyDrop(Rigidbody rigidbody)
rigidbody.interpolation = _interpolationBeforeGrab;
_isInterpolationOverridden = false;
}
+
+ private void ReleaseFromSockets()
+ {
+ if (interactionManager == null)
+ return;
+
+ for (var i = interactorsSelecting.Count - 1; i >= 0; i--)
+ {
+ if (interactorsSelecting[i] is XriSocketBridge socket)
+ interactionManager.SelectExit((IXRSelectInteractor)socket, (IXRSelectInteractable)this);
+ }
+ }
}
}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs
index 7c946ed56..4e49f338f 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriBackend.cs
@@ -9,8 +9,9 @@ namespace Gothic.VR.Interaction.Backends.Xri
/// XR Interaction Toolkit backend. Always compiled, as XRI is a free package.
/// Implemented: input, haptics, SFX, UI laser pointers, the player rig (XriRig on "VR - Player XRI.prefab",
/// with ScreenFade/WallFade on its camera), the XR Interaction Simulator and item grabbing (Near-Far interactor
- /// per hand, GothicGrabInteractable + XriInteractableBridge on oCItem).
- /// Missing: grabbing mobs/NPCs (phase 2b), sockets (3), doors/chests/ladders (4) and hands (5).
+ /// per hand, GothicGrabInteractable + XriInteractableBridge on oCItem), using mobs/NPCs and the socket core
+ /// (VrSocketSlot + XriSocketBridge, "VR Socket Slot.prefab").
+ /// Missing: backpack/holsters (phase 3b), chests/NPC loot (3c), doors/chests/ladders (4) and hands (5).
///
public class XriBackend : IVrBackend
{
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriInteractableBridge.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriInteractableBridge.cs
index 92d76630f..2fec2fb2f 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriInteractableBridge.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriInteractableBridge.cs
@@ -32,6 +32,7 @@ public class XriInteractableBridge : MonoBehaviour, IVrInteractableBackend
private XRBaseInteractable _xrInteractable;
private VrInteractable _interactable;
private bool _isSubscribed;
+ private float _lastHandReleaseTime = float.NegativeInfinity;
public bool IsActive => _vrBackend is XriBackend;
public bool IsHeld => _xrInteractable != null && _xrInteractable.isSelected;
@@ -41,6 +42,26 @@ public class XriInteractableBridge : MonoBehaviour, IVrInteractableBackend
///
public bool IsSelectOnly => _xrInteractable != null && _xrInteractable is not XRGrabInteractable;
+ ///
+ /// Selected by a player hand (XriGrabberBridge).
+ ///
+ public bool IsHeldByHand
+ {
+ get
+ {
+ if (_xrInteractable == null)
+ return false;
+
+ foreach (var interactor in _xrInteractable.interactorsSelecting)
+ {
+ if (interactor is Component component && component.TryGetComponent(out XriGrabberBridge _))
+ return true;
+ }
+
+ return false;
+ }
+ }
+
public IReadOnlyList Colliders => _xrInteractable != null
? _xrInteractable.colliders
: System.Array.Empty();
@@ -113,7 +134,15 @@ public void SetupColliders()
public void SetSocketSounds(AudioClip socketed, AudioClip unsocketed)
{
- // TODO HVR phase 3: XRI sockets.
+ // Nothing to do: VrInteractable keeps the clips and VrSocketSlot plays them (backend-neutral).
+ }
+
+ ///
+ /// A player hand released this interactable within the last seconds (XriSocketBridge takes such items).
+ ///
+ public bool IsJustReleasedByHand(float seconds)
+ {
+ return Time.time - _lastHandReleaseTime <= seconds;
}
///
@@ -207,6 +236,9 @@ private void OnSelectExited(SelectExitEventArgs args)
if (!_selectArgs.Remove(args.interactorObject, out var grabArgs))
grabArgs = CreateOtherArgs(args.interactorObject);
+ if (args.interactorObject is Component component && component.TryGetComponent(out XriGrabberBridge _))
+ _lastHandReleaseTime = Time.time;
+
_interactable.RaiseReleased(grabArgs);
}
}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs
new file mode 100644
index 000000000..35baab00e
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs
@@ -0,0 +1,216 @@
+using Gothic.Core.Logging;
+using Gothic.VR.Interaction.Components;
+using Gothic.VR.Interaction.Ports;
+using Reflex.Attributes;
+using UnityEngine;
+using UnityEngine.XR.Interaction.Toolkit;
+using UnityEngine.XR.Interaction.Toolkit.Interactables;
+using UnityEngine.XR.Interaction.Toolkit.Interactors;
+using Logger = Gothic.Core.Logging.Logger;
+
+namespace Gothic.VR.Interaction.Backends.Xri
+{
+ ///
+ /// XRI part of a VrSocketSlot (same GameObject). XRI's socket does the detection (trigger collider), hover and
+ /// select. The Gothic part (re-parenting the VobLoader, rotation, scale, sounds, events) is done by VrSocketSlot:
+ /// - Only Gothic items are accepted, and only while a hand holds them or right after a hand released them
+ /// (items lying around next to a slot are not pulled in).
+ /// - XRI must not move the grabbable child: socket snapping and the socket's scale mode are off, and
+ /// GothicGrabInteractable doesn't process (move) itself while only a socket selects it.
+ ///
+ /// Disabled in the prefab. Enables itself on the XRI backend only, so that no XRInteractionManager is searched
+ /// or created on other backends.
+ ///
+ public class XriSocketBridge : XRSocketInteractor, IVrSocketBackend
+ {
+ [Tooltip("Seconds after a hand released an item, in which the slot still takes it.")]
+ [SerializeField] private float _handReleaseWindow = 0.5f;
+
+ [Inject] private readonly IVrBackend _vrBackend;
+
+ private VrSocketSlot _slot;
+ private XRGrabInteractable _pending;
+ private bool _isDetectionEnabled = true;
+
+ public bool IsActive => _vrBackend is XriBackend;
+
+ protected override void Awake()
+ {
+ base.Awake();
+
+ _slot = GetComponent();
+ if (_slot == null)
+ {
+ Logger.LogError($"[XRI] {name} needs a {nameof(VrSocketSlot)} next to {nameof(XriSocketBridge)}.",
+ LogCat.VR);
+ return;
+ }
+
+ // The VobLoader is moved by VrSocketSlot, not the grabbable child by XRI.
+ hoverSocketSnapping = false;
+ socketScaleMode = SocketScaleMode.None;
+ showInteractableHoverMeshes = false;
+ keepSelectedTargetValid = true;
+ startingSelectedInteractable = null;
+
+ if (IsActive)
+ enabled = true;
+ }
+
+ private void Update()
+ {
+ TrySelectPending();
+ }
+
+ public bool TryPlace(VrInteractable interactable)
+ {
+ var grabInteractable = interactable != null ? interactable.GetComponent() : null;
+ if (grabInteractable == null || (hasSelection && !IsSelecting(grabInteractable)))
+ return false;
+
+ _pending = grabInteractable;
+ TrySelectPending();
+ return true;
+ }
+
+ public void ForceRelease()
+ {
+ _pending = null;
+ if (!hasSelection || interactionManager == null)
+ return;
+
+ interactionManager.SelectExit((IXRSelectInteractor)this, firstInteractableSelected);
+ }
+
+ public void SetDetectionEnabled(bool isEnabled)
+ {
+ // socketActive off would also release a held item (isSelectActive is false then).
+ _isDetectionEnabled = isEnabled;
+ }
+
+ public override bool CanHover(IXRHoverInteractable interactable)
+ {
+ if (!_isDetectionEnabled || !base.CanHover(interactable) || _slot == null)
+ return false;
+
+ var vrInteractable = GetInteractable(interactable);
+ if (vrInteractable == null || vrInteractable == _slot.Held || !VrSocketSlot.IsItem(vrInteractable))
+ return false;
+
+ return IsHeldOrJustReleasedByHand(interactable);
+ }
+
+ public override bool CanSelect(IXRSelectInteractable interactable)
+ {
+ if (IsSelecting(interactable))
+ return base.CanSelect(interactable);
+
+ // Programmatic placement ignores the filters (IVrSocket.TryPlace).
+ if (_pending != null && ReferenceEquals(interactable, _pending))
+ return !hasSelection;
+
+ if (!_isDetectionEnabled || !base.CanSelect(interactable) || _slot == null || _slot.IsOccupied)
+ return false;
+
+ var vrInteractable = GetInteractable(interactable);
+ return vrInteractable != null && VrSocketSlot.IsItem(vrInteractable) &&
+ IsJustReleasedByHand(interactable);
+ }
+
+ protected override bool StartSocketSnapping(XRGrabInteractable grabInteractable)
+ {
+ // XRI's socket grab transformer would move (and scale) the grabbable child.
+ return false;
+ }
+
+ protected override void OnHoverEntered(HoverEnterEventArgs args)
+ {
+ base.OnHoverEntered(args);
+ if (_slot != null)
+ _slot.OnBackendHoverEntered(GetInteractable(args.interactableObject));
+ }
+
+ protected override void OnHoverExited(HoverExitEventArgs args)
+ {
+ base.OnHoverExited(args);
+ if (_slot != null)
+ _slot.OnBackendHoverExited(GetInteractable(args.interactableObject));
+ }
+
+ ///
+ /// Before the interactable's OnSelectEntering (GothicGrabInteractable.Grab), so that it keeps the VobLoader
+ /// as its parent and becomes kinematic in the slot.
+ ///
+ protected override void OnSelectEntering(SelectEnterEventArgs args)
+ {
+ if (ReferenceEquals(args.interactableObject, _pending))
+ _pending = null;
+
+ base.OnSelectEntering(args);
+ if (_slot != null)
+ _slot.OnBackendPlacing(GetInteractable(args.interactableObject));
+ }
+
+ protected override void OnSelectEntered(SelectEnterEventArgs args)
+ {
+ base.OnSelectEntered(args);
+ if (_slot != null)
+ _slot.OnBackendPlaced(GetInteractable(args.interactableObject));
+ }
+
+ ///
+ /// After the interactable's OnSelectExiting (drop), so that VrSocketSlot restores the hierarchy last.
+ /// A hand taking the item: GothicGrabInteractable releases it from the socket before the hand's grab.
+ ///
+ protected override void OnSelectExited(SelectExitEventArgs args)
+ {
+ base.OnSelectExited(args);
+ if (_slot != null)
+ _slot.OnBackendRemoved(GetInteractable(args.interactableObject));
+ }
+
+ ///
+ /// Finishes a programmatic placement once the interactable is registered (freshly spawned items enable their
+ /// XRI interactable in Start).
+ ///
+ private void TrySelectPending()
+ {
+ // Destroyed or deactivated before it was selected: VrSocketSlot still holds it in Gothic terms.
+ if (_pending == null || !_pending.gameObject.activeInHierarchy)
+ return;
+
+ if (!isActiveAndEnabled || interactionManager == null || !_pending.isActiveAndEnabled ||
+ !interactionManager.IsRegistered((IXRInteractable)_pending))
+ return;
+
+ var pending = _pending;
+ if (pending.isSelected)
+ interactionManager.CancelInteractableSelection((IXRSelectInteractable)pending);
+
+ interactionManager.SelectEnter((IXRSelectInteractor)this, (IXRSelectInteractable)pending);
+ if (!IsSelecting(pending))
+ {
+ Logger.LogWarning($"[XRI] {name} couldn't select {pending.name}.", LogCat.VR);
+ _pending = null;
+ _slot.ForceRelease();
+ }
+ }
+
+ private bool IsHeldOrJustReleasedByHand(IXRInteractable interactable)
+ {
+ var bridge = (interactable as Component)?.GetComponent();
+ return bridge != null && (bridge.IsHeldByHand || bridge.IsJustReleasedByHand(_handReleaseWindow));
+ }
+
+ private bool IsJustReleasedByHand(IXRInteractable interactable)
+ {
+ var bridge = (interactable as Component)?.GetComponent();
+ return bridge != null && !bridge.IsHeldByHand && bridge.IsJustReleasedByHand(_handReleaseWindow);
+ }
+
+ private static VrInteractable GetInteractable(IXRInteractable interactable)
+ {
+ return interactable is Component component ? component.GetComponent() : null;
+ }
+ }
+}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs.meta
new file mode 100644
index 000000000..5468a0b0d
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: bf2e310a0e2e45642afdccbcb8ff797c
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs
new file mode 100644
index 000000000..872c4e1dc
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs
@@ -0,0 +1,123 @@
+using System.Collections.Generic;
+using Gothic.Core.Logging;
+using Gothic.VR.Interaction.Models;
+using Gothic.VR.Interaction.Ports;
+using UnityEngine;
+using Logger = Gothic.Core.Logging.Logger;
+
+namespace Gothic.VR.Interaction.Components
+{
+ ///
+ /// Trigger volume (e.g. inside an open chest). An item released by a hand while it's inside the volume is put
+ /// into a free slot of the container. Needs a trigger collider on this GameObject on a layer that collides with
+ /// held items.
+ /// The placement happens one frame after the release, so that the backend has finished the release (and a slot
+ /// that caught the item itself wins).
+ ///
+ public class VrContainerDropZone : MonoBehaviour
+ {
+ [Tooltip("GameObject with the IVrSocketContainer (e.g. VrSocketContainer). Empty: this GameObject or its " +
+ "parents.")]
+ [SerializeField] private GameObject _containerGo;
+
+ private readonly Dictionary _colliderCounts = new();
+ private readonly List _pendingDrops = new();
+ private readonly List _dropBuffer = new();
+ private IVrSocketContainer _container;
+
+ public IVrSocketContainer Container
+ {
+ get
+ {
+ if (_container == null)
+ _container = _containerGo != null
+ ? _containerGo.GetComponent()
+ : GetComponentInParent(true);
+ return _container;
+ }
+ }
+
+ private void Awake()
+ {
+ if (Container == null)
+ Logger.LogWarning($"[Socket] Drop zone {name} has no IVrSocketContainer.", LogCat.VR);
+ }
+
+ private void OnDisable()
+ {
+ foreach (var interactable in _colliderCounts.Keys)
+ {
+ if (interactable != null)
+ interactable.Released -= OnReleased;
+ }
+
+ _colliderCounts.Clear();
+ _pendingDrops.Clear();
+ }
+
+ private void Update()
+ {
+ if (_pendingDrops.Count == 0)
+ return;
+
+ _dropBuffer.AddRange(_pendingDrops);
+ _pendingDrops.Clear();
+
+ foreach (var interactable in _dropBuffer)
+ {
+ // Destroyed, grabbed again (other hand) or already caught by a slot.
+ if (interactable == null || interactable.IsHeld || Container == null)
+ continue;
+
+ if (!Container.TryPlace(interactable))
+ Logger.Log($"[Socket] No free slot for {interactable.name} in {name}.", LogCat.VR);
+ }
+
+ _dropBuffer.Clear();
+ }
+
+ private void OnTriggerEnter(Collider other)
+ {
+ var interactable = GetInteractable(other);
+ if (interactable == null)
+ return;
+
+ _colliderCounts.TryGetValue(interactable, out var count);
+ _colliderCounts[interactable] = count + 1;
+ if (count == 0)
+ interactable.Released += OnReleased;
+ }
+
+ private void OnTriggerExit(Collider other)
+ {
+ var interactable = GetInteractable(other);
+ if (interactable == null || !_colliderCounts.TryGetValue(interactable, out var count))
+ return;
+
+ if (count > 1)
+ {
+ _colliderCounts[interactable] = count - 1;
+ return;
+ }
+
+ _colliderCounts.Remove(interactable);
+ interactable.Released -= OnReleased;
+ }
+
+ private void OnReleased(VrGrabArgs args)
+ {
+ if (!args.IsHandOrDistance || !VrSocketSlot.IsItem(args.Interactable))
+ return;
+
+ if (!_pendingDrops.Contains(args.Interactable))
+ _pendingDrops.Add(args.Interactable);
+ }
+
+ private static VrInteractable GetInteractable(Collider other)
+ {
+ var body = other.attachedRigidbody;
+ var interactable = body != null ? body.GetComponent() : null;
+ return interactable != null ? interactable : other.GetComponentInParent();
+ }
+ }
+}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs.meta
new file mode 100644
index 000000000..1ce40788f
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: d7941123ebdc7d646a9e2268240bc009
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrInteractable.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrInteractable.cs
index 083458333..b30e4dfc8 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Components/VrInteractable.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrInteractable.cs
@@ -35,6 +35,16 @@ public class VrInteractable : MonoBehaviour
///
public bool IsSelectOnly => Backend != null && Backend.IsSelectOnly;
+ ///
+ /// Played by VrSocketSlot when this interactable is put into a slot (instead of the slot's fallback).
+ ///
+ public AudioClip SocketedSound { get; private set; }
+
+ ///
+ /// Played by VrSocketSlot when this interactable is taken out of a slot (instead of the slot's fallback).
+ ///
+ public AudioClip UnsocketedSound { get; private set; }
+
public IReadOnlyList Colliders => Backend != null ? Backend.Colliders : Array.Empty();
private IVrInteractableBackend Backend
@@ -90,6 +100,8 @@ public void SetupColliders()
///
public void SetSocketSounds(AudioClip socketed, AudioClip unsocketed)
{
+ SocketedSound = socketed;
+ UnsocketedSound = unsocketed;
Backend?.SetSocketSounds(socketed, unsocketed);
}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs
new file mode 100644
index 000000000..ea15b5287
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs
@@ -0,0 +1,111 @@
+using System.Collections.Generic;
+using Gothic.VR.Interaction.Ports;
+using UnityEngine;
+
+namespace Gothic.VR.Interaction.Components
+{
+ ///
+ /// Backend-neutral group of VrSocketSlots (backpack inventory, chest content).
+ /// Uses the slots listed in the inspector or, if the list is empty, all VrSocketSlots below this GameObject.
+ ///
+ public class VrSocketContainer : MonoBehaviour, IVrSocketContainer
+ {
+ [Tooltip("Slots of this container in placement order. Empty: all VrSocketSlots below this GameObject.")]
+ [SerializeField] private List _slots = new();
+
+ private readonly List _sockets = new();
+ private bool _isCollected;
+ private bool _isAutoCollected;
+ private bool _isSilent;
+
+ public IReadOnlyList Sockets
+ {
+ get
+ {
+ CollectSlots();
+ return _sockets;
+ }
+ }
+
+ public IReadOnlyList Slots
+ {
+ get
+ {
+ CollectSlots();
+ return _slots;
+ }
+ }
+
+ private void Awake()
+ {
+ CollectSlots();
+ }
+
+ ///
+ /// Re-collects the slots below this GameObject (after slots were added or removed at runtime).
+ /// Only if no slots are listed in the inspector.
+ ///
+ public void RefreshSlots()
+ {
+ _isCollected = false;
+ CollectSlots();
+ }
+
+ public bool TryFindFree(VrInteractable interactable, out IVrSocket socket)
+ {
+ socket = null;
+ if (interactable == null)
+ return false;
+
+ foreach (var slot in Slots)
+ {
+ if (slot == null || slot.IsOccupied || !slot.isActiveAndEnabled)
+ continue;
+
+ socket = slot;
+ return true;
+ }
+
+ return false;
+ }
+
+ public bool TryPlace(VrInteractable interactable, bool silent = false)
+ {
+ return TryFindFree(interactable, out var socket) && socket.TryPlace(interactable, silent);
+ }
+
+ public void SetSilent(bool isSilent)
+ {
+ _isSilent = isSilent;
+ foreach (var slot in Slots)
+ {
+ if (slot != null)
+ slot.SetSilent(isSilent);
+ }
+ }
+
+ private void CollectSlots()
+ {
+ if (_isCollected)
+ return;
+
+ _isCollected = true;
+ if (_isAutoCollected || _slots.Count == 0 || _slots.TrueForAll(slot => slot == null))
+ {
+ _isAutoCollected = true;
+ _slots.Clear();
+ GetComponentsInChildren(true, _slots);
+ }
+
+ _sockets.Clear();
+ foreach (var slot in _slots)
+ {
+ if (slot == null)
+ continue;
+
+ _sockets.Add(slot);
+ slot.SetSilent(_isSilent);
+ }
+ }
+ }
+}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs.meta
new file mode 100644
index 000000000..1f0403648
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 94103fbff7b07d64fb8a87c5cc6e1a54
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs
new file mode 100644
index 000000000..2c909c46e
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs
@@ -0,0 +1,100 @@
+using Gothic.VR.Interaction.Models;
+using UnityEngine;
+
+namespace Gothic.VR.Interaction.Components
+{
+ ///
+ /// Ring feedback of a VrSocketSlot: while an item hovers the slot, the ring grows and shows the valid (slot is
+ /// free) or invalid (slot is occupied) material. Otherwise, it shows the normal material at its original scale.
+ ///
+ public class VrSocketHoverFeedback : MonoBehaviour
+ {
+ [Tooltip("The slot to watch. Empty: the VrSocketSlot on this GameObject or its parents.")]
+ [SerializeField] private VrSocketSlot _slot;
+
+ [Tooltip("Renderer of the ring.")]
+ [SerializeField] private Renderer _ringRenderer;
+
+ [Tooltip("Transform scaled while hovered. Empty: the ring renderer's transform.")]
+ [SerializeField] private Transform _scaleTarget;
+
+ [Tooltip("Scale factor while an item hovers the slot.")]
+ [SerializeField] private float _hoverScale = 1.25f;
+
+ [Tooltip("Material while nothing hovers the slot.")]
+ [SerializeField] private Material _normalMaterial;
+
+ [Tooltip("Material while an item hovers a free slot.")]
+ [SerializeField] private Material _validMaterial;
+
+ [Tooltip("Material while an item hovers an occupied slot.")]
+ [SerializeField] private Material _invalidMaterial;
+
+ private Vector3 _originalScale;
+ private bool _isSubscribed;
+
+ private Transform ScaleTarget => _scaleTarget != null
+ ? _scaleTarget
+ : _ringRenderer != null ? _ringRenderer.transform : null;
+
+ private void Awake()
+ {
+ if (_slot == null)
+ _slot = GetComponentInParent(true);
+ if (ScaleTarget != null)
+ _originalScale = ScaleTarget.localScale;
+ if (_normalMaterial == null && _ringRenderer != null)
+ _normalMaterial = _ringRenderer.sharedMaterial;
+
+ if (_slot == null)
+ return;
+
+ _slot.HoverEntered += OnHoverChanged;
+ _slot.HoverExited += OnHoverChanged;
+ _slot.Grabbed += OnHeldChanged;
+ _slot.Released += OnHeldChanged;
+ _isSubscribed = true;
+ }
+
+ private void OnEnable()
+ {
+ Refresh();
+ }
+
+ private void OnDestroy()
+ {
+ if (!_isSubscribed || _slot == null)
+ return;
+
+ _slot.HoverEntered -= OnHoverChanged;
+ _slot.HoverExited -= OnHoverChanged;
+ _slot.Grabbed -= OnHeldChanged;
+ _slot.Released -= OnHeldChanged;
+ }
+
+ private void OnHoverChanged(VrInteractable interactable)
+ {
+ Refresh();
+ }
+
+ private void OnHeldChanged(VrGrabArgs args)
+ {
+ Refresh();
+ }
+
+ private void Refresh()
+ {
+ if (_slot == null)
+ return;
+
+ var isHovered = _slot.IsHovered;
+ var material = !isHovered ? _normalMaterial : _slot.IsOccupied ? _invalidMaterial : _validMaterial;
+ if (_ringRenderer != null && material != null)
+ _ringRenderer.sharedMaterial = material;
+
+ var scaleTarget = ScaleTarget;
+ if (scaleTarget != null)
+ scaleTarget.localScale = isHovered ? _originalScale * _hoverScale : _originalScale;
+ }
+ }
+}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs.meta
new file mode 100644
index 000000000..04dfa2afb
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 875ac271adb28194bbf02de59244807a
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs
new file mode 100644
index 000000000..2079b93ce
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs
@@ -0,0 +1,530 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Gothic.Core.Adapters.Vob;
+using Gothic.Core.Logging;
+using Gothic.Core.Models.Vm;
+using Gothic.VR.Interaction.Models;
+using Gothic.VR.Interaction.Ports;
+using Reflex.Attributes;
+using UnityEngine;
+using ZenKit.Daedalus;
+using ZenKit.Vobs;
+using Logger = Gothic.Core.Logging.Logger;
+
+namespace Gothic.VR.Interaction.Components
+{
+ ///
+ /// Backend-neutral slot for one item: hip holster, backpack slot, chest slot, NPC loot slot.
+ /// Owns the Gothic behaviour:
+ /// - Our items are VobLoader -> Grabbable (VrInteractable) -> Mesh. Culling and saving work on the VobLoader, so
+ /// the slot re-parents the VobLoader (not the grabbable child) and restores the previous parent on removal.
+ /// - Rotation by item category, scale-to-fit by the bounds of the item's renderers (cached per item), kinematic
+ /// while inside.
+ /// - Sounds (the interactable's socket sounds or the slot's fallbacks) and the IVrSocket events
+ /// (BeforeGrabbed/Grabbed = placed, Released = removed).
+ ///
+ /// A backend part (IVrSocketBackend, e.g. XriSocketBridge) on the same GameObject detects items released into
+ /// the slot and holds them in the backend's terms. Without an active backend part, the slot still works for
+ /// programmatic TryPlace/ForceRelease (nothing is detected).
+ ///
+ public class VrSocketSlot : MonoBehaviour, IVrSocket
+ {
+ private const int MaxFitFrames = 5;
+ private const float MinExtent = 0.0001f;
+
+ private static readonly ConditionalWeakTable> _boundsCache = new();
+ private static readonly List _rendererBuffer = new();
+
+ [Tooltip("Where the item is put. Empty: this transform.")]
+ [SerializeField] private Transform _attachPoint;
+
+ [Tooltip("Scales the item while it's inside, so that half of its largest side is Size (like HVR's socket " +
+ "Size: backpack slot 0.05, chest slot 0.18). Measured in the attach point's space.")]
+ [SerializeField] private bool _shouldScaleToFit = true;
+
+ [Tooltip("Half of the largest side of an item inside the slot (see Should Scale To Fit).")]
+ [SerializeField] private float _size = 0.05f;
+
+ [Tooltip("Rotates the item by its Gothic category (weapons diagonal, documents flat, ...).")]
+ [SerializeField] private bool _shouldRotateByCategory = true;
+
+ [Tooltip("Moves the item so that the center of its bounds is on the attach point.")]
+ [SerializeField] private bool _shouldCenterItem = true;
+
+ [Tooltip("Over-the-shoulder slot (backpack, right shoulder holster). See IVrSocket.IsShoulder.")]
+ [SerializeField] private bool _isShoulder;
+
+ [Tooltip("Played when an item without own socket sounds is put in.")]
+ [SerializeField] private AudioClip _placedSoundFallback;
+
+ [Tooltip("Played when an item without own socket sounds is taken out.")]
+ [SerializeField] private AudioClip _removedSoundFallback;
+
+ [Inject] private readonly IVrSfxPlayer _sfxPlayer;
+
+ public event Action BeforeGrabbed;
+ public event Action Grabbed;
+ public event Action Released;
+
+ ///
+ /// An interactable which the backend accepts as candidate is hovering the slot (hand-held or just released).
+ ///
+ public event Action HoverEntered;
+
+ public event Action HoverExited;
+
+ private readonly List _hovered = new();
+
+ private IVrSocketBackend _backend;
+ private bool _isBackendResolved;
+ private bool _isSilent;
+
+ private VrInteractable _held;
+ private VobLoader _heldLoader;
+ private Transform _previousParent;
+ private Vector3 _previousScale;
+ private Rigidbody _heldRigidbody;
+ private bool _wasKinematic;
+ private bool _isPlacedEventRaised;
+ private Coroutine _fitRoutine;
+
+ public VrGrabberKind Kind => VrGrabberKind.Socket;
+ public VrHandSide HandSide => VrHandSide.Left;
+ public VrInteractable Held => _held;
+ public Transform Transform => transform;
+ public bool IsOccupied => _held != null;
+ public bool IsShoulder => _isShoulder;
+ public bool IsHovered => _hovered.Count > 0;
+ public Transform AttachPoint => _attachPoint != null ? _attachPoint : transform;
+
+ public float Size
+ {
+ get => _size;
+ set => _size = value;
+ }
+
+ public AudioClip PlacedSoundFallback
+ {
+ get => _placedSoundFallback;
+ set => _placedSoundFallback = value;
+ }
+
+ public AudioClip RemovedSoundFallback
+ {
+ get => _removedSoundFallback;
+ set => _removedSoundFallback = value;
+ }
+
+ private IVrSocketBackend Backend
+ {
+ get
+ {
+ if (!_isBackendResolved)
+ {
+ _backend = FindActiveBackend();
+ _isBackendResolved = true;
+ }
+
+ return _backend;
+ }
+ }
+
+ ///
+ /// True if the interactable is a Gothic item (VobLoader of an oCItem). Only items go into slots.
+ ///
+ public static bool IsItem(VrInteractable interactable)
+ {
+ if (interactable == null || interactable.IsSelectOnly)
+ return false;
+
+ var vob = interactable.GetComponentInParent(true)?.Container?.Vob;
+ return vob != null && vob.Type == VirtualObjectType.oCItem;
+ }
+
+ ///
+ /// Local rotation of an item inside a slot, by its category. Identity for unknown items.
+ ///
+ public static Quaternion GetCategoryRotation(ItemInstance item)
+ {
+ if (item == null)
+ return Quaternion.identity;
+
+ switch ((VmGothicEnums.ItemFlags)item.MainFlag)
+ {
+ case VmGothicEnums.ItemFlags.ItemKatNf:
+ case VmGothicEnums.ItemFlags.ItemKatMun:
+ return Quaternion.Euler(-45, 0, -90);
+ case VmGothicEnums.ItemFlags.ItemKatFf:
+ return Quaternion.Euler(45, 180, -90);
+ case VmGothicEnums.ItemFlags.ItemKatArmor:
+ return Quaternion.Euler(0, 90, 0);
+ case VmGothicEnums.ItemFlags.ItemKatFood:
+ return Quaternion.Euler(0, 0, 22.5f);
+ case VmGothicEnums.ItemFlags.ItemKatDocs:
+ return Quaternion.Euler(90, 0, 90);
+ case VmGothicEnums.ItemFlags.ItemKatRune:
+ // Runes and magic scrolls
+ return Quaternion.Euler(-90, 0, 90);
+ case VmGothicEnums.ItemFlags.ItemKatMagic:
+ // Rings and amulets
+ return Quaternion.Euler(0, 0, 45);
+ case VmGothicEnums.ItemFlags.ItemKatPotions:
+ return Quaternion.identity;
+ default:
+ // Misc and all others.
+ return Quaternion.Euler(-45, 135, -45);
+ }
+ }
+
+ private void Awake()
+ {
+ _ = Backend;
+ }
+
+ private void OnDisable()
+ {
+ // Hover exits of a disabled backend may not arrive anymore.
+ _hovered.Clear();
+ }
+
+ ///
+ /// True if the interactable can go into this slot now (free, or it's already inside).
+ ///
+ public bool IsValidFor(VrInteractable interactable)
+ {
+ return !IsOccupied || _held == interactable;
+ }
+
+ ///
+ /// While silent, no placed/removed sounds are played (bulk spawning of chest content).
+ ///
+ public void SetSilent(bool isSilent)
+ {
+ _isSilent = isSilent;
+ }
+
+ ///
+ /// Off: the slot doesn't detect items released into it (TryPlace still works).
+ ///
+ public void SetDetectionEnabled(bool isEnabled)
+ {
+ Backend?.SetDetectionEnabled(isEnabled);
+ }
+
+ public bool TryPlace(VrInteractable interactable, bool silent)
+ {
+ if (interactable == null || IsOccupied)
+ return false;
+
+ var loader = interactable.GetComponentInParent(true);
+ if (loader == null)
+ {
+ Logger.LogWarning($"[Socket] {interactable.name} has no VobLoader and can't be put into {name}.",
+ LogCat.VR);
+ return false;
+ }
+
+ var args = CreateArgs(interactable);
+ BeforeGrabbed?.Invoke(args);
+ Attach(interactable, loader);
+ _isPlacedEventRaised = true;
+ PlaySound(interactable.SocketedSound != null ? interactable.SocketedSound : _placedSoundFallback, silent);
+ Grabbed?.Invoke(args);
+
+ if (Backend == null || Backend.TryPlace(interactable))
+ return true;
+
+ Logger.LogWarning($"[Socket] Backend couldn't hold {interactable.name} in {name}. Taken out again.",
+ LogCat.VR);
+ Remove(true);
+ return false;
+ }
+
+ public void ForceRelease()
+ {
+ if (!IsOccupied)
+ return;
+
+ // The backend reports its release via OnBackendRemoved.
+ Backend?.ForceRelease();
+
+ // No backend, or the backend hadn't finished the placement yet.
+ if (IsOccupied)
+ Remove(false);
+ }
+
+ ///
+ /// The backend starts holding an interactable (released into the slot). Called before the backend moves or
+ /// configures it.
+ ///
+ internal void OnBackendPlacing(VrInteractable interactable)
+ {
+ if (interactable == null || interactable == _held)
+ return;
+
+ if (IsOccupied)
+ {
+ Logger.LogWarning($"[Socket] {name} is occupied by {_held.name}, {interactable.name} is ignored.",
+ LogCat.VR);
+ return;
+ }
+
+ var loader = interactable.GetComponentInParent(true);
+ if (loader == null)
+ return;
+
+ BeforeGrabbed?.Invoke(CreateArgs(interactable));
+ Attach(interactable, loader);
+ _isPlacedEventRaised = false;
+ }
+
+ ///
+ /// The backend holds the interactable now.
+ ///
+ internal void OnBackendPlaced(VrInteractable interactable)
+ {
+ if (interactable == null || interactable != _held || _isPlacedEventRaised)
+ return;
+
+ _isPlacedEventRaised = true;
+ PlaySound(interactable.SocketedSound != null ? interactable.SocketedSound : _placedSoundFallback, false);
+ Grabbed?.Invoke(CreateArgs(interactable));
+ }
+
+ ///
+ /// The backend released the interactable (taken by a hand, ForceRelease, ...).
+ ///
+ internal void OnBackendRemoved(VrInteractable interactable)
+ {
+ if (interactable == null || interactable != _held)
+ return;
+
+ Remove(false);
+ }
+
+ internal void OnBackendHoverEntered(VrInteractable interactable)
+ {
+ if (interactable == null || interactable == _held || _hovered.Contains(interactable))
+ return;
+
+ _hovered.Add(interactable);
+ HoverEntered?.Invoke(interactable);
+ }
+
+ internal void OnBackendHoverExited(VrInteractable interactable)
+ {
+ if (!_hovered.Remove(interactable))
+ return;
+
+ HoverExited?.Invoke(interactable);
+ }
+
+ private IVrSocketBackend FindActiveBackend()
+ {
+ foreach (var backend in GetComponents())
+ {
+ if (backend.IsActive)
+ return backend;
+ }
+
+ return null;
+ }
+
+ private VrGrabArgs CreateArgs(VrInteractable interactable)
+ {
+ return new VrGrabArgs(VrGrabberKind.Socket, VrHandSide.Left, interactable, gameObject);
+ }
+
+ private void Attach(VrInteractable interactable, VobLoader loader)
+ {
+ _held = interactable;
+ _heldLoader = loader;
+ OnBackendHoverExited(interactable);
+
+ var loaderTransform = loader.transform;
+ _previousParent = loaderTransform.parent;
+ _previousScale = loaderTransform.localScale;
+
+ // A hand moved the grabbable child away from its VobLoader. Inside the slot, both are at one place again.
+ var grabbableTransform = interactable.transform;
+ if (grabbableTransform != loaderTransform)
+ {
+ if (grabbableTransform.parent != loaderTransform)
+ grabbableTransform.SetParent(loaderTransform, false);
+ grabbableTransform.SetLocalPositionAndRotation(Vector3.zero, Quaternion.identity);
+ }
+
+ _heldRigidbody = interactable.GetComponent();
+ if (_heldRigidbody != null)
+ {
+ _wasKinematic = _heldRigidbody.isKinematic;
+ if (!_heldRigidbody.isKinematic)
+ {
+ _heldRigidbody.linearVelocity = Vector3.zero;
+ _heldRigidbody.angularVelocity = Vector3.zero;
+ }
+
+ _heldRigidbody.isKinematic = true;
+ }
+
+ loaderTransform.SetParent(AttachPoint, false);
+ loaderTransform.SetLocalPositionAndRotation(Vector3.zero, GetRotation(loader));
+
+ // Meshes of freshly spawned items may come a frame later.
+ if (!TryFitToSlot(loader) && isActiveAndEnabled)
+ _fitRoutine = StartCoroutine(FitWhenMeshesExist(loader));
+ }
+
+ private void Remove(bool silent)
+ {
+ var interactable = _held;
+ var loader = _heldLoader;
+ var args = CreateArgs(interactable);
+
+ Detach(loader);
+ PlaySound(interactable != null && interactable.UnsocketedSound != null
+ ? interactable.UnsocketedSound
+ : _removedSoundFallback, silent);
+ Released?.Invoke(args);
+ }
+
+ private void Detach(VobLoader loader)
+ {
+ if (_fitRoutine != null)
+ {
+ StopCoroutine(_fitRoutine);
+ _fitRoutine = null;
+ }
+
+ if (_heldRigidbody != null)
+ _heldRigidbody.isKinematic = _wasKinematic;
+
+ var previousParent = _previousParent;
+ var previousScale = _previousScale;
+ _held = null;
+ _heldLoader = null;
+ _heldRigidbody = null;
+ _previousParent = null;
+
+ // Skip VobLoaders which are destroyed or being destroyed (deactivated before Destroy()).
+ if (loader == null || !loader.gameObject.activeSelf)
+ return;
+
+ var loaderTransform = loader.transform;
+ loaderTransform.SetParent(previousParent, true);
+ loaderTransform.localScale = previousScale;
+ }
+
+ private Quaternion GetRotation(VobLoader loader)
+ {
+ if (!_shouldRotateByCategory || loader.Container?.Vob == null)
+ return Quaternion.identity;
+
+ return GetCategoryRotation(loader.Container.GetItemInstance());
+ }
+
+ private IEnumerator FitWhenMeshesExist(VobLoader loader)
+ {
+ for (var i = 0; i < MaxFitFrames; i++)
+ {
+ yield return null;
+ if (loader == null || loader != _heldLoader)
+ yield break;
+ if (TryFitToSlot(loader))
+ break;
+ }
+
+ _fitRoutine = null;
+ }
+
+ private bool TryFitToSlot(VobLoader loader)
+ {
+ if (!_shouldScaleToFit && !_shouldCenterItem)
+ return true;
+ if (!TryGetLocalBounds(loader, out var bounds))
+ return false;
+
+ var loaderTransform = loader.transform;
+ var extents = bounds.extents;
+ var maxExtent = Mathf.Max(extents.x, Mathf.Max(extents.y, extents.z));
+ if (_shouldScaleToFit && maxExtent > MinExtent)
+ loaderTransform.localScale = Vector3.one * (_size / maxExtent);
+
+ if (_shouldCenterItem)
+ {
+ var scaledCenter = Vector3.Scale(bounds.center, loaderTransform.localScale);
+ loaderTransform.localPosition = -(loaderTransform.localRotation * scaledCenter);
+ }
+
+ return true;
+ }
+
+ ///
+ /// Bounds of the item's renderers in the VobLoader's local space (grabbable child at its local origin).
+ /// Cached per VobLoader once meshes exist.
+ ///
+ private static bool TryGetLocalBounds(VobLoader loader, out Bounds bounds)
+ {
+ if (_boundsCache.TryGetValue(loader, out var cached))
+ {
+ bounds = cached.Value;
+ return true;
+ }
+
+ bounds = default;
+ var hasBounds = false;
+ var worldToLoader = loader.transform.worldToLocalMatrix;
+
+ loader.GetComponentsInChildren(true, _rendererBuffer);
+ foreach (var itemRenderer in _rendererBuffer)
+ {
+ if (itemRenderer is not (MeshRenderer or SkinnedMeshRenderer))
+ continue;
+
+ var toLoader = worldToLoader * itemRenderer.localToWorldMatrix;
+ var local = itemRenderer.localBounds;
+ for (var corner = 0; corner < 8; corner++)
+ {
+ var point = local.center + Vector3.Scale(local.extents, new Vector3(
+ (corner & 1) == 0 ? -1 : 1,
+ (corner & 2) == 0 ? -1 : 1,
+ (corner & 4) == 0 ? -1 : 1));
+ var loaderPoint = toLoader.MultiplyPoint3x4(point);
+
+ if (!hasBounds)
+ {
+ bounds = new Bounds(loaderPoint, Vector3.zero);
+ hasBounds = true;
+ }
+ else
+ {
+ bounds.Encapsulate(loaderPoint);
+ }
+ }
+ }
+
+ _rendererBuffer.Clear();
+
+ if (hasBounds)
+ {
+ _boundsCache.Remove(loader);
+ _boundsCache.Add(loader, new StrongBox(bounds));
+ }
+
+ return hasBounds;
+ }
+
+ private void PlaySound(AudioClip clip, bool silent)
+ {
+ if (silent || _isSilent || clip == null)
+ return;
+
+ if (_sfxPlayer != null)
+ _sfxPlayer.Play(clip, transform.position);
+ else
+ AudioSource.PlayClipAtPoint(clip, transform.position);
+ }
+ }
+}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs.meta
new file mode 100644
index 000000000..89d62b5fd
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 471f1450115357944ad35a0a2e0a45e0
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs
new file mode 100644
index 000000000..c7612a58e
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs
@@ -0,0 +1,37 @@
+using Gothic.VR.Interaction.Components;
+
+namespace Gothic.VR.Interaction.Ports
+{
+ ///
+ /// Implemented by the backend part of a VrSocketSlot (e.g. XriSocketBridge) on the same GameObject. It detects
+ /// interactables (hover, release into the slot) and holds them in the backend's terms. The Gothic part
+ /// (re-parenting the VobLoader, rotation, scale, sounds, events) stays in VrSocketSlot. The backend reports to
+ /// the slot via VrSocketSlot.OnBackend*().
+ /// Game code uses VrSocketSlot (IVrSocket) instead.
+ ///
+ public interface IVrSocketBackend
+ {
+ ///
+ /// Whether this bridge belongs to the active VR backend.
+ ///
+ bool IsActive { get; }
+
+ ///
+ /// Selects the interactable in the backend, ignoring the filters. The slot has already attached it and
+ /// raised its events. May finish later (e.g. while the interactable isn't registered at the backend yet).
+ ///
+ bool TryPlace(VrInteractable interactable);
+
+ ///
+ /// Releases the interactable (or drops a placement which hasn't finished yet). The backend reports the
+ /// release via VrSocketSlot.OnBackendRemoved, if it had selected the interactable.
+ ///
+ void ForceRelease();
+
+ ///
+ /// Off: the slot neither detects nor accepts interactables (e.g. backpack slots while the backpack is on the
+ /// shoulder). A held interactable stays.
+ ///
+ void SetDetectionEnabled(bool isEnabled);
+ }
+}
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs.meta b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs.meta
new file mode 100644
index 000000000..d547fe19c
--- /dev/null
+++ b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs.meta
@@ -0,0 +1,2 @@
+fileFormatVersion: 2
+guid: 5bef4231084655d4ca325ff66bde4c3d
\ No newline at end of file
diff --git a/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketContainer.cs b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketContainer.cs
index f390f511a..44b24a4b2 100644
--- a/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketContainer.cs
+++ b/Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketContainer.cs
@@ -16,8 +16,14 @@ public interface IVrSocketContainer
bool TryFindFree(VrInteractable interactable, out IVrSocket socket);
///
- /// Places the interactable into the first free socket which accepts it (with sound).
+ /// Places the interactable into the first free socket which accepts it.
///
- bool TryAdd(VrInteractable interactable);
+ /// If true, no "placed" sound is played.
+ bool TryPlace(VrInteractable interactable, bool silent = false);
+
+ ///
+ /// While silent, the sockets play no placed/removed sounds (e.g. while spawning the content of a chest).
+ ///
+ void SetSilent(bool isSilent);
}
}
diff --git a/Docs/HVR-replacement/README.md b/Docs/HVR-replacement/README.md
index ac6121bda..0946bf05a 100644
--- a/Docs/HVR-replacement/README.md
+++ b/Docs/HVR-replacement/README.md
@@ -22,6 +22,8 @@ Quest 2/3, Pico, PCVR, including Index / Steam Frame finger tracking.
| 5 | [phase-5-hands.md](phase-5-hands.md) - **on hold**; physics hands skipped, Gothic fist meshes later | L-XL |
| 6 | [phase-6-removal.md](phase-6-removal.md) - delete HVR, define, CI injection; update docs | S |
+Bugs found on the way (not caused by the replacement): [found-bugs.md](found-bugs.md).
+
## TL;DR
- HVR 2.9.3: ~43k LoC. We use about a third of it, but that third is the hard part: **physics hands, physics
diff --git a/Docs/HVR-replacement/found-bugs.md b/Docs/HVR-replacement/found-bugs.md
new file mode 100644
index 000000000..8411e43c7
--- /dev/null
+++ b/Docs/HVR-replacement/found-bugs.md
@@ -0,0 +1,52 @@
+# Bugs found during the HVR replacement
+
+Bugs that showed up while implementing and testing the phases. None of them is caused by the XRI backend; they
+exist in shared code (some on HVR as well). Kept here instead of tickets so they travel with the feature branch.
+Move them to GitHub issues when the branch is merged, if they are still open.
+
+## Inventory: picking up a world item adds 0 (`Amount = 0`)
+
+- **Seen:** Phase 2a, again in 3b. Both VR backends.
+- **What:** `VRPlayerService.SetGrab` adds `vobItem.Amount` items, and every world item checked has `Amount = 0`. With
+ `Amount = 1` set at runtime the counting works (+1 on pickup, two-hand grab counts once, drop removes it).
+- **Possible fix:** `Math.Max(1, Amount)` in `VRPlayerService`, or set `Amount` when the vob is created. Check that
+ stacks (arrows, gold, ore) still get their real amount.
+- **Test:** pick up a torch/beer at the G1 spawn and a stack of ore nuggets; open the backpack; drop them again.
+
+## Speech-to-text throws without a microphone
+
+- **Seen:** Phase 1d (simulator trigger started speech-to-text).
+- **What:** `IndexOutOfRangeException` in `SpeechToTextHandler.GetMicrophoneDeviceName` when no microphone exists.
+- **Expected:** one `Logger.LogWarning(..., LogCat.VR)` and speech-to-text disabled, no exception.
+
+## Eating a held item throws `MissingReferenceException`
+
+- **Seen:** Phase 2b.
+- **What:** `VobMeshCullingDomain.StopTrackVobPositionUpdatesDelayed` (around line 539) still references the vob
+ after it was destroyed by eating it while held. Not backend specific.
+- **Possible fix:** destroyed-check, or cancel the delayed call when the vob is destroyed.
+- **Test:** hold berries to the mouth; no exception, nothing keeps being tracked.
+
+## "Couldn't find object in Culling list" when grabbing runtime-spawned chest/loot items
+
+- **Seen:** Phase 3c.
+- **What:** grabbing an item that was spawned at runtime into a chest slot or an NPC loot slot logs
+ `Couldn't find object in Culling list ... (Loader)`, from `VRVobItem` -> `StartTrackVobPositionUpdates`. Shared
+ code of both backends, so HVR very likely has it too.
+- **Related, HVR only so far:** closing NPC loot once logged `Cannot set the parent of 'ITMW_1H_SWORD_04 (Loader)'
+ while it is being destroyed`.
+- **Maybe related:** the existing GitHub issue "Chest -> Backpack - throwing exception".
+- **Test:** take an item out of a chest; loot a dead NPC, take an item, close the panel; no errors.
+
+## Known, not new
+
+- Chests with more than 12 items: pagination is missing (an error is logged, as before).
+- Taking items from a chest doesn't change the chest's saved contents.
+- Chest slot spheres overlap on small chests (same as with HVR).
+
+
+## Misc
+1. Head never sticks into the wall as it is blocked from going into (poking back)
+1. Marvin menu is rotated wrong (fixed later, when we have a hand and actually see, where it should be placed).
+1. Raycaster for selecting objects is waaay to long. Should have same length as HVR - configurable via Config setting.
+1. Holding object in hand stutters. And if I move my hand too fast, it is stuck in the air
diff --git a/Docs/HVR-replacement/phase-3-sockets-inventory.md b/Docs/HVR-replacement/phase-3-sockets-inventory.md
index d4d6928be..2101ae85c 100644
--- a/Docs/HVR-replacement/phase-3-sockets-inventory.md
+++ b/Docs/HVR-replacement/phase-3-sockets-inventory.md
@@ -5,6 +5,10 @@
**Size:** L (2-3 weeks) · **Depends on:** Phases 0-2 · **Parallel with:** Phase 4
+Split into three PRs: **3a** socket core (slot, container, hover feedback, drop zone, XRI bridge, prefab and assets,
+PlayMode test), **3b** backpack + shoulder socket + hip holsters + HeadRelativeInventory on the XRI rig, **3c** chests
+(VrChestSlots, drop zone) + NPC loot.
+
## Exit criteria
- [ ] Backpack: grab from the shoulder, browse categories/pages, take items out, put items in; the inventory and the
@@ -81,17 +85,33 @@ as HVR's head-relative inventory). `VRBackpack.OnItemPutIntoHolster/OutOfHolster
## Work items
-- [ ] `VrSocketSlot`, `VrSocketContainer`, `VrSocketHoverFeedback`, `VrContainerDropZone` (backend-neutral).
-- [ ] `XriSocketBridge`, filter, attach override.
-- [ ] Move the logic of `VRSocket` / `VRShoulderSocket` into `VrSocketSlot`. The HVR versions stay in
- `Backends/Hvr` until Phase 6.
+- [x] `VrSocketSlot`, `VrSocketContainer`, `VrSocketHoverFeedback`, `VrContainerDropZone` (backend-neutral). (3a)
+ `VrSocketSlot` implements `IVrSocket` itself; its backend part is the new port `IVrSocketBackend` (same
+ GameObject, reports back via `VrSocketSlot.OnBackend*`). Programmatic `TryPlace` attaches and raises the events
+ synchronously (like HVR's `TryGrab`), the backend selection may follow a frame later (items enable their XRI
+ interactable in `Start`). Additionally to the spec: the item is centered on the attach point by its bounds,
+ the grabbable child is reset to its VobLoader's origin, the Rigidbody is kinematic inside and restored on
+ removal. Per-item socket sounds now live on `VrInteractable` (`SocketedSound`/`UnsocketedSound`), so
+ `XriInteractableBridge.SetSocketSounds` has nothing to do. `IVrSocketContainer.TryAdd` became
+ `TryPlace(interactable, silent = false)` + `SetSilent(bool)` (HVR bridge: nulls/restores fallback clips).
+- [x] `XriSocketBridge`, filter, attach override. (3a) Accepts only Gothic items (`VrSocketSlot.IsItem`: VobLoader
+ of an oCItem) and only while a hand holds them or within 0.5 s after a hand released them (items lying next
+ to a slot are never pulled in). Socket snapping, XRI's scale mode and hover meshes are off;
+ `GothicGrabInteractable` doesn't process (move) itself and doesn't unparent while only a socket selects it,
+ and a hand grab first releases the item from the socket (HVR order). Disabled in the prefab, enables itself
+ on the Xri backend only.
+- [x] Move the logic of `VRSocket` / `VRShoulderSocket` into `VrSocketSlot`. The HVR versions stay in
+ `Backends/Hvr` until Phase 6. (3a; `HvrSocket` now uses `VrSocketSlot.GetCategoryRotation`, same table.)
- [ ] `VRBackpack`: uses `IVrSocketContainer` (already after Phase 0), handles page refresh with
`TryPlace(…, raiseEvents: false)` instead of `_tempIgnoreSocketing` if possible.
- [ ] `VRVobContainerPhysicsChest`: split into `VrChestLid` (Phase 4) + `VrChestSlots` (this phase: resize slots to the
chest mesh, as today's `AlignSockets` logic, bulk spawn silently, pagination still TODO as logged today).
- [ ] `VRNpcLoot`: create `VrSocketSlot`s at runtime instead of `HVRSocket`s; `Removed` → `OnItemTakenFromLoot`.
- [ ] `VRVobContainerSocketInventory`: subscribe to container events.
-- [ ] **Assets (UnityMCP):** new `VR Socket Slot.prefab` (replaces `VRBackpack(Large)InventorySlot` + HVR's
+- [ ] **Assets (UnityMCP):** (3a done: `Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab` - slot,
+ XriSocketBridge, sphere trigger r=0.07, hover feedback, GameObjectSelfInjector; `Gothic-VR/Meshes/SocketRing.asset`
+ procedural torus; `Gothic-VR/Materials/SocketRing(-Valid/-Invalid).mat` URP Unlit transparent. Rest: 3b/3c)
+ new `VR Socket Slot.prefab` (replaces `VRBackpack(Large)InventorySlot` + HVR's
`LargeInventorySlot` / `SocketOval`), ring mesh (simple torus, our own) + `SocketRing/Valid/Invalid` URP materials,
add slots to `oCMobContainer.prefab` and the backpack prefab, shoulder + hip anchors on the XRI rig.
- [ ] Remove `GUZ-VR-Socketables.asset` usage on the Xri path (keep the file until Phase 6).
@@ -109,6 +129,10 @@ as HVR's head-relative inventory). `VRBackpack.OnItemPutIntoHolster/OutOfHolster
7. Kill a bandit, loot: equipped weapon shows `[E]`, taking it removes the weapon mesh from the corpse.
8. Hover an item over a full slot → invalid feedback; over a free slot → valid.
+PlayMode test (3a, no Gothic install needed): `Gothic-Tests/PlayMode/VrSocketSlotTest.cs` checks the VobLoader
+hierarchy, scale-to-fit, centering and kinematic state after place/remove without backend and on XRI (programmatic
+place, slot moved, ForceRelease, a hand interactor taking the item out).
+
## Risks
| Risk | Mitigation |