From 7fdc154560d1c33e38003bbbe9bf948b82758033 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Thu, 22 Oct 2026 21:22:41 +0200 Subject: [PATCH 1/3] feat: socket core for the Xri backend - backend-neutral VrSocketSlot (re-parents the item's VobLoader, rotates by item category, scales it to fit and centers it, kinematic while inside, restores everything on removal, socket sounds, placed/removed events), VrSocketContainer (TryFindFree/TryPlace/SetSilent), ring hover feedback (valid/invalid material, grows while hovered) and VrContainerDropZone; XriSocketBridge takes only items a hand holds or just released, without XRI moving the grabbable child; "VR Socket Slot.prefab" with own ring mesh and materials; PlayMode test for the VobLoader hierarchy (HVR phase 3a). Not used by the backpack, holsters, chests or loot yet (3b/3c). No player-visible change on the default Hvr backend. Co-Authored-By: Claude Opus 5.5 --- .../PlayMode/Gothic.Tests.PlayMode.asmdef | 2 + .../Gothic-Tests/PlayMode/VrSocketSlotTest.cs | 235 ++++++++ .../PlayMode/VrSocketSlotTest.cs.meta | 2 + .../Materials/SocketRing-Invalid.mat | 74 +++ .../Materials/SocketRing-Invalid.mat.meta | 8 + .../Gothic-VR/Materials/SocketRing-Valid.mat | 74 +++ .../Materials/SocketRing-Valid.mat.meta | 8 + Assets/Gothic-VR/Materials/SocketRing.mat | 74 +++ .../Gothic-VR/Materials/SocketRing.mat.meta | 8 + Assets/Gothic-VR/Meshes.meta | 8 + Assets/Gothic-VR/Meshes/SocketRing.asset | 180 ++++++ Assets/Gothic-VR/Meshes/SocketRing.asset.meta | 8 + .../Player-Elements/VR Socket Slot.prefab | 251 +++++++++ .../VR Socket Slot.prefab.meta | 7 + .../Scripts/Adapters/Player/VRBackpack.cs | 2 +- .../Interaction/Backends/Hvr/HvrSocket.cs | 38 +- .../Backends/Hvr/HvrSocketContainerBridge.cs | 36 +- .../Backends/Xri/GothicGrabInteractable.cs | 66 +++ .../Interaction/Backends/Xri/XriBackend.cs | 5 +- .../Backends/Xri/XriInteractableBridge.cs | 34 +- .../Backends/Xri/XriSocketBridge.cs | 216 +++++++ .../Backends/Xri/XriSocketBridge.cs.meta | 2 + .../Components/VrContainerDropZone.cs | 123 ++++ .../Components/VrContainerDropZone.cs.meta | 2 + .../Interaction/Components/VrInteractable.cs | 12 + .../Components/VrSocketContainer.cs | 111 ++++ .../Components/VrSocketContainer.cs.meta | 2 + .../Components/VrSocketHoverFeedback.cs | 100 ++++ .../Components/VrSocketHoverFeedback.cs.meta | 2 + .../Interaction/Components/VrSocketSlot.cs | 530 ++++++++++++++++++ .../Components/VrSocketSlot.cs.meta | 2 + .../Interaction/Ports/IVrSocketBackend.cs | 37 ++ .../Ports/IVrSocketBackend.cs.meta | 2 + .../Interaction/Ports/IVrSocketContainer.cs | 10 +- .../phase-3-sockets-inventory.md | 34 +- 35 files changed, 2257 insertions(+), 48 deletions(-) create mode 100644 Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs create mode 100644 Assets/Gothic-Tests/PlayMode/VrSocketSlotTest.cs.meta create mode 100644 Assets/Gothic-VR/Materials/SocketRing-Invalid.mat create mode 100644 Assets/Gothic-VR/Materials/SocketRing-Invalid.mat.meta create mode 100644 Assets/Gothic-VR/Materials/SocketRing-Valid.mat create mode 100644 Assets/Gothic-VR/Materials/SocketRing-Valid.mat.meta create mode 100644 Assets/Gothic-VR/Materials/SocketRing.mat create mode 100644 Assets/Gothic-VR/Materials/SocketRing.mat.meta create mode 100644 Assets/Gothic-VR/Meshes.meta create mode 100644 Assets/Gothic-VR/Meshes/SocketRing.asset create mode 100644 Assets/Gothic-VR/Meshes/SocketRing.asset.meta create mode 100644 Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab create mode 100644 Assets/Gothic-VR/Resources/VR/Prefabs/Player-Elements/VR Socket Slot.prefab.meta create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Backends/Xri/XriSocketBridge.cs.meta create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrContainerDropZone.cs.meta create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketContainer.cs.meta create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketHoverFeedback.cs.meta create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Components/VrSocketSlot.cs.meta create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs create mode 100644 Assets/Gothic-VR/Scripts/Interaction/Ports/IVrSocketBackend.cs.meta 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/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 | From d86bdc6743bb05bf3755e8be2e873fc200b12cb1 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 24 Oct 2026 06:33:56 +0200 Subject: [PATCH 2/3] docs: Bugs found during the HVR replacement are documented in found-bugs.md instead of tickets, linked from the README. Co-Authored-By: Claude Haiku 5.5 --- Docs/HVR-replacement/README.md | 2 ++ Docs/HVR-replacement/found-bugs.md | 49 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 Docs/HVR-replacement/found-bugs.md 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..66437cb3e --- /dev/null +++ b/Docs/HVR-replacement/found-bugs.md @@ -0,0 +1,49 @@ +# 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. + +## 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). + +## 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). From 97ea61f7e19d19355f5ae8ff5144d4cebcab0ed9 Mon Sep 17 00:00:00 2001 From: JaXt0r Date: Sat, 24 Oct 2026 06:37:01 +0200 Subject: [PATCH 3/3] docs: bugs. --- Docs/HVR-replacement/found-bugs.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Docs/HVR-replacement/found-bugs.md b/Docs/HVR-replacement/found-bugs.md index 66437cb3e..8411e43c7 100644 --- a/Docs/HVR-replacement/found-bugs.md +++ b/Docs/HVR-replacement/found-bugs.md @@ -38,12 +38,15 @@ Move them to GitHub issues when the branch is merged, if they are still open. - **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. -## 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). - ## 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