[
  {
    "path": ".gitattributes",
    "content": "*.a filter=lfs diff=lfs merge=lfs -text\n*.lib filter=lfs diff=lfs merge=lfs -text\n*.uasset filter=lfs diff=lfs merge=lfs -text\n*.png filter=lfs diff=lfs merge=lfs -text\n*.umap filter=lfs diff=lfs merge=lfs -text\n*.jpg filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "content": "name: 🐛 Bug Report\ndescription: Report a reproducible bug or regression.\ntitle: '[BUG] '\nbody:\n  - type: markdown\n    attributes:\n      value: Thank you for taking the time to report an issue!\n  - type: input\n    id: version\n    attributes:\n      label: Unreal Engine version\n      placeholder: 5.3.0\n    validations:\n      required: true\n  - type: dropdown\n    id: meta-fork-unreal\n    attributes:\n      label: Using the Meta fork of Unreal Engine?\n      options:\n        - Yes, using the Meta fork\n        - No, using the standard Epic build\n    validations:\n      required: true\n  - type: checkboxes\n    id: where\n    attributes:\n      label: Where does the issue occur?\n      options:\n        - label: In Unreal Editor\n          required: false\n        - label: In Quest builds\n          required: false\n  - type: textarea\n    id: description\n    attributes:\n      label: Description\n      description: A clear and concise description of what the bug is.\n    validations:\n      required: true\n  - type: textarea\n    id: reproduction\n    attributes:\n      label: Steps to reproduce\n      description: The list of steps that reproduce the issue.\n    validations:\n      required: true\n  - type: textarea\n    id: logs\n    attributes:\n      label: Logs\n      description: |\n        For in-editor bugs, paste the logs from the \"Output Log\" window in the Unreal Editor.\n        For on-device Quest bugs, paste the output of `adb logcat -s \"UE\"`\n      render: text\n    validations:\n      required: true\n  - type: textarea\n    id: extra\n    attributes:\n      label: Additional info\n      description: Please provide screenshots, a video, or any other relevant information.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Visual Studio 2015 user specific files\n.vs/\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Fortran module files\n*.mod\n\n# Executables\n*.exe\n*.out\n*.app\n*.ipa\n\n# These project files can be generated by the engine\n*.xcodeproj\n*.xcworkspace\n*.sln\n*.suo\n*.opensdf\n*.sdf\n*.VC.db\n*.VC.opendb\n\n# Precompiled Assets\nSourceArt/**/*.png\nSourceArt/**/*.tga\n\n# Binary Files\nBinaries/*\nPlugins/*/Binaries/*\n\n# Builds\nBuild/*\n\n# Whitelist PakBlacklist-<BuildConfiguration>.txt files\n!Build/*/\nBuild/*/**\n!Build/*/PakBlacklist*.txt\n\n# Don't ignore icon files in Build\n!Build/*/**/\n!Build/**/*.ico\n!Build/**/*.png\n\n# Built data for maps\n*_BuiltData.uasset\n\n# Configuration files generated by the Editor\nSaved/*\n\n# Compiled source files for the engine to use\nIntermediate/*\nPlugins/*/Intermediate/*\n\n# Cache files for the editor to use\nDerivedDataCache/*\n\nStoreAssets/\nGenerateSln.ps1\nConfig/DefaultEditorSettings.ini\n\nBuild/Android/res/drawable*/\nlfs/\nhooks/\n.vsconfig\n"
  },
  {
    "path": "CODE_OF_CONDUCT.md",
    "content": "# Code of Conduct\n\n## Our Pledge\n\nIn the interest of fostering an open and welcoming environment, we as\ncontributors and maintainers pledge to make participation in our project and\nour community a harassment-free experience for everyone, regardless of age, body\nsize, disability, ethnicity, sex characteristics, gender identity and expression,\nlevel of experience, education, socio-economic status, nationality, personal\nappearance, race, religion, or sexual identity and orientation.\n\n## Our Standards\n\nExamples of behavior that contributes to creating a positive environment\ninclude:\n\n* Using welcoming and inclusive language\n* Being respectful of differing viewpoints and experiences\n* Gracefully accepting constructive criticism\n* Focusing on what is best for the community\n* Showing empathy towards other community members\n\nExamples of unacceptable behavior by participants include:\n\n* The use of sexualized language or imagery and unwelcome sexual attention or\nadvances\n* Trolling, insulting/derogatory comments, and personal or political attacks\n* Public or private harassment\n* Publishing others' private information, such as a physical or electronic\naddress, without explicit permission\n* Other conduct which could reasonably be considered inappropriate in a\nprofessional setting\n\n## Our Responsibilities\n\nProject maintainers are responsible for clarifying the standards of acceptable\nbehavior and are expected to take appropriate and fair corrective action in\nresponse to any instances of unacceptable behavior.\n\nProject maintainers have the right and responsibility to remove, edit, or\nreject comments, commits, code, wiki edits, issues, and other contributions\nthat are not aligned to this Code of Conduct, or to ban temporarily or\npermanently any contributor for other behaviors that they deem inappropriate,\nthreatening, offensive, or harmful.\n\n## Scope\n\nThis Code of Conduct applies within all project spaces, and it also applies when\nan individual is representing the project or its community in public spaces.\nExamples of representing a project or community include using an official\nproject e-mail address, posting via an official social media account, or acting\nas an appointed representative at an online or offline event. Representation of\na project may be further defined and clarified by project maintainers.\n\nThis Code of Conduct also applies outside the project spaces when there is a\nreasonable belief that an individual's behavior may have a negative impact on\nthe project or its community.\n\n## Enforcement\n\nInstances of abusive, harassing, or otherwise unacceptable behavior may be\nreported by contacting the project team at <opensource-conduct@fb.com>. All\ncomplaints will be reviewed and investigated and will result in a response that\nis deemed necessary and appropriate to the circumstances. The project team is\nobligated to maintain confidentiality with regard to the reporter of an incident.\nFurther details of specific enforcement policies may be posted separately.\n\nProject maintainers who do not follow or enforce the Code of Conduct in good\nfaith may face temporary or permanent repercussions as determined by other\nmembers of the project's leadership.\n\n## Attribution\n\nThis Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,\navailable at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html\n\n[homepage]: https://www.contributor-covenant.org\n\nFor answers to common questions about this code of conduct, see\nhttps://www.contributor-covenant.org/faq\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "# Contributing\nWe want to make contributing to this project as easy and transparent as possible.\n\n## Pull Requests\nWe actively welcome your pull requests.\n\n1. Fork the repo and create your branch from `main`.\n2. If you've added code that should be tested, add tests.\n3. If you've changed APIs, update the documentation.\n4. Ensure the test suite passes.\n5. Make sure your code lints.\n6. If you haven't already, complete the Contributor License Agreement (\"CLA\").\n\n## Contributor License Agreement (\"CLA\")\nIn order to accept your pull request, we need you to submit a CLA. You only need to do this once to work on any of Facebook's open source projects.\n\nComplete your CLA here: <https://code.facebook.com/cla>\n\n## Issues\nWe use GitHub issues to track public bugs. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue.\n\nFacebook has a [bounty program](https://www.facebook.com/whitehat/) for the safe disclosure of security bugs. In those cases, please go through the process outlined on that page and do not file a public issue.\n\n## License\nBy contributing, you agree that your contributions will be licensed under the LICENSE file in the root directory of this source tree.\n"
  },
  {
    "path": "Config/Android/AndroidEngine.ini",
    "content": "[DevOptions.Shaders]\nbNeedsShaderStableKeys=True\n\n"
  },
  {
    "path": "Config/DefaultDeviceProfiles.ini",
    "content": "[Oculus_Quest2 DeviceProfile]\nDeviceType=Android\nBaseProfileName=Oculus_Quest\nbIsVisibleForAssets=False\n-CVars=r.Mobile.Oculus.ForceSymmetric=1\n-CVars=fx.NiagaraAllowGPUParticles=1\n-CVars=FX.AllowGPUSorting=1\n-CVars=r.Mobile.AdrenoOcclusionMode=1\n-CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8f\n-CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2f\n-CVars=r.so.VisualizeBufferXOffset=500\n-CVars=r.so.VisualizeBufferYOffset=500\n+CVars=r.Mobile.Oculus.ForceSymmetric=1\n+CVars=fx.NiagaraAllowGPUParticles=1\n+CVars=FX.AllowGPUSorting=1\n+CVars=r.Mobile.AdrenoOcclusionMode=1\n+CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8\n+CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2\n+CVars=r.so.VisualizeBufferXOffset=500\n+CVars=r.so.VisualizeBufferYOffset=500\n\n[Meta_Quest_Pro DeviceProfile]\nDeviceType=Android\nBaseProfileName=Oculus_Quest2\nbIsVisibleForAssets=False\n+CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8\n+CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2\n\n[Meta_Quest_3 DeviceProfile]\nDeviceType=Android\nBaseProfileName=Meta_Quest_Pro\nbIsVisibleForAssets=False\n-CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.7f\n-CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.6f\n+CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8\n+CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2\n\n[Meta_Quest_3S DeviceProfile]\nDeviceType=Android\nBaseProfileName=Meta_Quest_3\nbIsVisibleForAssets=False\n+CVars=r.Oculus.DynamicResolution.PixelDensityMin=0.8\n+CVars=r.Oculus.DynamicResolution.PixelDensityMax=1.2\n\n"
  },
  {
    "path": "Config/DefaultEditor.ini",
    "content": ""
  },
  {
    "path": "Config/DefaultEngine.ini",
    "content": "[/Script/HardwareTargeting.HardwareTargetingSettings]\nTargetedHardwareClass=Mobile\nAppliedTargetedHardwareClass=Mobile\nDefaultGraphicsPerformance=Scalable\nAppliedDefaultGraphicsPerformance=Scalable\n\n[/Script/Engine.Engine]\n+ActiveGameNameRedirects=(OldGameName=\"TP_Blank\",NewGameName=\"/Script/SHOHandRecognition\")\n+ActiveGameNameRedirects=(OldGameName=\"/Script/TP_Blank\",NewGameName=\"/Script/SHOHandRecognition\")\n+ActiveGameNameRedirects=(OldGameName=\"/Script/HandPoseShowcase\", NewGameName=\"/Script/HandGameplay\")\n+ActiveClassRedirects=(OldClassName=\"TP_BlankGameModeBase\",NewClassName=\"SHOHandRecognitionGameModeBase\")\n\n[/Script/Engine.RendererSettings]\nr.Mobile.DisableVertexFog=True\nr.Shadow.CSM.MaxMobileCascades=2\nr.MobileMSAA=4\nr.Mobile.UseLegacyShadingModel=False\nr.Mobile.AllowDitheredLODTransition=False\nr.Mobile.AllowSoftwareOcclusion=False\nr.Mobile.VirtualTextures=False\nr.DiscardUnusedQuality=False\nr.AllowOcclusionQueries=True\nr.MinScreenRadiusForLights=0.030000\nr.MinScreenRadiusForDepthPrepass=0.030000\nr.MinScreenRadiusForCSMDepth=0.010000\nr.PrecomputedVisibilityWarning=False\nr.TextureStreaming=True\nCompat.UseDXT5NormalMaps=False\nr.VirtualTextures=False\nr.VirtualTexturedLightmaps=False\nr.VRS.Enable=True\nr.VRS.EnableImage=False\nr.VT.TileSize=128\nr.VT.TileBorderSize=4\nr.vt.FeedbackFactor=16\nr.VT.EnableCompressZlib=True\nr.VT.EnableCompressCrunch=False\nr.ClearCoatNormal=False\nr.AnisotropicBRDF=False\nr.ReflectionCaptureResolution=128\nr.ReflectionEnvironmentLightmapMixBasedOnRoughness=True\nr.ForwardShading=True\nr.VertexFoggingForOpaque=True\nr.AllowStaticLighting=True\nr.NormalMapsForStaticLighting=False\nr.GenerateMeshDistanceFields=False\nr.DistanceFieldBuild.EightBit=False\nr.GenerateLandscapeGIData=False\nr.DistanceFieldBuild.Compress=False\nr.TessellationAdaptivePixelsPerTriangle=48.000000\nr.SeparateTranslucency=False\nr.TranslucentSortPolicy=0\nTranslucentSortAxis=(X=0.000000,Y=-1.000000,Z=0.000000)\nr.CustomDepth=0\nr.CustomDepthTemporalAAJitter=False\nr.PostProcessing.PropagateAlpha=0\nr.DefaultFeature.Bloom=False\nr.DefaultFeature.AmbientOcclusion=False\nr.DefaultFeature.AmbientOcclusionStaticFraction=True\nr.DefaultFeature.AutoExposure=False\nr.DefaultFeature.AutoExposure.Method=0\nr.DefaultFeature.AutoExposure.Bias=1.000000\nr.DefaultFeature.AutoExposure.ExtendDefaultLuminanceRange=False\nr.UsePreExposure=True\nr.EyeAdaptation.EditorOnly=False\nr.DefaultFeature.MotionBlur=False\nr.DefaultFeature.LensFlare=False\nr.TemporalAA.Upsampling=False\nr.SSGI.Enable=False\nr.DefaultFeature.AntiAliasing=3\nr.DefaultFeature.LightUnits=1\nr.DefaultBackBufferPixelFormat=4\nr.Shadow.UnbuiltPreviewInGame=True\nr.StencilForLODDither=False\nr.EarlyZPass=0\nr.EarlyZPassOnlyMaterialMasking=False\nr.DBuffer=True\nr.ClearSceneMethod=1\nr.BasePassOutputsVelocity=False\nr.VertexDeformationOutputsVelocity=False\nr.SelectiveBasePassOutputs=False\nbDefaultParticleCutouts=False\nfx.GPUSimulationTextureSizeX=1024\nfx.GPUSimulationTextureSizeY=1024\nr.AllowGlobalClipPlane=False\nr.GBufferFormat=1\nr.MorphTarget.Mode=True\nr.GPUCrashDebugging=False\nvr.InstancedStereo=True\nr.MobileHDR=False\nvr.MobileMultiView=True\nr.Mobile.UseHWsRGBEncoding=True\nvr.RoundRobinOcclusion=False\nvr.ODSCapture=False\nr.MeshStreaming=False\nr.WireframeCullThreshold=5.000000\nr.RayTracing=False\nr.RayTracing.UseTextureLod=False\nr.SupportStationarySkylight=True\nr.SupportLowQualityLightmaps=True\nr.SupportPointLightWholeSceneShadows=True\nr.SupportAtmosphericFog=True\nr.SupportSkyAtmosphere=True\nr.SupportSkyAtmosphereAffectsHeightFog=False\nr.SkinCache.CompileShaders=False\nr.SkinCache.DefaultBehavior=1\nr.SkinCache.SceneMemoryLimitInMB=128.000000\nr.Mobile.EnableStaticAndCSMShadowReceivers=False\nr.Mobile.EnableMovableLightCSMShaderCulling=True\nr.Mobile.AllowDistanceFieldShadows=False\nr.Mobile.AllowMovableDirectionalLights=False\nr.MobileNumDynamicPointLights=0\nr.MobileDynamicPointLightsUseStaticBranch=True\nr.Mobile.EnableMovableSpotlights=False\nr.GPUSkin.Support16BitBoneIndex=False\nr.GPUSkin.Limit2BoneInfluences=False\nr.SupportDepthOnlyIndexBuffers=True\nr.SupportReversedIndexBuffers=True\nr.SupportMaterialLayers=False\nr.LightPropagationVolume=False\nr.Mobile.AmbientOcclusion=False\nr.Mobile.AntiAliasing=3\nr.MSAACount=4\nr.AntiAliasingMethod=3\n\n[/Script/EngineSettings.GameMapsSettings]\nEditorStartupMap=/Game/HandGameplay/Levels/HandGameplayShowcase.HandGameplayShowcase\nLocalMapOptions=\nTransitionMap=None\nbUseSplitscreen=False\nTwoPlayerSplitscreenLayout=Horizontal\nThreePlayerSplitscreenLayout=FavorTop\nFourPlayerSplitscreenLayout=Grid\nbOffsetPlayerGamepadIds=False\nGameInstanceClass=/Script/Engine.GameInstance\nGameDefaultMap=/Game/HandGameplay/Levels/HandGameplayShowcase.HandGameplayShowcase\nServerDefaultMap=/Engine/Maps/Entry.Entry\nGlobalDefaultGameMode=/Game/HandGameplay/Blueprints/HandsGameMode.HandsGameMode_C\nGlobalDefaultServerGameMode=None\n\n[/Script/Slate.SlateSettings]\nbExplicitCanvasChildZOrder=True\n\n[/Script/AndroidRuntimeSettings.AndroidRuntimeSettings]\nPackageName=com.samples.[PROJECT]\nStoreVersion=1\nStoreVersionOffsetArm64=0\nStoreVersionOffsetX8664=0\nApplicationDisplayName=\nVersionDisplayName=1.0\nMinSDKVersion=32\nTargetSDKVersion=32\nInstallLocation=Auto\nbEnableLint=False\nbPackageDataInsideApk=True\nbCreateAllPlatformsInstall=False\nbDisableVerifyOBBOnStartUp=False\nbForceSmallOBBFiles=False\nbAllowLargeOBBFiles=False\nbAllowPatchOBBFile=False\nbAllowOverflowOBBFiles=False\nbUseExternalFilesDir=False\nbPublicLogFiles=True\nOrientation=Landscape\nMaxAspectRatio=2.100000\nbUseDisplayCutout=False\nbAllowResizing=False\nbSupportSizeChanges=False\nbRestoreNotificationsOnReboot=False\nbFullScreen=True\nbEnableNewKeyboard=True\nDepthBufferPreference=Default\nbValidateTextureFormats=True\nbForceCompressNativeLibs=False\nbEnableAdvancedBinaryCompression=True\nbEnableBundle=False\nbEnableUniversalAPK=False\nbBundleABISplit=True\nbBundleLanguageSplit=True\nbBundleDensitySplit=True\n+ExtraApplicationNodeTags=android:allowBackup=\"false\"\nExtraApplicationSettings=<meta-data android:name=\"com.oculus.supportedDevices\" android:value=\"quest|quest2|questpro|quest3\" />\nExtraActivitySettings=\nbAndroidVoiceEnabled=False\nbEnableMulticastSupport=False\nbPackageForMetaQuest=True\nbRemoveOSIG=True\nKeyStore=\nKeyAlias=\nKeyStorePassword=\nKeyPassword=\nbBuildForArm64=True\nbBuildForX8664=False\nbBuildForES31=False\nbSupportsVulkan=True\nbSupportsVulkanSM5=False\nDebugVulkanLayerDirectory=(Path=\"\")\nbAndroidOpenGLSupportsBackbufferSampling=False\nbDetectVulkanByDefault=True\nbBuildWithHiddenSymbolVisibility=False\nbDisableStackProtector=False\nbDisableLibCppSharedDependencyValidation=False\nbSaveSymbols=False\nbStripShaderReflection=True\nbStripReflectOfAndroidShader=False\nbEnableGooglePlaySupport=False\nbUseGetAccounts=False\nGamesAppID=\nbEnableSnapshots=False\nbSupportAdMob=True\nAdMobAppID=\nTagForChildDirectedTreatment=TAG_FOR_CHILD_DIRECTED_TREATMENT_UNSPECIFIED\nTagForUnderAgeOfConsent=TAG_FOR_UNDER_AGE_OF_CONSENT_UNSPECIFIED\nMaxAdContentRating=MAX_AD_CONTENT_RATING_G\nAdMobAdUnitID=\nGooglePlayLicenseKey=\nGCMClientSenderID=\nbShowLaunchImage=True\nbAllowIMU=True\nbAllowControllers=True\nbBlockAndroidKeysOnControllers=False\nbControllersBlockDeviceFeedback=False\nAndroidAudio=Default\nAudioSampleRate=44100\nAudioCallbackBufferFrameSize=1024\nAudioNumBuffersToEnqueue=4\nAudioMaxChannels=0\nAudioNumSourceWorkers=0\nSpatializationPlugin=Oculus Audio\nSourceDataOverridePlugin=\nReverbPlugin=Oculus Audio\nOcclusionPlugin=\nCompressionOverrides=(bOverrideCompressionTimes=False,DurationThreshold=5.000000,MaxNumRandomBranches=0,SoundCueQualityIndex=0)\nCacheSizeKB=0\nMaxChunkSizeOverrideKB=0\nbResampleForDevice=False\nSoundCueCookQualityIndex=-1\nMaxSampleRate=0.000000\nHighSampleRate=0.000000\nMedSampleRate=0.000000\nLowSampleRate=0.000000\nMinSampleRate=0.000000\nCompressionQualityModifier=0.000000\nAutoStreamingThreshold=0.000000\nAndroidGraphicsDebugger=None\nMaliGraphicsDebuggerPath=(Path=\"\")\nbEnableMaliPerfCounters=False\nbMultiTargetFormat_ETC2=False\nbMultiTargetFormat_DXT=False\nbMultiTargetFormat_ASTC=True\nTextureFormatPriority_ETC2=0.200000\nTextureFormatPriority_DXT=0.600000\nTextureFormatPriority_ASTC=0.900000\nSDKAPILevelOverride=\nNDKAPILevelOverride=\nBuildToolsOverride=\nbStreamLandscapeMeshLODs=False\nbEnableDomStorage=False\n\n[/Script/OculusHMD.OculusHMDRuntimeSettings]\nHandTrackingSupport=HandsOnly\nFFRLevel=FFR_Medium\nFFRDynamic=True\nXrApi=LegacyOVRPlugin\nHandTrackingFrequency=HIGH\nbLateLatching=True\nbPhaseSync=True\nbAutoEnabled=True\nbEnableSpecificColorGamut=False\nColorSpace=Quest\nbSupportsDash=True\nbCompositesDepth=True\nbHQDistortion=False\nPixelDensityMin=0.500000\nPixelDensityMax=1.000000\nOSSplashScreen=(FilePath=\"Plugins/OculusUtils/Resources/oculus_samples.png\")\nCPULevel=2\nGPULevel=3\nbFocusAware=True\nbRequiresSystemKeyboard=False\n\n[/Script/Engine.CollisionProfile]\n-Profiles=(Name=\"NoCollision\",CollisionEnabled=NoCollision,ObjectTypeName=\"WorldStatic\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore),(Channel=\"Camera\",Response=ECR_Ignore)),HelpMessage=\"No collision\",bCanModify=False)\n-Profiles=(Name=\"BlockAll\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"WorldStatic\",CustomResponses=,HelpMessage=\"WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. \",bCanModify=False)\n-Profiles=(Name=\"OverlapAll\",CollisionEnabled=QueryOnly,ObjectTypeName=\"WorldStatic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Visibility\",Response=ECR_Overlap),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. \",bCanModify=False)\n-Profiles=(Name=\"BlockAllDynamic\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"WorldDynamic\",CustomResponses=,HelpMessage=\"WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. \",bCanModify=False)\n-Profiles=(Name=\"OverlapAllDynamic\",CollisionEnabled=QueryOnly,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Visibility\",Response=ECR_Overlap),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. \",bCanModify=False)\n-Profiles=(Name=\"IgnoreOnlyPawn\",CollisionEnabled=QueryOnly,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Vehicle\",Response=ECR_Ignore)),HelpMessage=\"WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.\",bCanModify=False)\n-Profiles=(Name=\"OverlapOnlyPawn\",CollisionEnabled=QueryOnly,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Ignore)),HelpMessage=\"WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. \",bCanModify=False)\n-Profiles=(Name=\"Pawn\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"Pawn\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"Pawn object. Can be used for capsule of any playerable character or AI. \",bCanModify=False)\n-Profiles=(Name=\"Spectator\",CollisionEnabled=QueryOnly,ObjectTypeName=\"Pawn\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Block),(Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Visibility\",Response=ECR_Ignore),(Channel=\"WorldDynamic\",Response=ECR_Ignore),(Channel=\"Camera\",Response=ECR_Ignore),(Channel=\"PhysicsBody\",Response=ECR_Ignore),(Channel=\"Vehicle\",Response=ECR_Ignore),(Channel=\"Destructible\",Response=ECR_Ignore)),HelpMessage=\"Pawn object that ignores all other actors except WorldStatic.\",bCanModify=False)\n-Profiles=(Name=\"CharacterMesh\",CollisionEnabled=QueryOnly,ObjectTypeName=\"Pawn\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Vehicle\",Response=ECR_Ignore),(Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"Pawn object that is used for Character Mesh. All other channels will be set to default.\",bCanModify=False)\n-Profiles=(Name=\"PhysicsActor\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"PhysicsBody\",CustomResponses=,HelpMessage=\"Simulating actors\",bCanModify=False)\n-Profiles=(Name=\"Destructible\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"Destructible\",CustomResponses=,HelpMessage=\"Destructible actors\",bCanModify=False)\n-Profiles=(Name=\"InvisibleWall\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"WorldStatic\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"WorldStatic object that is invisible.\",bCanModify=False)\n-Profiles=(Name=\"InvisibleWallDynamic\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"WorldDynamic object that is invisible.\",bCanModify=False)\n-Profiles=(Name=\"Trigger\",CollisionEnabled=QueryOnly,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Visibility\",Response=ECR_Ignore),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldDynamic object that is used for trigger. All other channels will be set to default.\",bCanModify=False)\n-Profiles=(Name=\"Ragdoll\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"PhysicsBody\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"Simulating Skeletal Mesh Component. All other channels will be set to default.\",bCanModify=False)\n-Profiles=(Name=\"Vehicle\",CollisionEnabled=QueryAndPhysics,ObjectTypeName=\"Vehicle\",CustomResponses=,HelpMessage=\"Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.\",bCanModify=False)\n-Profiles=(Name=\"UI\",CollisionEnabled=QueryOnly,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Visibility\",Response=ECR_Block),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. \",bCanModify=False)\n+Profiles=(Name=\"NoCollision\",CollisionEnabled=NoCollision,bCanModify=False,ObjectTypeName=\"WorldStatic\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore),(Channel=\"Camera\",Response=ECR_Ignore)),HelpMessage=\"No collision\")\n+Profiles=(Name=\"BlockAll\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"WorldStatic\",CustomResponses=,HelpMessage=\"WorldStatic object that blocks all actors by default. All new custom channels will use its own default response. \")\n+Profiles=(Name=\"OverlapAll\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"WorldStatic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Visibility\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. \")\n+Profiles=(Name=\"BlockAllDynamic\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"WorldDynamic\",CustomResponses=,HelpMessage=\"WorldDynamic object that blocks all actors by default. All new custom channels will use its own default response. \")\n+Profiles=(Name=\"OverlapAllDynamic\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Visibility\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldDynamic object that overlaps all actors by default. All new custom channels will use its own default response. \")\n+Profiles=(Name=\"IgnoreOnlyPawn\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Vehicle\",Response=ECR_Ignore)),HelpMessage=\"WorldDynamic object that ignores Pawn and Vehicle. All other channels will be set to default.\")\n+Profiles=(Name=\"OverlapOnlyPawn\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Ignore),(Channel=\"Vehicle\",Response=ECR_Overlap)),HelpMessage=\"WorldDynamic object that overlaps Pawn, Camera, and Vehicle. All other channels will be set to default. \")\n+Profiles=(Name=\"Pawn\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"Pawn\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"Pawn object. Can be used for capsule of any playerable character or AI. \")\n+Profiles=(Name=\"Spectator\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"Pawn\",CustomResponses=((Channel=\"WorldDynamic\",Response=ECR_Ignore),(Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Visibility\",Response=ECR_Ignore),(Channel=\"Camera\",Response=ECR_Ignore),(Channel=\"PhysicsBody\",Response=ECR_Ignore),(Channel=\"Vehicle\",Response=ECR_Ignore),(Channel=\"Destructible\",Response=ECR_Ignore)),HelpMessage=\"Pawn object that ignores all other actors except WorldStatic.\")\n+Profiles=(Name=\"CharacterMesh\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"Pawn\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Visibility\",Response=ECR_Ignore),(Channel=\"Vehicle\",Response=ECR_Ignore)),HelpMessage=\"Pawn object that is used for Character Mesh. All other channels will be set to default.\")\n+Profiles=(Name=\"PhysicsActor\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"PhysicsBody\",CustomResponses=,HelpMessage=\"Simulating actors\")\n+Profiles=(Name=\"Destructible\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"Destructible\",CustomResponses=,HelpMessage=\"Destructible actors\")\n+Profiles=(Name=\"InvisibleWall\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"WorldStatic\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"WorldStatic object that is invisible.\")\n+Profiles=(Name=\"InvisibleWallDynamic\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"WorldDynamic object that is invisible.\")\n+Profiles=(Name=\"Trigger\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Visibility\",Response=ECR_Ignore),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldDynamic object that is used for trigger. All other channels will be set to default.\")\n+Profiles=(Name=\"Ragdoll\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"PhysicsBody\",CustomResponses=((Channel=\"Pawn\",Response=ECR_Ignore),(Channel=\"Visibility\",Response=ECR_Ignore)),HelpMessage=\"Simulating Skeletal Mesh Component. All other channels will be set to default.\")\n+Profiles=(Name=\"Vehicle\",CollisionEnabled=QueryAndPhysics,bCanModify=False,ObjectTypeName=\"Vehicle\",CustomResponses=,HelpMessage=\"Vehicle object that blocks Vehicle, WorldStatic, and WorldDynamic. All other channels will be set to default.\")\n+Profiles=(Name=\"UI\",CollisionEnabled=QueryOnly,bCanModify=False,ObjectTypeName=\"WorldDynamic\",CustomResponses=((Channel=\"WorldStatic\",Response=ECR_Overlap),(Channel=\"WorldDynamic\",Response=ECR_Overlap),(Channel=\"Pawn\",Response=ECR_Overlap),(Channel=\"Camera\",Response=ECR_Overlap),(Channel=\"PhysicsBody\",Response=ECR_Overlap),(Channel=\"Vehicle\",Response=ECR_Overlap),(Channel=\"Destructible\",Response=ECR_Overlap)),HelpMessage=\"WorldStatic object that overlaps all actors by default. All new custom channels will use its own default response. \")\n+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Ignore,bTraceType=True,bStaticObject=False,Name=\"Interactable\")\n+DefaultChannelResponses=(Channel=ECC_GameTraceChannel2,DefaultResponse=ECR_Overlap,bTraceType=False,bStaticObject=False,Name=\"FingerTip\")\n-ProfileRedirects=(OldName=\"BlockingVolume\",NewName=\"InvisibleWall\")\n-ProfileRedirects=(OldName=\"InterpActor\",NewName=\"IgnoreOnlyPawn\")\n-ProfileRedirects=(OldName=\"StaticMeshComponent\",NewName=\"BlockAllDynamic\")\n-ProfileRedirects=(OldName=\"SkeletalMeshActor\",NewName=\"PhysicsActor\")\n-ProfileRedirects=(OldName=\"InvisibleActor\",NewName=\"InvisibleWallDynamic\")\n+ProfileRedirects=(OldName=\"BlockingVolume\",NewName=\"InvisibleWall\")\n+ProfileRedirects=(OldName=\"InterpActor\",NewName=\"IgnoreOnlyPawn\")\n+ProfileRedirects=(OldName=\"StaticMeshComponent\",NewName=\"BlockAllDynamic\")\n+ProfileRedirects=(OldName=\"SkeletalMeshActor\",NewName=\"PhysicsActor\")\n+ProfileRedirects=(OldName=\"InvisibleActor\",NewName=\"InvisibleWallDynamic\")\n-CollisionChannelRedirects=(OldName=\"Static\",NewName=\"WorldStatic\")\n-CollisionChannelRedirects=(OldName=\"Dynamic\",NewName=\"WorldDynamic\")\n-CollisionChannelRedirects=(OldName=\"VehicleMovement\",NewName=\"Vehicle\")\n-CollisionChannelRedirects=(OldName=\"PawnMovement\",NewName=\"Pawn\")\n+CollisionChannelRedirects=(OldName=\"Static\",NewName=\"WorldStatic\")\n+CollisionChannelRedirects=(OldName=\"Dynamic\",NewName=\"WorldDynamic\")\n+CollisionChannelRedirects=(OldName=\"VehicleMovement\",NewName=\"Vehicle\")\n+CollisionChannelRedirects=(OldName=\"PawnMovement\",NewName=\"Pawn\")\n+CollisionChannelRedirects=(OldName=\"Hand\",NewName=\"FingerTip\")\n\n[/Script/Engine.UserInterfaceSettings]\nUIScaleRule=ShortestSide\nUIScaleCurve=(EditorCurveData=(Keys=((Time=0.256264,Value=1.000000)),DefaultValue=340282346638528859811704183484516925440.000000,PreInfinityExtrap=RCCE_Constant,PostInfinityExtrap=RCCE_Constant),ExternalCurve=None)\n\n[/Script/WindowsTargetPlatform.WindowsTargetSettings]\nSpatializationPlugin=Oculus Audio\nReverbPlugin=Oculus Audio\n\n[/Script/OculusAudio.OculusAudioSettings]\nLateReverberation=True\n\n[/Script/LuminRuntimeSettings.LuminRuntimeSettings]\nIconModelPath=(Path=\"\")\nIconPortalPath=(Path=\"\")\n\n[Core.Log]\nLogHandPoseRecognition=warning\n\n[/Script/AndroidFileServerEditor.AndroidFileServerRuntimeSettings]\nbEnablePlugin=True\nbAllowNetworkConnection=True\nSecurityToken=8FCC7359434D4251D8FC3DAF8446EBE2\nbIncludeInShipping=False\nbAllowExternalStartInShipping=False\nbCompileAFSProject=False\nbUseCompression=False\nbLogFiles=False\nbReportStats=False\nConnectionType=USBOnly\nbUseManualIPAddress=False\nManualIPAddress=\n\n[/Script/OculusXRHMD.OculusXRHMDRuntimeSettings]\nSystemSplashBackground=Black\nbAutoEnabled=False\nbHorizonOSVersionOverride=False\nMinOSVersion=(Version=0,bLatest=True)\nTargetOSVersion=(Version=0,bLatest=True)\nXrApi=NativeOpenXR\nColorSpace=P3\nControllerPoseAlignment=Default\nbThumbstickDpadEmulationEnabled=True\nOculusXRSimulatorPreferredVersion=81\nbNotifyWhenNewVersionIsAvailable=True\nbSupportsDash=True\nbCompositesDepth=True\nbHQDistortion=False\nbSetActivePIEToPrimary=True\nbSetCVarPIEToPrimary=True\nbUpdateHeadPoseForInactivePlayer=False\nMPPoseRestoreType=Disabled\nbDynamicResolution=False\n+SupportedDevices=Quest3\n+SupportedDevices=QuestPro\n+SupportedDevices=Quest2\n+SupportedDevices=Quest3S\nSuggestedCpuPerfLevel=SustainedLow\nSuggestedGpuPerfLevel=SustainedHigh\nFoveatedRenderingMethod=FixedFoveatedRendering\nFoveatedRenderingLevel=Off\nbDynamicFoveatedRendering=False\nbSupportEyeTrackedFoveatedRendering=False\nbCompositeDepthMobile=False\nbFocusAware=True\nbLateLatching=False\nbRequiresSystemKeyboard=False\nHandTrackingSupport=HandsOnly\nHandTrackingFrequency=LOW\nHandTrackingVersion=Default\nbInsightPassthroughEnabled=False\nbAnchorSupportEnabled=False\nbAnchorSharingEnabled=False\nbSceneSupportEnabled=False\nbBoundaryVisibilitySupportEnabled=False\nbDefaultBoundaryVisibilitySuppressed=False\nbColocationSessionsEnabled=False\nbBodyTrackingEnabled=False\nBodyTrackingFidelity=Low\nBodyTrackingJointSet=UpperBody\nbEyeTrackingEnabled=False\nbFaceTrackingEnabled=False\nFaceTrackingDataSource=()\nbFaceTrackingVisemesEnabled=False\nbDeploySoToDevice=False\nbIterativeCookOnTheFly=False\nbSupportExperimentalFeatures=False\nProcessorFavor=FavorEqually\nbTileTurnOffEnabled=False\nbSupportSBC=False\nSBCPath=files/UnrealGame/HandGameplay/HandGameplay/Saved/VulkanCache\nEnableWorldLock=True\n\n"
  },
  {
    "path": "Config/DefaultGame.ini",
    "content": "\n\n[/Script/EngineSettings.GeneralProjectSettings]\nProjectID=C55AF2354CBEC3833CFBFB923FDA1579\nCopyrightNotice=Copyright (c) Meta Platforms, Inc. and affiliates.\nbStartInVR=True\n\n[/Script/UnrealEd.ProjectPackagingSettings]\nStagingDirectory=(Path=\"../../../../../../../../QuestPackagingArea/SHO_HandGameplay/Android_ASTC\")\n+DirectoriesToNeverCook=(Path=\"/Game/Developers/jasonmeisel\")\n"
  },
  {
    "path": "Config/DefaultInput.ini",
    "content": "[/Script/Engine.InputSettings]\n-AxisConfig=(AxisKeyName=\"Gamepad_LeftX\",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f))\n-AxisConfig=(AxisKeyName=\"Gamepad_LeftY\",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f))\n-AxisConfig=(AxisKeyName=\"Gamepad_RightX\",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f))\n-AxisConfig=(AxisKeyName=\"Gamepad_RightY\",AxisProperties=(DeadZone=0.25,Exponent=1.f,Sensitivity=1.f))\n-AxisConfig=(AxisKeyName=\"MouseX\",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f))\n-AxisConfig=(AxisKeyName=\"MouseY\",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f))\n-AxisConfig=(AxisKeyName=\"Mouse2D\",AxisProperties=(DeadZone=0.f,Exponent=1.f,Sensitivity=0.07f))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Left_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Left_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Left_Trackpad_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Left_Touch1_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Left_Touch1_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Left_Touch1_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Mouse2D\",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MotionController_Right_Thumbstick_Z\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Right_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Right_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Right_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Right_Trackpad_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Right_Touch1_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Right_Touch1_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Right_Touch1_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_Thumbstick\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_FaceButton1\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_Trigger\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_FaceButton2\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_IndexPointing\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_ThumbUp\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_Thumbstick\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_FaceButton1\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_Trigger\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_FaceButton2\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_IndexPointing\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_ThumbUp\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Left_ThumbPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Left_IndexPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Left_MiddlePinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Left_RingPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Left_PinkPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Right_ThumbPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Right_IndexPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Right_MiddlePinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Right_RingPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusHand_Right_PinkPinchStrength\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Thumbstick_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Thumbstick_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Trackpad_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_LeftX\",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_LeftY\",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_RightX\",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_RightY\",AxisProperties=(DeadZone=0.250000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MouseX\",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MouseY\",AxisProperties=(DeadZone=0.000000,Sensitivity=0.070000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MouseWheelAxis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_LeftTriggerAxis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_RightTriggerAxis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_Special_Left_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Gamepad_Special_Left_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Daydream_Left_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Daydream_Left_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Daydream_Right_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Daydream_Right_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Vive_Left_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Vive_Left_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Vive_Left_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Vive_Right_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Vive_Right_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"Vive_Right_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Left_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Left_Thumbstick_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Left_Thumbstick_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Left_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Left_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Right_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Right_Thumbstick_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Right_Thumbstick_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Right_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MixedReality_Right_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_Grip_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_Thumbstick_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_Thumbstick_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_Grip_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_Thumbstick_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_Thumbstick_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Grip_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Grip_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Thumbstick_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Thumbstick_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Trackpad_X\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Trackpad_Y\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Trackpad_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Left_Trackpad_Touch\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Grip_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"ValveIndex_Right_Grip_Force\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MotionController_Left_Thumbstick_Z\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"MagicLeap_Left_Trigger_Axis\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Left_ThumbRest\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\n+AxisConfig=(AxisKeyName=\"OculusTouch_Right_ThumbRest\",AxisProperties=(DeadZone=0.000000,Sensitivity=1.000000,Exponent=1.000000,bInvert=False))\nbAltEnterTogglesFullscreen=True\nbF11TogglesFullscreen=True\nbUseMouseForTouch=False\nbEnableMouseSmoothing=True\nbEnableFOVScaling=True\nbCaptureMouseOnLaunch=True\nbEnableLegacyInputScales=True\nbEnableMotionControls=True\nbFilterInputByPlatformUser=False\nbEnableInputDeviceSubsystem=True\nbShouldFlushPressedKeysOnViewportFocusLost=True\nbEnableDynamicComponentInputBinding=True\nbAlwaysShowTouchInterface=False\nbShowConsoleOnFourFingerTap=True\nbEnableGestureRecognizer=False\nbUseAutocorrect=False\nDefaultViewportMouseCaptureMode=CapturePermanently_IncludingInitialMouseDown\nDefaultViewportMouseLockMode=LockOnCapture\nFOVScale=0.011110\nDoubleClickTime=0.200000\nDefaultPlayerInputClass=/Script/EnhancedInput.EnhancedPlayerInput\nDefaultInputComponentClass=/Script/EnhancedInput.EnhancedInputComponent\nDefaultTouchInterface=None\n-ConsoleKeys=Tilde\n+ConsoleKeys=Tilde\n\n"
  },
  {
    "path": "Content/HandGameplay/Audio/GrabSound.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b9849f17e45079ec2902ffee437cd8d186f29aafbf3e732eab5f688f0649a7e6\nsize 8916\n"
  },
  {
    "path": "Content/HandGameplay/Audio/LAST_TestAudio_Release01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:1978301e2d109ee5baa2b3df67445779d460a4886b9046b2677b2879cecc96b8\nsize 44157\n"
  },
  {
    "path": "Content/HandGameplay/Audio/LAST_TestAudio_Release02.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:1cb0cf23601b97676c977ab2c9c535ca9c06717af91495ad2b71ea5545699dfd\nsize 44136\n"
  },
  {
    "path": "Content/HandGameplay/Audio/LAST_TestAudio_Release03.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:558c2159e23d0499cd3bb75743973a7b19f2267d437e7d7250901008e7a09762\nsize 44242\n"
  },
  {
    "path": "Content/HandGameplay/Audio/ReleaseSound.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:7409697cfa348d34ead533785d90e153499d095fff519020338abfabe177226d\nsize 7811\n"
  },
  {
    "path": "Content/HandGameplay/Audio/SoundAttenuationForSpatialization.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5a90478a20b64771a4318e6a74b3a21201b3146ff29a4a5d3fb26df42fbd8460\nsize 1723\n"
  },
  {
    "path": "Content/HandGameplay/Audio/generic_grab_01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a2c245034b32b6f5ddd707597c564248464333e501f325e0ba3adda25959a5fd\nsize 37481\n"
  },
  {
    "path": "Content/HandGameplay/Audio/generic_grab_02.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:dbe114715a7464e7fd61bd705448106fd4f0ec07df42ebddfc9ff5f4ccc11666\nsize 36937\n"
  },
  {
    "path": "Content/HandGameplay/Audio/generic_grab_03.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6b7e28b3d1e250ff7c16318291fd001955f755b4edfb3c4af9dffa6cdf63eaba\nsize 42133\n"
  },
  {
    "path": "Content/HandGameplay/Audio/generic_grab_04.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0757440c8431852e8b0ce3ae3bb291890c6a33a868c489588ea886bec7dea106\nsize 36674\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:eaebcbd95610bc979b02ed9221ee0fc256c17dfbdbeb34cdb7d6dc10f807dbaa\nsize 20528\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_02.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8bf0bb6c80d8715e2503d243e0d711c59fbce56a8d91d11869ed41e3d5982758\nsize 27057\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_03.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:eb040580284faf0a36126c46039399b3d308473e974e4a6f0501ceade9a1c949\nsize 23010\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_04.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:aded0c4d042fc7b92871673996a091dce6e91da0bcaf0016cfacb9cd5b391ec5\nsize 21826\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_05.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a163e311b2ccf596cdb478c3c361d8b71f5d53fd361b0e3498187a07b1b27a83\nsize 22654\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_06.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6e0d5ce075fb26cc9b81da4fbaa93a48f31193ca2960cc5c6ed5ca1c5cb64f3a\nsize 20680\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_07.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3be5c781b4eabe164845d2b39182a57dbe39893c4deda2bc5de9137095762978\nsize 24363\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_primary_block_Cue.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:99f5e033a67b622cf4960b6d740c24fa815b14553ab999408ed280d5dd63dc87\nsize 13769\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:1bf1cdfc4f9bf89928f94fba5b314576505b63be63f0f51b2857f2f7fa3c4e7a\nsize 21480\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_02.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4b1c604e88462ea7cb419cc59aeea14e7e85dccbb0f34775ebe3b1d085ac30a1\nsize 25422\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_03.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5f6decb8de7585619fd12a51ff9143e4cc0d0cdaa4c29251d953075ec8461f13\nsize 35536\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_04.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:388306dc74efc7cfbe22e4519635332d621133ac47daf91f433ffe6d85da5ab5\nsize 28496\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_05.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4d12f15c9ab424b4ec5a0b62c61b4cbe574795f29737c06143e3907e20c22275\nsize 45672\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_06.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:da46fa6bccaa2960263bbda1204210a1b64b73d3de15b028ebc02333fe7eca27\nsize 27113\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_07.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:59e4866391a9321a713ae1a98f0690a121666d508775160fc4393e602c5f6cca\nsize 30256\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_08.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cc7d95a24185342728913aef8f08248a5a80f40bf5c5306a4c31849510fdf482\nsize 26386\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_09.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0450e4e94724af41df543aff6656da566d8e8565dda267b428207ebdcbad9108\nsize 34593\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_10.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f3c9df420ac2e410031f394c5942c9785081b1e128ffaecf2f59ac33af3c69fd\nsize 34240\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_11.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4ca7163f0abdd9a17ec71eb907aca2273774f0d997df26e0351543988b0636fb\nsize 31648\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_12.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:679b16faadd419a792ba4e37e32935c9620c8c7c0500a60aed132fbe5dd0ca09\nsize 37212\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_13.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:234096a925df53199d57c3d2a8cab3d8eb68ad7cb6d1182affb06e5eb13d2a44\nsize 27979\n"
  },
  {
    "path": "Content/HandGameplay/Audio/impacts/sfx_impact_secondary_block_Cue.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:880d452734472b35c3463209b2cbeebea27ccdfed1f039e2ac0d107b19a092be\nsize 21332\n"
  },
  {
    "path": "Content/HandGameplay/Audio/laser/LaserSound.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:64a611c5eb7065b1b4428957a6ea5d67ce0dbcb9774bb6744232b7bdc11c8421\nsize 7671\n"
  },
  {
    "path": "Content/HandGameplay/Audio/laser/sfx_laser_end.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:09d282ba8ca20e06fbaeccbe4efeedc68c90bb67d01795f867959dc2afce1312\nsize 203679\n"
  },
  {
    "path": "Content/HandGameplay/Audio/laser/sfx_laser_lp.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3b62e80c18f39438f0be03d7923b94206a0ae96a7d8d4140da60b94a82f9a515\nsize 705462\n"
  },
  {
    "path": "Content/HandGameplay/Audio/laser/sfx_laser_start.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:18c1bb23a20cf98917e72a067a7234256b78133655e2065bb9589e77bed0684d\nsize 161306\n"
  },
  {
    "path": "Content/HandGameplay/Audio/mus_showcase_hands01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8cac4df454ab7e5ec71671950148129e614bc27de9c3c7cef92d62963a2de38a\nsize 31912467\n"
  },
  {
    "path": "Content/HandGameplay/Audio/sfx_ambient_outdoor_forest_ambix.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:7e85a9b706e63552e98e5b84381b410223eb6c69ee6b50213c42cc9d2f8c79a5\nsize 39309174\n"
  },
  {
    "path": "Content/HandGameplay/Blueprints/HandsCharacter.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4871e75eb2308c491e320f8fe4b0f704c1afeae43a934d67bb3449afc45558a7\nsize 77585\n"
  },
  {
    "path": "Content/HandGameplay/Blueprints/HandsGameMode.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a71313d9a1c73496b14fc115426a21922a4412a1ee91027b1923a9e4b7bc2dd1\nsize 20601\n"
  },
  {
    "path": "Content/HandGameplay/Environment/DustMotes/DustMotes_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0b57e575286cb77fdf3432392fe72d265dda2a5c57133fbbef80581e55a7c742\nsize 80610\n"
  },
  {
    "path": "Content/HandGameplay/Environment/DustMotes/DustMotes_system.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:790bc72151f44604020e0c23d6f8514005023d0d1f7a3eaf931962b619a701ef\nsize 836668\n"
  },
  {
    "path": "Content/HandGameplay/Environment/DustMotes/DustMotes_vfx.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5f0bc288750ae2cf9eaddfbaeb00ef2fb54a2ba03952c763346990753bcbbcf6\nsize 185051\n"
  },
  {
    "path": "Content/HandGameplay/Environment/DustMotes/T_Epic_sub_dustParticle.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a5e5b2b4fee995be51884405dfe667a5d0d22da0ff4141c38d28ca59486ada19\nsize 38225\n"
  },
  {
    "path": "Content/HandGameplay/Environment/DustMotes/vfxm_Dust.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:381c95e433bd9090d5af77e83e4ca4613b99f41b8e1adf9421328cc075906124\nsize 85304\n"
  },
  {
    "path": "Content/HandGameplay/Environment/DustMotes/vfxm_GradientCircle.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:52b49b3f3069e7cec68ef7684defbadd4d5a1ace9a4635befaf09f14de4757f9\nsize 94105\n"
  },
  {
    "path": "Content/HandGameplay/Environment/DustMotes/vfxm_GradientCircle_Inst.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e657cf3213a2d9483a4a5d7a34171808de8e2979f277df44774e522c95783e98\nsize 75299\n"
  },
  {
    "path": "Content/HandGameplay/Environment/EnvMatParms.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ca178d89f6d9f0498998a502f84c4cd581409b85954accac4cfe5d44196609e4\nsize 2336\n"
  },
  {
    "path": "Content/HandGameplay/Environment/HT_cube_Tex.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:08bbb2ab01456d61878518b53c0e6a2bbf9af202c7b507c6457f41088b2d43e5\nsize 18267320\n"
  },
  {
    "path": "Content/HandGameplay/Environment/snow_envi/materials/front_rock_snow_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:baa2f367209dde9f4e46d460d859ffce0a4d33dc23500b03387025942f804bf1\nsize 5150032\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/back_wall_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c902e85b67af6d2cd99a5bad91fd479b4019fc2c432fdf62a1ca65fdaa04074d\nsize 5590326\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/back_wall_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b7ce974ef29b45d8057886ad362204e46368008e8251f5fd5be7344ad2718048\nsize 137768\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/back_wall_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:90740901bf42d83576dd7a26d3f9ef9056e6ddf6debf867b25ad30e52484cebc\nsize 313718\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/cieling_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:30f503a452aaa88c690e11d2c97e5a9484ee6968cbcb5002010401e2f7445c86\nsize 6142030\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/cieling_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5a7126c3a42f491ac1d7817fec8c6e54a22149118f63dbe06d137cc2f679dadb\nsize 156936\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/cieling_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b354f382457e82b44a0ab155312f0f4325f9690fdd1e71a2b8927270757f5a09\nsize 152293\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/cloud.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:80f38f5943a40ad9e0905d7a17c83460d5c3db4ede8e1ea7fd74ef4b57d12eba\nsize 109892\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/cloud_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a4ba8c1172c2f1fbd713523780fd5c0afac7f04d43f4d69e54662738971c91eb\nsize 568262\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/clouds_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:17c542e80cc6c53706b6cb9fd8109b342e2e4be5eebd39a04c61236d03af6b36\nsize 1814529\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/collision.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6701718b9264456e2dab0f07b31fb5ee775cecb8b1c6c34a9d168420e718c7d3\nsize 145862\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/front_rock_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:801cab027ce8e7aee8027265865a22365ad254c8e7aff707b7274e6a5419e90a\nsize 7457494\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/front_rock_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:7b0067ced418dd4b5f864e379cfd5b77849a659fa16ae6fc1e4912a25b458fd4\nsize 142811\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/front_rock_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3aaff68e03a00c02d06ef932e4aeb6c4b5e206f66c74feb055c729ddb3956e93\nsize 632117\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/front_statue_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4dbfe761d9130aec0383bc7c45e5e4ccef4b8e9ae00308fb4e627f111fb5654e\nsize 7524738\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/front_statue_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:96c313008f0bd1a2e80f07d48cbbe91a074b0f6ca88aaa6c0533ed6d4b4d6fb8\nsize 151266\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/front_statue_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e17241fc5fd27964e71773d8e5294609077f4b766d020cdcfd0da0d51ee974fd\nsize 881118\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/grass_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c6b73bcd76fce0f7ec32f5288db40970cbf72719606e9abb5baadaf92a65ac92\nsize 5004470\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/grass_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:bc4dbb177cb2a909979699381ef3e7a3b32d2473b71453c8b9947c06f3b3c04c\nsize 147603\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/grass_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e9601ad317b3220937f78a66e4b5ebd68f100e111255c0140738635a5c2c68b3\nsize 180524\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/left_wall_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:7129dcd59dc43c7dbc5ce39260bbd2befe6ef36ff2b02e2704a7214a05778ea1\nsize 7846747\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/left_wall_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:12a70a31bb78083ae856811bd588c762950d718c6d77d332196d0c8fbbdef9fc\nsize 148314\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/left_wall_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:598d63e3ac2943e70e28ce02dd4cdc4b83a321d71c09a13d1d85e688b4cf59d8\nsize 392141\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/main_floor_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:402e435c4cfcf9535ec4bf11a284d69dbf579b7d22e6bfb4e56991bae4dc9d4d\nsize 8532169\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/main_floor_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6ac6712d14e4c6975797273b78278c632003f9bdac15da6081ee902d26f24f5a\nsize 144198\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/main_floor_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:31e702b6f9fc44a16d81118fbe12a141f3e9eb4b31a85ab06ccdeabfb982c6de\nsize 200465\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/right_wall_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:17098acc888a0d0fec0585d4eca19aff287bfd1affc39114fb723cd47609e5f5\nsize 6306874\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/right_wall_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:79319e598a0a81e75a0ecd140ba4f64e843014ab5f0c392a740dcf3ea7d05538\nsize 134751\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/right_wall_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:bb7eb1f3fc3fdfafaaed5e7c7e3122278c5a9a79d9379b7513f69952f58744ed\nsize 523096\n"
  },
  {
    "path": "Content/HandGameplay/Environment/summer_envi/vines_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8657ebe24ff6867959d9dafd4c66ca147dda405825ca00ffa31bff2bc2837126\nsize 1431233\n"
  },
  {
    "path": "Content/HandGameplay/Environment/vfxm_WindFoliage.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:aba24694c700ed06d22b01a1b426e677d954b9f40d2d7e8cfe37503bb40b9713\nsize 163497\n"
  },
  {
    "path": "Content/HandGameplay/Environment/vfxm_WindGrass_Inst.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4075091fc2884277ae3265b6c4c116ae9780fc1518d4917ccb1590938846bbe3\nsize 142642\n"
  },
  {
    "path": "Content/HandGameplay/Environment/vfxm_WindVines_Inst.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:88384ad7256f73e82b3964b58e815877e928d7cb7df691247481627b4cda2369\nsize 128071\n"
  },
  {
    "path": "Content/HandGameplay/Environment/vfxsm_vinesVtxAlpha.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:1e506290165640ad39a6ae18e87ede4d0e61548f3d5b84b172db290406e0af4e\nsize 310382\n"
  },
  {
    "path": "Content/HandGameplay/Hands/Models/HandMat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6219b9c63ee1ab3b0b4675d08b30db656d0febfb61f68b066d1f03ebc8f505cd\nsize 129357\n"
  },
  {
    "path": "Content/HandGameplay/Hands/Models/OculusHand_L.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e9d5ff1272c22f7d2bf481ca9094e57e088603281b66b17fdb4087a92b45cda8\nsize 500387\n"
  },
  {
    "path": "Content/HandGameplay/Hands/Models/OculusHand_L_Skeleton.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3bfd455bd71d7e72070815035bb6e042ef8e2a45ad44e88b3a9f971e029d8ef3\nsize 18086\n"
  },
  {
    "path": "Content/HandGameplay/Hands/Models/OculusHand_R.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3d03d878f090cd0f06bb17510e0a6040b998f9cb3511ac8b81630c80ebb7e815\nsize 500443\n"
  },
  {
    "path": "Content/HandGameplay/Hands/Models/OculusHand_R_Skeleton.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cd6ad521c2923a9b13bb81a24e2fd5e2cc63250ada54e9c8c2c2c85927459f1d\nsize 18086\n"
  },
  {
    "path": "Content/HandGameplay/Hands/TutorialHand.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2ac93caef3fa82b4392c6f1929c7af3f6329fe1ff28e39304692bcd47bb1fa00\nsize 100097\n"
  },
  {
    "path": "Content/HandGameplay/Hands/TutorialHandLeft.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5da72c2cbcd284e2251c3567d5a1965e1a0697be2907cea3ae749e89fd6f643f\nsize 44984\n"
  },
  {
    "path": "Content/HandGameplay/Hands/TutorialHandRight.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:240c9b6594a30c1e582952db71e79964adfb3f4b5115197d80868e8e143e9ee4\nsize 38781\n"
  },
  {
    "path": "Content/HandGameplay/Input/Actions/AvatarLeftSystemGesture.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:db687a87c065b4da5889c279bbb85bc36e4cad91b95707db1e643bb82e4da4a5\nsize 1400\n"
  },
  {
    "path": "Content/HandGameplay/Input/Actions/LogGestureState.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cf62c8e733b90fd4ae2807a286ca6cb695bb95634bb41ad202a13583310d903e\nsize 1360\n"
  },
  {
    "path": "Content/HandGameplay/Input/Actions/LogLeftHand.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0bec330b1f1d1c08d7b3701fc8f67f21ac76a043af47a04908b56cba2a7970b9\nsize 1340\n"
  },
  {
    "path": "Content/HandGameplay/Input/Actions/LogRightHand.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4ea0afae7721ecaa22a2d57f555261c0da4c1ac5dd87e26c5b994cc76f0f2692\nsize 1345\n"
  },
  {
    "path": "Content/HandGameplay/Input/Actions/TogglePoseRecording.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5fdbd587886c526d16b1654bdfe3813445ca582af5448adc7a4d7d31d24008c7\nsize 1380\n"
  },
  {
    "path": "Content/HandGameplay/Input/Actions/TogglePoseRecordingLeft.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e05a4a6eb3d46fbe6e918301caab9797bc816222eccb7a1929cb05173c496dac\nsize 1400\n"
  },
  {
    "path": "Content/HandGameplay/Input/Actions/TogglePoseRecordingRight.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:49061cfaed6bc737b226117849187a5aabd9fba4295d2d6226eca67b9491864e\nsize 1405\n"
  },
  {
    "path": "Content/HandGameplay/Input/InputMappingContext.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:97a2825de20eb4846b544ac3a94e80cc89e01accaa8639fdd71216109ff58548\nsize 6734\n"
  },
  {
    "path": "Content/HandGameplay/Levels/HandGameplayShowcase.umap",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:07c8e5339550f019e1c79d4660b4ce249fec3b39e5697537167e11aa18815016\nsize 346692\n"
  },
  {
    "path": "Content/HandGameplay/Levels/HandRecognitionShowcaseArt.umap",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:117d793adc535717d693bff0e08f42552fdaf96bcfd39f5a08899d88bd7180d7\nsize 49341\n"
  },
  {
    "path": "Content/HandGameplay/Levels/HandRecognitionShowcaseAudio.umap",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:adf30fae34a9e7df004da751fd6763529d8dc3c7b16c423d2b3396f587037960\nsize 11278\n"
  },
  {
    "path": "Content/HandGameplay/Levels/HandRecognitionShowcaseVFX.umap",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:6299627c430c30e2cc28e28b5c0dd14d3726ee1061c186d9bd2600c7a39e00f6\nsize 24789\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/Blocks001_Throwable_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:400f80d39b38df00cc322fabefdcb1e5665053fb47b72c8339094b96ed759912\nsize 116820\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/Blocks01_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:11d069f39ac48bae24f2b1acb90bf90261733268b6c4b55bda86d1207a7dbdf0\nsize 373341\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/Blocks01_bc_Emissive.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:230f528f6dcf748855b08ad6687c796c2e16a7c74d00be907363c4059c861c6a\nsize 166015\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/Blocks01_nm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:55e19d90494d33137e9f044d96729702123e9a8ec837161180179b33fd3b0d89\nsize 448108\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/InteractableBrick.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:fe26a7b219c4255ce56765f18d3c0e73f3fad6bd35232401e0d55eceb2efbde4\nsize 77988\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/NS_BrickImpact.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:9ee79b9bd8d2f29f41e55bc3142c9ef1bcebdf9ae7a4901de5debf458ea93614\nsize 1201603\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/T_Epic_SUB_UV_Small_Rocks.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:398b9586620051eb4eb594a89d885580287b8df4e1ea4dd142455f6a88daafe7\nsize 69776\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/vfxm_Blocks01_Throwable.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c3e37b2c1fbe3438dfc64c7c80d17b786774ef16e156ef5edf8397eaa3b3d78f\nsize 145820\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/vfxm_Blocks01_Throwable_Inst.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:35ded812adb41a0d1156c11fdaafd91a718e0c139d7040b12d287b022115a9a0\nsize 131607\n"
  },
  {
    "path": "Content/HandGameplay/Props/Blocks/vfxm_ParticleSpriteUnlitSubUV.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:1c0eb5ad0e0562ba7a3a8e45635669050d88fd9c9a36bf9464fd0b1c37cbd145\nsize 99625\n"
  },
  {
    "path": "Content/HandGameplay/Props/Button/PushButtonBP.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:96054da4ed1469b8b1e3488aa649caf8187cdc93b4e4d33d8ae4ab7cac42658d\nsize 33958\n"
  },
  {
    "path": "Content/HandGameplay/Props/Button/SFX/GameConsole_button_press_in_01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e6b03d935a18205e06a46a9c5529da3bb331ddcda0032db317eacb15568f9c04\nsize 78128\n"
  },
  {
    "path": "Content/HandGameplay/Props/Button/SFX/GameConsole_button_press_out_04.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ca9d8977d512d6008e945d014abb37ccbdcc1f8002ba5a357f094e6dcc5b62b1\nsize 67611\n"
  },
  {
    "path": "Content/HandGameplay/Props/Button/ToggleButtonBP.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:83a1ddf8dd873ac663b3f2ded9debbc012bec912b4ca61c9a2ac0fc202e93f1a\nsize 59851\n"
  },
  {
    "path": "Content/HandGameplay/Props/Button/testbutton_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2937db3174474c119042cec9b802a6c6af41365998f3c26963ef5e58249b5b0a\nsize 106713\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/2HandedBeamProp_Mesh.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:dccb18897ebeecba8cfddf6722e4e1dd7e15ed7bdb10c27bdf4d11fc22ca3fae\nsize 136727\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/InteractableArtefactHandle.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c1bd40c9c5640ff847874f06a51c53a8f503ad8666fb6333c2f5ab29043cbbec\nsize 22921\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/InteractableTwoHandedArtefact.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:44baf3b7ad3dbec11623e8aaefb097b87cb72e72848f7d7af0c2e090f92bf444\nsize 406172\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/TwoHandArtefact_StartBeam.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:54fe03d0134e0eef428b5b742e76b4ff06dd0f8cffe38982a99ab1edc957f407\nsize 306728\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/TwoHandedBeamSystem.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3c945eeb2e333e3f2abf4061b754b61d40a25435471f6a36842181fdb2f90031\nsize 905952\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/vfxm_2HandedBeamProp.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e64d98e9fc6a558f9441454c48a616a583b085c07050689ef3120c30da3b67d2\nsize 129560\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/vfxm_OrbFloating.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:9ceb862c2a51374ba5bdb8235a9bb61b7b2ec57674ec3b18fcc0b7fc55bbbdc1\nsize 129313\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/vfxm_TwoHandedArtefactBeam.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:daf831d5e14cb4c01178afbf354c8c2ef13da010d29c43a437df52421c2bb272\nsize 120677\n"
  },
  {
    "path": "Content/HandGameplay/Props/RingWeapon/vfxm_TwoHandedArtefactBeam_Inst.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:afc44e2fdae94a4ff70eaa5a7023d1d9eb8b509329912d2402c3a700ae814c00\nsize 86454\n"
  },
  {
    "path": "Content/HandGameplay/Props/Table/table_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e8f4e092d9e979fa974073fbda3e3a85bc625ebbaa6badae2d5646f8a51107e8\nsize 2873280\n"
  },
  {
    "path": "Content/HandGameplay/Props/Table/table_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2ee811fdfef12013624d479c0710e8c2f1c35057081889265a7783ca71569837\nsize 122049\n"
  },
  {
    "path": "Content/HandGameplay/Props/Table/table_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:05e7df80416abb00f0f907278722785ba6dd1f63c70baa037bf39c4f717d8854\nsize 125429\n"
  },
  {
    "path": "Content/HandGameplay/Props/Target/TargetBP.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:489e960fea29eee0a5cdfdae2af6ad98429e21b83a5ecb57c307551d5ca600dd\nsize 168606\n"
  },
  {
    "path": "Content/HandGameplay/Props/Target/sfx_spinning_target_01_loop.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0c6f7b263811dce4f7ba17c113776cf47f89b2a21c3f6770f2ffc52db455806a\nsize 2029960\n"
  },
  {
    "path": "Content/HandGameplay/Props/Target/target_m.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0f90611a4b3546bfbe3301812624af3dfcb7e1045bf7ef17439eecf11bace8f0\nsize 137620\n"
  },
  {
    "path": "Content/HandGameplay/Props/Target/target_pentagon_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:40810460e0acd422e7f79bdc76b0f112b4fd32502fcfc35009a44895d16c6e2e\nsize 111988\n"
  },
  {
    "path": "Content/HandGameplay/Props/Target/targets_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d3c3a8e05f71a62586c6e3ebbebebb750968d28276d524d6b73e1cc3c0f02c0d\nsize 1393767\n"
  },
  {
    "path": "Content/HandGameplay/Props/Target/targets_em.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2013bdb1db3c56115fca9bf3d8b6fc665fb6e0a055a2f431f7b3c8d437a9e0d3\nsize 1461838\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/Activate_vfx.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f5e5063e39d93d5186a072e59cb7da62b4305da0ffcfc97594da5314d8172a06\nsize 219044\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/NS_Activate.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ff6745c9df34b10b024daffc552deb880712f9abe7299bafa2f62aa0baee79dc\nsize 1120235\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/TeleportBeacon.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4fb8c9859a53a30bc3b58b9d16b9af7a0ecb29c1fecb7eaa39a0baa28c674458\nsize 115086\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/TeleportationPad.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b33f746beb7471a7d51b772c0a0f8bda81df64b64935b3c0e8768e09ac458182\nsize 220236\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/TeleportationPadMesh.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3352cb0e602349b25be96f31a176edbd2a1210ca99c56e9e7bf54f11186f946c\nsize 103637\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/vfxm_ActivateGlow_Inst.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d16ea1baab686e825db506f5fd3c68ee6ca5ff70c41f6dac1ae68b23d0ff14ae\nsize 82813\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/vfxm_AlphaPanner.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:81ce8e1f1bf4edcee75faa6f7c4859328d56762459861f6e7b485dc24ca9a78a\nsize 141201\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/vfxm_TeleportBeaconPointerArrow.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:305a011c5d554a9d31a16abefa2329d658cfd5f61d3de7606196d554588318ae\nsize 119615\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/vfxmi_TeleportBeacon.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:391b9f9f78709f79e952028b104e19fc1001c468a5ad10953a357399fdd62cfd\nsize 126239\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/vfxmi_TeleportPointerArrowRing.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:655832a84c389743614cfeb4a940fca36f2e36265843667c94820a721816f4a0\nsize 114278\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/vfxsm_TeleportBeacon.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e57765522db7755917a7065be479277f5f29136a1ff5753d7b30cba823aea8d0\nsize 148367\n"
  },
  {
    "path": "Content/HandGameplay/Props/TeleportBeacon/vfxsm_TeleportPointerArrowRing.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ac19002af2dd2c8195bb367b2364f7d0c95590d0c6a188f539ff9697e0ef74a3\nsize 137894\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/Art/ball_bc.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c738d56d7c0aa6f3bfb999f7675dc1164e647f306e25bd9e5983bf6ce4ca2d97\nsize 1241517\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/Art/ball_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:fa16847b663305a08a1dc76f5f67b11c7e61351da8260dfc6f9b5b4f8eea259e\nsize 123074\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/Art/ball_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8bb4bc79fd6fe522eaa4879f07631230aa55e337103849f7938717f1356c292e\nsize 155978\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/Art/tether_ball_base.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ed7f9002f59b8e0a40025addd009ebf0887e86d55d9d35c3b590bf54373e059e\nsize 674230\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/Art/tether_ball_base_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cd91b5084903baf8a0a6efe2d947cc56543c37f2dca855ce43c94a4c0392c2f0\nsize 118285\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/Art/tether_ball_base_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:7cc3f92bc854e0019cb4b912527a16e8ac0df97f378acee9a45f07b350fcfcd6\nsize 109258\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/Art/tether_mat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2e09393041414955f2180675afffe0851f1fd35c56d4a169f633e799fa865e85\nsize 115800\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBallHit.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3a8704b2611d072bbe8b50d5bfb27edf3971bc544d223b1a40558aacfd86d7de\nsize 16110\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBallSoundConcurrency.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:622ee5df7d8e40c606792c2ca659a94263d6652e0fc98e64d9871a4f00f2116e\nsize 1549\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4d62109799fb674d78162e53011364ccce82423aa10bf1c688dc3f60252c57ba\nsize 43153\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_02.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:dcadb7b0591413a37ba76c9717196dee27dc56caa6af26ffeacd3aedd183671a\nsize 33888\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_03.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:026dd90f3857c99e8f4e6fdcd3a0616aabea03cb067971090e63f26b2be0d4e4\nsize 48030\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_04.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4ea7141b208fffaa22ba03276833ad9e17c61a0f2608f2da0179196081545092\nsize 62567\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_05.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e726ebddfd66dbf5f9f8100b80f984e42be59715a92da8ed3fb8c0c5fce24e01\nsize 43129\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_06.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a877d19496c02e40cdbfbec6b1f1e9d155f0c1c105046ae3253eb0df06bcd1eb\nsize 36879\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_07.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5b8910e973a1db85ba729aff9a38ee42ac23b17df628ee4914935ea89d694c77\nsize 30504\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_08.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2ef4387350baa1ee0b82432080ad64616413becffd7d7a135fde13f92f1867d1\nsize 27541\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/SFX/TetherBall_RubberThick_Imp_09.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:02a2de4ddfd2caca2f16ec72b1c0fdccfa0a90a66fe8abb6694645ad75cab26f\nsize 39754\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/TetherBallBP.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a18056945e3328042cbf984ab8551b66c4160a003a5d0a147e85130a692482ab\nsize 200287\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/ThetherBallPhysMat.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d0cb195b43e06d51342ac25f054f5785d2297c2b065e21a5361a9180cf6b6083\nsize 1665\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/VFX/spawn_tetherballBase_ps.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ffbb463f7c2c0efcbd1d33797ac6f1726b76f0f7ecaab705ff8b63b785b5b806\nsize 45863\n"
  },
  {
    "path": "Content/HandGameplay/Props/TetherBall/tether_ball_sm.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8eb309b42c11125e98086c44b90a212db8287902645446cf6b8a5b57b55da609\nsize 162513\n"
  },
  {
    "path": "Content/HandGameplay/SharedArt/Blue.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4d29c50208c87b00d2442ba40ec3bb323115988b1017db0350a94715671bd720\nsize 111177\n"
  },
  {
    "path": "Content/HandGameplay/SharedArt/FlatBlue.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:4adf7a3032a3f8b0b41679d9f58a37902a400eab565d8189ff9c14371e93d14e\nsize 104258\n"
  },
  {
    "path": "Content/HandGameplay/SharedArt/Red.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ba3bd8c301cf90755b3516af72f17c786d3fc10c356ec1364ada32cfb241e5a3\nsize 113428\n"
  },
  {
    "path": "Content/HandGameplay/SharedArt/T_Caustic01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:c579f1a4402b4aaa42b6f080aa6acce3bebb9808e44bd53c61b7cc5d0aa4dbee\nsize 1530727\n"
  },
  {
    "path": "Content/HandGameplay/SharedArt/T_WorleyNoise01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:31bc66ed0c48b42be7b627b057dfd357b491fc6bedd5ec8bb13a13cc9b1f8ae6\nsize 448233\n"
  },
  {
    "path": "Content/HandGameplay/SharedArt/TextBackground.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ceb8c1e3729c63698365449612b9c8da558cea0e5adb96a99020247afda4bd55\nsize 109062\n"
  },
  {
    "path": "HandGameplay.uproject",
    "content": "{\n\t\"FileVersion\": 3,\n\t\"EngineAssociation\": \"\",\n\t\"Category\": \"\",\n\t\"Description\": \"\",\n\t\"Modules\": [\n\t\t{\n\t\t\t\"Name\": \"HandGameplay\",\n\t\t\t\"Type\": \"Runtime\",\n\t\t\t\"LoadingPhase\": \"Default\",\n\t\t\t\"AdditionalDependencies\": [\n\t\t\t\t\"Engine\"\n\t\t\t]\n\t\t}\n\t],\n\t\"Plugins\": [\n\t\t{\n\t\t\t\"Name\": \"ApexDestruction\",\n\t\t\t\"Enabled\": true\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"GooglePAD\",\n\t\t\t\"Enabled\": false\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"OculusXR\",\n\t\t\t\"Enabled\": true,\n\t\t\t\"MarketplaceURL\": \"com.epicgames.launcher://ue/marketplace/product/8313d8d7e7cf4e03a33e79eb757bccba\",\n\t\t\t\"SupportedTargetPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"OpenXRHandTracking\",\n\t\t\t\"Enabled\": true,\n\t\t\t\"SupportedTargetPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Linux\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t}\n\t]\n}"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) Facebook, Inc. and its affiliates.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Platforms/HoloLens/Config/HoloLensEngine.ini",
    "content": "\n\n[/Script/HoloLensPlatformEditor.HoloLensTargetSettings]\nbBuildForEmulation=False\nbBuildForDevice=True\nbUseNameForLogo=True\nbBuildForRetailWindowsStore=False\nbAutoIncrementVersion=False\nbShouldCreateAppInstaller=False\nAppInstallerInstallationURL=\nHoursBetweenUpdateChecks=0\nbEnablePIXProfiling=False\nTileBackgroundColor=(B=64,G=0,R=0,A=255)\nSplashScreenBackgroundColor=(B=64,G=0,R=0,A=255)\n+PerCultureResources=(CultureId=\"\",Strings=(PackageDisplayName=\"\",PublisherDisplayName=\"\",PackageDescription=\"\",ApplicationDisplayName=\"\",ApplicationDescription=\"\"),Images=())\nTargetDeviceFamily=Windows.Holographic\nMinimumPlatformVersion=10.0.18362.0\nMaximumPlatformVersionTested=10.0.19041.0\nMaxTrianglesPerCubicMeter=500.000000\nSpatialMeshingVolumeSize=20.000000\nCompilerVersion=Default\nWindows10SDKVersion=10.0.18362.0\n+CapabilityList=internetClientServer\n+CapabilityList=privateNetworkClientServer\n+Uap2CapabilityList=spatialPerception\nbSetDefaultCapabilities=False\nSpatializationPlugin=\nSourceDataOverridePlugin=\nReverbPlugin=\nOcclusionPlugin=\nSoundCueCookQualityIndex=-1\n\n"
  },
  {
    "path": "Plugins/OculusHandTools/.gitattributes",
    "content": "*.a filter=lfs diff=lfs merge=lfs -text\n*.lib filter=lfs diff=lfs merge=lfs -text\n*.uasset filter=lfs diff=lfs merge=lfs -text\n*.png filter=lfs diff=lfs merge=lfs -text\n*.umap filter=lfs diff=lfs merge=lfs -text\n*.jpg filter=lfs diff=lfs merge=lfs -text\n"
  },
  {
    "path": "Plugins/OculusHandTools/.gitignore",
    "content": "# Visual Studio 2015 user specific files\n.vs/\n\n# Compiled Object files\n*.slo\n*.lo\n*.o\n*.obj\n\n# Precompiled Headers\n*.gch\n*.pch\n\n# Fortran module files\n*.mod\n\n# Executables\n*.exe\n*.out\n*.app\n*.ipa\n\n# These project files can be generated by the engine\n*.xcodeproj\n*.xcworkspace\n*.sln\n*.suo\n*.opensdf\n*.sdf\n*.VC.db\n*.VC.opendb\n\n# Precompiled Assets\nSourceArt/**/*.png\nSourceArt/**/*.tga\n\n# Binary Files\nBinaries/*\nPlugins/*/Binaries/*\n\n# Builds\nBuild/*\n\n# Whitelist PakBlacklist-<BuildConfiguration>.txt files\n!Build/*/\nBuild/*/**\n!Build/*/PakBlacklist*.txt\n\n# Don't ignore icon files in Build\n!Build/*/**/\n!Build/**/*.ico\n!Build/**/*.png\n\n# Built data for maps\n*_BuiltData.uasset\n\n# Configuration files generated by the Editor\nSaved/*\n\n# Compiled source files for the engine to use\nIntermediate/*\nPlugins/*/Intermediate/*\n\n# Cache files for the editor to use\nDerivedDataCache/*\n\nStoreAssets/\nGenerateSln.ps1\n"
  },
  {
    "path": "Plugins/OculusHandTools/Config/FilterPlugin.ini",
    "content": "[FilterPlugin]\n; This section lists additional files which will be packaged along with your plugin. Paths should be listed relative to the root plugin directory, and\n; may include \"...\", \"*\", and \"?\" wildcards to match directories, files, and individual characters respectively.\n;\n; Examples:\n;    /README.txt\n;    /Extras/...\n;    /Binaries/ThirdParty/*.dll\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/BlueprintMacros.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:fd8a267cc165f554ad3caca177326a8ffdc8d28f0e465eda99986f44474f955b\nsize 19603\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Button/IsButtonableInterface.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:bbc6041451dbced11619631f508c5bba488a8c751c6807027058155431a77e34\nsize 8842\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Button/PushButtonBaseBP.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:bc31619c51f959dd3d66f795acdb4b2278cecd65fe431b45bcd54c76cfa7010d\nsize 420145\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Black.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:32771fd4caa2a3602a864be461f4648c71c44a6525ba97cfb3f90204b77d1b1e\nsize 158824\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-BlackItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:478864f05f6d4be2278d511f832853bcc9e9ee22f8fcf3703e6bec9fc6ef29a2\nsize 193332\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-BlackItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:47c5b118bf8405524375bbcacfb6910b90c110f4ec8272f47cf6437048f50ab7\nsize 9053\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Black_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ceb0a59d1241f49c24fbd1c8b1511ddeca5f630195be570de617f55655ff652c\nsize 8641\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Bold.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5311a26a07352ceb5e038fde8584c38231f42f4c2926ad617dc6a53876695792\nsize 157470\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-BoldItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3980178ca9739b58ce89f889be02ef11a9e35332149930d70de0a3b02bd6ef88\nsize 190918\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-BoldItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:ee0e5d9b718f4dbffc4cbb075aeee3df287a41094275f130f1c4f5969d24388e\nsize 9150\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Bold_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:60e3255a84e3abdf54526b725235cbb1d20c2e2f8ba99c502b365ce4fa4ca433\nsize 8676\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBold.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5a1213f99a6198d5344c9ffdc1289f46a91bf928b845f4bf18dc956b98bf3690\nsize 157094\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBoldItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3ab4e717e741ce59e63e9f887f78b2b0de3e552b2cf1e43410964af97a68deb0\nsize 191850\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBoldItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:fe14c12e99f34c5de5836e7375f93de0f556b16b7b9c577d25589d77d7a5eac4\nsize 9168\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-DemiBold_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e5d3096fdd763e6e7f950f15cf65bf2d37d881b6eb5768628c89fb9466683717\nsize 8781\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Light.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:43ffafdee0fda042fdb2c5bc68c289385982536652234ad7b2384b56392d077b\nsize 157532\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-LightItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a2170036e9e99f4e5084cada6e4a415d032f9c02366b3ce82fc9907aef1dcf81\nsize 189180\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-LightItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:56d9fd7cb3521ef1651d623d937e052396e185dbc809f030188855f5a6962520\nsize 9330\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Light_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:64992b5f8cd6ea277842b76862e5dd4de494c31e3638e5a68d80a73ddf77035e\nsize 8608\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Medium.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e3656a97844e3300b46ee7ac8ad48329fd1eef673c9acc701f2c051ce9cccd54\nsize 144430\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-MediumItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:06a652b0d2208ce5bec58021f44ca7d304389e6dbe1b0b2c001bd414ffb1a9eb\nsize 187234\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-MediumItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:89b71292847af0e4776c0f4679391a26144b71189555afec3e1b780320743949\nsize 9367\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Medium_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f80911b61ff847747655c9e3f159df168a3b1cc4eaacb42c32149ef9c973c40d\nsize 8835\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Normal.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a94395cc1f34cbc8d5b0c482ffd769450c65bed2231c985f3926d2afacc36b6f\nsize 155734\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-NormalItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e201ffc9b185bc0edee5241cce68000b6dca445166fbc5f60132007fd2012845\nsize 191538\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-NormalItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:1218b2fd1e272348793881364a83b34cd58464103844a5a91d86848fa9d25924\nsize 9455\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Normal_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:0b710fa5fbdcabfcedadfbb516a918fa01f0ed6dbb93bc6f5828f03fef395008\nsize 8882\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Regular.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:77691182bc12af00b5ea06e4d81ff70c7a4bed5c58be727cb8b31cb455754656\nsize 157876\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-RegularItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d128ad81e480e1535a2fe3f89c7aa304b1730994022c1e69cfa38121188d8c41\nsize 189356\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-RegularItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:fcedc6cbb14e81b0b1d6f017e830c8b27f1db03f35722a6c0fa355dc2aa35af7\nsize 9370\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Regular_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b8fa7e6ca6a15773f0c2b3a3a6cec65a368f3c05656bb62923db5a5eb5cd373a\nsize 8948\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBold.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b45e9eb70fd35ecc3b1051ac503e8e41dbbab1be67c057db5c6abf0a89870055\nsize 139022\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBoldItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:77d8797a5d6fda4c105946d782339b184b4a4fa13ecb257c1030c60ade5d2d3c\nsize 188166\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBoldItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:cf4128d4ee4e48a42666ce26e30e8880d95cbb9e491162f4b9868ec1e16a37f1\nsize 8959\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-SemiBold_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3b6506d5c861eeb27575386fdfc5638e17999a01eca3eed100317305533de002\nsize 8559\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Thin.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:660bd26d26249b97875e1dcad98c279995bcc3ae2a2f3a95a7a1df71ae2cfe96\nsize 74730\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-ThinItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:b814c0746547695abc1958721d673c715a4783d7413024a0d336d8810afec699\nsize 155030\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-ThinItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:f9b4e8279d87af58943e723ae7776e722c210f73da2b3dba7b1b5dfba7a56dd7\nsize 9096\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Thin_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:35df1772e8a52af570664cb5ffcac7ccc2e5ef61672d6fd912d9d72327dfc482\nsize 8159\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Ultra.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:e2cbe30fe7f907a9275178800d1cb0520348c7664e8bf14894f4232608eaf44b\nsize 78320\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-UltraItalic.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:a6493fec0fd50c1525add29b3764f39ea0d2b00fd51eacfed41dd0d420c63247\nsize 164176\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-UltraItalic_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:760804e0434aa3917132456e13d67ba2f2c98f6977ed08f1fc47535e707f26b6\nsize 9228\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Fonts/OculusSans-Ultra_Font.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:76c5a834e42047acebba52777552f1b4d1f537addb4429e68706bfc6d56ffdbd\nsize 8511\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/HandDebug/HandDebugActor.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:44406daaad17618ee0158bcb182d52ee7b13fee81306cf30f1684c1f23efbec5\nsize 491492\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/HandDebug/HandDebugWidget.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:3aae4ad96088bd115446a24b81e1062d238947a150e564404a461ecf4f24b1ae\nsize 808054\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/HandDebug/STADIUM_Black-01.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:9cc4e46fb0b5e26007c69227011fd7b52d2ede4382c61e51a753f3c4c584f20b\nsize 12116\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/HandDebug/WidgetInvisibleMaterial.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:61ff3feacce79561a4519ee16539ee4147b974381f7b314c91606ff501728d94\nsize 88316\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Hands/HandsCharacterBase.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:16b0f518b3dddea779a9e1b4e6263af40c710c94dba7c00aa7e7bf3098c3a055\nsize 259675\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Hands/HandsCharacterHandState.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:30d88c023a9cec698c205b42103315fcdab02a8fedeeffaeb9bf853caa967d50\nsize 608331\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Hands/TeleportSelector.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:339afc81c650e5b098e391cd286c9d3f18ae5119f81c9f84775a447d3c957fa8\nsize 36134\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Hands/TutorialHand.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:d33902179993bbf5c81b590a5b0d52dc3b0e65db2ed5cf26c8d6ebfcae70d85e\nsize 58115\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/RespawnProps/FloorIdentifier.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:feb0d6df608e45ab8ccb8a769c2c5b8ed9b40f1de4610b8540abc2c8a3cea5d4\nsize 55755\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/RespawnProps/RespawnFromFloor.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5a93c3ecc1f9d612b2454ea506a725a575ad6c6465f79c82087d0f78c75d960d\nsize 58532\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Selectors/AimingGlow.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:2001a56ea1f231e9ef6eed6fe806f00961f3d7ef304f84ae717e6c261cf750e1\nsize 80386\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Selectors/DefaultAimingActor.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:5fc11596db979534c1661daeaf72afaeff80663fa0da224a1ce47abad5322664\nsize 96075\n"
  },
  {
    "path": "Plugins/OculusHandTools/Content/Selectors/DefaultAimingSphere.uasset",
    "content": "version https://git-lfs.github.com/spec/v1\noid sha256:8efa83ee2630b4e6741bd55163b0969bbc9cfe4cc30d9c03f3207d57c62bfe52\nsize 90949\n"
  },
  {
    "path": "Plugins/OculusHandTools/OculusHandTools.uplugin",
    "content": "{\n\t\"FileVersion\": 3,\n\t\"Version\": 1,\n\t\"VersionName\": \"1.1\",\n\t\"FriendlyName\": \"OculusHandTools\",\n\t\"Description\": \"\",\n\t\"Category\": \"Other\",\n\t\"CreatedBy\": \"\",\n\t\"CreatedByURL\": \"\",\n\t\"DocsURL\": \"\",\n\t\"MarketplaceURL\": \"\",\n\t\"SupportURL\": \"\",\n\t\"CanContainContent\": true,\n\t\"IsBetaVersion\": false,\n\t\"IsExperimentalVersion\": false,\n\t\"Installed\": false,\n\t\"SupportedTargetPlatforms\": [\n\t\t\"Win64\",\n\t\t\"Mac\",\n\t\t\"Android\"\n\t],\n\t\"Modules\": [\n\t\t{\n\t\t\t\"Name\": \"OculusHandPoseRecognition\",\n\t\t\t\"Type\": \"Runtime\",\n\t\t\t\"LoadingPhase\": \"Default\",\n\t\t\t\"WhitelistPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Mac\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"OculusInteractable\",\n\t\t\t\"Type\": \"Runtime\",\n\t\t\t\"LoadingPhase\": \"Default\",\n\t\t\t\"WhitelistPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Mac\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"HandInput\",\n\t\t\t\"Type\": \"Runtime\",\n\t\t\t\"LoadingPhase\": \"Default\",\n\t\t\t\"WhitelistPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Mac\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"HandTrackingFilter\",\n\t\t\t\"Type\": \"Runtime\",\n\t\t\t\"LoadingPhase\": \"Default\",\n\t\t\t\"WhitelistPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Mac\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"OculusThrowAssist\",\n\t\t\t\"Type\": \"Runtime\",\n\t\t\t\"LoadingPhase\": \"Default\",\n\t\t\t\"WhitelistPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Mac\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t}\n\t],\n\t\"Plugins\": [\n\t\t{\n\t\t\t\"Name\": \"OculusXR\",\n\t\t\t\"Enabled\": true,\n\t\t\t\"MarketplaceURL\": \"com.epicgames.launcher://ue/marketplace/product/8313d8d7e7cf4e03a33e79eb757bccba\",\n\t\t\t\"SupportedTargetPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t},\n\t\t{\n\t\t\t\"Name\": \"OculusUtils\",\n\t\t\t\"Enabled\":  true\n\t\t}\n\t]\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/README.md",
    "content": "# Meta Quest Hand Tools For UE4\n\n## Meta Quest Hand Tracking\n\nThe Meta Quest (Oculus) is the first platform to offer native hand tracking using inside-out cameras. Its hand tracking API provides raw hand bone rotations, hand location and orientation, confidence levels, and recognition of some higher-level hand poses like pinch strength and system poses.\n\nTo help developers start with raw hand bone rotations, the Meta Quest DevTech team created the Meta Quest Hand Tools plugin for UE4. It supports many common hand tracking mechanics and utilities.\n\nMost mechanics are implemented as blueprints in the [Content](./Content/) folder. More details follow below. Additional mechanics and utilities are available in these C++ modules:\n\n- [HandInput module](./README_HandInput.md)\n- [HandPoseRecognition module](./README_HandPoseRecognition.md)\n- [OculusHandTrackingFilter module](./README_HandTrackingFilter.md)\n- [OculusInteractable module](./README_Interactable.md)\n- [OculusThrowAssist module](./README_ThrowAssist.md)\n- [OculusUtils module](./README_OculusUtils.md)\n\n## Mechanics Implementations\n\nThe Content folder contains utilities for hand tracking gameplay mechanics. Examples appear in the *Hand Gameplay Showcase*.\n\nAll mechanics integrate into the *HandsCharacterHandState* component. This component is created in the construction script of *HandsCharacterBase*, which sets up references to all relevant actor components.\n\nTo add these mechanics to your project, create a new blueprint class with *HandsCharacterBase* as its parent. The showcase’s *HandsCharacter* class demonstrates this.\n\n### Throwing\n\nThe *HandsCharacterHandState* class implements throwing in the \"Throwing\" section of its event graph. Its *Throw with Assist* function calls the *Get Throw Vector* method of the *ThrowingComponent*. This component tracks hand position over a short time to calculate an accurate, predictable velocity for thrown objects.\n\n### Punching\n\nPunching is a simple, fun mechanic using hand tracking. In the \"Fist / punching\" section of the *HandsCharacterHandState* event graph, a collision sphere on the hand activates only when the hand forms a fist and no object is grabbed.\n\nThe *TetherBallBP* class in the Hand Gameplay Showcase provides an example of a \"punchable\" object.\n\n### Teleportation\n\nYou can implement a simple teleportation system using *InteractableSelector* actors from the *OculusInteractable* module. In the \"Grabbing\" section of the *HandsCharacterHandState* event graph, if a grab fails, it checks for a selected teleporter and teleports the player there. Teleporter selection happens in the \"Selection\" section of the same graph.\n\nTo restrict selection to teleporters, use the *TeleportSelector* actor instead of the standard *InteractableSelector*.\n\nAdd this to your project's *DefaultEngine.ini*:\n\n```ini\n[/Script/Engine.CollisionProfile]\n+DefaultChannelResponses=(Channel=ECC_GameTraceChannel1,DefaultResponse=ECR_Ignore,bTraceType=True,bStaticObject=False,Name=\"Interactable\")\n```\n\n### Pushing Buttons\n\nThe *PushButtonBaseBP* class handles buttons pushed by the player’s pointer finger. The button moves with the finger and plays clicking sounds when fully pressed. Use the *OnButtonPress* event to define the button’s response.\n\nThe button’s Box component uses the *FingerTip* collision channel. The *RightFingerTip* component in *HandsCharacterBase* uses the same channel.\n\nAdd this to your project's *DefaultEngine.ini*:\n\n```ini\n[/Script/Engine.CollisionProfile]\n+DefaultChannelResponses=(Channel=ECC_GameTraceChannel2,DefaultResponse=ECR_Overlap,bTraceType=False,bStaticObject=False,Name=\"FingerTip\")\n```\n\n### Two-Handed Aiming\n\nDuring *First Steps with Hands*, we found the two-handed rifle very stable and satisfying to aim. The *InteractableTwoHandedArtefact* class shows how to implement this aiming style using the grabbing system.\n\n### Example Hands for Tutorials\n\n<img width=\"256\" src=\"./Media/tutorialhand.png\" />\n\nUse the *TutorialHand* actor to display hand poses to players. Set the pose via the [*Pose String*](./README_HandPoseRecognition.md#pose-strings) property.\n\n<img width=\"256\" src=\"./Media/tutorialhand_details.png\" />\n\n## Future Improvements\n\nWe hope this showcase inspires your hand tracking projects. The team looks forward to seeing your work and improvements. We are developing ways for you to share your projects with the Meta Quest community.\n"
  },
  {
    "path": "Plugins/OculusHandTools/README_HandInput.md",
    "content": "# Hand Input\n\n## Grabbing (with Pose Override)\n\nThe *CameraHandInput* component handles grabbing. It uses the *IsGripping* and *IsPinching* methods to detect various grab poses.\n\nAdditional logic appears in *HandsCharacterHandState* under the \"Grabbing,\" \"Final attachment,\" and \"Pose\" sections of its event graph.\n\nTo keep the hand wrapped naturally around a grabbed object, you must override the hand pose. Provide the component with a \"Bone Map\" to apply poses to the hand mesh.\n\n<img width=\"512\" src=\"./Media/bonemap.png\" />\n\nAn actor must inherit from the *Interactable* class to be grabbable. Set grab pose properties using hand pose strings within this class. For an example, see the *InteractableBrick* class in the Hand Gameplay Showcase.\n\n## Finger Stabilization\n\nThe *CameraHandInput* component also stabilizes and smooths the hand skeleton. You can adjust or disable this filtering through its properties:\n\n<img width=\"512\" src=\"./Media/camerahandinput_filtering.png\" />\n"
  },
  {
    "path": "Plugins/OculusHandTools/README_HandPoseRecognition.md",
    "content": "# The OculusHandPoseRecognition Module\n\nThis module includes two [USceneComponent](https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/Engine/USceneComponent?application_version=5.6) subclasses. They recognize specific hand poses—defined by relative hand bone angles—and hand gestures—defined as sequences of hand poses. It also provides a library of useful blueprint utility nodes.\n\n## 1. Hand Pose Recognizer Component\n\nThe [UHandPoseRecognizer](./Source/OculusHandPoseRecognition/Public/HandPoseRecognizer.h) class is the core of the hand recognition system. It regularly polls the [Oculus Hands API](../../../../../Engine/Plugins/Runtime/Oculus/OculusVR/Source/OculusInput/Public/OculusXRInputFunctionLibrary.h) to record hand bone data when reliable. Wrist bone data comes from the motion controller component attached to the player's VR character.\n\n<img width=\"256\" src=\"./Media/recognizer_attachment.png\" alt=\"Recognizer attached to controller.\" />\n\nYou can use multiple recognizers per hand, each with its own set of hand poses. Group poses into one recognizer when they are mutually exclusive. For example, our showcase uses one recognizer for American Sign Language numbers and another for letters. Although ASL distinguishes the letter 'O' from the number '0', our example uses static hand poses recognized by separate recognizers to detect both simultaneously.\n\n### Pose Strings\n\nEach recognizer defines what it can recognize as a set of bone angles. Here is the thumbs-up pose for the left hand:\n\n    L T0-52-18+51 T1+13-8+30 T2+7-9-10 T3-10+21+8\n      I1+6-72+1 I2-3-108+1 I3+1-55-3\n      M1+1-77-8 M2-1-99+1 M3-6-51-8\n      R1-4-85-10 R2-5-100-1 R3-4-50-1\n      P0+15-6-25 P1+8-88+4 P2-8-94-7 P3-4-54+2\n      W+81+0+0\n\nThe description starts with 'L' or 'R' for left or right hand. It lists hand bones in order: 'T' (Thumb), 'I' (Index), 'M' (Middle), 'R' (Ring), 'P' (Pinky), and 'W' (Wrist). All bones except the wrist include a bone index starting at 0 for thumb and pinky, and 1 for the other fingers.\n\nAngles are three signed numbers for pitch, yaw, and roll, in degrees.\n\nYou can generate new pose strings using the logger described in [Using a Hand Pose Recognizer](#using-a-hand-pose-recognizer).\n\n### Computing Distance (Error) to Hand Pose\n\nMatching a hand to a reference pose starts by summing the squares of all angular differences. A lower sum means a better match. You decide what error value counts as a good match; this is covered in the next section.\n\nSometimes, you may want to ignore some bones. For example, in a \"gun\" pose, some people use only the index finger, others use both index and middle fingers. To match both with one pose, remove the middle finger bones from the reference pose.\n\nYou can also ignore specific angles (pitch, yaw, or roll) of a bone. For example, in the thumbs-up pose, you only need to know if the thumb points up or down, not its facing direction. Use +0 to ignore an angle. Use +1 or -1 to recognize angles near zero.\n\nWe have realized that for some bones are more (or less) important than others for some poses.  For that purpose, you can add between the bone identifier and the pitch/yaw/roll values a weight value.  Here's a full example that combines weights and ignores specific fingers:\n\n    L T0*3-61+1+31 T1*3+12-8+30 T2*3+6-13-10 T3*3-10+21+8\n      I1+16-22-1 I2+1-2-3 I3+3+2-2\n      R1-2-77-12 R2-5-100-1 R3-4-57-1\n      P0+15-6-25 P1+8-90-3 P2-5-76-8 P3-4+1+3\n\nThis left hand \"gun\" pose differs from the \"gun shot\" pose because the thumb is lifted away from the index. The thumb's weight is increased by 3. The middle finger is ignored to match both single and double-barrel poses. The wrist is ignored, so hand orientation of which way the hand is pointed does not matter.\n\n### Error vs Confidence Level\n\nWeights and ignoring angles or fingers affect the error range. You may also want to be more lenient with some poses.\n\nTo normalize this, each pose defines a maximum error value for 100% confidence (error at max confidence, EAMC).\n\nFor example, if you set EAMC to 2000, any error below 2000 means full confidence (1.0). An error of 4000 corresponds to 0.5 confidence, and so on.\n\n| Error | Confidence (EAMC = 2000) |\n| ----- | ------------------------ |\n| 500   | 1.00                     |\n| 2000  | 1.00                     |\n| 3000  | 0.66                     |\n| 4000  | 0.50                     |\n| 6000  | 0.33                     |\n| 8000  | 0.25                     |\n\nConfidence levels simplify handling recognition results, but raw errors remain useful during development to set EAMC.\n\n### Configuring a Hand Pose Recognizer\n\nIn the hand pose recognizer's details panel, find the *Hand Pose Recognition* section.\n\n<img width=\"256\" src=\"./Media/recognizer_config.png\" alt=\"Recognizer configuration.\" />\n\nSet the *side* value to recognize the left or right hand. Set it to none to disable the recognizer.\n\nThe recognition interval throttles recognition frequency. The default 0s runs recognition every tick.\n\nThe confidence floor sets the minimum confidence required to recognize a pose. You can set a default at the recognizer level and customize it per pose.\n\nThe damping factor controls how slowly bone updates integrate per recognition interval. By default, the latest values fully replace the current state every tick. A value of 0.2 blends 80% of the latest value with the current state.\n\nYou can configure an array of [poses](#pose-strings). There is no limit to the number of poses per recognizer.\n\nEach pose has a name, which need not be unique. You also set the encoded pose, custom confidence floor, and error at max confidence.\n\n### Using a Hand Pose Recognizer\n\nThe *Log Encoded Hand Pose* blueprint node outputs the current hand pose as an encoded string to the output log. The example below shows recognizers for both hands wired to input events.\n\n<img width=\"256\" src=\"./Media/log_encoded_pose.png\" alt=\"Log hand pose.\" />\n\nIn this example, CTRL-L and CTRL-R keyboard events trigger logging. A typical workflow is to generate hand poses, log them, then transfer or modify the strings for use in a hand pose recognizer.\n\nThe *Get Recognized Hand Pose* node retrieves the current recognition state.\n\n<img width=\"512\" src=\"./Media/get_recognized_hand_pose.png\" alt=\"Get recognized hand pose.\" />\n\nThis image from the Hand Pose Showcase shows the VRCharacter blueprint forwarding the recognizer's state to display on a projector slide.\n\nWhen a pose is recognized (confidence above the floor), you get its index, name, duration held (seconds), raw error, and confidence level.\n\n## 2. Hand Gesture Recognizer Component\n\nThe [UHandGestureRecognizer](./Source/OculusHandPoseRecognition/Public/HandGestureRecognizer.h) class handles gesture recognition.\n\nA gesture is a sequence of hand poses. Our implementation recognizes gestures only from one hand pose recognizer, which is the common case. Attach the gesture recognizer to the pose recognizer it depends on.\n\n<img width=\"256\" src=\"./Media/gesture_recognizer.png\" alt=\"Gesture recognizer.\" />\n\nIn this screenshot, *LeftFlickSwipeGestureRecognizer* attaches to *LeftFlickSwipePoseRecognizer*. This gesture moves the projector to the previous slide. Similar recognizers on the right hand move to the next slide.\n\n### Defining a Gesture as a Sequence of Poses\n\nIn the Hand Pose Showcase, a sci-fi movie gesture changes channels by pointing the right index and middle fingers at the screen, then flicking left.\n\nWe tested other gestures but found this one easy to learn and perform. First, the static hand pose recognizer:\n\n<img width=\"256\" src=\"./Media/flick_pose.png\" alt=\"Flick pose recognition.\" />\n\nIt recognizes two poses: pointing and flicking. Next, the gesture recognizer:\n\n<img width=\"256\" src=\"./Media/flick_gesture.png\" alt=\"Flick gesture recognition.\" />\n\nThe recognition interval throttles gesture recognition frequency. It defaults to every game tick.\n\nThe skipped frames value adds delay control but is experimental and may be removed.\n\nThis example recognizes one gesture, \"Flick,\" which transitions from \"Point\" to \"Flick.\" Gesture names need not be unique.\n\n*Max Transition Time* sets the maximum allowed time (seconds) between poses, including when no pose is recognized. Here, it tolerates up to 0.2 seconds between \"Point\" and \"Flick.\"\n\nYou can require holding a pose before continuing. For example:\n\n    Point/200, Flick\n\nThe user must hold \"Point\" for 200 milliseconds before \"Flick\" triggers the gesture.\n\nThe *Is Looping* flag enables recognition of looping gestures, like waving your hand.\n\n### Using a Hand Gesture Recognizer\n\nThe gesture recognizer supports *force grab* and *force throw* gestures. Here is part of the grabbing code in VRCharacter:\n\n<img width=\"512\" src=\"./Media/force_grab_gesture.png\" alt=\"Grab gesture recognition.\" />\n\nThis node returns whether a gesture was recognized. If so, you get the gesture index, name (not necessarily unique), and direction.\n\nThe gesture direction is a vector from an average location near the end of the first pose to an average location near the start of the last pose.\n\nWe also experimented with gesture duration measures. The outer duration spans from the start of the first pose to the end of the last. The inner duration spans from near the end of the first pose to near the start of the last.\n\nThe *Behavior* argument controls how the recognizer resets after recognizing a gesture. By default, only the recognized gesture resets. You can reset all gestures if needed.\n\n<img width=\"512\" src=\"./Media/gesture_state.png\" alt=\"Gesture progress.\" />\n\nThe Hand Pose Showcase shows how to detect a gesture in progress. Here, the player's hand highlights when ready to flick to the previous or next slide. The \"Flick\" gesture is queried to check if it is in progress.\n\n## 3. Hand Recognition Blueprint Library\n\n<img width=\"512\" src=\"./Media/wait_nodes.png\" alt=\"Pose and gesture wait nodes.\" />\n\n### Waiting for Hand Pose\n\nUE4 supports latent blueprint nodes that pause execution until a condition is met.\n\nThe *Wait for Hand Pose* node waits for a specified UHandPoseRecognizer instance to recognize a pose. Specify *Min Pose Duration* to require the pose be held for a minimum time. If you set a non-negative *Time to Wait*, the node exits via the *Time Out* pin if no pose is recognized within that time.\n\nWhen a pose is recognized, execution resumes at the *Pose Seen* pin, providing the recognized pose index and name.\n\n### Waiting for Gesture\n\nThe *Wait for Hand Gesture* node waits for a gesture recognition. Specify how long to wait with *Time to Wait*. The *Behavior* argument matches that in the *GetRecognizedHandGesture* node.\n\nWhen a gesture is recognized, execution resumes at the *Gesture Seen* pin, providing gesture index, name, direction, and durations.\n\nIf *Time to Wait* expires, the node exits via the *Time Out* pin.\n\nYou can loop back into the wait node to wait again, as shown in the screenshot.\n\n### Recording a Pose Range\n\n<img width=\"512\" src=\"./Media/pose_range_recording.png\" alt=\"Pose range recording.\" />\n\nThe *Record Hand Pose* blueprint records the range of hand bone angles. In the screenshot from *HandPoseShowcase*, you start and stop recording with a keyboard key. When stopped, the node outputs a report like this (recording the [royal wave](https://www.youtube.com/watch?v=n5pkDB7zEeo)):\n\n    Hand Pose Range Recorded #0\n      Thumb_0 pitch  11.58 [ -55.70 ..  -44.12]  yaw  13.78 [ -26.18 ..  -12.40]  roll  16.16 [ +44.29 ..  +60.45]\n      Thumb_1 pitch  14.34 [ +14.35 ..  +28.70]  yaw   9.21 [  -6.92 ..   +2.29]  roll   1.96 [ +30.00 ..  +31.96]\n      Thumb_2 pitch   1.39 [  -0.03 ..   +1.37]  yaw   7.99 [ -45.73 ..  -37.74]  roll   0.46 [ -10.10 ..   -9.64]\n      Thumb_3 pitch   2.92 [  -7.76 ..   -4.84]  yaw  17.45 [ -12.73 ..   +4.72]  roll   0.45 [  +9.26 ..   +9.71]\n      Index_1 pitch   5.86 [  +4.05 ..   +9.91]  yaw  10.05 [ -31.78 ..  -21.73]  roll   1.99 [  +0.69 ..   +2.69]\n      Index_2 pitch   0.46 [  -0.03 ..   +0.43]  yaw   8.70 [ -16.26 ..   -7.55]  roll   0.02 [  -3.04 ..   -3.03]\n      Index_3 pitch   0.17 [  +3.06 ..   +3.22]  yaw   5.54 [  +2.21 ..   +7.75]  roll   0.19 [  -1.82 ..   -1.63]\n     Middle_1 pitch   8.14 [  -1.27 ..   +6.87]  yaw  13.01 [ -37.69 ..  -24.67]  roll   4.12 [  -4.32 ..   -0.20]\n     Middle_2 pitch   0.20 [  +0.20 ..   +0.40]  yaw   8.38 [ -12.74 ..   -4.36]  roll   0.06 [  -1.39 ..   -1.33]\n     Middle_3 pitch   0.70 [  +0.11 ..   +0.81]  yaw   8.97 [  +0.51 ..   +9.48]  roll   0.95 [  -4.95 ..   -4.00]\n       Ring_1 pitch   5.89 [  -0.00 ..   +5.89]  yaw  18.23 [ -34.62 ..  -16.39]  roll   3.38 [  -9.76 ..   -6.39]\n       Ring_2 pitch   0.83 [  -0.93 ..   -0.10]  yaw  11.61 [ -18.00 ..   -6.39]  roll   0.16 [  -4.15 ..   -3.99]\n       Ring_3 pitch   0.06 [  -3.40 ..   -3.34]  yaw   6.10 [  -3.92 ..   +2.18]  roll   0.11 [  -0.60 ..   -0.50]\n      Pinky_0 pitch   0.00 [ +15.32 ..  +15.32]  yaw   0.00 [  -5.57 ..   -5.57]  roll   0.00 [ -24.89 ..  -24.89]\n      Pinky_1 pitch  10.01 [  -9.62 ..   +0.39]  yaw  19.92 [ -27.22 ..   -7.30]  roll   3.09 [ +10.10 ..  +13.20]\n      Pinky_2 pitch   1.93 [  +2.19 ..   +4.11]  yaw  17.13 [ -25.88 ..   -8.75]  roll   1.73 [  -7.25 ..   -5.52]\n      Pinky_3 pitch   0.05 [  -5.64 ..   -5.59]  yaw  11.15 [  -8.46 ..   +2.69]  roll   0.58 [  -0.06 ..   +0.53]\n        Wrist pitch  32.72 [  -7.06 ..  +25.67]  yaw   0.72 [-179.60 .. +179.67]  roll  28.95 [ -90.53 ..  -61.58]\n    Hand pose range total square error - full=1325.39 half=331.35 quarter=82.84\n"
  },
  {
    "path": "Plugins/OculusHandTools/README_HandTrackingFilter.md",
    "content": "# Filtered Hand Tracking\n\nThe *HandTrackingFilterComponent* improves hand-tracking in games by stabilizing hand movement when tracking quality is low or lost. Attach this component to the *MotionControllerComponent* in your Character to achieve smoother hand tracking.\n\nFor more details, see the \"Hand tracking accuracy mitigation\" section in [Adding Hand Tracking To First Steps](https://developers.meta.com/horizon/blog/adding-hand-tracking-to-first-steps/).\n"
  },
  {
    "path": "Plugins/OculusHandTools/README_Interactable.md",
    "content": "# The OculusInteractable Module\n\nThe *HandPoseShowcase* brings a proven hand gameplay mechanic to UE4. One of our internal Meta Quest teams created a demo for Unity called Tiny Castles. We chose their *force grab/throw* mechanic to showcase in this project. To implement this, we created our own far field selector, [AInteractableSelector](./Source/OculusInteractable/Public/InteractableSelector.h), designed to attach to the VR character's motion controller. It can also attach to the HMD or other components.\n\nThe interactable selector selects game actors subclassed from [AInteractable](./Source/OculusInteractable/Public/Interactable.h).\n\n## Interactable Actors\n\nAInteractable actors have two main events triggered by the selector: *BeginSelection* and *EndSelection*. The selector selects only one object at a time. This object receives notifications when selection starts and ends.\n\nTypically, selection highlights the chosen actor.\n\nThe interactable interface also defines three user events for game-specific use. The plugin does not use these events or define their meaning. In *HandPoseShowcase*, *Interaction1*, *Interaction2*, and *Interaction3* correspond to *BeginGrab* (user starts grabbing), *EndGrab* (object reaches the user's hand), and *Drop* (user releases the object), respectively.\n\nTo be selectable, an AInteractable actor must have at least one mesh that generates overlap events with the first game trace channel. In our implementation (see [AInteractableSelector](./Source/OculusInteractable/Private/InteractableSelector.cpp)), the following is defined at the top of the file:\n\n```cpp\nECollisionChannel InteractableTraceChannel = ECollisionChannel::ECC_GameTraceChannel1;\n```\n\nThis trace channel is added in the project settings under the engine collision section, as shown below.\n\n<img width=\"400\" src=\"./Media/trace_channel.png\" alt=\"Interactable trace channel.\" />\n\nIf your project uses this channel, assign `InteractableTraceChannel` to a different collision channel.\n\n## Interactable Selector\n\nThe far field selector works out of the box but is meant to be subclassed and customized. It has many configuration parameters.\n\n<img width=\"400\" src=\"./Media/selector.png\" alt=\"Default far field selector.\" />\n\nThe selector usually starts deactivated. In *HandPoseShowcase*, it activates when the user makes an open palm hand pose. The selection ray begins at the specified *Raycast Offset* relative to the selector actor and traces forward for the specified *Raycast Distance*.\n\nTwo angles control selector behavior: *Raycast Angle* and *Raycast Stickiness Angle*. The *Raycast Angle* defines the selection cone—only objects inside this cone can be selected. The *Raycast Stickiness Angle* allows the current selection to remain active even if it moves outside the stricter cone.\n\nThe selector’s visuals include an *Aiming Actor* and a particle beam effect.\n\nIf no *Aiming Actor* exists, the *Aiming Actor Class* spawns one. You can align the aiming actor with the surface hit normal and control its rotation speed using *Aiming Actor Rotation Rate*.\n\nThe *Dampening Factor* affects aiming stability. Without dampening, aiming jitters with the user's hand movements.\n"
  },
  {
    "path": "Plugins/OculusHandTools/README_OculusUtils.md",
    "content": "# The OculusUtils Module\n\n## Continuous Overlap\n\nThe *RightFingerTip* in *HandsCharacterBase* is a *ContinuousOverlapSphereComponent*, unlike the fist spheres, which use a standard *SphereComponent*.\n\n<img width=\"256\" src=\"./Media/continuousoverlapsphere.png\" />\n\nThis component prevents the fingertip from passing through buttons without triggering them. The standard *SphereComponent* only supports continuous **collision** detection (enabled with \"Use CCD\"), not continuous **overlap** detection.\n\n## Tick Until\n\n<img width=\"256\" src=\"./Media/tickuntil.png\" />\n\nThe *Tick Until* blueprint node runs once per tick from the *Completed* pin until it hits *Break*.\n"
  },
  {
    "path": "Plugins/OculusHandTools/README_ThrowAssist.md",
    "content": "# The OculusThrowAssist Module\n\nThis module implements the throwing system. It includes *ThrowingComponent*, an ActorComponent that tracks a hand and estimates throwing velocities.\n\nTo use *ThrowingComponent* in your Character or Hand, add one instance per hand to your Actor. Call *Initialize* with the component controlling the hand’s transform (usually a *MotionControllerComponent*). Call *Update* every Tick to track the transform. Finally, call *GetThrowVector* to estimate the velocity of an object thrown from the tracked hand.\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/CameraHandInput.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"CameraHandInput.h\"\n\n#include \"OculusXRInputFunctionLibrary.h\"\n#include \"Components/PoseableMeshComponent.h\"\n#include \"HandPose.h\"\n#include \"IXRTrackingSystem.h\"\n#include \"OculusXRHandComponent.h\"\n#include \"QuatUtil.h\"\n\n#define ConvertBoneToFinger UOculusXRInputFunctionLibrary::ConvertBoneToFinger\n#define GetFingerTrackingConfidence UOculusXRInputFunctionLibrary::GetFingerTrackingConfidence\n\nnamespace {\n\tbool IsOpenXRSystem()\n\t{\n\t\tconst FName SystemName(TEXT(\"OpenXR\"));\n\t\treturn GEngine->XRSystem.IsValid() && (GEngine->XRSystem->GetSystemName() == SystemName);\n\t}\n}\n\nUCameraHandInput::UCameraHandInput(FObjectInitializer const& ObjectInitializer)\n\t: Super(ObjectInitializer)\n{\n\tPrimaryComponentTick.bCanEverTick = true;\n\tPrimaryComponentTick.TickGroup = TG_PostPhysics;\n\tbIsActive = false;\n\n\tfor (auto& Quat : BoneRotations)\n\t{\n\t\tQuat = FQuat::Identity;\n\t}\n\tfor (auto& Quat : BoneVelocities)\n\t{\n\t\tQuat = FQuat::Identity;\n\t}\n\tfor (auto& Time : BoneLastFrozenTimes)\n\t{\n\t\tTime = -99999;\n\t}\n}\n\nvoid UCameraHandInput::BeginPlay()\n{\n\tSuper::BeginPlay();\n}\n\nvoid UCameraHandInput::SetHand(EControllerHand InHand)\n{\n\tHand = InHand == EControllerHand::Left ? EOculusXRHandType::HandLeft : EOculusXRHandType::HandRight;\n}\n\nvoid UCameraHandInput::SetUpBoneMap(UPoseableMeshComponent* HandMesh, TArray<FHandBoneMapping>& BoneMap)\n{\n\tif (HandMesh == nullptr)\n\t\treturn;\n\n\tauto SkinnedAsset = HandMesh->GetSkinnedAsset();\n\tif (SkinnedAsset == nullptr)\n\t\treturn;\n\n\tauto& RefSkeleton = SkinnedAsset->GetRefSkeleton();\n\t\n\t// set the bone ids for fast lookup\n\tfor (auto& BoneMapping : BoneMap)\n\t{\n\t\tauto BoneName = BoneMapping.BoneName;\n\t\tauto const BoneIndex = RefSkeleton.FindBoneIndex(BoneName);\n\t\tBoneMapping.BoneId = BoneIndex;\n\n\t\tif (BoneIndex < 0)\n\t\t{\n\t\t\tUE_LOG(LogHandInput, Warning, TEXT(\"Hand Tracking: Unable to find bone named '%s' in hand skeleton.\"),\n\t\t\t\t*BoneMapping.BoneName.ToString());\n\t\t}\n\t\telse\n\t\t{\n\t\t\tBoneMapping.ReferenceTransform = RefSkeleton.GetRefBonePose()[BoneIndex];\n\t\t}\n\t}\n}\n\nvoid UCameraHandInput::SetPoseableMeshComponent(UPoseableMeshComponent* PoseableMeshComponent)\n{\n\tHandMesh = PoseableMeshComponent;\n\tUpdateMeshVisibility();\n\n\tif (ensureMsgf(HandMesh, TEXT(\"SetPoseableMeshComponent failed\")) && ensureMsgf(HandMesh->GetSkinnedAsset(),\n\t\tTEXT(\"SetPoseableMeshComponent failed\")))\n\t{\n\t\tSetUpBoneMap(HandMesh, BoneMap);\n\t\tGripBoneId = HandMesh->GetSkinnedAsset()->GetRefSkeleton().FindBoneIndex(GripBoneName);\n\t\tOnInitializeMesh.Broadcast(this);\n\t}\n}\n\nvoid UCameraHandInput::SetupInput(UInputComponent* Input)\n{\n\tif (Hand == EOculusXRHandType::HandLeft)\n\t{\n\t\tInput->BindAxisKey(\"OculusHand_Left_IndexPinchStrength\", this, &UCameraHandInput::IndexFingerPinchUpdate);\n\t\tInput->BindAxisKey(\"OculusHand_Left_MiddlePinchStrength\", this, &UCameraHandInput::MiddleFingerPinchUpdate);\n\t\tInput->BindAxisKey(\"OculusHand_Left_RingPinchStrength\", this, &UCameraHandInput::RingFingerPinchUpdate);\n\t\tInput->BindAxisKey(\"OculusHand_Left_PinkPinchStrength\", this, &UCameraHandInput::PinkyFingerPinchUpdate);\n\t}\n\telse\n\t{\n\t\tInput->BindAxisKey(\"OculusHand_Right_IndexPinchStrength\", this, &UCameraHandInput::IndexFingerPinchUpdate);\n\t\tInput->BindAxisKey(\"OculusHand_Right_MiddlePinchStrength\", this, &UCameraHandInput::MiddleFingerPinchUpdate);\n\t\tInput->BindAxisKey(\"OculusHand_Right_RingPinchStrength\", this, &UCameraHandInput::RingFingerPinchUpdate);\n\t\tInput->BindAxisKey(\"OculusHand_Right_PinkPinchStrength\", this, &UCameraHandInput::PinkyFingerPinchUpdate);\n\t}\n\n\tbInputIsInitialized = true;\n}\n\nbool UCameraHandInput::IsActive()\n{\n\treturn UOculusXRInputFunctionLibrary::IsHandTrackingEnabled();\n}\n\nvoid UCameraHandInput::TickComponent(float DeltaTime, ELevelTick TickType,\n\tFActorComponentTickFunction* ThisTickFunction)\n{\n\tSuper::TickComponent(DeltaTime, TickType, ThisTickFunction);\n\n\t// there must be a better way to do this that isn't polling\n\tif (!bInputIsInitialized)\n\t{\n\t\tif (auto const InputComponent = GetOwner()->InputComponent)\n\t\t{\n\t\t\tSetupInput(InputComponent);\n\t\t}\n\t}\n\n\tauto const bIsActiveThisFrame = IsActive();\n\tif (bIsActive != bIsActiveThisFrame)\n\t{\n\t\tbIsActive = bIsActiveThisFrame;\n\t\tUpdateMeshVisibility();\n\t}\n\n\tif (!bIsActiveThisFrame)\n\t{\n\t\treturn;\n\t}\n\n\tUpdateTracking();\n\tUpdateSkeleton();\n\tif (bAlwaysUpdateGrab || IsTracked())\n\t{\n\t\tUpdateGrabInput();\n\t}\n\tUpdatePointingInput();\n\n\tif (HandMesh)\n\t{\n\t\tfor (auto&& BoneMapping : BoneMap)\n\t\t{\n\t\t\tBoneCache[BoneMapping.MappedBone] = HandMesh->GetSocketTransform(BoneMapping.BoneName);\n\t\t}\n\t}\n\n\t/*\n\tfloat Size = GetGrippingAxis() * 20.0f;\n\tFColor Color = IsPinching() ? FColor::Green : FColor::Red;\n\tDrawDebugSphere(GetWorld(), HandComponent->GetComponentLocation(), Size, 32, Color);\n\t*/\n\n\t// GEngine->AddOnScreenDebugMessage(-1, 0, FColor::Yellow, FString::Printf(TEXT(\"%i %f\"), bInputIsInitialized ? 1 : 0, GetHighestPinchValue()));\n}\n\nvoid UCameraHandInput::UpdateTracking()\n{\n\tauto const IsTrackedThisFrame = IsTracked();\n\tif (IsTrackedThisFrame && !WasTrackedLastFrame)\n\t{\n\t\tTimeWhenTrackingLastGained = GetWorld()->GetTimeSeconds();\n\t}\n\n\tWasTrackedLastFrame = IsTrackedThisFrame;\n}\n\nbool UCameraHandInput::IsTracked() const\n{\n\treturn UOculusXRInputFunctionLibrary::GetTrackingConfidence(Hand) == EOculusXRTrackingConfidence::High;\n}\n\nvoid UCameraHandInput::FilterBoneRotation(EOculusXRBone Bone, FQuat LastRotation, FQuat& Rotation)\n{\n\tauto const Now = GetWorld()->GetTimeSeconds();\n\tauto& LastFrozenTime = BoneLastFrozenTimes[Bone];\n\n\tauto& LastVelocity = BoneVelocities[Bone];\n\tauto const Finger = ConvertBoneToFinger(Bone);\n\n\tauto const ActualAngularDistance = LastRotation.AngularDistance(Rotation);\n\tif (MaxBoneSmoothingAngularDistance > ActualAngularDistance)\n\t{\n\t\tauto const Alpha = FMath::Max(FMath::GetRangePct(MinBoneAngularDistance, MaxBoneSmoothingAngularDistance,\n\t\t\tActualAngularDistance), 0.0f);\n\t\tRotation = FQuat::Slerp(LastRotation, Rotation, Alpha);\n\t\tLastFrozenTime = Now;\n\t}\n\telse\n\t{\n\t\tauto const Alpha = FMath::Clamp((Now - LastFrozenTime) / BoneUnfreezeTime, 0.0f, 1.0f);\n\t\tRotation = FQuat::Slerp(LastRotation, Rotation, Alpha);\n\t}\n\n\tif (bAlwaysClampBoneSpeed || GetFingerTrackingConfidence(Hand, Finger) != EOculusXRTrackingConfidence::High)\n\t{\n\t\tauto const DeltaSeconds = GetWorld()->GetDeltaSeconds();\n\t\tauto const AngularDistance = LastRotation.AngularDistance(Rotation);\n\t\tauto const MaxAngularDistance = MaxBoneAngularSpeed * DeltaSeconds;\n\t\tif (MaxAngularDistance < AngularDistance)\n\t\t{\n\t\t\tRotation = Scale(LastVelocity, DeltaSeconds) * LastRotation;\n\t\t\tLastVelocity = Scale(LastVelocity, BoneVelocityDamping);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tLastVelocity = Scale(Rotation * LastRotation.Inverse(), 1.0f / DeltaSeconds);\n\t\t}\n\t}\n}\n\nvoid UCameraHandInput::SetBoneRotation(UPoseableMeshComponent* HandMesh, FHandBoneMapping BoneMapping,\n\tFQuat BoneRotation, bool IsLeft)\n{\n\tif (BoneMapping.BoneId == -1 || HandMesh == nullptr || HandMesh->BoneSpaceTransforms.Num() <= BoneMapping.BoneId)\n\t{\n\t\treturn;\n\t}\n\tif (IsOpenXRSystem() && BoneMapping.BoneId == 0)\n\t{\n\t\tBoneMapping.RotationOffset = IsLeft ? LeftHandRootFixupRotationOpenXR : RightHandRootFixupRotationOpenXR;\n\t}\n\t\n\tBoneRotation = BoneMapping.RotationOffset * BoneRotation;\n\tBoneRotation.Normalize();\n\tHandMesh->BoneSpaceTransforms[BoneMapping.BoneId] = FTransform(\n\t\tBoneRotation,\n\t\tBoneMapping.ReferenceTransform.GetTranslation(),\n\t\tBoneMapping.ReferenceTransform.GetScale3D());\n\n\tif (BoneMapping.MappedBone == EOculusXRBone::Wrist_Root)\n\t{\n\t\tHandMesh->BoneSpaceTransforms[BoneMapping.BoneId].SetLocation(FVector::ZeroVector);\n\t}\n}\n\nvoid UCameraHandInput::UpdateSkeleton()\n{\n\tif (!HandMesh)\n\t{\n\t\treturn;\n\t}\n\n\tauto const bHasCustomGestureThisFrame = bHasCustomGesture;\n\tif (bHasCustomGesture)\n\t{\n\t\t// custom gestures have to be applied every frame, so reset the flag here\n\t\tbHasCustomGesture = false;\n\t\tbHadCustomGestureLastFrame = true;\n\t}\n\telse if (bHadCustomGestureLastFrame)\n\t{\n\t\t// reset the grip bone\n\t\tauto&& Pose = HandMesh->GetSkinnedAsset()->GetRefSkeleton().GetRefBonePose();\n\t\tif (ensureMsgf(Pose.IsValidIndex(GripBoneId), TEXT(\"GripBoneId is %i\"), GripBoneId))\n\t\t{\n\t\t\tHandMesh->BoneSpaceTransforms[GripBoneId] = Pose[GripBoneId];\n\t\t}\n\t\tbHadCustomGestureLastFrame = false;\n\t}\n\n\tif (bHasCustomGestureThisFrame && DigitsMaskedFromCustomGesture == 0)\n\t{\n\t\t// get the bone rotations anyway, since we need them for gesture detection (eg. dropping)\n\t\tfor (auto Index = 0; Index != static_cast<int>(EOculusXRBone::Bone_Max); Index += 1)\n\t\t{\n\t\t\tauto const Bone = static_cast<EOculusXRBone>(Index);\n\t\t\tauto const Rotation = UOculusXRInputFunctionLibrary::GetBoneRotation(Hand, Bone);\n\t\t\tRawLocalSpaceRotations[Bone] = Rotation;\n\t\t}\n\n\t\treturn;\n\t}\n\n\t// Update finger rotations\n\tfor (auto Index = 0; Index != static_cast<int>(EOculusXRBone::Bone_Max); Index += 1)\n\t{\n\t\tauto const Bone = static_cast<EOculusXRBone>(Index);\n\t\tauto& LastRotation = BoneRotations[Bone];\n\t\tauto Rotation = UOculusXRInputFunctionLibrary::GetBoneRotation(Hand, Bone);\n\t\tRawLocalSpaceRotations[Bone] = Rotation;\n\t\tif (bBoneRotationFilteringEnabled)\n\t\t{\n\t\t\tFilterBoneRotation(Bone, LastRotation, Rotation);\n\t\t}\n\t\tLastRotation = Rotation;\n\t}\n\n\tfor (auto BoneMapping : BoneMap)\n\t{\n\t\tif (BoneMapping.BoneId >= 0)\n\t\t{\n\t\t\t// skip updating if we're in a custom gesture and the bone's digit isn't masked out\n\t\t\tif (bHasCustomGestureThisFrame)\n\t\t\t{\n\t\t\t\tif (BoneMapping.Digit == EHandDigit::None)\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\tif ((static_cast<uint8>(BoneMapping.Digit) & DigitsMaskedFromCustomGesture) != static_cast<uint8>(BoneMapping.Digit))\n\t\t\t\t{\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tauto const BoneRotation = BoneRotations[BoneMapping.MappedBone];\n\t\t\t\n\t\t\tSetBoneRotation(HandMesh, BoneMapping, BoneRotation, GetHand() == EOculusXRHandType::HandLeft);\n\t\t}\n\t}\n\n\tHandMesh->MarkRefreshTransformDirty();\n\n\tif (bDynamicScalingEnabled)\n\t{\n\t\tauto const Scale = UOculusXRInputFunctionLibrary::GetHandScale(Hand);\n\t\tHandMesh->SetRelativeScale3D(FVector(Scale));\n\t}\n}\n\nvoid UCameraHandInput::UpdateGrabInput()\n{\n\t// Hand pose grab input\n\tauto const GrabAxisValue = GetGrippingAxis();\n\tif (bIsInGrabPose)\n\t{\n\t\tif (GrabAxisValue < ReleaseThresholdForGrip)\n\t\t{\n\t\t\tbIsInGrabPose = false;\n\t\t}\n\t}\n\telse if (GrabAxisValue > GrabThresholdForGrip)\n\t{\n\t\tbIsInGrabPose = true;\n\t}\n\n\t// pinch axis input\n\tauto const HighestPinchValueThisFrame = GetHighestPinchValue();\n\n\tif (bIsPinching)\n\t{\n\t\tif (HighestPinchValueLastFrame > ReleaseThreshold && HighestPinchValueThisFrame <= ReleaseThreshold)\n\t\t{\n\t\t\tbIsPinching = false;\n\t\t}\n\t}\n\telse\n\t{\n\t\tif (HighestPinchValueLastFrame < GrabThreshold && HighestPinchValueThisFrame >= GrabThreshold)\n\t\t{\n\t\t\tbIsPinching = true;\n\t\t}\n\t}\n\n\tHighestPinchValueLastFrame = HighestPinchValueThisFrame;\n}\n\nvoid UCameraHandInput::UpdatePointingInput()\n{\n\tbIsPointing = GetPointingAxis() > 0.5f;\n}\n\nvoid UCameraHandInput::IndexFingerPinchUpdate(float Value) { FingerPinchUpdate(0, Value); }\nvoid UCameraHandInput::MiddleFingerPinchUpdate(float Value) { FingerPinchUpdate(1, Value); }\nvoid UCameraHandInput::RingFingerPinchUpdate(float Value) { FingerPinchUpdate(2, Value); }\nvoid UCameraHandInput::PinkyFingerPinchUpdate(float Value) { FingerPinchUpdate(3, Value); }\n\nvoid UCameraHandInput::FingerPinchUpdate(int FingerIndex, float Value)\n{\n\t// don't update if the hand, finger, or thumb have low confidence tracking\n\tif (!IsTracked())\n\t{\n\t\treturn;\n\t}\n\n\tif (GetFingerTrackingConfidence(Hand, EOculusXRFinger::Thumb) ==\n\t\tEOculusXRTrackingConfidence::Low)\n\t{\n\t\treturn;\n\t}\n\n\tauto const OculusFinger = static_cast<EOculusXRFinger>(FingerIndex + 1);\n\tif (GetFingerTrackingConfidence(Hand, OculusFinger) == EOculusXRTrackingConfidence::Low)\n\t{\n\t\treturn;\n\t}\n\n\tFingerPinchValues[FingerIndex] = Value;\n}\n\nfloat UCameraHandInput::GetHighestPinchValue()\n{\n\t// only look at the index and middle fingers\n\treturn FMath::Max(FingerPinchValues[0], FingerPinchValues[1]);\n}\n\nFTransform UCameraHandInput::GetGripBoneTransform(EBoneSpaces::Type BoneSpace)\n{\n\tauto const TransformSpace = BoneSpace == EBoneSpaces::ComponentSpace ?\n\t\tRTS_Component :\n\t\tRTS_World;\n\n\tif (HandMesh)\n\t{\n\t\treturn HandMesh->GetSocketTransform(GripBoneName, TransformSpace);\n\t}\n\treturn FTransform::Identity;\n}\n\nEOculusXRHandType UCameraHandInput::GetHand() const\n{\n\treturn Hand;\n}\n\nUPoseableMeshComponent* UCameraHandInput::GetMesh() const\n{\n\treturn HandMesh;\n}\n\nFName UCameraHandInput::GetGripBoneName() const\n{\n\treturn GripBoneName;\n}\n\nvoid UCameraHandInput::ForceMeshHidden(bool Hidden)\n{\n\tif (Hidden != bForceMeshHidden)\n\t{\n\t\tbForceMeshHidden = Hidden;\n\t\tUpdateMeshVisibility();\n\t}\n}\n\nvoid UCameraHandInput::UpdateMeshVisibility() const\n{\n\tauto const bVisible = bIsActive && !bForceMeshHidden;\n\tif (HandMesh)\n\t{\n\t\tHandMesh->SetHiddenInGame(!bVisible);\n\t}\n}\n\n// these are all a mess and we really need a generic (data-driven) system to handle this\nfloat UCameraHandInput::GetPointingAxis()\n{\n\tauto const SummedJoints =\n\t\tRawLocalSpaceRotations[EOculusXRBone::Index_1].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Index_2].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Index_3].GetNormalized().GetAngle();\n\n\treturn FMath::GetMappedRangeValueClamped(PointingAxisJointRotationRange, FVector2D(0.f, 1.f), SummedJoints);\n}\n\nfloat UCameraHandInput::GetGrippingAxis()\n{\n\tauto const SummedJoints =\n\t\tRawLocalSpaceRotations[EOculusXRBone::Middle_1].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Middle_2].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Middle_3].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Ring_1].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Ring_2].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Ring_3].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Pinky_1].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Pinky_2].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Pinky_3].GetNormalized().GetAngle();\n\n\treturn FMath::GetMappedRangeValueClamped(GrippingAxisJointRotationRange, FVector2D(0.f, 1.f), SummedJoints);\n}\n\nfloat UCameraHandInput::GetThumbUpAxis()\n{\n\tauto const SummedJoints =\n\t\tRawLocalSpaceRotations[EOculusXRBone::Thumb_0].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Thumb_1].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Thumb_2].GetNormalized().GetAngle() +\n\t\tRawLocalSpaceRotations[EOculusXRBone::Thumb_3].GetNormalized().GetAngle();\n\n\treturn FMath::GetMappedRangeValueClamped(ThumbUpAxisJointRotationRange, FVector2D(0.f, 1.f), SummedJoints);\n}\n\nbool UCameraHandInput::ApplyPoseToMesh(\n\tFString PoseString, UPoseableMeshComponent* HandMesh,\n\tTArray<FHandBoneMapping> const& BoneMappings, bool IsLeft)\n{\n\tauto BoneToRecognizedBone = [](EOculusXRBone Bone)\n\t{\n\t\tswitch (Bone)\n\t\t{\n\t\tcase EOculusXRBone::Wrist_Root: return Wrist;\n\t\tcase EOculusXRBone::Thumb_0: return Thumb_0;\n\t\tcase EOculusXRBone::Thumb_1: return Thumb_1;\n\t\tcase EOculusXRBone::Thumb_2: return Thumb_2;\n\t\tcase EOculusXRBone::Thumb_3: return Thumb_3;\n\t\tcase EOculusXRBone::Index_1: return Index_1;\n\t\tcase EOculusXRBone::Index_2: return Index_2;\n\t\tcase EOculusXRBone::Index_3: return Index_3;\n\t\tcase EOculusXRBone::Middle_1: return Middle_1;\n\t\tcase EOculusXRBone::Middle_2: return Middle_2;\n\t\tcase EOculusXRBone::Middle_3: return Middle_3;\n\t\tcase EOculusXRBone::Ring_1: return Ring_1;\n\t\tcase EOculusXRBone::Ring_2: return Ring_2;\n\t\tcase EOculusXRBone::Ring_3: return Ring_3;\n\t\tcase EOculusXRBone::Pinky_0: return Pinky_0;\n\t\tcase EOculusXRBone::Pinky_1: return Pinky_1;\n\t\tcase EOculusXRBone::Pinky_2: return Pinky_2;\n\t\tcase EOculusXRBone::Pinky_3: return Pinky_3;\n\t\tdefault: return static_cast<ERecognizedBone>(-1);\n\t\t}\n\t};\n\n\tFHandPose Pose;\n\tPose.CustomEncodedPose = PoseString;\n\tif (HandMesh == nullptr || Pose.Decode() == false)\n\t{\n\t\treturn false;\n\t}\n\n\tfor (auto Mapping : BoneMappings)\n\t{\n\t\tauto const Bone = BoneToRecognizedBone(Mapping.MappedBone);\n\t\tif (Bone != static_cast<ERecognizedBone>(-1))\n\t\t{\n\t\t\tauto Rotator = Bone == Wrist ? FRotator::ZeroRotator : Pose.GetRotator(Bone);\n\t\t\tSetBoneRotation(HandMesh, Mapping, Rotator.Quaternion(), IsLeft);\n\t\t}\n\t}\n\n\tHandMesh->RefreshBoneTransforms();\n\treturn true;\n}\n\nbool UCameraHandInput::SetPose(FString PoseString)\n{\n\tif (ApplyPoseToMesh(PoseString, HandMesh, BoneMap, GetHand() == EOculusXRHandType::HandLeft))\n\t{\n\t\tbHasCustomGesture = true;\n\t\treturn true;\n\t}\n\n\treturn false;\n}\n\n#undef ConvertBoneToFinger\n#undef GetFingerTrackingConfidence\n\n#ifdef EOculusXRFinger\n#undef EOculusXRFinger\n#endif\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/CameraHandInput.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"HandInput.h\"\n\n#include \"OculusXRInputFunctionLibrary.h\"\n#include \"EnumMap.h\"\n\n#include \"CameraHandInput.generated.h\"\n\nclass UTransformBufferComponent;\n\nnamespace OculusInput\n{\n\tenum class EOculusXRHandButton;\n}\n\nclass UPoseableMeshComponent;\nclass UThrowingComponent;\nclass UHandComponentBase;\n\nUENUM(BlueprintType, meta = (Bitflags, UseEnumValuesAsMaskValuesInEditor = \"true\"))\nenum class EHandDigit : uint8\n{\n\tNone = 0,\n\tThumb = 1 << 0,\n\tIndex = 1 << 1,\n\tMiddle = 1 << 2,\n\tRing = 1 << 3,\n\tPinky = 1 << 4,\n\n\tCount = 6\n};\n\nUSTRUCT(BlueprintType)\nstruct HANDINPUT_API FHandBoneMapping\n{\n\tGENERATED_BODY()\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere)\n\tFName BoneName;\n\n\tUPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient)\n\tint BoneId{};\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere)\n\tEOculusXRBone MappedBone{};\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere)\n\tFQuat RotationOffset = FQuat::Identity;\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere)\n\tEHandDigit Digit{};\n\n\tUPROPERTY(BlueprintReadOnly, VisibleAnywhere, Transient)\n\tFTransform ReferenceTransform;\n};\n\nUCLASS(meta = (BlueprintSpawnableComponent))\nclass HANDINPUT_API UCameraHandInput : public UActorComponent, public IHandInput\n{\n\tGENERATED_BODY()\n\npublic:\n\tUCameraHandInput(FObjectInitializer const& ObjectInitializer);\n\n\tvirtual void BeginPlay() override;\n\n\tUFUNCTION(BlueprintCallable)\n\tvoid SetPoseableMeshComponent(UPoseableMeshComponent* PoseableMeshComponent);\n\tvoid SetupInput(UInputComponent* Input);\n\tvoid ForceMeshHidden(bool Hidden);\n\n\tUFUNCTION(BlueprintCallable)\n\tstatic bool ApplyPoseToMesh(\n\t\tFString PoseString, UPoseableMeshComponent* HandMesh,\n\t\tTArray<FHandBoneMapping> const& BoneMappings, bool IsLeft);\n\n\tUFUNCTION(BlueprintCallable)\n\tstatic void SetUpBoneMap(UPoseableMeshComponent* HandMesh, UPARAM(ref) TArray<FHandBoneMapping>& BoneMap);\n\n\t// IHandInput\n\tUFUNCTION(BlueprintCallable)\n\tvirtual void SetHand(EControllerHand InHand) override;\n\tvirtual bool IsActive() override;\n\tvirtual FTransform GetGripBoneTransform(EBoneSpaces::Type BoneSpace) override;\n\tvirtual bool IsPointing() override { return bIsPointing; }\n\tvirtual EHandTrackingMode GetTrackingMode() override { return EHandTrackingMode::Camera; }\n\t// ~IHandInput\n\n\tUFUNCTION(BlueprintPure)\n\tEOculusXRHandType GetHand() const;\n\n\tUFUNCTION(BlueprintPure)\n\tUPoseableMeshComponent* GetMesh() const;\n\n\tUFUNCTION(BlueprintPure)\n\tFName GetGripBoneName() const;\n\n\tUPROPERTY(EditAnywhere, Category = \"Hand Input\")\n\tFName GripBoneName;\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tbool bDynamicScalingEnabled = true;\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere, Category = \"Hand Input\")\n\tfloat GrabThreshold = 0.8f;\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere, Category = \"Hand Input\")\n\tfloat ReleaseThreshold = 0.3f;\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere, Category = \"Hand Input\")\n\tfloat GrabThresholdForGrip = 0.6f;\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere, Category = \"Hand Input\")\n\tfloat ReleaseThresholdForGrip = 0.3f;\n\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere, Category = \"Hand Input\")\n\tfloat ThrowGestureReleaseFactor = 1.0f;\n\n\t// How long to delay before releasing the grip when a previously gripped hand loses tracking and is\n\t// regained in a non-gripped pose\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere, Category = \"Hand Input\")\n\tfloat GripReleaseDelayOnTrackingResumed = .5f;\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tbool bDropDelayEnabled = true;\n\n\tUPROPERTY(EditAnywhere, BlueprintReadOnly, Category = \"Hand Input\")\n\tTArray<FHandBoneMapping> BoneMap;\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tbool bAlwaysUpdateGrab = false;\n\n\tUPROPERTY(BlueprintReadWrite, Category = \"Hand Input\", Transient)\n\tUPoseableMeshComponent* HandMesh = nullptr;\n\n\t// grip release delay with good tracking\n\tUPROPERTY(BlueprintReadOnly, EditAnywhere, Category = \"Hand Input\")\n\tfloat GlobalDropDelay = 0.05f;\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tbool bGlobalDropDelayEnabled = true;\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tfloat GlobalDropDelayReductionFactorWhenThrowing = 2.f;\n\n\t/// minimum distance for a bone to not be frozen\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Bone Jitter Mitigation\")\n\tfloat MinBoneAngularDistance = 0.01f;\n\n\t/// maximum distance for a bone to be smoothed\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Bone Jitter Mitigation\")\n\tfloat MaxBoneSmoothingAngularDistance = 0.25f;\n\n\t/// amount of time to smooth back from frozen to unfrozen\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Bone Jitter Mitigation\")\n\tfloat BoneUnfreezeTime = 0.1f;\n\n\t/// max speed for a bone to extrapolate\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Bone Data Filter\")\n\tfloat MaxBoneAngularSpeed = 0.5f;\n\n\t/// when enabled, bone speed will be clamped even if data is high quality\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Bone Data Filter\")\n\tbool bAlwaysClampBoneSpeed = false;\n\n\t/// per-frame damping of extrapolated bone speed\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Bone Data Filter\")\n\tfloat BoneVelocityDamping = 0.95f;\n\n\t/// when enabled, bone filtering will be active\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Bone Data Filter\")\n\tbool bBoneRotationFilteringEnabled = true;\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tFVector2D PointingAxisJointRotationRange = FVector2D(14.f, 18.f);\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tFVector2D GrippingAxisJointRotationRange = FVector2D(55.f, 44.f);\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Hand Input\")\n\tFVector2D ThumbUpAxisJointRotationRange = FVector2D(21.8f, 22.7f);\n\n\tDECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnInitializeMesh, UCameraHandInput *, CameraHandInput);\n\n\tUPROPERTY(BlueprintAssignable, Category = \"Hand Input\")\n\tFOnInitializeMesh OnInitializeMesh;\n\n\tUFUNCTION(BlueprintPure)\n\tbool IsPinching() const { return bIsPinching; }\n\n\tUFUNCTION(BlueprintPure)\n\tbool IsInGrabPose() const { return bIsInGrabPose; }\n\n\tUFUNCTION(BlueprintPure)\n\tFTransform GetBoneTransformWorld(EOculusXRBone Bone) { return BoneCache[Bone]; }\n\n\tUFUNCTION(BlueprintPure)\n\tbool IsTracked() const;\n\n\tUFUNCTION(BlueprintPure)\n\tfloat GetPointingAxis();\n\n\tUFUNCTION(BlueprintPure)\n\tfloat GetGrippingAxis();\n\n\tUFUNCTION(BlueprintPure)\n\tfloat GetThumbUpAxis();\n\n\tUFUNCTION(BlueprintCallable)\n\tbool SetPose(FString PoseString);\n\nprotected:\n\tvirtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;\n\nprivate:\n\tEOculusXRHandType Hand = EOculusXRHandType::None;\n\tbool bIsActive = false;\n\n\tvoid UpdateTracking();\n\tbool WasTrackedLastFrame = false;\n\tfloat TimeWhenTrackingLastGained = -1;\n\n\tvoid FilterBoneRotation(EOculusXRBone Bone, FQuat LastRotation, FQuat& Rotation);\n\tstatic void SetBoneRotation(UPoseableMeshComponent* HandMesh, FHandBoneMapping BoneMapping, FQuat BoneRotation, bool IsLeft);\n\tvoid UpdateSkeleton();\n\n\tbool bInputIsInitialized = false;\n\tvoid UpdateGrabInput();\n\tfloat GetHighestPinchValue();\n\tvoid IndexFingerPinchUpdate(float Value);\n\tvoid MiddleFingerPinchUpdate(float Value);\n\tvoid RingFingerPinchUpdate(float Value);\n\tvoid PinkyFingerPinchUpdate(float Value);\n\tvoid FingerPinchUpdate(int FingerIndex, float Value);\n\tfloat FingerPinchValues[4] = {0};\n\tbool bIsPinching = false;\n\tfloat HighestPinchValueLastFrame = 0;\n\tbool bIsInGrabPose = false;\n\n\tvoid UpdatePointingInput();\n\tbool bIsPointing = false;\n\n\tint GripBoneId = -1;\n\n\tbool bHasCustomGesture = false;\n\tbool bHadCustomGestureLastFrame = false;\n\tuint8 DigitsMaskedFromCustomGesture = 0;\n\n\tbool bForceMeshHidden = false;\n\tvoid UpdateMeshVisibility() const;\n\n\t// cache bone transforms from hand tracking for use by gameplay code\n\tTEnumMap<EOculusXRBone, FTransform> BoneCache;\n\tTEnumMap<EOculusXRBone, FQuat> RawLocalSpaceRotations;\n\n\t// cache bone rotations from hand tracking for smoothing\n\tTEnumMap<EOculusXRBone, FQuat> BoneRotations;\n\tTEnumMap<EOculusXRBone, FQuat> BoneVelocities;\n\tTEnumMap<EOculusXRBone, float> BoneLastFrozenTimes;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/EnumMap.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"Containers/StaticArray.h\"\n\ntemplate <typename EEnum, typename InElementType, int ExtraElements = 0>\nclass TEnumMap : public TStaticArray<InElementType, ExtraElements + static_cast<int>(EEnum::Invalid)>\n{\n\tusing Super = TStaticArray<InElementType, ExtraElements + static_cast<int>(EEnum::Invalid)>;\n\npublic:\n\tInElementType& operator[](EEnum Index)\n\t{\n\t\treturn (*static_cast<Super*>(this))[static_cast<uint32>(Index)];\n\t}\n\n\tInElementType const& operator[](EEnum Index) const\n\t{\n\t\treturn (*static_cast<Super const*>(this))[static_cast<uint32>(Index)];\n\t}\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/HandInput.Build.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\n\npublic class HandInput : ModuleRules\n{\n\tpublic HandInput(ReadOnlyTargetRules Target) : base(Target)\n\t{\n\t\tPCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;\n\t\t//CppStandard = CppStandardVersion.Latest;\n\n\t\tPublicDependencyModuleNames.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t\"Core\",\n\t\t\t\t\"CoreUObject\",\n\t\t\t\t\"Engine\",\n\t\t\t\t\"InputCore\",\n\t\t\t\t\"OculusXRInput\",\n\t\t\t\t\"HeadMountedDisplay\"\n\t\t\t}\n\t\t);\n\n\t\tPrivateDependencyModuleNames.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t\"OculusHandPoseRecognition\",\n\t\t\t\t\"OculusUtils\"\n\t\t\t}\n\t\t);\n\n\t\tPublicIncludePaths.AddRange(new string[] {\n\t\t\t// TODO(els): Relative to UE4\\Source... not sure why this fixes the build since we do depend on Core.\n\t\t\t// \"Runtime/Core/Public/Containers\",\n\t\t\t// \"Runtime/CoreUObject/Public/UObject\",\n\t\t});\n\n\t\tPrivateIncludePaths.AddRange(new string[] {\n\t\t});\n\n\t\tIncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6;\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/HandInput.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandInput.h\"\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/HandInput.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"HandInput.generated.h\"\n\nUENUM(BlueprintType)\nenum class EHandTrackingMode : uint8\n{\n\tController,\n\tCamera,\n\tUnknown\n};\n\nUINTERFACE()\nclass HANDINPUT_API UHandInput : public UInterface\n{\n\tGENERATED_BODY()\n};\n\nclass HANDINPUT_API IHandInput\n{\n\tGENERATED_BODY()\n\t\npublic:\n\tvirtual void SetHand(EControllerHand Hand) = 0;\n\tvirtual bool IsActive() = 0;\n\tvirtual FTransform GetGripBoneTransform(EBoneSpaces::Type BoneSpace) = 0;\n\tvirtual bool IsPointing() = 0;\n\tvirtual EHandTrackingMode GetTrackingMode() = 0;\n};\n\nDECLARE_LOG_CATEGORY_EXTERN(LogHandInput, Log, All);\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/HandInputModule.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandInputModule.h\"\n#include \"HandInput.h\"\n\n#define LOCTEXT_NAMESPACE \"FHandInputModule\"\n\n#include \"OculusDeveloperTelemetry.h\"\n\nOCULUS_TELEMETRY_LOAD_MODULE(\"Unreal-HandInput\");\n\nbool FHandInputModule::IsGameModule() const\n{\n\treturn true;\n}\n\n#undef LOCTEXT_NAMESPACE\n\nIMPLEMENT_MODULE(FHandInputModule, HandInput);\n\nDEFINE_LOG_CATEGORY(LogHandInput);\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/HandInputModule.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"Modules/ModuleInterface.h\"\n\nclass HANDINPUT_API FHandInputModule : public IModuleInterface\n{\npublic:\n\tvirtual bool IsGameModule() const override;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandInput/QuatUtil.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"Math/Quat.h\"\n\nstatic FORCEINLINE FQuat Scale(FQuat Rotation, float S)\n{\n\treturn FQuat::Slerp(FQuat::Identity, Rotation, S);\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilter.Build.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\n\npublic class HandTrackingFilter : ModuleRules\n{\n\tpublic HandTrackingFilter(ReadOnlyTargetRules Target) : base(Target)\n\t{\n\t\tPCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;\n\t\t//CppStandard = CppStandardVersion.Latest;\n\n\t\tPublicDependencyModuleNames.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t\"Core\",\n\t\t\t\t\"CoreUObject\",\n\t\t\t\t\"Engine\",\n\t\t\t\t\"InputCore\",\n\t\t\t\t\"OculusXRInput\",\n\t\t\t\t\"HeadMountedDisplay\"\n\t\t\t}\n\t\t);\n\n\t\tPrivateDependencyModuleNames.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t\"OculusUtils\",\n\t\t\t\t\"XRBase\",\n\t\t\t}\n\t\t);\n\n\t\tPublicIncludePaths.AddRange(new string[] {\n\t\t\t// TODO(els): Relative to UE4\\Source... not sure why this fixes the build since we do depend on Core.\n\t\t\t// \"Runtime/Core/Public/Containers\",\n\t\t\t// \"Runtime/CoreUObject/Public/UObject\",\n\t\t});\n\n\t\tPrivateIncludePaths.AddRange(new string[] {\n\t\t});\n\n\t\tIncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6;\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilter.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandTrackingFilter.h\"\n\n#define LOCTEXT_NAMESPACE \"FHandTrackingFilterModule\"\n\n#include \"OculusDeveloperTelemetry.h\"\n\nOCULUS_TELEMETRY_LOAD_MODULE(\"Unreal-HandTrackingFilter\");\n\nbool FHandTrackingFilterModule::IsGameModule() const\n{\n\treturn true;\n}\n\n#undef LOCTEXT_NAMESPACE\n\nIMPLEMENT_MODULE(FHandTrackingFilterModule, HandTrackingFilter);\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilter.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"Modules/ModuleInterface.h\"\n\nclass HANDTRACKINGFILTER_API FHandTrackingFilterModule : public IModuleInterface\n{\npublic:\n\tvirtual bool IsGameModule() const override;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilterComponent.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandTrackingFilterComponent.h\"\n\n#include \"MotionControllerComponent.h\"\n#include \"OculusXRInputFunctionLibrary.h\"\n#include \"Camera/CameraComponent.h\"\n#include \"QuatUtil.h\"\n#include \"XRMotionControllerBase.h\"\n\nDECLARE_LOG_CATEGORY_EXTERN(LogHandTrackingFilter, Log, All);\n\nDEFINE_LOG_CATEGORY(LogHandTrackingFilter);\n\n// use real time because this can get hit multiple times per frame\n#define NOW FPlatformTime::Seconds()\n\nUHandTrackingFilterComponent::UHandTrackingFilterComponent()\n{\n\tbAutoActivate = true;\n}\n\nvoid UHandTrackingFilterComponent::BeginPlay()\n{\n\tSuper::BeginPlay();\n\n\tLastFrameData = FHandTrackingFilterData{NOW, GetComponentTransform()};\n\n\tif (auto Controller = Cast<UMotionControllerComponent>(GetAttachParent()))\n\t{\n\t\tUOculusXRInputFunctionLibrary::HandMovementFilter.AddWeakLambda(this,\n\t\t\t[this, ThisHand = Controller->GetTrackingSource()]\n\t\t(\n\t\t\tEControllerHand Hand,\n\t\t\tFVector* Location,\n\t\t\tFRotator* Orientation,\n\t\t\tbool* Success\n\t\t)\n\t\t\t{\n\t\t\t\tif (Hand == ThisHand && UOculusXRInputFunctionLibrary::IsHandTrackingEnabled())\n\t\t\t\t{\n\t\t\t\t\tif (IsInGameThread() && PreFilterComponent)\n\t\t\t\t\t{\n\t\t\t\t\t\tPreFilterComponent->SetActive(*Success);\n\t\t\t\t\t\tif (*Success)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tPreFilterComponent->SetRelativeRotation(*Orientation);\n\t\t\t\t\t\t\tPreFilterComponent->SetRelativeLocation(*Location);\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tDoFiltering(*Location, *Orientation, !*Success);\n\t\t\t\t\t*Success = true;\n\t\t\t\t}\n\t\t\t});\n\t}\n}\n\nvoid UHandTrackingFilterComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)\n{\n\tUOculusXRInputFunctionLibrary::HandMovementFilter.RemoveAll(this);\n\n\tSuper::EndPlay(EndPlayReason);\n}\n\nEHandTrackingDataQuality UHandTrackingFilterComponent::GetDataQualityOverride() const\n{\n\tif (bIgnoreConfidence)\n\t\treturn EHandTrackingDataQuality::None;\n\n\tif (auto const Controller = Cast<UMotionControllerComponent>(GetAttachParent()))\n\t{\n\t\tauto Hand = EControllerHand::Left;\n\t\tFXRMotionControllerBase::GetHandEnumForSourceName(Controller->MotionSource, Hand);\n\t\tauto const DeviceHand = Hand == EControllerHand::Left ? EOculusXRHandType::HandLeft : EOculusXRHandType::HandRight;\n\t\treturn Controller->IsTracked() ?\n\t\t\tUOculusXRInputFunctionLibrary::GetTrackingConfidence(DeviceHand) == EOculusXRTrackingConfidence::High ?\n\t\t\tEHandTrackingDataQuality::Good :\n\t\t\tEHandTrackingDataQuality::None :\n\t\t\tEHandTrackingDataQuality::Bad;\n\t}\n\n\treturn EHandTrackingDataQuality::None;\n}\n\nFVector UHandTrackingFilterComponent::SmoothPosition(FVector StartPos, FVector TargetPos)\n{\n\tif (SmoothPositionFactor > 0.99f)\n\t{\n\t\t// Updating disabled\n\t\treturn StartPos;\n\t}\n\n\tauto const Diff = TargetPos - StartPos;\n\tauto const Dist = Diff.Size();\n\n\tif (Dist < MinSmoothPositionDistance)\n\t{\n\t\t// Not enough of a change to update\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - SmoothPos - Not enough of a change to update\"), *GetName());\n\t\tLastGoodVelocity = FVector::ZeroVector;\n\t\treturn StartPos;\n\t}\n\n\tif (Dist >= MaxSmoothPositionDistance)\n\t{\n\t\t// Clamp max distance from target\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - SmoothPos - Clamp max distance from target\"), *GetName());\n\t\tauto const Dir = Diff / Dist;\n\t\tauto const MoveDist = Dist - MaxSmoothPositionDistance;\n\t\treturn StartPos + Dir * MoveDist;\n\t}\n\n\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - SmoothPos - Smooth\"), *GetName());\n\tauto const SmoothFactor = 1.0f - SmoothPositionFactor;\n\treturn FMath::Lerp(StartPos, TargetPos, SmoothFactor);\n}\n\nvoid UHandTrackingFilterComponent::SetPreFilterComponent(USceneComponent* Component)\n{\n\tPreFilterComponent = Component;\n}\n\nFQuat UHandTrackingFilterComponent::SmoothRotation(FQuat StartRot, FQuat TargetRot)\n{\n\tif (SmoothRotationFactorMin > 0.99f)\n\t{\n\t\t// Updating disabled\n\t\treturn StartRot;\n\t}\n\n\tauto const CosAngle = StartRot | TargetRot;\n\tif (CosAngle > SmoothRotationDotMax)\n\t{\n\t\t// Not enough of a change to update\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - SmoothRotation - Not enough of a change to update\"), *GetName());\n\t\treturn StartRot;\n\t}\n\n\tfloat SmoothFactor;\n\tif (CosAngle <= SmoothRotationDotMin)\n\t{\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - SmoothRotation - CosAngle %f <= SmoothRotationDotMin %f\"), *GetName(), CosAngle, SmoothRotationDotMin);\n\t\tSmoothFactor = 1.0f - SmoothRotationFactorMin;\n\t}\n\telse\n\t{\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - SmoothRotation - CosAngle %f > SmoothRotationDotMin %f\"), *GetName(), CosAngle, SmoothRotationDotMin);\n\t\tauto const Weight = (CosAngle - SmoothRotationDotMin) / (SmoothRotationDotMax - SmoothRotationDotMin);\n\t\tSmoothFactor = 1.0f - FMath::Lerp(SmoothRotationFactorMin,\n\t\t\tSmoothRotationFactorMax, Weight);\n\t}\n\n\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - SmoothRotation - %f\"), *GetName(), SmoothFactor);\n\treturn FQuat::Slerp(StartRot, TargetRot, SmoothFactor);\n}\n\nbool UHandTrackingFilterComponent::DoFirstPassFilter(\n\tFHandTrackingFilterData const& LastData,\n\tFHandTrackingFilterData const& ThisFrameInitData,\n\tFTransform& NewTransform)\n{\n\tauto const LastLocation = LastData.Transform.GetLocation();\n\tauto const NewLocation = ThisFrameInitData.Transform.GetLocation();\n\tauto const DeltaLocation = NewLocation - LastLocation;\n\tauto const DistanceSquared = DeltaLocation.SizeSquared();\n\n\t// if there hasn't been a tracking update, extrapolate\n\tif (DistanceSquared < MinTrackingDistance * MinTrackingDistance)\n\t{\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - DoFirstPassFilter - if the location hasn't changed (%f), there hasn't been a tracking update\"), *GetName(), DistanceSquared);\n\t\tNewTransform = LastSetTransform;\n\t\treturn true;\n\t}\n\n\tauto const NewRotation = ThisFrameInitData.Transform.GetRotation();\n\n\tauto const SmoothedPosition = SmoothPosition(LastSetTransform.GetLocation(), NewLocation);\n\tNewTransform.SetLocation(SmoothedPosition);\n\n\tauto const SmoothedRotation = SmoothRotation(LastSetTransform.GetRotation(), NewRotation);\n\tNewTransform.SetRotation(SmoothedRotation);\n\n\treturn false;\n}\n\nFTransform UHandTrackingFilterComponent::DoFilteringImpl(FTransform HandTransform, bool bForceBadData)\n{\n\tauto CalculatedData = FHandTrackingFilterCalculatedData();\n\n\tauto const LastData = LastFrameData;\n\tauto ThisFrameInitData = FHandTrackingFilterData{NOW, HandTransform};\n\n\tauto const DeltaTime = ThisFrameInitData.Time - LastData.Time;\n\n\tauto MitigatedTransform = ThisFrameInitData.Transform;\n\tauto const EarlyOut = DoFirstPassFilter(LastData, ThisFrameInitData, MitigatedTransform);\n\tif (!bForceBadData && EarlyOut)\n\t{\n\t\tLastSetTransform = MitigatedTransform;\n\t\treturn MitigatedTransform;\n\t}\n\n\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - DoFilteringImpl - dt %lf\"), *GetName(), DeltaTime);\n\n\tauto& Data = LastFrameData;\n\tData = ThisFrameInitData;\n\n\tauto const DeltaLocation = MitigatedTransform.GetLocation() - LastSetTransform.GetLocation();\n\tauto const Distance = DeltaLocation.Size();\n\tCalculatedData.Distance = Distance;\n\n\tData.Velocity = DeltaLocation / DeltaTime;\n\tCalculatedData.Velocity = Data.Velocity;\n\n\tauto const DeltaVelocity = Data.Velocity - LastData.Velocity;\n\tauto const Acceleration = CalculatedData.Acceleration = DeltaVelocity / DeltaTime;\n\tCalculatedData.AccelerationScalar = Acceleration.Size();\n\n\tauto DeltaRotation = Data.Transform.GetRotation() * LastSetTransform.GetRotation().Inverse();\n\tData.AngularVelocity = Scale(DeltaRotation, 1 / DeltaTime);\n\tCalculatedData.AngularVelocityScalar = Data.AngularVelocity.GetAngle();\n\n\tauto BadData = CalculatedData.AccelerationScalar > MaxAcceleration ||\n\t\tDistance > MaxDistancePerFrame ||\n\t\tData.Velocity.SizeSquared() > MaxSpeed * MaxSpeed ||\n\t\tCalculatedData.AngularVelocityScalar > MaxAngularVelocity;\n\n\tauto const QualityOverride = GetDataQualityOverride();\n\tif (QualityOverride == EHandTrackingDataQuality::Good)\n\t\tBadData = false;\n\telse if (QualityOverride == EHandTrackingDataQuality::Bad)\n\t\tBadData = true;\n\n\tauto const Camera = GetOwner()->FindComponentByClass<UCameraComponent>();\n\tif (Camera != nullptr)\n\t{\n\t\tCalculatedData.CameraDistance = FVector::Dist(Data.Transform.GetLocation(), Camera->GetComponentLocation());\n\t\tif ((QualityOverride != EHandTrackingDataQuality::Good || bCameraRadiusIgnoreConfidence) && CalculatedData.CameraDistance < IgnoreCameraLocationRadius)\n\t\t\tBadData = true;\n\t}\n\telse\n\t{\n\t\tCalculatedData.CameraDistance = -1;\n\t}\n\n\tif (bForceBadData)\n\t{\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - DoFilteringImpl - Bad Data Forced\"), *GetName());\n\t\tBadData = true;\n\t}\n\n\tauto const OriginalDistance = Distance;\n\tauto const LastSetTransformActual = LastSetTransform;\n\tauto NewTransform = IntegrateFilterData(MitigatedTransform, Data, DeltaTime, BadData);\n\tif (FVector::Dist(LastSetTransformActual.GetLocation(), NewTransform.GetLocation()) > OriginalDistance)\n\t{\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - DoFilteringImpl - over 5 oh no - %s %f\"),\n\t\t\t*GetName(),\n\t\t\tBadData ? TEXT(\"bad data\") : TEXT(\"good data\"),\n\t\t\tOriginalDistance);\n\t}\n\n\tCalculatedData.QualityOverride = QualityOverride;\n\tCalculatedData.BadData = BadData;\n\n\tOnCalculatedData.Broadcast(CalculatedData);\n\n\tif (UE_LOG_ACTIVE(LogHandTrackingFilter, VeryVerbose))\n\t{\n\t\tauto CalculatedDataText = FString(TEXT(\"\"));\n\t\tFHandTrackingFilterCalculatedData::StaticStruct()->ExportText(CalculatedDataText, &CalculatedData, nullptr,\n\t\t\tthis,\n\t\t\tPPF_ExportsNotFullyQualified | PPF_Copy | PPF_Delimited | PPF_IncludeTransient | PPF_ExternalEditor, nullptr);\n\t\tUE_LOG(LogHandTrackingFilter, VeryVerbose, TEXT(\"%s - OnCalculatedData - (%i,%i,%i) %s\"),\n\t\t\t*GetName(),\n\t\t\t(int)NewTransform.GetLocation().X,\n\t\t\t(int)NewTransform.GetLocation().Y,\n\t\t\t(int)NewTransform.GetLocation().Z,\n\t\t\t*CalculatedDataText);\n\t}\n\n\treturn NewTransform;\n}\n\nvoid UHandTrackingFilterComponent::DoFiltering(FVector& Location, FRotator& Orientation, bool bForceBadData)\n{\n\tif (IsActive() == false)\n\t\treturn;\n\n\tif (bForceZeroTransform)\n\t{\n\t\tLocation = FVector::ZeroVector;\n\t\tOrientation = FRotator::ZeroRotator;\n\t\treturn;\n\t}\n\n\tauto ParentTransform = GetAttachParent()->GetAttachParent()->GetComponentTransform();\n\tauto RelativeTransform = FTransform(Orientation, Location);\n\tauto WorldTransform = DoFilteringImpl(RelativeTransform * ParentTransform, bForceBadData);\n\tauto NewRelativeTransform = WorldTransform * ParentTransform.Inverse();\n\tLocation = NewRelativeTransform.GetLocation();\n\tOrientation = NewRelativeTransform.Rotator();\n}\n\nvoid UHandTrackingFilterComponent::ExtrapolateTransform(float DeltaTime, FVector& FakeLocation, FQuat& FakeRotation)\n{\n\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - ExtrapolateTransform - LastGoodVelocity = %f\"), *GetName(), LastGoodVelocity.Size());\n\tFakeLocation = LastSetTransform.GetLocation() + LastGoodVelocity * DeltaTime;\n\tFakeRotation = Scale(LastGoodAngularVelocity, DeltaTime) * LastSetTransform.GetRotation();\n}\n\nFTransform UHandTrackingFilterComponent::IntegrateFilterData(\n\tFTransform MitigatedTransform,\n\tFHandTrackingFilterData const& Data, float DeltaTime,\n\tbool BadData)\n{\n\tif (BadData)\n\t{\n\t\tLastBadDataTime = Data.Time;\n\t\tLastGoodVelocity *= VelocityDamping; // damp the velocity so it doesn't fly off\n\t\tLastGoodAngularVelocity = Scale(LastGoodAngularVelocity, VelocityDamping);\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - IntegrateFilterData - Bad Data\"), *GetName());\n\t}\n\telse\n\t{\n\t\tLastGoodVelocity = FMath::Lerp(LastGoodVelocity, Data.Velocity.GetClampedToMaxSize(MaxFakeVelocity), GoodVelocityBlendRate);\n\t\tLastGoodAngularVelocity = Scale(Data.AngularVelocity,\n\t\t\tFMath::Max(Data.AngularVelocity.GetAngle() / MaxAngularVelocity, 1.0f));\n\t\tUE_LOG(LogHandTrackingFilter, Verbose, TEXT(\"%s - IntegrateFilterData - Good Data\"), *GetName());\n\t}\n\n\tFVector FakeLocation;\n\tFQuat FakeRotation;\n\tExtrapolateTransform(DeltaTime, FakeLocation, FakeRotation);\n\n\tif (BadData)\n\t{\n\t\t// when the data is bad it's possible for the MitigatedTransform to be invalid (containing NaNs)\n\t\t// so we set LastSetTransform directly instead of using the lerps\n\t\tLastSetTransform.SetLocation(FakeLocation);\n\t\tLastSetTransform.SetRotation(FakeRotation);\n\t}\n\telse\n\t{\n\t\tauto const FadePercent = FMath::Clamp((Data.Time - LastBadDataTime) / BadTransformFadeTime, 0.0, 1.0);\n\t\tLastSetTransform.SetLocation(FMath::LerpStable(FakeLocation, MitigatedTransform.GetLocation(), FadePercent));\n\t\tLastSetTransform.SetRotation(FQuat::Slerp(FakeRotation, MitigatedTransform.GetRotation(), FadePercent));\n\t}\n\n\treturn LastSetTransform;\n}\n\n#undef NOW\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandTrackingFilter/HandTrackingFilterComponent.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Components/SceneComponent.h\"\n#include \"HandTrackingFilterComponent.generated.h\"\n\nstruct HANDTRACKINGFILTER_API FHandTrackingFilterData\n{\n\tdouble Time;\n\tFTransform Transform;\n\tFVector Velocity;\n\tFQuat AngularVelocity;\n};\n\nUENUM(BlueprintType)\nenum class EHandTrackingDataQuality : uint8\n{\n\tNone,\n\tGood,\n\tBad\n};\n\nUSTRUCT(BlueprintType)\nstruct HANDTRACKINGFILTER_API FHandTrackingFilterCalculatedData\n{\n\tGENERATED_BODY()\n\n\tUPROPERTY(BlueprintReadOnly)\n\tFVector Acceleration = FVector::ZeroVector;\n\n\tUPROPERTY(BlueprintReadOnly)\n\tfloat AccelerationScalar{};\n\n\tUPROPERTY(BlueprintReadOnly)\n\tfloat AngularVelocityScalar{};\n\n\tUPROPERTY(BlueprintReadOnly)\n\tfloat CameraDistance{};\n\n\tUPROPERTY(BlueprintReadOnly)\n\tfloat Distance{};\n\n\tUPROPERTY(BlueprintReadOnly)\n\tFVector Velocity = FVector::ZeroVector;\n\n\t/// hand confidence information\n\tUPROPERTY(BlueprintReadOnly)\n\tEHandTrackingDataQuality QualityOverride{};\n\n\t/// if the data should be ignored\n\tUPROPERTY(BlueprintReadOnly)\n\tbool BadData{};\n};\n\nUCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))\nclass HANDTRACKINGFILTER_API UHandTrackingFilterComponent : public USceneComponent\n{\n\tGENERATED_BODY()\n\npublic:\n\tUHandTrackingFilterComponent();\n\nprotected:\n\tvirtual void BeginPlay() override;\n\tvirtual void EndPlay(EEndPlayReason::Type const EndPlayReason) override;\n\n\tFHandTrackingFilterData LastFrameData;\n\n\tdouble LastBadDataTime = -99999;\n\tFVector LastGoodVelocity = FVector::ZeroVector;\n\tFQuat LastGoodAngularVelocity = FQuat::Identity;\n\tFTransform LastSetTransform = FTransform::Identity;\n\n\tbool DoFirstPassFilter(FHandTrackingFilterData const& LastData, FHandTrackingFilterData const& ThisFrameInitData, FTransform& NewTransform);\n\tFTransform DoFilteringImpl(FTransform HandTransform, bool bForceBadData);\n\tFTransform IntegrateFilterData(FTransform MitigatedTransform, FHandTrackingFilterData const& Data, float DeltaTime, bool BadData);\n\tvoid DoFiltering(FVector& Location, FRotator& Orientation, bool bForceBadData);\n\tvoid ExtrapolateTransform(float DeltaTime, FVector& FakeLocation, FQuat& FakeRotation);\n\tEHandTrackingDataQuality GetDataQualityOverride() const;\n\tFQuat SmoothRotation(FQuat StartRot, FQuat TargetRot);\n\tFVector SmoothPosition(FVector StartPos, FVector TargetPos);\n\n\tdouble LastFrozenMovementTime = -99999;\n\tdouble LastFrozenRotationTime = -99999;\n\n\tUPROPERTY(Transient)\n\tUSceneComponent* PreFilterComponent = nullptr;\n\npublic:\n\t/** Percentage to de-jitter the position by */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Jitter Mitigation\")\n\tfloat SmoothPositionFactor = 0.875f;\n\n\t/** Max distance that should be considered \"no movement\" (cm) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Jitter Mitigation\")\n\tfloat MinSmoothPositionDistance = 0.2f;\n\n\t/** Max distance that should be considered \"jittery movement\" (cm) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Jitter Mitigation\")\n\tfloat MaxSmoothPositionDistance = 1.0f;\n\n\t/** Minimum percentage to de-jitter the position by */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Jitter Mitigation\")\n\tfloat SmoothRotationFactorMin = 0.0f;\n\n\t/** Maximum percentage to de-jitter the position by */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Jitter Mitigation\")\n\tfloat SmoothRotationFactorMax = 0.75f;\n\n\t/** Max angle that should be considered \"no rotation\" (cosine units) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Jitter Mitigation\")\n\tfloat SmoothRotationDotMin = FMath::Cos(FMath::DegreesToRadians(3.0f));\n\n\t/** Max angle that should be considered \"jittery rotation\" (cosine units) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Jitter Mitigation\")\n\tfloat SmoothRotationDotMax = FMath::Cos(FMath::DegreesToRadians(0.5f));\n\n\t/** Acceleration limit to trigger the filter (cm/s^2)*/\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Triggers\")\n\tfloat MaxAcceleration = 100000.0f;\n\n\t/** Speed limit to trigger the filter (cm/s) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Triggers\")\n\tfloat MaxSpeed = 2000.0f;\n\n\t/** Distance per frame limit to trigger the filter (cm) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Triggers\")\n\tfloat MaxDistancePerFrame = 9999999.0f;\n\n\t/** Angular velocity limit to trigger the filter (rad/s) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Triggers\")\n\tfloat MaxAngularVelocity = 3.0f;\n\n\t/** Time it will take to interpolate from filtered data to good data (s) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Effects\")\n\tfloat BadTransformFadeTime = 0.1f;\n\n\t/** Radius from the HMD to trigger the filter (cm) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Triggers\")\n\tfloat IgnoreCameraLocationRadius = 10.0f;\n\n\t/** Radius from the HMD to trigger the filter (cm) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Triggers\")\n\tbool bCameraRadiusIgnoreConfidence = true;\n\n\t/** Speed to clamp the extrapolated velocity (cm/s) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Filter Effects\")\n\tfloat MaxFakeVelocity = 0.8f;\n\n\t/** Per-frame multiplier on the extrapolated velocity (%/frame) */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, meta = (UIMin = 0, UIMax = 1), Category = \"Filter Effects\")\n\tfloat VelocityDamping = 0.96f;\n\n\t/** DEBUG - Disables usage of confidence to determine filtering */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere)\n\tbool bIgnoreConfidence = false;\n\n\t/** DEBUG - Enable for the filter to always return a zero transform */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere)\n\tbool bForceZeroTransform = false;\n\n\t// Minimum distance the hand must move to be recognized as a new \"tick\" of tracking\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere)\n\tfloat MinTrackingDistance = 0.001f;\n\n\t// How quickly to integrate presumed good velocity data into the extrapolation velocity\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere)\n\tfloat GoodVelocityBlendRate = 0.5f;\n\n\tDECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FOnCalculatedDataEvent, FHandTrackingFilterCalculatedData const &, Data);\n\n\t/**\n\t * @brief Called every tick with the data calculated by the filter. Used for debugging and tuning.\n\t */\n\tUPROPERTY(BlueprintAssignable)\n\tFOnCalculatedDataEvent OnCalculatedData;\n\n\t/**\n\t * @return The pre-filter component\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tUSceneComponent* GetPreFilterComponent() const { return PreFilterComponent; }\n\n\t/**\n\t * @param Component A scene component that should be updated with the pre-filtered transform data.\n\t * Used for accessing pre-filtered hand tracking data.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tvoid SetPreFilterComponent(USceneComponent* Component);\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/HandTrackingFilter/QuatUtil.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"Math/Quat.h\"\n\nFORCEINLINE FQuat HANDTRACKINGFILTER_API Scale(FQuat Rotation, float S)\n{\n\treturn FQuat::Slerp(FQuat::Identity, Rotation, S);\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/OculusHandPoseRecognition.Build.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\n\npublic class OculusHandPoseRecognition : ModuleRules\n{\n\tpublic OculusHandPoseRecognition (ReadOnlyTargetRules Target) : base(Target)\n\t{\n\t\tPCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;\n\n\t\tPublicIncludePaths.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t// ... add public include paths required here ...\n\t\t\t}\n\t\t\t);\n\n\n\t\tPrivateIncludePaths.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t// ... add other private include paths required here ...\n\t\t\t}\n\t\t\t);\n\n\n\t\tPublicDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"Core\",\n\t\t\t\t// ... add other public dependencies that you statically link with here ...\n\t\t\t}\n\t\t\t);\n\n\n\t\tPrivateDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"CoreUObject\",\n\t\t\t\t\"Engine\",\n\t\t\t\t\"Slate\",\n\t\t\t\t\"SlateCore\",\n\t\t\t\t\"OculusXRInput\",\n\t\t\t\t\"OculusUtils\"\n\t\t\t}\n\t\t\t);\n\n\n\t\tDynamicallyLoadedModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t// ... add any modules that your module loads dynamically here ...\n\t\t\t}\n\t\t\t);\n\n\t\tIncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6;\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/FRecordHandPoseAction.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"UObject/WeakObjectPtr.h\"\n#include \"Engine/LatentActionManager.h\"\n#include \"LatentActions.h\"\n#include \"HandPoseRecognizer.h\"\n#include \"OculusHandPoseRecognitionModule.h\"\n\n/** Latent action for a blueprint node that can record the range of motion of a pose. */\nclass FRecordHandPoseAction : public FPendingLatentAction\n{\npublic:\n\tFName ExecutionFunction;\n\tint32 OutputLink;\n\tFWeakObjectPtr CallbackTarget;\n\n\tUHandPoseRecognizer* Recognizer;\n\n\tconst ERecordHandPoseEntryType* InExecs;\n\tERecordHandPoseExitType* OutExecs;\n\n\tFRecordHandPoseAction(const FLatentActionInfo& LatentInfo, UHandPoseRecognizer* Recognizer, const ERecordHandPoseEntryType* InExecs, ERecordHandPoseExitType* OutExecs)\n\t\t: ExecutionFunction(LatentInfo.ExecutionFunction)\n\t\t, OutputLink(LatentInfo.Linkage)\n\t\t, CallbackTarget(LatentInfo.CallbackTarget)\n\t\t, Recognizer(Recognizer)\n\t\t, InExecs(InExecs)\n\t\t, OutExecs(OutExecs)\n\t{\n\t\tbIsRecording = false;\n\t\tHandPoseRangeIndex = 0;\n\t}\n\n\tvirtual void UpdateOperation(FLatentResponse& Response) override\n\t{\n\t\tif (*InExecs == ERecordHandPoseEntryType::StartRecording)\n\t\t{\n\t\t\tif (!bIsRecording)\n\t\t\t{\n\t\t\t\t// Let's reset min and max poses.\n\t\t\t\tMinPose.UpdatePose(Recognizer->Side, Recognizer->GetComponentRotation());\n\t\t\t\tMaxPose.UpdatePose(Recognizer->Side, Recognizer->GetComponentRotation());\n\t\t\t\tbIsRecording = true;\n\n\t\t\t\t*OutExecs = ERecordHandPoseExitType::RecordingStarted;\n\t\t\t\tResponse.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\tFHandPose NewPose;\n\t\t\t\tNewPose.UpdatePose(Recognizer->Side, Recognizer->GetComponentRotation());\n\n\t\t\t\tMinPose.Min(NewPose);\n\t\t\t\tMaxPose.Max(NewPose);\n\t\t\t}\n\t\t}\n\t\telse // if (*InExecs == ERecordHandPoseEntryType::StopRecording)\n\t\t{\n\t\t\tif (bIsRecording)\n\t\t\t{\n\t\t\t\tLogHandPoseRecordedRange();\n\t\t\t\tbIsRecording = false;\n\t\t\t}\n\n\t\t\t*OutExecs = ERecordHandPoseExitType::RecordingStopped;\n\t\t\tResponse.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);\n\t\t}\n\t}\n\n#if WITH_EDITOR\n\t// Returns a human readable description of the latent operation's current state\n\tvirtual FString GetDescription() const override\n\t{\n\t\treturn FString::Format(\n\t\t\tTEXT(\"Hand Pose Recorder is %srecording the %s\"),\n\t\t\t{\n\t\t\t\tbIsRecording ? TEXT(\"\") : TEXT(\"not \"),\n\t\t\t\t*UEnum::GetValueAsString(Recognizer->Side)\n\t\t\t});\n\t}\n#endif\n\nprotected:\n\tvoid LogHandPoseRecordedRange()\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"Hand Pose Range Recorded #%d\"), HandPoseRangeIndex++);\n\n\t\t// Error at 0.5 (half full range) is the maximum error.\n\t\tauto TotalSquare0500Error = 0.0f;\n\t\tauto TotalSquare0250Error = 0.0f;\n\t\tauto TotalSquare0125Error = 0.0f;\n\n\t\tfor (auto Bone = 0; Bone < NUM; ++Bone)\n\t\t{\n\t\t\tauto MinRot = MinPose.GetRotator((ERecognizedBone)Bone);\n\t\t\tauto MaxRot = MaxPose.GetRotator((ERecognizedBone)Bone);\n\n\t\t\t// Range is as large as 180 degrees.\n\t\t\tauto PitchRange = FMath::Abs(FMath::FindDeltaAngleDegrees(MaxRot.Pitch, MinRot.Pitch));\n\t\t\tauto YawRange = FMath::Abs(FMath::FindDeltaAngleDegrees(MaxRot.Yaw, MinRot.Yaw));\n\t\t\tauto RollRange = FMath::Abs(FMath::FindDeltaAngleDegrees(MaxRot.Roll, MinRot.Roll));\n\n\t\t\t// Max error is at most 180 degrees (0.5 * range).\n\t\t\tTotalSquare0500Error += FMath::Square(0.500 * PitchRange) + FMath::Square(0.500 * YawRange) + FMath::Square(0.500 * RollRange);\n\t\t\tTotalSquare0250Error += FMath::Square(0.250 * PitchRange) + FMath::Square(0.250 * YawRange) + FMath::Square(0.250 * RollRange);\n\t\t\tTotalSquare0125Error += FMath::Square(0.125 * PitchRange) + FMath::Square(0.125 * YawRange) + FMath::Square(0.125 * RollRange);\n\n\t\t\tUE_LOG(LogHandPoseRecognition, Warning, TEXT(\"%8s pitch %6.2f [%+7.2f .. %+7.2f]  yaw %6.2f [%+7.2f .. %+7.2f]  roll %6.2f [%+7.2f .. %+7.2f]\"),\n\t\t\t\t*UEnum::GetValueAsString((ERecognizedBone)Bone),\n\t\t\t\tPitchRange, MinRot.Pitch, MaxRot.Pitch,\n\t\t\t\tYawRange, MinRot.Yaw, MaxRot.Yaw,\n\t\t\t\tRollRange, MinRot.Roll, MaxRot.Roll);\n\t\t}\n\n\t\tUE_LOG(LogHandPoseRecognition, Warning,\n\t\t\tTEXT(\"Hand pose range total square error - full=%0.2f half=%0.2f quarter=%0.2f\"),\n\t\t\tTotalSquare0500Error, TotalSquare0250Error, TotalSquare0125Error);\n\t}\n\nprivate:\n\tbool bIsRecording;\n\tFHandPose MinPose{}, MaxPose{};\n\tint HandPoseRangeIndex;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/FWaitForHandGestureAction.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"UObject/WeakObjectPtr.h\"\n#include \"Engine/LatentActionManager.h\"\n#include \"LatentActions.h\"\n#include \"HandGestureRecognizer.h\"\n\n/** Latent action for blueprint node that waits for a gesture to be recognized. */\nclass FWaitForHandGestureAction : public FPendingLatentAction\n{\npublic:\n\tFName ExecutionFunction;\n\tint32 OutputLink;\n\tFWeakObjectPtr CallbackTarget;\n\n\tTWeakObjectPtr<UHandGestureRecognizer> HandGestureRecognizer;\n\tfloat TimeToWait;\n\tEGestureConsumptionBehavior Behavior;\n\tint* GestureIndex;\n\tFString* GestureName;\n\tFVector* GestureDirection;\n\tfloat* GestureOuterDuration;\n\tfloat* GestureInnerDuration;\n\n\tEWaitForHandGestureExitType* OutExecs;\n\n\tFWaitForHandGestureAction(const FLatentActionInfo& LatentInfo, UHandGestureRecognizer* HandGestureRecognizer, float TimeToWait, EGestureConsumptionBehavior Behavior, int* GestureIndex, FString* GestureName, FVector* GestureDirection, float* GestureOuterDuration, float* GestureInnerDuration, EWaitForHandGestureExitType* OutExecs)\n\t\t: ExecutionFunction(LatentInfo.ExecutionFunction)\n\t\t, OutputLink(LatentInfo.Linkage)\n\t\t, CallbackTarget(LatentInfo.CallbackTarget)\n\t\t, HandGestureRecognizer(HandGestureRecognizer)\n\t\t, TimeToWait(TimeToWait)\n\t\t, Behavior(Behavior)\n\t\t, GestureIndex(GestureIndex)\n\t\t, GestureName(GestureName)\n\t\t, GestureDirection(GestureDirection)\n\t\t, GestureOuterDuration(GestureOuterDuration)\n\t\t, GestureInnerDuration(GestureInnerDuration)\n\t\t, OutExecs(OutExecs)\n\t{\n\t}\n\n\tvirtual void UpdateOperation(FLatentResponse& Response) override\n\t{\n\t\t// Time out?\n\t\tif (TimeToWait > 0.0f)\n\t\t{\n\t\t\tTimeToWait -= Response.ElapsedTime();\n\t\t\tif (TimeToWait <= 0.0f)\n\t\t\t{\n\t\t\t\t// We have timed out while waiting.\n\t\t\t\t*GestureIndex = -1;\n\t\t\t\tGestureName->Empty();\n\t\t\t\t*OutExecs = EWaitForHandGestureExitType::TimeOut;\n\t\t\t\tResponse.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);\n\t\t\t}\n\t\t}\n\n\t\t// Did we recognize a gesture?\n\t\tint Index;\n\t\tFString Name;\n\t\tFVector Direction;\n\t\tfloat OuterDuration, InnerDuration;\n\n\t\tif (HandGestureRecognizer->GetRecognizedHandGesture(Behavior, Index, Name, Direction, OuterDuration, InnerDuration))\n\t\t{\n\t\t\t*GestureIndex = Index;\n\t\t\t*GestureName = Name;\n\t\t\t*GestureDirection = Direction;\n\t\t\t*GestureOuterDuration = OuterDuration;\n\t\t\t*GestureInnerDuration = InnerDuration;\n\t\t\t*OutExecs = EWaitForHandGestureExitType::GestureSeen;\n\t\t\tResponse.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);\n\t\t}\n\t}\n\n#if WITH_EDITOR\n\t// Returns a human readable description of the latent operation's current state\n\tvirtual FString GetDescription() const override\n\t{\n\t\tFString Msg = TEXT(\"Waiting for Hand Gesture\");\n\t\treturn Msg;\n\t}\n#endif\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/FWaitForHandPoseAction.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"UObject/WeakObjectPtr.h\"\n#include \"Engine/LatentActionManager.h\"\n#include \"LatentActions.h\"\n#include \"HandPoseRecognizer.h\"\n\n/** Latent action for blueprint node that waits for a pose to be recognized. */\nclass FWaitForHandPoseAction : public FPendingLatentAction\n{\npublic:\n\tFName ExecutionFunction;\n\tint32 OutputLink;\n\tFWeakObjectPtr CallbackTarget;\n\n\tTWeakObjectPtr<UHandPoseRecognizer> HandPoseRecognizer;\n\tfloat PoseMinDuration;\n\tfloat TimeToWait;\n\tint* PoseIndex;\n\tFString* PoseName;\n\n\tEWaitForHandPoseExitType* OutExecs;\n\n\tFWaitForHandPoseAction(const FLatentActionInfo& LatentInfo, UHandPoseRecognizer* HandPoseRecognizer, float PoseMinDuration, float TimeToWait, int* PoseIndex, FString* PoseName, EWaitForHandPoseExitType* OutExecs)\n\t\t: ExecutionFunction(LatentInfo.ExecutionFunction)\n\t\t, OutputLink(LatentInfo.Linkage)\n\t\t, CallbackTarget(LatentInfo.CallbackTarget)\n\t\t, HandPoseRecognizer(HandPoseRecognizer)\n\t\t, PoseMinDuration(PoseMinDuration)\n\t\t, TimeToWait(TimeToWait)\n\t\t, PoseIndex(PoseIndex)\n\t\t, PoseName(PoseName)\n\t\t, OutExecs(OutExecs)\n\t{\n\t}\n\n\tvirtual void UpdateOperation(FLatentResponse& Response) override\n\t{\n\t\t// Time out?\n\t\tif (TimeToWait > 0.0f)\n\t\t{\n\t\t\tTimeToWait -= Response.ElapsedTime();\n\t\t\tif (TimeToWait <= 0.0f)\n\t\t\t{\n\t\t\t\t// We have timed out while waiting.\n\t\t\t\t*PoseIndex = -1;\n\t\t\t\tPoseName->Empty();\n\t\t\t\t*OutExecs = EWaitForHandPoseExitType::TimeOut;\n\t\t\t\tResponse.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);\n\t\t\t}\n\t\t}\n\n\t\t// Did we recognize a pose?\n\t\tint Index;\n\t\tFString Name;\n\t\tfloat Duration;\n\t\tfloat Error;\n\t\tfloat Confidence;\n\n\t\tif (HandPoseRecognizer->GetRecognizedHandPose(Index, Name, Duration, Error, Confidence) && Duration >= PoseMinDuration)\n\t\t{\n\t\t\t*PoseIndex = Index;\n\t\t\t*PoseName = Name;\n\t\t\t*OutExecs = EWaitForHandPoseExitType::PoseSeen;\n\t\t\tResponse.FinishAndTriggerIf(true, ExecutionFunction, OutputLink, CallbackTarget);\n\t\t}\n\t}\n\n#if WITH_EDITOR\n\t// Returns a human readable description of the latent operation's current state\n\tvirtual FString GetDescription() const override\n\t{\n\t\tFString Msg = TEXT(\"Waiting for Hand Pose\");\n\t\treturn Msg;\n\t}\n#endif\n};\n\n\n\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandGesture.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandGesture.h\"\n#include \"OculusHandPoseRecognitionModule.h\"\n#include \"HandPoseRecognizer.h\"\n#include \"Misc/Char.h\"\n\nbool FHandGesture::ProcessEncodedGestureString(UHandPoseRecognizer* HandPoseRecognizer)\n{\n\tTCHAR const* Buffer = CustomEncodedGesture.GetCharArray().GetData();\n\n\twhile (Buffer && *Buffer)\n\t{\n\t\tint PoseIndex;\n\t\tfloat PoseMinDuration;\n\n\t\tif (!ReadTimedPose(HandPoseRecognizer, &Buffer, &PoseIndex, &PoseMinDuration))\n\t\t{\n\t\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"Hand gesture error near position %d of %s\"),\n\t\t\t\tCustomEncodedGesture.GetCharArray().GetData() - Buffer,\n\t\t\t\t*CustomEncodedGesture);\n\t\t\treturn false;\n\t\t}\n\n\t\tTimedPoses.Add({PoseIndex, PoseMinDuration, 0.0f, 0.0f});\n\t}\n\n\t// Reset state\n\tReset(true);\n\n\treturn true;\n}\n\nbool FHandGesture::Step(int PoseIndex, float PoseDuration, float DeltaTime, float CurrentTime, FVector const& Location)\n{\n\t// Nothing to do if there are no poses\n\tif (TimedPoses.Num() == 0)\n\t{\n\t\treturn false;\n\t}\n\n\tif (GestureState == EGestureState::GestureNotStarted)\n\t{\n\t\t// We only go into \"gesture in progress\" state when we have held the initial pose a minimum amount of time.\n\t\tif (TimedPoses.Num() > 0 && TimedPoses[0].PoseIndex == PoseIndex && TimedPoses[0].PoseMinDuration < PoseDuration)\n\t\t{\n\t\t\tGestureState = EGestureState::GestureInProgress;\n\t\t\tCurrentStep = 0;\n\t\t\tGestureStartLocation = GestureEndLocation = Location;\n\t\t\tDurationInCurrentStep = PoseDuration;\n\t\t\tTimedPoses[0].StepFirstTime = TimedPoses[0].StepLastTime = CurrentTime;\n\n\t\t\t// Debugging\n\t\t\tif (bGestureDebugLog)\n\t\t\t{\n\t\t\t\tUE_LOG(LogHandPoseRecognition, Display,\n\t\t\t\t\tTEXT(\"Gesture %s step: index=%d duration=%0.2fs delta=%0.2fs time=%0.2fs\"),\n\t\t\t\t\t*GestureName,\n\t\t\t\t\tPoseIndex, PoseDuration, DeltaTime, CurrentTime);\n\t\t\t\tUE_LOG(LogHandPoseRecognition, Display, TEXT(\"   Started with %0.2fs on first pose\"), PoseDuration);\n\t\t\t}\n\t\t}\n\t}\n\telse // if (GestureState == GestureInProgress || (IsLooping && GestureState == GestureCompleted))\n\t{\n\t\t// Debugging\n\t\tif (bGestureDebugLog)\n\t\t{\n\t\t\tUE_LOG(LogHandPoseRecognition, Display,\n\t\t\t\tTEXT(\"Gesture %s step: index=%d duration=%0.2fs delta=%0.2fs time=%0.2fs\"),\n\t\t\t\t*GestureName,\n\t\t\t\tPoseIndex, PoseDuration, DeltaTime, CurrentTime);\n\t\t}\n\n\t\tif (TimedPoses[CurrentStep].PoseIndex == PoseIndex)\n\t\t{\n\t\t\t// We need a minimum of time in the current pose before we can move on to the next one.\n\t\t\tDurationInCurrentStep = PoseDuration;\n\t\t\tDurationInTransition = 0.0f;\n\n\t\t\tTimedPoses[CurrentStep].StepLastTime = CurrentTime;\n\n\t\t\t// For gesture start location, we are looking for a 'late' location in the first step,\n\t\t\t// and for the end location, we are looking for an 'early' location in the last step.\n\t\t\tif (CurrentStep == 0)\n\t\t\t{\n\t\t\t\tGestureStartLocation = GestureStartLocation * DirectionBufferingFactor + Location * (1.0f - DirectionBufferingFactor);\n\t\t\t}\n\t\t\telse if (CurrentStep == (TimedPoses.Num() - 1))\n\t\t\t{\n\t\t\t\tGestureEndLocation = GestureEndLocation * (1.0f - DirectionBufferingFactor) + Location * DirectionBufferingFactor;\n\t\t\t}\n\n\t\t\t// Debugging\n\t\t\tif (bGestureDebugLog)\n\t\t\t{\n\t\t\t\tUE_LOG(LogHandPoseRecognition, Display, TEXT(\"   Pose held for %0.2fs\"), DurationInCurrentStep);\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// We also cannot be in any other pose longer than the MaxTransitionTime.\n\t\t\tauto const NextStep = (CurrentStep + 1) % (TimedPoses.Num() + (bIsLooping ? 0 : 1));\n\n\t\t\tif (NextStep < TimedPoses.Num() &&\n\t\t\t\tDurationInCurrentStep >= TimedPoses[CurrentStep].PoseMinDuration &&\n\t\t\t\tTimedPoses[NextStep].PoseIndex == PoseIndex)\n\t\t\t{\n\t\t\t\t// We meet all the conditions to move forward\n\t\t\t\tCurrentStep = NextStep;\n\t\t\t\tDurationInCurrentStep = PoseDuration;\n\t\t\t\tDurationInTransition = 0.0f;\n\n\t\t\t\tTimedPoses[CurrentStep].StepFirstTime = TimedPoses[CurrentStep].StepLastTime = CurrentTime;\n\n\t\t\t\tif (CurrentStep == (TimedPoses.Num() - 1))\n\t\t\t\t{\n\t\t\t\t\tGestureEndLocation = Location;\n\t\t\t\t}\n\n\t\t\t\t// Debugging\n\t\t\t\tif (bGestureDebugLog)\n\t\t\t\t{\n\t\t\t\t\tUE_LOG(LogHandPoseRecognition, Display,\n\t\t\t\t\t\tTEXT(\"   Moved to next pose since current pose duration %0.2fs > minimum %0.2fs\"),\n\t\t\t\t\t\tDurationInCurrentStep, TimedPoses[CurrentStep].PoseMinDuration);\n\t\t\t\t}\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t// This is not the current pose, and we are not ready to move to the next one,\n\t\t\t\t// so this counts as a transition.\n\t\t\t\tDurationInTransition += DeltaTime;\n\n\t\t\t\t// Non-looping gestures do not allow transitions on the last pose.\n\t\t\t\t// In all other situations, we test against the max transition time.\n\t\t\t\tif (DurationInTransition > MaxTransitionTime || (GestureState == EGestureState::GestureCompleted && !bIsLooping))\n\t\t\t\t{\n\t\t\t\t\tif (bGestureDebugLog)\n\t\t\t\t\t{\n\t\t\t\t\t\tUE_LOG(LogHandPoseRecognition, Display,\n\t\t\t\t\t\t\tTEXT(\"   In transition for %0.2fs > max transition time %0.2fs => reset\"),\n\t\t\t\t\t\t\tDurationInTransition, MaxTransitionTime);\n\t\t\t\t\t}\n\n\t\t\t\t\tReset();\n\t\t\t\t\treturn false;\n\t\t\t\t}\n\t\t\t\tif (bGestureDebugLog)\n\t\t\t\t{\n\t\t\t\t\tUE_LOG(LogHandPoseRecognition, Display,\n\t\t\t\t\t\tTEXT(\"   In transition for %0.2fs\"),\n\t\t\t\t\t\tDurationInTransition);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Checking for completion.\n\tif (GestureState != EGestureState::GestureNotStarted &&\n\t\tCurrentStep == (TimedPoses.Num() - 1) &&\n\t\tDurationInCurrentStep >= TimedPoses[CurrentStep].PoseMinDuration)\n\t{\n\t\tif (bGestureDebugLog && GestureState != EGestureState::GestureCompleted)\n\t\t{\n\t\t\tUE_LOG(LogHandPoseRecognition, Display, TEXT(\"   Gesture completed!\"));\n\t\t}\n\n\t\tGestureState = EGestureState::GestureCompleted;\n\t}\n\n\treturn GestureState == EGestureState::GestureCompleted;\n}\n\nfloat FHandGesture::ComputeTransitionTime(\n\tint FirstPoseIndex /* = 0 */, EPoseTimeType FirstPoseType /* = PoseLastTime */,\n\tint SecondPoseIndex /* = -1 */, EPoseTimeType SecondPoseType /* = PoseFirstTime */) const\n{\n\tauto const NumPoses = TimedPoses.Num();\n\n\tif (SecondPoseIndex == -1)\n\t{\n\t\tSecondPoseIndex = TimedPoses.Num() - 1;\n\t}\n\n\tif (FirstPoseIndex < 0 || FirstPoseIndex >= NumPoses ||\n\t\tSecondPoseIndex < 0 || SecondPoseIndex >= NumPoses ||\n\t\tFirstPoseIndex >= SecondPoseIndex)\n\t{\n\t\treturn 0.0f;\n\t}\n\n\tauto const FirstTime = FirstPoseType == PoseFirstTime ? TimedPoses[FirstPoseIndex].StepFirstTime : TimedPoses[FirstPoseIndex].StepLastTime;\n\tauto const SecondTime = SecondPoseType == PoseFirstTime ? TimedPoses[SecondPoseIndex].StepFirstTime : TimedPoses[SecondPoseIndex].StepLastTime;\n\n\tauto const Duration = SecondTime - FirstTime;\n\treturn Duration < 0.0f ? 0.0f : Duration;\n}\n\nvoid FHandGesture::Reset(bool Force /* = false */)\n{\n\tif (Force || GestureState != EGestureState::GestureNotStarted)\n\t{\n\t\tGestureState = EGestureState::GestureNotStarted;\n\t\tCurrentStep = -1;\n\t\tDurationInCurrentStep = 0.0f;\n\t\tDurationInTransition = 0.0f;\n\n\t\tGestureStartLocation.Set(0, 0, 0);\n\t\tGestureEndLocation.Set(0, 0, 0);\n\t\tDirectionBufferingFactor = 0.75f;\n\n\t\tfor (auto& TimedPose : TimedPoses)\n\t\t{\n\t\t\tTimedPose.StepFirstTime = 0.0f;\n\t\t\tTimedPose.StepLastTime = 0.0f;\n\t\t}\n\t}\n}\n\nvoid FHandGesture::DumpGestureState(int GestureIndex, UHandPoseRecognizer const* HandPoseRecognizer) const\n{\n\tFString StateString;\n\tswitch (GestureState)\n\t{\n\tcase EGestureState::GestureNotStarted:\n\t\tStateString = TEXT(\"NotStarted\");\n\t\tbreak;\n\tcase EGestureState::GestureInProgress:\n\t\tStateString = TEXT(\"InProgress\");\n\t\tbreak;\n\tcase EGestureState::GestureCompleted:\n\t\tStateString = TEXT(\"Completed\");\n\t\tbreak;\n\t}\n\n\tUE_LOG(LogHandPoseRecognition, Display, TEXT(\"Gesture %s[%d] %s\"), *GestureName, GestureIndex, *StateString);\n\tUE_LOG(LogHandPoseRecognition, Display, TEXT(\"Duration %05.3f Transition %05.3f\"), DurationInCurrentStep, DurationInTransition);\n\n\tauto Step = 0;\n\tfor (auto const& TimedPose : TimedPoses)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Display, TEXT(\" %c %d %8s[%d] %05.3f - %05.3f\"),\n\t\t\tCurrentStep==Step ? TEXT('>') : TEXT(' '), Step,\n\t\t\t*(HandPoseRecognizer->Poses[TimedPose.PoseIndex].PoseName),\n\t\t\tTimedPose.PoseIndex,\n\t\t\tTimedPose.StepFirstTime,\n\t\t\tTimedPose.StepLastTime);\n\n\t\tStep++;\n\t}\n}\n\nint FHandGesture::FindPoseIndex(UHandPoseRecognizer* Recognizer, FString const& PoseName)\n{\n\tfor (auto PoseIndex = 0; PoseIndex < Recognizer->Poses.Num(); ++PoseIndex)\n\t{\n\t\tif (Recognizer->Poses[PoseIndex].PoseName == PoseName)\n\t\t{\n\t\t\treturn PoseIndex;\n\t\t}\n\t}\n\n\treturn -1;\n}\n\nbool FHandGesture::ReadTimedPose(UHandPoseRecognizer* Recognizer, TCHAR const** Buffer, int* PoseIndex, float* PoseMinDuration)\n{\n\t// Pose name\n\tFString PoseNameRead;\n\twhile (FChar::IsWhitespace(**Buffer))\n\t{\n\t\t++(*Buffer);\n\t}\n\twhile (**Buffer && **Buffer != '/' && **Buffer != ',' && !FChar::IsWhitespace(**Buffer))\n\t{\n\t\tPoseNameRead += **Buffer;\n\t\t++(*Buffer);\n\t}\n\n\t// Pose index\n\tauto const PoseIndexFound = FindPoseIndex(Recognizer, PoseNameRead);\n\tif (PoseIndexFound == -1)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"Unrecognized pose called %s in %s\"), *PoseNameRead, *Recognizer->GetName());\n\t\treturn false;\n\t}\n\n\t// Pose min duration\n\tauto PoseMinDurationReadMillis = 0;\n\twhile (FChar::IsWhitespace(**Buffer))\n\t{\n\t\t++(*Buffer);\n\t}\n\tif (**Buffer == '/')\n\t{\n\t\t++(*Buffer);\n\n\t\t// We have a min duration in milliseconds\n\t\twhile (FChar::IsWhitespace(**Buffer))\n\t\t{\n\t\t\t++(*Buffer);\n\t\t}\n\t\twhile (FChar::IsDigit(**Buffer))\n\t\t{\n\t\t\tPoseMinDurationReadMillis *= 10;\n\t\t\tPoseMinDurationReadMillis += (**Buffer) - '0';\n\t\t\t++(*Buffer);\n\t\t}\n\t}\n\n\t// Consume end of timed pose\n\twhile (FChar::IsWhitespace(**Buffer))\n\t{\n\t\t++(*Buffer);\n\t}\n\tif (**Buffer == ',')\n\t{\n\t\t++(*Buffer);\n\t}\n\telse if (**Buffer)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"End of timed pose expected near '%s'\"), *Buffer);\n\t\treturn false;\n\t}\n\n\t// Return results\n\t*PoseIndex = PoseIndexFound;\n\t*PoseMinDuration = PoseMinDurationReadMillis * 0.001f;\n\n\treturn true;\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandGestureRecognizer.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandGestureRecognizer.h\"\n#include \"OculusHandPoseRecognitionModule.h\"\n#include \"Kismet/GameplayStatics.h\"\n\nUHandGestureRecognizer::UHandGestureRecognizer(const FObjectInitializer& ObjectInitializer /* = FObjectInitializer::Get() */):\n\tSuper(ObjectInitializer)\n{\n\tPrimaryComponentTick.bCanEverTick = true;\n\tRecognitionInterval = 0.0f;\n\tRecognitionSkippedFrames = 1;\n\tbHasRecognizedGesture = false;\n\tTimeSinceLastRecognition = 0.0f;\n\tSkippedFramesSinceLastRecognition = 0;\n}\n\nvoid UHandGestureRecognizer::BeginPlay()\n{\n\tSuper::BeginPlay();\n\n\t// We must be attached to a HandPoseRecognizer component\n\tTArray<USceneComponent*> Parents;\n\tGetParentComponents(Parents);\n\tFString ImmediateParentClassName;\n\tif (Parents.Num() >= 1)\n\t{\n\t\tImmediateParentClassName = Parents[0]->GetClass()->GetName();\n\t\tHandPoseRecognizer = Cast<UHandPoseRecognizer>(Parents[0]);\n\t}\n\n\tauto TurnOffGestureRecognition = false;\n\tif (!HandPoseRecognizer)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"UHandGestureRecognizer called %s MUST be attached to a UHandPoseRecognizer not a %s.\"),\n\t\t\t*GetName(), *ImmediateParentClassName);\n\t\tTurnOffGestureRecognition = true;\n\t}\n\n\tif (HandPoseRecognizer->Side == EOculusXRHandType::None)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Warning, TEXT(\"UHandGestureRecognizer called %s is attached to a disabled UHandPoseRecognizer.\"),\n\t\t\t*GetName());\n\t\tTurnOffGestureRecognition = true;\n\t}\n\n\tif (TurnOffGestureRecognition)\n\t{\n\t\tSetComponentTickEnabled(false);\n\t\treturn;\n\t}\n\n\t// We decode the hand gestures\n\tfor (auto GestureIndex = 0; GestureIndex < Gestures.Num(); ++GestureIndex)\n\t{\n\t\tif (!Gestures[GestureIndex].ProcessEncodedGestureString(HandPoseRecognizer))\n\t\t{\n\t\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"UHandGestureRecognizer gesture at index %d is invalid.\"), GestureIndex);\n\t\t}\n\t}\n}\n\nvoid UHandGestureRecognizer::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)\n{\n\tSuper::TickComponent(DeltaTime, TickType, ThisTickFunction);\n\n\tif (!HandPoseRecognizer) return;\n\n\t// Recognition is throttled\n\tTimeSinceLastRecognition += DeltaTime;\n\tif (TimeSinceLastRecognition < RecognitionInterval)\n\t{\n\t\tSkippedFramesSinceLastRecognition++;\n\t\treturn;\n\t}\n\n\tif (SkippedFramesSinceLastRecognition < RecognitionSkippedFrames)\n\t{\n\t\tSkippedFramesSinceLastRecognition++;\n\n\t\t// For better recognition of gesture strength (speed of transition from first to last pose),\n\t\t// it is necessary to skip at least one cycle.\n\t\t// UE_LOG(LogOculusHandPoseRecognition, Error, TEXT(\"*** Skipping Frame (skipped %d)\"), SkippedFramesSinceLastRecognition);\n\n\t\treturn;\n\t}\n\n\tTimeSinceLastRecognition = 0.0f;\n\tSkippedFramesSinceLastRecognition = 0;\n\n\t// We need the current game time for recognizing the transition time between the first and last poses.\n\tauto const World = GetWorld();\n\tcheck(World != nullptr);\n\tauto const CurrentTime = UGameplayStatics::GetTimeSeconds(World);\n\n\t// Currently recognized hand pose\n\tint PoseIndex;\n\tFString PoseName;\n\tfloat PoseDuration;\n\tfloat PoseError;\n\tfloat PoseConfidence;\n\tHandPoseRecognizer->GetRecognizedHandPose(PoseIndex, PoseName, PoseDuration, PoseError, PoseConfidence);\n\n\t// By getting the relative location this way we have:\n\t// X+ is forward, Y+ is right, Z+ is up\n\tauto const ActorRelativeLocation = GetComponentTransform().GetRelativeTransform(GetOwner()->GetTransform()).GetLocation();\n\n\t// We process all hand gestures\n\tCompletedGestures.Reset();\n\n\tfor (auto GestureIndex = 0; GestureIndex < Gestures.Num(); ++GestureIndex)\n\t{\n\t\tif (Gestures[GestureIndex].Step(PoseIndex, PoseDuration, DeltaTime, CurrentTime, ActorRelativeLocation))\n\t\t{\n\t\t\tCompletedGestures.Push(GestureIndex);\n\t\t}\n\t}\n\n\t// Updating property that indicates that at least one gesture is ready.\n\tbHasRecognizedGesture = CompletedGestures.Num() > 0;\n}\n\nbool UHandGestureRecognizer::GetRecognizedHandGesture(\n\tEGestureConsumptionBehavior Behavior,\n\tint& Index, FString& Name,\n\tFVector& GestureDirection,\n\tfloat& GestureOuterDuration, float& GestureInnerDuration)\n{\n\tif (CompletedGestures.Num() == 0)\n\t{\n\t\t// There are no completed gestures at this time\n\t\tIndex = -1;\n\t\tName = \"None\";\n\t\tGestureDirection = FVector::ZeroVector;\n\t\tGestureOuterDuration = 0.0f;\n\t\tGestureInnerDuration = 0.0f;\n\t\treturn false;\n\t}\n\n\tIndex = CompletedGestures.Pop(EAllowShrinking::No);\n\tName = Gestures[Index].GestureName;\n\tGestureDirection = Gestures[Index].GetGestureDirection();\n\tGestureOuterDuration = Gestures[Index].ComputeOuterDuration();\n\tGestureInnerDuration = Gestures[Index].ComputeInnerDuration();\n\n\t// UE_LOG(LogOculusHandPoseRecognition, Warning, TEXT(\"Recognized gesture %d:%s.\"), Index, *Name);\n\n\t// Reset gesture(s) according to preference\n\tif (Behavior == EGestureConsumptionBehavior::Reset)\n\t{\n\t\tResetHandGesture(Index);\n\t}\n\telse if (Behavior == EGestureConsumptionBehavior::ResetAll)\n\t{\n\t\tResetAllHandGestures();\n\t\tCompletedGestures.Reset();\n\t}\n\n\t// Updating property that indicates that at least one other gesture is ready.\n\tbHasRecognizedGesture = CompletedGestures.Num() > 0;\n\n\treturn true;\n}\n\nEGestureState UHandGestureRecognizer::GetGestureRecognitionState(int Index)\n{\n\tif (Index >= 0 && Index < Gestures.Num())\n\t{\n\t\treturn Gestures[Index].GetGestureState();\n\t}\n\n\treturn EGestureState::GestureNotStarted;\n}\n\nvoid UHandGestureRecognizer::ResetHandGesture(int& Index)\n{\n\tif (Index >= 0 && Index < Gestures.Num())\n\t{\n\t\tGestures[Index].Reset();\n\t}\n}\n\nvoid UHandGestureRecognizer::ResetAllHandGestures()\n{\n\tfor (auto& Gesture : Gestures)\n\t{\n\t\tGesture.Reset();\n\t}\n}\n\nvoid UHandGestureRecognizer::DumpAllGestureStates() const\n{\n\tUE_LOG(LogHandPoseRecognition, Warning, TEXT(\"Gesture states for %s\"), *GetName());\n\n\tfor (auto GestureIndex = 0; GestureIndex < Gestures.Num(); ++GestureIndex)\n\t{\n\t\tGestures[GestureIndex].DumpGestureState(GestureIndex, HandPoseRecognizer);\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandPose.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandPose.h\"\n#include \"OculusXRInputFunctionLibrary.h\"\n#include <limits>\n\nvoid FHandPose::UpdatePose(EOculusXRHandType Side, FRotator Wrist)\n{\n\tHand = Side;\n\tRotations[Thumb_0] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_0).Rotator();\n\tRotations[Thumb_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_1).Rotator();\n\tRotations[Thumb_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_2).Rotator();\n\tRotations[Thumb_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Thumb_3).Rotator();\n\tRotations[Index_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Index_1).Rotator();\n\tRotations[Index_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Index_2).Rotator();\n\tRotations[Index_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Index_3).Rotator();\n\tRotations[Middle_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Middle_1).Rotator();\n\tRotations[Middle_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Middle_2).Rotator();\n\tRotations[Middle_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Middle_3).Rotator();\n\tRotations[Ring_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Ring_1).Rotator();\n\tRotations[Ring_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Ring_2).Rotator();\n\tRotations[Ring_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Ring_3).Rotator();\n\tRotations[Pinky_0] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_0).Rotator();\n\tRotations[Pinky_1] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_1).Rotator();\n\tRotations[Pinky_2] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_2).Rotator();\n\tRotations[Pinky_3] = UOculusXRInputFunctionLibrary::GetBoneRotation(Side, EOculusXRBone::Pinky_3).Rotator();\n\tRotations[ERecognizedBone::Wrist] = Wrist;\n}\n\nvoid FHandPose::Encode()\n{\n\tCustomEncodedPose.Empty(1024);\n\n\tCustomEncodedPose\n\t\t.Append(Hand == EOculusXRHandType::HandLeft ? \"L\" : \"R\")\n\t\t.Append(*FmtRot(TEXT(\" T0\"), Rotations[Thumb_0]))\n\t\t.Append(*FmtRot(TEXT(\" T1\"), Rotations[Thumb_1]))\n\t\t.Append(*FmtRot(TEXT(\" T2\"), Rotations[Thumb_2]))\n\t\t.Append(*FmtRot(TEXT(\" T3\"), Rotations[Thumb_3]))\n\t\t.Append(*FmtRot(TEXT(\"  I1\"), Rotations[Index_1]))\n\t\t.Append(*FmtRot(TEXT(\" I2\"), Rotations[Index_2]))\n\t\t.Append(*FmtRot(TEXT(\" I3\"), Rotations[Index_3]))\n\t\t.Append(*FmtRot(TEXT(\"  M1\"), Rotations[Middle_1]))\n\t\t.Append(*FmtRot(TEXT(\" M2\"), Rotations[Middle_2]))\n\t\t.Append(*FmtRot(TEXT(\" M3\"), Rotations[Middle_3]))\n\t\t.Append(*FmtRot(TEXT(\"  R1\"), Rotations[Ring_1]))\n\t\t.Append(*FmtRot(TEXT(\" R2\"), Rotations[Ring_2]))\n\t\t.Append(*FmtRot(TEXT(\" R3\"), Rotations[Ring_3]))\n\t\t.Append(*FmtRot(TEXT(\"  P0\"), Rotations[Pinky_0]))\n\t\t.Append(*FmtRot(TEXT(\" P1\"), Rotations[Pinky_1]))\n\t\t.Append(*FmtRot(TEXT(\" P2\"), Rotations[Pinky_2]))\n\t\t.Append(*FmtRot(TEXT(\" P3\"), Rotations[Pinky_3]))\n\t\t.Append(*FmtRot(TEXT(\"  W\"), Rotations[Wrist]));\n}\n\nbool FHandPose::Decode()\n{\n\tconst TCHAR* Buffer = CustomEncodedPose.GetCharArray().GetData();\n\tif (!Buffer)\n\t{\n\t\tHand = EOculusXRHandType::None;\n\t\treturn false;\n\t}\n\n\tSkipWhitespace(&Buffer);\n\n\t// Hand\n\tif (Buffer && *Buffer == 'L')\n\t{\n\t\tHand = EOculusXRHandType::HandLeft;\n\t}\n\telse if (Buffer && *Buffer == 'R')\n\t{\n\t\tHand = EOculusXRHandType::HandRight;\n\t}\n\telse\n\t{\n\t\tHand = EOculusXRHandType::None;\n\t\treturn false;\n\t}\n\t++Buffer;\n\n\t// Rotators\n\tauto const Successful =\n\t\tReadRot(&Buffer, TEXT(\"T0\"), Rotations[Thumb_0], Weights[Thumb_0]) &&\n\t\tReadRot(&Buffer, TEXT(\"T1\"), Rotations[Thumb_1], Weights[Thumb_1]) &&\n\t\tReadRot(&Buffer, TEXT(\"T2\"), Rotations[Thumb_2], Weights[Thumb_2]) &&\n\t\tReadRot(&Buffer, TEXT(\"T3\"), Rotations[Thumb_3], Weights[Thumb_3]) &&\n\t\tReadRot(&Buffer, TEXT(\"I1\"), Rotations[Index_1], Weights[Index_1]) &&\n\t\tReadRot(&Buffer, TEXT(\"I2\"), Rotations[Index_2], Weights[Index_2]) &&\n\t\tReadRot(&Buffer, TEXT(\"I3\"), Rotations[Index_3], Weights[Index_3]) &&\n\t\tReadRot(&Buffer, TEXT(\"M1\"), Rotations[Middle_1], Weights[Middle_1]) &&\n\t\tReadRot(&Buffer, TEXT(\"M2\"), Rotations[Middle_2], Weights[Middle_2]) &&\n\t\tReadRot(&Buffer, TEXT(\"M3\"), Rotations[Middle_3], Weights[Middle_3]) &&\n\t\tReadRot(&Buffer, TEXT(\"R1\"), Rotations[Ring_1], Weights[Ring_1]) &&\n\t\tReadRot(&Buffer, TEXT(\"R2\"), Rotations[Ring_2], Weights[Ring_2]) &&\n\t\tReadRot(&Buffer, TEXT(\"R3\"), Rotations[Ring_3], Weights[Ring_3]) &&\n\t\tReadRot(&Buffer, TEXT(\"P0\"), Rotations[Pinky_0], Weights[Pinky_0]) &&\n\t\tReadRot(&Buffer, TEXT(\"P1\"), Rotations[Pinky_1], Weights[Pinky_1]) &&\n\t\tReadRot(&Buffer, TEXT(\"P2\"), Rotations[Pinky_2], Weights[Pinky_2]) &&\n\t\tReadRot(&Buffer, TEXT(\"P3\"), Rotations[Pinky_3], Weights[Pinky_3]) &&\n\t\tReadRot(&Buffer, TEXT(\"W\"), Rotations[Wrist], Weights[Wrist]);\n\n\tif (!Successful)\n\t{\n\t\tHand = EOculusXRHandType::None;\n\t}\n\n\treturn Successful;\n}\n\nfloat FHandPose::ComputeConfidence(const FHandPose& Other, float* RawError /* = nullptr */) const\n{\n\tauto const Err =\n\t\tRotError(Thumb_0, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Thumb_1, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Thumb_2, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Thumb_3, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Index_1, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Index_2, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Index_3, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Index_1, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Middle_1, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Middle_2, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Middle_3, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Ring_1, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Ring_2, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Ring_3, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Pinky_0, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Pinky_1, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Pinky_2, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Pinky_3, Rotations, Weights, Other.Rotations) +\n\t\tRotError(Wrist, Rotations, Weights, Other.Rotations);\n\n\tauto const MinErr = FMath::Max(ErrorAtMaxConfidence, 100.0f);\n\tauto const Confidence = MinErr / FMath::Max(Err, MinErr);\n\n\tif (RawError)\n\t{\n\t\t*RawError = Err;\n\t}\n\n\treturn Confidence;\n}\n\nvoid FHandPose::AddWeighted(const FHandPose& Other, float OtherRatio)\n{\n\tOtherRatio = FMath::Clamp(OtherRatio, 0.0f, 1.0f);\n\n\tfor (auto Bone = 0; Bone < NUM; ++Bone)\n\t{\n\t\tRotations[Bone] *= 1.0f - OtherRatio;\n\t\tRotations[Bone] += Other.Rotations[Bone] * OtherRatio;\n\t}\n}\n\nvoid FHandPose::Min(const FHandPose& Other)\n{\n\tfor (auto Bone = 0; Bone < NUM; ++Bone)\n\t{\n\t\tRotations[Bone].Pitch = FMath::Min(Rotations[Bone].Pitch, Other.Rotations[Bone].Pitch);\n\t\tRotations[Bone].Yaw = FMath::Min(Rotations[Bone].Yaw, Other.Rotations[Bone].Yaw);\n\t\tRotations[Bone].Roll = FMath::Min(Rotations[Bone].Roll, Other.Rotations[Bone].Roll);\n\t}\n}\n\nvoid FHandPose::Max(const FHandPose& Other)\n{\n\tfor (auto Bone = 0; Bone < NUM; ++Bone)\n\t{\n\t\tRotations[Bone].Pitch = FMath::Max(Rotations[Bone].Pitch, Other.Rotations[Bone].Pitch);\n\t\tRotations[Bone].Yaw = FMath::Max(Rotations[Bone].Yaw, Other.Rotations[Bone].Yaw);\n\t\tRotations[Bone].Roll = FMath::Max(Rotations[Bone].Roll, Other.Rotations[Bone].Roll);\n\t}\n}\n\nint FHandPose::NormalizedOutputAngle(float Angle)\n{\n\t// We never return 0, as it is used in reference poses to ignore angles.\n\tauto const IntAngle = static_cast<int>(roundf(Angle));\n\treturn IntAngle ? IntAngle : 1;\n}\n\nFString FHandPose::FmtRot(FString Prefix, FRotator R)\n{\n\t// Never output 0 degree angles, as they are used to disable comparisons.\n\tauto const Pitch = NormalizedOutputAngle(R.Pitch);\n\tauto const Yaw = NormalizedOutputAngle(R.Yaw);\n\tauto const Roll = NormalizedOutputAngle(R.Roll);\n\n\treturn FString::Printf(TEXT(\"%s%+0d%+0d%+0d\"), *Prefix, Pitch, Yaw, Roll);\n}\n\nvoid FHandPose::SkipWhitespace(const TCHAR** Buffer)\n{\n\twhile (**Buffer == ' ' || **Buffer == '\\t')\n\t\t++*Buffer;\n}\n\nbool FHandPose::ReadRotComp(const TCHAR** Buffer, double* RotComp)\n{\n\tSkipWhitespace(Buffer);\n\n\tauto Negative = false;\n\n\tif (**Buffer == '+')\n\t{\n\t\t++*Buffer;\n\t}\n\telse if (**Buffer == '-')\n\t{\n\t\tNegative = true;\n\t\t++*Buffer;\n\t}\n\telse\n\t{\n\t\treturn false;\n\t}\n\n\t*RotComp = 0.0;\n\twhile (**Buffer >= '0' && **Buffer <= '9')\n\t{\n\t\t*RotComp *= 10;\n\t\t*RotComp += **Buffer - '0';\n\t\t++*Buffer;\n\t}\n\n\tif (Negative)\n\t{\n\t\t*RotComp *= -1.0;\n\t}\n\n\treturn true;\n}\n\nbool FHandPose::ReadWeight(const TCHAR** Buffer, float* Weight)\n{\n\tSkipWhitespace(Buffer);\n\n\t// Check for weight marker\n\tif (**Buffer != '*')\n\t{\n\t\t*Weight = 1.0f; // Defaults to 1\n\t\treturn true;\n\t}\n\t++*Buffer;\n\n\tSkipWhitespace(Buffer);\n\n\t// Read weight factor\n\tauto Denominator = 0.0f;\n\tauto Value = 0.0f;\n\twhile (**Buffer >= '0' && **Buffer <= '9' || **Buffer == '.')\n\t{\n\t\tif (**Buffer == '.')\n\t\t{\n\t\t\tDenominator = 1.0f;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tValue *= 10.0f;\n\t\t\tValue += **Buffer - '0';\n\t\t\tDenominator *= 10.0f; // Stays 0.0 as long as we have not seen the decimal point\n\t\t}\n\n\t\t++*Buffer;\n\t}\n\n\tif (Denominator > 0.0f)\n\t{\n\t\tValue /= Denominator;\n\t}\n\n\t*Weight = Value;\n\treturn true;\n}\n\nbool FHandPose::ReadRot(const TCHAR** Buffer, const TCHAR* Prefix, FRotator& R, float& Weight)\n{\n\tSkipWhitespace(Buffer);\n\n\t// Check if prefix matches\n\tauto PrefixPtr = Prefix;\n\tauto BufferPtr = *Buffer;\n\n\twhile (*PrefixPtr && *BufferPtr && *PrefixPtr == *BufferPtr)\n\t{\n\t\tPrefixPtr++;\n\t\tBufferPtr++;\n\t}\n\n\tif (*PrefixPtr)\n\t{\n\t\t// Prefix mismatch, this may be a missing bone.\n\t\tR.Pitch = R.Yaw = R.Roll = 0.0f;\n\t\tWeight = 0.0f;\n\t\treturn true;\n\t}\n\n\t// Looks good, let's get the rotations and optional weight\n\t*Buffer = BufferPtr;\n\treturn\n\t\tReadWeight(Buffer, &Weight) &&\n\t\tReadRotComp(Buffer, &R.Pitch) &&\n\t\tReadRotComp(Buffer, &R.Yaw) &&\n\t\tReadRotComp(Buffer, &R.Roll);\n}\n\nfloat FHandPose::ComputeAngleError(float Ref, float Angle)\n{\n\t// A reference angle of 0.0 is ignored.\n\tif (Ref == 0.0f) return 0.0f;\n\n\t// We find the minimum angle and square it\n\tauto const DeltaAngleDegrees = FMath::FindDeltaAngleDegrees(Ref, Angle);\n\treturn DeltaAngleDegrees * DeltaAngleDegrees;\n}\n\nfloat FHandPose::RotError(ERecognizedBone Bone, const FRotator* RefRot, const float* RefWeight, const FRotator* OtherRot)\n{\n\treturn\n\t\tRefWeight[Bone] *\n\t\t(ComputeAngleError(RefRot[Bone].Pitch, OtherRot[Bone].Pitch) +\n\t\t\tComputeAngleError(RefRot[Bone].Yaw, OtherRot[Bone].Yaw) +\n\t\t\tComputeAngleError(RefRot[Bone].Roll, OtherRot[Bone].Roll));\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandPoseRecognizer.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandPoseRecognizer.h\"\n#include \"OculusHandPoseRecognitionModule.h\"\n#include <limits>\n\nUHandPoseRecognizer::UHandPoseRecognizer(const FObjectInitializer& ObjectInitializer):\n\tSuper(ObjectInitializer)\n{\n\tPrimaryComponentTick.bCanEverTick = true;\n\n\t// Recognition default parameters\n\tSide = EOculusXRHandType::None;\n\tRecognitionInterval = 0.0f;\n\tDefaultConfidenceFloor = 0.5;\n\tDampingFactor = 0.0f;\n\n\t// Current hand pose being recognized\n\tTimeSinceLastRecognition = 0.0f;\n\tCurrentHandPose = -1;\n\tCurrentHandPoseDuration = 0.0f;\n\tCurrentHandPoseConfidence = 0.0f;\n\tCurrentHandPoseError = std::numeric_limits<float>::max();\n\n\t// Encoded hand pose logged index\n\tLoggedIndex = 0;\n}\n\nvoid UHandPoseRecognizer::BeginPlay()\n{\n\tSuper::BeginPlay();\n\n\t// We decode the hand poses\n\tfor (auto PatternIndex = 0; PatternIndex < Poses.Num(); ++PatternIndex)\n\t{\n\t\tif (!Poses[PatternIndex].Decode())\n\t\t{\n\t\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"UHandPoseRecognizer(%s) encoded pose at index %d is invalid.\"),\n\t\t\t\t*GetName(),\n\t\t\t\tPatternIndex);\n\t\t}\n\t}\n}\n\nFRotator UHandPoseRecognizer::GetWristRotator(FQuat ComponentQuat) const\n{\n\tauto ComponentRotator = ComponentQuat.Rotator();\n\n\tauto const World = GetWorld();\n\tif (!World) return ComponentRotator;\n\n\tauto const PlayerController = World->GetFirstPlayerController();\n\tif (!PlayerController) return ComponentRotator;\n\n\tauto const ComponentRotationToCamera = PlayerController->PlayerCameraManager->GetTransform().InverseTransformRotation(ComponentQuat).Rotator();\n\tComponentRotator.Yaw = ComponentRotationToCamera.Yaw;\n\n\treturn ComponentRotator;\n}\n\nvoid UHandPoseRecognizer::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)\n{\n\tSuper::TickComponent(DeltaTime, TickType, ThisTickFunction);\n\n\tif (Side == EOculusXRHandType::None)\n\t{\n\t\t// Recognizer is disabled\n\t\treturn;\n\t}\n\n\t// Recognition is throttled\n\tTimeSinceLastRecognition += DeltaTime;\n\tif (TimeSinceLastRecognition < RecognitionInterval)\n\t{\n\t\treturn;\n\t}\n\n\t// Ignore low confidence cases\n\tif (UOculusXRInputFunctionLibrary::GetTrackingConfidence(Side) == EOculusXRTrackingConfidence::Low)\n\t{\n\t\treturn;\n\t}\n\n\t// Updating tracked hand.\n\t// Note that the wrist rotation pitch and roll are world relative, and the yaw is hmd relative.\n\tPose.UpdatePose(Side, GetWristRotator(GetComponentQuat()));\n\n\t// Finding closest pattern\n\tauto ClosestHandPose = -1;\n\tauto ClosestHandPoseConfidence = DefaultConfidenceFloor;\n\tauto ClosestHandPoseError = std::numeric_limits<float>::max();\n\tauto HighestConfidence = 0.0f;\n\n\tfor (auto PatternIndex = 0; PatternIndex < Poses.Num(); ++PatternIndex)\n\t{\n\t\t// Skip patterns that are not for this side\n\t\tif (Poses[PatternIndex].GetHandType() != Side)\n\t\t\tcontinue;\n\n\t\t// Computing confidence (we ignore the wrist yaw by default)\n\t\tauto RawError = 0.0f;\n\t\tauto const Confidence = Poses[PatternIndex].ComputeConfidence(Pose, &RawError);\n\t\t// UE_LOG(LogHandPoseRecognition, Error, TEXT(\"%s confidence %0.0f raw error %0.0f\"), *Poses[PatternIndex].PoseName, Confidence, RawError);\n\n\t\t// We always record the smallest error, in case no pattern matches\n\t\tif (HighestConfidence < Confidence)\n\t\t\tHighestConfidence = Confidence;\n\n\t\t// We update the best pattern match (first: best match so far has lower confidence than the current pose)\n\t\tif (ClosestHandPoseConfidence < Confidence)\n\t\t{\n\t\t\t// Second: checking for custom pose error ceiling\n\t\t\tif (Poses[PatternIndex].CustomConfidenceFloor > 0.0f && Confidence < Poses[PatternIndex].CustomConfidenceFloor)\n\t\t\t\tcontinue;\n\n\t\t\tClosestHandPoseConfidence = Confidence;\n\t\t\tClosestHandPoseError = RawError;\n\n\t\t\tClosestHandPose = PatternIndex;\n\t\t}\n\t}\n\n\t// If we have no match, we report the highest confidence seen.\n\tif (ClosestHandPose == -1)\n\t{\n\t\tClosestHandPoseConfidence = HighestConfidence;\n\t}\n\n\tif (CurrentHandPose == ClosestHandPose)\n\t{\n\t\t// Same pose as before is being held\n\t\tCurrentHandPoseDuration += TimeSinceLastRecognition;\n\t\tTimeSinceLastRecognition = 0.0;\n\t\tCurrentHandPoseConfidence = DampingFactor * CurrentHandPoseConfidence + (1.0f - DampingFactor) * ClosestHandPoseConfidence;\n\t\tCurrentHandPoseError = DampingFactor * CurrentHandPoseError + (1.0f - DampingFactor) * ClosestHandPoseError;\n\t}\n\telse\n\t{\n\t\t// Change of pose\n\t\tCurrentHandPose = ClosestHandPose;\n\t\tCurrentHandPoseDuration = 0.0f;\n\t\tCurrentHandPoseConfidence = ClosestHandPoseConfidence;\n\t\tCurrentHandPoseError = ClosestHandPoseError;\n\t}\n}\n\nbool UHandPoseRecognizer::GetRecognizedHandPose(int& Index, FString& Name, float& Duration, float& Error, float& Confidence)\n{\n\tIndex = CurrentHandPose;\n\tDuration = CurrentHandPoseDuration;\n\tError = CurrentHandPoseError;\n\tConfidence = CurrentHandPoseConfidence;\n\n\tif (Index >= 0 && Index < Poses.Num())\n\t{\n\t\tName = Poses[Index].PoseName;\n\t\treturn true;\n\t}\n\tName = TEXT(\"None\");\n\treturn false;\n}\n\nvoid UHandPoseRecognizer::LogEncodedHandPose()\n{\n\tPose.Encode();\n\tUE_LOG(LogHandPoseRecognition, Warning, TEXT(\"HAND POSE %d: %s\"), LoggedIndex++, *Pose.CustomEncodedPose);\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandRecognitionFunctionLibrary.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandRecognitionFunctionLibrary.h\"\n#include \"Engine.h\"\n#include \"FWaitForHandPoseAction.h\"\n#include \"FWaitForHandGestureAction.h\"\n#include \"FRecordHandPoseAction.h\"\n\nvoid UHandRecognitionFunctionLibrary::WaitForHandPose(\n\tUObject* WorldContextObject,\n\tUHandPoseRecognizer* HandPoseRecognizer,\n\tfloat PoseMinDuration, float TimeToWait,\n\tEWaitForHandPoseExitType& OutExecs,\n\tint& PoseIndex, FString& PoseName,\n\tFLatentActionInfo LatentInfo)\n{\n\tif (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))\n\t{\n\t\tauto& LatentActionManager = World->GetLatentActionManager();\n\t\tif (LatentActionManager.FindExistingAction<FWaitForHandPoseAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)\n\t\t{\n\t\t\tLatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID,\n\t\t\t\tnew FWaitForHandPoseAction(LatentInfo, HandPoseRecognizer, PoseMinDuration, TimeToWait, &PoseIndex, &PoseName, &OutExecs));\n\t\t}\n\t}\n}\n\nvoid UHandRecognitionFunctionLibrary::WaitForHandGesture(\n\tUObject* WorldContextObject,\n\tUHandGestureRecognizer* HandGestureRecognizer,\n\tfloat TimeToWait, EGestureConsumptionBehavior Behavior,\n\tEWaitForHandGestureExitType& OutExecs,\n\tint& GestureIndex, FString& GestureName, FVector& GestureDirection, float& GestureOuterDuration, float& GestureInnerDuration,\n\tFLatentActionInfo LatentInfo)\n{\n\tif (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))\n\t{\n\t\tauto& LatentActionManager = World->GetLatentActionManager();\n\t\tif (LatentActionManager.FindExistingAction<FWaitForHandGestureAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)\n\t\t{\n\t\t\tLatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID,\n\t\t\t\tnew FWaitForHandGestureAction(LatentInfo, HandGestureRecognizer, TimeToWait, Behavior, &GestureIndex, &GestureName, &GestureDirection, &GestureOuterDuration, &GestureInnerDuration, &OutExecs));\n\t\t}\n\t}\n}\n\nvoid UHandRecognitionFunctionLibrary::RecordHandPose(UObject* WorldContextObject, UHandPoseRecognizer* Recognizer, const ERecordHandPoseEntryType& InExecs, ERecordHandPoseExitType& OutExecs, FLatentActionInfo LatentInfo)\n{\n\tif (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))\n\t{\n\t\tauto& LatentActionManager = World->GetLatentActionManager();\n\t\tif (LatentActionManager.FindExistingAction<FRecordHandPoseAction>(LatentInfo.CallbackTarget, LatentInfo.UUID) == nullptr)\n\t\t{\n\t\t\tLatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FRecordHandPoseAction(LatentInfo, Recognizer, &InExecs, &OutExecs));\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/HandRecognitionGameMode.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandRecognitionGameMode.h\"\n#include \"OculusHandPoseRecognitionModule.h\"\n#include \"OculusXRInputFunctionLibrary.h\"\n#include \"OculusXRHandComponent.h\"\n#include \"HandPose.h\"\n\nint AHandRecognitionGameMode::LoggedIndex = 0;\n\nvoid AHandRecognitionGameMode::LogHandPose(FString Side)\n{\n\tauto const World = GetWorld();\n\tif (!World)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"LogHandPose failed to find a valid world.\"));\n\t\treturn;\n\t}\n\n\tauto const Controller = World->GetFirstPlayerController();\n\tif (!Controller)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"LogHandPose failed to find a player controller.\"));\n\t\treturn;\n\t}\n\n\tauto const Pawn = Controller->GetPawn();\n\tif (!Pawn)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"LogHandPose failed to find a pawn.\"));\n\t\treturn;\n\t}\n\n\t// We use the first matching OculusHandComponent.\n\tauto HandType = EOculusXRHandType::None;\n\tif (Side.Equals(TEXT(\"left\"), ESearchCase::IgnoreCase))\n\t\tHandType = EOculusXRHandType::HandLeft;\n\telse if (Side.Equals(TEXT(\"right\"), ESearchCase::IgnoreCase))\n\t\tHandType = EOculusXRHandType::HandRight;\n\n\tif (HandType == EOculusXRHandType::None)\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"LogHandPose requires \\\"left\\\" or \\\"right\\\" parameter, but received \\\"%s\\\"\"), *Side);\n\t\treturn;\n\t}\n\n\tTArray<UOculusXRHandComponent*> OculusHandComponents;\n\tPawn->GetComponents(OculusHandComponents);\n\tfor (auto HandComponent : OculusHandComponents)\n\t{\n\t\tif (HandComponent->SkeletonType == HandType)\n\t\t{\n\t\t\tFHandPose Pose;\n\t\t\tPose.UpdatePose(HandType, HandComponent->GetComponentRotation());\n\t\t\tPose.Encode();\n\t\t\tUE_LOG(LogHandPoseRecognition, Warning, TEXT(\"HAND POSE %d: %s\"), LoggedIndex++, *Pose.CustomEncodedPose);\n\t\t\treturn;\n\t\t}\n\t}\n\n\tUE_LOG(LogHandPoseRecognition, Error, TEXT(\"LogHandPose did not find a valid OculusHandComponent on the %s side\"), *Side);\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/OculusHandPoseRecognitionModule.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"OculusHandPoseRecognitionModule.h\"\n\n#define LOCTEXT_NAMESPACE \"FOculusHandPoseRecognitionModule\"\n\nDEFINE_LOG_CATEGORY(LogHandPoseRecognition);\n\n#include \"OculusDeveloperTelemetry.h\"\n\nOCULUS_TELEMETRY_LOAD_MODULE(\"Unreal-OculusHandPoseRecognition\");\n\nvoid FOculusHandPoseRecognitionModule::StartupModule()\n{\n\t// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module\n}\n\nvoid FOculusHandPoseRecognitionModule::ShutdownModule()\n{\n\t// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,\n\t// we call this function before unloading the module.\n}\n\n#undef LOCTEXT_NAMESPACE\n\nIMPLEMENT_MODULE(FOculusHandPoseRecognitionModule, OculusHandPoseRecognition)\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Private/PoseableHandComponent.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"PoseableHandComponent.h\"\n#include \"OculusHandPoseRecognitionModule.h\"\n\nvoid UPoseableHandComponent::BeginPlay()\n{\n\tif (GetSkinnedAsset() != nullptr)\n\t{\n\t\tbCustomPoseableHandMesh = true;\n\t}\n\n\tSuper::BeginPlay();\n}\n\nvoid UPoseableHandComponent::TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)\n{\n\tSuper::TickComponent(DeltaTime, TickType, ThisTickFunction);\n\n\tif (CustomPoseWeightCurrent != CustomPoseWeightTarget)\n\t{\n\t\tCustomPoseWeightCurrent = FMath::FInterpConstantTo(CustomPoseWeightCurrent, CustomPoseWeightTarget, DeltaTime, CustomPoseWeightLerpSpeed);\n\t}\n\n\tif (CustomPoseWeightCurrent > 0.f)\n\t{\n\t\tUpdateBonePose(EOculusXRBone::Thumb_0, Thumb_0);\n\t\tUpdateBonePose(EOculusXRBone::Thumb_1, Thumb_1);\n\t\tUpdateBonePose(EOculusXRBone::Thumb_2, Thumb_2);\n\t\tUpdateBonePose(EOculusXRBone::Thumb_3, Thumb_3);\n\n\t\tUpdateBonePose(EOculusXRBone::Index_1, Index_1);\n\t\tUpdateBonePose(EOculusXRBone::Index_2, Index_2);\n\t\tUpdateBonePose(EOculusXRBone::Index_3, Index_3);\n\n\t\tUpdateBonePose(EOculusXRBone::Middle_1, Middle_1);\n\t\tUpdateBonePose(EOculusXRBone::Middle_2, Middle_2);\n\t\tUpdateBonePose(EOculusXRBone::Middle_3, Middle_3);\n\n\t\tUpdateBonePose(EOculusXRBone::Ring_1, Ring_1);\n\t\tUpdateBonePose(EOculusXRBone::Ring_2, Ring_2);\n\t\tUpdateBonePose(EOculusXRBone::Ring_3, Ring_3);\n\n\t\tUpdateBonePose(EOculusXRBone::Pinky_0, Pinky_0);\n\t\tUpdateBonePose(EOculusXRBone::Pinky_1, Pinky_1);\n\t\tUpdateBonePose(EOculusXRBone::Pinky_2, Pinky_2);\n\t\tUpdateBonePose(EOculusXRBone::Pinky_3, Pinky_3);\n\t}\n}\n\nvoid UPoseableHandComponent::UpdateBonePose(EOculusXRBone Bone, ERecognizedBone PosedBone)\n{\n\tauto BoneIndex = (int)Bone;\n\n\tif (bCustomPoseableHandMesh && BoneNameMappings.Contains(Bone))\n\t{\n\t\tauto const MappedBoneIndex = GetSkinnedAsset()->GetRefSkeleton().FindBoneIndex(BoneNameMappings[Bone]);\n\t\tif (MappedBoneIndex != INDEX_NONE)\n\t\t{\n\t\t\tBoneIndex = MappedBoneIndex;\n\t\t}\n\t}\n\n\tif (BoneSpaceTransforms.Num() > BoneIndex)\n\t{\n\t\tauto const TrackedRotation = BoneSpaceTransforms[BoneIndex].GetRotation();\n\t\tauto const PosedRotation = CustomPose.GetRotator(PosedBone).Quaternion();\n\t\tauto const PosedWeight = CustomPose.GetWeight(PosedBone) * CustomPoseWeightCurrent;\n\n\t\tif (PosedWeight > 0)\n\t\t{\n\t\t\tauto const NewRotation = FQuat::Slerp(TrackedRotation, PosedRotation, PosedWeight);\n\t\t\tBoneSpaceTransforms[BoneIndex].SetRotation(NewRotation);\n\t\t}\n\t}\n}\n\n\nvoid UPoseableHandComponent::SetPose(FString PoseString, float LerpSpeedOverride)\n{\n\tCustomPose.CustomEncodedPose = PoseString;\n\n\n\tif (CustomPose.Decode())\n\t{\n\t\tCustomPoseWeightTarget = 1.f;\n\t\tCustomPoseWeightCurrent = 0.f;\n\t\tCustomPoseWeightLerpSpeed = LerpSpeedOverride > 0 ? LerpSpeedOverride : DefaultLerpSpeed;\n\t}\n\telse\n\t{\n\t\tUE_LOG(LogHandPoseRecognition, Warning, TEXT(\"Pose string '%s' can not be decoded.\"), *PoseString);\n\t}\n}\n\nvoid UPoseableHandComponent::ClearPose(float LerpSpeedOverride)\n{\n\tCustomPoseWeightTarget = 0.f;\n\tCustomPoseWeightLerpSpeed = LerpSpeedOverride > 0 ? LerpSpeedOverride : DefaultLerpSpeed;\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandGesture.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"HandGesture.generated.h\"\n\nclass UHandPoseRecognizer;\n\n/** Gesture progress. */\nUENUM(BlueprintType)\nenum class EGestureState : uint8\n{\n\tGestureNotStarted,\n\tGestureInProgress,\n\tGestureCompleted\n};\n\n\n/** A single pose with minimum duration. */\nstruct FHandGestureStep\n{\n\t// Values provided by the gesture description.\n\tint PoseIndex;\n\tfloat PoseMinDuration;\n\n\t// Timings acquired during recognition of this step.\n\tfloat StepFirstTime, StepLastTime;\n};\n\n/** A struct that represents a series of poses over time. */\nUSTRUCT(BlueprintType)\nstruct OCULUSHANDPOSERECOGNITION_API FHandGesture\n{\n\tGENERATED_BODY()\n\n\t/** Name for this gesture. */\n\tUPROPERTY(Category = \"Hand Gesture\", EditAnywhere, BlueprintReadWrite)\n\tFString GestureName;\n\n\t/** Tolerance (in seconds) for intermediate poses that do not match the sequence. */\n\tUPROPERTY(Category = \"Hand Gesture\", EditAnywhere, BlueprintReadWrite)\n\tfloat MaxTransitionTime{};\n\n\t/** Is this gesture representing a looping series of poses? (e.g. waving) */\n\tUPROPERTY(Category = \"Hand Gesture\", EditAnywhere, BlueprintReadWrite)\n\tbool bIsLooping{};\n\n\t/** Series of poses as a string. */\n\tUPROPERTY(Category = \"Hand Gesture\", EditAnywhere, BlueprintReadWrite)\n\tFString CustomEncodedGesture;\n\n\t/** If true, will log gesture updates and transitions. */\n\tUPROPERTY(Category = \"Hand Gesture\", EditAnywhere, BlueprintReadWrite)\n\tbool bGestureDebugLog = false;\n\n\t/**\n\t * Decodes the gesture string.\n\t * @param HandPoseRecognizer - Needed to identify poses by name.\n\t */\n\tbool ProcessEncodedGestureString(UHandPoseRecognizer* HandPoseRecognizer);\n\n\t/**\n\t * Called regularly with current pose information to recognize the gesture.\n\t * @param PoseIndex - HandPoseRecognizer current recognized pose index.\n\t * @param PoseDuration - How long this pose has been held.\n\t * @param DeltaTime - Time elapsed since last call.\n\t * @param CurrentTime - Current time.\n\t * @param Location - Current controller location.\n\t * @return A boolean that indicates if the gesture is recognized.\n\t */\n\tbool Step(int PoseIndex, float PoseDuration, float DeltaTime, float CurrentTime, const FVector& Location);\n\n\t/** Each pose has a first and last game time registered. */\n\tenum EPoseTimeType\n\t{\n\t\tPoseFirstTime,\n\t\tPoseLastTime\n\t};\n\n\t/**\n\t * Computes the time between two gestures steps.\n\t * By default, it computes the time taken from the end of the first pose and the last.\n\t * @param FirstPoseIndex - Index of the first pose\n\t * @param FirstPoseType - Time type of the first pose\n\t * @param SecondPoseIndex - Index of the second pose\n\t * @param SecondPoseType - Time type of the second pose\n\t * @return Duration to transition from the first and second poses.\n\t */\n\tfloat ComputeTransitionTime(int FirstPoseIndex = 0, EPoseTimeType FirstPoseType = PoseLastTime, int SecondPoseIndex = -1, EPoseTimeType SecondPoseType = PoseFirstTime) const;\n\n\t/** Returns the outer gesture duration. */\n\tfloat ComputeOuterDuration() const\n\t{\n\t\treturn ComputeTransitionTime(0, PoseFirstTime, -1, PoseLastTime);\n\t}\n\n\t/** Returns the outer gesture duration. */\n\tfloat ComputeInnerDuration() const\n\t{\n\t\treturn ComputeTransitionTime(0, PoseLastTime, -1, PoseFirstTime);\n\t}\n\n\t/**\n\t * Called to reset the gesture recognition state.\n\t * @param Force - Normally only resets when in progress or completed.\n\t */\n\tvoid Reset(bool Force = false);\n\n\t/**\n\t * Debug gesture with full log dump.\n\t * @param GestureIndex - Supplied by the gesture recognition system.\n\t * @param HandPoseRecognizer - Used to retrieve the pose name.\n\t */\n\tvoid DumpGestureState(int GestureIndex, const UHandPoseRecognizer* HandPoseRecognizer) const;\n\n\t/** Returns the gesture direction. */\n\tFVector GetGestureDirection() const\n\t{\n\t\treturn GestureEndLocation - GestureStartLocation;\n\t}\n\n\t/** Returns the gesture recognition state. */\n\tEGestureState GetGestureState() const\n\t{\n\t\treturn GestureState;\n\t}\n\nprotected:\n\t/** Array of decoded poses that represent the gesture. */\n\tTArray<FHandGestureStep> TimedPoses;\n\n\t/** Gesture progress. */\n\tEGestureState GestureState = EGestureState::GestureNotStarted;\n\n\tint CurrentStep = -1;\n\tFVector GestureStartLocation, GestureEndLocation;\n\tfloat DirectionBufferingFactor = 0.0f;\n\tfloat DurationInCurrentStep = 0.0f, DurationInTransition = 0.0f;\n\nprivate:\n\tstatic int FindPoseIndex(UHandPoseRecognizer* Recognizer, const FString& PoseName);\n\tstatic bool ReadTimedPose(UHandPoseRecognizer* Recognizer, const TCHAR** Buffer, int* PoseIndex, float* PoseMinDuration);\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandGestureRecognizer.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Components/ActorComponent.h\"\n#include \"HandGesture.h\"\n#include \"HandPoseRecognizer.h\"\n#include \"HandGestureRecognizer.generated.h\"\n\n/**\n * What to do with a gesture after it has been returned by GetRecognizedHandGesture().\n */\nUENUM(BlueprintType)\nenum class EGestureConsumptionBehavior : uint8\n{\n\tReset,\n\tResetAll,\n\tNoReset\n};\n\n/**\n * Bones for gesture strength settings.\n */\nUENUM(BlueprintType)\nenum class EGestureStrengthBone : uint8\n{\n\tThumb0 = 0,\n\tThumb1,\n\tThumb2,\n\tThumb3,\n\tIndex1,\n\tIndex2,\n\tIndex3,\n\tMiddle1,\n\tMiddle2,\n\tMiddle3,\n\tRing1,\n\tRing2,\n\tRing3,\n\tPinky0,\n\tPinky1,\n\tPinky2,\n\tPinky3,\n\tWrist\n};\n\n/**\n * Angles for gesture strength settings.\n */\nUENUM(BlueprintType)\nenum class EGestureStrengthBoneAngle : uint8\n{\n\tPitch = 0,\n\tYaw,\n\tRoll\n};\n\n/**\n * Actor component that recognizes gestures (i.e. poses over time).\n * \n * @warning Must be attached to a UHandPoseRecognizer.\n */\nUCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))\nclass OCULUSHANDPOSERECOGNITION_API UHandGestureRecognizer : public USceneComponent\n{\n\tGENERATED_BODY()\n\npublic:\n\t/** Sets default values for this component's properties. */\n\tUHandGestureRecognizer(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());\n\nprotected:\n\t/** Called when the game starts. */\n\tvirtual void BeginPlay() override;\n\n\t/** Parent hand pose recognizer. */\n\tUHandPoseRecognizer* HandPoseRecognizer = nullptr;\n\npublic:\n\t/** Called every frame. */\n\tvirtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;\n\n\t/** Minimum time interval between hand recognitions (throttling). */\n\tUPROPERTY(Category = \"Hand Gesture Recognition\", EditAnywhere, BlueprintReadWrite)\n\tfloat RecognitionInterval;\n\n\t/** Minimum number of frames to skip between hand recognitions (throttling). */\n\tUPROPERTY(Category = \"Hand Gesture Recognition\", EditAnywhere, BlueprintReadWrite)\n\tint RecognitionSkippedFrames;\n\n\t/** Collection of hand gestures to recognize. */\n\tUPROPERTY(Category = \"Hand Gesture Recognition\", EditAnywhere, BlueprintReadWrite)\n\tTArray<FHandGesture> Gestures;\n\n\t/**\n\t * Call to check if there is a recognized gesture pending.\n\t * @return A boolean that indicates if there's at least one pending gesture recognized.\n\t */\n\tUPROPERTY(Category = \"Hand Gesture Recognition\", BlueprintReadOnly)\n\tbool bHasRecognizedGesture;\n\n\t/**\n\t * Call to get the currently recognized gesture.\n\t * @param Behavior - Auto-reset of the behavior returned.\n\t * @param Index - Index of the recognized gesture.\n\t * @param Name - Name of the recognized gesture.\n\t * @param GestureDirection - Gesture vector, relative to owning actor.\n\t * @param GestureOuterDuration - Overall gesture duration.\n\t * @param GestureInnerDuration - Inner gesture duration.\n\t * @return A boolean that indicates if a gesture is currently recognized.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tUPARAM(DisplayName = \"Gesture Recognized\")\n\tbool GetRecognizedHandGesture(\n\t\tEGestureConsumptionBehavior Behavior,\n\t\tint& Index, FString& Name,\n\t\tFVector& GestureDirection,\n\t\tfloat& GestureOuterDuration, float& GestureInnerDuration);\n\n\t/**\n\t * Call to get the state of recognition of a specific gesture.\n\t * @param Index - Index of the gesture to query.\n\t * @return Current state of gesture recognition.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tUPARAM(DisplayName = \"Gesture State\")\n\tEGestureState GetGestureRecognitionState(int Index);\n\n\t/**\n\t * Resets the specified hand gesture by index.\n\t * @param Index - Where to store the index of the recognized gesture.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tvoid ResetHandGesture(int& Index);\n\n\t/** Resets all hand gestures. */\n\tUFUNCTION(BlueprintCallable)\n\tvoid ResetAllHandGestures();\n\n\t/** For debugging purposes: a state dump of all hand gestures. */\n\tUFUNCTION(BlueprintCallable)\n\tvoid DumpAllGestureStates() const;\n\nprivate:\n\t/** Recognition state. */\n\tint SkippedFramesSinceLastRecognition;\n\tfloat TimeSinceLastRecognition;\n\n\t/** Gestures that were completed in the last tick. */\n\tTArray<int> CompletedGestures;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandPose.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"OculusXRInputFunctionLibrary.h\"\n#include \"HandPose.generated.h\"\n\n/** Bones that we care about. */\nUENUM()\nenum ERecognizedBone\n{\n\tThumb_0 = 0,\n\tThumb_1,\n\tThumb_2,\n\tThumb_3,\n\tIndex_1,\n\tIndex_2,\n\tIndex_3,\n\tMiddle_1,\n\tMiddle_2,\n\tMiddle_3,\n\tRing_1,\n\tRing_2,\n\tRing_3,\n\tPinky_0,\n\tPinky_1,\n\tPinky_2,\n\tPinky_3,\n\tWrist,\n\tNUM\n};\n\n/** A struct that represents a hand pose. */\nUSTRUCT(BlueprintType)\nstruct OCULUSHANDPOSERECOGNITION_API FHandPose\n{\n\tGENERATED_BODY()\n\npublic:\n\t/** Name for this pose. */\n\tUPROPERTY(Category = \"Hand Pose\", EditAnywhere, BlueprintReadWrite)\n\tFString PoseName;\n\n\t/** Hand pose encoded in a string. */\n\tUPROPERTY(Category = \"Hand Pose\", EditAnywhere, BlueprintReadWrite)\n\tFString CustomEncodedPose;\n\n\t/** Hand pose custom confidence floor, when greater than 0. */\n\tUPROPERTY(Category = \"Hand Pose\", EditAnywhere, BlueprintReadWrite)\n\tfloat CustomConfidenceFloor = 0.0f;\n\n\t/**\n\t * Hand pose error at max confidence of 1.0.\n\t * Confidence of 0.50 corresponds to twice this error.\n\t * Confidence of 0.25 corresponds to four times this error.\n\t * Confidence of 0.10 corresponds to ten times this error.\n\t */\n\tUPROPERTY(Category = \"Hand Pose\", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = \"100.0\", UIMin = \"100.0\"))\n\tfloat ErrorAtMaxConfidence = 2000.0f;\n\n\t/** Returns the side that this */\n\tEOculusXRHandType GetHandType() const\n\t{\n\t\treturn Hand;\n\t}\n\n\t/**\n\t * Rotator access.\n\t * @param Bone\n\t * @return A const reference to the rotator of that bone.\n\t */\n\tconst FRotator& GetRotator(ERecognizedBone Bone) const\n\t{\n\t\treturn Rotations[Bone];\n\t}\n\n\t/**\n\t* Weight access.\n\t* @param Bone\n\t* @return The weight of the given bone.\n\t*/\n\tfloat GetWeight(ERecognizedBone Bone) const\n\t{\n\t\treturn Weights[Bone];\n\t}\n\n\t/**\n\t * Updates the structure with the current bone rotations for the side specified.\n\t * Wrist information is received from the HandPoseRecognizer.\n\t *\n\t * @param Side - EOculusXRHandType to track\n\t * @param Wrist - FRotator from the controller.\n\t */\n\tvoid UpdatePose(EOculusXRHandType Hand, FRotator Wrist);\n\n\t/** Encodes rotators to string form, without weights. */\n\tvoid Encode();\n\n\t/**\n\t * Decodes the encoded pose string into rotators and weights.\n\t * This is always called to configure references poses.\n\t * @return True if there were no issues during decoding.\n\t */\n\tbool Decode();\n\n\t/**\n\t * Computes the confidence of the other pose being similar to this reference pose.\n\t * @param Other - The hand pose to evaluate.\n\t * @param RawError - The raw error (optional).\n\t * @return The confidence level.\n\t * @warning You should call this method on a reference pose.\n\t */\n\tfloat ComputeConfidence(const FHandPose& Other, float* RawError = nullptr) const;\n\n\t/**\n\t * Used for average bone rotations: weighted transition to another pose.\n\t * @param Other - Hand pose.\n\t * @param OtherRatio - How much of the other pose we take.\n\t */\n\tvoid AddWeighted(const FHandPose& Other, float OtherRatio);\n\n\t/**\n\t * Used to record the minimum bone angles.\n\t * Copies all pitch/yaw/roll angles from Other that are smaller that our current ones.\n \t * @param Other - Hand pose.\n\t */\n\tvoid Min(const FHandPose& Other);\n\n\t/**\n\t * Used to record the maximum bone angles.\n\t * Copies all pitch/yaw/roll angles from Other that are greater that our current ones.\n\t * @param Other - Hand pose.\n\t */\n\tvoid Max(const FHandPose& Other);\n\nprotected:\n\t/** Hand side that will be set during parsing. */\n\tEOculusXRHandType Hand = EOculusXRHandType::None;\n\n\t/** Hand bone rotators. */\n\tFRotator Rotations[NUM];\n\n\t/** Hand bone weights. */\n\tfloat Weights[NUM] = {};\n\nprivate:\n\tstatic int NormalizedOutputAngle(float Angle);\n\tstatic FString FmtRot(FString Prefix, FRotator R);\n\tstatic void SkipWhitespace(const TCHAR** Buffer);\n\tstatic bool ReadRotComp(const TCHAR** Buffer, double* RotComp);\n\tstatic bool ReadWeight(const TCHAR** Buffer, float* Weight);\n\tstatic bool ReadRot(const TCHAR** Buffer, const TCHAR* Prefix, FRotator& R, float& Weight);\n\tstatic float ComputeAngleError(float Ref, float Angle);\n\tstatic float RotError(ERecognizedBone Bone, const FRotator* RefRot, const float* RefWeight, const FRotator* OtherRot);\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandPoseRecognizer.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Components/ActorComponent.h\"\n#include \"HandPose.h\"\n#include \"OculusXRHandComponent.h\"\n#include \"OculusXRInputFunctionLibrary.h\"\n#include \"HandPoseRecognizer.generated.h\"\n\n/**\n * Actor component that recognizes hand poses.\n *\n * @warning Must be attached to a component that moves with hands.\n */\nUCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))\nclass OCULUSHANDPOSERECOGNITION_API UHandPoseRecognizer : public USceneComponent\n{\n\tGENERATED_BODY()\n\npublic:\n\t/** Sets default values for this component's properties. */\n\tUHandPoseRecognizer(const FObjectInitializer& ObjectInitializer = FObjectInitializer::Get());\n\nprotected:\n\t/** Called when the game starts. */\n\tvirtual void BeginPlay() override;\n\npublic:\n\t/** Called every frame. */\n\tvirtual void TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;\n\n\t/** Hand side recognized (set to None to disable the component). */\n\tUPROPERTY(Category = \"Hand Pose Recognition\", EditAnywhere, BlueprintReadWrite)\n\tEOculusXRHandType Side;\n\n\t/** Minimum time interval between hand recognitions (throttling). */\n\tUPROPERTY(Category = \"Hand Pose Recognition\", EditAnywhere, BlueprintReadWrite)\n\tfloat RecognitionInterval;\n\n\t/** Minimum confidence level needed to recognize a pose.  Can be overridden for each individual pose. */\n\tUPROPERTY(Category = \"Hand Pose Recognition\", EditAnywhere, BlueprintReadWrite)\n\tfloat DefaultConfidenceFloor;\n\n\t/** Fraction of the current confidence level that we keep per unit of time. */\n\tUPROPERTY(Category = \"Hand Pose Recognition\", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = \"0.0\", ClampMax = \"1.0\", UIMin = \"0.0\", UIMax = \"1.0\"))\n\tfloat DampingFactor;\n\n\t/** Recognized hand pose patterns. */\n\tUPROPERTY(Category = \"Hand Pose Recognition\", EditAnywhere, BlueprintReadWrite)\n\tTArray<FHandPose> Poses;\n\n\t/**\n\t * Call to get the currently recognized hand pose.\n\t * @param Index - Index of the recognized pose.\n\t * @param Name - Name of the recognized pose.\n\t * @param Duration - How long this pose has been held.\n\t * @param Error - Raw recognition error.\n\t * @param Confidence - Confidence level from 0.0 to 1.0.\n\t * @return A boolean that indicates if a pose is currently recognized.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tUPARAM(DisplayName = \"Pose Recognized\")\n\tbool GetRecognizedHandPose(int& Index, FString& Name, float& Duration, float& Error, float& Confidence);\n\n\t/** Access to the last hand pose information. */\n\tconst FHandPose& GetCurrentPose() const\n\t{\n\t\treturn Pose;\n\t}\n\n\t/**\n\t * Call to log the current hand pose.\n\t * This is used to create reference poses that can then be tweaked.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tvoid LogEncodedHandPose();\n\nprotected:\n\t/** Structure storing the current bone rotators. */\n\tFHandPose Pose;\n\n\t/**\n\t * Returns the wrist rotation with yaw adjusted relative to the player's gaze.\n\t * @param ComponentQuat - Quaternion representing the controller rotation.\n\t * @return The adjusted rotator.\n\t */\n\tFRotator GetWristRotator(FQuat ComponentQuat) const;\n\nprivate:\n\t/** Recognition state. */\n\tfloat TimeSinceLastRecognition;\n\tint CurrentHandPose;\n\tfloat CurrentHandPoseDuration;\n\tfloat CurrentHandPoseConfidence;\n\tfloat CurrentHandPoseError;\n\n\t/** Index incremented every time LogEncodedHandPose() is called, to help identify reference poses in the logs. */\n\tint LoggedIndex;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandRecognitionFunctionLibrary.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Kismet/BlueprintFunctionLibrary.h\"\n#include \"HandRecognitionFunctionLibrary.generated.h\"\n\nclass UHandPoseRecognizer;\nclass UHandGestureRecognizer;\n\n/** Output exec pins for the WaitForHandPose blueprint node. */\nUENUM(BlueprintType)\nenum class EWaitForHandPoseExitType : uint8\n{\n\tPoseSeen,\n\tTimeOut,\n};\n\n/** Output exec pins for the WaitForHandGesture blueprint node. */\nUENUM(BlueprintType)\nenum class EWaitForHandGestureExitType : uint8\n{\n\tGestureSeen,\n\tTimeOut,\n};\n\n/** Input/output exec pins for the RecordHandPose blueprint node. */\nUENUM(BlueprintType)\nenum class ERecordHandPoseEntryType : uint8\n{\n\tStartRecording,\n\tStopRecording,\n};\n\nUENUM(BlueprintType)\nenum class ERecordHandPoseExitType : uint8\n{\n\tRecordingStarted,\n\tRecordingStopped,\n};\n\n/** Library for all hand pose recognition nodes. */\nUCLASS()\nclass OCULUSHANDPOSERECOGNITION_API UHandRecognitionFunctionLibrary : public UBlueprintFunctionLibrary\n{\n\tGENERATED_BODY()\n\npublic:\n\t/**\n\t * Waits for a hand pose to be recognized.\n\t * @param WorldContextObject - World context\n\t * @param HandPoseRecognizer - The hand pose recognizer component to wait on.\n\t * @param PoseMinDuration - The pose must be maintained for this amount of time to be recognized.\n\t * @param TimeToWait - The node will exit with TimeOut if no pose is recognized in this amount of time.  Use negative seconds to never time out.\n\t * @param PoseIndex - Recognized pose index.\n\t * @param PoseName - Recognized pose name.\n\t * @param LatentInfo - For latent node\n\t * @param OutExecs - Exit pins.\n\t */\n\tUFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = \"LatentInfo\", WorldContext = \"WorldContextObject\", ExpandEnumAsExecs = \"OutExecs\", PoseMinDuration = \"0\", TimeToWait = \"-1\"), Category = \"Hand Recognition\")\n\tstatic void WaitForHandPose(\n\t\tUObject* WorldContextObject,\n\t\tUHandPoseRecognizer* HandPoseRecognizer,\n\t\tfloat PoseMinDuration, float TimeToWait,\n\t\tEWaitForHandPoseExitType& OutExecs,\n\t\tint& PoseIndex, FString& PoseName,\n\t\tFLatentActionInfo LatentInfo);\n\n\t/**\n\t * Waits for a hand gesture to be recognized.\n\t * @param WorldContextObject - world context\n\t * @param HandGestureRecognizer - The hand gesture recognizer component to wait on.\n\t * @param TimeToWait - The node will exit with TimeOut if no gesture is recognized in this amount of time.  Use negative seconds to never time out.\n\t * @param Behavior - Auto-reset of the behavior returned.\n\t * @param GestureIndex - Recognized gesture index.\n\t * @param GestureName - Recognized gesture name.\n\t * @param GestureDirection - Recognized gesture direction.\n\t * @param GestureOuterDuration - Recognized outer gesture duration\n\t * @param GestureInnerDuration - Recognized inner gesture duration\n\t * @param LatentInfo - for latent node\n\t * @param OutExecs - Exit pins.\n\t */\n\tUFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = \"LatentInfo\", WorldContext = \"WorldContextObject\", ExpandEnumAsExecs = \"OutExecs\", PoseMinDuration = \"0\", TimeToWait = \"-1\"), Category = \"Hand Recognition\")\n\tstatic void WaitForHandGesture(\n\t\tUObject* WorldContextObject,\n\t\tUHandGestureRecognizer* HandGestureRecognizer,\n\t\tfloat TimeToWait, EGestureConsumptionBehavior Behavior,\n\t\tEWaitForHandGestureExitType& OutExecs,\n\t\tint& GestureIndex, FString& GestureName, FVector& GestureDirection, float& GestureOuterDuration, float& GestureInnerDuration,\n\t\tFLatentActionInfo LatentInfo);\n\n\t/**\n\t * Records the range of hand pose to be recognized.\n\t * @param WorldContextObject - World context\n\t * @param Recognizer - HandPoseRecognizer used for hand side and wrist information.\n\t * @param InExecs - Entry pins.\n\t * @param OutExecs - Exit pins.\n\t * @param LatentInfo - For latent node\n\t */\n\tUFUNCTION(BlueprintCallable, meta = (Latent, LatentInfo = \"LatentInfo\", WorldContext = \"WorldContextObject\", ExpandEnumAsExecs = \"InExecs,OutExecs\"), Category = \"Hand Recognition\")\n\tstatic void RecordHandPose(UObject* WorldContextObject, UHandPoseRecognizer* Recognizer, const ERecordHandPoseEntryType& InExecs, ERecordHandPoseExitType& OutExecs, FLatentActionInfo LatentInfo);\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/HandRecognitionGameMode.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/GameModeBase.h\"\n#include \"Misc/CoreMisc.h\"\n#include \"HandRecognitionGameMode.generated.h\"\n\n/**\n * A game mode that offers some console commands to write poses to the log.\n */\nUCLASS()\nclass OCULUSHANDPOSERECOGNITION_API AHandRecognitionGameMode : public AGameModeBase\n{\n\tGENERATED_BODY()\n\npublic:\n\t/**\n\t * Prints a hand pose string in the output log.\n\t * @param Side - Either \"left\" or \"right\", case insensitive.\n\t */\n\tUFUNCTION(Exec)\n\tvoid LogHandPose(FString Side);\n\nprivate:\n\t/** Helps keep track of poses logged. */\n\tstatic int LoggedIndex;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/OculusHandPoseRecognitionModule.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Modules/ModuleManager.h\"\n\nDECLARE_LOG_CATEGORY_EXTERN(LogHandPoseRecognition, Log, All);\n\nclass FOculusHandPoseRecognitionModule : public IModuleInterface\n{\npublic:\n\t/** IModuleInterface implementation */\n\tvirtual void StartupModule() override;\n\tvirtual void ShutdownModule() override;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusHandPoseRecognition/Public/PoseableHandComponent.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"OculusXRHandComponent.h\"\n#include \"HandPose.h\"\n\n#include \"PoseableHandComponent.generated.h\"\n\nUCLASS(Blueprintable, meta = (BlueprintSpawnableComponent), ClassGroup = OculusHand)\nclass OCULUSHANDPOSERECOGNITION_API UPoseableHandComponent : public UOculusXRHandComponent\n{\n\tGENERATED_BODY()\n\npublic:\n\tvirtual void BeginPlay() override;\n\n\tvirtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;\n\n\tUFUNCTION(BlueprintCallable)\n\tvoid SetPose(FString PoseString, float LerpSpeedOverride = -1.f);\n\n\tUFUNCTION(BlueprintCallable)\n\tvoid ClearPose(float LerpSpeedOverride = -1.f);\n\n\tUPROPERTY(EditAnywhere, BlueprintReadWrite)\n\tfloat DefaultLerpSpeed = 10.f;\n\nprotected:\n\tbool bCustomPoseableHandMesh = false;\n\nprivate:\n\tvoid UpdateBonePose(EOculusXRBone Bone, ERecognizedBone PosedBone);\n\n\tfloat CustomPoseWeightCurrent;\n\tfloat CustomPoseWeightTarget;\n\tfloat CustomPoseWeightLerpSpeed;\n\n\tFHandPose CustomPose;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/OculusInteractable.Build.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\n\npublic class OculusInteractable : ModuleRules\n{\n\tpublic OculusInteractable (ReadOnlyTargetRules Target) : base(Target)\n\t{\n\t\tPCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;\n\n\t\tPublicIncludePaths.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t// ... add public include paths required here ...\n\t\t\t}\n\t\t\t);\n\n\n\t\tPrivateIncludePaths.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t// ... add other private include paths required here ...\n\t\t\t}\n\t\t\t);\n\n\n\t\tPublicDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"Core\",\n\t\t\t\t// ... add other public dependencies that you statically link with here ...\n\t\t\t}\n\t\t\t);\n\n\n\t\tPrivateDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"CoreUObject\",\n\t\t\t\t\"Engine\",\n\t\t\t\t\"Slate\",\n\t\t\t\t\"SlateCore\",\n\t\t\t\t\"OculusXRInput\",\n\t\t\t\t\"OculusUtils\",\n\t\t\t}\n\t\t\t);\n\n\n\t\tDynamicallyLoadedModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t// ... add any modules that your module loads dynamically here ...\n\t\t\t}\n\t\t\t);\n\n\t\tIncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6;\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/AimingActor.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"AimingActor.h\"\n\n// Sets default values\nAAimingActor::AAimingActor()\n{\n\t// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.\n\tPrimaryActorTick.bCanEverTick = true;\n}\n\n// Called when the game starts or when spawned\nvoid AAimingActor::BeginPlay()\n{\n\tSuper::BeginPlay();\n}\n\n// Called every frame\nvoid AAimingActor::Tick(float DeltaTime)\n{\n\tSuper::Tick(DeltaTime);\n}\n\n// Called when the actor is activated\nvoid AAimingActor::Activate_Implementation()\n{\n\tSetActorHiddenInGame(false);\n}\n\n// Called when the actor is deactivated\nvoid AAimingActor::Deactivate_Implementation(AInteractable* Selected)\n{\n\tSetActorHiddenInGame(true);\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/HandGrabbingComponent.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandGrabbingComponent.h\"\n\n#include \"Interactable.h\"\n#include \"Engine/OverlapResult.h\"\n\nAInteractable* UHandGrabbingComponent::TryGrab(FTransform GrabTransform)\n{\n\tauto HitResults = TArray<FOverlapResult>{};\n\tauto QueryParams = FCollisionQueryParams::DefaultQueryParam;\n\tauto ResponseParams = FCollisionResponseParams(ECR_Overlap);\n\n\tGetWorld()->OverlapMultiByChannel(HitResults, GrabTransform.GetLocation(), GrabTransform.GetRotation(), ECC_WorldDynamic, FCollisionShape::MakeCapsule(GrabCapsuleRadius, GrabCapsuleHeight / 2.f), FCollisionQueryParams::DefaultQueryParam, ResponseParams);\n\n\tif (HitResults.Num() > 0)\n\t{\n\t\tauto ClosestInteractable = (AInteractable*)nullptr;\n\t\tauto DistanceToClosestInteractable = 0.0f;\n\t\tfor (auto&& HitResult : HitResults)\n\t\t{\n\t\t\tif (auto HitActor = HitResult.GetActor())\n\t\t\t{\n\t\t\t\tauto Interactable = Cast<AInteractable>(HitActor);\n\t\t\t\tif (Interactable && Interactable->IsMovable())\n\t\t\t\t{\n\t\t\t\t\tauto Distance = FVector::Dist(GrabTransform.GetLocation(), Interactable->GetActorLocation());\n\t\t\t\t\tif (ClosestInteractable == nullptr || Distance < DistanceToClosestInteractable)\n\t\t\t\t\t{\n\t\t\t\t\t\tClosestInteractable = Interactable;\n\t\t\t\t\t\tDistanceToClosestInteractable = Distance;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (ClosestInteractable)\n\t\t{\n\t\t\tauto InteractableRoot = ClosestInteractable->GetRootComponent();\n\n\t\t\tif (auto OtherHand = Cast<UHandGrabbingComponent>(InteractableRoot->GetAttachParent()))\n\t\t\t{\n\t\t\t\tOtherHand->TryRelease();\n\t\t\t}\n\n\t\t\tif (InteractableRoot != nullptr)\n\t\t\t{\n\t\t\t\tauto GrabbedPrimitive = Cast<UPrimitiveComponent>(ClosestInteractable->GetRootComponent());\n\t\t\t\tif(GrabbedPrimitive != nullptr && GrabbedPrimitive->IsSimulatingPhysics())\n\t\t\t\t{\n\t\t\t\t\tbGrabbedActorHasPhysics = true;\n\t\t\t\t\tClosestInteractable->SetInteractablePhysicsSimulation(false);\n\t\t\t\t}\n\n\t\t\t\tClosestInteractable->Interaction1();\n\n\t\t\t\tGrabbedActor = ClosestInteractable;\n\t\t\t\tGrabbedActor->OnDestroyed.AddDynamic(this, &UHandGrabbingComponent::HandleHeldActorDestroyed);\n\t\t\t}\n\t\t}\n\t}\n\n\treturn GrabbedActor;\n}\n\nAInteractable* UHandGrabbingComponent::TryRelease(bool bReenablePhysics)\n{\n\tauto const ReleasedActor = GrabbedActor;\n\tif (GrabbedActor != nullptr)\n\t{\n\t\tGrabbedActor->OnDestroyed.RemoveDynamic(this, &UHandGrabbingComponent::HandleHeldActorDestroyed);\n\n\t\tif(bReenablePhysics && bGrabbedActorHasPhysics)\n\t\t{\n\t\t\tGrabbedActor->SetInteractablePhysicsSimulation(true);\n\t\t}\n\n\t\tGrabbedActor = nullptr;\n\t\tbGrabbedActorHasPhysics = false;\n\t}\n\treturn ReleasedActor;\n}\n\nvoid UHandGrabbingComponent::HandleHeldActorDestroyed(AActor* DestroyedActor)\n{\n\tif (DestroyedActor == GrabbedActor)\n\t{\n\t\tTryRelease();\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/Interactable.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"Interactable.h\"\n#include \"InteractableSelector.h\"\n#include \"TransformString.h\"\n#include \"OculusInteractableModule.h\"\n\nAInteractable::AInteractable()\n{\n\t// Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.\n\tPrimaryActorTick.bCanEverTick = true;\n\n\t// Defaults.\n\tFarFieldSelectionDelayMs = 100.0f;\n}\n\nvoid AInteractable::BeginPlay()\n{\n\tSuper::BeginPlay();\n}\n\nvoid AInteractable::EndPlay(const EEndPlayReason::Type EndPlayReason)\n{\n\twhile (Selectors.Num() > 0)\n\t{\n\t\tauto Selector = Selectors.Pop(EAllowShrinking::No);\n\t\tSelector->SetSelectedInteractable(nullptr, false); // Remove with no notification.\n\t}\n\n\tSuper::EndPlay(EndPlayReason);\n}\n\nvoid AInteractable::Tick(float DeltaTime)\n{\n\tSuper::Tick(DeltaTime);\n}\n\nvoid AInteractable::BeginSelection_Implementation(AInteractableSelector* Selector)\n{\n\t// Meant to be subclassed, but do not forget to call Super::BeginSelection_Implementation.\n\tif (!Selectors.Contains(Selector))\n\t{\n\t\tSelectors.Add(Selector);\n\t}\n}\n\nvoid AInteractable::EndSelection_Implementation(AInteractableSelector* Selector)\n{\n\t// Meant to be subclassed, but do not forget to call Super::EndSelection_Implementation.\n\tif (Selectors.Contains(Selector))\n\t{\n\t\tSelectors.Remove(Selector);\n\t}\n}\n\nbool AInteractable::IsSelected() const\n{\n\treturn Selectors.Num() > 0;\n}\n\nconst TArray<AInteractableSelector*>& AInteractable::GetSelectors() const\n{\n\treturn Selectors;\n}\n\nvoid AInteractable::SetInteractablePhysicsSimulation_Implementation(bool SimulatePhysics)\n{\n\tauto RootPrimitive = Cast<UPrimitiveComponent>(GetRootComponent());\n\tif (RootPrimitive)\n\t{\n\t\tRootPrimitive->SetSimulatePhysics(SimulatePhysics);\n\t}\n}\n\nbool AInteractable::IsMovable_Implementation()\n{\n\tauto const RootPrimitive = Cast<UPrimitiveComponent>(GetRootComponent());\n\tif (RootPrimitive)\n\t{\n\t\treturn RootPrimitive->Mobility == EComponentMobility::Movable;\n\t}\n\n\treturn false;\n}\n\nvoid AInteractable::SelectGrabPose(EHandSide Side, bool& GrabPoseFound, FString& GrabPoseName, FTransform& GrabTransform, FString& GrabHandPose)\n{\n\tauto& GrabPoses = Side == EHandSide::HandLeft ? GrabPosesLeftHand : GrabPosesRightHand;\n\n\tGrabPoseFound = GrabPoses.Num() > 0;\n\tif (GrabPoseFound)\n\t{\n\t\t// Random selection for this first pass.\n\t\tauto const RandomGrabIndex = FMath::RandRange(0, GrabPoses.Num() - 1);\n\n\t\tauto const EncodedTransform = GrabPoses[RandomGrabIndex].RelativeHandTransform;\n\n\t\tif (FTransformString::StringToTransform(EncodedTransform, GrabTransform))\n\t\t{\n\t\t\tGrabPoseName = GrabPoses[RandomGrabIndex].PoseName;\n\t\t\tGrabHandPose = GrabPoses[RandomGrabIndex].HandPose;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tUE_LOG(LogInteractable, Warning, TEXT(\"Invalid grab transform on %s, %s side at index %d: \\\"%s\\\"\"),\n\t\t\t\t*GetHumanReadableName(),\n\t\t\t\tSide == EHandSide::HandLeft ? TEXT(\"left\") : TEXT(\"right\"),\n\t\t\t\tRandomGrabIndex,\n\t\t\t\t*EncodedTransform);\n\t\t\tGrabPoseFound = false;\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/InteractableFunctionLibrary.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"InteractableFunctionLibrary.h\"\n#include \"OculusInteractableModule.h\"\n#include \"TransformString.h\"\n\nECollisionChannel UInteractableFunctionLibrary::InteractableTraceChannel = ECC_GameTraceChannel1;\n\nvoid UInteractableFunctionLibrary::LogNearbyInteractables(AActor* ReferenceActor, float RadiusAroundActor)\n{\n\tauto World = ReferenceActor ? ReferenceActor->GetWorld() : nullptr;\n\tif (!World)\n\t\treturn;\n\n\tauto CollisionSphere = FCollisionShape::MakeSphere(RadiusAroundActor);\n\tTArray<FHitResult> Hits;\n\tauto ReferenceTransform = ReferenceActor->GetTransform();\n\tauto Loc = ReferenceTransform.GetLocation();\n\n\tWorld->SweepMultiByChannel(Hits, Loc, Loc, FQuat::Identity, InteractableTraceChannel, CollisionSphere);\n\tif (Hits.Num() > 0)\n\t{\n\t\tUE_LOG(LogInteractable, Error, TEXT(\"Interactables near %s radius %0.2f\"),\n\t\t\t*ReferenceActor->GetHumanReadableName(), RadiusAroundActor);\n\n\t\tfor (const auto& Hit : Hits)\n\t\t{\n\t\t\tauto ActorTransform = Hit.GetActor()->GetTransform();\n\t\t\tFTransform RelativeActorTransform;\n\t\t\tRelativeActorTransform.SetLocation(ReferenceTransform.InverseTransformPosition(ActorTransform.GetLocation()));\n\t\t\tRelativeActorTransform.SetRotation(ReferenceTransform.InverseTransformRotation(ActorTransform.GetRotation()));\n\t\t\tRelativeActorTransform.SetScale3D(ActorTransform.GetScale3D());\n\n\t\t\tFString RelativeActorTransformString;\n\t\t\tFTransformString::TransformToString(RelativeActorTransform, RelativeActorTransformString);\n\n\t\t\tUE_LOG(LogInteractable, Warning, TEXT(\"Interactable %s %s\"),\n\t\t\t\t*Hit.GetActor()->GetHumanReadableName(),\n\t\t\t\t*RelativeActorTransformString);\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/InteractableSelector.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"InteractableSelector.h\"\n#include \"DrawDebugHelpers.h\"\n#include \"Kismet/GameplayStatics.h\"\n\nECollisionChannel AInteractableSelector::InteractableTraceChannel = ECC_GameTraceChannel1;\nFName AInteractableSelector::BeamSource(\"Source\");\nFName AInteractableSelector::BeamTarget(\"Target\");\n\nAInteractableSelector::AInteractableSelector()\n{\n\t// Property defaults\n\tbSelectorStartsActivated = false;\n\tNearFieldRadius = 10.0f;\n\tRaycastOffset = 0.0f;\n\tRaycastDistance = 1000.0f;\n\tRaycastAngleDegrees = 15.0f;\n\tRaycastStickinessAngleDegrees = 20.0f;\n\tDampeningFactor = 0.95f;\n\tAimingActorRotationRate = 0.1f;\n\tbRaycastDebugTrace = false;\n\n\t// State\n\tbAlignAimingActorWithHitNormal = false;\n\tbSelectorActivated = false;\n\tbAimingActorOwned = false;\n\tCandidatePreSelection = nullptr;\n\tCandidatePreSelectionTimeMs = 0.0f;\n\tSelectedInteractable = nullptr;\n\n\tPrimaryActorTick.bCanEverTick = true;\n\n\t// Root\n\tthis->RootComponent = CreateDefaultSubobject<USceneComponent>(TEXT(\"Root\"));\n\n#if WITH_EDITOR\n\tArrowComponent = CreateEditorOnlyDefaultSubobject<UArrowComponent>(TEXT(\"Arrow\"));\n\n\tif (!IsRunningCommandlet())\n\t{\n\t\tif (ArrowComponent)\n\t\t{\n\t\t\tArrowComponent->SetupAttachment(RootComponent);\n\t\t\tArrowComponent->ArrowColor = FColor(128, 92, 207);\n\t\t\tArrowComponent->ArrowSize = 1.0f;\n\t\t\tArrowComponent->bIsScreenSizeScaled = true;\n\t\t}\n\t}\n#endif //WITH_EDITOR\n}\n\nvoid AInteractableSelector::BeginPlay()\n{\n\tSuper::BeginPlay();\n\n\tBuildAimingActor();\n\tBuildBeam();\n\n\tif (bSelectorStartsActivated)\n\t{\n\t\tActivateSelector(true);\n\t}\n}\n\nvoid AInteractableSelector::EndPlay(EEndPlayReason::Type const EndPlayReason)\n{\n\tActivateSelector(false);\n\tDestroyBeam();\n\tDestroyAimingActor();\n\tSetSelectedInteractable(nullptr); // Notify selected interactable that we are going away.\n\n\tSuper::EndPlay(EndPlayReason);\n}\n\nvoid AInteractableSelector::Tick(float DeltaTime)\n{\n\tSuper::Tick(DeltaTime);\n\n\tauto TestShouldSelect = [this](TWeakObjectPtr<AActor> Actor)\n\t{\n\t\treturn ShouldSelect(Cast<AInteractable>(Actor.Get()));\n\t};\n\n\tif (bSelectorActivated)\n\t{\n\t\tauto const World = GetWorld();\n\t\tif (!World)\n\t\t{\n\t\t\treturn;\n\t\t}\n\n\t\tAInteractable* Candidate = nullptr;\n\t\tauto CandidateInNearField = false;\n\t\tFVector StartCast, EndCast;\n\t\tComputeNearFieldRaycastEndpoints(StartCast, EndCast);\n\t\tTArray<FHitResult> Hits;\n\n\t\tauto UpdateAimingActorTransform = [&]\n\t\t{\n\t\t\tWorld->LineTraceMultiByChannel(Hits, StartCast, EndCast, ECC_Visibility);\n\n\t\t\tauto const AimingQuat = AimingActor->GetActorQuat();\n\n\t\t\t// Default target aiming when we have not hit normal.\n\t\t\tauto const RightVector = FVector::CrossProduct(DampenedForwardVector, FVector::UpVector);\n\t\t\tauto ForwardVector = FVector::CrossProduct(StartCast - EndCast, RightVector);\n\t\t\tauto TargetAimingQuat = FQuat(FRotationMatrix::MakeFromXZ(ForwardVector, StartCast - EndCast).Rotator());\n\n\t\t\tif (Hits.Num() > 0)\n\t\t\t{\n\t\t\t\tNonInteractableActorHit = Hits[0].GetActor();\n\n\t\t\t\tif (bAlignAimingActorWithHitNormal)\n\t\t\t\t{\n\t\t\t\t\t// Align with hit normal.\n\t\t\t\t\tForwardVector = FVector::CrossProduct(Hits[0].ImpactNormal, RightVector);\n\t\t\t\t\tTargetAimingQuat = FQuat(FRotationMatrix::MakeFromXZ(ForwardVector, Hits[0].ImpactNormal).Rotator());\n\t\t\t\t}\n\n\t\t\t\tAimingActor->SetActorLocation(Hits[0].ImpactPoint);\n\t\t\t}\n\t\t\telse\n\t\t\t{\n\t\t\t\t// Place the actor at the end of the cast of nothing is hit.\n\t\t\t\tAimingActor->SetActorLocation(EndCast);\n\t\t\t}\n\n\t\t\tauto const LerpedTargetAimingQuat = FQuat::FastLerp(AimingQuat, TargetAimingQuat, AimingActorRotationRate);\n\t\t\tAimingActor->SetActorRotation(LerpedTargetAimingQuat);\n\t\t};\n\n\t\t// Near-field selection has priority.\n\t\tif (NearFieldRadius > 0.0f)\n\t\t{\n\t\t\tauto const NearFieldCollisionSphere = FCollisionShape::MakeSphere(NearFieldRadius);\n\t\t\tauto CandidateDistance = NearFieldRadius * 100.0f;\n\n\t\t\tWorld->SweepMultiByChannel(Hits, StartCast, StartCast, FQuat::Identity, InteractableTraceChannel, NearFieldCollisionSphere);\n\n\t\t\tif (Hits.Num() > 0)\n\t\t\t{\n\t\t\t\tfor (auto const& Hit : Hits)\n\t\t\t\t{\n\t\t\t\t\tif (Hit.GetActor() && Hit.GetActor()->GetClass()->IsChildOf(AInteractable::StaticClass()) && TestShouldSelect(Hit.GetActor()))\n\t\t\t\t\t{\n\t\t\t\t\t\tauto const Distance = FVector::Distance(Hit.GetActor()->GetActorLocation(), StartCast);\n\t\t\t\t\t\tif (CandidateDistance > Distance)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCandidateDistance = Distance;\n\t\t\t\t\t\t\tCandidate = Cast<AInteractable>(Hit.GetActor());\n\t\t\t\t\t\t\tCandidateInNearField = true;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\t// UE_LOG(LogTemp, Error, TEXT(\"Hit near field %s at %0.0f\"), *Hit.GetActor()->GetName(), CandidateDistance);\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// If no candidates found in the near-field, we perform far-field selection.\n\t\tif (!Candidate)\n\t\t{\n\t\t\tUpdateDampenedForwardVector(DampeningFactor);\n\t\t\tComputeFarFieldRaycastEndpoints(StartCast, EndCast);\n\n\t\t\tif (bRaycastDebugTrace)\n\t\t\t{\n\t\t\t\tDrawDebugLine(World, StartCast, EndCast, FColor::Green, false, -1.0f, 0, 0.1f);\n\t\t\t}\n\n\t\t\t// Trace in a cone against interactable.\n\t\t\tauto const SphereRadius = ComputeSphereRadiusForCast();\n\t\t\tauto const CollisionSphere = FCollisionShape::MakeSphere(SphereRadius);\n\t\t\tWorld->SweepMultiByChannel(Hits, StartCast, EndCast, FQuat::Identity, InteractableTraceChannel, CollisionSphere);\n\n\t\t\t// Looking for the closest candidate by angle and distance.\n\t\t\tauto CandidateAngle = RaycastAngleDegrees;\n\t\t\tauto CandidateDistance = RaycastDistance;\n\n\t\t\tif (Hits.Num() > 0)\n\t\t\t{\n\t\t\t\tfor (auto const& Hit : Hits)\n\t\t\t\t{\n\t\t\t\t\t// GEngine->AddOnScreenDebugMessage(-1, 0, FColor::Blue, FString::Printf(TEXT(\"Hit %s at %f\"), *Hit.GetActor()->GetName(), Hit.Distance));\n\t\t\t\t\tif (Hit.GetActor() && Hit.GetActor()->GetClass()->IsChildOf(AInteractable::StaticClass()) && TestShouldSelect(Hit.GetActor()))\n\t\t\t\t\t{\n\t\t\t\t\t\tauto const Angle = ComputeAngularDistance(Hit.GetActor());\n\n\t\t\t\t\t\t// Since CandidateAngle starts at RaycastAngleDegrees, we cannot select outside of the cone.\n\t\t\t\t\t\tif (Angle > CandidateAngle || Angle == CandidateAngle && Hit.Distance >= CandidateDistance)\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tcontinue;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tCandidateAngle = Angle;\n\t\t\t\t\t\tCandidateDistance = Hit.Distance;\n\t\t\t\t\t\tCandidate = Cast<AInteractable>(Hit.GetActor());\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Do not change the selection if it is within some angle.\n\t\t\tif (SelectedInteractable != nullptr && Candidate != nullptr)\n\t\t\t{\n\t\t\t\tauto const AngleToCurrentSelection = ComputeAngularDistance(SelectedInteractable);\n\t\t\t\tif (AngleToCurrentSelection <= RaycastStickinessAngleDegrees &&\n\t\t\t\t\tAngleToCurrentSelection < ComputeAngularDistance(Candidate))\n\t\t\t\t{\n\t\t\t\t\t// Re-orient the beam.\n\t\t\t\t\tOrientBeam();\n\t\t\t\t\tif (bAlwaysShowAimingActor)\n\t\t\t\t\t{\n\t\t\t\t\t\tUpdateAimingActorTransform();\n\t\t\t\t\t}\n\t\t\t\t\treturn;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif (!Candidate || bAlwaysShowAimingActor)\n\t\t{\n\t\t\tif (AimingActor)\n\t\t\t{\n\t\t\t\t// If there are no hits, we help by displaying the aiming actor.\n\t\t\t\tActivateAimingActor(true);\n\t\t\t\tUpdateAimingActorTransform();\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\t// Do no show the aiming actor when we hit an interactable.\n\t\t\tActivateAimingActor(false);\n\n\t\t\t// When we have a candidate, this first non-interactable actor hit is cleared.\n\t\t\tNonInteractableActorHit.Reset();\n\t\t}\n\n\t\t// Near-field selection is immediate.  , otherwise we update selection time on candidate in pre-selection.\n\t\tif (CandidateInNearField)\n\t\t{\n\t\t\tCandidatePreSelection = Candidate;\n\t\t\tCandidatePreSelectionTimeMs = Candidate->FarFieldSelectionDelayMs;\n\t\t}\n\t\telse if (CandidatePreSelection != Candidate)\n\t\t{\n\t\t\tCandidatePreSelection = Candidate;\n\t\t\tCandidatePreSelectionTimeMs = 0.0f;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tCandidatePreSelectionTimeMs += DeltaTime * 1000.0f;\n\t\t}\n\n\t\tSetSelectedInteractable(CandidatePreSelection, CandidatePreSelectionTimeMs, true, CandidateInNearField);\n\t}\n}\n\nvoid AInteractableSelector::SetSelectedInteractable(AInteractable* Candidate, float SelectionDurationMs /* = 0.0f */, bool Notify /* = true */, bool CandidateInNearField /* = false */)\n{\n\tauto const SameCandidate = SelectedInteractable == Candidate;\n\tauto const SameField = SelectedInteractableInNearField == CandidateInNearField;\n\n\t// Old interactable.\n\tif (SelectedInteractable)\n\t{\n\t\tif (SameCandidate)\n\t\t{\n\t\t\t// Same non-null target in the same field: just need to update beam orientation.\n\t\t\tif (SameField)\n\t\t\t{\n\t\t\t\tif (!CandidateInNearField)\n\t\t\t\t{\n\t\t\t\t\tOrientBeam();\n\t\t\t\t}\n\n\t\t\t\treturn;\n\t\t\t}\n\t\t}\n\t\telse\n\t\t{\n\t\t\tif (Notify)\n\t\t\t{\n\t\t\t\tSelectedInteractable->EndSelection(this);\n\t\t\t}\n\t\t}\n\t}\n\n\t// Activating beam on the candidate.\n\tTargetBeam(CandidateInNearField ? nullptr : Candidate);\n\n\t// Swapping to candidate.\n\tif (Candidate && Candidate->FarFieldSelectionDelayMs <= SelectionDurationMs)\n\t{\n\t\tSelectedInteractable = Candidate;\n\t\tSelectedInteractableInNearField = CandidateInNearField;\n\t}\n\telse\n\t{\n\t\tSelectedInteractable = nullptr;\n\t}\n\n\t// New interactable (potentially nullptr).\n\tif (SelectedInteractable && !SameCandidate && Notify)\n\t{\n\t\tSelectedInteractable->BeginSelection(this);\n\t}\n}\n\nAInteractable* AInteractableSelector::GetSelectedInteractable(bool& SelectedInNearField) const\n{\n\tSelectedInNearField = SelectedInteractableInNearField;\n\treturn SelectedInteractable;\n}\n\nAActor* AInteractableSelector::GetNonInteractableHit() const\n{\n\t// Can return null.\n\treturn NonInteractableActorHit.Get();\n}\n\nbool AInteractableSelector::ShouldSelect_Implementation(AInteractable* Interactable) const\n{\n\treturn true;\n}\n\nvoid AInteractableSelector::ActivateSelector(bool Activate)\n{\n\tif (bSelectorActivated != Activate)\n\t{\n\t\tif (Activate)\n\t\t{\n\t\t\tUpdateDampenedForwardVector(0.0f);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tSetSelectedInteractable(nullptr);\n\t\t\tActivateAimingActor(false);\n\t\t}\n\n\t\tbSelectorActivated = Activate;\n\t}\n}\n\nvoid AInteractableSelector::UpdateDampenedForwardVector(float Dampening)\n{\n\tDampenedForwardVector *= Dampening;\n\tDampenedForwardVector += GetActorForwardVector() * (1.0f - Dampening);\n\tDampenedForwardVector.Normalize();\n}\n\nvoid AInteractableSelector::ComputeNearFieldRaycastEndpoints(FVector& Start, FVector& End) const\n{\n\tauto const ActorPos = GetActorLocation();\n\tauto const ActorFwd = GetActorForwardVector();\n\tStart = ActorPos - ActorFwd * NearFieldRadius;\n\tEnd = ActorPos + ActorFwd * NearFieldRadius;\n}\n\nvoid AInteractableSelector::ComputeFarFieldRaycastEndpoints(FVector& Start, FVector& End) const\n{\n\tauto const ActorPos = GetActorLocation();\n\tauto const ActorFwd = DampenedForwardVector;\n\tStart = ActorPos + ActorFwd * RaycastOffset;\n\tEnd = ActorPos + ActorFwd * (RaycastOffset + RaycastDistance);\n}\n\nfloat AInteractableSelector::ComputeSphereRadiusForCast() const\n{\n\treturn RaycastDistance * FMath::Tan(FMath::DegreesToRadians(RaycastAngleDegrees * 0.5f));\n}\n\nfloat AInteractableSelector::ComputeAngularDistance(AActor* Target) const\n{\n\tauto const LineStart = GetActorLocation();\n\tauto const LineEnd = GetActorLocation() + 100.0f * DampenedForwardVector; // Any point will do, but projecting forward will reduce errors.\n\tauto const TargetLocation = Target->GetActorLocation();\n\n\tauto const NearestLocation = FMath::ClosestPointOnInfiniteLine(LineStart, LineEnd, TargetLocation);\n\n\tauto const DistNearestToUs = FVector::Distance(NearestLocation, LineStart);\n\tauto const DistNearestToTargetCenter = FVector::Distance(NearestLocation, TargetLocation);\n\tauto const DistNearestToTarget = FMath::Max(DistNearestToTargetCenter - Target->GetSimpleCollisionRadius() * 0.5f, 0.0f);\n\n\tauto const AngleToTargetRadians = FMath::FastAsin(DistNearestToTarget / DistNearestToUs);\n\n\treturn FMath::RadiansToDegrees(AngleToTargetRadians);\n}\n\nvoid AInteractableSelector::ActivateAimingActor(bool Activate) const\n{\n\tif (!AimingActor)\n\t{\n\t\treturn;\n\t}\n\n\tif (Activate)\n\t{\n\t\t// This is one of AAimingActor's events.\n\t\tAimingActor->Activate();\n\t}\n\telse\n\t{\n\t\t// This is one of AAimingActor's events.\n\t\tAimingActor->Deactivate(SelectedInteractable);\n\t}\n}\n\nvoid AInteractableSelector::BuildAimingActor()\n{\n\t// Minimum handling of multiplayer: don't spawn aiming actor if we are not on the server.\n\tif (!HasAuthority())\n\t{\n\t\treturn;\n\t}\n\n\tauto World = GetWorld();\n\tif (!World)\n\t{\n\t\treturn;\n\t}\n\n\tif (!AimingActor && AimingActorClass)\n\t{\n\t\tFActorSpawnParameters Params;\n\t\tParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;\n\t\tParams.bAllowDuringConstructionScript = true;\n\t\tParams.Owner = this;\n\n\t\t// Spawn actor of desired class.\n\t\tauto Location = GetActorLocation();\n\t\tauto Rotation = GetActorRotation();\n\t\tAimingActor = Cast<AAimingActor>(World->SpawnActor(AimingActorClass, &Location, &Rotation, Params));\n\n\t\tif (!AimingActor)\n\t\t{\n\t\t\tUE_LOG(LogTemp, Error, TEXT(\"Could not spawn aiming actor of class %s for interactable selector %s\"),\n\t\t\t\t*AimingActorClass->GetName(),\n\t\t\t\t*GetName());\n\t\t}\n\t\telse\n\t\t{\n\t\t\tbAimingActorOwned = true;\n\t\t\tAimingActor->SetActorTickEnabled(true);\n\t\t\tAimingActor->SetActorEnableCollision(false);\n\t\t}\n\t}\n}\n\nvoid AInteractableSelector::DestroyAimingActor()\n{\n\tauto World = GetWorld();\n\tif (!World)\n\t{\n\t\treturn;\n\t}\n\n\tif (bAimingActorOwned && AimingActor->HasAuthority() && IsValid(AimingActor) && !AimingActor->IsUnreachable())\n\t{\n\t\tWorld->DestroyActor(AimingActor);\n\t\tAimingActor = nullptr;\n\t\tbAimingActorOwned = false;\n\t}\n}\n\nvoid AInteractableSelector::TargetBeam(AActor* Target)\n{\n\tif (!Beam)\n\t{\n\t\treturn;\n\t}\n\n\tif (Target)\n\t{\n\t\tBeam->SetActorParameter(BeamSource, this);\n\t\tBeam->SetActorParameter(BeamTarget, Target);\n\t\tBeam->SetComponentTickEnabled(true);\n\t\tBeam->SetHiddenInGame(false);\n\t\tOrientBeam();\n\t}\n\telse\n\t{\n\t\tBeam->SetHiddenInGame(true);\n\t\tBeam->SetComponentTickEnabled(false);\n\t}\n}\n\nvoid AInteractableSelector::OrientBeam() const\n{\n\tif (Beam)\n\t{\n\t\tauto const BeamTangent = DampenedForwardVector;\n\t\t// UE_LOG(LogTemp, Error, TEXT(\"*** BEAM UPDATE %0.2f %0.2f %0.2f\"), DampenedForwardVector.X, DampenedForwardVector.Y, DampenedForwardVector.Z);\n\t\tBeam->SetBeamSourceTangent(0, BeamTangent, 0);\n\t}\n}\n\nvoid AInteractableSelector::BuildBeam()\n{\n\tif (BeamTemplate)\n\t{\n\t\tBeam = UGameplayStatics::SpawnEmitterAttached(BeamTemplate, this->GetRootComponent());\n\t\tTargetBeam(nullptr);\n\t}\n}\n\nvoid AInteractableSelector::DestroyBeam()\n{\n\tif (Beam)\n\t{\n\t\tBeam->DestroyComponent();\n\t\tBeam = nullptr;\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/OculusInteractableModule.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"OculusInteractableModule.h\"\n\n#define LOCTEXT_NAMESPACE \"FOculusInteractableModule\"\n\n#include \"OculusDeveloperTelemetry.h\"\n\nOCULUS_TELEMETRY_LOAD_MODULE(\"Unreal-OculusInteractable\");\n\nDEFINE_LOG_CATEGORY(LogInteractable);\n\nvoid FOculusInteractableModule::StartupModule()\n{\n\t// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module\n}\n\nvoid FOculusInteractableModule::ShutdownModule()\n{\n\t// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,\n\t// we call this function before unloading the module.\n}\n\n#undef LOCTEXT_NAMESPACE\n\nIMPLEMENT_MODULE(FOculusInteractableModule, OculusInteractable)\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/TransformString.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"TransformString.h\"\n\nvoid FTransformString::TransformToString(FTransform Transform, FString& String)\n{\n\tauto const Location = Transform.GetLocation();\n\tauto const Rotation = Transform.GetRotation();\n\tauto const Scale = Transform.GetScale3D();\n\tString = FString::Printf(\n\t\tTEXT(\"LOC X%0.3f Y%0.3f Z%0.3f ROT W%0.3f X%0.3f Y%0.3f Z%0.3f\"),\n\t\tLocation.X, Location.Y, Location.Z,\n\t\tRotation.W, Rotation.X, Rotation.Y, Rotation.Z);\n\n\tfloat const Tolerance = 0.01;\n\tif (FMath::Abs(Scale.X - 1.0f) > Tolerance ||\n\t\tFMath::Abs(Scale.Y - 1.0f) > Tolerance ||\n\t\tFMath::Abs(Scale.Z - 1.0f) > Tolerance)\n\t{\n\t\tString.Append(FString::Printf(\n\t\t\tTEXT(\" SCA X%0.3f Y%0.3f Z%0.3f\"),\n\t\t\tScale.X, Scale.Y, Scale.Z));\n\t}\n}\n\nbool FTransformString::StringToTransform(FString String, FTransform& Transform)\n{\n\tFVector Location;\n\tFQuat Rotation;\n\tFVector Scale(1.0f, 1.0f, 1.0f);\n\n\tTArray<FString> Elems;\n\tauto const NumElems = String.ParseIntoArray(Elems, TEXT(\" \"), true);\n\n\tif (NumElems != 9 && NumElems != 13) return false;\n\n\tif (Elems[0] != TEXT(\"LOC\")) return false;\n\tif (!ReadFloat(Elems[1], TEXT(\"X\"), Location.X)) return false;\n\tif (!ReadFloat(Elems[2], TEXT(\"Y\"), Location.Y)) return false;\n\tif (!ReadFloat(Elems[3], TEXT(\"Z\"), Location.Z)) return false;\n\n\tif (Elems[4] != TEXT(\"ROT\")) return false;\n\tif (!ReadFloat(Elems[5], TEXT(\"W\"), Rotation.W)) return false;\n\tif (!ReadFloat(Elems[6], TEXT(\"X\"), Rotation.X)) return false;\n\tif (!ReadFloat(Elems[7], TEXT(\"Y\"), Rotation.Y)) return false;\n\tif (!ReadFloat(Elems[8], TEXT(\"Z\"), Rotation.Z)) return false;\n\n\tif (NumElems == 13)\n\t{\n\t\tif (Elems[9] != TEXT(\"SCA\")) return false;\n\t\tif (!ReadFloat(Elems[10], TEXT(\"X\"), Scale.X)) return false;\n\t\tif (!ReadFloat(Elems[11], TEXT(\"Y\"), Scale.Y)) return false;\n\t\tif (!ReadFloat(Elems[12], TEXT(\"Z\"), Scale.Z)) return false;\n\t}\n\n\tTransform.SetLocation(Location);\n\tTransform.SetRotation(Rotation);\n\tTransform.SetScale3D(Scale);\n\treturn true;\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Private/TransformString.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\n/**\n * Class for transform encoding.\n */\nclass FTransformString\n{\npublic:\n\t/**\n\t * Transform to String.\n\t * @param Transform - Transform to convert to string.\n\t * @param String - Output string.\n\t */\n\tstatic void TransformToString(FTransform Transform, FString& String);\n\n\t/**\n\t * String to Transform.\n\t * @param String - String encoded transform to convert to Transform.\n\t * @param Transform - Decoded Transform.\n\t * @return bool - Whether the encoded string is proper.\n\t */\n\tstatic bool StringToTransform(FString String, FTransform& Transform);\n\nprivate:\n\t/**\n\t * Parsing float from string, with expected prefix.\n\t * @param Source - Transform sub-string of the form <letter><float>.\n\t * @param ExpectedPrefix - The expected one-letter prefix for this sub-field.\n\t * @param Destination - Output float.\n\t * @return bool - True if prefix matches.\n\t */\n\tstatic bool ReadFloat(FString& Source, const TCHAR* ExpectedPrefix, double& Destination)\n\t{\n\t\tif (**Source != *ExpectedPrefix) return false;\n\t\tDestination = FCString::Atof(*Source + 1);\n\t\treturn true;\n\t}\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Public/AimingActor.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Actor.h\"\n#include \"AimingActor.generated.h\"\n\nUCLASS()\nclass OCULUSINTERACTABLE_API AAimingActor : public AActor\n{\n\tGENERATED_BODY()\n\npublic:\n\t/** Sets default values for this actor's properties. */\n\tAAimingActor();\n\nprotected:\n\t/** Called when the game starts or when spawned. */\n\tvirtual void BeginPlay() override;\n\npublic:\n\t/** Called every frame. */\n\tvirtual void Tick(float DeltaTime) override;\n\n\t/**\n\t * Called by the selector when the aiming actor is activated.\n\t * By default, it makes the actor visible.\n\t */\n\tUFUNCTION(BlueprintNativeEvent)\n\tvoid Activate();\n\n\t/**\n\t * Called by the selector when the aiming actor is deactivated.\n\t * By default, it makes the actor invisible.\n\t * @param Selected - Interactable selected, if any.\n\t */\n\tUFUNCTION(BlueprintNativeEvent)\n\tvoid Deactivate(AInteractable* Selected);\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Public/HandGrabbingComponent.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\n#include \"Interactable.h\"\n#include \"OculusXRInputFunctionLibrary.h\"\n\n#include \"HandGrabbingComponent.generated.h\"\n\nclass OCULUSINTERACTABLE_API AInteractable;\n\nUCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))\nclass OCULUSINTERACTABLE_API UHandGrabbingComponent : public USceneComponent\n{\n\tGENERATED_BODY()\n\npublic:\n\tUFUNCTION(BlueprintCallable, Category = \"Grabbing\")\n\tAInteractable* TryGrab(FTransform GrabTransform);\n\n\tUFUNCTION(BlueprintCallable, Category = \"Grabbing\")\n\tAInteractable* TryRelease(bool bReenablePhysics = true);\n\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Grabbing\")\n\tEOculusXRHandType Hand = EOculusXRHandType::HandLeft;\n\n\tUPROPERTY(BlueprintReadOnly, Transient, Category = \"Grabbing\")\n\tAInteractable* GrabbedActor = nullptr;\n\n\tUPROPERTY(BlueprintReadOnly, Transient, Category = \"Grabbing\")\n\tbool bGrabbedActorHasPhysics = false;\n\n\tUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = \"Grabbing\")\n\tfloat GrabCapsuleHeight = 30.f;\n\n\tUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = \"Grabbing\")\n\tfloat GrabCapsuleRadius = 20.f;\n\nprivate:\n\tUFUNCTION()\n\tvoid HandleHeldActorDestroyed(AActor* DestroyedActor);\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Public/Interactable.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Actor.h\"\n#include \"InteractablePose.h\"\n#include \"Interactable.generated.h\"\n\nclass AInteractableSelector;\n\nUENUM(BlueprintType)\nenum class EHandSide : uint8\n{\n\tHandLeft,\n\tHandRight\n};\n\n/** Base actor class of interactable objects. */\nUCLASS()\nclass OCULUSINTERACTABLE_API AInteractable : public AActor\n{\n\tGENERATED_BODY()\n\npublic:\n\t/** Sets default values for this actor's properties. */\n\tAInteractable();\n\nprotected:\n\t/** Called when the game starts or when spawned. */\n\tvirtual void BeginPlay() override;\n\n\t/** Called when this actor is being removed from the level. */\n\tvirtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;\n\npublic:\n\t/** Called every frame. */\n\tvirtual void Tick(float DeltaTime) override;\n\n\t/** Minimum time required for the selector to stay on this object before it can be selected in the far-field. */\n\tUPROPERTY(EditAnywhere, BlueprintReadWrite, Category = \"Interactable Grab Pose\")\n\tfloat FarFieldSelectionDelayMs;\n\n\t/**\n\t * Event fired when selection starts.\n\t * Can only be called from C++.\n\t * @param Selector - The Interactable Selector that just selected us.\n\t */\n\tUFUNCTION(BlueprintNativeEvent)\n\tvoid BeginSelection(AInteractableSelector* Selector);\n\n\t/**\n\t * Event fired when selection ends.\n\t * Can only be called from C++.\n\t * @param Selector - The Interactable Selector that stopped selecting us.\n\t */\n\tUFUNCTION(BlueprintNativeEvent)\n\tvoid EndSelection(AInteractableSelector* Selector);\n\n\t/**\n\t * Call to check if you are currently selected.\n\t * @return boolean\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tbool IsSelected() const;\n\n\t/**\n\t * Optional generic user event.\n\t * The meaning, implementation and invocation is left to the user.\n\t */\n\tUFUNCTION(BlueprintCallable, BlueprintImplementableEvent)\n\tvoid Interaction1();\n\n\t/**\n\t * Optional generic user event.\n\t * The meaning, implementation and invocation is left to the user.\n\t */\n\tUFUNCTION(BlueprintCallable, BlueprintImplementableEvent)\n\tvoid Interaction2();\n\n\t/**\n\t * Optional generic user event.\n\t * The meaning, implementation and invocation is left to the user.\n\t */\n\tUFUNCTION(BlueprintCallable, BlueprintImplementableEvent)\n\tvoid Interaction3();\n\n\t/**\n\t * Call to get all selectors currently selecting us.\n\t * @return An array of Interactable Selectors.\n\t */\n\tconst TArray<AInteractableSelector*>& GetSelectors() const;\n\n\t/**\n\t * Method called to turn on/off physics on interactable.\n\t * By default it checks if the root component is a primitive and applies the change there.\n\t * You can override this method in blueprint for special cases.\n\t * @param SimulatePhysics - boolean.\n\t */\n\tUFUNCTION(BlueprintCallable, BlueprintNativeEvent)\n\tvoid SetInteractablePhysicsSimulation(bool SimulatePhysics);\n\n\t/**\n\t * Method to check if object is movable.\n\t * By default it checks if the root component is movable.\n\t * You can override this method in blueprint for special cases.\n\t */\n\tUFUNCTION(BlueprintCallable, BlueprintNativeEvent)\n\tbool IsMovable();\n\n\t/** Hand poses when grabbed. */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Interactable Grab Pose\")\n\tTArray<FInteractablePose> GrabPosesLeftHand;\n\n\t/** Hand poses when grabbed. */\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Interactable Grab Pose\")\n\tTArray<FInteractablePose> GrabPosesRightHand;\n\n\t/**\n\t * Selects the best grab pose for the interactable, if any.\n\t * @param Side - EOculusXRHandType to select left or right hand.\n\t * @param GrabPoseFound - returns a boolean indicating if a grab pose was found.\n\t * @param GrabPoseName - if GrabPoseFound, returns the hand pose while grabbing.\n\t * @param GrabTransform - if GrabPoseFound, returns the Interactable's transform relative to the hand.\n\t * @param GrabHandPose - returns the grab pose string\n\t */\n\tUFUNCTION(BlueprintCallable, Category = \"Interactable Grab Pose\")\n\tvoid SelectGrabPose(EHandSide Side, bool& GrabPoseFound, FString& GrabPoseName, FTransform& GrabTransform, FString& GrabHandPose);\n\nprotected:\n\t/** List of selectors currently selecting us. */\n\tTArray<AInteractableSelector*> Selectors;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Public/InteractableFunctionLibrary.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Kismet/BlueprintFunctionLibrary.h\"\n#include \"InteractableFunctionLibrary.generated.h\"\n\nUCLASS()\nclass OCULUSINTERACTABLE_API UInteractableFunctionLibrary : public UBlueprintFunctionLibrary\n{\n\tGENERATED_BODY()\n\npublic:\n\t/**\n\t * Locate nearby Interactables and give their relative position and orientation to\n\t * some reference object.\n\t * @param ReferenceActor \n\t * @param RadiusAroundActor \n\t */\n\tUFUNCTION(BlueprintCallable, Category = \"Interactable\")\n\tstatic void LogNearbyInteractables(AActor* ReferenceActor, float RadiusAroundActor);\n\nprivate:\n\tstatic ECollisionChannel InteractableTraceChannel;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Public/InteractablePose.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"InteractablePose.generated.h\"\n\n/** A struct that represents a hand pose. */\nUSTRUCT(BlueprintType)\nstruct OCULUSINTERACTABLE_API FInteractablePose\n{\n\tGENERATED_BODY()\n\n\t/** Name for this pose. */\n\tUPROPERTY(Category = \"Interactable Grab Pose\", EditAnywhere, BlueprintReadWrite)\n\tFString PoseName;\n\n\t/** Hand pose. */\n\tUPROPERTY(Category = \"Interactable Grab Pose\", EditAnywhere, BlueprintReadWrite)\n\tFString HandPose;\n\n\t/** Relative hand transform. */\n\tUPROPERTY(Category = \"Interactable Grab Pose\", EditAnywhere, BlueprintReadWrite)\n\tFString RelativeHandTransform;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Public/InteractableSelector.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/Actor.h\"\n#include \"Components/ArrowComponent.h\"\n#include \"Particles/ParticleSystemComponent.h\"\n#include \"Interactable.h\"\n#include \"AimingActor.h\"\n#include \"InteractableSelector.generated.h\"\n\n/**\n * Base actor class for interactable selectors.\n */\n\nUCLASS()\nclass OCULUSINTERACTABLE_API AInteractableSelector : public AActor\n{\n\tfriend class AInteractable;\n\n\tGENERATED_BODY()\n\npublic:\n\t/** Sets default values for this actor's properties. */\n\tAInteractableSelector();\n\n\t/** Arrow component to indicate forward direction of selection. */\n#if WITH_EDITORONLY_DATA\nprivate:\n\tUPROPERTY()\n\tclass UArrowComponent* ArrowComponent;\npublic:\n#endif\n\n\t/** Initial activation state of the selector. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tbool bSelectorStartsActivated;\n\n\t/** Radius of near-field selection around the selector.  Disabled if <= 0.0. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tfloat NearFieldRadius;\n\n\t/** Distance from selector at which we start raycasting. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tfloat RaycastOffset;\n\n\t/** Raycast range. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tfloat RaycastDistance;\n\n\t/** Raycast angle: we select within a cone. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tfloat RaycastAngleDegrees;\n\n\t/** Raycast stickiness angle: we stick to a selection while we do not exceed this angle. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tfloat RaycastStickinessAngleDegrees;\n\n\t/** Align aiming actor with hit normal. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tbool bAlignAimingActorWithHitNormal;\n\n\t/** Aiming actor placed at targetting location. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tAAimingActor* AimingActor;\n\n\t/** Aiming actor class to spawn if no AimingActor is specified. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tTSubclassOf<AAimingActor> AimingActorClass;\n\n\t/** Particle system for beam. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadOnly)\n\tUParticleSystem* BeamTemplate;\n\n\t/** Aiming actor rotation rate. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tbool bAlwaysShowAimingActor = false;\n\n\t/** Aiming actor rotation rate. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tfloat AimingActorRotationRate;\n\n\t/** Aiming using a number of samples for stabilization. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite, meta = (ClampMin = \"0.0\", ClampMax = \"1.0\", UIMin = \"0.0\", UIMax = \"1.0\"))\n\tfloat DampeningFactor;\n\n\t/** Raycast debug. */\n\tUPROPERTY(Category = \"Selector\", EditAnywhere, BlueprintReadWrite)\n\tbool bRaycastDebugTrace;\n\n\tUFUNCTION(Category = \"Selector\", BlueprintNativeEvent)\n\tbool ShouldSelect(AInteractable* Interactable) const;\n\n\t/**\n\t * Call to activate / deactivate the selector.\n\t * @param Activate - A boolean.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tvoid ActivateSelector(bool Activate);\n\n\t/**\n\t * Access to the currently selected interactable actor.\n\t * @param SelectedInNearField - When the return value is not null, indicates if selected in near field.\n\t * @return AInteractable or nullptr if none is selected.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tAInteractable* GetSelectedInteractable(bool& SelectedInNearField) const;\n\n\t/**\n\t * Access any non-interactable actor hit by the selector.\n\t * @return AActor or nullptr if none is selected.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tAActor* GetNonInteractableHit() const;\n\n\t/** Called every frame. */\n\tvirtual void Tick(float DeltaTime) override;\n\nprotected:\n\t/** Called when the game starts or when spawned. */\n\tvirtual void BeginPlay() override;\n\n\t/** Called when this actor is being removed from the level. */\n\tvirtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;\n\n\t/** Whether the selector is currently activated or not. */\n\tbool bSelectorActivated;\n\n\t/** Dampened forward vector. */\n\tFVector DampenedForwardVector;\n\n\t/**\n\t * Updates DampenedForwardVector.\n\t * @param Dampening - Fraction of the current vector to keep.\n\t */\n\tvoid UpdateDampenedForwardVector(float Dampening);\n\n\t/**\n\t * Computes the current start and end locations for the near-field selector.\n\t * @param Start - Where to store the start location.\n\t * @param End - Where to store the end location.\n\t */\n\tvoid ComputeNearFieldRaycastEndpoints(FVector& Start, FVector& End) const;\n\n\t/**\n\t * Computes the current start and end locations for the far-field selector.\n\t * @param Start - Where to store the start location.\n\t * @param End - Where to store the end location.\n\t */\n\tvoid ComputeFarFieldRaycastEndpoints(FVector& Start, FVector& End) const;\n\n\t/**\n\t * Computes the radius of the sphere required to hit at RaycastDistance and RaycastAngle.\n\t * @return Radius of sphere.\n\t */\n\tfloat ComputeSphereRadiusForCast() const;\n\n\t/**\n\t * Computes our approximative angular distance to an Actor.\n\t * It is approximative because we take the actor's bounding volume into consideration.\n\t * @param Target - The actor.\n\t * @return An approximative angular distance in degrees.\n\t */\n\tfloat ComputeAngularDistance(AActor* Target) const;\n\n\t/** Candidate in pre-selection, when object has a FarFieldSelectionDelay > 0.0. */\n\tUPROPERTY() AInteractable* CandidatePreSelection;\n\tfloat CandidatePreSelectionTimeMs;\n\n\t/** Interactable selected. */\n\tUPROPERTY() AInteractable* SelectedInteractable;\n\n\t/** Interactable is in near-field. */\n\tbool SelectedInteractableInNearField;\n\n\t/** If we hit a non-interactable object, we keep a reference to it. */\n\tTWeakObjectPtr<AActor> NonInteractableActorHit;\n\n\t/**\n\t * Implements changes of target.\n\t * @param Candidate - An interactable actor.\n\t * @param SelectionDurationMs - How long the candidate has been under selection in milliseconds.\n\t * @param Notify - Whether we should notify the old and/or new selections.\n\t * @param CandidateInNearField - Whether the selected interactable\n\t */\n\tvoid SetSelectedInteractable(AInteractable* Candidate, float SelectionDurationMs = 0.0f, bool Notify = true, bool CandidateInNearField = false);\n\n\t/**\n\t * Call to change the activation of the aiming actor.\n\t * @param Activate - New activation state.\n\t */\n\tvoid ActivateAimingActor(bool Activate) const;\n\n\t/** Do we own the aiming actor? */\n\tbool bAimingActorOwned;\n\n\t/** Constructs the aiming actor, if necessary. */\n\tvoid BuildAimingActor();\n\n\t/** Destroys the aiming actor, if we own it. */\n\tvoid DestroyAimingActor();\n\n\t/**\n\t * Call to target the beam at an actor.\n\t * @param Target - Actor to target the beam to, or nullptr to deactivate.\n\t */\n\tvoid TargetBeam(AActor* Target);\n\n\t/** Updates the beam start tangent. */\n\tvoid OrientBeam() const;\n\n\t/** Constructs the selection beam, if a template was provided. */\n\tvoid BuildBeam();\n\n\t/** Destroys the selection beam, if one has been created. */\n\tvoid DestroyBeam();\n\n\t/** The beam particle system component created based on the BeatTemplate particle system. */\n\tUPROPERTY() UParticleSystemComponent* Beam;\n\nprivate:\n\tstatic ECollisionChannel InteractableTraceChannel;\n\tstatic FName BeamSource;\n\tstatic FName BeamTarget;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusInteractable/Public/OculusInteractableModule.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\nDECLARE_LOG_CATEGORY_EXTERN(LogInteractable, Log, All);\n\nclass FOculusInteractableModule : public IModuleInterface\n{\npublic:\n\t/** IModuleInterface implementation */\n\tvirtual void StartupModule() override;\n\tvirtual void ShutdownModule() override;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusThrowAssist/OculusThrowAssist.Build.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\n\npublic class OculusThrowAssist  : ModuleRules\n{\n\tpublic OculusThrowAssist  (ReadOnlyTargetRules Target) : base(Target)\n\t{\n\t\tPCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;\n\t\t\n\t\tPublicIncludePaths.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t// ... add public include paths required here ...\n\t\t\t}\n\t\t\t);\n\t\t\t\t\n\t\t\n\t\tPrivateIncludePaths.AddRange(\n\t\t\tnew string[] {\n\t\t\t\t// ... add other private include paths required here ...\n\t\t\t}\n\t\t\t);\n\t\t\t\n\t\t\n\t\tPublicDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"Core\",\n\t\t\t\t// ... add other public dependencies that you statically link with here ...\n\t\t\t}\n\t\t\t);\n\t\t\t\n\t\t\n\t\tPrivateDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"CoreUObject\",\n\t\t\t\t\"Engine\",\n\t\t\t\t\"Slate\",\n\t\t\t\t\"SlateCore\",\n\t\t\t\t\"OculusUtils\"\n\t\t\t}\n\t\t\t);\n\t\t\n\t\t\n\t\tDynamicallyLoadedModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t// ... add any modules that your module loads dynamically here ...\n\t\t\t}\n\t\t\t);\n\n\t\tIncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6;\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusThrowAssist/Private/OculusThrowAssistModule.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"OculusThrowAssistModule.h\"\n\n#include \"OculusDeveloperTelemetry.h\"\n\nOCULUS_TELEMETRY_LOAD_MODULE(\"Unreal-OculusThrowAssist\");\n\n#define LOCTEXT_NAMESPACE \"FOculusThrowAssistModule\"\n\nDEFINE_LOG_CATEGORY(LogOculusThrowAssist);\n\nvoid FOculusThrowAssistModule::StartupModule()\n{\n\t// This code will execute after your module is loaded into memory; the exact timing is specified in the .uplugin file per-module\n}\n\nvoid FOculusThrowAssistModule::ShutdownModule()\n{\n\t// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,\n\t// we call this function before unloading the module.\n}\n\n#undef LOCTEXT_NAMESPACE\n\nIMPLEMENT_MODULE(FOculusThrowAssistModule, OculusThrowAssist)\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusThrowAssist/Private/ThrowingComponent.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"ThrowingComponent.h\"\n\n#include \"UObject/UObjectGlobals.h\"\n#include \"Engine/World.h\"\n#include \"DrawDebugHelpers.h\"\n#include \"TransformBufferComponent.h\"\n\nstatic TAutoConsoleVariable<int> CVarDebugDrawThrowingVector(\n\tTEXT(\"throw.DebugDrawThrowingVector\"),\n\t0,\n\tTEXT(\"Shows throwing vector for camera-tracked hands. 1- draw when throwing. 2- draw always\"),\n\tECVF_Cheat);\n\nUThrowingComponent::UThrowingComponent()\n{\n\tPrimaryComponentTick.bCanEverTick = true;\n}\n\nvoid UThrowingComponent::Initialize(USceneComponent* AttachParent)\n{\n\tauto const UniqueName = [&](auto&& Base) { return MakeUniqueObjectName(GetOwner(), UTransformBufferComponent::StaticClass(), Base); };\n\t\n\tHighConfidenceTransformBuffer = NewObject<UTransformBufferComponent>(GetOwner(), UniqueName(TEXT(\"HandHighConfidenceTransformBuffer\")));\n\tHighConfidenceTransformBuffer->RegisterComponent();\n\tHighConfidenceTransformBuffer->AttachToComponent(AttachParent, FAttachmentTransformRules::KeepRelativeTransform);\n\tHighConfidenceTransformBuffer->UpdateMode = ETransformBufferUpdateMode::Manual;\n\tHighConfidenceTransformBuffer->MaxBufferTimeSeconds = 1.f;\n\tGetOwner()->AddInstanceComponent(HighConfidenceTransformBuffer);\n\t\n\tAllTransformsTransformBuffer = NewObject<UTransformBufferComponent>(GetOwner(), UniqueName(TEXT(\"HandAllTransformsTransformBuffer\")));\n\tAllTransformsTransformBuffer->RegisterComponent();\n\tAllTransformsTransformBuffer->AttachToComponent(AttachParent, FAttachmentTransformRules::KeepRelativeTransform);\n\tAllTransformsTransformBuffer->UpdateMode = ETransformBufferUpdateMode::Manual;\n\tAllTransformsTransformBuffer->MaxBufferTimeSeconds = 1.f;\n\tGetOwner()->AddInstanceComponent(AllTransformsTransformBuffer);\n}\n\nvoid UThrowingComponent::Update(bool IsTracked)\n{\n\tif (!HighConfidenceTransformBuffer || !AllTransformsTransformBuffer)\n\t{\n\t\treturn;\n\t}\n\n\tif (IsTracked)\n\t{\n\t\tHighConfidenceTransformBuffer->BufferCurrentData();\n\t}\n\tAllTransformsTransformBuffer->BufferCurrentData();\n\n\tFTransformBufferData TransformBufferDataNow;\n\tAllTransformsTransformBuffer->GetBufferData(0, TransformBufferDataNow);\n\tif (IsTracked != WasTrackedLastFrame)\n\t{\n\t\tauto const TimeNow = GetWorld()->GetTimeSeconds();\n\n\t\tif (IsTracked)\n\t\t{\n\t\t\tMostRecentTrackingGainTime = TimeNow;\n\t\t\tMostRecentTrackingGainTransform = TransformBufferDataNow.Transform;\n\t\t\tMostRecentTrackingGainVelocity = TransformBufferDataNow.Velocity;\n\t\t}\n\t\telse\n\t\t{\n\t\t\tMostRecentTrackingLossTime = TimeNow;\n\t\t\tMostRecentTrackingLossTransform = TransformBufferDataNow.Transform;\n\t\t\tMostRecentTrackingLossVelocity = TransformBufferDataNow.Velocity;\n\t\t}\n\n\t\tWasTrackedLastFrame = IsTracked;\n\t}\n}\n\nFVector UThrowingComponent::GetThrowVector(FVector LookDirection) const\n{\n\tif (bSelectBestThrowVectorFromPast)\n\t{\n\t\tauto BestThrowVector = FVector::ZeroVector;\n\t\tauto BestThrowVectorScore = FLT_MIN;\n\t\tcheck(NumThrowVectorSamples > 0);\n\t\tauto const TimeStep = OldestPossibleThrowVectorSeconds / NumThrowVectorSamples;\n\t\tfor (auto i = 0; i < NumThrowVectorSamples; ++i)\n\t\t{\n\t\t\tauto const TimeInPast = i * TimeStep;\n\t\t\tauto const ThrowVector = GetThrowVectorInPast(TimeInPast, LookDirection);\n\t\t\tauto const Score = GetScoreForThrowVector(ThrowVector, LookDirection, i / (float)NumThrowVectorSamples);\n\t\t\tif (Score > BestThrowVectorScore)\n\t\t\t{\n\t\t\t\tBestThrowVector = ThrowVector;\n\t\t\t\tBestThrowVectorScore = Score;\n\t\t\t}\n\t\t}\n\n\t\treturn BestThrowVector;\n\t}\n\treturn GetThrowVectorInPast(ThrowLatencyAdjustmentTimeSeconds, LookDirection);\n}\n\nFVector UThrowingComponent::GetThrowVectorInPast(float SecondsAgo, FVector LookDirection) const\n{\n#if !UE_BUILD_SHIPPING\n\tauto ArrowColor = FColor::Green;\n#endif\n\n\tFVector ThrowVector;\n\tFTransformBufferData TransformBufferData;\n\n\tauto const TimeWithGoodTracking = GetTimeWithGoodTracking();\n\n\t// High confidence throwing\n\tif (TimeWithGoodTracking >= HighConfidenceThrowMinTrackingTime)\n\t{\n\t\tHighConfidenceTransformBuffer->GetBufferData(SecondsAgo, TransformBufferData);\n\t\tThrowVector = TransformBufferData.Velocity;\n\t}\n\telse\n\t{\n\t\t// check whether we can get a decent vector from the all transforms buffer\n\t\tAllTransformsTransformBuffer->GetBufferData(SecondsAgo, TransformBufferData);\n\n\t\t// Low confidence throwing\n\t\tif (TransformBufferData.Velocity.Size() > LowConfidenceThrowMinSpeed &&\n\t\t\tFVector::DotProduct(TransformBufferData.Velocity, LookDirection) > 0)\n\t\t{\n\t\t\tauto const ThrowSpeed = TransformBufferData.Velocity.Size();\n\t\t\tThrowVector = TransformBufferData.Velocity.GetSafeNormal() * (1 - LowConfidenceHeadForwardFactor) +\n\t\t\t\tLookDirection * LowConfidenceHeadForwardFactor;\n\t\t\tThrowVector *= ThrowSpeed;\n\n#if !UE_BUILD_SHIPPING\n\t\t\tArrowColor = FColor::Yellow;\n#endif\n\t\t}\n\t\t// Very low confidence throwing\n\t\telse\n\t\t{\n\t\t\tauto const TrackingLossVector = TransformBufferData.Transform.GetLocation() -\n\t\t\t\tMostRecentTrackingLossTransform.GetLocation();\n\t\t\tThrowVector = TrackingLossVector.GetSafeNormal() * VeryLowConfidenceVectorFactor +\n\t\t\t\tLookDirection * VeryLowConfidenceHeadForwardFactor;\n\t\t\tThrowVector *= TrackingLossVector.Size() * VeryLowConfidenceSpeedFactor;\n\n#if !UE_BUILD_SHIPPING\n\t\t\tArrowColor = FColor::Red;\n#endif\n\t\t}\n\t}\n\n#if !UE_BUILD_SHIPPING\n\n\tif (CVarDebugDrawThrowingVector.GetValueOnAnyThread() > 0)\n\t{\n\t\tDrawDebugDirectionalArrow(\n\t\t\tGetWorld(),\n\t\t\tTransformBufferData.Transform.GetLocation(),\n\t\t\tTransformBufferData.Transform.GetLocation() + ThrowVector * 0.2f,\n\t\t\t1.0f,\n\t\t\tArrowColor,\n\t\t\tfalse,\n\t\t\t4.0f,\n\t\t\t0,\n\t\t\t1.f);\n\t}\n\n#endif\n\n\treturn ThrowVector;\n}\n\nfloat UThrowingComponent::GetTimeWithGoodTracking() const\n{\n\tif (!WasTrackedLastFrame)\n\t{\n\t\treturn 0.f;\n\t}\n\n\tauto const TimeNow = GetWorld()->GetTimeSeconds();\n\treturn TimeNow - MostRecentTrackingGainTime;\n}\n\nfloat UThrowingComponent::GetScoreForThrowVector(FVector ThrowVector, FVector LookDirection, float TimeInPastNormalized) const\n{\n\t// recency\n\tauto const RecencyScore = (1 - TimeInPastNormalized) * ThrowVectorSelectionRecencyScoring;\n\n\t// direction\n\tauto DirectionScore = FVector::DotProduct(LookDirection, ThrowVector.GetSafeNormal()) / 2.f + 0.5f;\n\tDirectionScore *= ThrowVectorSelectionDirectionScoring;\n\n\t// speed\n\tauto SpeedScore = FMath::GetMappedRangeValueClamped(FVector2D(0, 200), FVector2D(0, 1), ThrowVector.Size());\n\tSpeedScore *= ThrowVectorSelectionSpeedScoring;\n\n\treturn RecencyScore + DirectionScore + SpeedScore;\n}\n\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusThrowAssist/Private/TransformBufferComponent.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"TransformBufferComponent.h\"\n#include \"DrawDebugHelpers.h\"\n#include \"Kismet/KismetMathLibrary.h\"\n#include \"OculusThrowAssistModule.h\"\n\nstatic TAutoConsoleVariable<int> CVarDebugDrawTransformBuffer(\n\tTEXT(\"mnux.DebugDrawTransformBuffer\"),\n\t0,\n\tTEXT(\"Draws all transforms in the transform buffer\"),\n\tECVF_Cheat);\n\nUTransformBufferComponent::UTransformBufferComponent(FObjectInitializer const& ObjectInitializer) :\n\tSuper(ObjectInitializer)\n{\n\tPrimaryComponentTick.bCanEverTick = true;\n}\n\nvoid UTransformBufferComponent::TickComponent(float DeltaTime, enum ELevelTick TickType,\n\tFActorComponentTickFunction* ThisTickFunction)\n{\n\tSuper::TickComponent(DeltaTime, TickType, ThisTickFunction);\n\n\tif (UpdateMode == ETransformBufferUpdateMode::EveryFrame)\n\t{\n\t\tBufferCurrentData();\n\t}\n\n#if !UE_BUILD_SHIPPING\n\tif (CVarDebugDrawTransformBuffer.GetValueOnAnyThread() > 0)\n\t{\n\t\tDebugDrawBuffer();\n\t}\n#endif\n}\n\nvoid UTransformBufferComponent::BufferCurrentData()\n{\n\tauto Timestamp = GetWorld()->GetTimeSeconds();\n\tauto const Transform = GetComponentTransform();\n\tauto const PrevIndex = BufferPosition == 0 ? (int)Buffer.Capacity() - 1 : BufferPosition - 1;\n\tauto const PrevTransform = Buffer[PrevIndex].Value.Transform;\n\tauto Velocity = FVector::ZeroVector;\n\tif (!PrevTransform.Equals(FTransform::Identity))\n\t{\n\t\tauto const Delta = Transform.GetLocation() - PrevTransform.GetLocation();\n\t\tauto const Time = Timestamp - Buffer[PrevIndex].Key;\n\t\tif (Time > 0)\n\t\t{\n\t\t\tVelocity = Delta / Time;\n\t\t\tBuffer[BufferPosition] = TTuple<float, FTransformBufferData>(\n\t\t\t\tTimestamp,\n\t\t\t\tFTransformBufferData(GetComponentTransform(), Velocity)\n\t\t\t);\n\t\t\t++BufferPosition;\n\t\t}\n\t}\n\telse\n\t{\n\t\tBuffer[BufferPosition] = TTuple<float, FTransformBufferData>(\n\t\t\tTimestamp,\n\t\t\tFTransformBufferData(GetComponentTransform(), Velocity)\n\t\t);\n\t\t++BufferPosition;\n\t}\n}\n\nbool UTransformBufferComponent::GetBufferData(float SecondsAgo, FTransformBufferData& OutBufferData)\n{\n\tif (SecondsAgo <= 0)\n\t{\n\t\tint Index = BufferPosition == 0 ? Buffer.Capacity() - 1 : BufferPosition - 1;\n\t\tOutBufferData = Buffer[Index].Value;\n\t\treturn true;\n\t}\n\n\tif (SecondsAgo > MaxBufferTimeSeconds)\n\t{\n\t\tUE_LOG(LogOculusThrowAssist, Warning,\n\t\t\tTEXT(\"UTransformBufferComponent::GetTransform: SecondsAgo can not be greater than MaxBufferTimeSeconds.\"\n\t\t\t));\n\t\tSecondsAgo = MaxBufferTimeSeconds;\n\t}\n\n\t// find the pair of buffer values to interpolate between\n\tauto LookupTime = GetWorld()->GetTimeSeconds() - SecondsAgo;\n\tauto Index = BufferPosition - 1;\n\n\t// check for the case that the most recent buffered value is older than the lookup time\n\tif (Buffer[Index].Key <= LookupTime)\n\t{\n\t\tUE_LOG(LogOculusThrowAssist, Warning,\n\t\t\tTEXT(\"UTransformBufferComponent::GetTransform: No data recent enough for an accurate result.\"));\n\t\tOutBufferData = Buffer[Index].Value;\n\t\treturn false;\n\t}\n\n\tint const BufferCapacity = Buffer.Capacity();\n\tfor (auto i = 0; i < BufferCapacity; ++i)\n\t{\n\t\t--Index;\n\t\tif (Index < 0)\n\t\t{\n\t\t\tIndex = BufferCapacity + Index;\n\t\t}\n\n\t\tauto BufferTime = Buffer[Index].Key;\n\t\tif (BufferTime <= LookupTime)\n\t\t{\n\t\t\tauto NextElementTime = Buffer[Index + 1].Key;\n\t\t\tauto t = (LookupTime - BufferTime) / (NextElementTime - BufferTime);\n\t\t\tauto PrevData = Buffer[Index].Value;\n\t\t\tauto NextData = Buffer[Index + 1].Value;\n\t\t\tauto Transform = UKismetMathLibrary::TLerp(PrevData.Transform, NextData.Transform, t);\n\t\t\tauto Velocity = FMath::Lerp(PrevData.Velocity, NextData.Velocity, t);\n\n\t\t\tOutBufferData = FTransformBufferData(Transform, Velocity);\n\n\t\t\tauto const MaxPeriodForReliableData = 0.1f;\n\t\t\treturn NextElementTime - BufferTime < MaxPeriodForReliableData;\n\t\t}\n\t}\n\n\tUE_LOG(LogOculusThrowAssist, Warning,\n\t\tTEXT(\"UTransformBufferComponent::GetTransform: No data old enough for an accurate result.\"));\n\tOutBufferData = Buffer[BufferPosition].Value; // return the oldest (could be default value)\n\treturn false;\n}\n\nvoid UTransformBufferComponent::DebugDrawBuffer() const\n{\n\tauto const TimeNow = GetWorld()->GetTimeSeconds();\n\tauto const OldestTime = TimeNow - MaxBufferTimeSeconds;\n\tauto const MaxScale = 5.0f;\n\n\tfor (uint32 i = 0; i < Buffer.Capacity(); ++i)\n\t{\n\t\tauto BufferedTransform = Buffer[i].Value.Transform;\n\t\tauto const BufferedTimestamp = Buffer[i].Key;\n\t\tauto const NormalizedTime = FMath::GetMappedRangeValueClamped(\n\t\t\tFVector2D(OldestTime, TimeNow), FVector2D(0, 1), BufferedTimestamp);\n\t\tauto const Scale = NormalizedTime * MaxScale;\n\t\tDrawDebugCoordinateSystem(GetWorld(), BufferedTransform.GetLocation(), BufferedTransform.Rotator(), Scale);\n\n\t\tauto BufferedVelocity = Buffer[i].Value.Velocity;\n\t\tDrawDebugDirectionalArrow(\n\t\t\tGetWorld(),\n\t\t\tBufferedTransform.GetLocation(),\n\t\t\tBufferedTransform.GetLocation() + BufferedVelocity * 0.2f,\n\t\t\t1.f,\n\t\t\tFColor::Orange,\n\t\t\tfalse,\n\t\t\t-1,\n\t\t\t0,\n\t\t\t.3f);\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusThrowAssist/Public/OculusThrowAssistModule.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\nDECLARE_LOG_CATEGORY_EXTERN(LogOculusThrowAssist, Log, All);\n\nclass FOculusThrowAssistModule : public IModuleInterface\n{\npublic:\n\t/** IModuleInterface implementation */\n\tvirtual void StartupModule() override;\n\tvirtual void ShutdownModule() override;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusThrowAssist/Public/ThrowingComponent.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\n#include \"Components/ActorComponent.h\"\n#include \"ThrowingComponent.generated.h\"\n\nclass UTransformBufferComponent;\n\nUCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))\nclass OCULUSTHROWASSIST_API UThrowingComponent : public UActorComponent\n{\n\tGENERATED_BODY()\n\npublic:\n\tUThrowingComponent();\n\n\t/**\n\t * @brief Called to initialize the throw calculator.\n\t * @param AttachParent Usually the hand to be tracked for throwing objects.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tvoid Initialize(USceneComponent* AttachParent);\n\n\t/**\n\t * @param LookDirection The world-space direction the player is looking in (to assist with aiming).\n\t * @return The world-space velocity of the calculated throw.\n\t */\n\tUFUNCTION(BlueprintPure)\n\tFVector GetThrowVector(FVector LookDirection) const;\n\n\t/**\n\t * @brief Tick the throw calculator with new info from its parent transform.\n\t * @param IsTracked Whether or not the current data is considered high quality.\n\t */\n\tUFUNCTION(BlueprintCallable)\n\tvoid Update(bool IsTracked);\n\n\t/// How much time to look back in the past when making a throw to account for input and render latency\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing\")\n\tfloat ThrowLatencyAdjustmentTimeSeconds = 0.04f;\n\n\t/// minimum amount of time with uninterrupted tracking needed in order to make a high-confidence throw\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: High Confidence\")\n\tfloat HighConfidenceThrowMinTrackingTime = 0.08f;\n\n\t/// minimum throw vector speed for a low-confidence throw\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Low Confidence\")\n\tfloat LowConfidenceThrowMinSpeed = 50.f;\n\n\t/// factor that determines how much to weigh the head forward vector with low confidence throwing\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Low Confidence\")\n\tfloat LowConfidenceHeadForwardFactor = 0.4f;\n\n\t/// speed factor for very low confidence throws\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Very Low Confidence\")\n\tfloat VeryLowConfidenceSpeedFactor = 5.f;\n\n\t/// factor that determines how much to weigh the very low confidence vector\n\t/// (the point that tracking was lost to the current point)\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Very Low Confidence\")\n\tfloat VeryLowConfidenceVectorFactor = 0.3f;\n\n\t/// factor that determines how much to weigh the head forward vector with very low confidence throwing\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Very Low Confidence\")\n\tfloat VeryLowConfidenceHeadForwardFactor = 0.7f;\n\n\t/// High-confidence transform buffer for the hands\n\tUPROPERTY(Transient)\n\tUTransformBufferComponent* HighConfidenceTransformBuffer;\n\n\t/// Transform buffer for the hands\n\tUPROPERTY(Transient)\n\tUTransformBufferComponent* AllTransformsTransformBuffer;\n\n\t/// Whether to choose the \"best\" throw vector or just a simple look back.\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Throw Vector Selection\")\n\tbool bSelectBestThrowVectorFromPast = true;\n\n\t/// How far back to track throw vector samples.\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Throw Vector Selection\")\n\tfloat OldestPossibleThrowVectorSeconds = 1.0f;\n\n\t/// How many throw vector samples to track.\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Throw Vector Selection\")\n\tint NumThrowVectorSamples = 10;\n\n\t/// Score weight for recency.\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Throw Vector Selection\")\n\tfloat ThrowVectorSelectionRecencyScoring = 1.0f;\n\n\t/// Score weight for direction relative to look direction.\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Throw Vector Selection\")\n\tfloat ThrowVectorSelectionDirectionScoring = 1.0f;\n\n\t/// Score weight for speed.\n\tUPROPERTY(BlueprintReadWrite, EditAnywhere, Category = \"Throwing: Throw Vector Selection\")\n\tfloat ThrowVectorSelectionSpeedScoring = 1.0f;\n\nprivate:\n\tFVector GetThrowVectorInPast(float SecondsAgo, FVector LookDirection) const;\n\tfloat GetScoreForThrowVector(FVector ThrowVector, FVector LookDirection, float TimeInPastNormalized) const;\n\tfloat GetTimeWithGoodTracking() const;\n\n\tbool WasTrackedLastFrame = false;\n\n\tfloat MostRecentTrackingLossTime;\n\tFTransform MostRecentTrackingLossTransform;\n\tFVector MostRecentTrackingLossVelocity;\n\n\tfloat MostRecentTrackingGainTime;\n\tFTransform MostRecentTrackingGainTransform;\n\tFVector MostRecentTrackingGainVelocity;\n};\n"
  },
  {
    "path": "Plugins/OculusHandTools/Source/OculusThrowAssist/Public/TransformBufferComponent.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Components/SceneComponent.h\"\n#include \"Containers/CircularBuffer.h\"\n#include \"TransformBufferComponent.generated.h\"\n\nUENUM()\nenum class ETransformBufferUpdateMode : uint8\n{\n\tManual,\n\tEveryFrame\n};\n\nUSTRUCT(BlueprintType)\nstruct FTransformBufferData\n{\n\tGENERATED_BODY()\n\n\tFTransformBufferData() :\n\t\tTransform(FTransform::Identity), Velocity(FVector::ZeroVector)\n\t{\n\t}\n\n\tFTransformBufferData(FTransform InTransform, FVector InVelocity) :\n\t\tTransform(InTransform), Velocity(InVelocity)\n\t{\n\t}\n\n\tFTransform Transform;\n\tFVector Velocity;\n};\n\nUCLASS(ClassGroup=(Custom), meta=(BlueprintSpawnableComponent))\nclass OCULUSTHROWASSIST_API UTransformBufferComponent : public USceneComponent\n{\n\tGENERATED_BODY()\n\npublic:\n\texplicit UTransformBufferComponent(FObjectInitializer const& ObjectInitializer);\n\n\t/// How the buffer will be updated with new data.\n\tUPROPERTY(EditAnywhere, Category = \"Transform Buffer\")\n\tETransformBufferUpdateMode UpdateMode = ETransformBufferUpdateMode::Manual;\n\n\t/// How long to buffer data.\n\tUPROPERTY(EditAnywhere, Category = \"Transform Buffer\")\n\tfloat MaxBufferTimeSeconds = 1.0f;\n\n\t/// Force an update to the buffer with new data.\n\tUFUNCTION(BlueprintCallable)\n\tvoid BufferCurrentData();\n\n\tvirtual void TickComponent(float DeltaTime, enum ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction) override;\n\n\t/// Get transform data from the buffer. Returns true if the data can be considered reliable, otherwise false.\n\tUFUNCTION(BlueprintCallable)\n\tbool GetBufferData(float SecondsAgo, FTransformBufferData& OutBufferData);\n\nprivate:\n\t// buffer size assumes max FPS at 90\n\tTCircularBuffer<TTuple<float, FTransformBufferData>> Buffer{(uint32)FMath::CeilToInt(MaxBufferTimeSeconds * 90)};\n\tint BufferPosition = 0;\n\n\tvoid DebugDrawBuffer() const;\n};\n"
  },
  {
    "path": "Plugins/OculusUtils/OculusUtils.uplugin",
    "content": "{\n\t\"FileVersion\": 3,\n\t\"Version\": 1,\n\t\"VersionName\": \"1.0\",\n\t\"FriendlyName\": \"Oculus Utils\",\n\t\"Description\": \"Utilities used by other Oculus plugins.\",\n\t\"Category\": \"Utilities\",\n\t\"CreatedBy\": \"Oculus\",\n\t\"CreatedByURL\": \"https://www.oculus.com/\",\n\t\"DocsURL\": \"\",\n\t\"MarketplaceURL\": \"\",\n\t\"SupportURL\": \"\",\n\t\"CanContainContent\": true,\n\t\"IsBetaVersion\": false,\n\t\"IsExperimentalVersion\": false,\n\t\"Installed\": false,\n\t\"Modules\": [\n\t\t{\n\t\t\t\"Name\": \"OculusUtils\",\n\t\t\t\"Type\": \"Runtime\",\n\t\t\t\"LoadingPhase\": \"Default\",\n\t\t\t\"WhitelistPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t}\n\t],\n\t\"Plugins\": [\n\t\t{\n\t\t\t\"Name\": \"OculusXR\",\n\t\t\t\"Enabled\": true,\n\t\t\t\"MarketplaceURL\": \"com.epicgames.launcher://ue/marketplace/product/8313d8d7e7cf4e03a33e79eb757bccba\",\n\t\t\t\"SupportedTargetPlatforms\": [\n\t\t\t\t\"Win64\",\n\t\t\t\t\"Android\"\n\t\t\t]\n\t\t}\n\t]\n}"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/OculusUtils.Build.cs",
    "content": "// Copyright (c) Facebook, Inc. and its affiliates.\n\nusing System.IO;\nusing UnrealBuildTool;\n\npublic class OculusUtils : ModuleRules\n{\n\tpublic OculusUtils(ReadOnlyTargetRules Target) : base(Target)\n\t{\n\t\tPCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;\n\n\t\tPrivateIncludePaths.Add(Path.Combine(GetModuleDirectory(\"OculusXRHMD\"), \"Private\"));\n\n\n\t\tPublicDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"Core\",\n\t\t\t\t// ... add other public dependencies that you statically link with here ...\n\t\t\t}\n\t\t);\n\n\n\t\tPrivateDependencyModuleNames.AddRange(\n\t\t\tnew string[]\n\t\t\t{\n\t\t\t\t\"CoreUObject\",\n\t\t\t\t\"Engine\",\n\t\t\t\t\"Slate\",\n\t\t\t\t\"SlateCore\",\n\t\t\t\t\"OculusXRHMD\",\n\t\t\t\t\"DeveloperSettings\",\n\t\t\t\t\"OVRPluginXR\"\n\t\t\t}\n\t\t);\n\n\t\tif (Target.bBuildEditor)\n\t\t{\n\t\t\tPrivateDependencyModuleNames.Add(\"UnrealEd\");\n\t\t\tPrivateDependencyModuleNames.Add(\"DetailCustomizations\");\n\t\t\tPrivateDependencyModuleNames.Add(\"ToolWidgets\");\n\t\t}\n\n\t\tbLegacyParentIncludePaths = true;\n\n\t\ttry\n\t\t{\n\t\t\tstring telemetryPath = GetModuleDirectory(\"OculusXRTelemetry\");\n\t\t\tif (telemetryPath != \"\")\n\t\t\t{\n\t\t\t\tPrivateDependencyModuleNames.AddRange(new string[] { \"OculusXRTelemetry\" });\n\t\t\t\tPrivateDefinitions.Add(\"OCULUS_XR_TELEMETRY=1\");\n\t\t\t}\n\t\t}\n\t\tcatch\n\t\t{\n\t\t\t// do nothing, the module doesn't exist\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Private/ContinuousOverlapSphereComponent.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"ContinuousOverlapSphereComponent.h\"\n\n#include \"OculusUtilsModule.h\"\n\nvoid UContinuousOverlapSphereComponent::SetLocation_Direct(FVector Location)\n{\n\tif (GetAttachParent() != nullptr && !IsUsingAbsoluteLocation())\n\t{\n\t\tauto const ParentToWorld = GetAttachParent()->GetSocketTransform(GetAttachSocketName());\n\t\tLocation = ParentToWorld.InverseTransformPosition(Location);\n\t}\n\tSetRelativeLocation_Direct(Location);\n\tUpdateComponentToWorld();\n}\n\nvoid UContinuousOverlapSphereComponent::OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport)\n{\n\t// Prevent infinite loop\n\tif (bIsInOnUpdateTransform)\n\t\treturn;\n\t\n\tbIsInOnUpdateTransform = true;\n\t\n\tauto const NewTransform = GetComponentTransform();\n\n\tauto const Delta = NewTransform.GetLocation() - LastLocation;\n\tif (bIsInitialized && Teleport == ETeleportType::None && !Delta.IsNearlyZero())\n\t{\n\t\tSetLocation_Direct(LastLocation);\n\t\tMoveComponent(Delta, NewTransform.GetRotation(), true);\n\t}\n\n\tbIsInitialized = true;\n\tLastLocation = NewTransform.GetLocation();\n\t\n\tSuper::OnUpdateTransform(UpdateTransformFlags, Teleport);\n\t\n\tbIsInOnUpdateTransform = false;\n}\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Private/OculusDeveloperTelemetry.cpp",
    "content": "// Copyright (c) Facebook, Inc. and its affiliates.\n\n#include \"OculusDeveloperTelemetry.h\"\n\n#include \"OculusXRHMD/Private/OculusXRHMDModule.h\"\n#include \"Widgets/Input/SHyperlink.h\"\n#include \"Widgets/Text/SRichTextBlock.h\"\n\n#ifdef OCULUS_XR_TELEMETRY\n#include \"OculusXRTelemetry.h\"\n#endif\n\n\n#define PRIVACY_POLICY_URL \"https://www.meta.com/legal/quest/privacy-policy/\"\n\n#define LOCTEXT_NAMESPACE \"FOculusUtilsModule\"\n\n#if WITH_EDITOR\n#include \"Dialog/SCustomDialog.h\"\n#include \"DetailLayoutBuilder.h\"\n#include \"DetailCategoryBuilder.h\"\n#include \"DetailWidgetRow.h\"\n\nvoid UOculusDeveloperTelemetry::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)\n{\n\tSuper::PostEditChangeProperty(PropertyChangedEvent);\n\tFlush();\n}\n\nauto MakeTelemetryTextBlock(float Width)\n{\n\tauto const TelemetryMessage = LOCTEXT(\"EnableTelemetryMessage\",\n\t\t\"Enabling telemetry will transmit data to Meta about your usage of its samples and tools. \"\n\t\t\"This information is used by Meta to improve our products and better serve our developers. For more information, go to this url: \"\n\t\t\"<a id=\\\"link\\\" href=\\\"\" PRIVACY_POLICY_URL \"\\\">\" PRIVACY_POLICY_URL \"</>\");\n\t\n\tauto const OnPrivacyPolicyClicked = FSlateHyperlinkRun::FOnClick::CreateLambda(\n\t\t[](auto)\n\t\t{\n\t\t\tFPlatformProcess::LaunchURL(TEXT(PRIVACY_POLICY_URL), nullptr, nullptr);\n\t\t});\n\t\n\treturn SNew(SRichTextBlock).\n\t\tText(TelemetryMessage).\n\t\tWrapTextAt(Width).\n\t\tAutoWrapText(Width == 0)\n\t\t+ SRichTextBlock::HyperlinkDecorator(TEXT(\"link\"), OnPrivacyPolicyClicked);\n}\n\nstatic auto const EnableText = LOCTEXT(\"EnableTelemetryEnableButton\", \"Enable\");\nstatic auto const OptOutText = LOCTEXT(\"EnableTelemetryOptOut\", \"Opt out\");\n#endif\n\nvoid UOculusDeveloperTelemetry::SendEvent(const char* eventName, const char* param, const char* source)\n{\n\tOnFlush.AddLambda([eventName, param, source]\n\t{\n#ifdef OCULUS_XR_TELEMETRY\n\t\tOculusXRTelemetry::SendEvent(eventName, param, source);\n#else\n\t\tFOculusXRHMDModule::GetPluginWrapper().SendEvent2(eventName, param, source);\n#endif\n\t});\n\tFlush();\n}\n\nvoid UOculusDeveloperTelemetry::Flush()\n{\n#if WITH_EDITOR\n\tif (bHasPrompted == false)\n\t{\n\t\tauto const Title = LOCTEXT(\"EnableTelemetryTitle\", \"Enable Meta Telemetry\");\n\n\t\tTSharedRef<SCustomDialog> CustomDialog = SNew(SCustomDialog).\n\t\t\tTitle(Title).\n\t\t\tContent()[\n\t\t\t\tMakeTelemetryTextBlock(400)\n\t\t\t].\n\t\t\tButtons({\n\t\t\t\tSCustomDialog::FButton(EnableText),\n\t\t\t\tSCustomDialog::FButton(OptOutText),\n\t\t\t\t});\n\t\t\n\t\tauto const Return = CustomDialog->ShowModal();\n\n\t\tbHasPrompted = true;\n\t\tbIsEnabled = Return == 0;\n\n\t\tModify();\n\t\tSaveConfig();\n\t}\n\n\tif (bIsEnabled)\n\t{\n#ifdef OCULUS_XR_TELEMETRY\n\t\tOculusXRTelemetry::SetDeveloperTelemetryConsent(true);\n#else\n\t\tif (FOculusXRHMDModule::GetPluginWrapper().SetDeveloperMode)\n\t\t{\n\t\t\tFOculusXRHMDModule::GetPluginWrapper().SetDeveloperMode(true);\n\t\t}\n#endif\n\t\tOnFlush.Broadcast();\n\t\tOnFlush.Clear();\n\t}\n\n#endif\n}\n\n#if WITH_EDITOR\nvoid FOculusTelemetrySettingsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)\n{\n\tTArray<TWeakObjectPtr<UObject>> Objects;\n\tDetailBuilder.GetObjectsBeingCustomized(Objects);\n\n\tfor (auto Object : Objects)\n\t{\n\t\tauto Telemetry = MakeWeakObjectPtr(Cast<UOculusDeveloperTelemetry>(Object));\n\t\tif (Telemetry != nullptr)\n\t\t{\n\t\t\tauto&& Category = DetailBuilder.EditCategory(TEXT(\"Privacy\"));\n\t\t\tauto IsEnabledProperty = DetailBuilder.GetProperty(TEXT(\"bIsEnabled\"));\n\n\t\t\tauto IsEnabled = [Telemetry]{ return Telemetry.IsValid() && Telemetry->bIsEnabled ? ECheckBoxState::Checked : ECheckBoxState::Unchecked; };\n\t\t\tauto IsOptedOut = [Telemetry]{ return Telemetry.IsValid() && Telemetry->bIsEnabled ? ECheckBoxState::Unchecked : ECheckBoxState::Checked; };\n\t\t\tauto SetEnabled = [Telemetry](ECheckBoxState state)\n\t\t\t{\n\t\t\t\tif (Telemetry.IsValid())\n\t\t\t\t{\n\t\t\t\t\tTelemetry->bIsEnabled = state == ECheckBoxState::Checked;\n\t\t\t\t\tTelemetry->bHasPrompted = true;\n\t\t\t\t\tTelemetry->Modify();\n\t\t\t\t\tTelemetry->SaveConfig();\n\t\t\t\t\tTelemetry->Flush();\n\t\t\t\t}\n\t\t\t};\n\t\t\tauto SetOptOut = [SetEnabled](ECheckBoxState state){ SetEnabled(state == ECheckBoxState::Unchecked ? ECheckBoxState::Checked : ECheckBoxState::Unchecked); };\n\n\t\t\tCategory.InitiallyCollapsed(false)\n\t\t\t\t\t.AddProperty(IsEnabledProperty)\n\t\t\t\t\t.ShouldAutoExpand(true)\n\t\t\t\t\t.CustomWidget(true)\n\t\t\t\t\t[\n\t\t\t\t\t\tSNew(SVerticalBox)\n\t\t\t\t\t\t+ SVerticalBox::Slot().AutoHeight() [ MakeTelemetryTextBlock(0) ]\n\t\t\t\t\t\t+ SVerticalBox::Slot().AutoHeight()\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tSNew(SCheckBox).\n\t\t\t\t\t\t\t\tIsChecked_Lambda(IsEnabled).\n\t\t\t\t\t\t\t\tOnCheckStateChanged_Lambda(SetEnabled).\n\t\t\t\t\t\t\t\tContent() [ SNew(STextBlock).Text(EnableText) ]\n\t\t\t\t\t\t]\n\t\t\t\t\t\t+ SVerticalBox::Slot().AutoHeight()\n\t\t\t\t\t\t[\n\t\t\t\t\t\t\tSNew(SCheckBox).\n\t\t\t\t\t\t\t\tIsChecked_Lambda(IsOptedOut).\n\t\t\t\t\t\t\t\tOnCheckStateChanged_Lambda(SetOptOut).\n\t\t\t\t\t\t\t\tContent() [ SNew(STextBlock).Text(OptOutText) ]\n\t\t\t\t\t\t]\n\t\t\t\t\t];\n\t\t}\n\t}\n}\n#endif\n\n#undef LOCTEXT_NAMESPACE\n\n#undef PRIVACY_POLICY_URL\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Private/OculusUtilsLibrary.cpp",
    "content": "// Copyright (c) Facebook, Inc. and its affiliates.\n\n#include \"OculusUtilsLibrary.h\"\n\n#include \"DelayAction.h\"\n#include \"OculusUtilsModule.h\"\n#include \"Misc/DefaultValueHelper.h\"\n#include \"Misc/DateTime.h\"\n\nbool UOculusUtilsLibrary::GetOculusBuildInfo(FString& SourceControlChangelist, FString& BuildDateTimeString)\n{\n\tauto BuildInfo = GetDefault<UBuildInfo>();\n\tif (!BuildInfo)\n\t{\n\t\tSourceControlChangelist = \"Unknown Changelist\";\n\t\tBuildDateTimeString = \"Unknown Build Time\";\n\t\treturn false;\n\t}\n\n\t// Oculus Store builds have a changelist number, otherwise it's a development one.\n\tSourceControlChangelist = BuildInfo->PackageChangelist;\n\tUE_LOG(LogOculusUtils, Display, TEXT(\"Build Info - source control changelist: \\\"%s\\\"\"), *SourceControlChangelist);\n\n\t// Extracting date and time values.\n\tFString DateString, TimeString;\n\tauto DateNumber = 0, TimeNumber = 0;\n\tif (BuildInfo->PackageDateAndTime.Split(TEXT(\" \"), &DateString, &TimeString) &&\n\t\tFDefaultValueHelper::ParseInt(DateString, DateNumber) &&\n\t\tFDefaultValueHelper::ParseInt(TimeString, TimeNumber))\n\t{\n\t\tUE_LOG(LogOculusUtils, Display, TEXT(\"Build Info - parsed package date %d and time %d\"), DateNumber, TimeNumber);\n\n\t\t// Separating date/time components.\n\t\tauto const Day = DateNumber % 100;\n\t\tDateNumber /= 100;\n\t\tauto const Month = DateNumber % 100;\n\t\tauto const Year = DateNumber / 100;\n\n\t\tauto const Second = TimeNumber % 100;\n\t\tTimeNumber /= 100;\n\t\tauto const Minute = TimeNumber % 100;\n\t\tauto const Hour = TimeNumber / 100;\n\n\t\t// We use a generic PT for both PDT and PST, where Oculus build servers are located.\n\t\tFDateTime BuildDate(Year, Month, Day, Hour, Minute, Second);\n\t\tBuildDateTimeString = BuildDate.ToString(TEXT(\"%Y-%m-%d %H:%M:%S PT\"));\n\t}\n\telse\n\t{\n\t\t// If unparsable, we keep it as is.\n\t\tUE_LOG(LogOculusUtils, Display, TEXT(\"Build Info - cannot parse package date and time: \\\"%s\\\"\"), *(BuildInfo->PackageDateAndTime));\n\t\tBuildDateTimeString = BuildInfo->PackageDateAndTime;\n\t}\n\treturn !(SourceControlChangelist.IsEmpty() && BuildDateTimeString.IsEmpty());\n}\n\nTArray<UActorComponent*> UOculusUtilsLibrary::SortComponentsByName(TArray<UActorComponent*> const& Components)\n{\n\tauto SortedComponents = Components;\n\n\tSortedComponents.Sort(\n\t\t[](UActorComponent const& A, UActorComponent const& B) -> bool\n\t\t{\n\t\t\treturn A.GetName().Compare(B.GetName()) < 0;\n\t\t});\n\treturn SortedComponents;\n}\n\nclass FTickUntilAction : public FPendingLatentAction\n{\npublic:\n\tFName ExecutionFunction;\n\tint32 OutputLink;\n\tFWeakObjectPtr CallbackTarget;\n\tbool bIsComplete = false;\n\n\tFTickUntilAction(FLatentActionInfo const& LatentInfo) :\n\t\tExecutionFunction(LatentInfo.ExecutionFunction),\n\t\tOutputLink(LatentInfo.Linkage),\n\t\tCallbackTarget(LatentInfo.CallbackTarget)\n\t{\n\t}\n\n\tvirtual void UpdateOperation(FLatentResponse& Response) override\n\t{\n\t\tif (bIsComplete)\n\t\t{\n\t\t\tResponse.DoneIf(true);\n\t\t}\n\t\telse\n\t\t{\n\t\t\tResponse.TriggerLink(ExecutionFunction, OutputLink, CallbackTarget);\n\t\t}\n\t}\n};\n\nvoid UOculusUtilsLibrary::TickUntil(UObject const* WorldContextObject, ETickUntilInputPin InputPin, FLatentActionInfo LatentInfo)\n{\n\tif (auto World = GEngine->GetWorldFromContextObject(WorldContextObject, EGetWorldErrorMode::LogAndReturnNull))\n\t{\n\t\tauto& LatentActionManager = World->GetLatentActionManager();\n\t\tauto Action = LatentActionManager.FindExistingAction<FTickUntilAction>(LatentInfo.CallbackTarget, LatentInfo.UUID);\n\t\tif (InputPin == ETickUntilInputPin::Start)\n\t\t{\n\t\t\tif (Action == nullptr)\n\t\t\t{\n\t\t\t\tLatentActionManager.AddNewAction(LatentInfo.CallbackTarget, LatentInfo.UUID, new FTickUntilAction(LatentInfo));\n\t\t\t}\n\t\t}\n\t\telse if (InputPin == ETickUntilInputPin::Break)\n\t\t{\n\t\t\tif (Action != nullptr)\n\t\t\t{\n\t\t\t\tAction->bIsComplete = true;\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Private/OculusUtilsModule.cpp",
    "content": "// Copyright (c) Facebook, Inc. and its affiliates.\n\n#include \"OculusUtilsModule.h\"\n\n#if WITH_EDITOR\n#include \"ISettingsModule.h\"\n#endif\n\n#include \"OculusDeveloperTelemetry.h\"\n\nOCULUS_TELEMETRY_LOAD_MODULE(\"Unreal-OculusUtils\");\n\n#define LOCTEXT_NAMESPACE \"FOculusUtilsModule\"\n\nDEFINE_LOG_CATEGORY(LogOculusUtils);\n\nvoid FOculusUtilsModule::StartupModule()\n{\n#if WITH_EDITOR\n\tauto&& PropertyModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(\"PropertyEditor\");\n\tPropertyModule.RegisterCustomClassLayout(\n\t\t\"OculusDeveloperTelemetry\",\n\t\tFOnGetDetailCustomizationInstance::CreateLambda([]\n\t\t{ \n\t\t\treturn MakeShared<FOculusTelemetrySettingsCustomization>(); \n\t\t}));\n\n\tauto SettingsModule = FModuleManager::GetModulePtr<ISettingsModule>(\"Settings\");\n\tSettingsModule->RegisterSettings(\n\t\t\"Editor\", \"Privacy\", \"Oculus Developer Telemetry\",\n\t\tLOCTEXT(\"PrivacyAnalyticsSettingsName\", \"Oculus Developer Telemetry\"),\n\t\tLOCTEXT(\"PrivacyAnalyticsSettingsDescription\", \"Configure the way your Editor usage information is handled.\"),\n\t\tGetMutableDefault<UOculusDeveloperTelemetry>());\n#endif\n}\n\nvoid FOculusUtilsModule::ShutdownModule()\n{\n\t// This function may be called during shutdown to clean up your module.  For modules that support dynamic reloading,\n\t// we call this function before unloading the module.\n}\n\n#undef LOCTEXT_NAMESPACE\n\nIMPLEMENT_MODULE(FOculusUtilsModule, OculusUtils)\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Public/ContinuousOverlapSphereComponent.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"Components/SphereComponent.h\"\n\n#include \"ContinuousOverlapSphereComponent.generated.h\"\n\n/**\n * Used similarly to SphereComponent, but modified to use continuous \"collision\" for triggering overlaps.\n * Useful for fast-moving spheres with non-overlap collision disabled.\n */\nUCLASS(BlueprintType, meta=(BlueprintSpawnableComponent))\nclass OCULUSUTILS_API UContinuousOverlapSphereComponent : public USphereComponent\n{\n\tGENERATED_BODY()\n\n\tFVector LastLocation;\n\tbool bIsInitialized = false;\n\tbool bIsInOnUpdateTransform = false;\n\t\n\tvoid SetLocation_Direct(FVector Location);\n\npublic:\n\tvirtual void OnUpdateTransform(EUpdateTransformFlags UpdateTransformFlags, ETeleportType Teleport) override;\n};\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Public/OculusDeveloperTelemetry.h",
    "content": "// Copyright (c) Facebook, Inc. and its affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\n#if WITH_EDITOR\n#include \"IDetailCustomization.h\"\n#endif\n\n#include \"OculusDeveloperTelemetry.generated.h\"\n\nUCLASS(Config=EditorPerProjectUserSettings, meta=(DisplayName=\"Oculus Developer Telemetry\"))\nclass OCULUSUTILS_API UOculusDeveloperTelemetry : public UObject\n{\n\tGENERATED_BODY()\n\n\tDECLARE_MULTICAST_DELEGATE(FOnFlushEvent);\n\tFOnFlushEvent OnFlush;\n\npublic:\n#if WITH_EDITOR\n\tvirtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;\n#endif\n\t\n\tUPROPERTY(EditAnywhere, Config, meta=(Tooltip=\"Enabling telemetry will transmit data to Oculus about your usage of its samples and tools.\"))\n\tbool bIsEnabled = false;\n\t\n\tUPROPERTY(Config)\n\tbool bHasPrompted = false;\n\t\n\tvoid SendEvent(const char* eventName, const char* param, const char* source);\n\tvoid Flush();\n};\n\n#if WITH_EDITOR\nclass FOculusTelemetrySettingsCustomization : public IDetailCustomization\n{\npublic:\n\tvirtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;\n};\n#endif\n\n#if WITH_EDITOR\n\t#define OCULUS_TELEMETRY_LOAD_MODULE(ModuleName) \\\n\t\tstatic FDelayedAutoRegisterHelper ANONYMOUS_VARIABLE(GMetricsHelper) (EDelayedRegisterRunPhase::EndOfEngineInit, [] \\\n\t\t{ \\\n\t\t\tGetMutableDefault<UOculusDeveloperTelemetry>()->SendEvent(\"module_loaded\", ModuleName, \"integration\"); \\\n\t\t});\n#else\n\t#define OCULUS_TELEMETRY_LOAD_MODULE(...)\n#endif\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Public/OculusUtilsLibrary.h",
    "content": "// Copyright (c) Facebook, Inc. and its affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"Kismet/BlueprintFunctionLibrary.h\"\n#include \"Components/ActorComponent.h\"\n#include \"OculusUtilsLibrary.generated.h\"\n\nUENUM(BlueprintType)\nenum class ETickUntilInputPin : uint8\n{\n\tStart,\n\tBreak\n};\n\n/**\n * Build configuration from the project's game configuration.\n */\nUCLASS(Config=Game)\nclass OCULUSUTILS_API UBuildInfo : public UObject\n{\n\tGENERATED_BODY()\n\npublic:\n\t/** Source control changelist */\n\tUPROPERTY(Config)\n\tFString PackageChangelist;\n\n\t/** Build date and time (Pacific time) \"YYYYMMDD HHMMSS\" */\n\tUPROPERTY(Config)\n\tFString PackageDateAndTime;\n};\n\n/**\n * Oculus Utils Blueprint Library.\n */\nUCLASS()\nclass OCULUSUTILS_API UOculusUtilsLibrary : public UBlueprintFunctionLibrary\n{\n\tGENERATED_BODY()\n\npublic:\n\t/** Returns the Oculus store version. */\n\tUFUNCTION(BlueprintCallable, Category = \"Oculus Utils\")\n\tstatic bool GetOculusBuildInfo(FString& SourceControlChangelist, FString& BuildDateTimeString);\n\n\t/** Returns the array of components sorted by name. */\n\tUFUNCTION(BlueprintCallable, Category = \"Oculus Utils\", meta = (ComponentClass = \"ActorComponent\", DeterminesOutputType = \"Components\"))\n\tstatic TArray<UActorComponent*> SortComponentsByName(const TArray<UActorComponent*>& Components);\n\n\t/** \n\t* Executes the \"Completed\" pin every tick until Break is hit. Calling Start again while it is still ticking will be ignored.\n\t* \n\t* @param WorldContextObject\tWorld context.\n\t* @param InputPin\tWhich pin is being called.\n\t* @param LatentInfo \tThe latent action.\n\t*/\n\tUFUNCTION(BlueprintCallable, Category=\"Utilities|FlowControl\", meta=(Latent, WorldContext=\"WorldContextObject\", LatentInfo=\"LatentInfo\", Keywords=\"sleep\", ExpandEnumAsExecs=\"InputPin\"))\n\tstatic void TickUntil(const UObject* WorldContextObject, ETickUntilInputPin InputPin, struct FLatentActionInfo LatentInfo);\n};\n"
  },
  {
    "path": "Plugins/OculusUtils/Source/OculusUtils/Public/OculusUtilsModule.h",
    "content": "// Copyright (c) Facebook, Inc. and its affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\nDECLARE_LOG_CATEGORY_EXTERN(LogOculusUtils, Log, All);\n\nclass FOculusUtilsModule : public IModuleInterface\n{\npublic:\n\t/** IModuleInterface implementation */\n\tvirtual void StartupModule() override;\n\tvirtual void ShutdownModule() override;\n};\n"
  },
  {
    "path": "README.md",
    "content": "# Hand Gameplay Showcase\n\n[<img width=\"100%\" src=\"./Media/hands-hero.png\" />](https://www.meta.com/experiences/oculus-hand-gameplay-showcase-for-unreal/4232440213539049/)\n\nThis project offers reusable components built on the robust hand tracking mechanics from [First Steps with Hand Tracking](https://www.meta.com/experiences/first-steps-with-hand-tracking/3974885535895823/) and [Tiny Castles](https://www.meta.com/experiences/tiny-castles/3647163948685453/).\n\nTry the showcase yourself on the [Horizon Store](https://www.meta.com/experiences/4232440213539049/).\n\n## Build Instructions\n\n### Download the Project\n\nFirst, install Git LFS by running:\n```sh\ngit lfs install\n```\n\nNext, clone this repository using the \"Code\" button above or run:\n```sh\ngit clone https://github.com/oculus-samples/Unreal-HandGameplay.git\n```\n\nFinally, open the project in Unreal Editor using one of the following methods.\n\n#### Epic Games Launcher\n\nThe easiest way to start is with the prebuilt Unreal Engine from the Epic Games Launcher. Note that the [Hand Movement Filtering](./Plugins/OculusHandTools/README_HandTrackingFilter.md) will not work without the Oculus fork described below.\n\n1. Install the [Epic Games Launcher](https://store.epicgames.com/en-US/download).\n2. Install Unreal Engine 5.3 or later via the launcher.\n3. Launch Unreal Editor.\n4. Click \"More\" <br /><img width=\"400\" src=\"https://user-images.githubusercontent.com/791774/148618198-afbe2e70-18a4-41ec-9bad-bf90fac05edc.png\" />\n5. Click \"Browse\" and select `HandGameplay.uproject`.\n\n#### Oculus Unreal Fork\n\nThe Oculus Unreal fork provides the latest Oculus feature integration but requires building the editor from source.\n\n1. [Get access to the Unreal source code](https://developers.meta.com/horizon/documentation/unreal/unreal-building-ue4-from-source/#prerequisites).\n2. [Clone the `oculus-5.6` branch of the Oculus fork](https://github.com/Oculus-VR/UnrealEngine/tree/oculus-5.6).\n3. [Install Visual Studio](https://developers.meta.com/horizon/documentation/unreal/unreal-building-ue4-from-source/#to-download-and-build-unreal-engine).\n4. Open a command prompt in the Unreal root directory and run:\n```sh\n.\\GenerateProjectFiles.bat -Game HandGameplay -Engine <full path to Unreal-HandGameplay directory>\\HandGameplay.uproject\n```\n5. Open the generated `HandGameplay.sln` file in the `Unreal-HandGameplay` directory.\n6. Set `HandGameplay` as the start-up project and `Development Editor` as the configuration.\n7. Press `F5` to build and debug the project and engine.\n\n### Use as Plugin\n\nTo add these features to your project, install the OculusHandTools plugin. Download the latest release of `OculusHandTools.zip` and extract it into your project's `Plugins` folder.\n\nFor a detailed explanation of the mechanics, see [here](./Plugins/OculusHandTools/README.md#mechanics-implementations). The OculusHandTools plugin also includes several useful C++ modules:\n\n- [HandInput](./Plugins/OculusHandTools/README_HandInput.md)\n- [HandPoseRecognition](./Plugins/OculusHandTools/README_HandPoseRecognition.md)\n- [OculusHandTrackingFilter](./Plugins/OculusHandTools/README_HandTrackingFilter.md)\n- [OculusInteractable](./Plugins/OculusHandTools/README_Interactable.md)\n- [OculusThrowAssist](./Plugins/OculusHandTools/README_ThrowAssist.md)\n- [OculusUtils](./Plugins/OculusHandTools/README_OculusUtils.md)\n\n## Features\n\n| Feature | Image | Description |\n|---------|-------|-------------|\n| **[Teleportation](./Plugins/OculusHandTools/README.md#teleportation)** | <img width=\"256\" src=\"./Media/teleportation.png\" /> | Simple movement using pose recognition from the Hand Pose Showcase. |\n| **[Grabbing](./Plugins/OculusHandTools/README_HandInput.md)** | <img width=\"256\" src=\"./Media/grabbing.png\" /> | Recognizes natural grab gestures, attaches objects to your hand, and overrides hand pose for visual feedback. |\n| **[Throwing](./Plugins/OculusHandTools/README.md#throwing)** | &nbsp; | Uses hand history data to calculate the velocity of thrown objects. |\n| **[Button Pushing](./Plugins/OculusHandTools/README.md#pushing-buttons)** | <img width=\"256\" src=\"./Media/button.png\" /> | Reliable digital interaction (on/off). (Bonus: your pointer finger is a digit!) |\n| **[Punching](./Plugins/OculusHandTools/README.md#punching)** | <img width=\"256\" src=\"./Media/punching.png\" /> | A satisfying hand interaction, despite fast movement sometimes causing tracking loss. |\n| **[Hand Movement Filtering](./Plugins/OculusHandTools/README_HandTrackingFilter.md)** | &nbsp; | Stabilizes hand and finger movement during low-quality or lost tracking, improving feel especially during punching. More details [here](https://developers.meta.com/horizon/blog/adding-hand-tracking-to-first-steps/). |\n| **[Two-handed Aiming](./Plugins/OculusHandTools/README.md#two-handed-aiming)** | <img width=\"256\" src=\"./Media/aiming.png\" /> | Reliable and fulfilling hand interaction using both hands. |\n| **[Example Hands for Tutorials](./Plugins/OculusHandTools/README.md#example-hands-for-tutorials)** | &nbsp; | Illustrates the poses your app expects from users. |\n\n## License\n\nThis codebase serves as a reference and template for multiplayer VR games. All code and assets follow the license found [here](./LICENSE), unless otherwise noted.\n\n## Contribution\n\nSee the [CONTRIBUTING](./CONTRIBUTING.md) file for contribution guidelines.\n\n# Updates\n\n## 20 December 2023 Update\n\nWe updated the project to UE5.3.\n\n## March 2025 Update\n\nWe updated the project to use OpenXR from Epic with meta vendor extensions.\nPlease note, you can still use the OVRPlugin, but you'll need to update the Grab Poses on:\n\n* Content/HandGameplay/Probs/Blocks/InteractableBrick\n* Content/HandGameplay/Probs/RingWeapon/InteractableArtifactHandle\n\nExample for InteractableBrick:\n\n* Change relative Hand Transform left to: ``LOC X1.623 Y12.178 Z8.067 ROT W0.160 X0.189 Y-0.832 Z-0.497``\n* Change relative Hand Transform right to: ``LOC X5.690 Y-10.448 Z-8.640 ROT W0.840 X0.534 Y0.064 Z0.068``\n\nExample for InteractableArtifactHandle:\n\n* Change relative Hand Transform left to: ``LOC X1.094 Y6.294 Z11.635 ROT W0.101 X-0.548 Y-0.830 Z-0.009``\n* Change relative Hand Transform right to: ``LOC X-0.519 Y-6.451 Z-12.047 ROT W0.859 X0.053 Y0.058 Z-0.506``\n\nBest way to get new HandTransforms:\n\n* Open HansCharacterHandsState from OculusHandTools/Content/Hands/\n* Reconnect the Blueprint-flow-nodes\n* This will output the location of your hand when grabbing an object in the correct format for copy and paste.\n\n## 04 December 2025 Update\n\nWe fixed an issue related to objects jittering when grabbed.\nWe updated the project to UE5.6. + Meta SDK v81\n"
  },
  {
    "path": "Source/HandGameplay/HandGameplay.Build.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\n\npublic class HandGameplay : ModuleRules\n{\n\tpublic HandGameplay(ReadOnlyTargetRules Target) : base(Target)\n\t{\n\t\tPCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;\n\t\n\t\tPublicDependencyModuleNames.AddRange(new string[] { \"Core\", \"CoreUObject\", \"Engine\", \"InputCore\", \"OculusHandPoseRecognition\" });\n\n\t\tPrivateDependencyModuleNames.AddRange(new string[] { \"OculusUtils\" });\n\n\t\t// Uncomment if you are using Slate UI\n\t\t// PrivateDependencyModuleNames.AddRange(new string[] { \"Slate\", \"SlateCore\" });\n\n\t\t// Uncomment if you are using online features\n\t\t// PrivateDependencyModuleNames.Add(\"OnlineSubsystem\");\n\n\t\t// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true\n\n\t\tif (Target.Platform == UnrealTargetPlatform.Android)\n\t\t{\n\t\t\tvar manifestFile = System.IO.Path.Combine(ModuleDirectory, \"UpdatePermissions.xml\");\n\t\t\tAdditionalPropertiesForReceipt.Add(\"AndroidPlugin\", manifestFile);\n\t\t}\n\t}\n}"
  },
  {
    "path": "Source/HandGameplay/HandGameplay.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandGameplay.h\"\n#include \"Modules/ModuleManager.h\"\n\n#include \"OculusDeveloperTelemetry.h\"\n\nOCULUS_TELEMETRY_LOAD_MODULE(\"Unreal-HandGameplayShowcase\");\n\nIMPLEMENT_PRIMARY_GAME_MODULE(\n\tFDefaultGameModuleImpl,\n\tHandGameplay,\n\t\"HandGameplay\"\n);\n\nDEFINE_LOG_CATEGORY(LogHandGameplayShowcase);\n"
  },
  {
    "path": "Source/HandGameplay/HandGameplay.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n\nDECLARE_LOG_CATEGORY_EXTERN(LogHandGameplayShowcase, Log, All);\n"
  },
  {
    "path": "Source/HandGameplay/HandGameplayGameModeBase.cpp",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#include \"HandGameplayGameModeBase.h\"\n"
  },
  {
    "path": "Source/HandGameplay/HandGameplayGameModeBase.h",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\n#pragma once\n\n#include \"CoreMinimal.h\"\n#include \"GameFramework/GameModeBase.h\"\n#include \"HandGameplayGameModeBase.generated.h\"\n\n/**\n * \n */\nUCLASS()\nclass HANDGAMEPLAY_API AHandGameplayGameModeBase : public AGameModeBase\n{\n\tGENERATED_BODY()\n};\n"
  },
  {
    "path": "Source/HandGameplay/UpdatePermissions.xml",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root xmlns:android=\"http://schemas.android.com/apk/res/android\">\n\t<androidManifestUpdates>\n\t\t<removePermission android:name=\"android.permission.READ_EXTERNAL_STORAGE\" />\n\t\t<removePermission android:name=\"android.permission.WRITE_EXTERNAL_STORAGE\" />\n\t</androidManifestUpdates>\n</root>\n"
  },
  {
    "path": "Source/HandGameplay.Target.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\nusing System.Collections.Generic;\n\npublic class HandGameplayTarget : TargetRules\n{\n\tpublic HandGameplayTarget( TargetInfo Target) : base(Target)\n\t{\n\t\tType = TargetType.Game;\n\t\tDefaultBuildSettings = BuildSettingsVersion.V4;\n\t\tIncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6;\n\t\tExtraModuleNames.AddRange( new string[] { \"HandGameplay\" } );\n\t}\n}\n"
  },
  {
    "path": "Source/HandGameplayEditor.Target.cs",
    "content": "// Copyright (c) Meta Platforms, Inc. and affiliates.\n\nusing UnrealBuildTool;\nusing System.Collections.Generic;\n\npublic class HandGameplayEditorTarget : TargetRules\n{\n\tpublic HandGameplayEditorTarget( TargetInfo Target) : base(Target)\n\t{\n\t\tType = TargetType.Editor;\n\t\tDefaultBuildSettings = BuildSettingsVersion.V4;\n\t\tIncludeOrderVersion = EngineIncludeOrderVersion.Unreal5_6;\n\t\tExtraModuleNames.AddRange( new string[] { \"HandGameplay\" } );\n\t}\n}\n"
  }
]