From 849e63fdd0ee93ae44520c35c0ceaab15fe5ce9f Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Mon, 18 May 2026 19:01:20 -0400 Subject: [PATCH 01/22] Fixing VR lighting. --- Assets/Runtime/TopLevel/Scripts/DesktopMode.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Assets/Runtime/TopLevel/Scripts/DesktopMode.cs b/Assets/Runtime/TopLevel/Scripts/DesktopMode.cs index 271d31c5..9c71240f 100644 --- a/Assets/Runtime/TopLevel/Scripts/DesktopMode.cs +++ b/Assets/Runtime/TopLevel/Scripts/DesktopMode.cs @@ -201,6 +201,8 @@ public class DesktopMode : MonoBehaviour public void EnableVR() { vrEnabled = true; + // PROBE: testing whether HDR output is a compounding cause of VR overbright. Revert after test. + vrCamera.allowHDR = false; StartCoroutine(EnableVRCoroutine()); desktopRig.SetActive(false); vrRig.transform.position = desktopRig.transform.position; From 733204665730fe7319e12467eaaaf03ca8f33f58 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 19:01:09 -0400 Subject: [PATCH 02/22] Adding additional world load helper APIs --- .../WorldBrowserUtilities/Scripts/World.cs | 50 ++- .../JavascriptHandler/Tests/WorldAPITests.cs | 254 +++++++++++++ .../Tests/WorldAPITests.cs.meta | 2 + .../VEMLHandler/Scripts/VEMLHandler.cs | 334 +++++++++++++++++- .../Runtime/Scripts/WebVerseRuntime.cs | 54 ++- 5 files changed, 677 insertions(+), 17 deletions(-) create mode 100644 Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs create mode 100644 Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs.meta diff --git a/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs b/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs index 4d7867de..486f269b 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/APIs/WorldBrowserUtilities/Scripts/World.cs @@ -20,6 +20,15 @@ public static string GetQueryParam(string key) return WebVerseRuntime.Instance.straightFour.GetParam(key); } + /// + /// Get the URL of the currently loaded World or Web Page. + /// + /// The URL of the current World or Web Page, or null if none has been loaded. + public static string GetWorldURL() + { + return WebVerseRuntime.Instance.currentURL; + } + /// /// Get the current World Load State. /// @@ -51,6 +60,19 @@ public static string GetWorldLoadState() /// /// The URL of the World to load. public static void LoadWorld(string url) + { + LoadWorld(url, null); + } + + /// + /// Load a World from a URL, along with a script to run in the same JINT engine as the world's + /// own scripts. + /// + /// The URL of the World to load. + /// Either inline JavaScript logic, or a URI ending in ".js" pointing + /// to a script resource. The script is prepended to the world's script list and runs first. + /// Only supported for VEML worlds; ignored for x3d and glTF worlds. + public static void LoadWorld(string url, string requireScript) { WebVerseRuntime.Instance.LoadWorld(url, new System.Action((name) => { @@ -60,7 +82,33 @@ public static void LoadWorld(string url) multibar.ToggleMultibar(); multibar.ToggleMultibar(); } - })); + }), requireScript); + } + + /// + /// Dry-run validation of a World's VEML without switching to it. Downloads and parses the + /// VEML, downloads (but does not execute) referenced scripts, and HEAD-requests referenced + /// asset URIs. Reports the result via the JS callback. Does not unload the active world, + /// mutate runtime state, or touch the JINT engine. + /// + /// The URL of the World to test. + /// Name of a JS function to invoke when the test completes. The + /// function is called with three arguments: (success: bool, errorMessage: string|null, title: + /// string|null). On success, errorMessage is null. On failure, errorMessage is a + /// newline-separated list of issues. title is the parsed metadata.title when the document + /// parsed, otherwise null. + public static void TestLoadWorld(string url, string onTestComplete) + { + WebVerseRuntime.Instance.TestLoadWorld(url, + new System.Action((success, errorMessage, title) => + { + if (string.IsNullOrEmpty(onTestComplete)) + { + return; + } + WebVerseRuntime.Instance.javascriptHandler.CallWithParams( + onTestComplete, new object[] { success, errorMessage, title }); + })); } /// diff --git a/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs new file mode 100644 index 00000000..c0efa868 --- /dev/null +++ b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs @@ -0,0 +1,254 @@ +// Copyright (c) 2019-2026 Five Squared Interactive. All rights reserved. + +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using FiveSQD.WebVerse.Runtime; +using FiveSQD.WebVerse.LocalStorage; +using System.IO; +using System.Reflection; +using WorldAPI = FiveSQD.WebVerse.Handlers.Javascript.APIs.Utilities.World; + +/// +/// Unit tests for the World JavaScript API. +/// +public class WorldAPITests +{ + private WebVerseRuntime runtime; + private GameObject runtimeGO; + private string testDirectory; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + LogAssert.ignoreFailingMessages = true; + } + + [SetUp] + public void SetUp() + { + LogAssert.ignoreFailingMessages = true; + + runtimeGO = new GameObject("runtime"); + runtime = runtimeGO.AddComponent(); + + runtime.highlightMaterial = new Material(Shader.Find("Standard")); + runtime.skyMaterial = new Material(Shader.Find("Standard")); + runtime.characterControllerPrefab = new GameObject("DummyCharacterController"); + runtime.inputEntityPrefab = new GameObject("DummyInputEntity"); + runtime.voxelPrefab = new GameObject("DummyVoxel"); + runtime.webVerseWebViewPrefab = new GameObject("DummyWebView"); + + testDirectory = Path.Combine(Path.GetTempPath(), "WorldAPITests"); + runtime.Initialize(LocalStorageManager.LocalStorageMode.Cache, 128, 128, 128, testDirectory); + } + + [TearDown] + public void TearDown() + { + WebVerseRuntime.Instance = null; + if (runtime != null && Directory.Exists(testDirectory)) + { + Directory.Delete(testDirectory, true); + } + if (runtimeGO != null) + { + Object.DestroyImmediate(runtimeGO); + } + } + + private static void SetCurrentURL(WebVerseRuntime runtime, string url) + { + PropertyInfo prop = typeof(WebVerseRuntime).GetProperty( + "currentURL", BindingFlags.Public | BindingFlags.Instance); + Assert.NotNull(prop, "currentURL property must exist on WebVerseRuntime."); + prop.SetValue(runtime, url); + } + + [Test] + public void GetWorldURL_BeforeAnyLoad_ReturnsNull() + { + Assert.IsNull(WorldAPI.GetWorldURL()); + } + + [Test] + public void GetWorldURL_AfterCurrentURLSet_ReturnsThatURL() + { + const string url = "https://example.test/world.veml"; + SetCurrentURL(runtime, url); + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void GetWorldURL_ReflectsLatestAssignment() + { + SetCurrentURL(runtime, "https://example.test/first.veml"); + SetCurrentURL(runtime, "https://example.test/second.veml"); + + Assert.AreEqual("https://example.test/second.veml", WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_SetsCurrentURL() + { + const string url = "https://example.test/load-world.veml"; + + // The full load pipeline pulls in handlers and HTTP that aren't wired + // up in this test context, so downstream work may throw. We only care + // that the currentURL assignment at the top of LoadWorld ran. + try { runtime.LoadWorld(url, null); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWebPage_SetsCurrentURL() + { + const string url = "https://example.test/page.html"; + + try { runtime.LoadWebPage(url, null); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_WithInlineRequireScript_AcceptsArgument() + { + const string url = "https://example.test/with-require.veml"; + + // The new overload should accept an inline JS body without throwing at the + // signature/dispatch boundary. Downstream load machinery may still throw. + try { runtime.LoadWorld(url, null, "var __requireSentinel = 1;"); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_WithURIRequireScript_AcceptsArgument() + { + const string url = "https://example.test/with-require-uri.veml"; + + try { runtime.LoadWorld(url, null, "init.js"); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void LoadWorld_DefaultRequireScript_BackwardCompatible() + { + // The single-onLoaded overload signature still works (default param = null). + const string url = "https://example.test/no-require.veml"; + + try { runtime.LoadWorld(url, null); } catch { } + + Assert.AreEqual(url, WorldAPI.GetWorldURL()); + } + + [Test] + public void JSAPI_LoadWorld_OneArg_Compiles() + { + // Verifies the World.LoadWorld(url) overload remains callable. + try { WorldAPI.LoadWorld("https://example.test/one-arg.veml"); } catch { } + } + + [Test] + public void JSAPI_LoadWorld_TwoArg_Compiles() + { + // Verifies the new World.LoadWorld(url, requireScript) overload is callable. + try + { + WorldAPI.LoadWorld("https://example.test/two-arg.veml", "var __sentinel = 1;"); + } + catch { } + } + + [Test] + public void TestLoadWorld_DoesNotMutateCurrentURL() + { + // Pre-condition: simulate a previously loaded world. + const string sentinel = "https://example.test/already-loaded.veml"; + SetCurrentURL(runtime, sentinel); + + try + { + runtime.TestLoadWorld("https://example.test/test-target.veml", + (success, errorMessage, title) => { }); + } + catch { } + + // The contract: TestLoadWorld must not overwrite currentURL even if the network + // call later fails or hangs. + Assert.AreEqual(sentinel, WorldAPI.GetWorldURL()); + } + + [Test] + public void TestLoadWorld_RejectsNonVEMLExtensions() + { + bool callbackFired = false; + bool reportedSuccess = true; + string reportedError = null; + + try + { + runtime.TestLoadWorld("https://example.test/world.glb", + (success, errorMessage, title) => + { + callbackFired = true; + reportedSuccess = success; + reportedError = errorMessage; + }); + } + catch { } + + Assert.IsTrue(callbackFired, "Callback should fire synchronously for non-VEML extensions."); + Assert.IsFalse(reportedSuccess); + StringAssert.Contains("VEML", reportedError); + } + + [Test] + public void TestLoadWorld_X3DAlsoRejected() + { + bool callbackFired = false; + bool reportedSuccess = true; + + try + { + runtime.TestLoadWorld("https://example.test/world.x3d", + (success, errorMessage, title) => + { + callbackFired = true; + reportedSuccess = success; + }); + } + catch { } + + Assert.IsTrue(callbackFired); + Assert.IsFalse(reportedSuccess); + } + + [Test] + public void JSAPI_TestLoadWorld_Compiles() + { + // Verifies the JS-facing World.TestLoadWorld(url, callbackName) signature is callable. + try + { + WorldAPI.TestLoadWorld("https://example.test/world.veml", "onTestComplete"); + } + catch { } + } + + [Test] + public void JSAPI_TestLoadWorld_NullCallback_DoesNotThrow() + { + // A null/empty callback name should be tolerated (just no JS invocation). + Assert.DoesNotThrow(() => + { + try + { + WorldAPI.TestLoadWorld("https://example.test/world.glb", null); + } + catch { } + }); + } +} diff --git a/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs.meta b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs.meta new file mode 100644 index 00000000..0c6c48e8 --- /dev/null +++ b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: 3ebf932f86bb43babcd6706210a81af1 diff --git a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs index 8af1c975..b10b555c 100644 --- a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs +++ b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs @@ -121,7 +121,7 @@ public void GetWorldName(string resourceURI, Action onComplete) /// URI of the world file. /// Action to invoke upon completion of world loading. /// Provides a success/fail indication. - public void LoadVEMLDocumentIntoWorld(string resourceURI, Action onComplete) + public void LoadVEMLDocumentIntoWorld(string resourceURI, Action onComplete, string requireScript = null) { Action onDownloaded = () => { @@ -135,12 +135,287 @@ public void LoadVEMLDocumentIntoWorld(string resourceURI, Action onComplet } else { - StartCoroutine(ApplyVEMLDocument(veml, Path.GetDirectoryName(resourceURI), onComplete)); + StartCoroutine(ApplyVEMLDocument(veml, Path.GetDirectoryName(resourceURI), onComplete, requireScript)); } }; DownloadVEML(resourceURI, onDownloaded); } + /// + /// Dry-run validation of a VEML document. Downloads and parses the VEML, downloads (but does + /// not execute) referenced scripts, and HEAD-requests referenced asset URIs (meshes, textures, + /// audio, images, html links, panorama, sky textures, terrain-layer textures). Does not touch + /// the active world, runtime state, currentURL, or the JINT engine. + /// + /// URI of the VEML document to test. + /// Invoked with (success, errorMessage, title). errorMessage is + /// null on success; on failure it is a newline-separated list of issues. title is the parsed + /// metadata.title if the document parsed, otherwise null. + public void TestVEMLDocument(string resourceURI, Action onTestComplete) + { + Action onDownloaded = () => + { + Schema.V3_0.veml veml; + try + { + veml = LoadVEML(Path.Combine(runtime.fileHandler.fileDirectory, + FileHandler.ToFileURI(resourceURI))); + } + catch (Exception ex) + { + onTestComplete.Invoke(false, + "Failed to parse VEML at " + resourceURI + ": " + ex.Message, null); + return; + } + + if (veml == null) + { + onTestComplete.Invoke(false, + "Not a valid VEML document: " + resourceURI, null); + return; + } + + StartCoroutine(TestApplyVEMLDocument(veml, + Path.GetDirectoryName(resourceURI), onTestComplete)); + }; + DownloadVEML(resourceURI, onDownloaded); + } + + /// + /// Dry-run coroutine for a parsed VEML document. See TestVEMLDocument for the contract. + /// + private IEnumerator TestApplyVEMLDocument(Schema.V3_0.veml veml, string baseURI, + Action onTestComplete) + { + string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); + List errors = new List(); + string title = null; + + // Metadata structural validation. + if (veml.metadata == null) + { + errors.Add("Missing required field: metadata."); + } + else + { + title = veml.metadata.title; + if (string.IsNullOrEmpty(title)) + { + errors.Add("Missing required field: metadata.title."); + } + } + + // Download all script URIs (we never execute them). Inline scripts are skipped — nothing + // to check at the URI level. + Dictionary scriptResults = new Dictionary(); + if (veml.metadata != null && veml.metadata.script != null) + { + foreach (string script in veml.metadata.script) + { + if (string.IsNullOrEmpty(script) || !script.EndsWith(".js")) + { + continue; + } + string fullURI = VEMLUtilities.FullyQualifyURI(script, formattedBaseURI); + if (scriptResults.ContainsKey(fullURI)) + { + continue; + } + scriptResults[fullURI] = null; + string capturedURI = fullURI; + LoadScriptResourceAsString(fullURI, new Action((content) => + { + scriptResults[capturedURI] = content ?? ""; + })); + } + } + + // Walk entity tree, collecting asset URIs to HEAD-check. + List assetURIs = new List(); + if (veml.environment != null && veml.environment.entity != null) + { + foreach (Schema.V3_0.entity ent in veml.environment.entity) + { + CollectEntityAssetURIs(ent, formattedBaseURI, assetURIs); + } + } + + // Background-level asset URIs (panorama image, lite procedural sky textures). + if (veml.environment != null && veml.environment.background != null) + { + Schema.V3_0.background bg = veml.environment.background; + if (bg.ItemElementName == Schema.V3_0.ItemChoiceType.panorama + && bg.Item is string panoramaURI && !string.IsNullOrEmpty(panoramaURI)) + { + assetURIs.Add(VEMLUtilities.FullyQualifyURI(panoramaURI, formattedBaseURI)); + } + else if (bg.ItemElementName == Schema.V3_0.ItemChoiceType.liteproceduralsky + && bg.Item is Schema.V3_0.liteproceduralsky lps) + { + if (!string.IsNullOrEmpty(lps.startextureuri)) + { + assetURIs.Add(VEMLUtilities.FullyQualifyURI(lps.startextureuri, formattedBaseURI)); + } + if (!string.IsNullOrEmpty(lps.cloudstextureuri)) + { + assetURIs.Add(VEMLUtilities.FullyQualifyURI(lps.cloudstextureuri, formattedBaseURI)); + } + } + } + + // HEAD-request each unique asset URI. 0 = pending, -1 = HEAD not supported in this build. + Dictionary headResults = new Dictionary(); + foreach (string uri in assetURIs) + { + if (string.IsNullOrEmpty(uri) || headResults.ContainsKey(uri)) + { + continue; + } + headResults[uri] = 0; +#if USE_WEBINTERFACE + string capturedURI = uri; + Action, byte[]> onHeadResponse = + new Action, byte[]>((code, headers, data) => + { + headResults[capturedURI] = code == 0 ? -2 : code; + }); + HTTPRequest headReq = new HTTPRequest(uri, HTTPRequest.HTTPMethod.Head, onHeadResponse); + headReq.Send(); +#else + headResults[uri] = -1; +#endif + } + + // Wait for all scripts and HEAD-requests to settle, or for timeout. + float elapsed = 0f; + while (elapsed < timeout) + { + bool allDone = true; + foreach (KeyValuePair kv in scriptResults) + { + if (kv.Value == null) { allDone = false; break; } + } + if (allDone) + { + foreach (KeyValuePair kv in headResults) + { + if (kv.Value == 0) { allDone = false; break; } + } + } + if (allDone) break; + yield return new WaitForSeconds(0.25f); + elapsed += 0.25f; + } + + // Aggregate failures. + foreach (KeyValuePair kv in scriptResults) + { + if (kv.Value == null) + { + errors.Add("Script load timeout: " + kv.Key); + } + else if (kv.Value.Length == 0) + { + errors.Add("Script empty or unreachable: " + kv.Key); + } + } + foreach (KeyValuePair kv in headResults) + { + if (kv.Value == 0) + { + errors.Add("Asset URI request timeout: " + kv.Key); + } + else if (kv.Value == -1) + { + // Build lacks USE_WEBINTERFACE; we couldn't verify. Don't flag as error. + } + else if (kv.Value == -2) + { + errors.Add("Asset URI request failed (network/no response): " + kv.Key); + } + else if (kv.Value > 399) + { + errors.Add("Asset URI returned " + kv.Value + ": " + kv.Key); + } + } + + bool success = errors.Count == 0; + string errorMessage = success ? null : string.Join("\n", errors); + onTestComplete.Invoke(success, errorMessage, title); + } + + /// + /// Recursively collect asset URIs referenced by an entity and its descendants. NOTE: when new + /// entity types are added to the V3.0 schema with new URI fields, extend this method to pick + /// them up — otherwise TestVEMLDocument's asset checks will silently miss them. + /// + private void CollectEntityAssetURIs(Schema.V3_0.entity ent, string baseURI, List output) + { + if (ent == null) return; + + // Mesh-based entity types each carry a meshresource[] array. + string[] meshResources = null; + if (ent is Schema.V3_0.mesh m) meshResources = m.meshresource; + else if (ent is Schema.V3_0.airplane a) meshResources = a.meshresource; + else if (ent is Schema.V3_0.automobile au) meshResources = au.meshresource; + else if (ent is Schema.V3_0.water w) meshResources = w.meshresource; + else if (ent is Schema.V3_0.character c) meshResources = c.meshresource; + + if (meshResources != null) + { + foreach (string mr in meshResources) + { + if (!string.IsNullOrEmpty(mr)) + { + output.Add(VEMLUtilities.FullyQualifyURI(mr, baseURI)); + } + } + } + + if (ent is Schema.V3_0.image img && !string.IsNullOrEmpty(img.imagefile)) + { + output.Add(VEMLUtilities.FullyQualifyURI(img.imagefile, baseURI)); + } + if (ent is Schema.V3_0.audio aud && !string.IsNullOrEmpty(aud.audiofile)) + { + output.Add(VEMLUtilities.FullyQualifyURI(aud.audiofile, baseURI)); + } + if (ent is Schema.V3_0.html h && !string.IsNullOrEmpty(h.url)) + { + output.Add(VEMLUtilities.FullyQualifyURI(h.url, baseURI)); + } + + // Terrain-layer textures. + if (ent is Schema.V3_0.terrain t && t.layer != null) + { + foreach (Schema.V3_0.terrainlayer layer in t.layer) + { + if (layer == null) continue; + if (!string.IsNullOrEmpty(layer.diffusetexture)) + { + output.Add(VEMLUtilities.FullyQualifyURI(layer.diffusetexture, baseURI)); + } + if (!string.IsNullOrEmpty(layer.normaltexture)) + { + output.Add(VEMLUtilities.FullyQualifyURI(layer.normaltexture, baseURI)); + } + if (!string.IsNullOrEmpty(layer.masktexture)) + { + output.Add(VEMLUtilities.FullyQualifyURI(layer.masktexture, baseURI)); + } + } + } + + // Recurse into children (the schema names this field "entity1" because "entity" is taken). + if (ent.entity1 != null) + { + foreach (Schema.V3_0.entity child in ent.entity1) + { + CollectEntityAssetURIs(child, baseURI, output); + } + } + } + /// /// Download a VEML document. /// @@ -607,7 +882,7 @@ private void FinishVEMLDownload(string uri, int responseCode, byte[] rawData) /// Base URI of the VEML document. /// Action to invoke upon completion of world loading. /// Provides a success/fail indication. - private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string baseURI, Action onComplete) + private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string baseURI, Action onComplete, string requireScript = null) { string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); @@ -620,7 +895,7 @@ private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string base scriptsDoneProcessing = true; }); - if (ProcessMetadata(vemlDocument, baseURI, onScriptsProcessed) == false) + if (ProcessMetadata(vemlDocument, baseURI, onScriptsProcessed, requireScript) == false) { Logging.LogWarning("[VEMLHandler->ApplyVEMLDocument] Error processing metadata."); onComplete.Invoke(false); @@ -697,7 +972,7 @@ private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string base /// Action to invoke when scripts are processed. Provides an array of /// strings containing the script contents. /// Whether or not the operation succeeded. - private bool ProcessMetadata(Schema.V3_0.veml vemlDocument, string baseURI, Action onScriptsProcessed) + private bool ProcessMetadata(Schema.V3_0.veml vemlDocument, string baseURI, Action onScriptsProcessed, string requireScript = null) { string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); @@ -725,7 +1000,7 @@ private bool ProcessMetadata(Schema.V3_0.veml vemlDocument, string baseURI, Acti return false; } - StartCoroutine(ProcessScripts(vemlDocument, baseURI, onScriptsProcessed)); + StartCoroutine(ProcessScripts(vemlDocument, baseURI, onScriptsProcessed, requireScript)); if (ProcessInputEvents(vemlDocument, baseURI) == false) { @@ -821,12 +1096,33 @@ private bool ProcessEnvironment(Schema.V3_0.veml vemlDocument, string baseURI) /// Base URI of the VEML document. /// Action to invoke when scripts are processed. Provides an array of /// strings containing the script contents. - private IEnumerator ProcessScripts(Schema.V3_0.veml vemlDocument, string baseURI, Action onProcessed) + private IEnumerator ProcessScripts(Schema.V3_0.veml vemlDocument, string baseURI, Action onProcessed, string requireScript = null) { string formattedBaseURI = VEMLUtilities.FormatURI(baseURI); Dictionary scriptsToRun = new Dictionary(); + // Set up requireScript (loaded ahead of VEML's own scripts so it runs first). + string resolvedRequireScript = null; + bool requireScriptLoaded = string.IsNullOrEmpty(requireScript); + if (!string.IsNullOrEmpty(requireScript)) + { + if (requireScript.EndsWith(".js")) + { + LoadScriptResourceAsString(VEMLUtilities.FullyQualifyURI(requireScript, formattedBaseURI), + new Action((scr) => + { + resolvedRequireScript = scr; + requireScriptLoaded = true; + })); + } + else + { + resolvedRequireScript = requireScript; + requireScriptLoaded = true; + } + } + // Set up scripts. if (vemlDocument.metadata.script != null) { @@ -857,20 +1153,30 @@ private IEnumerator ProcessScripts(Schema.V3_0.veml vemlDocument, string baseURI bool allLoaded = true; do { - allLoaded = true; - foreach (string script in scriptsToRun.Values) + allLoaded = requireScriptLoaded; + if (allLoaded) { - if (script == null) + foreach (string script in scriptsToRun.Values) { - allLoaded = false; - yield return new WaitForSeconds(0.25f); - elapsedTime += 0.25f; - break; + if (script == null) + { + allLoaded = false; + break; + } } } + if (!allLoaded) + { + yield return new WaitForSeconds(0.25f); + elapsedTime += 0.25f; + } } while (allLoaded == false && elapsedTime < timeout); List scripts = new List(); + if (!string.IsNullOrEmpty(resolvedRequireScript)) + { + scripts.Add(resolvedRequireScript); + } foreach (string script in scriptsToRun.Values) { scripts.Add(script); diff --git a/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs b/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs index 9ebed62d..ce8cd8f1 100644 --- a/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs +++ b/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs @@ -671,7 +671,11 @@ public void LoadURL(string url, Action onLoaded = null) /// /// URL containing the world to load. /// Action to perform on load. Provides string containing loaded world name. - public void LoadWorld(string url, Action onLoaded) + /// Optional. Either inline JavaScript logic or a URI ending in ".js" + /// pointing to a script resource. The script is prepended to the world's script list and runs in + /// the same JINT engine as the world's own scripts. Only honored for VEML worlds; a warning is + /// logged and the script is ignored for x3d and glTF worlds. + public void LoadWorld(string url, Action onLoaded, string requireScript = null) { if (straightFour == null) { @@ -679,6 +683,8 @@ public void LoadWorld(string url, Action onLoaded) return; } + currentURL = url; + if (StraightFour.StraightFour.ActiveWorld != null) { UnloadWorld(); @@ -692,6 +698,14 @@ public void LoadWorld(string url, Action onLoaded) queryParams = url.Substring(url.IndexOf('?') + 1); } + if (!string.IsNullOrEmpty(requireScript) + && (baseURL.EndsWith(".x3d") || baseURL.EndsWith(".x3db") || baseURL.EndsWith(".x3dv") + || baseURL.EndsWith(".glb") || baseURL.EndsWith(".gltf"))) + { + Logging.LogWarning("[WebVerseRuntime->LoadWorld] requireScript is only supported for " + + "VEML worlds; ignoring for " + baseURL); + } + if (baseURL.EndsWith(".x3d") || baseURL.EndsWith(".x3db") || baseURL.EndsWith(".x3dv")) { x3dHandler.GetX3DTitle(baseURL, (title) => @@ -783,13 +797,48 @@ public void LoadWorld(string url, Action onLoaded) enableDefault = Logging.GetConfiguration().enableDefault }; StraightFour.StraightFour.LoadWorld(title, queryParams); - vemlHandler.LoadVEMLDocumentIntoWorld(baseURL, onLoadComplete); + vemlHandler.LoadVEMLDocumentIntoWorld(baseURL, onLoadComplete, requireScript); }; vemlHandler.GetWorldName(baseURL, onFound); } } + /// + /// Dry-run validation of a world's VEML document without switching to it. Downloads and + /// parses the VEML, downloads (but does not execute) referenced scripts, and HEAD-requests + /// referenced asset URIs. Does not unload the active world, mutate currentURL, change runtime + /// state, or touch the JINT engine. + /// + /// URL of the VEML world to test. + /// Invoked with (success, errorMessage, title). errorMessage is + /// null on success; on failure it is a newline-separated list of issues. title is the parsed + /// metadata.title if the document parsed, otherwise null. + public void TestLoadWorld(string url, Action onTestComplete) + { + if (vemlHandler == null) + { + onTestComplete.Invoke(false, "VEML handler not initialized.", null); + return; + } + + string baseURL = url; + if (url.Contains("?")) + { + baseURL = url.Substring(0, url.IndexOf('?')); + } + + if (baseURL.EndsWith(".x3d") || baseURL.EndsWith(".x3db") || baseURL.EndsWith(".x3dv") + || baseURL.EndsWith(".glb") || baseURL.EndsWith(".gltf")) + { + onTestComplete.Invoke(false, + "TestLoadWorld currently supports VEML worlds only.", null); + return; + } + + vemlHandler.TestVEMLDocument(baseURL, onTestComplete); + } + /// /// Unload a world. /// @@ -876,6 +925,7 @@ public void UnloadWorld() /// Action to perform on load. Provides string indicating web page. public void LoadWebPage(string url, Action onLoaded) { + currentURL = url; state = RuntimeState.WebPage; webverseWebView.Show(); webverseWebView.LoadURL(url); From 51361864e342ba2df838988c61ca67c8193e1a89 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 19:13:09 -0400 Subject: [PATCH 03/22] Resolving error --- Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs index b10b555c..18cea3a0 100644 --- a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs +++ b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs @@ -358,7 +358,6 @@ private void CollectEntityAssetURIs(Schema.V3_0.entity ent, string baseURI, List if (ent is Schema.V3_0.mesh m) meshResources = m.meshresource; else if (ent is Schema.V3_0.airplane a) meshResources = a.meshresource; else if (ent is Schema.V3_0.automobile au) meshResources = au.meshresource; - else if (ent is Schema.V3_0.water w) meshResources = w.meshresource; else if (ent is Schema.V3_0.character c) meshResources = c.meshresource; if (meshResources != null) From 4dc36e062faa19859d36e92cf76727d78e0df213 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 19:27:50 -0400 Subject: [PATCH 04/22] Resolving error --- .../Handlers/JavascriptHandler/Tests/WorldAPITests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs index c0efa868..bd7bc5fe 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/Tests/WorldAPITests.cs @@ -7,6 +7,7 @@ using FiveSQD.WebVerse.LocalStorage; using System.IO; using System.Reflection; +using System.Text.RegularExpressions; using WorldAPI = FiveSQD.WebVerse.Handlers.Javascript.APIs.Utilities.World; /// @@ -107,6 +108,10 @@ public void LoadWebPage_SetsCurrentURL() { const string url = "https://example.test/page.html"; + // The placeholder WebView in this test context isn't fully set up, so LoadURL on it + // intentionally errors. We only care that currentURL was assigned at the top of LoadWebPage + // before the WebView call ran. + LogAssert.Expect(LogType.Error, "[WebVerseWebView->LoadURL] WebVerse WebView not set up."); try { runtime.LoadWebPage(url, null); } catch { } Assert.AreEqual(url, WorldAPI.GetWorldURL()); From 69671a5f5d5ddcbdcc05b8cc1b7d2f1e396af513 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 20:41:28 -0400 Subject: [PATCH 05/22] Adding grounding tests --- .../CharacterEntityGroundingTests.cs | 336 ++++++++++++++++++ .../CharacterEntityGroundingTests.cs.meta | 2 + 2 files changed, 338 insertions(+) create mode 100644 Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs create mode 100644 Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs.meta diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs new file mode 100644 index 00000000..bafaf120 --- /dev/null +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -0,0 +1,336 @@ +// Copyright (c) 2019-2026 Five Squared Interactive. All rights reserved. + +using System; +using System.Collections; +using System.Collections.Generic; +using NUnit.Framework; +using UnityEngine; +using UnityEngine.TestTools; +using FiveSQD.StraightFour; +using FiveSQD.StraightFour.Entity; +using UnityEditor; + +/// +/// PlayMode tests that exercise CharacterEntity grounding and gravity behavior to validate four +/// hypotheses about a VR floating/oscillating bug: +/// (1) IsOnSurface raycast (0.25 units) is too short to reliably detect contact. +/// (2) CharacterController.isGrounded is never consulted. +/// (3) Pure-vertical Move() calls behave differently from mixed-axis ones. +/// (4) currentVelocity is treated inconsistently as both velocity (m/s) and per-tick displacement. +/// +/// Each test is self-contained: it builds a floor (or no floor for free-fall) and a CharacterEntity, +/// then asserts on grounded/position state after a known physics interval. The tests are tuned so +/// they fail on the current code path and pass with the proposed fixes (longer raycast, isGrounded +/// fallback, units fix, etc.) — so they double as a regression gate. +/// +public class CharacterEntityGroundingTests +{ + private GameObject weGO; + private StraightFour straightFour; + private GameObject cameraGO; + private GameObject floorGO; + + private const float CHARACTER_HEIGHT = 2.0f; + + [OneTimeSetUp] + public void OneTimeSetUp() + { + LogAssert.ignoreFailingMessages = true; + } + + [SetUp] + public void SetUp() + { + LogAssert.ignoreFailingMessages = true; + } + + [TearDown] + public void TearDown() + { + try + { + if (StraightFour.ActiveWorld != null) + { + StraightFour.UnloadWorld(); + } + } + catch (Exception) + { + // CameraManager may already be destroyed. + } + if (floorGO != null) { UnityEngine.Object.DestroyImmediate(floorGO); floorGO = null; } + if (cameraGO != null) { UnityEngine.Object.DestroyImmediate(cameraGO); cameraGO = null; } + } + + /// + /// Builds a StraightFour world, a flat floor at y=0 (top surface), and spawns a CharacterEntity + /// at the provided spawnPosition. fixHeight is disabled so the rescue-warp doesn't mask grounding + /// bugs. The returned CharacterEntity is fully initialized. + /// + private IEnumerator BuildSceneAndCharacter(Vector3 spawnPosition, bool buildFloor, + Action callback) + { + LogAssert.ignoreFailingMessages = true; + + cameraGO = new GameObject("TestCamera"); + Camera camera = cameraGO.AddComponent(); + camera.transform.position = new Vector3(0, 0, -10); + cameraGO.tag = "MainCamera"; + + if (buildFloor) + { + // A wide flat box collider with its top surface at y=0. + floorGO = GameObject.CreatePrimitive(PrimitiveType.Cube); + floorGO.name = "TestFloor"; + floorGO.transform.position = new Vector3(0, -0.5f, 0); + floorGO.transform.localScale = new Vector3(20f, 1f, 20f); + } + + weGO = new GameObject("WE"); + straightFour = weGO.AddComponent(); + straightFour.characterControllerPrefab = AssetDatabase.LoadAssetAtPath( + "Assets/Runtime/StraightFour/Entity/Character/Prefabs/UserAvatar.prefab"); + straightFour.skyMaterial = AssetDatabase.LoadAssetAtPath( + "Assets/Runtime/StraightFour/Environment/Materials/Skybox.mat"); + yield return null; + StraightFour.LoadWorld("grounding-test"); + + bool loaded = false; + Guid charId = StraightFour.ActiveWorld.entityManager.LoadCharacterEntity( + null, null, Vector3.zero, Quaternion.identity, new Vector3(0, 2, 0), + spawnPosition, Quaternion.identity, Vector3.one, + tag: "test-char", + onLoaded: () => { loaded = true; }); + + float elapsed = 0f; + while (!loaded && elapsed < 5f) + { + yield return new WaitForSeconds(0.1f); + elapsed += 0.1f; + } + + CharacterEntity ce = StraightFour.ActiveWorld.entityManager.FindEntity(charId) as CharacterEntity; + Assert.IsNotNull(ce, "CharacterEntity failed to load within 5s."); + ce.fixHeight = false; // Disable rescue warp so we observe raw grounding behavior. + callback(ce); + } + + /// + /// Helper: project the character's foot Y from transform.position and CharacterController height. + /// + private static float FootY(CharacterEntity ce) + { + CharacterController cc = ce.GetComponent(); + return ce.transform.position.y - cc.height / 2f + cc.center.y; + } + + // --------------------------------------------------------------------------------------------- + // Suspect 1 + 4: After dropping from 3m, the character should settle with foot on the floor. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_Drops_FromThreeMeters_SettlesOnFloor() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Let physics settle. Free-fall from 3m takes ~0.78s; allow generous margin. + yield return new WaitForSeconds(3f); + + float foot = FootY(ce); + Assert.That(foot, Is.EqualTo(0f).Within(0.10f), + $"Character did not settle on floor. Foot y = {foot:F3}, expected ~0. " + + $"Transform y = {ce.transform.position.y:F3}. Suspect 1 (short raycast), " + + $"Suspect 4 (units), or a Rigidbody fighting CharacterController."); + } + + // --------------------------------------------------------------------------------------------- + // Suspect 1: After landing, the character should hold position. Bobbing > a few mm is the bug. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_AfterLanding_DoesNotOscillate() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Wait for initial settle. + yield return new WaitForSeconds(3f); + + // Sample foot y over ~1s of physics. With CharacterEntity throttled to ~25 Hz updates, + // we sample 40 frames to cover one second comfortably. + List samples = new List(); + for (int i = 0; i < 50; i++) + { + samples.Add(FootY(ce)); + yield return new WaitForFixedUpdate(); + } + + float min = float.MaxValue, max = float.MinValue; + foreach (float y in samples) { if (y < min) min = y; if (y > max) max = y; } + float swing = max - min; + + Assert.Less(swing, 0.02f, + $"Character oscillated by {swing:F4} m after settling (min={min:F4} max={max:F4}). " + + "Suspect 1 (short raycast) or Rigidbody-vs-CharacterController fight."); + } + + // --------------------------------------------------------------------------------------------- + // Suspect 1 direct: IsOnSurface() must return true when the foot is touching the floor. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_IsOnSurface_TrueAtSmallGapAboveFloor() + { + CharacterEntity ce = null; + // Place character so foot is 0.05 m above floor — well within ANY reasonable grounding margin. + // Transform y must be height/2 + 0.05 = 1.05. + yield return BuildSceneAndCharacter(new Vector3(0, CHARACTER_HEIGHT / 2f + 0.05f, 0), + buildFloor: true, c => ce = c); + + // One physics tick so the controller registers position. + yield return new WaitForFixedUpdate(); + yield return new WaitForFixedUpdate(); + + Assert.IsTrue(ce.IsOnSurface(), + "IsOnSurface returned false when foot was only 0.05 m above floor. " + + "Suspect 1 (raycast too short) — but 0.05 < 0.25 so this should always pass; " + + "if it fails, the foot computation in IsOnSurface is wrong (transform.position " + + "may not be where we think it is)."); + } + + /// + /// IsOnSurface SHOULD return false when the gap is well beyond the raycast distance — this + /// documents current behavior and pairs with the previous test as a boundary regression. + /// + [UnityTest] + public IEnumerator CharacterEntity_IsOnSurface_FalseAtLargeGapAboveFloor() + { + CharacterEntity ce = null; + // Foot 1.5 m above floor. + yield return BuildSceneAndCharacter(new Vector3(0, CHARACTER_HEIGHT / 2f + 1.5f, 0), + buildFloor: true, c => ce = c); + + // Disable gravity so the character can't fall during the check. + Rigidbody rb = ce.GetComponent(); + if (rb != null) rb.useGravity = false; + + yield return new WaitForFixedUpdate(); + + Assert.IsFalse(ce.IsOnSurface(), + "IsOnSurface returned true at a 1.5 m gap. Either the raycast was extended (good!) " + + "or it's hitting something other than the floor (the character's own collider?)."); + } + + // --------------------------------------------------------------------------------------------- + // Suspect 4: Free-fall integration should approximate s = 0.5 * g * t^2. The unit-confusion + // bug (gravity adds to velocity per second, but velocity is passed as per-tick displacement) + // produces an order-of-magnitude faster fall. + // + // Analytic free-fall over t=0.3s: dy = 0.5 * 9.81 * 0.09 ≈ 0.44 m. + // Buggy code: dy ≈ -0.196 * (1+2+...+N) where N ≈ t/0.04 ≈ 7 ticks → dy ≈ 5.5 m. + // + // We assert |displacement| < 1.0 m. Real physics: well under. Buggy: way over. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_FreeFall_RoughlyMatchesGravity() + { + CharacterEntity ce = null; + // No floor. Spawn high so the character has room to fall. + yield return BuildSceneAndCharacter(new Vector3(0, 100f, 0), buildFloor: false, c => ce = c); + + // Let one tick pass to ensure the character is initialized in a normal state. + yield return new WaitForFixedUpdate(); + float startY = ce.transform.position.y; + + yield return new WaitForSeconds(0.3f); + float endY = ce.transform.position.y; + float dropped = startY - endY; + + Assert.Less(dropped, 1.0f, + $"Character fell {dropped:F3} m in 0.3 s. Analytic free-fall is ~0.44 m; >1 m strongly " + + "suggests Suspect 4: currentVelocity is being treated as per-tick displacement when " + + "gravity is added as v += a·dt (units mismatch)."); + Assert.Greater(dropped, 0.05f, + $"Character barely moved ({dropped:F3} m in 0.3 s). Gravity may be disabled or " + + "the rigidbody is kinematic and the custom gravity path didn't run."); + } + + // --------------------------------------------------------------------------------------------- + // Suspect 2 + 3: CharacterController.isGrounded should be consulted in grounding decisions, and + // pure-vertical Move() may behave worse than mixed-axis. This test compares behavior of an + // idle character vs one that's been "tickled" with a tiny horizontal perturbation each frame. + // If the perturbation produces visibly different (less bouncy) settling, Suspect 3 is real. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_PureVerticalSettle_NoWorseThan_MixedAxisSettle() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Phase A: idle settle for 3s, measure final y. + yield return new WaitForSeconds(3f); + float idleFootY = FootY(ce); + + // Reset position to drop again, this time with tiny horizontal Move()s during fall. + ce.SetPosition(new Vector3(0, 3f, 0), local: true, synchronize: false); + yield return new WaitForFixedUpdate(); + + for (int i = 0; i < 120; i++) + { + // Tiny non-zero horizontal nudge — invokes the CharacterController's mixed-axis solver. + ce.Move(new Vector3(0.0001f, 0, 0), synchronize: false); + yield return new WaitForFixedUpdate(); + } + float nudgedFootY = FootY(ce); + + // Both should settle near the floor (y=0). If the nudged version is closer to the floor than + // the idle version by more than 5 cm, Suspect 3 is confirmed. + float diff = Mathf.Abs(idleFootY - nudgedFootY); + Assert.Less(diff, 0.05f, + $"Idle-settle foot y = {idleFootY:F4}, nudged-settle foot y = {nudgedFootY:F4}, " + + $"diff = {diff:F4} m. A large diff means the CharacterController.Move() solver " + + "behaves differently for pure-vertical vs mixed-axis input (Suspect 3) — fix C from " + + "the diagnosis would address this."); + } + + // --------------------------------------------------------------------------------------------- + // Diagnostic dump: not a pass/fail test, but exercises the path and logs the key signals every + // tick so a human can read the console after a run. Useful first-look during investigation. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + [Explicit("Diagnostic only — prints per-tick grounding state. Run manually when investigating.")] + public IEnumerator CharacterEntity_LogsGroundingStateEachTick() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + CharacterController cc = ce.GetComponent(); + Rigidbody rb = ce.GetComponent(); + + for (int i = 0; i < 80; i++) + { + Vector3 foot = ce.transform.position - new Vector3(0, cc.height / 2f, 0); + bool onSurface = ce.IsOnSurface(); + bool ccGrounded = cc.isGrounded; + + float gap = -1f; + if (Physics.Raycast(foot, Vector3.down, out RaycastHit hit, 10f, + ~0, QueryTriggerInteraction.Ignore)) + { + gap = hit.distance; + } + + Debug.Log($"[t={Time.time:F3}] footY={foot.y:F4} gap={gap:F4} " + + $"IsOnSurface={onSurface} cc.isGrounded={ccGrounded} " + + $"rb.kinematic={rb?.isKinematic} rb.useGravity={rb?.useGravity}"); + + yield return new WaitForFixedUpdate(); + } + + Assert.Pass("Diagnostic log complete — inspect Console output."); + } +} diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs.meta b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs.meta new file mode 100644 index 00000000..a032713f --- /dev/null +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs.meta @@ -0,0 +1,2 @@ +fileFormatVersion: 2 +guid: a56f70a55ee044ed9648d2baff9ae115 From ba61d41b0dbeace6624bf964aa079fd95bef171c Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 20:47:53 -0400 Subject: [PATCH 06/22] Resolving error --- .../EntityTests/CharacterEntityGroundingTests.cs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs index bafaf120..ded56cf4 100644 --- a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -111,7 +111,18 @@ private IEnumerator BuildSceneAndCharacter(Vector3 spawnPosition, bool buildFloo CharacterEntity ce = StraightFour.ActiveWorld.entityManager.FindEntity(charId) as CharacterEntity; Assert.IsNotNull(ce, "CharacterEntity failed to load within 5s."); + + // Initialize() leaves the character in Hidden state (gameObject.SetActive(false)) so + // FixedUpdate doesn't run. Switch to Physical to match the live VR scenario. + ce.SetInteractionState(BaseEntity.InteractionState.Physical); ce.fixHeight = false; // Disable rescue warp so we observe raw grounding behavior. + + // Re-apply spawn position after going Physical, in case prior state changed transform. + ce.SetPosition(spawnPosition, local: true, synchronize: false); + + // Give one fixed tick for the controller to register. + yield return new WaitForFixedUpdate(); + callback(ce); } From e19d8ae71013e71b2f355db07d9bf9a3446218dd Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 21:01:05 -0400 Subject: [PATCH 07/22] Resolving error --- .../Character/Scripts/CharacterEntity.cs | 40 +++++++++++++++---- 1 file changed, 32 insertions(+), 8 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index d85ca6c5..9ef3d8cd 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -131,10 +131,18 @@ public override string entityTag private GameObject highlightCube; /// - /// The current applied velocity. + /// The current horizontal displacement applied this tick (x and z). Resets after each + /// FixedUpdate. y is unused; vertical motion lives in verticalVelocity. /// private Vector3 currentVelocity = Vector3.zero; + /// + /// Vertical velocity in meters/second. Integrated by gravity each FixedUpdate; displacement + /// is verticalVelocity * Time.deltaTime applied via CharacterController.Move. Jump() and the + /// y component of Move(amount) treat this as velocity (m/s), not per-frame displacement. + /// + private float verticalVelocity = 0f; + /// /// Get the character GameObject. /// @@ -313,7 +321,8 @@ public bool Move(Vector3 amount, bool synchronize = true) return false; } - currentVelocity = new Vector3(currentVelocity.x + amount.x, currentVelocity.y + amount.y, currentVelocity.z + amount.z); + currentVelocity.x += amount.x; + currentVelocity.z += amount.z; characterController.Move(amount); if (synchronizer != null && synchronize == true) @@ -347,7 +356,7 @@ public bool Jump(float amount, bool discardIfFalling = true, bool synchronize = if (IsOnSurface() || !discardIfFalling) { - currentVelocity.y += amount; + verticalVelocity += amount; } if (synchronizer != null && synchronize == true) @@ -376,7 +385,16 @@ public bool IsOnSurface() return false; } - return Physics.Raycast(transform.position - new Vector3(0, characterController.height / 2, 0), Vector3.down, 0.25f); + // CharacterController.isGrounded is the canonical signal; it's updated each Move() with + // the controller's internal slope/penetration logic and accounts for skinWidth. + if (characterController.isGrounded) return true; + + // Fallback raycast for the pre-first-Move case and as a small-gap safety net. + // Distance is skinWidth + a small margin so we catch the "just barely above floor" + // state without falsely reporting grounded at large gaps. + float rayDistance = characterController.skinWidth + 0.1f; + return Physics.Raycast(transform.position - new Vector3(0, characterController.height / 2f, 0), + Vector3.down, rayDistance); } public bool IsAboveGround() @@ -1049,16 +1067,22 @@ void FixedUpdate() return; } - if (IsOnSurface() && currentVelocity.y < 0) + // Reset vertical velocity when grounded and falling — prevents gravity from compounding + // while on a surface, and zeroes any tiny residual downward velocity from prior ticks. + if (IsOnSurface() && verticalVelocity < 0) { - currentVelocity.y = 0f; + verticalVelocity = 0f; } if (rigidBody.useGravity) { - currentVelocity.y += -9.81f * Time.deltaTime; // TODO: Magic number, tie into larger gravity system. + verticalVelocity += -9.81f * Time.deltaTime; // TODO: tie into larger gravity system. } - characterController.Move(currentVelocity); + + // currentVelocity.x/z are per-frame displacement (legacy semantics callers depend on). + // verticalVelocity is in m/s — multiply by dt to convert to displacement-this-tick. + characterController.Move(new Vector3( + currentVelocity.x, verticalVelocity * Time.deltaTime, currentVelocity.z)); currentVelocity.x = currentVelocity.z = 0; if (fixHeight) From 059fd5c155941514bb4c161493903182a9438f6e Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 21:09:31 -0400 Subject: [PATCH 08/22] Resolving error --- .../CharacterEntityGroundingTests.cs | 45 ++++++++++++------- 1 file changed, 29 insertions(+), 16 deletions(-) diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs index ded56cf4..8312d28a 100644 --- a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -30,7 +30,15 @@ public class CharacterEntityGroundingTests private GameObject cameraGO; private GameObject floorGO; - private const float CHARACTER_HEIGHT = 2.0f; + /// + /// Helper: compute the transform.y needed to place the controller's foot at the given world Y. + /// CharacterController.center stays at Unity default (0,0,0), so foot = transform.y - height/2. + /// + private static float TransformYForFoot(CharacterEntity ce, float footY) + { + CharacterController cc = ce.GetComponent(); + return footY + cc.height / 2f - cc.center.y; + } [OneTimeSetUp] public void OneTimeSetUp() @@ -194,20 +202,23 @@ public IEnumerator CharacterEntity_AfterLanding_DoesNotOscillate() public IEnumerator CharacterEntity_IsOnSurface_TrueAtSmallGapAboveFloor() { CharacterEntity ce = null; - // Place character so foot is 0.05 m above floor — well within ANY reasonable grounding margin. - // Transform y must be height/2 + 0.05 = 1.05. - yield return BuildSceneAndCharacter(new Vector3(0, CHARACTER_HEIGHT / 2f + 0.05f, 0), - buildFloor: true, c => ce = c); + // Spawn somewhere generic; we'll reposition once we know the actual controller height. + yield return BuildSceneAndCharacter(new Vector3(0, 5f, 0), buildFloor: true, c => ce = c); + + // Disable gravity so the position holds while we measure. + Rigidbody rb = ce.GetComponent(); + if (rb != null) rb.useGravity = false; - // One physics tick so the controller registers position. + // Place foot 0.05 m above floor — well within the new skinWidth+0.1 raycast or cc.isGrounded. + ce.SetPosition(new Vector3(0, TransformYForFoot(ce, 0.05f), 0), + local: true, synchronize: false); yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate(); Assert.IsTrue(ce.IsOnSurface(), - "IsOnSurface returned false when foot was only 0.05 m above floor. " + - "Suspect 1 (raycast too short) — but 0.05 < 0.25 so this should always pass; " + - "if it fails, the foot computation in IsOnSurface is wrong (transform.position " + - "may not be where we think it is)."); + $"IsOnSurface returned false when foot was only 0.05 m above floor. " + + $"Foot y={FootY(ce):F4}, transform y={ce.transform.position.y:F4}. " + + "Raycast distance or grounding fallback is still too strict."); } /// @@ -218,19 +229,21 @@ public IEnumerator CharacterEntity_IsOnSurface_TrueAtSmallGapAboveFloor() public IEnumerator CharacterEntity_IsOnSurface_FalseAtLargeGapAboveFloor() { CharacterEntity ce = null; - // Foot 1.5 m above floor. - yield return BuildSceneAndCharacter(new Vector3(0, CHARACTER_HEIGHT / 2f + 1.5f, 0), - buildFloor: true, c => ce = c); + yield return BuildSceneAndCharacter(new Vector3(0, 5f, 0), buildFloor: true, c => ce = c); - // Disable gravity so the character can't fall during the check. + // Disable gravity so the character holds the test position. Rigidbody rb = ce.GetComponent(); if (rb != null) rb.useGravity = false; + // Foot 1.5 m above floor — well above any reasonable grounding margin. + ce.SetPosition(new Vector3(0, TransformYForFoot(ce, 1.5f), 0), + local: true, synchronize: false); yield return new WaitForFixedUpdate(); Assert.IsFalse(ce.IsOnSurface(), - "IsOnSurface returned true at a 1.5 m gap. Either the raycast was extended (good!) " + - "or it's hitting something other than the floor (the character's own collider?)."); + $"IsOnSurface returned true at a 1.5 m gap. Foot y={FootY(ce):F4}. " + + "Either the raycast is too long, or it's hitting something other than the floor " + + "(the character's own collider?)."); } // --------------------------------------------------------------------------------------------- From f52425257785a229742be3dea6c75fcb9bc817d1 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Wed, 20 May 2026 21:17:14 -0400 Subject: [PATCH 09/22] Resolving error --- .../CharacterEntityGroundingTests.cs | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs index 8312d28a..4312632b 100644 --- a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -202,23 +202,36 @@ public IEnumerator CharacterEntity_AfterLanding_DoesNotOscillate() public IEnumerator CharacterEntity_IsOnSurface_TrueAtSmallGapAboveFloor() { CharacterEntity ce = null; - // Spawn somewhere generic; we'll reposition once we know the actual controller height. yield return BuildSceneAndCharacter(new Vector3(0, 5f, 0), buildFloor: true, c => ce = c); // Disable gravity so the position holds while we measure. Rigidbody rb = ce.GetComponent(); if (rb != null) rb.useGravity = false; - // Place foot 0.05 m above floor — well within the new skinWidth+0.1 raycast or cc.isGrounded. - ce.SetPosition(new Vector3(0, TransformYForFoot(ce, 0.05f), 0), - local: true, synchronize: false); + CharacterController cc = ce.GetComponent(); + float targetY = TransformYForFoot(ce, 0.05f); + Debug.Log($"[Test] BEFORE placement: transform.y={ce.transform.position.y:F4} " + + $"cc.height={cc.height} cc.center={cc.center} target transform.y={targetY:F4}"); + + // CharacterController locks transform during simulation in some Unity versions. Disable it, + // teleport, re-enable. Direct transform.position assignment bypasses BaseEntity.SetPosition + // and any worldOffset adjustment. + cc.enabled = false; + ce.transform.position = new Vector3(0, targetY, 0); + cc.enabled = true; + + Debug.Log($"[Test] AFTER direct teleport: transform.y={ce.transform.position.y:F4}"); + yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate(); + Debug.Log($"[Test] AFTER two fixed updates: transform.y={ce.transform.position.y:F4} " + + $"FootY={FootY(ce):F4} cc.isGrounded={cc.isGrounded} IsOnSurface={ce.IsOnSurface()}"); + Assert.IsTrue(ce.IsOnSurface(), - $"IsOnSurface returned false when foot was only 0.05 m above floor. " + - $"Foot y={FootY(ce):F4}, transform y={ce.transform.position.y:F4}. " + - "Raycast distance or grounding fallback is still too strict."); + $"IsOnSurface returned false when foot was 0.05 m above floor. " + + $"Foot y={FootY(ce):F4}, transform y={ce.transform.position.y:F4}, " + + $"cc.isGrounded={cc.isGrounded}."); } /// @@ -231,19 +244,22 @@ public IEnumerator CharacterEntity_IsOnSurface_FalseAtLargeGapAboveFloor() CharacterEntity ce = null; yield return BuildSceneAndCharacter(new Vector3(0, 5f, 0), buildFloor: true, c => ce = c); - // Disable gravity so the character holds the test position. Rigidbody rb = ce.GetComponent(); if (rb != null) rb.useGravity = false; - // Foot 1.5 m above floor — well above any reasonable grounding margin. - ce.SetPosition(new Vector3(0, TransformYForFoot(ce, 1.5f), 0), - local: true, synchronize: false); + CharacterController cc = ce.GetComponent(); + float targetY = TransformYForFoot(ce, 1.5f); + + cc.enabled = false; + ce.transform.position = new Vector3(0, targetY, 0); + cc.enabled = true; + yield return new WaitForFixedUpdate(); Assert.IsFalse(ce.IsOnSurface(), - $"IsOnSurface returned true at a 1.5 m gap. Foot y={FootY(ce):F4}. " + - "Either the raycast is too long, or it's hitting something other than the floor " + - "(the character's own collider?)."); + $"IsOnSurface returned true at a 1.5 m gap. Foot y={FootY(ce):F4}, " + + $"transform y={ce.transform.position.y:F4}, cc.isGrounded={cc.isGrounded}. " + + "Either the raycast is too long, or it's hitting something other than the floor."); } // --------------------------------------------------------------------------------------------- From d3784f4460d441e7194ea2d88a3c4ba0ca7d84e8 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 21 May 2026 06:44:15 -0400 Subject: [PATCH 10/22] Resolving error --- .../APIs/Input/Scripts/Input.cs | 13 ++++ .../Character/Scripts/CharacterEntity.cs | 28 ++++++++ .../CharacterEntityGroundingTests.cs | 68 ++++++++++++++++--- 3 files changed, 99 insertions(+), 10 deletions(-) diff --git a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs index f1645ac0..d9054370 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs @@ -623,6 +623,13 @@ public static bool AddRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Add(entityToFollowRig.internalEntity); } } + + // The rig is now the sole writer of this entity's position. Suppress the entity's own + // motion (gravity, Move) so the two systems don't fight and produce avatar ghosting. + if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) + { + ce.externalPositionControl = true; + } return true; } @@ -710,6 +717,12 @@ public static bool RemoveRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Remove(entityToFollowRig.internalEntity); } } + + // Restore entity-owned motion now that the rig no longer drives this entity's position. + if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) + { + ce.externalPositionControl = false; + } return true; } diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 9ef3d8cd..1471b070 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -143,6 +143,14 @@ public override string entityTag /// private float verticalVelocity = 0f; + /// + /// When true, FixedUpdate skips gravity, grounding, and Move() entirely. Some other system + /// (typically a VR rig that has this entity as a follower) is the sole writer of position + /// each frame, so the entity must not also write or the two writers fight and the avatar + /// flickers between positions. Set via Input.AddRigFollower / RemoveRigFollower. + /// + public bool externalPositionControl = false; + /// /// Get the character GameObject. /// @@ -982,6 +990,18 @@ private void MakePhysical() } gameObject.SetActive(true); + // Keep Rigidbody kinematic in Physical state — character motion is owned by + // CharacterController.Move() in FixedUpdate. A non-kinematic Rigidbody on the same + // GameObject causes per-frame position fighting (visible as the avatar/label rendering + // at two flickering positions). Defends against SetMotion(Moving) flipping this. + if (rigidBody != null) + { + rigidBody.isKinematic = true; + } + if (capsuleCollider != null) + { + capsuleCollider.enabled = true; + } interactionState = InteractionState.Physical; } @@ -1067,6 +1087,14 @@ void FixedUpdate() return; } + // If some other system (typically a VR rig) is positioning this entity each frame, do + // nothing here — running gravity or Move would create a second position writer and the + // avatar would flicker between the two writers' positions. + if (externalPositionControl) + { + return; + } + // Reset vertical velocity when grounded and falling — prevents gravity from compounding // while on a surface, and zeroes any tiny residual downward velocity from prior ticks. if (IsOnSurface() && verticalVelocity < 0) diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs index 4312632b..750c5b90 100644 --- a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -210,24 +210,16 @@ public IEnumerator CharacterEntity_IsOnSurface_TrueAtSmallGapAboveFloor() CharacterController cc = ce.GetComponent(); float targetY = TransformYForFoot(ce, 0.05f); - Debug.Log($"[Test] BEFORE placement: transform.y={ce.transform.position.y:F4} " + - $"cc.height={cc.height} cc.center={cc.center} target transform.y={targetY:F4}"); - // CharacterController locks transform during simulation in some Unity versions. Disable it, - // teleport, re-enable. Direct transform.position assignment bypasses BaseEntity.SetPosition - // and any worldOffset adjustment. + // CharacterController suppresses direct transform.position writes during simulation in + // some Unity versions. Canonical workaround: disable, teleport, re-enable. cc.enabled = false; ce.transform.position = new Vector3(0, targetY, 0); cc.enabled = true; - Debug.Log($"[Test] AFTER direct teleport: transform.y={ce.transform.position.y:F4}"); - yield return new WaitForFixedUpdate(); yield return new WaitForFixedUpdate(); - Debug.Log($"[Test] AFTER two fixed updates: transform.y={ce.transform.position.y:F4} " + - $"FootY={FootY(ce):F4} cc.isGrounded={cc.isGrounded} IsOnSurface={ce.IsOnSurface()}"); - Assert.IsTrue(ce.IsOnSurface(), $"IsOnSurface returned false when foot was 0.05 m above floor. " + $"Foot y={FootY(ce):F4}, transform y={ce.transform.position.y:F4}, " + @@ -336,6 +328,62 @@ public IEnumerator CharacterEntity_PureVerticalSettle_NoWorseThan_MixedAxisSettl "the diagnosis would address this."); } + // --------------------------------------------------------------------------------------------- + // externalPositionControl: when a CharacterEntity is a rig follower, FixedUpdate must NOT + // write to the transform. Otherwise gravity + Move() fight the rig's per-frame SetPosition and + // the avatar visibly flickers between two positions. + // --------------------------------------------------------------------------------------------- + + [UnityTest] + public IEnumerator CharacterEntity_ExternalPositionControl_PreventsFixedUpdateMotion() + { + CharacterEntity ce = null; + // Spawn at y=3 with a floor below — gravity would normally drop the character. + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Simulate the rig taking over: set the flag and pin the entity at a specific position + // each frame the way VRRig.UpdateFollowers would. + ce.externalPositionControl = true; + + CharacterController cc = ce.GetComponent(); + Vector3 pinnedPosition = new Vector3(0, 3f, 0); + + // Hold the position across multiple physics ticks. Without externalPositionControl this + // wouldn't work — gravity would integrate verticalVelocity downward and Move() would drag + // the transform off pinnedPosition between our writes. + for (int i = 0; i < 50; i++) + { + cc.enabled = false; + ce.transform.position = pinnedPosition; + cc.enabled = true; + yield return new WaitForFixedUpdate(); + } + + Assert.That(ce.transform.position.y, Is.EqualTo(pinnedPosition.y).Within(0.01f), + $"externalPositionControl=true did not suppress FixedUpdate motion. " + + $"Final transform.y={ce.transform.position.y:F4}, expected {pinnedPosition.y:F4}."); + } + + [UnityTest] + public IEnumerator CharacterEntity_ExternalPositionControl_FalseRestoresMotion() + { + CharacterEntity ce = null; + yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); + + // Toggle on/off — final state false means gravity should resume. + ce.externalPositionControl = true; + yield return new WaitForSeconds(0.5f); + float yAfterSuppressed = ce.transform.position.y; + + ce.externalPositionControl = false; + yield return new WaitForSeconds(2f); + float yAfterRestored = ce.transform.position.y; + + Assert.Less(yAfterRestored, yAfterSuppressed - 0.5f, + $"After clearing externalPositionControl, character should fall under gravity. " + + $"Before={yAfterSuppressed:F4}, after={yAfterRestored:F4}."); + } + // --------------------------------------------------------------------------------------------- // Diagnostic dump: not a pass/fail test, but exercises the path and logs the key signals every // tick so a human can read the console after a run. Useful first-look during investigation. From cc1cabe3bc9e1d6f761d13f9ec5cc635b89581f4 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 21 May 2026 18:45:40 -0400 Subject: [PATCH 11/22] Resolving error --- .../APIs/Input/Scripts/Input.cs | 31 ++++- .../VEMLHandler/Scripts/VEMLHandler.cs | 4 + .../UserInterface/Input/Scripts/VRRig.cs | 106 ++++++++++++++++++ 3 files changed, 135 insertions(+), 6 deletions(-) diff --git a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs index d9054370..3ba776e0 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs @@ -785,7 +785,8 @@ public static bool RemoveRightHandFollower(BaseEntity entityToFollowRightHand) } /// - /// Whether or not gravity is enabled for desktop input. + /// Whether or not gravity is enabled. Routes to whichever rig is active: DesktopRig for + /// desktop/mobile, VRRig for VR (via the ContinuousMoveProvider on the XR Origin). /// public static bool gravityEnabled { @@ -795,6 +796,10 @@ public static bool gravityEnabled { WebVerseRuntime.Instance.inputManager.desktopRig.gravityEnabled = value; } + if (WebVerseRuntime.Instance.vrRig != null) + { + WebVerseRuntime.Instance.vrRig.gravityEnabled = value; + } } get @@ -803,6 +808,10 @@ public static bool gravityEnabled { return WebVerseRuntime.Instance.inputManager.desktopRig.gravityEnabled; } + if (WebVerseRuntime.Instance.vrRig != null) + { + return WebVerseRuntime.Instance.vrRig.gravityEnabled; + } return false; } } @@ -923,7 +932,8 @@ public static float lookSpeed } /// - /// Set the avatar entity by tag for desktop input. + /// Set the avatar entity by tag. Routes to whichever rig is active so worlds don't need + /// platform-specific setup. /// /// The tag of the entity to use as the avatar. /// Whether or not the operation was successful. @@ -935,14 +945,23 @@ public static bool SetAvatarEntityByTag(string entityTag) return false; } + bool any = false; if (WebVerseRuntime.Instance.inputManager.desktopRig != null) { WebVerseRuntime.Instance.inputManager.desktopRig.SetAvatarEntityByTag(entityTag); - return true; + any = true; } - - Logging.LogWarning("[Input->SetAvatarEntityByTag] Desktop rig not available."); - return false; + if (WebVerseRuntime.Instance.vrRig != null) + { + WebVerseRuntime.Instance.vrRig.SetAvatarEntityByTag(entityTag); + any = true; + } + if (!any) + { + Logging.LogWarning("[Input->SetAvatarEntityByTag] No rig available."); + return false; + } + return true; } /// diff --git a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs index 18cea3a0..7bba9171 100644 --- a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs +++ b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs @@ -931,6 +931,10 @@ private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string base { WebVerseRuntime.Instance.inputManager.desktopRig.SetAvatarEntityByTag(pendingAvatarEntityTag); } + if (WebVerseRuntime.Instance.vrRig != null) + { + WebVerseRuntime.Instance.vrRig.SetAvatarEntityByTag(pendingAvatarEntityTag); + } pendingAvatarEntityTag = null; // Clear the pending tag } diff --git a/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs b/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs index ce35dfbc..618e153e 100644 --- a/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs +++ b/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs @@ -533,6 +533,35 @@ public bool joystickMotionEnabled set { if (dynamicMoveProvider != null) dynamicMoveProvider.enabled = value; } } + /// + /// Whether or not gravity is applied to the VR rig's locomotion. The rig is the single + /// authority for VR avatar position (see CharacterEntity.externalPositionControl), so + /// gravity must live on the rig's move provider, not on the avatar entity itself. Uses + /// reflection on the dynamicMoveProvider's "useGravity" property so this compiles across + /// XR Interaction Toolkit versions where the move provider's exact class name may differ. + /// + public bool gravityEnabled + { + get + { + if (dynamicMoveProvider == null) return false; + var prop = dynamicMoveProvider.GetType().GetProperty("useGravity", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + if (prop == null || !prop.CanRead) return false; + return (bool)prop.GetValue(dynamicMoveProvider); + } + set + { + if (dynamicMoveProvider == null) return; + var prop = dynamicMoveProvider.GetType().GetProperty("useGravity", + System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); + if (prop != null && prop.CanWrite) + { + prop.SetValue(dynamicMoveProvider, value); + } + } + } + public bool leftGrabMoveEnabled { get => leftGrabMoveProvider != null && leftGrabMoveProvider.enabled; @@ -591,12 +620,89 @@ public void Initialize() if (leftHandFollowers == null) leftHandFollowers = new List(); if (rightHandFollowers == null) rightHandFollowers = new List(); + // Auto-discover a CharacterEntity tagged "avatar" if one exists in the active world. + // This matches DesktopRig behavior and means world authors don't need to manually call + // Input.AddRigFollower from a script. + TryAutoAttachAvatar(); + // Set up platform-specific controller models SetupPlatformControllerModels(); Logging.Log($"[VRRig] Initialized. RayType={rayInteractorType}, HandTracking={enableHandTracking}"); } + /// + /// Find a CharacterEntity with the given tag in the active world and make it follow the rig. + /// Removes any previously-attached avatar from rigFollowers and clears its + /// externalPositionControl. Mirrors DesktopRig.SetAvatarEntityByTag so worlds work the same + /// way on both platforms. + /// + public void SetAvatarEntityByTag(string entityTag) + { + if (string.IsNullOrEmpty(entityTag) || StraightFour.StraightFour.ActiveWorld == null) + { + return; + } + + CharacterEntity found = null; + foreach (var entity in StraightFour.StraightFour.ActiveWorld.entityManager.GetAllEntities()) + { + if (entity is CharacterEntity characterEntity && entity.entityTag == entityTag) + { + found = characterEntity; + break; + } + } + + if (found == null) + { + Logging.LogWarning($"[VRRig->SetAvatarEntityByTag] Could not find character entity with tag: {entityTag}"); + return; + } + + // Detach any previously-attached avatar entities (clear their externalPositionControl). + if (rigFollowers != null) + { + foreach (var existing in rigFollowers) + { + if (existing is CharacterEntity prevAvatar && prevAvatar != found) + { + prevAvatar.externalPositionControl = false; + } + } + rigFollowers.RemoveAll(e => e is CharacterEntity && e != found); + if (!rigFollowers.Contains(found)) + { + rigFollowers.Add(found); + } + } + else + { + rigFollowers = new List { found }; + } + + // Rig is now the sole position writer for this entity. + found.externalPositionControl = true; + } + + /// + /// Try to auto-attach an avatar entity (tagged "avatar") if no avatar is currently attached + /// to the rig. Called from Initialize and can be called again after world load. + /// + private void TryAutoAttachAvatar() + { + if (StraightFour.StraightFour.ActiveWorld == null) return; + // If a CharacterEntity is already a rig follower, leave it alone. + if (rigFollowers != null) + { + foreach (var existing in rigFollowers) + { + if (existing is CharacterEntity) return; + } + } + SetAvatarEntityByTag("avatar"); + } + /// /// Terminate the VR rig. /// From 39d5e5106c03cfd925be529a6999fd366ab2f0b8 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 21 May 2026 20:04:09 -0400 Subject: [PATCH 12/22] Resolving error --- .../APIs/Input/Scripts/Input.cs | 44 +------- .../VEMLHandler/Scripts/VEMLHandler.cs | 4 - .../Character/Scripts/CharacterEntity.cs | 16 --- .../CharacterEntityGroundingTests.cs | 56 --------- .../UserInterface/Input/Scripts/VRRig.cs | 106 ------------------ 5 files changed, 6 insertions(+), 220 deletions(-) diff --git a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs index 3ba776e0..f1645ac0 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs @@ -623,13 +623,6 @@ public static bool AddRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Add(entityToFollowRig.internalEntity); } } - - // The rig is now the sole writer of this entity's position. Suppress the entity's own - // motion (gravity, Move) so the two systems don't fight and produce avatar ghosting. - if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) - { - ce.externalPositionControl = true; - } return true; } @@ -717,12 +710,6 @@ public static bool RemoveRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Remove(entityToFollowRig.internalEntity); } } - - // Restore entity-owned motion now that the rig no longer drives this entity's position. - if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) - { - ce.externalPositionControl = false; - } return true; } @@ -785,8 +772,7 @@ public static bool RemoveRightHandFollower(BaseEntity entityToFollowRightHand) } /// - /// Whether or not gravity is enabled. Routes to whichever rig is active: DesktopRig for - /// desktop/mobile, VRRig for VR (via the ContinuousMoveProvider on the XR Origin). + /// Whether or not gravity is enabled for desktop input. /// public static bool gravityEnabled { @@ -796,10 +782,6 @@ public static bool gravityEnabled { WebVerseRuntime.Instance.inputManager.desktopRig.gravityEnabled = value; } - if (WebVerseRuntime.Instance.vrRig != null) - { - WebVerseRuntime.Instance.vrRig.gravityEnabled = value; - } } get @@ -808,10 +790,6 @@ public static bool gravityEnabled { return WebVerseRuntime.Instance.inputManager.desktopRig.gravityEnabled; } - if (WebVerseRuntime.Instance.vrRig != null) - { - return WebVerseRuntime.Instance.vrRig.gravityEnabled; - } return false; } } @@ -932,8 +910,7 @@ public static float lookSpeed } /// - /// Set the avatar entity by tag. Routes to whichever rig is active so worlds don't need - /// platform-specific setup. + /// Set the avatar entity by tag for desktop input. /// /// The tag of the entity to use as the avatar. /// Whether or not the operation was successful. @@ -945,23 +922,14 @@ public static bool SetAvatarEntityByTag(string entityTag) return false; } - bool any = false; if (WebVerseRuntime.Instance.inputManager.desktopRig != null) { WebVerseRuntime.Instance.inputManager.desktopRig.SetAvatarEntityByTag(entityTag); - any = true; - } - if (WebVerseRuntime.Instance.vrRig != null) - { - WebVerseRuntime.Instance.vrRig.SetAvatarEntityByTag(entityTag); - any = true; - } - if (!any) - { - Logging.LogWarning("[Input->SetAvatarEntityByTag] No rig available."); - return false; + return true; } - return true; + + Logging.LogWarning("[Input->SetAvatarEntityByTag] Desktop rig not available."); + return false; } /// diff --git a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs index 7bba9171..18cea3a0 100644 --- a/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs +++ b/Assets/Runtime/Handlers/VEMLHandler/Scripts/VEMLHandler.cs @@ -931,10 +931,6 @@ private IEnumerator ApplyVEMLDocument(Schema.V3_0.veml vemlDocument, string base { WebVerseRuntime.Instance.inputManager.desktopRig.SetAvatarEntityByTag(pendingAvatarEntityTag); } - if (WebVerseRuntime.Instance.vrRig != null) - { - WebVerseRuntime.Instance.vrRig.SetAvatarEntityByTag(pendingAvatarEntityTag); - } pendingAvatarEntityTag = null; // Clear the pending tag } diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 1471b070..4329ae90 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -143,14 +143,6 @@ public override string entityTag /// private float verticalVelocity = 0f; - /// - /// When true, FixedUpdate skips gravity, grounding, and Move() entirely. Some other system - /// (typically a VR rig that has this entity as a follower) is the sole writer of position - /// each frame, so the entity must not also write or the two writers fight and the avatar - /// flickers between positions. Set via Input.AddRigFollower / RemoveRigFollower. - /// - public bool externalPositionControl = false; - /// /// Get the character GameObject. /// @@ -1087,14 +1079,6 @@ void FixedUpdate() return; } - // If some other system (typically a VR rig) is positioning this entity each frame, do - // nothing here — running gravity or Move would create a second position writer and the - // avatar would flicker between the two writers' positions. - if (externalPositionControl) - { - return; - } - // Reset vertical velocity when grounded and falling — prevents gravity from compounding // while on a surface, and zeroes any tiny residual downward velocity from prior ticks. if (IsOnSurface() && verticalVelocity < 0) diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs index 750c5b90..af7405d6 100644 --- a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -328,62 +328,6 @@ public IEnumerator CharacterEntity_PureVerticalSettle_NoWorseThan_MixedAxisSettl "the diagnosis would address this."); } - // --------------------------------------------------------------------------------------------- - // externalPositionControl: when a CharacterEntity is a rig follower, FixedUpdate must NOT - // write to the transform. Otherwise gravity + Move() fight the rig's per-frame SetPosition and - // the avatar visibly flickers between two positions. - // --------------------------------------------------------------------------------------------- - - [UnityTest] - public IEnumerator CharacterEntity_ExternalPositionControl_PreventsFixedUpdateMotion() - { - CharacterEntity ce = null; - // Spawn at y=3 with a floor below — gravity would normally drop the character. - yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); - - // Simulate the rig taking over: set the flag and pin the entity at a specific position - // each frame the way VRRig.UpdateFollowers would. - ce.externalPositionControl = true; - - CharacterController cc = ce.GetComponent(); - Vector3 pinnedPosition = new Vector3(0, 3f, 0); - - // Hold the position across multiple physics ticks. Without externalPositionControl this - // wouldn't work — gravity would integrate verticalVelocity downward and Move() would drag - // the transform off pinnedPosition between our writes. - for (int i = 0; i < 50; i++) - { - cc.enabled = false; - ce.transform.position = pinnedPosition; - cc.enabled = true; - yield return new WaitForFixedUpdate(); - } - - Assert.That(ce.transform.position.y, Is.EqualTo(pinnedPosition.y).Within(0.01f), - $"externalPositionControl=true did not suppress FixedUpdate motion. " + - $"Final transform.y={ce.transform.position.y:F4}, expected {pinnedPosition.y:F4}."); - } - - [UnityTest] - public IEnumerator CharacterEntity_ExternalPositionControl_FalseRestoresMotion() - { - CharacterEntity ce = null; - yield return BuildSceneAndCharacter(new Vector3(0, 3f, 0), buildFloor: true, c => ce = c); - - // Toggle on/off — final state false means gravity should resume. - ce.externalPositionControl = true; - yield return new WaitForSeconds(0.5f); - float yAfterSuppressed = ce.transform.position.y; - - ce.externalPositionControl = false; - yield return new WaitForSeconds(2f); - float yAfterRestored = ce.transform.position.y; - - Assert.Less(yAfterRestored, yAfterSuppressed - 0.5f, - $"After clearing externalPositionControl, character should fall under gravity. " + - $"Before={yAfterSuppressed:F4}, after={yAfterRestored:F4}."); - } - // --------------------------------------------------------------------------------------------- // Diagnostic dump: not a pass/fail test, but exercises the path and logs the key signals every // tick so a human can read the console after a run. Useful first-look during investigation. diff --git a/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs b/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs index 618e153e..ce35dfbc 100644 --- a/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs +++ b/Assets/Runtime/UserInterface/Input/Scripts/VRRig.cs @@ -533,35 +533,6 @@ public bool joystickMotionEnabled set { if (dynamicMoveProvider != null) dynamicMoveProvider.enabled = value; } } - /// - /// Whether or not gravity is applied to the VR rig's locomotion. The rig is the single - /// authority for VR avatar position (see CharacterEntity.externalPositionControl), so - /// gravity must live on the rig's move provider, not on the avatar entity itself. Uses - /// reflection on the dynamicMoveProvider's "useGravity" property so this compiles across - /// XR Interaction Toolkit versions where the move provider's exact class name may differ. - /// - public bool gravityEnabled - { - get - { - if (dynamicMoveProvider == null) return false; - var prop = dynamicMoveProvider.GetType().GetProperty("useGravity", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); - if (prop == null || !prop.CanRead) return false; - return (bool)prop.GetValue(dynamicMoveProvider); - } - set - { - if (dynamicMoveProvider == null) return; - var prop = dynamicMoveProvider.GetType().GetProperty("useGravity", - System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance); - if (prop != null && prop.CanWrite) - { - prop.SetValue(dynamicMoveProvider, value); - } - } - } - public bool leftGrabMoveEnabled { get => leftGrabMoveProvider != null && leftGrabMoveProvider.enabled; @@ -620,89 +591,12 @@ public void Initialize() if (leftHandFollowers == null) leftHandFollowers = new List(); if (rightHandFollowers == null) rightHandFollowers = new List(); - // Auto-discover a CharacterEntity tagged "avatar" if one exists in the active world. - // This matches DesktopRig behavior and means world authors don't need to manually call - // Input.AddRigFollower from a script. - TryAutoAttachAvatar(); - // Set up platform-specific controller models SetupPlatformControllerModels(); Logging.Log($"[VRRig] Initialized. RayType={rayInteractorType}, HandTracking={enableHandTracking}"); } - /// - /// Find a CharacterEntity with the given tag in the active world and make it follow the rig. - /// Removes any previously-attached avatar from rigFollowers and clears its - /// externalPositionControl. Mirrors DesktopRig.SetAvatarEntityByTag so worlds work the same - /// way on both platforms. - /// - public void SetAvatarEntityByTag(string entityTag) - { - if (string.IsNullOrEmpty(entityTag) || StraightFour.StraightFour.ActiveWorld == null) - { - return; - } - - CharacterEntity found = null; - foreach (var entity in StraightFour.StraightFour.ActiveWorld.entityManager.GetAllEntities()) - { - if (entity is CharacterEntity characterEntity && entity.entityTag == entityTag) - { - found = characterEntity; - break; - } - } - - if (found == null) - { - Logging.LogWarning($"[VRRig->SetAvatarEntityByTag] Could not find character entity with tag: {entityTag}"); - return; - } - - // Detach any previously-attached avatar entities (clear their externalPositionControl). - if (rigFollowers != null) - { - foreach (var existing in rigFollowers) - { - if (existing is CharacterEntity prevAvatar && prevAvatar != found) - { - prevAvatar.externalPositionControl = false; - } - } - rigFollowers.RemoveAll(e => e is CharacterEntity && e != found); - if (!rigFollowers.Contains(found)) - { - rigFollowers.Add(found); - } - } - else - { - rigFollowers = new List { found }; - } - - // Rig is now the sole position writer for this entity. - found.externalPositionControl = true; - } - - /// - /// Try to auto-attach an avatar entity (tagged "avatar") if no avatar is currently attached - /// to the rig. Called from Initialize and can be called again after world load. - /// - private void TryAutoAttachAvatar() - { - if (StraightFour.StraightFour.ActiveWorld == null) return; - // If a CharacterEntity is already a rig follower, leave it alone. - if (rigFollowers != null) - { - foreach (var existing in rigFollowers) - { - if (existing is CharacterEntity) return; - } - } - SetAvatarEntityByTag("avatar"); - } - /// /// Terminate the VR rig. /// From 0a08b2f5b7e6baaf2d929780a749c555fb4c6617 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 21 May 2026 20:11:44 -0400 Subject: [PATCH 13/22] Resolving error --- .../Character/Scripts/CharacterEntity.cs | 17 ++++-------- .../CharacterEntityGroundingTests.cs | 26 +++++++++---------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 4329ae90..1730c288 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -1057,22 +1057,15 @@ private void SetUpHighlightVolume() highlightCube.SetActive(false); } - private float timeToWaitForUpdate = 0.025f; - private float timeWaitedForUpdate = 0; private int stepToRaise = 1; private int maxStepToRaise = 1024; void FixedUpdate() { - timeWaitedForUpdate += Time.deltaTime; - if (timeWaitedForUpdate >= timeToWaitForUpdate) - { - timeWaitedForUpdate = 0; - } - else - { - return; - } - + // No throttling here — FixedUpdate already runs at the fixed physics rate (50 Hz by + // default), which is the correct cadence for gravity integration. The previous throttle + // (0.025s) caused FixedUpdate to fire only every other tick at fixedDeltaTime=0.02s but + // still used Time.deltaTime=0.02s in the integration math — producing gravity at half + // the intended rate. if (characterController == null) { //LogSystem.LogError("[CharacterEntity->Update] No character controller for character entity."); diff --git a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs index af7405d6..74c4d4a6 100644 --- a/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs +++ b/Assets/Runtime/StraightFour/Testing/EntityTests/CharacterEntityGroundingTests.cs @@ -255,14 +255,13 @@ public IEnumerator CharacterEntity_IsOnSurface_FalseAtLargeGapAboveFloor() } // --------------------------------------------------------------------------------------------- - // Suspect 4: Free-fall integration should approximate s = 0.5 * g * t^2. The unit-confusion - // bug (gravity adds to velocity per second, but velocity is passed as per-tick displacement) - // produces an order-of-magnitude faster fall. + // Free-fall integration should approximate s = 0.5 * g * t^2. // // Analytic free-fall over t=0.3s: dy = 0.5 * 9.81 * 0.09 ≈ 0.44 m. - // Buggy code: dy ≈ -0.196 * (1+2+...+N) where N ≈ t/0.04 ≈ 7 ticks → dy ≈ 5.5 m. - // - // We assert |displacement| < 1.0 m. Real physics: well under. Buggy: way over. + // Discrete sum at 50 Hz fixed-step: ≈ 0.47 m. + // Pre-fix bugs to catch: + // - Unit-confusion (velocity stored as displacement): dy ≈ 5–7 m (way over). + // - FixedUpdate-throttle gating with mismatched dt: dy ≈ 0.11 m (half-rate, way under). // --------------------------------------------------------------------------------------------- [UnityTest] @@ -280,13 +279,14 @@ public IEnumerator CharacterEntity_FreeFall_RoughlyMatchesGravity() float endY = ce.transform.position.y; float dropped = startY - endY; - Assert.Less(dropped, 1.0f, - $"Character fell {dropped:F3} m in 0.3 s. Analytic free-fall is ~0.44 m; >1 m strongly " + - "suggests Suspect 4: currentVelocity is being treated as per-tick displacement when " + - "gravity is added as v += a·dt (units mismatch)."); - Assert.Greater(dropped, 0.05f, - $"Character barely moved ({dropped:F3} m in 0.3 s). Gravity may be disabled or " + - "the rigidbody is kinematic and the custom gravity path didn't run."); + Assert.Less(dropped, 0.8f, + $"Character fell {dropped:F3} m in 0.3 s. Analytic free-fall is ~0.44 m; >0.8 m suggests " + + "the units bug (currentVelocity used as displacement per tick)."); + Assert.Greater(dropped, 0.25f, + $"Character only fell {dropped:F3} m in 0.3 s — significantly less than the expected " + + "~0.44 m of free-fall. Suggests gravity is being applied at half rate (FixedUpdate " + + "throttle gate using Time.deltaTime instead of the throttle interval), or gravity is " + + "otherwise weakened."); } // --------------------------------------------------------------------------------------------- From 27f543b56e69fe7d2e65de088ac28e7c945b3075 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Thu, 21 May 2026 20:20:56 -0400 Subject: [PATCH 14/22] Resolving error --- .../Entity/Character/Scripts/CharacterEntity.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 1730c288..6216e61e 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -990,10 +990,11 @@ private void MakePhysical() { rigidBody.isKinematic = true; } - if (capsuleCollider != null) - { - capsuleCollider.enabled = true; - } + // Leave capsuleCollider disabled — the CharacterController is the canonical collider + // for character motion. Enabling the CapsuleCollider on the same GameObject made the + // character settle ~0.5 m above the floor (the CapsuleCollider's center.y) because the + // two colliders fight during depenetration. MakeHidden and MakeStatic both disable it + // for the same reason; Physical should match. interactionState = InteractionState.Physical; } From 3a485be9a981ebe6448c1feef8c719d1ebbb4fb7 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 22 May 2026 06:20:13 -0400 Subject: [PATCH 15/22] Resolving error --- .../Character/Scripts/CharacterEntity.cs | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 6216e61e..521dd238 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -757,11 +757,22 @@ public void Initialize(Guid idToSet, GameObject characterObjectPrefab, { characterGO = Instantiate(characterObjectPrefab); characterGO.SetActive(true); - GameObject characterLabel = Instantiate(StraightFour.ActiveWorld.entityManager.characterControllerLabelPrefab); - characterLabel.transform.SetParent(characterGO.transform); - characterLabel.transform.localPosition = avatarLabelOffset; - //characterLabel.transform.localRotation = Quaternion.identity; - //characterLabel.transform.localScale = Vector3.one; + + // If the custom prefab already has a label baked in, attach a Billboard to it + // instead of instantiating a duplicate. Having two labels at different localPositions + // visually looks like the label "flickering between two positions" in motion. + TextMeshProUGUI existingCustomLabel = characterGO.GetComponentInChildren(); + GameObject characterLabel; + if (existingCustomLabel != null) + { + characterLabel = existingCustomLabel.gameObject; + } + else + { + characterLabel = Instantiate(StraightFour.ActiveWorld.entityManager.characterControllerLabelPrefab); + characterLabel.transform.SetParent(characterGO.transform); + characterLabel.transform.localPosition = avatarLabelOffset; + } // Add Billboard component to make the label always face the camera Billboard billboard = characterLabel.GetComponent(); From 5f76fa67897de0d439f82b3b1aeb2f10e13c147e Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 22 May 2026 06:34:01 -0400 Subject: [PATCH 16/22] Resolving error --- .../Character/Scripts/CharacterEntity.cs | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 521dd238..6216e61e 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -757,22 +757,11 @@ public void Initialize(Guid idToSet, GameObject characterObjectPrefab, { characterGO = Instantiate(characterObjectPrefab); characterGO.SetActive(true); - - // If the custom prefab already has a label baked in, attach a Billboard to it - // instead of instantiating a duplicate. Having two labels at different localPositions - // visually looks like the label "flickering between two positions" in motion. - TextMeshProUGUI existingCustomLabel = characterGO.GetComponentInChildren(); - GameObject characterLabel; - if (existingCustomLabel != null) - { - characterLabel = existingCustomLabel.gameObject; - } - else - { - characterLabel = Instantiate(StraightFour.ActiveWorld.entityManager.characterControllerLabelPrefab); - characterLabel.transform.SetParent(characterGO.transform); - characterLabel.transform.localPosition = avatarLabelOffset; - } + GameObject characterLabel = Instantiate(StraightFour.ActiveWorld.entityManager.characterControllerLabelPrefab); + characterLabel.transform.SetParent(characterGO.transform); + characterLabel.transform.localPosition = avatarLabelOffset; + //characterLabel.transform.localRotation = Quaternion.identity; + //characterLabel.transform.localScale = Vector3.one; // Add Billboard component to make the label always face the camera Billboard billboard = characterLabel.GetComponent(); From 0e65d3a5910fb365489b2ec32aa338753ba496fc Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 22 May 2026 06:39:45 -0400 Subject: [PATCH 17/22] Resolving error --- .../Character/Scripts/CharacterEntity.cs | 29 +++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 6216e61e..5d338282 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -323,7 +323,17 @@ public bool Move(Vector3 amount, bool synchronize = true) currentVelocity.x += amount.x; currentVelocity.z += amount.z; + // DIAGNOSTIC: remove after VR joystick issue is resolved. + if (amount.sqrMagnitude > 0.0000001f) + { + LogSystem.Log($"[CharDiag] Move(amount=({amount.x:F4},{amount.y:F4},{amount.z:F4})) " + + $"posBefore=({transform.position.x:F3},{transform.position.y:F3},{transform.position.z:F3})"); + } characterController.Move(amount); + if (amount.sqrMagnitude > 0.0000001f) + { + LogSystem.Log($"[CharDiag] Move posAfter=({transform.position.x:F3},{transform.position.y:F3},{transform.position.z:F3})"); + } if (synchronizer != null && synchronize == true) { @@ -1073,6 +1083,11 @@ void FixedUpdate() return; } + // DIAGNOSTIC: remove after VR joystick issue is resolved. + Vector3 fuPosBefore = transform.position; + bool fuOnSurface = IsOnSurface(); + bool fuCcGrounded = characterController.isGrounded; + // Reset vertical velocity when grounded and falling — prevents gravity from compounding // while on a surface, and zeroes any tiny residual downward velocity from prior ticks. if (IsOnSurface() && verticalVelocity < 0) @@ -1087,8 +1102,18 @@ void FixedUpdate() // currentVelocity.x/z are per-frame displacement (legacy semantics callers depend on). // verticalVelocity is in m/s — multiply by dt to convert to displacement-this-tick. - characterController.Move(new Vector3( - currentVelocity.x, verticalVelocity * Time.deltaTime, currentVelocity.z)); + Vector3 fuDelta = new Vector3( + currentVelocity.x, verticalVelocity * Time.deltaTime, currentVelocity.z); + characterController.Move(fuDelta); + // DIAGNOSTIC: log only when something meaningful happened (input or fall). + if (fuDelta.sqrMagnitude > 0.0000001f || Mathf.Abs(verticalVelocity) > 0.01f) + { + LogSystem.Log($"[CharDiag] FU posBefore=({fuPosBefore.x:F3},{fuPosBefore.y:F3},{fuPosBefore.z:F3}) " + + $"posAfter=({transform.position.x:F3},{transform.position.y:F3},{transform.position.z:F3}) " + + $"delta=({fuDelta.x:F4},{fuDelta.y:F4},{fuDelta.z:F4}) " + + $"vVel={verticalVelocity:F3} useGravity={rigidBody.useGravity} " + + $"onSurface={fuOnSurface} ccGrounded={fuCcGrounded} kinematic={rigidBody.isKinematic}"); + } currentVelocity.x = currentVelocity.z = 0; if (fixHeight) From 4cc04d8e0e8537db911652b389fb4169cfb3fa9b Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Fri, 22 May 2026 07:28:12 -0400 Subject: [PATCH 18/22] Resolving error --- .../APIs/Input/Scripts/Input.cs | 13 ++++++ .../Character/Scripts/CharacterEntity.cs | 44 ++++++++----------- 2 files changed, 31 insertions(+), 26 deletions(-) diff --git a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs index f1645ac0..1539a4d1 100644 --- a/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs +++ b/Assets/Runtime/Handlers/JavascriptHandler/APIs/Input/Scripts/Input.cs @@ -623,6 +623,13 @@ public static bool AddRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Add(entityToFollowRig.internalEntity); } } + + // The rig is now the sole writer of this entity's position via UpdateFollowers. + // Suppress the entity's own FixedUpdate motion to prevent two-writer ghosting. + if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) + { + ce.externalPositionControl = true; + } return true; } @@ -710,6 +717,12 @@ public static bool RemoveRigFollower(BaseEntity entityToFollowRig) WebVerseRuntime.Instance.vrRig.rigFollowers.Remove(entityToFollowRig.internalEntity); } } + + // Restore entity-owned FixedUpdate motion now that the rig no longer drives position. + if (entityToFollowRig.internalEntity is StraightFour.Entity.CharacterEntity ce) + { + ce.externalPositionControl = false; + } return true; } diff --git a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs index 5d338282..b4418fc1 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Character/Scripts/CharacterEntity.cs @@ -143,6 +143,15 @@ public override string entityTag /// private float verticalVelocity = 0f; + /// + /// When true, FixedUpdate skips gravity, grounding, and Move() entirely. Some other system + /// (typically the VR rig writing position from rigOrigin every Update via UpdateFollowers) + /// is the sole writer of this entity's position, so the entity must not also write or the + /// two writers fight and the avatar flickers between positions. Wired via + /// Input.AddRigFollower / RemoveRigFollower. + /// + public bool externalPositionControl = false; + /// /// Get the character GameObject. /// @@ -323,17 +332,7 @@ public bool Move(Vector3 amount, bool synchronize = true) currentVelocity.x += amount.x; currentVelocity.z += amount.z; - // DIAGNOSTIC: remove after VR joystick issue is resolved. - if (amount.sqrMagnitude > 0.0000001f) - { - LogSystem.Log($"[CharDiag] Move(amount=({amount.x:F4},{amount.y:F4},{amount.z:F4})) " + - $"posBefore=({transform.position.x:F3},{transform.position.y:F3},{transform.position.z:F3})"); - } characterController.Move(amount); - if (amount.sqrMagnitude > 0.0000001f) - { - LogSystem.Log($"[CharDiag] Move posAfter=({transform.position.x:F3},{transform.position.y:F3},{transform.position.z:F3})"); - } if (synchronizer != null && synchronize == true) { @@ -1083,10 +1082,13 @@ void FixedUpdate() return; } - // DIAGNOSTIC: remove after VR joystick issue is resolved. - Vector3 fuPosBefore = transform.position; - bool fuOnSurface = IsOnSurface(); - bool fuCcGrounded = characterController.isGrounded; + // If some other system (typically the VR rig via UpdateFollowers) is writing this + // entity's position each frame, don't add a second writer here — they would fight and + // the avatar would flicker between the two writers' positions. + if (externalPositionControl) + { + return; + } // Reset vertical velocity when grounded and falling — prevents gravity from compounding // while on a surface, and zeroes any tiny residual downward velocity from prior ticks. @@ -1102,18 +1104,8 @@ void FixedUpdate() // currentVelocity.x/z are per-frame displacement (legacy semantics callers depend on). // verticalVelocity is in m/s — multiply by dt to convert to displacement-this-tick. - Vector3 fuDelta = new Vector3( - currentVelocity.x, verticalVelocity * Time.deltaTime, currentVelocity.z); - characterController.Move(fuDelta); - // DIAGNOSTIC: log only when something meaningful happened (input or fall). - if (fuDelta.sqrMagnitude > 0.0000001f || Mathf.Abs(verticalVelocity) > 0.01f) - { - LogSystem.Log($"[CharDiag] FU posBefore=({fuPosBefore.x:F3},{fuPosBefore.y:F3},{fuPosBefore.z:F3}) " + - $"posAfter=({transform.position.x:F3},{transform.position.y:F3},{transform.position.z:F3}) " + - $"delta=({fuDelta.x:F4},{fuDelta.y:F4},{fuDelta.z:F4}) " + - $"vVel={verticalVelocity:F3} useGravity={rigidBody.useGravity} " + - $"onSurface={fuOnSurface} ccGrounded={fuCcGrounded} kinematic={rigidBody.isKinematic}"); - } + characterController.Move(new Vector3( + currentVelocity.x, verticalVelocity * Time.deltaTime, currentVelocity.z)); currentVelocity.x = currentVelocity.z = 0; if (fixHeight) From 34a67886984b4d738c1e369f01ca12cb1fe112ef Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Sun, 24 May 2026 10:39:42 -0400 Subject: [PATCH 19/22] Entity placement fix. --- .../Entity/Airplane/Scripts/AirplaneEntity.cs | 4 +++- .../Entity/Automobile/Scripts/AutomobileEntity.cs | 4 +++- .../StraightFour/Entity/Mesh/Scripts/MeshEntity.cs | 8 ++++++-- .../Entity/Water/Scripts/WaterBlockerEntity.cs | 4 +++- .../StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs | 4 +++- 5 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs b/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs index e14ad7f8..d0f53274 100644 --- a/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs @@ -690,9 +690,11 @@ private void MakePlacing() gameObject.SetActive(true); rbody.isKinematic = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. foreach (MeshCollider meshCollider in meshColliders) { - meshCollider.enabled = true; + meshCollider.enabled = false; } interactionState = InteractionState.Placing; } diff --git a/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs b/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs index b4f875be..6796b37e 100644 --- a/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs @@ -809,9 +809,11 @@ private void MakePlacing() gameObject.SetActive(true); rbody.isKinematic = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. foreach (MeshCollider meshCollider in meshColliders) { - meshCollider.enabled = true; + meshCollider.enabled = false; } interactionState = InteractionState.Placing; } diff --git a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs index 913e6b26..6ed18e2c 100644 --- a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs @@ -629,11 +629,15 @@ private void MakePlacing() gameObject.SetActive(true); rigidBody.isKinematic = true; + // Disable colliders during placement so the placement raycast (camera/pointer) passes + // through the preview to hit world geometry. With colliders on, the entity blocks its + // own placement raycast, producing erratic positioning. Make* methods that exit Placing + // (Static/Physical) re-enable colliders normally. foreach (MeshCollider meshCollider in meshColliders) { - meshCollider.enabled = true; + meshCollider.enabled = false; } - boxCollider.enabled = true; + boxCollider.enabled = false; interactionState = InteractionState.Placing; } diff --git a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs index 5dce8c8a..ef7d6743 100644 --- a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs @@ -579,7 +579,9 @@ private void MakePlacing() gameObject.SetActive(true); rigidBody.isKinematic = true; - boxCollider.enabled = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. + boxCollider.enabled = false; interactionState = InteractionState.Placing; } diff --git a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs index 00a70b2c..69b54200 100644 --- a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs @@ -689,7 +689,9 @@ private void MakePlacing() gameObject.SetActive(true); rigidBody.isKinematic = true; - meshCollider.enabled = true; + // Disable colliders during placement so the placement raycast passes through the + // preview to hit world geometry. See MeshEntity.MakePlacing for context. + meshCollider.enabled = false; interactionState = InteractionState.Placing; } From a500571ecda403dd865a6af0d6d4426242d88980 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Sun, 24 May 2026 10:57:04 -0400 Subject: [PATCH 20/22] Resolving error --- .../Entity/Airplane/Scripts/AirplaneEntity.cs | 13 ++++++------- .../Automobile/Scripts/AutomobileEntity.cs | 13 ++++++------- .../Entity/Mesh/Scripts/MeshEntity.cs | 16 +++++++++------- .../Entity/Water/Scripts/WaterBlockerEntity.cs | 13 ++++++------- .../Entity/Water/Scripts/WaterBodyEntity.cs | 13 ++++++------- 5 files changed, 33 insertions(+), 35 deletions(-) diff --git a/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs b/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs index d0f53274..dc781925 100644 --- a/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Airplane/Scripts/AirplaneEntity.cs @@ -772,17 +772,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs b/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs index 6796b37e..0cb15b8a 100644 --- a/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Automobile/Scripts/AutomobileEntity.cs @@ -891,17 +891,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs index 6ed18e2c..5e39db92 100644 --- a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs @@ -714,17 +714,19 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). The previous code only + // removed one Collider from the root, so entities with multiple colliders or any + // colliders on child GameObjects left the preview raycastable. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + // Remove Rigidbodies from root + descendants for consistency. + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs index ef7d6743..f2465716 100644 --- a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBlockerEntity.cs @@ -658,17 +658,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { diff --git a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs index 69b54200..87f2f4ca 100644 --- a/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Water/Scripts/WaterBodyEntity.cs @@ -768,17 +768,16 @@ private void SetUpPreviewObject() DestroyImmediate(entity); } - Collider collider = previewObject.GetComponent(); - if (collider) + // Remove ALL colliders on the preview (root + descendants). See MeshEntity for context. + foreach (Collider c in previewObject.GetComponentsInChildren(true)) { - Destroy(collider); + DestroyImmediate(c); } - Rigidbody rbody = previewObject.GetComponent(); - if (rbody) + foreach (Rigidbody rb in previewObject.GetComponentsInChildren(true)) { - Destroy(rbody); - } + DestroyImmediate(rb); + } foreach (MeshRenderer rend in previewObject.GetComponentsInChildren()) { From 6bc1e786a957587d7b337d9c73f1ddf61b182947 Mon Sep 17 00:00:00 2001 From: Dylan Baker Date: Sun, 24 May 2026 11:48:21 -0400 Subject: [PATCH 21/22] Resolving error --- Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs b/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs index ce8cd8f1..3dd8f5b3 100644 --- a/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs +++ b/Assets/Runtime/Runtime/Scripts/WebVerseRuntime.cs @@ -293,6 +293,14 @@ public struct RuntimeSettings [Tooltip("Material to use for highlighting.")] public Material highlightMaterial; + /// + /// Material to use for the placement preview of entities (the "ghost" mesh shown while + /// the user is positioning an entity). If left unassigned, preview meshes render with + /// Unity's missing-material pink. + /// + [Tooltip("Material to use for entity placement previews.")] + public Material previewMaterial; + /// /// Material to use for the sky. /// @@ -1006,6 +1014,7 @@ private void InitializeComponents(LocalStorageManager.LocalStorageMode storageMo } straightFour.airplaneEntityPrefab = airplaneEntityPrefab; straightFour.highlightMaterial = highlightMaterial; + straightFour.previewMaterial = previewMaterial; straightFour.skyMaterial = skyMaterial; straightFour.liteProceduralSkyMaterial = liteProceduralSkyMaterial; straightFour.liteProceduralSkyObject = liteProceduralSkyObject; From 322237c46388273c574cd45e4c8f9598015807b5 Mon Sep 17 00:00:00 2001 From: "Dylan Z. Baker" Date: Wed, 27 May 2026 18:35:08 -0400 Subject: [PATCH 22/22] Updates --- .gitignore | 1 + .../StraightFour/Entity/Airplane/Tests.meta | 2 - .../Entity/Base/Scripts/BaseEntity.cs | 30 +++++ .../Character/Materials/SimpleAvatarHead.mat | 3 +- .../Character/Materials/SimpleAvatarTorso.mat | 3 +- .../Entity/Mesh/Scripts/MeshEntity.cs | 1 + .../Environment/Materials/Skybox.mat | 2 +- .../TopLevel/Scenes/MobileRuntime.unity | 125 ++++++++++++++++++ .../XR/Settings/OpenXR Package Settings.asset | 64 ++++----- Packages/packages-lock.json | 9 ++ 10 files changed, 204 insertions(+), 36 deletions(-) delete mode 100644 Assets/Runtime/StraightFour/Entity/Airplane/Tests.meta diff --git a/.gitignore b/.gitignore index 1a756e4c..666a9d05 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,4 @@ Assets/Digger/ Assets/Samples/ Assets/Silantro/ Assets/Runtime/StraightFour/3rd-party/ +.worldkit/ diff --git a/Assets/Runtime/StraightFour/Entity/Airplane/Tests.meta b/Assets/Runtime/StraightFour/Entity/Airplane/Tests.meta deleted file mode 100644 index 59f5c39a..00000000 --- a/Assets/Runtime/StraightFour/Entity/Airplane/Tests.meta +++ /dev/null @@ -1,2 +0,0 @@ -fileFormatVersion: 2 -guid: 42b51bc48c0b7d04fbc18152d3bdfbb8 \ No newline at end of file diff --git a/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs b/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs index f8d0b0d8..10e49ea1 100644 --- a/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Base/Scripts/BaseEntity.cs @@ -94,6 +94,11 @@ public enum InteractionState { Hidden, Static, Physical, Placing } /// private List seats = new List(); + /// + /// Animation names tracked by this entity's animation control methods. + /// + private HashSet definedAnimations = new HashSet(); + /// /// Interaction state of the entity. /// @@ -731,8 +736,10 @@ public virtual bool PlayAnimation(string animationName) AnimationClip clip = animation.GetClip(animationName); if (clip != null) { + definedAnimations.Add(animationName); animation[animationName].weight = 0.1f; animation.Play(animationName); + return true; } } } @@ -764,6 +771,29 @@ public virtual bool StopAnimation(string animationName) return false; } + /// + /// Stop all animations. + /// + /// Whether or not any animations were found. + public virtual bool StopAllAnimations() + { + if (definedAnimations.Count > 0) + { + bool foundAnimation = false; + foreach (string animationName in definedAnimations) + { + if (StopAnimation(animationName)) + { + foundAnimation = true; + } + } + + return foundAnimation; + } + + return false; + } + /// /// Set the speed of an animation. /// diff --git a/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarHead.mat b/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarHead.mat index d14b6891..36ba5e34 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarHead.mat +++ b/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarHead.mat @@ -97,6 +97,7 @@ Material: m_Offset: {x: 0, y: 0} m_Ints: [] m_Floats: + - _AddPrecomputedVelocity: 0 - _AlphaClip: 0 - _AlphaToMask: 0 - _Blend: 0 @@ -129,7 +130,7 @@ Material: - _ZWrite: 1 m_Colors: - _BaseColor: {r: 0, g: 1, b: 0.66944504, a: 1} - - _Color: {r: 0, g: 1, b: 0.66944504, a: 1} + - _Color: {r: 0, g: 1, b: 0.669445, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} m_BuildTextureStacks: [] diff --git a/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarTorso.mat b/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarTorso.mat index 2711394a..5645dcd7 100644 --- a/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarTorso.mat +++ b/Assets/Runtime/StraightFour/Entity/Character/Materials/SimpleAvatarTorso.mat @@ -84,6 +84,7 @@ Material: m_Offset: {x: 0, y: 0} m_Ints: [] m_Floats: + - _AddPrecomputedVelocity: 0 - _AlphaClip: 0 - _AlphaToMask: 0 - _Blend: 0 @@ -116,7 +117,7 @@ Material: - _ZWrite: 1 m_Colors: - _BaseColor: {r: 0, g: 1, b: 0.9905875, a: 1} - - _Color: {r: 0, g: 1, b: 0.9905875, a: 1} + - _Color: {r: 0, g: 1, b: 0.9905874, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} m_BuildTextureStacks: [] diff --git a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs index 5e39db92..6aebe31f 100644 --- a/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs +++ b/Assets/Runtime/StraightFour/Entity/Mesh/Scripts/MeshEntity.cs @@ -369,6 +369,7 @@ public override void Initialize(System.Guid idToSet) MakeHidden(); SetUpHighlightVolume(); + StopAllAnimations(); } /// diff --git a/Assets/Runtime/StraightFour/Environment/Materials/Skybox.mat b/Assets/Runtime/StraightFour/Environment/Materials/Skybox.mat index 2b3d42fc..e11df09f 100644 --- a/Assets/Runtime/StraightFour/Environment/Materials/Skybox.mat +++ b/Assets/Runtime/StraightFour/Environment/Materials/Skybox.mat @@ -132,6 +132,6 @@ Material: - _Color: {r: 1, g: 1, b: 1, a: 1} - _EmissionColor: {r: 0, g: 0, b: 0, a: 1} - _SpecColor: {r: 0.19999996, g: 0.19999996, b: 0.19999996, a: 1} - - _Tint: {r: 0.5019608, g: 0.5019608, b: 0.5019608, a: 0.5019608} + - _Tint: {r: 0.5, g: 0.5, b: 0.5, a: 1} m_BuildTextureStacks: [] m_AllowLocking: 1 diff --git a/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity b/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity index 9baddf75..e91ca612 100644 --- a/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity +++ b/Assets/Runtime/TopLevel/Scenes/MobileRuntime.unity @@ -119,6 +119,108 @@ NavMeshSettings: debug: m_Flags: 0 m_NavMeshData: {fileID: 0} +--- !u!1001 &45003657 +PrefabInstance: + m_ObjectHideFlags: 0 + serializedVersion: 2 + m_Modification: + serializedVersion: 3 + m_TransformParent: {fileID: 0} + m_Modifications: + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_Pivot.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_Pivot.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMax.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMax.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMin.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchorMin.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_SizeDelta.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_SizeDelta.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalPosition.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.w + value: 1 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalRotation.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchoredPosition.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_AnchoredPosition.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalEulerAnglesHint.x + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalEulerAnglesHint.y + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4011543431373168956, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_LocalEulerAnglesHint.z + value: 0 + objectReference: {fileID: 0} + - target: {fileID: 4071536258802031107, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + propertyPath: m_Name + value: TabUIWebViewPrefab + objectReference: {fileID: 0} + m_RemovedComponents: [] + m_RemovedGameObjects: [] + m_AddedGameObjects: [] + m_AddedComponents: [] + m_SourcePrefab: {fileID: 100100000, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} +--- !u!1 &45003658 stripped +GameObject: + m_CorrespondingSourceObject: {fileID: 4071536258802031107, guid: 7cc144b3038d1ba43be354d7a45820e9, type: 3} + m_PrefabInstance: {fileID: 45003657} + m_PrefabAsset: {fileID: 0} --- !u!1 &167713673 GameObject: m_ObjectHideFlags: 0 @@ -2166,6 +2268,7 @@ MonoBehaviour: stateSettings: {fileID: 11400000, guid: 3404c49a4170ad94e87f0e8545b86758, type: 2} airplaneEntityPrefab: {fileID: 2546332423327978143, guid: 9f84e1b9b912d9b4db6c879e27fa1ff8, type: 3} highlightMaterial: {fileID: 2100000, guid: fb72f213bae4057419539525b597f16d, type: 2} + previewMaterial: {fileID: 0} skyMaterial: {fileID: 2100000, guid: 0aade801f5d375245b8bd2843b11a686, type: 2} liteProceduralSkyMaterial: {fileID: 2100000, guid: acf78ab0ce284f148a485a21087ea77a, type: 2} liteProceduralSkyObject: {fileID: 1137534123} @@ -2197,6 +2300,7 @@ MonoBehaviour: prismMeshPrefab: {fileID: 7480807262433990556, guid: e4ea5490454ef8a4c9f704fccd459975, type: 3} archMeshPrefab: {fileID: 2347241635205284074, guid: 4965664f8d365f146b2afbe321a24139, type: 3} reflectionProbe: {fileID: 839982476} + resourceCleanupInterval: 60 --- !u!4 &472491404 Transform: m_ObjectHideFlags: 0 @@ -2799,6 +2903,7 @@ GameObject: serializedVersion: 6 m_Component: - component: {fileID: 1898133185} + - component: {fileID: 1898133186} m_Layer: 0 m_Name: MobileInterface m_TagString: Untagged @@ -2827,6 +2932,25 @@ Transform: - {fileID: 6574365494872069928} m_Father: {fileID: 0} m_LocalEulerAnglesHint: {x: 0, y: 0, z: 0} +--- !u!114 &1898133186 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 1898133184} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 781ffc189cb8d1a4181bf4e946009b29, type: 3} + m_Name: + m_EditorClassIdentifier: + runtime: {fileID: 472491403} + tabUIWebViewPrefab: {fileID: 45003658} + vrParent: {fileID: 0} + vrCamera: {fileID: 0} + maxTabs: 10 + maxSnapshotMemoryMB: 100 + homeUrl: --- !u!114 &62904377632733555 MonoBehaviour: m_ObjectHideFlags: 0 @@ -4334,3 +4458,4 @@ SceneRoots: - {fileID: 839982477} - {fileID: 1137534127} - {fileID: 167713676} + - {fileID: 45003657} diff --git a/Assets/XR/Settings/OpenXR Package Settings.asset b/Assets/XR/Settings/OpenXR Package Settings.asset index c84999bb..8a930c6f 100644 --- a/Assets/XR/Settings/OpenXR Package Settings.asset +++ b/Assets/XR/Settings/OpenXR Package Settings.asset @@ -33,6 +33,7 @@ MonoBehaviour: m_Name: MetaXRFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: Meta XR Feature version: 0.0.1 featureIdInternal: com.meta.openxr.feature.metaxr @@ -82,7 +83,6 @@ MonoBehaviour: m_Name: Standalone m_EditorClassIdentifier: features: - - {fileID: -7441576640521792224} - {fileID: -4251221511889710573} - {fileID: -8283934257360705991} - {fileID: -3043467695985465025} @@ -95,20 +95,6 @@ MonoBehaviour: - {fileID: -4233259048951607741} - {fileID: -2467792937570659600} - {fileID: -3907765551054486986} - - {fileID: -7296088344262875955} - - {fileID: 6770610698521954227} - - {fileID: -6477775478286594775} - - {fileID: -7222906391444432655} - - {fileID: 6575429890421189379} - - {fileID: 5913630344976663486} - - {fileID: -7313330577548468968} - - {fileID: 1158942647253497871} - - {fileID: 8492585916147005112} - - {fileID: 7295433065535495109} - - {fileID: 714606220454857056} - - {fileID: -3744789857744238503} - - {fileID: 1690885375729658985} - - {fileID: 9123467854060196379} - {fileID: -6322854048430258810} - {fileID: -2804446222640820088} - {fileID: 7243270749764318523} @@ -238,6 +224,7 @@ MonoBehaviour: m_Name: OpenXRLifeCycleFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: version: 0.1.0 featureIdInternal: MetaOpenXR-OpenXRLifeCycle @@ -278,6 +265,7 @@ MonoBehaviour: m_Name: OpenXRLifeCycleFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: version: 0.1.0 featureIdInternal: MetaOpenXR-OpenXRLifeCycle @@ -298,6 +286,7 @@ MonoBehaviour: m_Name: AROcclusionFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Occlusion' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-occlusion @@ -339,6 +328,7 @@ MonoBehaviour: m_Name: ARAnchorFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Anchors' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-anchor @@ -359,6 +349,7 @@ MonoBehaviour: m_Name: ARCameraFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Camera (Passthrough)' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-camera @@ -379,6 +370,7 @@ MonoBehaviour: m_Name: ARBoundingBoxFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Bounding Boxes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-bounding-boxes @@ -523,6 +515,7 @@ MonoBehaviour: m_Name: BoundaryVisibilityFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Boundary Visibility' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-boundary-visibility @@ -530,6 +523,7 @@ MonoBehaviour: company: Unity Technologies priority: 0 required: 0 + k__BackingField: 0 --- !u!114 &-5415651528169704530 MonoBehaviour: m_ObjectHideFlags: 0 @@ -565,6 +559,7 @@ MonoBehaviour: m_Name: ARRaycastFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Raycasts' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-raycast @@ -585,6 +580,7 @@ MonoBehaviour: m_Name: DisplayUtilitiesFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Display Utilities' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-display-utilities @@ -625,6 +621,7 @@ MonoBehaviour: m_Name: MetaXREyeTrackedFoveationFeature Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Eye Tracked Foveation version: 0.0.1 featureIdInternal: com.meta.openxr.feature.eyetrackedfoveation @@ -646,6 +643,7 @@ MonoBehaviour: m_Name: ARSessionFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Session' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-session @@ -726,6 +724,7 @@ MonoBehaviour: m_Name: ARCameraFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Camera (Passthrough)' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-camera @@ -766,6 +765,7 @@ MonoBehaviour: m_Name: MetaXRFeature Standalone m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: Meta XR Feature version: 0.0.1 featureIdInternal: com.meta.openxr.feature.metaxr @@ -978,6 +978,7 @@ MonoBehaviour: m_Name: MetaXRSubsampledLayout Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Subsampled Layout version: 0.0.1 featureIdInternal: com.meta.openxr.feature.subsampledLayout @@ -1018,6 +1019,7 @@ MonoBehaviour: m_Name: ARBoundingBoxFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Bounding Boxes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-bounding-boxes @@ -1095,6 +1097,7 @@ MonoBehaviour: m_Name: MetaXREyeTrackedFoveationFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Eye Tracked Foveation version: 0.0.1 featureIdInternal: com.meta.openxr.feature.eyetrackedfoveation @@ -1116,6 +1119,7 @@ MonoBehaviour: m_Name: ARAnchorFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Anchors' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-anchor @@ -1136,6 +1140,7 @@ MonoBehaviour: m_Name: ARPlaneFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Planes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-plane @@ -1156,6 +1161,7 @@ MonoBehaviour: m_Name: MetaXRFoveationFeature Standalone m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: Meta XR Foveation version: 1.0.0 featureIdInternal: com.meta.openxr.feature.foveation @@ -1176,6 +1182,7 @@ MonoBehaviour: m_Name: ARMeshFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Meshing' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-mesh @@ -1344,6 +1351,7 @@ MonoBehaviour: m_Name: AROcclusionFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Occlusion' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-occlusion @@ -1405,6 +1413,7 @@ MonoBehaviour: m_Name: ARPlaneFeature Android m_EditorClassIdentifier: m_enabled: 1 + k__BackingField: 0 nameUi: 'Meta Quest: Planes' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-plane @@ -1425,6 +1434,7 @@ MonoBehaviour: m_Name: MetaXRFoveationFeature Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Foveation version: 1.0.0 featureIdInternal: com.meta.openxr.feature.foveation @@ -1445,6 +1455,7 @@ MonoBehaviour: m_Name: ARMeshFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Meshing' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-mesh @@ -1466,6 +1477,7 @@ MonoBehaviour: m_Name: DisplayUtilitiesFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Display Utilities' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-display-utilities @@ -1486,6 +1498,7 @@ MonoBehaviour: m_Name: BoundaryVisibilityFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Boundary Visibility' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.meta-boundary-visibility @@ -1493,6 +1506,7 @@ MonoBehaviour: company: Unity Technologies priority: 0 required: 0 + k__BackingField: 0 --- !u!114 &7146165264086680457 MonoBehaviour: m_ObjectHideFlags: 0 @@ -1560,6 +1574,7 @@ MonoBehaviour: m_Name: ARSessionFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Session' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-session @@ -1600,7 +1615,6 @@ MonoBehaviour: m_Name: Android m_EditorClassIdentifier: features: - - {fileID: -7633581328313503459} - {fileID: 4674697878492794949} - {fileID: 7146165264086680457} - {fileID: -2340439152450337852} @@ -1612,21 +1626,6 @@ MonoBehaviour: - {fileID: 4436760230863042719} - {fileID: -5909640409290506278} - {fileID: -4594511983156627525} - - {fileID: 1085581369992130134} - - {fileID: -5643199017387485149} - - {fileID: -803836931037024053} - - {fileID: -4136287910980941023} - - {fileID: -4844709515900620156} - - {fileID: 2486025960671627241} - - {fileID: 4588223029664240611} - - {fileID: 5230357031813526908} - - {fileID: -5273211353462809153} - - {fileID: -4389619210768067104} - - {fileID: -4539300325892458710} - - {fileID: -8917394965726186233} - - {fileID: 5562653971502655839} - - {fileID: 8005351155306599530} - - {fileID: -1428233723045200180} - {fileID: 8316444451361848473} - {fileID: -2379692420766146042} - {fileID: -2367121910693502146} @@ -1658,6 +1657,7 @@ MonoBehaviour: m_Name: MetaXRSpaceWarp Android m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Space Warp version: 1.0.0 featureIdInternal: com.meta.openxr.feature.spacewarp @@ -1698,6 +1698,7 @@ MonoBehaviour: m_Name: ARRaycastFeature Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: 'Meta Quest: Raycasts' version: 0.1.0 featureIdInternal: com.unity.openxr.feature.arfoundation-meta-raycast @@ -1718,6 +1719,7 @@ MonoBehaviour: m_Name: MetaXRSubsampledLayout Standalone m_EditorClassIdentifier: m_enabled: 0 + k__BackingField: 0 nameUi: Meta XR Subsampled Layout version: 0.0.1 featureIdInternal: com.meta.openxr.feature.subsampledLayout diff --git a/Packages/packages-lock.json b/Packages/packages-lock.json index 281bebde..9142e2bf 100644 --- a/Packages/packages-lock.json +++ b/Packages/packages-lock.json @@ -36,6 +36,15 @@ "com.tivadar.best.websockets": "3.0.0" } }, + "com.tivadar.best.socketio": { + "version": "file:com.tivadar.best.socketio", + "depth": 0, + "source": "embedded", + "dependencies": { + "com.tivadar.best.http": "3.0.0", + "com.tivadar.best.websockets": "3.0.0" + } + }, "com.tivadar.best.websockets": { "version": "file:com.tivadar.best.websockets", "depth": 0,