From 9a3708667f645d7fb1b2174544b480cdbdddba73 Mon Sep 17 00:00:00 2001 From: mcarlson Date: Sun, 14 Sep 2025 10:15:46 +0800 Subject: [PATCH 01/29] now supports multiple simulators, controller navigation tab has simulator menu, qr code urls set simulator to correspond with each simulator, user can switch controller's simulator with menu, menu updates when new simulators attach and leave. (#53) (#55) Co-authored-by: Don Hopkins --- .../Resources/Prefabs/GroundPlane.prefab | 4 +- .../Assets/Scripts/Core/Brewster.cs | 14 +- .../Assets/Scripts/Core/SpaceCraft.cs | 62 +-- .../Assets/Scripts/Views/CollectionsView.cs | 18 +- .../Schemas/Generated/CollectionSchema.cs | 16 +- .../Views/Schemas/Generated/ItemSchema.cs | 22 +- .../StreamingAssets/SpaceCraft/spacecraft.js | 413 +++++++++++------- WebSites/SpaceCraft/Build/SpaceCraft.data | 4 +- .../SpaceCraft/Build/SpaceCraft.framework.js | 18 +- .../SpaceCraft/Build/SpaceCraft.loader.js | 2 +- WebSites/SpaceCraft/Build/SpaceCraft.wasm | 4 +- .../StreamingAssets/SpaceCraft/spacecraft.js | 413 +++++++++++------- WebSites/controller/build/SimulatorState.d.ts | 2 - .../controller/build/SimulatorState.d.ts.map | 2 +- WebSites/controller/build/SimulatorState.js | 8 - .../controller/build/SimulatorState.js.map | 2 +- .../controller/build/SpacetimeController.d.ts | 5 +- .../build/SpacetimeController.d.ts.map | 2 +- .../controller/build/SpacetimeController.js | 150 ++++++- .../build/SpacetimeController.js.map | 2 +- WebSites/controller/build/TabNavigate.d.ts | 1 + .../controller/build/TabNavigate.d.ts.map | 2 +- WebSites/controller/build/TabNavigate.js | 42 +- WebSites/controller/build/TabNavigate.js.map | 2 +- WebSites/controller/src/SimulatorState.ts | 8 - .../controller/src/SpacetimeController.ts | 148 ++++++- WebSites/controller/src/TabNavigate.ts | 45 +- 27 files changed, 931 insertions(+), 480 deletions(-) diff --git a/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab b/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab index 08070dda..5402b6bb 100644 --- a/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab +++ b/Unity/SpaceCraft/Assets/Resources/Prefabs/GroundPlane.prefab @@ -49,8 +49,8 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1019336410923223640} - m_Enabled: 0 - m_CastShadows: 1 + m_Enabled: 1 + m_CastShadows: 0 m_ReceiveShadows: 1 m_DynamicOccludee: 1 m_StaticShadowCaster: 0 diff --git a/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs b/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs index 9aac2ff8..70ff903e 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Core/Brewster.cs @@ -96,7 +96,7 @@ public void LoadContentFromJson(JObject content) return; } - Debug.Log("Brewster: Starting to load content from JSON"); + // Debug.Log("Brewster: Starting to load content from JSON"); //Debug.Log($"Brewster: Content JSON structure: {content}"); // Clear existing content first @@ -144,24 +144,24 @@ public void LoadContentFromJson(JObject content) Collection collection = ScriptableObject.CreateInstance(); // Debug the JSON before import - Debug.Log($"Brewster: About to import collection '{collectionId}' from JSON: {collectionJson}"); + // Debug.Log($"Brewster: About to import collection '{collectionId}' from JSON: {collectionJson}"); // Import from the nested collection object collection.ImportFromJToken(collectionJson); // Debug the collection after import - Debug.Log($"Brewster: After import - collection ID='{collection.Id}', Title='{collection.Title}'"); + // Debug.Log($"Brewster: After import - collection ID='{collection.Id}', Title='{collection.Title}'"); // Collection should already have ID from ImportFromJToken, // but if not, use the collection ID from the JSON key if (string.IsNullOrEmpty(collection.Id)) { - Debug.Log($"Brewster: Setting explicit ID for collection '{collectionId}' as it was not found in metadata"); + // Debug.Log($"Brewster: Setting explicit ID for collection '{collectionId}' as it was not found in metadata"); collection.Id = collectionId; } _collections[collectionId] = collection; - Debug.Log($"Brewster: Successfully added collection '{collectionId}' with ID '{collection.Id}'"); + // Debug.Log($"Brewster: Successfully added collection '{collectionId}' with ID '{collection.Id}'"); // Check for itemsIndex in the collection JArray itemsIndex = collectionDir["itemsIndex"] as JArray; @@ -238,7 +238,7 @@ public void LoadContentFromJson(JObject content) // If no ID from JSON, use the key as ID if (string.IsNullOrEmpty(item.Id)) { - Debug.Log($"Brewster: Setting explicit ID for item '{itemId}' as it was not found in metadata"); + // Debug.Log($"Brewster: Setting explicit ID for item '{itemId}' as it was not found in metadata"); item.Id = itemId; } @@ -256,7 +256,7 @@ public void LoadContentFromJson(JObject content) } // Notify that content is loaded - Debug.Log($"Brewster: Content loading complete. Loaded {_collections.Count} collections and {_items.Count} items from provided JSON."); + // Debug.Log($"Brewster: Content loading complete. Loaded {_collections.Count} collections and {_items.Count} items from provided JSON."); // Fire the event to notify all listeners OnAllContentLoaded?.Invoke(); diff --git a/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs b/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs index 6ea10f15..b31ce516 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Core/SpaceCraft.cs @@ -74,10 +74,10 @@ public List SelectedItemIds HashSet newSet = new HashSet(value ?? new List()); // Handle null input if (!currentSet.SetEquals(newSet)) { - Debug.Log($"[SpaceCraft] Setter: Updating SelectedItemIds (Count: {newSet.Count})"); + // Debug.Log($"[SpaceCraft] Setter: Updating SelectedItemIds (Count: {newSet.Count})"); _selectedItemIds = new List(newSet); // Store a copy defensively selectedItemsChanged = true; // Set flag because state list changed - Debug.Log($"[SpaceCraft] Setter: SelectedItemIds updated to {string.Join(", ", _selectedItemIds)}"); + // Debug.Log($"[SpaceCraft] Setter: SelectedItemIds updated to {string.Join(", ", _selectedItemIds)}"); UpdateSelectionVisuals(); } } @@ -95,10 +95,10 @@ public List HighlightedItemIds var newValue = value ?? new List(); if (!AreEquivalentHighlightLists(_highlightedItemIds, newValue)) { - Debug.Log($"[SpaceCraft] Setter: Updating HighlightedItemIds (Count: {newValue.Count})"); + // Debug.Log($"[SpaceCraft] Setter: Updating HighlightedItemIds (Count: {newValue.Count})"); _highlightedItemIds = new List(newValue); // Store a copy highlightedItemsChanged = true; // Set flag because state list changed - Debug.Log($"[SpaceCraft] Setter: HighlightedItemIds updated to {string.Join(", ", _highlightedItemIds)}"); + // Debug.Log($"[SpaceCraft] Setter: HighlightedItemIds updated to {string.Join(", ", _highlightedItemIds)}"); UpdateHighlightVisuals(); } } @@ -340,18 +340,18 @@ private void FixedUpdate() string firstItemId = collectionsView.GetFirstDisplayedItemId(); if (!string.IsNullOrEmpty(firstItemId)) { - Debug.Log($"[SpaceCraft] Automatically selecting first item: {firstItemId}"); + // Debug.Log($"[SpaceCraft] Automatically selecting first item: {firstItemId}"); if (SelectedItemIds.Count > 0) { SelectedItemIds.Clear(); selectedItemsChanged = true; - Debug.Log($"[SpaceCraft] Setter: SelectedItemIds cleared to {string.Join(", ", SelectedItemIds)}"); + // Debug.Log($"[SpaceCraft] Setter: SelectedItemIds cleared to {string.Join(", ", SelectedItemIds)}"); } SelectItem("auto_select", "System", firstItemId); } else { - Debug.Log("[SpaceCraft] No first item found to select automatically."); + // Debug.Log("[SpaceCraft] No first item found to select automatically."); } } @@ -429,7 +429,7 @@ public void SelectItem(string clientId, string clientName, string itemId) { if (string.IsNullOrEmpty(itemId)) return; // Log client info - Debug.Log($"[SpaceCraft] SelectItem API called by {clientName}({clientId}): {itemId}"); + // Debug.Log($"[SpaceCraft] SelectItem API called by {clientName}({clientId}): {itemId}"); List newList; // Handle single selection mode @@ -461,7 +461,7 @@ public void DeselectItem(string clientId, string clientName, string itemId) { if (string.IsNullOrEmpty(itemId) || !_selectedItemIds.Contains(itemId)) return; // Log client info - Debug.Log($"[SpaceCraft] DeselectItem API called by {clientName}({clientId}): {itemId}"); + // Debug.Log($"[SpaceCraft] DeselectItem API called by {clientName}({clientId}): {itemId}"); // Create a new list without the item List newList = new List(_selectedItemIds); @@ -489,7 +489,7 @@ public void DeselectItem(string clientId, string clientName, string itemId) /// public void DeselectAllItems() { - Debug.Log("[SpaceCraft] DeselectAllItems API called"); + // Debug.Log("[SpaceCraft] DeselectAllItems API called"); if (_selectedItemIds.Count > 0) { // Assign an empty list to the property @@ -548,17 +548,17 @@ public void ToggleCurrentItemSelection(string clientId, string clientName, strin public void ToggleItemSelection(string clientId, string clientName, string screenId, string itemId) { if (string.IsNullOrEmpty(itemId)) return; - Debug.Log($"[SpaceCraft] ToggleItemSelection API called by clientId: {clientId}, clientName: {clientName}, screenId: {screenId}, itemId: {itemId}"); + // Debug.Log($"[SpaceCraft] ToggleItemSelection API called by clientId: {clientId}, clientName: {clientName}, screenId: {screenId}, itemId: {itemId}"); List newList = new List(_selectedItemIds); if (newList.Contains(itemId)) { // Item is selected, so deselect it - Debug.Log($"[SpaceCraft] Toggle: Deselecting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Deselecting {itemId}"); newList.Remove(itemId); } else { // Item is not selected, so select it - Debug.Log($"[SpaceCraft] Toggle: Selecting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Selecting {itemId}"); if (!multiSelectEnabled) { // Single select mode: clear list before adding @@ -591,7 +591,7 @@ public void SetSelectedItems(string clientId, string clientName, List it { List newList = itemIds ?? new List(); // Log client info - Debug.Log($"[SpaceCraft] SetSelectedItems API called by {clientName}({clientId}) with {newList.Count} items."); + // Debug.Log($"[SpaceCraft] SetSelectedItems API called by {clientName}({clientId}) with {newList.Count} items."); // Enforce single selection mode if active if (!multiSelectEnabled && newList.Count > 1) @@ -621,7 +621,7 @@ public void AddSelectedItems(string clientId, string clientName, List it { if (itemIds == null || itemIds.Count == 0) return; // Log client info - Debug.Log($"[SpaceCraft] AddSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] AddSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); // Handle single select mode separately if (!multiSelectEnabled) @@ -662,7 +662,7 @@ public void RemoveSelectedItems(string clientId, string clientName, List { if (itemIds == null || itemIds.Count == 0 || _selectedItemIds.Count == 0) return; // Log client info - Debug.Log($"[SpaceCraft] RemoveSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] RemoveSelectedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); List newList = new List(_selectedItemIds); bool changed = false; @@ -696,7 +696,7 @@ public void RemoveSelectedItems(string clientId, string clientName, List public void SetMultiSelectMode(bool enable) { if (inputManager == null) return; - Debug.Log($"[SpaceCraft] SetMultiSelectMode called: {enable}"); + // Debug.Log($"[SpaceCraft] SetMultiSelectMode called: {enable}"); if (multiSelectEnabled != enable) { @@ -737,7 +737,7 @@ public int GetHighlightCount(string itemId) /// public void SetHighlightedItems(string clientId, string clientName, List itemIds) { - Debug.Log($"[SpaceCraft] SetHighlightedItems API called by {clientName}({clientId}) with {itemIds?.Count ?? 0} items."); + // Debug.Log($"[SpaceCraft] SetHighlightedItems API called by {clientName}({clientId}) with {itemIds?.Count ?? 0} items."); // Assign the new list to the property to trigger updates if different HighlightedItemIds = itemIds ?? new List(); @@ -749,7 +749,7 @@ public void SetHighlightedItems(string clientId, string clientName, List public void AddHighlightedItems(string clientId, string clientName, List itemIds) { if (itemIds == null || itemIds.Count == 0) return; - Debug.Log($"[SpaceCraft] AddHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] AddHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); // Create new list, add items, and assign back to property List newList = new List(HighlightedItemIds); @@ -763,7 +763,7 @@ public void AddHighlightedItems(string clientId, string clientName, List public void RemoveHighlightedItems(string clientId, string clientName, List itemIds) { if (itemIds == null || itemIds.Count == 0 || HighlightedItemIds.Count == 0) return; - Debug.Log($"[SpaceCraft] RemoveHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); + // Debug.Log($"[SpaceCraft] RemoveHighlightedItems API called by {clientName}({clientId}) with {itemIds.Count} items."); // Create new list and remove one instance of each specified item List newList = new List(HighlightedItemIds); @@ -972,7 +972,7 @@ private ItemView FindItemSafe(string itemId) ItemView itemView = FindItemViewById(itemId); // Call local method if (itemView == null) { - Debug.LogWarning($"[SpaceCraft] FindItemSafe: Item with ID '{itemId}' not found via CollectionsView."); + // Debug.LogWarning($"[SpaceCraft] FindItemSafe: Item with ID '{itemId}' not found via CollectionsView."); } return itemView; @@ -997,7 +997,7 @@ public void UpdateSelectionVisuals() { if (inputManager == null) return; - Debug.Log($"[SpaceCraft] Updating selection visuals for {SelectedItemIds.Count} selected items."); + // Debug.Log($"[SpaceCraft] Updating selection visuals for {SelectedItemIds.Count} selected items."); List allItemViews = inputManager.GetAllItemViews(); foreach (ItemView view in allItemViews) @@ -1051,7 +1051,7 @@ public void UpdateHighlightVisuals() { if (inputManager == null) return; - Debug.Log($"[SpaceCraft] Updating highlight visuals for {HighlightedItemIds.Count} total highlights."); + // Debug.Log($"[SpaceCraft] Updating highlight visuals for {HighlightedItemIds.Count} total highlights."); List allItemViews = inputManager.GetAllItemViews(); // Calculate counts first @@ -1082,15 +1082,15 @@ private void SendEvents() // Send events only when changes have occurred - reduce bridge traffic if (selectedItemsChanged) { - Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); + // Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); SendEventName("SelectedItemsChanged"); selectedItemsChanged = false; // Reset flag here - Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); + // Debug.Log("SpaceCraft: SelectedItemsChanged: selectedItemIds: " + string.Join(",", SelectedItemIds)); } if (highlightedItemsChanged) { - Debug.Log("SpaceCraft: HighlightedItemsChanged: highlightedItemIds: " + string.Join(",", HighlightedItemIds)); + // Debug.Log("SpaceCraft: HighlightedItemsChanged: highlightedItemIds: " + string.Join(",", HighlightedItemIds)); SendEventName("HighlightedItemsChanged"); highlightedItemsChanged = false; // Reset flag here } @@ -1129,17 +1129,17 @@ public void OnItemHighlightEnd(ItemView item) public void ToggleItemHighlight(string controllerId, string controllerName, string screenId, string itemId) { if (string.IsNullOrEmpty(itemId)) return; - Debug.Log($"[SpaceCraft] ToggleItemHighlight API called by controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, itemId: {itemId}"); + // Debug.Log($"[SpaceCraft] ToggleItemHighlight API called by controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, itemId: {itemId}"); List newList = new List(HighlightedItemIds); if (newList.Contains(itemId)) { // Item is highlighted, so unhighlight it - Debug.Log($"[SpaceCraft] Toggle: Unhighlighting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Unhighlighting {itemId}"); newList.Remove(itemId); } else { // Item is not highlighted, so highlight it - Debug.Log($"[SpaceCraft] Toggle: Highlighting {itemId}"); + // Debug.Log($"[SpaceCraft] Toggle: Highlighting {itemId}"); newList.Add(itemId); } // Assign the potentially modified list back to the property @@ -1152,7 +1152,7 @@ public void ToggleItemHighlight(string controllerId, string controllerName, stri public void MoveSelection(string controllerId, string controllerName, string screenId, string direction, float dx = 0f, float dy = 0f) { // Expects "north", "south", "east", "west" from controller - Debug.Log($"[SpaceCraft] MoveSelection called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}, dx: {dx}, dy: {dy}"); + // Debug.Log($"[SpaceCraft] MoveSelection called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}, dx: {dx}, dy: {dy}"); // Pass direction and mouse deltas to CollectionsView collectionsView?.MoveSelection(controllerId, controllerName, direction, dx, dy); // Note: CollectionsView will call SelectItem which will sync the highlight @@ -1164,7 +1164,7 @@ public void MoveSelection(string controllerId, string controllerName, string scr public void MoveHighlight(string controllerId, string controllerName, string screenId, string direction) { // Expects "north", "south", "east", "west" from controller - Debug.Log($"[SpaceCraft] MoveHighlight called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}"); + // Debug.Log($"[SpaceCraft] MoveHighlight called with controllerId: {controllerId}, controllerName: {controllerName}, screenId: {screenId}, direction: {direction}"); // Pass direction directly to CollectionsView collectionsView?.MoveHighlight(controllerId, controllerName, direction); } diff --git a/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs b/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs index db60ed8f..362c2a57 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Views/CollectionsView.cs @@ -35,7 +35,7 @@ private void Start() } else { - Debug.Log($"CollectionsView: itemInfoPanel is assigned: {itemInfoPanel.name}"); + // Debug.Log($"CollectionsView: itemInfoPanel is assigned: {itemInfoPanel.name}"); } // Ensure Brewster instance exists @@ -115,7 +115,7 @@ public void DisplayAllCollections() } } - Debug.Log($"CollectionsView: Displayed {collectionViews.Count} collections."); + // Debug.Log($"CollectionsView: Displayed {collectionViews.Count} collections."); } /// @@ -177,7 +177,7 @@ private CollectionView CreateCollectionView(Collection collection) /// private void HandleAllContentLoaded() { - Debug.Log("CollectionsView: Received OnAllContentLoaded event."); + // Debug.Log("CollectionsView: Received OnAllContentLoaded event."); // Hide details panel initially HideItemDetails(); @@ -215,7 +215,7 @@ private void UpdateDetailPanel() } else { - Debug.LogWarning($"CollectionsView: UpdateDetailPanel - Could not find ItemView for selected id: {selectedId}"); + // Debug.LogWarning($"CollectionsView: UpdateDetailPanel - Could not find ItemView for selected id: {selectedId}"); } } } @@ -227,7 +227,7 @@ private void UpdateDetailPanel() if (currentDisplayedItem != itemToDisplay) { currentDisplayedItem = itemToDisplay; - Debug.Log($"CollectionsView: UpdateDetailPanel - Displaying item details for: {itemToDisplay.Title}"); + // Debug.Log($"CollectionsView: UpdateDetailPanel - Displaying item details for: {itemToDisplay.Title}"); DisplayItemDetails(itemToDisplay); } } @@ -237,7 +237,7 @@ private void UpdateDetailPanel() if (currentDisplayedItem != null) { currentDisplayedItem = null; - Debug.Log("CollectionsView: UpdateDetailPanel - Hiding item details (no item to display)"); + // Debug.Log("CollectionsView: UpdateDetailPanel - Hiding item details (no item to display)"); HideItemDetails(); } } @@ -269,12 +269,12 @@ public void DisplayItemDetails(Item item) if (itemView != null && !string.IsNullOrEmpty(itemView.collectionId)) { Collection collection = Brewster.Instance.GetCollection(itemView.collectionId); - Debug.Log($"CollectionsView: Retrieved collection with ID='{collection?.Id}', Title='{collection?.Title}', Type={collection?.GetType().Name}"); + // Debug.Log($"CollectionsView: Retrieved collection with ID='{collection?.Id}', Title='{collection?.Title}', Type={collection?.GetType().Name}"); if (collection != null) { - Debug.Log($"CollectionsView: About to format title - collection.Title='{collection.Title}', item.Title='{item.Title}'"); + // Debug.Log($"CollectionsView: About to format title - collection.Title='{collection.Title}', item.Title='{item.Title}'"); displayTitle = $"{collection.Title}\n{item.Title}"; - Debug.Log($"CollectionsView: Final formatted displayTitle='{displayTitle}'"); + // Debug.Log($"CollectionsView: Final formatted displayTitle='{displayTitle}'"); } else { diff --git a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs index b1419fd3..95daff53 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/CollectionSchema.cs @@ -117,7 +117,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'id' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: id is null" ); + // Debug.Log($"ItemSchema: id is null" ); } // Use converter: StringOrNullToStringConverter @@ -132,7 +132,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'title' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: title is null" ); + // Debug.Log($"ItemSchema: title is null" ); } // Use converter: StringArrayOrStringOrNullToStringConverter @@ -147,7 +147,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'description' with StringArrayOrStringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: description is null" ); + // Debug.Log($"ItemSchema: description is null" ); } // Use converter: StringOrNullToStringConverter @@ -162,7 +162,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'creator' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: creator is null" ); + // Debug.Log($"ItemSchema: creator is null" ); } // Use converter: SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter @@ -177,7 +177,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'subject' with SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: subject is null" ); + // Debug.Log($"ItemSchema: subject is null" ); } // Use converter: StringOrNullToStringConverter @@ -192,7 +192,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'mediatype' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: mediatype is null" ); + // Debug.Log($"ItemSchema: mediatype is null" ); } // Use converter: StringOrNullToStringConverter @@ -207,7 +207,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverImage' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverImage is null" ); + // Debug.Log($"ItemSchema: coverImage is null" ); } // Use converter: StringOrNullToStringConverter @@ -222,7 +222,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'query' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: query is null" ); + // Debug.Log($"ItemSchema: query is null" ); } } diff --git a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs index 19d54a3f..156550f1 100644 --- a/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs +++ b/Unity/SpaceCraft/Assets/Scripts/Views/Schemas/Generated/ItemSchema.cs @@ -141,7 +141,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'id' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: id is null" ); + // Debug.Log($"ItemSchema: id is null" ); } // Use converter: StringOrNullToStringConverter @@ -156,7 +156,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'title' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: title is null" ); + // Debug.Log($"ItemSchema: title is null" ); } // Use converter: StringArrayOrStringOrNullToStringConverter @@ -171,7 +171,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'description' with StringArrayOrStringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: description is null" ); + // Debug.Log($"ItemSchema: description is null" ); } // Use converter: StringArrayOrStringOrNullToStringConverter @@ -186,7 +186,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'creator' with StringArrayOrStringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: creator is null" ); + // Debug.Log($"ItemSchema: creator is null" ); } // Use converter: SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter @@ -201,7 +201,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'subject' with SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: subject is null" ); + // Debug.Log($"ItemSchema: subject is null" ); } // Use converter: SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter @@ -216,7 +216,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'tags' with SemicolonSplitStringOrStringArrayOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: tags is null" ); + // Debug.Log($"ItemSchema: tags is null" ); } // Use converter: StringArrayOrStringOrNullToStringArrayConverter @@ -231,7 +231,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'collection' with StringArrayOrStringOrNullToStringArrayConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: collection is null" ); + // Debug.Log($"ItemSchema: collection is null" ); } // Use converter: StringOrNullToStringConverter @@ -246,7 +246,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'mediatype' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: mediatype is null" ); + // Debug.Log($"ItemSchema: mediatype is null" ); } // Use converter: StringOrNullToStringConverter @@ -261,7 +261,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverImage' with StringOrNullToStringConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverImage is null" ); + // Debug.Log($"ItemSchema: coverImage is null" ); } // Use converter: StringOrNumberOrNullToIntegerConverter @@ -276,7 +276,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverWidth' with StringOrNumberOrNullToIntegerConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverWidth is null" ); + // Debug.Log($"ItemSchema: coverWidth is null" ); } // Use converter: StringOrNumberOrNullToIntegerConverter @@ -291,7 +291,7 @@ protected override void ImportKnownProperties(JObject json) } catch (Exception ex) { Debug.LogError($"Error converting 'coverHeight' with StringOrNumberOrNullToIntegerConverter: {ex.Message}"); } } else { - Debug.Log($"ItemSchema: coverHeight is null" ); + // Debug.Log($"ItemSchema: coverHeight is null" ); } } diff --git a/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js b/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js index 12a8fcf3..c9ef6b1e 100644 --- a/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js +++ b/Unity/SpaceCraft/Assets/StreamingAssets/SpaceCraft/spacecraft.js @@ -71,10 +71,6 @@ // - action can be "tap" or directional ("north","south","east","west","up","down") // - "tap" applies a scale impulse to the highlighted item // - Directions call MoveHighlight -// 'broadcast' { event: 'simulatorTakeover' }: -// - {newSimulatorId, newSimulatorName, startTime} -// - Signals that a new simulator has taken control -// - Used to manage multiple simulator instances // // --- PRESENCE EVENTS (Client connection tracking) --- // 'presence' { event: 'sync' }: Full state of all connected clients @@ -116,6 +112,7 @@ class SpaceCraftSim { static deepIndexPath = 'StreamingAssets/Content/index-deep.json'; static controllerHtmlPath = '../controller/'; static clientChannelName = 'spacecraft'; + static simulatorNamePrefix = 'SpaceCraft'; /** * Get the channel name from URL query parameter or use default @@ -124,7 +121,10 @@ class SpaceCraftSim { static getChannelName() { const urlParams = new URLSearchParams(window.location.search); const channelFromUrl = urlParams.get('channel'); - return channelFromUrl || this.clientChannelName; + if (channelFromUrl) return channelFromUrl; + // Fallback: use the host name of the web page URL instead of the hard-coded default + const host = window.location && window.location.hostname; + return host; } /** @@ -138,7 +138,8 @@ class SpaceCraftSim { this.identity = { clientId: this.clientId, // Unique ID for this simulator instance clientType: "simulator", // Fixed type for simulator - clientName: "SpaceCraft Simulator", // Human-readable name + clientName: SpaceCraftSim.simulatorNamePrefix, // Start without a number; will assign on presence + simulatorIndex: 0, startTime: Date.now() // When this simulator instance started }; @@ -158,6 +159,8 @@ class SpaceCraftSim { // Supabase channel reference this.clientChannel = null; + this._indexClaims = []; + this._indexTimer = null; this.presenceVersion = 0; // For tracking changes to presence state // Fetch timeout reference @@ -196,7 +199,7 @@ class SpaceCraftSim { * Initializes the content promise to fetch data early */ initContentPromise() { - console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); + // console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); window.contentPromise = fetch(SpaceCraftSim.deepIndexPath) .then(response => { @@ -218,7 +221,7 @@ class SpaceCraftSim { if (this.domContentLoaded) return; // Prevent double execution this.domContentLoaded = true; - console.log("[SpaceCraft] DOM loaded. Initializing QR code."); + // console.log("[SpaceCraft] DOM loaded. Initializing QR code."); // Check for QR Code library dependency if (typeof QRCode === 'undefined') { @@ -226,12 +229,16 @@ class SpaceCraftSim { return; // Stop further initialization if QR code can't be generated } - // Generate QR code - this.generateQRCodes(); + // Generate QR code only after simulatorIndex is assigned; otherwise defer + if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) { + this.generateQRCodes(); + } else { + try { console.log('[Sim] QR generation deferred until simulatorIndex is assigned'); } catch {} + } // Basic initialization is considered complete after DOM/QR setup this.isInitialized = true; - console.log("[SpaceCraft] DOM and QR code initialization complete."); + // console.log("[SpaceCraft] DOM and QR code initialization complete."); // Now that the DOM is ready, proceed to configure and load Unity this.configureAndLoadUnity(); @@ -241,7 +248,7 @@ class SpaceCraftSim { * Generate QR code for controller based on qrCodeDefinitions. */ generateQRCodes() { - console.log("[SpaceCraft] Generating QR code based on definitions..."); + // console.log("[SpaceCraft] Generating QR code based on definitions..."); const qrContainer = document.getElementById('qrcodes-container'); if (!qrContainer) { @@ -273,6 +280,8 @@ class SpaceCraftSim { if (currentChannel !== SpaceCraftSim.clientChannelName) { qrParams.set('channel', currentChannel); } + // Include simulator index so controllers target the right simulator + try { if (this.simulatorIndex != null) qrParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} const currentSearchParams = qrParams.toString() ? '?' + qrParams.toString() : ''; @@ -284,19 +293,19 @@ class SpaceCraftSim { // Ensure it ends with a slash if not already present baseDirectory = explicitBaseUrl.endsWith('/') ? explicitBaseUrl : explicitBaseUrl + '/'; usingExplicitUrl = true; - console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); } else { // Fallback: Calculate the base directory path from window.location baseDirectory = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1); - console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); } - if (!usingExplicitUrl) { - console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); - } - console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); + // if (!usingExplicitUrl) { + // console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // } + // console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); // Loop through the defined QR codes this.qrCodeDefinitions.forEach(definition => { @@ -307,6 +316,8 @@ class SpaceCraftSim { if (definition.type) { qrSpecificParams.set('type', definition.type); } + // Always add simulator index to the QR links once assigned + try { if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) qrSpecificParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} // Build the complete search params string const finalSearchParams = qrSpecificParams.toString() ? '?' + qrSpecificParams.toString() : ''; @@ -316,32 +327,25 @@ class SpaceCraftSim { // Construct the full absolute URL for the QR code message const fullAbsoluteUrl = new URL(targetRelativeUrl, baseDirectory).toString(); - console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); - console.log(` - Relative URL: ${targetRelativeUrl}`); - console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); + // console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); + // console.log(` - Relative URL: ${targetRelativeUrl}`); + // console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); - // 1. Create the link element (will act as a button/link) + // 1. Create the link element (standard anchor with href for accessibility/copyability) const linkElement = document.createElement('a'); linkElement.classList.add('qrcode-link'); // Add a general class for styling - linkElement.style.cursor = 'pointer'; + linkElement.style.cursor = 'pointer'; + linkElement.href = fullAbsoluteUrl; // Allow right-click/copy link + linkElement.target = '_blank'; + linkElement.rel = 'noopener noreferrer'; // Add position class if defined if (definition.position) { linkElement.classList.add(`qr-position-${definition.position}`); } - // Define window features (optional, but helps encourage a new window) - const windowFeatures = 'resizable=yes,scrollbars=yes'; - // Define a unique window name based on the definition ID - const windowName = definition.id + '-window'; // e.g., "navigator-qr-window" - - // Add onclick handler to open in a new window - linkElement.onclick = (event) => { - event.preventDefault(); // Prevent any default link behavior if href was somehow still present - console.log(`[SpaceCraft QR Click] Opening ${windowName} for ${definition.label} with URL: ${fullAbsoluteUrl}`); - window.open(fullAbsoluteUrl, windowName, windowFeatures); - return false; // Prevent further event propagation - }; + // Define a unique window name if needed (left unused since standard anchor navigation is enabled) + // const windowName = definition.id + '-window'; // 2. Generate the QR code SVG const qrSvgElement = QRCode({ @@ -364,7 +368,8 @@ class SpaceCraftSim { qrContainer.appendChild(linkElement); }); - console.log("[SpaceCraft] QR code generated successfully."); + // console.log("[SpaceCraft] QR code generated successfully."); + this.qrCodesGenerated = true; } catch (error) { console.error("[SpaceCraft] Error generating QR code:", error); @@ -376,7 +381,7 @@ class SpaceCraftSim { * Configure and initiate the loading of the Unity instance. */ configureAndLoadUnity() { - console.log("[SpaceCraft] Configuring Unity..."); + // console.log("[SpaceCraft] Configuring Unity..."); // Ensure Bridge is available (should be loaded via script tag before this) window.bridge = window.bridge || new Bridge(); @@ -384,7 +389,7 @@ class SpaceCraftSim { console.error("[SpaceCraft] CRITICAL: Bridge object not found or invalid!"); return; // Cannot proceed without the Bridge } - console.log("[SpaceCraft] Bridge instance checked/created."); + // console.log("[SpaceCraft] Bridge instance checked/created."); // --- Unity Loader Configuration --- // Note: Template variables like {{{ LOADER_FILENAME }}} are replaced by Unity during build. @@ -392,7 +397,7 @@ class SpaceCraftSim { // IMPORTANT: Make sure these template variables match your Unity WebGL template settings const loaderUrl = buildUrl + "/SpaceCraft.loader.js"; // Assuming default naming - console.log("[SpaceCraft] Unity loader URL:", loaderUrl); + // console.log("[SpaceCraft] Unity loader URL:", loaderUrl); const config = { dataUrl: buildUrl + "/SpaceCraft.data", @@ -406,7 +411,7 @@ class SpaceCraftSim { // memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}", // symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}", }; - console.log("[SpaceCraft] Unity configuration prepared:", config); + // console.log("[SpaceCraft] Unity configuration prepared:", config); // --- Get DOM Elements --- const container = document.querySelector("#unity-container"); @@ -418,20 +423,20 @@ class SpaceCraftSim { console.error("[SpaceCraft] Required DOM elements (#unity-container, #unity-canvas, #unity-fullscreen-button) not found."); return; // Cannot proceed without essential DOM elements } - console.log("[SpaceCraft] Unity DOM elements retrieved."); + // console.log("[SpaceCraft] Unity DOM elements retrieved."); // Force canvas fullscreen sizing canvas.style.width = "100%"; canvas.style.height = "100%"; // --- Load Unity Script --- - console.log("[SpaceCraft] Creating Unity loader script element..."); + // console.log("[SpaceCraft] Creating Unity loader script element..."); const script = document.createElement("script"); script.src = loaderUrl; // --- Define Unity Instance Creation Logic (runs after loader script loads) --- script.onload = () => { - console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); + // console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); // Check if createUnityInstance function exists (it should be defined by loaderUrl script) if (typeof createUnityInstance === 'undefined') { @@ -441,20 +446,20 @@ class SpaceCraftSim { createUnityInstance(canvas, config, (progress) => { // Optional: Update loading progress UI - console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); + //console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); // if (progressBarFull) { // progressBarFull.style.width = 100 * progress + "%"; // } }).then((unityInstance) => { // Unity Instance Creation Complete - console.log("[SpaceCraft] Unity instance created successfully."); + // console.log("[SpaceCraft] Unity instance created successfully."); // Store Unity instance globally for access window.unityInstance = unityInstance; // Setup fullscreen button functionality fullscreenButton.onclick = () => { - console.log("[SpaceCraft] Fullscreen button clicked."); + // console.log("[SpaceCraft] Fullscreen button clicked."); unityInstance.SetFullscreen(1); }; @@ -462,9 +467,9 @@ class SpaceCraftSim { // This tells the Bridge JS library that Unity is ready and provides the instance. // The Bridge library internally handles linking with the Unity instance. // Bridge C# code (BridgeTransportWebGL.Awake/Start) will eventually send "StartedUnity". - console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); + // console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); window.bridge.start("WebGL", JSON.stringify({})); // Empty config for now - console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); + // console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); }).catch((message) => { console.error("[SpaceCraft] Error creating Unity instance:", message); @@ -473,7 +478,7 @@ class SpaceCraftSim { // --- Add Loader Script to Document --- document.body.appendChild(script); - console.log("[SpaceCraft] Unity loader script added to document."); + // console.log("[SpaceCraft] Unity loader script added to document."); } /** @@ -487,7 +492,7 @@ class SpaceCraftSim { try { const content = await window.contentPromise; if (content) { - console.log("[SpaceCraft] Successfully loaded content from early fetch"); + // console.log("[SpaceCraft] Successfully loaded content from early fetch"); return content; } } catch (earlyFetchError) { @@ -496,7 +501,7 @@ class SpaceCraftSim { } // Direct fetch if early fetch failed or wasn't available - console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); + // console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); const response = await fetch(SpaceCraftSim.deepIndexPath); if (!response.ok) { @@ -504,9 +509,7 @@ class SpaceCraftSim { } const content = await response.json(); - console.log("[SpaceCraft] Content fetch successful, got:", - Object.keys(content).join(", ") - ); + // console.log("[SpaceCraft] Content fetch successful, got:", Object.keys(content).join(", ")); // Return the exact content as-is, expecting it to be correctly formatted return content; @@ -522,7 +525,7 @@ class SpaceCraftSim { */ async loadCollectionsAndCreateSpaceCraft() { - console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); + // console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); // Ensure basic initialization (DOM, QR code) happened. It should have by now. if (!this.isInitialized) { @@ -559,11 +562,11 @@ class SpaceCraftSim { // Extract tags from loaded content items this.availableTags = this.createUnifiedTagsList(); this.state.tags = this.availableTags; - console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); + // console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); if (this.availableTags.length > 0) { - console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); + // console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); } else { - console.log("[SpaceCraft] No tags found in content items"); + // console.log("[SpaceCraft] No tags found in content items"); } this.setupSupabase(); @@ -574,7 +577,7 @@ class SpaceCraftSim { // Create the SpaceCraft object via Bridge - pass content exactly as received this.createSpaceCraftObject(this.loadedContent); - console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); + // console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); } /** @@ -582,7 +585,7 @@ class SpaceCraftSim { * @param {Object} content - The content data to initialize SpaceCraft with */ createSpaceCraftObject(content) { - console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); + // console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); // Create the actual SpaceCraft object via Bridge with content data this.spaceCraft = window.bridge.createObject({ @@ -597,9 +600,9 @@ class SpaceCraftSim { createMagnet: function (magnetData) { try { - console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); } let magnetId = magnetData.magnetId; if (!magnetId) { @@ -627,9 +630,9 @@ class SpaceCraftSim { updateMagnet: function (magnetData) { try { - console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); } const magnetId = magnetData.magnetId; if (!magnetId) { @@ -672,7 +675,7 @@ class SpaceCraftSim { window.bridge.destroyObject(magnetBridge); - console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); + // console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); return true; }, @@ -710,9 +713,9 @@ class SpaceCraftSim { "unityMetaData": "UnityMetaData" }, handler: (obj, results) => { - console.log("[SpaceCraft] ContentLoaded event received from Unity"); - console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); - console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); + // console.log("[SpaceCraft] ContentLoaded event received from Unity"); + // console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); + // console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); this.state.unityMetaData = results.unityMetaData; } }, @@ -753,7 +756,10 @@ class SpaceCraftSim { // Create the ground plane as a child of SpaceCraft this.groundPlane = window.bridge.createObject({ prefab: "Prefabs/GroundPlane", - parent: this.spaceCraft + parent: this.spaceCraft, + update: { + "transform:Cube/component:MeshRenderer/material/color": { r: 0.0, g: .2, b: 0.0 }, + } }); // Store references globally @@ -771,7 +777,8 @@ class SpaceCraftSim { // Client identity clientType: 'simulator', clientId: this.clientId, - clientName: 'Spacecraft Simulator', + clientName: 'Spacecraft', + simulatorIndex: 0, // Collection/screen state currentScreenId: 'main', @@ -833,15 +840,26 @@ class SpaceCraftSim { } } - /** - * Syncs the current state to Supabase presence - */ + mirrorIdentityToState() { + try { + if (!this.state) return; + this.state.clientId = this.identity.clientId; + this.state.clientType = this.identity.clientType; + this.state.clientName = this.identity.clientName; + // simulatorIndex may be undefined for non-simulators; default to 0 + this.state.simulatorIndex = (typeof this.identity.simulatorIndex === 'number') ? this.identity.simulatorIndex : 0; + try { console.log('[Sim] mirrorIdentityToState:', { id: this.state.clientId, name: this.state.clientName, simulatorIndex: this.state.simulatorIndex }); } catch {} + } catch {} + } + syncStateToPresence() { if (!this.clientChannel) { - console.warn("[SpaceCraft] Attempted to sync presence, but clientChannel is null."); return; } - + // Ensure identity is reflected in state before publishing + this.mirrorIdentityToState(); + + try { console.log('[Sim] syncStateToPresence track shared:', { simulatorIndex: this.state && this.state.simulatorIndex, clientName: this.state && this.state.clientName }); } catch {} this.clientChannel.track({ ...this.identity, shared: { ...this.state } @@ -852,7 +870,11 @@ class SpaceCraftSim { setupSupabase() { const channelName = SpaceCraftSim.getChannelName(); - console.log(`[SpaceCraft] Setting up Supabase client with channel: ${channelName}`); + try { console.log('[Sim] setupSupabase channel =', channelName); } catch {} + if (!window.supabase || typeof window.supabase.createClient !== 'function') { + console.error('[Sim] Supabase JS library missing or invalid on simulator page'); + return; + } // Create a Supabase client const client = window.supabase.createClient( @@ -861,14 +883,38 @@ class SpaceCraftSim { ); // Create a channel for client communication - this.clientChannel = client.channel(channelName); - + this.clientChannel = client.channel(channelName, { config: { presence: { key: this.identity.clientId } } }); + try { console.log('[Sim] clientChannel created for', channelName, 'with presence key', this.identity.clientId); } catch {} + this._indexClaims = []; + this._indexTimer = null; + this.clientChannel .on('broadcast', {}, (data) => { - console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); + // console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); if (data.payload && data.payload.targetSimulatorId) { - console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + // console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + } + }) + + .on('broadcast', { event: 'indexClaim' }, (data) => { + try { + const claim = data && data.payload || {}; + if (!claim.clientId || typeof claim.index !== 'number') return; + const now = Date.now(); + this._indexClaims.push({ clientId: claim.clientId, index: claim.index, ts: now }); + // keep recent 2s + this._indexClaims = this._indexClaims.filter(c => now - c.ts < 2000); + try { + console.log('[Sim] received indexClaim:', { clientId: claim.clientId, index: claim.index, ts: now }); + console.log('[Sim] indexClaim window (<=2s):', this._indexClaims); + } catch {} + } catch {} + }) + + .on('presence', { event: 'sync' }, () => { + try { console.log('[Sim] presence:sync received'); this.ensureUniqueSimulatorName(); } catch (e) { + console.warn('[SpaceCraft] ensureUniqueSimulatorName error:', e); } }) @@ -943,7 +989,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -976,7 +1022,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -1064,36 +1110,15 @@ class SpaceCraftSim { this.spaceCraft.pushMagnet(magnetId, deltaX, deltaY); }) - .on('broadcast', { event: 'simulatorTakeover' }, (data) => { - // Another simulator is trying to take over - if (data.payload.newSimulatorId === this.identity.clientId) { - return; // Our own takeover message, ignore - } - - // If this simulator started later than the new one, let it take over - if (this.identity.startTime < data.payload.startTime) { - console.log(`[SpaceCraft] Another simulator has taken over: ${data.payload.newSimulatorId}`); - this.shutdown(); - } else { - console.log(`[SpaceCraft] Ignoring takeover from older simulator: ${data.payload.newSimulatorId}`); - this.sendTakeoverEvent(); - } - }) - - .on('presence', { event: 'sync' }, () => { - const allPresences = this.clientChannel.presenceState(); - }) - .on('presence', { event: 'join' }, ({ newPresences }) => { for (const presence of newPresences) { // Skip our own presence if (presence.clientId === this.identity.clientId) continue; + // console.log(`[SpaceCraft] Another ${presence.clientType} joined: ${presence.clientId} ${presence.clientName}`); + // Check if this is a simulator joining if (presence.clientType === "simulator") { - console.log(`[SpaceCraft] Another simulator joined: ${presence.clientId}`); - // Send takeover event to establish dominance - this.sendTakeoverEvent(); } else { // A controller client joined this.updateClientInfo( @@ -1109,73 +1134,133 @@ class SpaceCraftSim { for (const presence of leftPresences) { // Remove from our client registry if (this.clients[presence.clientId]) { - console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); + // console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); delete this.clients[presence.clientId]; } } }) - .subscribe((status) => { + .subscribe((status) => { + try { console.log('[Sim] subscribe status:', status); } catch {} if (status === 'SUBSCRIBED') { - console.log("[SpaceCraft] Successfully subscribed to client channel"); - + try { console.log('[Sim] SUBSCRIBED: tracking presence and starting index negotiation'); } catch {} + try { console.log('[Sim] track identity:', { clientId: this.identity.clientId, clientName: this.identity.clientName, simulatorIndex: this.identity.simulatorIndex }); } catch {} this.clientChannel.track({ ...this.identity, - shared: { ...this.state } // Nest state under 'shared' + shared: { ...this.state } }); - - this.sendTakeoverEvent(); + try { this.ensureUniqueSimulatorName(); } catch {} } }); + // Attempt to restore previously assigned simulator index for this channel (persists across reconnects) + // Intentionally NOT restoring simulatorIndex from localStorage to avoid duplicate indices across tabs + try { console.log('[Sim] init: not restoring simulatorIndex from localStorage; will negotiate via presence'); } catch {} + this.syncStateToPresence(); } - /** - * Sends a takeover event to notify other simulators and clients - */ - sendTakeoverEvent() { - if (!this.clientChannel) { - console.warn("[SpaceCraft] Cannot send takeover event: Client channel not initialized."); - return; - } - - // console.log(`[SpaceCraft] Sending simulator takeover notification`); - - this.clientChannel.send({ - type: 'broadcast', - event: 'simulatorTakeover', - payload: { - newSimulatorId: this.identity.clientId, - newSimulatorName: this.identity.clientName, - startTime: this.identity.startTime - } - }).catch(err => console.error("[SpaceCraft] Error sending takeover event:", err)); + broadcastIndexClaim(index) { + try { + console.log('[Sim] broadcastIndexClaim sending', { index, clientId: this.identity && this.identity.clientId }); + this.clientChannel && this.clientChannel.send({ + type: 'broadcast', + event: 'indexClaim', + payload: { clientId: this.identity.clientId, index } + }); + } catch {} } - - /** - * Gracefully shuts down this simulator instance - */ - shutdown() { - console.log("[SpaceCraft] Shutting down simulator due to takeover"); - - // Clean up resources - no try/catch, just direct error logging - if (this.clientChannel) { - this.clientChannel.unsubscribe().catch(err => { - console.error("[SpaceCraft] Error unsubscribing from channel:", err); + + ensureUniqueSimulatorName() { + try { + // If already assigned, nothing to do + if (this.simulatorIndex && this.simulatorIndex > 0) return; + const state = this.clientChannel && this.clientChannel.presenceState ? this.clientChannel.presenceState() : null; + if (!state) return; + const prefix = SpaceCraftSim.simulatorNamePrefix; + // Compute observed max from presence (explicit index, name suffix) and shared maxIndexSeen + try { console.log('[Sim] ensureUniqueSimulatorName start; presence keys=', Object.keys(state || {})); } catch {} + let totalSimulators = 0; + let presenceMax = 0; + let maxSeenShared = 0; + Object.values(state).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + totalSimulators += 1; + try { console.log('[Sim] presence simulator:', { clientId: p.clientId, nameTop: p.clientName, nameShared: p.shared && p.shared.clientName, idxTop: p.simulatorIndex, idxShared: p.shared && p.shared.simulatorIndex, maxIndexSeen: p.shared && p.shared.maxIndexSeen }); } catch {} + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > presenceMax) presenceMax = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > presenceMax) presenceMax = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const m = p.clientName.match(/(\d+)\s*$/); + if (m) { + const n = parseInt(m[1], 10); + if (n > presenceMax) presenceMax = n; + } + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > maxSeenShared) maxSeenShared = p.shared.maxIndexSeen; + } + }); }); - } - - // Redirect to a shutdown page or reload - alert("Another SpaceCraft simulator has taken control. This window will now close."); - window.close(); - - // If window doesn't close (e.g., window not opened by script), reload - setTimeout(() => { - window.location.reload(); - }, 1000); + // Do not read maxIndexSeen from localStorage; avoid cross-tab coupling + const now = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now - c.ts < 2000); + const claimsMax = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const base = Math.max(presenceMax, maxSeenShared, claimsMax); + const sameBaseClaims = this._indexClaims.filter(c => c.index === base); + const rank = sameBaseClaims.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const proposed = (totalSimulators <= 1 && base === 0) ? 1 : (base + rank + 1); + console.log(`[Sim] proposal: ts=${now} tot=${totalSimulators} presenceMax=${presenceMax} maxSeenShared=${maxSeenShared} claimsMax=${claimsMax} base=${base} rank=${rank} proposed=${proposed}`); + this.broadcastIndexClaim(proposed); + // finalize after a short coordination window + clearTimeout(this._indexTimer); + this._indexTimer = setTimeout(() => { + const now2 = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now2 - c.ts < 2000); + const claimsMax2 = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const presenceMax2 = (() => { + let m = 0; + Object.values(this.clientChannel.presenceState() || {}).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > m) m = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > m) m = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const mm = p.clientName.match(/(\d+)\s*$/); + if (mm) m = Math.max(m, parseInt(mm[1], 10)); + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > m) m = p.shared.maxIndexSeen; + } + }); + }); + return m; + })(); + const base2 = Math.max(presenceMax2, claimsMax2); + const sameBase2 = this._indexClaims.filter(c => c.index === base2); + const rank2 = sameBase2.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const finalIndex = (final => (final > 0 ? final : 1))( (base2 + rank2 + 1) ); + console.log(`[Sim] finalize: claimsMax2=${claimsMax2} presenceMax2=${presenceMax2} base2=${base2} rank2=${rank2} finalIndex=${finalIndex}`); + if (!this.simulatorIndex || this.simulatorIndex === 0) { + this.simulatorIndex = finalIndex; + const nextName = `${prefix} ${this.simulatorIndex}`; + this.identity.clientName = nextName; + this.identity.simulatorIndex = this.simulatorIndex; + this.state.clientName = nextName; + this.state.simulatorIndex = this.simulatorIndex; + const newMaxSeen = Math.max(base2, this.simulatorIndex); + this.state.maxIndexSeen = newMaxSeen; + // Do not persist indices to localStorage to avoid cross-tab duplication + console.log(`[Sim] assigned: simulatorIndex=${this.simulatorIndex} clientName="${this.identity.clientName}"`); + this.syncStateToPresence(); + if (this.domContentLoaded) { + this.generateQRCodes(); + } + } + }, 400); + } catch {} } + // Takeover functionality removed: multi-simulator is supported + /** * Sends a Supabase broadcast event on the channel. * @param {string} clientId - The unique ID of the target client. @@ -1194,7 +1279,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); + // console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); // Add simulator ID to payload so client knows who sent it const fullPayload = { @@ -1220,7 +1305,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); + // console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); // Add simulator ID to payload so clients know who sent it const fullPayload = { @@ -1272,13 +1357,7 @@ class SpaceCraftSim { * Logs the current status of the SpaceCraft instance */ logStatus() { - console.log("[SpaceCraft Debug] Status:", { - DOMReady: this.domContentLoaded, - BasicInitialized: this.isInitialized, - BridgeAvailable: !!window.bridge, - SpaceCraft: !!this.spaceCraft, - SupabaseLoaded: typeof window.supabase !== 'undefined' - }); + // console.log("[SpaceCraft Debug] Status:", { DOMReady: this.domContentLoaded, BasicInitialized: this.isInitialized, BridgeAvailable: !!window.bridge, SpaceCraft: !!this.spaceCraft, SupabaseLoaded: typeof window.supabase !== 'undefined' }); } /** @@ -1354,7 +1433,7 @@ class SpaceCraftSim { */ createUnifiedTagsList() { if (!this.loadedContent || !this.loadedContent.collections) { - console.log("[SpaceCraft] No collections data available for creating tags list"); + // console.log("[SpaceCraft] No collections data available for creating tags list"); return []; } @@ -1380,7 +1459,7 @@ class SpaceCraftSim { } const sortedTags = Array.from(allTags).sort(); - console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); + // console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); return sortedTags; } } @@ -1408,4 +1487,4 @@ if (document.readyState === 'loading') { // setInterval(() => window.SpaceCraft.logStatus(), 5000); } -console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); +// console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.data b/WebSites/SpaceCraft/Build/SpaceCraft.data index 18d18e88..0036c9d5 100644 --- a/WebSites/SpaceCraft/Build/SpaceCraft.data +++ b/WebSites/SpaceCraft/Build/SpaceCraft.data @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7edc96cf0521ff7a52a41dc0bb07a08d609542314970a20ee4217cf1af03589b -size 10233506 +oid sha256:f959831d24b6f35803df1ae390b3cab7d3e34c2824fb5cf522734c50f6774fd6 +size 10229906 diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.framework.js b/WebSites/SpaceCraft/Build/SpaceCraft.framework.js index 30956cd2..89351c75 100644 --- a/WebSites/SpaceCraft/Build/SpaceCraft.framework.js +++ b/WebSites/SpaceCraft/Build/SpaceCraft.framework.js @@ -1277,10 +1277,10 @@ function dbg(text) { // === Body === var ASM_CONSTS = { - 5876176: () => { Module['emscripten_get_now_backup'] = performance.now; }, - 5876231: ($0) => { performance.now = function() { return $0; }; }, - 5876279: ($0) => { performance.now = function() { return $0; }; }, - 5876327: () => { performance.now = Module['emscripten_get_now_backup']; } + 5875856: () => { Module['emscripten_get_now_backup'] = performance.now; }, + 5875911: ($0) => { performance.now = function() { return $0; }; }, + 5875959: ($0) => { performance.now = function() { return $0; }; }, + 5876007: () => { performance.now = Module['emscripten_get_now_backup']; } }; @@ -18751,9 +18751,9 @@ var wasmImports = { "invoke_viiififiii": invoke_viiififiii, "invoke_viiifii": invoke_viiifii, "invoke_viiii": invoke_viiii, + "invoke_viiiiffi": invoke_viiiiffi, "invoke_viiiifi": invoke_viiiifi, "invoke_viiiii": invoke_viiiii, - "invoke_viiiiiffi": invoke_viiiiiffi, "invoke_viiiiii": invoke_viiiiii, "invoke_viiiiiifii": invoke_viiiiiifii, "invoke_viiiiiii": invoke_viiiiiii, @@ -19152,7 +19152,7 @@ var dynCall_vjiiiii = Module["dynCall_vjiiiii"] = createExportWrapper("dynCall_v /** @type {function(...*):?} */ var dynCall_jiiiii = Module["dynCall_jiiiii"] = createExportWrapper("dynCall_jiiiii"); /** @type {function(...*):?} */ -var dynCall_viiiiiffi = Module["dynCall_viiiiiffi"] = createExportWrapper("dynCall_viiiiiffi"); +var dynCall_viiiiffi = Module["dynCall_viiiiffi"] = createExportWrapper("dynCall_viiiiffi"); /** @type {function(...*):?} */ var dynCall_viiffii = Module["dynCall_viiffii"] = createExportWrapper("dynCall_viiffii"); /** @type {function(...*):?} */ @@ -19206,7 +19206,7 @@ var dynCall_vifffffi = Module["dynCall_vifffffi"] = createExportWrapper("dynCall /** @type {function(...*):?} */ var dynCall_viffffffi = Module["dynCall_viffffffi"] = createExportWrapper("dynCall_viffffffi"); /** @type {function(...*):?} */ -var dynCall_viiiiffi = Module["dynCall_viiiiffi"] = createExportWrapper("dynCall_viiiiffi"); +var dynCall_viiiiiffi = Module["dynCall_viiiiiffi"] = createExportWrapper("dynCall_viiiiiffi"); /** @type {function(...*):?} */ var dynCall_ijiii = Module["dynCall_ijiii"] = createExportWrapper("dynCall_ijiii"); /** @type {function(...*):?} */ @@ -20460,10 +20460,10 @@ function invoke_viiff(index,a1,a2,a3,a4) { } } -function invoke_viiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8) { +function invoke_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7) { var sp = stackSave(); try { - dynCall_viiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8); + dynCall_viiiiffi(index,a1,a2,a3,a4,a5,a6,a7); } catch(e) { stackRestore(sp); if (e !== e+0) throw e; diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.loader.js b/WebSites/SpaceCraft/Build/SpaceCraft.loader.js index 194cdf62..67b91e12 100644 --- a/WebSites/SpaceCraft/Build/SpaceCraft.loader.js +++ b/WebSites/SpaceCraft/Build/SpaceCraft.loader.js @@ -54,7 +54,7 @@ function createUnityInstance(canvas, config, onProgress) { preserveDrawingBuffer: false, powerPreference: 2, }, - wasmFileSize: 82679103, + wasmFileSize: 82670661, cacheControl: function (url) { return (url == Module.dataUrl || url.match(/\.bundle/)) ? "must-revalidate" : "no-store"; }, diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.wasm b/WebSites/SpaceCraft/Build/SpaceCraft.wasm index f1d9cdc7..9d58a0ce 100755 --- a/WebSites/SpaceCraft/Build/SpaceCraft.wasm +++ b/WebSites/SpaceCraft/Build/SpaceCraft.wasm @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:745f28ebcb1b888a55ff325d9f0efa4e01fc0ade972a956ddf37495d41095b18 -size 82679103 +oid sha256:e686457720fff6e536ad2728e990487564e68058ad4762a04b3772f3b6562606 +size 82670661 diff --git a/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js b/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js index 12a8fcf3..c9ef6b1e 100644 --- a/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js +++ b/WebSites/SpaceCraft/StreamingAssets/SpaceCraft/spacecraft.js @@ -71,10 +71,6 @@ // - action can be "tap" or directional ("north","south","east","west","up","down") // - "tap" applies a scale impulse to the highlighted item // - Directions call MoveHighlight -// 'broadcast' { event: 'simulatorTakeover' }: -// - {newSimulatorId, newSimulatorName, startTime} -// - Signals that a new simulator has taken control -// - Used to manage multiple simulator instances // // --- PRESENCE EVENTS (Client connection tracking) --- // 'presence' { event: 'sync' }: Full state of all connected clients @@ -116,6 +112,7 @@ class SpaceCraftSim { static deepIndexPath = 'StreamingAssets/Content/index-deep.json'; static controllerHtmlPath = '../controller/'; static clientChannelName = 'spacecraft'; + static simulatorNamePrefix = 'SpaceCraft'; /** * Get the channel name from URL query parameter or use default @@ -124,7 +121,10 @@ class SpaceCraftSim { static getChannelName() { const urlParams = new URLSearchParams(window.location.search); const channelFromUrl = urlParams.get('channel'); - return channelFromUrl || this.clientChannelName; + if (channelFromUrl) return channelFromUrl; + // Fallback: use the host name of the web page URL instead of the hard-coded default + const host = window.location && window.location.hostname; + return host; } /** @@ -138,7 +138,8 @@ class SpaceCraftSim { this.identity = { clientId: this.clientId, // Unique ID for this simulator instance clientType: "simulator", // Fixed type for simulator - clientName: "SpaceCraft Simulator", // Human-readable name + clientName: SpaceCraftSim.simulatorNamePrefix, // Start without a number; will assign on presence + simulatorIndex: 0, startTime: Date.now() // When this simulator instance started }; @@ -158,6 +159,8 @@ class SpaceCraftSim { // Supabase channel reference this.clientChannel = null; + this._indexClaims = []; + this._indexTimer = null; this.presenceVersion = 0; // For tracking changes to presence state // Fetch timeout reference @@ -196,7 +199,7 @@ class SpaceCraftSim { * Initializes the content promise to fetch data early */ initContentPromise() { - console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); + // console.log("[SpaceCraft] Initiating early fetch for index-deep.json"); window.contentPromise = fetch(SpaceCraftSim.deepIndexPath) .then(response => { @@ -218,7 +221,7 @@ class SpaceCraftSim { if (this.domContentLoaded) return; // Prevent double execution this.domContentLoaded = true; - console.log("[SpaceCraft] DOM loaded. Initializing QR code."); + // console.log("[SpaceCraft] DOM loaded. Initializing QR code."); // Check for QR Code library dependency if (typeof QRCode === 'undefined') { @@ -226,12 +229,16 @@ class SpaceCraftSim { return; // Stop further initialization if QR code can't be generated } - // Generate QR code - this.generateQRCodes(); + // Generate QR code only after simulatorIndex is assigned; otherwise defer + if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) { + this.generateQRCodes(); + } else { + try { console.log('[Sim] QR generation deferred until simulatorIndex is assigned'); } catch {} + } // Basic initialization is considered complete after DOM/QR setup this.isInitialized = true; - console.log("[SpaceCraft] DOM and QR code initialization complete."); + // console.log("[SpaceCraft] DOM and QR code initialization complete."); // Now that the DOM is ready, proceed to configure and load Unity this.configureAndLoadUnity(); @@ -241,7 +248,7 @@ class SpaceCraftSim { * Generate QR code for controller based on qrCodeDefinitions. */ generateQRCodes() { - console.log("[SpaceCraft] Generating QR code based on definitions..."); + // console.log("[SpaceCraft] Generating QR code based on definitions..."); const qrContainer = document.getElementById('qrcodes-container'); if (!qrContainer) { @@ -273,6 +280,8 @@ class SpaceCraftSim { if (currentChannel !== SpaceCraftSim.clientChannelName) { qrParams.set('channel', currentChannel); } + // Include simulator index so controllers target the right simulator + try { if (this.simulatorIndex != null) qrParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} const currentSearchParams = qrParams.toString() ? '?' + qrParams.toString() : ''; @@ -284,19 +293,19 @@ class SpaceCraftSim { // Ensure it ends with a slash if not already present baseDirectory = explicitBaseUrl.endsWith('/') ? explicitBaseUrl : explicitBaseUrl + '/'; usingExplicitUrl = true; - console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using explicit base_url from parameter: ${baseDirectory}`); } else { // Fallback: Calculate the base directory path from window.location baseDirectory = window.location.origin + window.location.pathname.substring(0, window.location.pathname.lastIndexOf('/') + 1); - console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // console.log(`[SpaceCraft QR] Using detected origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); } - if (!usingExplicitUrl) { - console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); - console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); - } - console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); + // if (!usingExplicitUrl) { + // console.log(`[SpaceCraft QR] Detected Origin: ${window.location.origin}`); + // console.log(`[SpaceCraft QR] Calculated Base Directory: ${baseDirectory}`); + // } + // console.log(`[SpaceCraft QR] Current Search Params: ${currentSearchParams || '(none)'}`); // Loop through the defined QR codes this.qrCodeDefinitions.forEach(definition => { @@ -307,6 +316,8 @@ class SpaceCraftSim { if (definition.type) { qrSpecificParams.set('type', definition.type); } + // Always add simulator index to the QR links once assigned + try { if (typeof this.simulatorIndex === 'number' && this.simulatorIndex > 0) qrSpecificParams.set('simulatorIndex', String(this.simulatorIndex)); } catch {} // Build the complete search params string const finalSearchParams = qrSpecificParams.toString() ? '?' + qrSpecificParams.toString() : ''; @@ -316,32 +327,25 @@ class SpaceCraftSim { // Construct the full absolute URL for the QR code message const fullAbsoluteUrl = new URL(targetRelativeUrl, baseDirectory).toString(); - console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); - console.log(` - Relative URL: ${targetRelativeUrl}`); - console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); + // console.log(`[SpaceCraft QR] Generating for ${definition.label} at position '${definition.position || 'default'}':`); + // console.log(` - Relative URL: ${targetRelativeUrl}`); + // console.log(` - Absolute URL (for QR & window): ${fullAbsoluteUrl}`); - // 1. Create the link element (will act as a button/link) + // 1. Create the link element (standard anchor with href for accessibility/copyability) const linkElement = document.createElement('a'); linkElement.classList.add('qrcode-link'); // Add a general class for styling - linkElement.style.cursor = 'pointer'; + linkElement.style.cursor = 'pointer'; + linkElement.href = fullAbsoluteUrl; // Allow right-click/copy link + linkElement.target = '_blank'; + linkElement.rel = 'noopener noreferrer'; // Add position class if defined if (definition.position) { linkElement.classList.add(`qr-position-${definition.position}`); } - // Define window features (optional, but helps encourage a new window) - const windowFeatures = 'resizable=yes,scrollbars=yes'; - // Define a unique window name based on the definition ID - const windowName = definition.id + '-window'; // e.g., "navigator-qr-window" - - // Add onclick handler to open in a new window - linkElement.onclick = (event) => { - event.preventDefault(); // Prevent any default link behavior if href was somehow still present - console.log(`[SpaceCraft QR Click] Opening ${windowName} for ${definition.label} with URL: ${fullAbsoluteUrl}`); - window.open(fullAbsoluteUrl, windowName, windowFeatures); - return false; // Prevent further event propagation - }; + // Define a unique window name if needed (left unused since standard anchor navigation is enabled) + // const windowName = definition.id + '-window'; // 2. Generate the QR code SVG const qrSvgElement = QRCode({ @@ -364,7 +368,8 @@ class SpaceCraftSim { qrContainer.appendChild(linkElement); }); - console.log("[SpaceCraft] QR code generated successfully."); + // console.log("[SpaceCraft] QR code generated successfully."); + this.qrCodesGenerated = true; } catch (error) { console.error("[SpaceCraft] Error generating QR code:", error); @@ -376,7 +381,7 @@ class SpaceCraftSim { * Configure and initiate the loading of the Unity instance. */ configureAndLoadUnity() { - console.log("[SpaceCraft] Configuring Unity..."); + // console.log("[SpaceCraft] Configuring Unity..."); // Ensure Bridge is available (should be loaded via script tag before this) window.bridge = window.bridge || new Bridge(); @@ -384,7 +389,7 @@ class SpaceCraftSim { console.error("[SpaceCraft] CRITICAL: Bridge object not found or invalid!"); return; // Cannot proceed without the Bridge } - console.log("[SpaceCraft] Bridge instance checked/created."); + // console.log("[SpaceCraft] Bridge instance checked/created."); // --- Unity Loader Configuration --- // Note: Template variables like {{{ LOADER_FILENAME }}} are replaced by Unity during build. @@ -392,7 +397,7 @@ class SpaceCraftSim { // IMPORTANT: Make sure these template variables match your Unity WebGL template settings const loaderUrl = buildUrl + "/SpaceCraft.loader.js"; // Assuming default naming - console.log("[SpaceCraft] Unity loader URL:", loaderUrl); + // console.log("[SpaceCraft] Unity loader URL:", loaderUrl); const config = { dataUrl: buildUrl + "/SpaceCraft.data", @@ -406,7 +411,7 @@ class SpaceCraftSim { // memoryUrl: buildUrl + "/{{{ MEMORY_FILENAME }}}", // symbolsUrl: buildUrl + "/{{{ SYMBOLS_FILENAME }}}", }; - console.log("[SpaceCraft] Unity configuration prepared:", config); + // console.log("[SpaceCraft] Unity configuration prepared:", config); // --- Get DOM Elements --- const container = document.querySelector("#unity-container"); @@ -418,20 +423,20 @@ class SpaceCraftSim { console.error("[SpaceCraft] Required DOM elements (#unity-container, #unity-canvas, #unity-fullscreen-button) not found."); return; // Cannot proceed without essential DOM elements } - console.log("[SpaceCraft] Unity DOM elements retrieved."); + // console.log("[SpaceCraft] Unity DOM elements retrieved."); // Force canvas fullscreen sizing canvas.style.width = "100%"; canvas.style.height = "100%"; // --- Load Unity Script --- - console.log("[SpaceCraft] Creating Unity loader script element..."); + // console.log("[SpaceCraft] Creating Unity loader script element..."); const script = document.createElement("script"); script.src = loaderUrl; // --- Define Unity Instance Creation Logic (runs after loader script loads) --- script.onload = () => { - console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); + // console.log("[SpaceCraft] Unity loader script loaded. Creating Unity instance..."); // Check if createUnityInstance function exists (it should be defined by loaderUrl script) if (typeof createUnityInstance === 'undefined') { @@ -441,20 +446,20 @@ class SpaceCraftSim { createUnityInstance(canvas, config, (progress) => { // Optional: Update loading progress UI - console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); + //console.log(`[SpaceCraft] Unity loading progress: ${Math.round(progress * 100)}%`); // if (progressBarFull) { // progressBarFull.style.width = 100 * progress + "%"; // } }).then((unityInstance) => { // Unity Instance Creation Complete - console.log("[SpaceCraft] Unity instance created successfully."); + // console.log("[SpaceCraft] Unity instance created successfully."); // Store Unity instance globally for access window.unityInstance = unityInstance; // Setup fullscreen button functionality fullscreenButton.onclick = () => { - console.log("[SpaceCraft] Fullscreen button clicked."); + // console.log("[SpaceCraft] Fullscreen button clicked."); unityInstance.SetFullscreen(1); }; @@ -462,9 +467,9 @@ class SpaceCraftSim { // This tells the Bridge JS library that Unity is ready and provides the instance. // The Bridge library internally handles linking with the Unity instance. // Bridge C# code (BridgeTransportWebGL.Awake/Start) will eventually send "StartedUnity". - console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); + // console.log("[SpaceCraft] Starting Bridge with WebGL driver (triggers C# setup)."); window.bridge.start("WebGL", JSON.stringify({})); // Empty config for now - console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); + // console.log("[SpaceCraft] Bridge start initiated. Waiting for 'StartedUnity' event from C#..."); }).catch((message) => { console.error("[SpaceCraft] Error creating Unity instance:", message); @@ -473,7 +478,7 @@ class SpaceCraftSim { // --- Add Loader Script to Document --- document.body.appendChild(script); - console.log("[SpaceCraft] Unity loader script added to document."); + // console.log("[SpaceCraft] Unity loader script added to document."); } /** @@ -487,7 +492,7 @@ class SpaceCraftSim { try { const content = await window.contentPromise; if (content) { - console.log("[SpaceCraft] Successfully loaded content from early fetch"); + // console.log("[SpaceCraft] Successfully loaded content from early fetch"); return content; } } catch (earlyFetchError) { @@ -496,7 +501,7 @@ class SpaceCraftSim { } // Direct fetch if early fetch failed or wasn't available - console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); + // console.log(`[SpaceCraft] Fetching content from ${SpaceCraftSim.deepIndexPath}`); const response = await fetch(SpaceCraftSim.deepIndexPath); if (!response.ok) { @@ -504,9 +509,7 @@ class SpaceCraftSim { } const content = await response.json(); - console.log("[SpaceCraft] Content fetch successful, got:", - Object.keys(content).join(", ") - ); + // console.log("[SpaceCraft] Content fetch successful, got:", Object.keys(content).join(", ")); // Return the exact content as-is, expecting it to be correctly formatted return content; @@ -522,7 +525,7 @@ class SpaceCraftSim { */ async loadCollectionsAndCreateSpaceCraft() { - console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); + // console.log("[SpaceCraft] 'StartedUnity' event received. Loading content and creating SpaceCraft object..."); // Ensure basic initialization (DOM, QR code) happened. It should have by now. if (!this.isInitialized) { @@ -559,11 +562,11 @@ class SpaceCraftSim { // Extract tags from loaded content items this.availableTags = this.createUnifiedTagsList(); this.state.tags = this.availableTags; - console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); + // console.log(`[SpaceCraft] Loaded ${this.availableTags.length} tags from content items`); if (this.availableTags.length > 0) { - console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); + // console.log(`[SpaceCraft] Available tags (${this.availableTags.length}):`, this.availableTags.slice(0, 10)); } else { - console.log("[SpaceCraft] No tags found in content items"); + // console.log("[SpaceCraft] No tags found in content items"); } this.setupSupabase(); @@ -574,7 +577,7 @@ class SpaceCraftSim { // Create the SpaceCraft object via Bridge - pass content exactly as received this.createSpaceCraftObject(this.loadedContent); - console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); + // console.log("[SpaceCraft] Content loaded and SpaceCraft object created."); } /** @@ -582,7 +585,7 @@ class SpaceCraftSim { * @param {Object} content - The content data to initialize SpaceCraft with */ createSpaceCraftObject(content) { - console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); + // console.log("[SpaceCraft] Creating SpaceCraft object in Bridge with loaded content..."); // Create the actual SpaceCraft object via Bridge with content data this.spaceCraft = window.bridge.createObject({ @@ -597,9 +600,9 @@ class SpaceCraftSim { createMagnet: function (magnetData) { try { - console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] createMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] createMagnet received magnetData (raw):', magnetData); } let magnetId = magnetData.magnetId; if (!magnetId) { @@ -627,9 +630,9 @@ class SpaceCraftSim { updateMagnet: function (magnetData) { try { - console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] updateMagnet received magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch (e) { - console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); + // console.log('[Sim JS] updateMagnet received magnetData (raw):', magnetData); } const magnetId = magnetData.magnetId; if (!magnetId) { @@ -672,7 +675,7 @@ class SpaceCraftSim { window.bridge.destroyObject(magnetBridge); - console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); + // console.log(`[Bridge] Deleted magnet: "${magnetId}". Total magnets: ${this.magnets.size}`); return true; }, @@ -710,9 +713,9 @@ class SpaceCraftSim { "unityMetaData": "UnityMetaData" }, handler: (obj, results) => { - console.log("[SpaceCraft] ContentLoaded event received from Unity"); - console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); - console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); + // console.log("[SpaceCraft] ContentLoaded event received from Unity"); + // console.log("[SpaceCraft] unityMetaData:", results.unityMetaData); + // console.log("[SpaceCraft] MagnetView metadata sample:", results.unityMetaData.MagnetView?.slice(0, 3)); this.state.unityMetaData = results.unityMetaData; } }, @@ -753,7 +756,10 @@ class SpaceCraftSim { // Create the ground plane as a child of SpaceCraft this.groundPlane = window.bridge.createObject({ prefab: "Prefabs/GroundPlane", - parent: this.spaceCraft + parent: this.spaceCraft, + update: { + "transform:Cube/component:MeshRenderer/material/color": { r: 0.0, g: .2, b: 0.0 }, + } }); // Store references globally @@ -771,7 +777,8 @@ class SpaceCraftSim { // Client identity clientType: 'simulator', clientId: this.clientId, - clientName: 'Spacecraft Simulator', + clientName: 'Spacecraft', + simulatorIndex: 0, // Collection/screen state currentScreenId: 'main', @@ -833,15 +840,26 @@ class SpaceCraftSim { } } - /** - * Syncs the current state to Supabase presence - */ + mirrorIdentityToState() { + try { + if (!this.state) return; + this.state.clientId = this.identity.clientId; + this.state.clientType = this.identity.clientType; + this.state.clientName = this.identity.clientName; + // simulatorIndex may be undefined for non-simulators; default to 0 + this.state.simulatorIndex = (typeof this.identity.simulatorIndex === 'number') ? this.identity.simulatorIndex : 0; + try { console.log('[Sim] mirrorIdentityToState:', { id: this.state.clientId, name: this.state.clientName, simulatorIndex: this.state.simulatorIndex }); } catch {} + } catch {} + } + syncStateToPresence() { if (!this.clientChannel) { - console.warn("[SpaceCraft] Attempted to sync presence, but clientChannel is null."); return; } - + // Ensure identity is reflected in state before publishing + this.mirrorIdentityToState(); + + try { console.log('[Sim] syncStateToPresence track shared:', { simulatorIndex: this.state && this.state.simulatorIndex, clientName: this.state && this.state.clientName }); } catch {} this.clientChannel.track({ ...this.identity, shared: { ...this.state } @@ -852,7 +870,11 @@ class SpaceCraftSim { setupSupabase() { const channelName = SpaceCraftSim.getChannelName(); - console.log(`[SpaceCraft] Setting up Supabase client with channel: ${channelName}`); + try { console.log('[Sim] setupSupabase channel =', channelName); } catch {} + if (!window.supabase || typeof window.supabase.createClient !== 'function') { + console.error('[Sim] Supabase JS library missing or invalid on simulator page'); + return; + } // Create a Supabase client const client = window.supabase.createClient( @@ -861,14 +883,38 @@ class SpaceCraftSim { ); // Create a channel for client communication - this.clientChannel = client.channel(channelName); - + this.clientChannel = client.channel(channelName, { config: { presence: { key: this.identity.clientId } } }); + try { console.log('[Sim] clientChannel created for', channelName, 'with presence key', this.identity.clientId); } catch {} + this._indexClaims = []; + this._indexTimer = null; + this.clientChannel .on('broadcast', {}, (data) => { - console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); + // console.log(`[SpaceCraft] RECEIVED BROADCAST EVENT: ${data.event}`, data); if (data.payload && data.payload.targetSimulatorId) { - console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + // console.log(`[SpaceCraft] Event targeted at simulator: ${data.payload.targetSimulatorId}, my ID: ${this.identity.clientId}`); + } + }) + + .on('broadcast', { event: 'indexClaim' }, (data) => { + try { + const claim = data && data.payload || {}; + if (!claim.clientId || typeof claim.index !== 'number') return; + const now = Date.now(); + this._indexClaims.push({ clientId: claim.clientId, index: claim.index, ts: now }); + // keep recent 2s + this._indexClaims = this._indexClaims.filter(c => now - c.ts < 2000); + try { + console.log('[Sim] received indexClaim:', { clientId: claim.clientId, index: claim.index, ts: now }); + console.log('[Sim] indexClaim window (<=2s):', this._indexClaims); + } catch {} + } catch {} + }) + + .on('presence', { event: 'sync' }, () => { + try { console.log('[Sim] presence:sync received'); this.ensureUniqueSimulatorName(); } catch (e) { + console.warn('[SpaceCraft] ensureUniqueSimulatorName error:', e); } }) @@ -943,7 +989,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast createMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -976,7 +1022,7 @@ class SpaceCraftSim { const clientName = data.payload.clientName || ""; const magnetData = data.payload.magnetData; try { - console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); + // console.log('[Sim JS] broadcast updateMagnet payload magnetData:', JSON.parse(JSON.stringify(magnetData))); } catch {} const magnetId = magnetData.magnetId; @@ -1064,36 +1110,15 @@ class SpaceCraftSim { this.spaceCraft.pushMagnet(magnetId, deltaX, deltaY); }) - .on('broadcast', { event: 'simulatorTakeover' }, (data) => { - // Another simulator is trying to take over - if (data.payload.newSimulatorId === this.identity.clientId) { - return; // Our own takeover message, ignore - } - - // If this simulator started later than the new one, let it take over - if (this.identity.startTime < data.payload.startTime) { - console.log(`[SpaceCraft] Another simulator has taken over: ${data.payload.newSimulatorId}`); - this.shutdown(); - } else { - console.log(`[SpaceCraft] Ignoring takeover from older simulator: ${data.payload.newSimulatorId}`); - this.sendTakeoverEvent(); - } - }) - - .on('presence', { event: 'sync' }, () => { - const allPresences = this.clientChannel.presenceState(); - }) - .on('presence', { event: 'join' }, ({ newPresences }) => { for (const presence of newPresences) { // Skip our own presence if (presence.clientId === this.identity.clientId) continue; + // console.log(`[SpaceCraft] Another ${presence.clientType} joined: ${presence.clientId} ${presence.clientName}`); + // Check if this is a simulator joining if (presence.clientType === "simulator") { - console.log(`[SpaceCraft] Another simulator joined: ${presence.clientId}`); - // Send takeover event to establish dominance - this.sendTakeoverEvent(); } else { // A controller client joined this.updateClientInfo( @@ -1109,73 +1134,133 @@ class SpaceCraftSim { for (const presence of leftPresences) { // Remove from our client registry if (this.clients[presence.clientId]) { - console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); + // console.log(`[SpaceCraft] Client left: ${presence.clientId} (${presence.clientType || 'unknown type'})`); delete this.clients[presence.clientId]; } } }) - .subscribe((status) => { + .subscribe((status) => { + try { console.log('[Sim] subscribe status:', status); } catch {} if (status === 'SUBSCRIBED') { - console.log("[SpaceCraft] Successfully subscribed to client channel"); - + try { console.log('[Sim] SUBSCRIBED: tracking presence and starting index negotiation'); } catch {} + try { console.log('[Sim] track identity:', { clientId: this.identity.clientId, clientName: this.identity.clientName, simulatorIndex: this.identity.simulatorIndex }); } catch {} this.clientChannel.track({ ...this.identity, - shared: { ...this.state } // Nest state under 'shared' + shared: { ...this.state } }); - - this.sendTakeoverEvent(); + try { this.ensureUniqueSimulatorName(); } catch {} } }); + // Attempt to restore previously assigned simulator index for this channel (persists across reconnects) + // Intentionally NOT restoring simulatorIndex from localStorage to avoid duplicate indices across tabs + try { console.log('[Sim] init: not restoring simulatorIndex from localStorage; will negotiate via presence'); } catch {} + this.syncStateToPresence(); } - /** - * Sends a takeover event to notify other simulators and clients - */ - sendTakeoverEvent() { - if (!this.clientChannel) { - console.warn("[SpaceCraft] Cannot send takeover event: Client channel not initialized."); - return; - } - - // console.log(`[SpaceCraft] Sending simulator takeover notification`); - - this.clientChannel.send({ - type: 'broadcast', - event: 'simulatorTakeover', - payload: { - newSimulatorId: this.identity.clientId, - newSimulatorName: this.identity.clientName, - startTime: this.identity.startTime - } - }).catch(err => console.error("[SpaceCraft] Error sending takeover event:", err)); + broadcastIndexClaim(index) { + try { + console.log('[Sim] broadcastIndexClaim sending', { index, clientId: this.identity && this.identity.clientId }); + this.clientChannel && this.clientChannel.send({ + type: 'broadcast', + event: 'indexClaim', + payload: { clientId: this.identity.clientId, index } + }); + } catch {} } - - /** - * Gracefully shuts down this simulator instance - */ - shutdown() { - console.log("[SpaceCraft] Shutting down simulator due to takeover"); - - // Clean up resources - no try/catch, just direct error logging - if (this.clientChannel) { - this.clientChannel.unsubscribe().catch(err => { - console.error("[SpaceCraft] Error unsubscribing from channel:", err); + + ensureUniqueSimulatorName() { + try { + // If already assigned, nothing to do + if (this.simulatorIndex && this.simulatorIndex > 0) return; + const state = this.clientChannel && this.clientChannel.presenceState ? this.clientChannel.presenceState() : null; + if (!state) return; + const prefix = SpaceCraftSim.simulatorNamePrefix; + // Compute observed max from presence (explicit index, name suffix) and shared maxIndexSeen + try { console.log('[Sim] ensureUniqueSimulatorName start; presence keys=', Object.keys(state || {})); } catch {} + let totalSimulators = 0; + let presenceMax = 0; + let maxSeenShared = 0; + Object.values(state).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + totalSimulators += 1; + try { console.log('[Sim] presence simulator:', { clientId: p.clientId, nameTop: p.clientName, nameShared: p.shared && p.shared.clientName, idxTop: p.simulatorIndex, idxShared: p.shared && p.shared.simulatorIndex, maxIndexSeen: p.shared && p.shared.maxIndexSeen }); } catch {} + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > presenceMax) presenceMax = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > presenceMax) presenceMax = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const m = p.clientName.match(/(\d+)\s*$/); + if (m) { + const n = parseInt(m[1], 10); + if (n > presenceMax) presenceMax = n; + } + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > maxSeenShared) maxSeenShared = p.shared.maxIndexSeen; + } + }); }); - } - - // Redirect to a shutdown page or reload - alert("Another SpaceCraft simulator has taken control. This window will now close."); - window.close(); - - // If window doesn't close (e.g., window not opened by script), reload - setTimeout(() => { - window.location.reload(); - }, 1000); + // Do not read maxIndexSeen from localStorage; avoid cross-tab coupling + const now = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now - c.ts < 2000); + const claimsMax = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const base = Math.max(presenceMax, maxSeenShared, claimsMax); + const sameBaseClaims = this._indexClaims.filter(c => c.index === base); + const rank = sameBaseClaims.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const proposed = (totalSimulators <= 1 && base === 0) ? 1 : (base + rank + 1); + console.log(`[Sim] proposal: ts=${now} tot=${totalSimulators} presenceMax=${presenceMax} maxSeenShared=${maxSeenShared} claimsMax=${claimsMax} base=${base} rank=${rank} proposed=${proposed}`); + this.broadcastIndexClaim(proposed); + // finalize after a short coordination window + clearTimeout(this._indexTimer); + this._indexTimer = setTimeout(() => { + const now2 = Date.now(); + this._indexClaims = (this._indexClaims || []).filter(c => now2 - c.ts < 2000); + const claimsMax2 = this._indexClaims.reduce((a, c) => Math.max(a, c.index), 0); + const presenceMax2 = (() => { + let m = 0; + Object.values(this.clientChannel.presenceState() || {}).forEach((arr) => { + (arr || []).forEach((p) => { + if (p.clientType === 'simulator') { + if (typeof p.simulatorIndex === 'number' && p.simulatorIndex > m) m = p.simulatorIndex; + if (p.shared && typeof p.shared.simulatorIndex === 'number' && p.shared.simulatorIndex > m) m = p.shared.simulatorIndex; + if (typeof p.clientName === 'string') { + const mm = p.clientName.match(/(\d+)\s*$/); + if (mm) m = Math.max(m, parseInt(mm[1], 10)); + } + if (p.shared && typeof p.shared.maxIndexSeen === 'number' && p.shared.maxIndexSeen > m) m = p.shared.maxIndexSeen; + } + }); + }); + return m; + })(); + const base2 = Math.max(presenceMax2, claimsMax2); + const sameBase2 = this._indexClaims.filter(c => c.index === base2); + const rank2 = sameBase2.filter(c => String(c.clientId) < String(this.identity.clientId)).length; + const finalIndex = (final => (final > 0 ? final : 1))( (base2 + rank2 + 1) ); + console.log(`[Sim] finalize: claimsMax2=${claimsMax2} presenceMax2=${presenceMax2} base2=${base2} rank2=${rank2} finalIndex=${finalIndex}`); + if (!this.simulatorIndex || this.simulatorIndex === 0) { + this.simulatorIndex = finalIndex; + const nextName = `${prefix} ${this.simulatorIndex}`; + this.identity.clientName = nextName; + this.identity.simulatorIndex = this.simulatorIndex; + this.state.clientName = nextName; + this.state.simulatorIndex = this.simulatorIndex; + const newMaxSeen = Math.max(base2, this.simulatorIndex); + this.state.maxIndexSeen = newMaxSeen; + // Do not persist indices to localStorage to avoid cross-tab duplication + console.log(`[Sim] assigned: simulatorIndex=${this.simulatorIndex} clientName="${this.identity.clientName}"`); + this.syncStateToPresence(); + if (this.domContentLoaded) { + this.generateQRCodes(); + } + } + }, 400); + } catch {} } + // Takeover functionality removed: multi-simulator is supported + /** * Sends a Supabase broadcast event on the channel. * @param {string} clientId - The unique ID of the target client. @@ -1194,7 +1279,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); + // console.log(`[SpaceCraft] Sending '${eventName}' event to client ${clientId}`); // Add simulator ID to payload so client knows who sent it const fullPayload = { @@ -1220,7 +1305,7 @@ class SpaceCraftSim { return; } - console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); + // console.log(`[SpaceCraft] Broadcasting '${eventName}' event to all clients`); // Add simulator ID to payload so clients know who sent it const fullPayload = { @@ -1272,13 +1357,7 @@ class SpaceCraftSim { * Logs the current status of the SpaceCraft instance */ logStatus() { - console.log("[SpaceCraft Debug] Status:", { - DOMReady: this.domContentLoaded, - BasicInitialized: this.isInitialized, - BridgeAvailable: !!window.bridge, - SpaceCraft: !!this.spaceCraft, - SupabaseLoaded: typeof window.supabase !== 'undefined' - }); + // console.log("[SpaceCraft Debug] Status:", { DOMReady: this.domContentLoaded, BasicInitialized: this.isInitialized, BridgeAvailable: !!window.bridge, SpaceCraft: !!this.spaceCraft, SupabaseLoaded: typeof window.supabase !== 'undefined' }); } /** @@ -1354,7 +1433,7 @@ class SpaceCraftSim { */ createUnifiedTagsList() { if (!this.loadedContent || !this.loadedContent.collections) { - console.log("[SpaceCraft] No collections data available for creating tags list"); + // console.log("[SpaceCraft] No collections data available for creating tags list"); return []; } @@ -1380,7 +1459,7 @@ class SpaceCraftSim { } const sortedTags = Array.from(allTags).sort(); - console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); + // console.log(`[SpaceCraft] Created unified tags list with ${sortedTags.length} unique tags`); return sortedTags; } } @@ -1408,4 +1487,4 @@ if (document.readyState === 'loading') { // setInterval(() => window.SpaceCraft.logStatus(), 5000); } -console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); +// console.log("[SpaceCraft] spacecraft.js loaded. Waiting for DOMContentLoaded to initialize..."); diff --git a/WebSites/controller/build/SimulatorState.d.ts b/WebSites/controller/build/SimulatorState.d.ts index d1014429..dcb60e47 100644 --- a/WebSites/controller/build/SimulatorState.d.ts +++ b/WebSites/controller/build/SimulatorState.d.ts @@ -13,8 +13,6 @@ export declare class SimulatorState extends Node { currentCollectionId: string; currentCollectionItems: Array; currentScreenId: string; - currentSearchGravity: number; - currentSearchString: string; highlightedItem: any; highlightedItemId: string; highlightedItemIds: Array; diff --git a/WebSites/controller/build/SimulatorState.d.ts.map b/WebSites/controller/build/SimulatorState.d.ts.map index ef527e1e..91c81426 100644 --- a/WebSites/controller/build/SimulatorState.d.ts.map +++ b/WebSites/controller/build/SimulatorState.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"SimulatorState.d.ts","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA8B,SAAS,EAAE,MAAM,QAAQ,CAAC;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,EAC7C,CAAC;AAEF,qBACa,cAAe,SAAQ,IAAI;IAG5B,QAAQ,EAAE,MAAM,CAAC;IAGjB,UAAU,EAAE,MAAM,CAAC;IAGnB,UAAU,EAAE,MAAM,CAAC;IAGnB,gBAAgB,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAG7B,iBAAiB,EAAE,UAAU,CAAC;IAG9B,mBAAmB,EAAE,MAAM,CAAC;IAG5B,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGtC,eAAe,EAAE,MAAM,CAAC;IAGxB,oBAAoB,EAAE,MAAM,CAAC;IAG7B,mBAAmB,EAAE,MAAM,CAAC;IAG5B,eAAe,EAAE,GAAG,CAAC;IAGrB,iBAAiB,EAAE,MAAM,CAAC;IAG1B,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGlC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAGpB,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGzB,YAAY,EAAE,GAAG,CAAC;IAGlB,cAAc,EAAE,MAAM,CAAC;IAGvB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAG/B,WAAW,EAAE,MAAM,CAAC;IAGpB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGpB,aAAa,EAAE,MAAM,CAAC;IAE9B,MAAM,CAAC,KAAK,EAAE,cAAc;CA0B/B"} \ No newline at end of file +{"version":3,"file":"SimulatorState.d.ts","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAA8B,SAAS,EAAE,MAAM,QAAQ,CAAC;AAErE,MAAM,MAAM,UAAU,GAAG;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,EAAE,EAAE,MAAM,CAAC;CACZ,CAAA;AAED,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,EAC7C,CAAC;AAEF,qBACa,cAAe,SAAQ,IAAI;IAG5B,QAAQ,EAAE,MAAM,CAAC;IAGjB,UAAU,EAAE,MAAM,CAAC;IAGnB,UAAU,EAAE,MAAM,CAAC;IAGnB,gBAAgB,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAG7B,iBAAiB,EAAE,UAAU,CAAC;IAG9B,mBAAmB,EAAE,MAAM,CAAC;IAG5B,sBAAsB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGtC,eAAe,EAAE,MAAM,CAAC;IAGxB,eAAe,EAAE,GAAG,CAAC;IAGrB,iBAAiB,EAAE,MAAM,CAAC;IAG1B,kBAAkB,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGlC,OAAO,EAAE,KAAK,CAAC,GAAG,CAAC,CAAC;IAGpB,SAAS,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGzB,YAAY,EAAE,GAAG,CAAC;IAGlB,cAAc,EAAE,MAAM,CAAC;IAGvB,eAAe,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAG/B,WAAW,EAAE,MAAM,CAAC;IAGpB,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;IAGpB,aAAa,EAAE,MAAM,CAAC;IAE9B,MAAM,CAAC,KAAK,EAAE,cAAc;CAwB/B"} \ No newline at end of file diff --git a/WebSites/controller/build/SimulatorState.js b/WebSites/controller/build/SimulatorState.js index 903c53ee..788613df 100644 --- a/WebSites/controller/build/SimulatorState.js +++ b/WebSites/controller/build/SimulatorState.js @@ -16,8 +16,6 @@ let SimulatorState = class SimulatorState extends Node { currentCollectionId: state.currentCollectionId || '', currentCollectionItems: state.currentCollectionItems || [], currentScreenId: state.currentScreenId || '', - currentSearchGravity: state.currentSearchGravity || 0, - currentSearchString: state.currentSearchString || '', highlightedItem: state.highlightedItem || null, highlightedItemId: state.highlightedItemId || '', highlightedItemIds: state.highlightedItemIds || [], @@ -56,12 +54,6 @@ __decorate([ __decorate([ ReactiveProperty({ type: String }) ], SimulatorState.prototype, "currentScreenId", void 0); -__decorate([ - ReactiveProperty({ type: Number }) -], SimulatorState.prototype, "currentSearchGravity", void 0); -__decorate([ - ReactiveProperty({ type: String }) -], SimulatorState.prototype, "currentSearchString", void 0); __decorate([ ReactiveProperty({ type: Object }) ], SimulatorState.prototype, "highlightedItem", void 0); diff --git a/WebSites/controller/build/SimulatorState.js.map b/WebSites/controller/build/SimulatorState.js.map index dd5fce39..5df9324c 100644 --- a/WebSites/controller/build/SimulatorState.js.map +++ b/WebSites/controller/build/SimulatorState.js.map @@ -1 +1 @@ -{"version":3,"file":"SimulatorState.js","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":"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","sourcesContent":["import { Node, Register, ReactiveProperty, NodeProps } from 'io-gui';\n\nexport type Collection = {\n description: string;\n id: string;\n}\n\nexport type SimulatorStateProps = NodeProps & {\n};\n\n@Register\nexport class SimulatorState extends Node {\n\n @ReactiveProperty({type: String})\n declare clientId: string;\n\n @ReactiveProperty({type: String})\n declare clientName: string;\n\n @ReactiveProperty({type: String})\n declare clientType: string;\n\n @ReactiveProperty()\n declare connectedClients: Array; // TODO: define type\n\n @ReactiveProperty({type: Object})\n declare currentCollection: Collection;\n\n @ReactiveProperty()\n declare currentCollectionId: string;\n\n @ReactiveProperty({type: Array})\n declare currentCollectionItems: Array;\n\n @ReactiveProperty({type: String})\n declare currentScreenId: string;\n\n @ReactiveProperty({type: Number})\n declare currentSearchGravity: number;\n\n @ReactiveProperty({type: String})\n declare currentSearchString: string;\n\n @ReactiveProperty({type: Object})\n declare highlightedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare highlightedItemId: string;\n\n @ReactiveProperty({type: Array})\n declare highlightedItemIds: Array;\n\n @ReactiveProperty({type: Array})\n declare magnets: Array; // TODO: define type\n\n @ReactiveProperty({type: Array})\n declare screenIds: Array;\n\n @ReactiveProperty({type: Object})\n declare selectedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare selectedItemId: string; // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: Array})\n declare selectedItemIds: Array;\n\n @ReactiveProperty({type: String})\n declare lastUpdated: string;\n\n @ReactiveProperty({type: Array})\n declare tags: Array;\n\n @ReactiveProperty({type: Number})\n declare updateCounter: number;\n\n update(state: SimulatorState) {\n this.setProperties({\n clientId: state.clientId || '',\n clientName: state.clientName || '',\n clientType: state.clientType || '',\n connectedClients: state.connectedClients || [],\n currentCollection: state.currentCollection || {},\n currentCollectionId: state.currentCollectionId || '',\n currentCollectionItems: state.currentCollectionItems || [],\n currentScreenId: state.currentScreenId || '',\n currentSearchGravity: state.currentSearchGravity || 0,\n currentSearchString: state.currentSearchString || '',\n highlightedItem: state.highlightedItem || null,\n highlightedItemId: state.highlightedItemId || '',\n highlightedItemIds: state.highlightedItemIds || [],\n magnets: state.magnets || [],\n screenIds: state.screenIds || [],\n selectedItem: state.selectedItem || null,\n selectedItemId: state.selectedItemId || '',\n selectedItemIds: state.selectedItemIds || [],\n lastUpdated: state.lastUpdated,\n tags: state.tags || [],\n updateCounter: state.updateCounter,\n });\n }\n\n}"]} \ No newline at end of file +{"version":3,"file":"SimulatorState.js","sourceRoot":"","sources":["../src/SimulatorState.ts"],"names":[],"mappings":"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","sourcesContent":["import { Node, Register, ReactiveProperty, NodeProps } from 'io-gui';\n\nexport type Collection = {\n description: string;\n id: string;\n}\n\nexport type SimulatorStateProps = NodeProps & {\n};\n\n@Register\nexport class SimulatorState extends Node {\n\n @ReactiveProperty({type: String})\n declare clientId: string;\n\n @ReactiveProperty({type: String})\n declare clientName: string;\n\n @ReactiveProperty({type: String})\n declare clientType: string;\n\n @ReactiveProperty()\n declare connectedClients: Array; // TODO: define type\n\n @ReactiveProperty({type: Object})\n declare currentCollection: Collection;\n\n @ReactiveProperty()\n declare currentCollectionId: string;\n\n @ReactiveProperty({type: Array})\n declare currentCollectionItems: Array;\n\n @ReactiveProperty({type: String})\n declare currentScreenId: string;\n\n @ReactiveProperty({type: Object})\n declare highlightedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare highlightedItemId: string;\n\n @ReactiveProperty({type: Array})\n declare highlightedItemIds: Array;\n\n @ReactiveProperty({type: Array})\n declare magnets: Array; // TODO: define type\n\n @ReactiveProperty({type: Array})\n declare screenIds: Array;\n\n @ReactiveProperty({type: Object})\n declare selectedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: String})\n declare selectedItemId: string; // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …}\n\n @ReactiveProperty({type: Array})\n declare selectedItemIds: Array;\n\n @ReactiveProperty({type: String})\n declare lastUpdated: string;\n\n @ReactiveProperty({type: Array})\n declare tags: Array;\n\n @ReactiveProperty({type: Number})\n declare updateCounter: number;\n\n update(state: SimulatorState) {\n this.setProperties({\n clientId: state.clientId || '',\n clientName: state.clientName || '',\n clientType: state.clientType || '',\n connectedClients: state.connectedClients || [],\n currentCollection: state.currentCollection || {},\n currentCollectionId: state.currentCollectionId || '',\n currentCollectionItems: state.currentCollectionItems || [],\n currentScreenId: state.currentScreenId || '',\n highlightedItem: state.highlightedItem || null,\n highlightedItemId: state.highlightedItemId || '',\n highlightedItemIds: state.highlightedItemIds || [],\n magnets: state.magnets || [],\n screenIds: state.screenIds || [],\n selectedItem: state.selectedItem || null,\n selectedItemId: state.selectedItemId || '',\n selectedItemIds: state.selectedItemIds || [],\n lastUpdated: state.lastUpdated,\n tags: state.tags || [],\n updateCounter: state.updateCounter,\n });\n }\n\n}"]} \ No newline at end of file diff --git a/WebSites/controller/build/SpacetimeController.d.ts b/WebSites/controller/build/SpacetimeController.d.ts index add4d00d..12521133 100644 --- a/WebSites/controller/build/SpacetimeController.d.ts +++ b/WebSites/controller/build/SpacetimeController.d.ts @@ -46,8 +46,10 @@ export declare class SpacetimeController extends IoElement { clientChannel: any; clientConnected: boolean; currentSimulatorId: string | null; + currentSimulators: Map; magnetViewMetadata: Array; simulatorState: SimulatorState; + simulatorRosterTick: number; constructor(props: IoElementProps); connect(): void; ready(): void; @@ -61,8 +63,9 @@ export declare class SpacetimeController extends IoElement { sendEventToSimulator(eventType: string, data: any): void; setupPresenceHandlers(): void; subscribeToChannel(): void; + setCurrentSimulator(simId: string): void; updatePresenceState(): Promise; - findLatestSimulator(presenceState: PresenceState): SimulatorPresence | null; + findSimulator(presenceState: PresenceState): SimulatorPresence | null; } export {}; //# sourceMappingURL=SpacetimeController.d.ts.map \ No newline at end of file diff --git a/WebSites/controller/build/SpacetimeController.d.ts.map b/WebSites/controller/build/SpacetimeController.d.ts.map index 9295a56f..5680318e 100644 --- a/WebSites/controller/build/SpacetimeController.d.ts.map +++ b/WebSites/controller/build/SpacetimeController.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"SpacetimeController.d.ts","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAiF,cAAc,EAAkB,MAAM,QAAQ,CAAC;AAMlJ,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAa7C,KAAK,aAAa,GAAG;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;CAC3B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAQD,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,GAAG,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAA;AAED,qBACa,mBAAoB,SAAQ,SAAS;IAC9C,MAAM,KAAK,KAAK,WAaf;IAED,MAAM,CAAC,WAAW,SAA8C;IAChE,MAAM,CAAC,eAAe,SAAsN;IAC5O,MAAM,CAAC,iBAAiB,SAAgB;IACxC,MAAM,CAAC,UAAU,SAAgB;IAEzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,GAAG,CAAC;IACpB,aAAa,EAAE,GAAG,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAElC,kBAAkB,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAGxC,cAAc,EAAE,cAAc,CAAC;gBAE3B,KAAK,EAAE,cAAc;IAejC,OAAO;IAmBP,KAAK;IA+BL,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAI3C,aAAa,CAAC,SAAS,EAAE,MAAM;IAI/B,eAAe,CAAC,MAAM,EAAE,MAAM;IAI9B,qBAAqB,CAAC,UAAU,EAAE,MAAM;IASxC,qBAAqB,CAAC,UAAU,EAAE,MAAM;IAIxC,qBAAqB,CAAC,QAAQ,EAAE,MAAM;IAItC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAIpE,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IA6BjD,qBAAqB;IAiBrB,kBAAkB;IAcZ,mBAAmB;IAezB,mBAAmB,CAAC,aAAa,EAAE,aAAa,GAAG,iBAAiB,GAAG,IAAI;CAa9E"} \ No newline at end of file +{"version":3,"file":"SpacetimeController.d.ts","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAiF,cAAc,EAAkB,MAAM,QAAQ,CAAC;AAMlJ,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACrD,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gBAAgB,CAAC;AAa7C,KAAK,aAAa,GAAG;IACnB,CAAC,GAAG,EAAE,MAAM,GAAG,QAAQ,EAAE,CAAC;CAC3B,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAED,KAAK,iBAAiB,GAAG;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,MAAM,CAAC;CACnB,CAAA;AAQD,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,GAAG,CAAC;IAClB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAC;IAClC,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;CACf,CAAA;AAED,qBACa,mBAAoB,SAAQ,SAAS;IAC9C,MAAM,KAAK,KAAK,WAaf;IAED,MAAM,CAAC,WAAW,SAA8C;IAChE,MAAM,CAAC,eAAe,SAAsN;IAC5O,MAAM,CAAC,iBAAiB,SAAgB;IACxC,MAAM,CAAC,UAAU,SAAgB;IAEzB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,cAAc,EAAE,GAAG,CAAC;IACpB,aAAa,EAAE,GAAG,CAAC;IACnB,eAAe,EAAE,OAAO,CAAC;IACzB,kBAAkB,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,iBAAiB,EAAE,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAAC;IAElD,kBAAkB,EAAE,KAAK,CAAC,YAAY,CAAC,CAAC;IAGxC,cAAc,EAAE,cAAc,CAAC;IAG/B,mBAAmB,EAAE,MAAM,CAAC;gBAExB,KAAK,EAAE,cAAc;IAiBjC,OAAO;IA0BP,KAAK;IA+BL,YAAY,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAI3C,aAAa,CAAC,SAAS,EAAE,MAAM;IAI/B,eAAe,CAAC,MAAM,EAAE,MAAM;IAI9B,qBAAqB,CAAC,UAAU,EAAE,MAAM;IASxC,qBAAqB,CAAC,UAAU,EAAE,MAAM;IAIxC,qBAAqB,CAAC,QAAQ,EAAE,MAAM;IAItC,mBAAmB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM;IAIpE,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG;IA6BjD,qBAAqB;IAoDrB,kBAAkB;IAclB,mBAAmB,CAAC,KAAK,EAAE,MAAM;IA2B3B,mBAAmB;IAezB,aAAa,CAAC,aAAa,EAAE,aAAa,GAAG,iBAAiB,GAAG,IAAI;CAkDxE"} \ No newline at end of file diff --git a/WebSites/controller/build/SpacetimeController.js b/WebSites/controller/build/SpacetimeController.js index 7fd41622..35c0c19b 100644 --- a/WebSites/controller/build/SpacetimeController.js +++ b/WebSites/controller/build/SpacetimeController.js @@ -51,7 +51,9 @@ let SpacetimeController = class SpacetimeController extends IoElement { this.clientChannel = null; this.clientConnected = false; this.currentSimulatorId = null; + this.currentSimulators = new Map(); this.magnetViewMetadata = []; + this.simulatorRosterTick = 0; this.connect(); } connect() { @@ -60,13 +62,19 @@ let SpacetimeController = class SpacetimeController extends IoElement { return; } try { - const channelName = new URLSearchParams(window.location.search).get('channel') || SpacetimeController_1.clientChannelName; + const params = new URLSearchParams(window.location.search); + const channelName = params.get('channel') || SpacetimeController_1.clientChannelName; this.supabaseClient = supabase.createClient(SpacetimeController_1.supabaseUrl, SpacetimeController_1.supabaseAnonKey); this.clientChannel = this.supabaseClient.channel(channelName, { config: { presence: { key: this.clientId } } }); this.setupPresenceHandlers(); this.subscribeToChannel(); + // Only honor simulatorIndex param (no legacy) + const simIndexFromUrl = params.get('simulatorIndex'); + if (simIndexFromUrl) { + this._preselectSimulatorIndex = parseInt(simIndexFromUrl, 10); + } } catch (error) { console.error('Controller connection failed:', error); @@ -158,15 +166,52 @@ let SpacetimeController = class SpacetimeController extends IoElement { this.clientChannel .on('presence', { event: 'sync' }, () => { const presenceState = this.clientChannel.presenceState(); - const simulator = this.findLatestSimulator(presenceState); + try { + const raw = Object.values(presenceState || {}).flat(); + const sims = raw.filter((p) => p && p.clientType === 'simulator'); + console.log('[Controller][presence:sync] presences:', raw.length, 'simulators:', sims.length); + console.log('[Controller][presence:sync] simulators raw:', sims.map((p) => ({ + clientId: p.clientId, + nameTop: p.clientName, + nameShared: p.shared && p.shared.clientName, + idxTop: typeof p.simulatorIndex === 'number' ? p.simulatorIndex : 0, + idxShared: p.shared && typeof p.shared.simulatorIndex === 'number' ? p.shared.simulatorIndex : 0 + }))); + } + catch { } + const simulator = this.findSimulator(presenceState); if (simulator) { + // If preselect by index requested, override with match when available + if (this._preselectSimulatorIndex) { + for (const sim of this.currentSimulators.values()) { + const idx = sim.simulatorIndex || (sim.shared && sim.shared.simulatorIndex); + if (idx === this._preselectSimulatorIndex) { + this.currentSimulatorId = sim.clientId; + break; + } + } + this._preselectSimulatorIndex = null; + } this.magnetViewMetadata = simulator.shared.unityMetaData?.MagnetView || []; - this.currentSimulatorId = simulator.clientId; + this.currentSimulatorId = this.currentSimulatorId || simulator.clientId; this.simulatorState.update(simulator.shared); + // bump tick so UI re-renders simulator menus + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; + try { + const list = Array.from(this.currentSimulators.values()).map((s) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] roster updated (tick', this.simulatorRosterTick, '):', list); + console.log('[Controller] currentSimulatorId:', this.currentSimulatorId); + } + catch { } + } + else { + // No simulator selected yet; still bump tick to refresh menu state + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; } - }) - .on('broadcast', { event: 'simulatorTakeover' }, (payload) => { - this.currentSimulatorId = payload.newSimulatorId; }); } subscribeToChannel() { @@ -182,6 +227,35 @@ let SpacetimeController = class SpacetimeController extends IoElement { } }); } + setCurrentSimulator(simId) { + this.currentSimulatorId = simId; + if (!this.clientChannel) + return; + // Pull fresh presence state from Supabase and switch to the selected simulator's state + const presenceState = this.clientChannel.presenceState(); + const sim = this.findSimulator(presenceState); + if (sim) { + this.magnetViewMetadata = sim.shared.unityMetaData?.MagnetView || []; + this.simulatorState.update(sim.shared); + } + try { + const sel = this.currentSimulators.get(simId); + console.log('[Controller] setCurrentSimulator:', simId, 'name=', sel && (sel.clientName || (sel.shared && sel.shared.clientName)), 'index=', sel && (sel.simulatorIndex || (sel.shared && sel.shared.simulatorIndex))); + } + catch { } + // Persist simulatorIndex in URL + try { + const selected = this.currentSimulators.get(simId); + const simIndex = selected && (selected.simulatorIndex || (selected.shared && selected.shared.simulatorIndex)); + if (simIndex) { + const url = new URL(window.location.href); + url.searchParams.set('simulatorIndex', String(simIndex)); + window.history.replaceState({}, '', url.toString()); + console.log('[Controller] URL simulatorIndex set to', simIndex); + } + } + catch { } + } async updatePresenceState() { if (this.clientConnected && this.clientChannel) { try { @@ -197,23 +271,65 @@ let SpacetimeController = class SpacetimeController extends IoElement { } } } - findLatestSimulator(presenceState) { - let latestSimulator = null; - let latestStartTime = 0; - Object.values(presenceState).forEach(presences => { - presences.forEach((presence) => { - if (presence.clientType === 'simulator' && presence.startTime > latestStartTime) { - latestSimulator = presence; - latestStartTime = presence.startTime; + findSimulator(presenceState) { + let lastSimulator = null; + let simulator = null; + const values = Object.values(presenceState); + this.currentSimulators = new Map(); + for (const presences of values) { + for (const presence of presences) { + // Only count fully-initialized simulators (index assigned) + const meta = presence; + const simIndexTop = typeof meta.simulatorIndex === 'number' ? meta.simulatorIndex : 0; + const simIndexShared = meta.shared && typeof meta.shared.simulatorIndex === 'number' ? meta.shared.simulatorIndex : 0; + const simIndex = simIndexTop || simIndexShared; + if (meta.clientType === 'simulator' && simIndex > 0) { + // Prefer shared view of fields if available + const merged = { + ...meta, + clientName: (meta.shared && meta.shared.clientName) ? meta.shared.clientName : meta.clientName, + simulatorIndex: simIndex, + shared: meta.shared || {} + }; + lastSimulator = merged; + this.currentSimulators.set(meta.clientId, merged); + if (meta.clientId == this.currentSimulatorId) { + simulator = lastSimulator; + } } - }); - }); - return latestSimulator; + else if (meta.clientType === 'simulator') { + console.log('[Controller] ignoring simulator without index yet:', { + clientId: meta.clientId, + nameTop: meta.clientName, + nameShared: meta.shared && meta.shared.clientName, + idxTop: simIndexTop, + idxShared: simIndexShared + }); + } + } + } + if (!simulator) { + simulator = lastSimulator; + } + this.currentSimulatorId = simulator?.clientId || null; + try { + const list = Array.from(this.currentSimulators.values()).map((s) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] currentSimulators:', list); + } + catch { } + return simulator; } }; __decorate([ ReactiveProperty({ type: SimulatorState, init: null }) ], SpacetimeController.prototype, "simulatorState", void 0); +__decorate([ + ReactiveProperty({ type: Number }) +], SpacetimeController.prototype, "simulatorRosterTick", void 0); SpacetimeController = SpacetimeController_1 = __decorate([ Register ], SpacetimeController); diff --git a/WebSites/controller/build/SpacetimeController.js.map b/WebSites/controller/build/SpacetimeController.js.map index 36302db4..6a287712 100644 --- a/WebSites/controller/build/SpacetimeController.js.map +++ b/WebSites/controller/build/SpacetimeController.js.map @@ -1 +1 @@ -{"version":3,"file":"SpacetimeController.js","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":"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","sourcesContent":["import { IoElement, Register, ioNavigator, MenuOption, Storage as $, ioMarkdown, ReactiveProperty, IoElementProps, ThemeSingleton } from 'io-gui';\nimport { tabNavigate } from './TabNavigate.js';\nimport { tabSelect } from './TabSelect.js';\nimport { tabInspect } from './TabInspect.js';\nimport { tabMagnet } from './TabMagnet.js';\nimport { tabAdjust } from './TabAdjust.js';\nimport { SimulatorState } from './SimulatorState.js';\nimport type { Magnet } from './types/Magnet';\n\nThemeSingleton.themeID = 'dark';\n\nfunction generateClientId() {\n return 'controller-' + Math.random().toString(36).substr(2, 9);\n}\nfunction generateClientName() {\n const now = new Date();\n const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);\n return `Controller-${timestamp}`;\n}\n\ntype PresenceState = {\n [key: string]: Presence[];\n};\n\ntype Presence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n startTime: number;\n}\n\ntype SimulatorPresence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n shared: SimulatorState;\n startTime: number;\n}\n\ntype SimulatorTakeoverPayload = {\n newSimulatorId: string;\n newSimulatorName: string;\n startTime: number;\n}\n\nexport type ViewMetadata = {\n canWrite: boolean;\n category: string;\n component: string;\n defaultValue: any;\n description: string;\n displayName: string;\n name: string;\n path: string;\n type: 'bool' | 'float' | 'string';\n unityType: string;\n min?: number,\n max?: number,\n step?: number,\n}\n\n@Register\nexport class SpacetimeController extends IoElement {\n static get Style() {\n return /* css */`\n :host {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n :host > io-navigator {\n flex: 1 1 auto;\n overflow: hidden;\n }\n `;\n }\n\n static supabaseUrl = 'https://gwodhwyvuftyrvbymmvc.supabase.co';\n static supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd3b2Rod3l2dWZ0eXJ2YnltbXZjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIzNDkyMDMsImV4cCI6MjA1NzkyNTIwM30.APVpyOupY84gQ7c0vBZkY-GqoJRPhb4oD4Lcj9CEzlc';\n static clientChannelName = 'spacecraft';\n static clientType = 'controller';\n\n declare clientId: string;\n declare clientName: string;\n declare supabaseClient: any;\n declare clientChannel: any;\n declare clientConnected: boolean;\n declare currentSimulatorId: string | null;\n\n declare magnetViewMetadata: Array;\n\n @ReactiveProperty({type: SimulatorState, init: null})\n declare simulatorState: SimulatorState;\n\n constructor(props: IoElementProps) {\n super(props);\n // Client Identity\n this.clientId = generateClientId();\n this.clientName = generateClientName();\n\n // Connection State\n this.supabaseClient = null;\n this.clientChannel = null;\n this.clientConnected = false;\n this.currentSimulatorId = null;\n this.magnetViewMetadata = [];\n this.connect();\n }\n\n connect() {\n if (typeof supabase === 'undefined') {\n console.error('Supabase library missing!');\n return;\n }\n try {\n const channelName = new URLSearchParams(window.location.search).get('channel') || SpacetimeController.clientChannelName;\n this.supabaseClient = supabase.createClient(SpacetimeController.supabaseUrl, SpacetimeController.supabaseAnonKey);\n this.clientChannel = this.supabaseClient.channel(channelName, {\n config: { presence: { key: this.clientId } }\n });\n this.setupPresenceHandlers();\n this.subscribeToChannel();\n } catch (error) {\n console.error('Controller connection failed:', error);\n console.error('[Controller] Connection failed:', error);\n }\n }\n\n ready() {\n this.render([\n ioNavigator({\n menu: 'top',\n caching: 'proactive',\n option: new MenuOption({\n id: 'root',\n options: [\n {id: 'About', icon: '📖'},\n {id: 'Navigate', icon: '🧭'},\n {id: 'Select', icon: '👆'},\n {id: 'Inspect', icon: '🔍'},\n {id: 'Magnet', icon: '🧲'},\n {id: 'Adjust', icon: '⚙️'},\n ],\n selectedID: $({key: 'page', storage: 'hash', value: 'About'})\n }),\n elements: [\n ioMarkdown({id: 'About', src: './docs/About.md'}),\n tabNavigate({id: 'Navigate', controller: this, simulatorState: this.simulatorState}),\n tabSelect({id: 'Select', controller: this, simulatorState: this.simulatorState}),\n tabInspect({id: 'Inspect', controller: this, simulatorState: this.simulatorState}),\n tabMagnet({id: 'Magnet', controller: this, simulatorState: this.simulatorState}),\n tabAdjust({id: 'Adjust', controller: this, simulatorState: this.simulatorState}),\n ]\n })\n ]);\n }\n\n // === UNITY COMMUNICATION ===\n\n sendPanEvent(deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pan', { panXDelta: deltaX, panYDelta: deltaY });\n }\n\n sendZoomEvent(zoomDelta: number) {\n this.sendEventToSimulator('zoom', { zoomDelta });\n }\n\n sendSelectEvent(action: string) {\n this.sendEventToSimulator('select', { action });\n }\n\n sendCreateMagnetEvent(magnetData: Magnet) {\n try {\n console.log('[Controller] sendCreateMagnetEvent magnetData:', JSON.parse(JSON.stringify(magnetData)));\n } catch (e) {\n console.log('[Controller] sendCreateMagnetEvent magnetData (raw):', magnetData);\n }\n this.sendEventToSimulator('createMagnet', { magnetData });\n }\n\n sendUpdateMagnetEvent(magnetData: Magnet) {\n this.sendEventToSimulator('updateMagnet', { magnetData });\n }\n\n sendDeleteMagnetEvent(magnetId: string) {\n this.sendEventToSimulator('deleteMagnet', { magnetId });\n }\n\n sendPushMagnetEvent(magnetId: string, deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pushMagnet', { magnetId, deltaX, deltaY });\n }\n\n sendEventToSimulator(eventType: string, data: any) {\n if (!this.clientChannel) {\n console.error('[Controller] Cannot send event - no client channel');\n return;\n }\n\n if (!this.currentSimulatorId) {\n console.error('[Controller] Cannot send event - no current simulator ID');\n return;\n }\n\n const payload = {\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n screenId: 'main',\n targetSimulatorId: this.currentSimulatorId,\n ...data\n };\n\n this.clientChannel.send({\n type: 'broadcast',\n event: eventType,\n payload: payload\n }).catch((err: any) => {\n console.error(`[Controller] Send '${eventType}' failed:`, err);\n });\n }\n\n setupPresenceHandlers() {\n this.clientChannel\n .on('presence', { event: 'sync' }, () => {\n const presenceState = this.clientChannel.presenceState();\n const simulator = this.findLatestSimulator(presenceState);\n \n if (simulator) {\n this.magnetViewMetadata = (simulator.shared as any).unityMetaData?.MagnetView || [];\n this.currentSimulatorId = simulator.clientId;\n this.simulatorState.update(simulator.shared);\n }\n })\n .on('broadcast', { event: 'simulatorTakeover' }, (payload: SimulatorTakeoverPayload) => {\n this.currentSimulatorId = payload.newSimulatorId;\n });\n }\n\n subscribeToChannel() {\n this.clientChannel.subscribe(async (status: string) => {\n if (status === 'SUBSCRIBED') {\n this.clientConnected = true;\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n }\n });\n }\n\n async updatePresenceState() {\n if (this.clientConnected && this.clientChannel) {\n try {\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n } catch (error) {\n console.error('[Connection] Failed to update presence:', error);\n }\n }\n }\n\n findLatestSimulator(presenceState: PresenceState): SimulatorPresence | null {\n let latestSimulator = null;\n let latestStartTime = 0;\n Object.values(presenceState).forEach(presences => {\n presences.forEach((presence: Presence) => {\n if (presence.clientType === 'simulator' && presence.startTime > latestStartTime) {\n latestSimulator = presence;\n latestStartTime = presence.startTime;\n }\n });\n });\n return latestSimulator;\n }\n}"]} \ No newline at end of file +{"version":3,"file":"SpacetimeController.js","sourceRoot":"","sources":["../src/SpacetimeController.ts"],"names":[],"mappings":";;;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,IAAI,CAAC,EAAE,UAAU,EAAE,gBAAgB,EAAkB,cAAc,EAAE,MAAM,QAAQ,CAAC;AAClJ,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAC3C,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAGrD,cAAc,CAAC,OAAO,GAAG,MAAM,CAAC;AAEhC,SAAS,gBAAgB;IACrB,OAAO,aAAa,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;AACnE,CAAC;AACD,SAAS,kBAAkB;IACvB,MAAM,GAAG,GAAG,IAAI,IAAI,EAAE,CAAC;IACvB,MAAM,SAAS,GAAG,GAAG,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;IACvE,OAAO,cAAc,SAAS,EAAE,CAAC;AACrC,CAAC;AA8CM,IAAM,mBAAmB,GAAzB,MAAM,mBAAoB,SAAQ,SAAS;;IAC9C,MAAM,KAAK,KAAK;QACZ,OAAO,SAAS,CAAA;;;;;;;;;;;KAWnB,CAAC;IACF,CAAC;IAED,MAAM,CAAC,WAAW,GAAG,0CAA0C,CAAC;IAChE,MAAM,CAAC,eAAe,GAAG,kNAAkN,CAAC;IAC5O,MAAM,CAAC,iBAAiB,GAAG,YAAY,CAAC;IACxC,MAAM,CAAC,UAAU,GAAG,YAAY,CAAC;IAkBjC,YAAY,KAAqB;QAC7B,KAAK,CAAC,KAAK,CAAC,CAAC;QACb,kBAAkB;QAClB,IAAI,CAAC,QAAQ,GAAG,gBAAgB,EAAE,CAAC;QACnC,IAAI,CAAC,UAAU,GAAG,kBAAkB,EAAE,CAAC;QAEvC,mBAAmB;QACnB,IAAI,CAAC,cAAc,GAAG,IAAI,CAAC;QAC3B,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC;QAC1B,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;QAC7B,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC;QAC/B,IAAI,CAAC,iBAAiB,GAAG,IAAI,GAAG,EAAE,CAAC;QACnC,IAAI,CAAC,kBAAkB,GAAG,EAAE,CAAC;QAC7B,IAAI,CAAC,mBAAmB,GAAG,CAAC,CAAC;QAC7B,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAED,OAAO;QACH,IAAI,OAAO,QAAQ,KAAK,WAAW,EAAE,CAAC;YAClC,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;YAC3C,OAAO;QACX,CAAC;QACD,IAAI,CAAC;YACD,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC;YAC3D,MAAM,WAAW,GAAG,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,qBAAmB,CAAC,iBAAiB,CAAC;YACnF,IAAI,CAAC,cAAc,GAAG,QAAQ,CAAC,YAAY,CAAC,qBAAmB,CAAC,WAAW,EAAE,qBAAmB,CAAC,eAAe,CAAC,CAAC;YAClH,IAAI,CAAC,aAAa,GAAG,IAAI,CAAC,cAAc,CAAC,OAAO,CAAC,WAAW,EAAE;gBAC1D,MAAM,EAAE,EAAE,QAAQ,EAAE,EAAE,GAAG,EAAE,IAAI,CAAC,QAAQ,EAAE,EAAE;aAC/C,CAAC,CAAC;YACH,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC7B,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC1B,8CAA8C;YAC9C,MAAM,eAAe,GAAG,MAAM,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC;YACrD,IAAI,eAAe,EAAE,CAAC;gBACjB,IAAY,CAAC,wBAAwB,GAAG,QAAQ,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;YAC3E,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACb,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,KAAK,CAAC,CAAC;YACtD,OAAO,CAAC,KAAK,CAAC,iCAAiC,EAAE,KAAK,CAAC,CAAC;QAC5D,CAAC;IACL,CAAC;IAGD,KAAK;QACD,IAAI,CAAC,MAAM,CAAC;YACR,WAAW,CAAC;gBACR,IAAI,EAAE,KAAK;gBACX,OAAO,EAAE,WAAW;gBACpB,MAAM,EAAE,IAAI,UAAU,CAAC;oBACnB,EAAE,EAAE,MAAM;oBACV,OAAO,EAAE;wBACL,EAAC,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,IAAI,EAAC;wBACzB,EAAC,EAAE,EAAE,UAAU,EAAE,IAAI,EAAE,IAAI,EAAC;wBAC5B,EAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAC;wBAC1B,EAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,IAAI,EAAC;wBAC3B,EAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAC;wBAC1B,EAAC,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,IAAI,EAAC;qBAC7B;oBACD,UAAU,EAAE,CAAC,CAAC,EAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAC,CAAC;iBAChE,CAAC;gBACF,QAAQ,EAAE;oBACN,UAAU,CAAC,EAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,iBAAiB,EAAC,CAAC;oBACjD,WAAW,CAAC,EAAC,EAAE,EAAE,UAAU,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAC,CAAC;oBACpF,SAAS,CAAC,EAAC,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAC,CAAC;oBAChF,UAAU,CAAC,EAAC,EAAE,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAC,CAAC;oBAClF,SAAS,CAAC,EAAC,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAC,CAAC;oBAChF,SAAS,CAAC,EAAC,EAAE,EAAE,QAAQ,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,CAAC,cAAc,EAAC,CAAC;iBACnF;aACJ,CAAC;SACL,CAAC,CAAC;IACP,CAAC;IAED,8BAA8B;IAE9B,YAAY,CAAC,MAAc,EAAE,MAAc;QACvC,IAAI,CAAC,oBAAoB,CAAC,KAAK,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC;IAC/E,CAAC;IAED,aAAa,CAAC,SAAiB;QAC3B,IAAI,CAAC,oBAAoB,CAAC,MAAM,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;IACrD,CAAC;IAED,eAAe,CAAC,MAAc;QAC1B,IAAI,CAAC,oBAAoB,CAAC,QAAQ,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;IACpD,CAAC;IAED,qBAAqB,CAAC,UAAkB;QACpC,IAAI,CAAC;YACD,OAAO,CAAC,GAAG,CAAC,gDAAgD,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;QAC1G,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACT,OAAO,CAAC,GAAG,CAAC,sDAAsD,EAAE,UAAU,CAAC,CAAC;QACpF,CAAC;QACD,IAAI,CAAC,oBAAoB,CAAC,cAAc,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,qBAAqB,CAAC,UAAkB;QACpC,IAAI,CAAC,oBAAoB,CAAC,cAAc,EAAE,EAAE,UAAU,EAAE,CAAC,CAAC;IAC9D,CAAC;IAED,qBAAqB,CAAC,QAAgB;QAClC,IAAI,CAAC,oBAAoB,CAAC,cAAc,EAAE,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC5D,CAAC;IAED,mBAAmB,CAAC,QAAgB,EAAE,MAAc,EAAE,MAAc;QAChE,IAAI,CAAC,oBAAoB,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1E,CAAC;IAED,oBAAoB,CAAC,SAAiB,EAAE,IAAS;QAC7C,IAAI,CAAC,IAAI,CAAC,aAAa,EAAE,CAAC;YACtB,OAAO,CAAC,KAAK,CAAC,oDAAoD,CAAC,CAAC;YACpE,OAAO;QACX,CAAC;QAED,IAAI,CAAC,IAAI,CAAC,kBAAkB,EAAE,CAAC;YAC3B,OAAO,CAAC,KAAK,CAAC,0DAA0D,CAAC,CAAC;YAC1E,OAAO;QACX,CAAC;QAED,MAAM,OAAO,GAAG;YACZ,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,UAAU,EAAE,qBAAmB,CAAC,UAAU;YAC1C,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,QAAQ,EAAE,MAAM;YAChB,iBAAiB,EAAE,IAAI,CAAC,kBAAkB;YAC1C,GAAG,IAAI;SACV,CAAC;QAEF,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC;YACpB,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,SAAS;YAChB,OAAO,EAAE,OAAO;SACnB,CAAC,CAAC,KAAK,CAAC,CAAC,GAAQ,EAAE,EAAE;YAClB,OAAO,CAAC,KAAK,CAAC,sBAAsB,SAAS,WAAW,EAAE,GAAG,CAAC,CAAC;QACnE,CAAC,CAAC,CAAC;IACP,CAAC;IAED,qBAAqB;QACjB,IAAI,CAAC,aAAa;aACb,EAAE,CAAC,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE;YACpC,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,aAAa,EAAE,CAAC;YACzD,IAAI,CAAC;gBACD,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;gBACtD,MAAM,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,KAAK,WAAW,CAAC,CAAC;gBACvE,OAAO,CAAC,GAAG,CAAC,wCAAwC,EAAE,GAAG,CAAC,MAAM,EAAE,aAAa,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;gBAC9F,OAAO,CAAC,GAAG,CAAC,6CAA6C,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;oBAC7E,QAAQ,EAAE,CAAC,CAAC,QAAQ;oBACpB,OAAO,EAAE,CAAC,CAAC,UAAU;oBACrB,UAAU,EAAE,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,UAAU;oBAC3C,MAAM,EAAE,OAAO,CAAC,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;oBACnE,SAAS,EAAE,CAAC,CAAC,MAAM,IAAI,OAAO,CAAC,CAAC,MAAM,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;iBACnG,CAAC,CAAC,CAAC,CAAC;YACT,CAAC;YAAC,MAAM,CAAC,CAAA,CAAC;YAEV,MAAM,SAAS,GAAG,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;YAEpD,IAAI,SAAS,EAAE,CAAC;gBACZ,sEAAsE;gBACtE,IAAK,IAAY,CAAC,wBAAwB,EAAE,CAAC;oBACzC,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC;wBAChD,MAAM,GAAG,GAAI,GAAW,CAAC,cAAc,IAAI,CAAE,GAAW,CAAC,MAAM,IAAK,GAAW,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;wBACvG,IAAI,GAAG,KAAM,IAAY,CAAC,wBAAwB,EAAE,CAAC;4BACjD,IAAI,CAAC,kBAAkB,GAAI,GAAW,CAAC,QAAQ,CAAC;4BAChD,MAAM;wBACV,CAAC;oBACL,CAAC;oBACA,IAAY,CAAC,wBAAwB,GAAG,IAAI,CAAC;gBAClD,CAAC;gBACD,IAAI,CAAC,kBAAkB,GAAI,SAAS,CAAC,MAAc,CAAC,aAAa,EAAE,UAAU,IAAI,EAAE,CAAC;gBACpF,IAAI,CAAC,kBAAkB,GAAG,IAAI,CAAC,kBAAkB,IAAI,SAAS,CAAC,QAAQ,CAAC;gBACxE,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC;gBAC7C,6CAA6C;gBAC7C,IAAI,CAAC,mBAAmB,GAAG,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;gBAC/D,IAAI,CAAC;oBACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;wBACtE,QAAQ,EAAE,CAAC,CAAC,QAAQ;wBACpB,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;wBAC7D,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC;qBAC5E,CAAC,CAAC,CAAC;oBACJ,OAAO,CAAC,GAAG,CAAC,mCAAmC,EAAE,IAAI,CAAC,mBAAmB,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;oBACvF,OAAO,CAAC,GAAG,CAAC,kCAAkC,EAAE,IAAI,CAAC,kBAAkB,CAAC,CAAC;gBAC7E,CAAC;gBAAC,MAAM,CAAC,CAAA,CAAC;YACd,CAAC;iBAAM,CAAC;gBACJ,mEAAmE;gBACnE,IAAI,CAAC,mBAAmB,GAAG,CAAC,IAAI,CAAC,mBAAmB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;YACnE,CAAC;QACL,CAAC,CAAC,CAAC;IACX,CAAC;IAED,kBAAkB;QACd,IAAI,CAAC,aAAa,CAAC,SAAS,CAAC,KAAK,EAAE,MAAc,EAAE,EAAE;YAClD,IAAI,MAAM,KAAK,YAAY,EAAE,CAAC;gBAC1B,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;gBAC5B,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;oBAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,UAAU,EAAE,qBAAmB,CAAC,UAAU;oBAC1C,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACxB,CAAC,CAAC;YACP,CAAC;QACL,CAAC,CAAC,CAAC;IACP,CAAC;IAED,mBAAmB,CAAC,KAAa;QAC7B,IAAI,CAAC,kBAAkB,GAAG,KAAK,CAAC;QAChC,IAAI,CAAC,IAAI,CAAC,aAAa;YAAE,OAAO;QAChC,uFAAuF;QACvF,MAAM,aAAa,GAAG,IAAI,CAAC,aAAa,CAAC,aAAa,EAAE,CAAC;QACzD,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,CAAC,aAAa,CAAC,CAAC;QAC9C,IAAI,GAAG,EAAE,CAAC;YACN,IAAI,CAAC,kBAAkB,GAAI,GAAG,CAAC,MAAc,CAAC,aAAa,EAAE,UAAU,IAAI,EAAE,CAAC;YAC9E,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QAC3C,CAAC;QACD,IAAI,CAAC;YACD,MAAM,GAAG,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAQ,CAAC;YACrD,OAAO,CAAC,GAAG,CAAC,mCAAmC,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,UAAU,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC,GAAG,CAAC,cAAc,IAAI,CAAC,GAAG,CAAC,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC;QAC3N,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,gCAAgC;QAChC,IAAI,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,KAAK,CAAQ,CAAC;YAC1D,MAAM,QAAQ,GAAG,QAAQ,IAAI,CAAC,QAAQ,CAAC,cAAc,IAAI,CAAC,QAAQ,CAAC,MAAM,IAAI,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC;YAC9G,IAAI,QAAQ,EAAE,CAAC;gBACX,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;gBAC1C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,EAAE,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;gBACzD,MAAM,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,EAAE,EAAE,GAAG,CAAC,QAAQ,EAAE,CAAC,CAAC;gBACpD,OAAO,CAAC,GAAG,CAAC,wCAAwC,EAAE,QAAQ,CAAC,CAAC;YACpE,CAAC;QACL,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;IACd,CAAC;IAED,KAAK,CAAC,mBAAmB;QACrB,IAAI,IAAI,CAAC,eAAe,IAAI,IAAI,CAAC,aAAa,EAAE,CAAC;YAC7C,IAAI,CAAC;gBACD,MAAM,IAAI,CAAC,aAAa,CAAC,KAAK,CAAC;oBAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,UAAU,EAAE,qBAAmB,CAAC,UAAU;oBAC1C,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;iBACxB,CAAC,CAAC;YACP,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACb,OAAO,CAAC,KAAK,CAAC,yCAAyC,EAAE,KAAK,CAAC,CAAC;YACpE,CAAC;QACL,CAAC;IACL,CAAC;IAED,aAAa,CAAC,aAA4B;QACtC,IAAI,aAAa,GAA6B,IAAI,CAAC;QACnD,IAAI,SAAS,GAA6B,IAAI,CAAC;QAC/C,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;QAC5C,IAAI,CAAC,iBAAiB,GAAG,IAAI,GAAG,EAAE,CAAC;QACnC,KAAK,MAAM,SAAS,IAAI,MAAM,EAAE,CAAC;YAC7B,KAAK,MAAM,QAAQ,IAAI,SAAuB,EAAE,CAAC;gBAC7C,2DAA2D;gBAC3D,MAAM,IAAI,GAAQ,QAAe,CAAC;gBAClC,MAAM,WAAW,GAAG,OAAO,IAAI,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;gBACtF,MAAM,cAAc,GAAG,IAAI,CAAC,MAAM,IAAI,OAAO,IAAI,CAAC,MAAM,CAAC,cAAc,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,CAAC;gBACtH,MAAM,QAAQ,GAAG,WAAW,IAAI,cAAc,CAAC;gBAC/C,IAAI,IAAI,CAAC,UAAU,KAAK,WAAW,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;oBAClD,4CAA4C;oBAC5C,MAAM,MAAM,GAAQ;wBAChB,GAAG,IAAI;wBACP,UAAU,EAAE,CAAC,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU;wBAC9F,cAAc,EAAE,QAAQ;wBACxB,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE;qBAC5B,CAAC;oBACF,aAAa,GAAG,MAA2B,CAAC;oBAC5C,IAAI,CAAC,iBAAiB,CAAC,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,MAA2B,CAAC,CAAC;oBACvE,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,kBAAkB,EAAE,CAAC;wBAC3C,SAAS,GAAG,aAAa,CAAC;oBAC9B,CAAC;gBACL,CAAC;qBAAM,IAAI,IAAI,CAAC,UAAU,KAAK,WAAW,EAAE,CAAC;oBACzC,OAAO,CAAC,GAAG,CAAC,oDAAoD,EAAE;wBAC9D,QAAQ,EAAE,IAAI,CAAC,QAAQ;wBACvB,OAAO,EAAE,IAAI,CAAC,UAAU;wBACxB,UAAU,EAAE,IAAI,CAAC,MAAM,IAAI,IAAI,CAAC,MAAM,CAAC,UAAU;wBACjD,MAAM,EAAE,WAAW;wBACnB,SAAS,EAAE,cAAc;qBAC5B,CAAC,CAAC;gBACP,CAAC;YACL,CAAC;QACL,CAAC;QACD,IAAI,CAAC,SAAS,EAAE,CAAC;YACb,SAAS,GAAG,aAAa,CAAC;QAC9B,CAAC;QACD,IAAI,CAAC,kBAAkB,GAAG,SAAS,EAAE,QAAQ,IAAI,IAAI,CAAC;QACtD,IAAI,CAAC;YACD,MAAM,IAAI,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;gBACtE,QAAQ,EAAE,CAAC,CAAC,QAAQ;gBACpB,UAAU,EAAE,CAAC,CAAC,UAAU,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,UAAU,CAAC;gBAC7D,cAAc,EAAE,CAAC,CAAC,cAAc,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,cAAc,CAAC;aAC5E,CAAC,CAAC,CAAC;YACJ,OAAO,CAAC,GAAG,CAAC,iCAAiC,EAAE,IAAI,CAAC,CAAC;QACzD,CAAC;QAAC,MAAM,CAAC,CAAA,CAAC;QACV,OAAO,SAAS,CAAC;IACrB,CAAC;;AA1SO;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,IAAI,EAAC,CAAC;2DACd;AAG/B;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,MAAM,EAAC,CAAC;gEACG;AAnC3B,mBAAmB;IAD/B,QAAQ;GACI,mBAAmB,CA2U/B","sourcesContent":["import { IoElement, Register, ioNavigator, MenuOption, Storage as $, ioMarkdown, ReactiveProperty, IoElementProps, ThemeSingleton } from 'io-gui';\nimport { tabNavigate } from './TabNavigate.js';\nimport { tabSelect } from './TabSelect.js';\nimport { tabInspect } from './TabInspect.js';\nimport { tabMagnet } from './TabMagnet.js';\nimport { tabAdjust } from './TabAdjust.js';\nimport { SimulatorState } from './SimulatorState.js';\nimport type { Magnet } from './types/Magnet';\n\nThemeSingleton.themeID = 'dark';\n\nfunction generateClientId() {\n return 'controller-' + Math.random().toString(36).substr(2, 9);\n}\nfunction generateClientName() {\n const now = new Date();\n const timestamp = now.toISOString().replace(/[:.]/g, '-').slice(0, 19);\n return `Controller-${timestamp}`;\n}\n\ntype PresenceState = {\n [key: string]: Presence[];\n};\n\ntype Presence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n startTime: number;\n}\n\ntype SimulatorPresence = {\n clientId: string;\n clientName: string;\n clientType: string;\n presence_ref: string;\n shared: SimulatorState;\n startTime: number;\n}\n\ntype SimulatorTakeoverPayload = {\n newSimulatorId: string;\n newSimulatorName: string;\n startTime: number;\n}\n\nexport type ViewMetadata = {\n canWrite: boolean;\n category: string;\n component: string;\n defaultValue: any;\n description: string;\n displayName: string;\n name: string;\n path: string;\n type: 'bool' | 'float' | 'string';\n unityType: string;\n min?: number,\n max?: number,\n step?: number,\n}\n\n@Register\nexport class SpacetimeController extends IoElement {\n static get Style() {\n return /* css */`\n :host {\n display: flex;\n flex-direction: column;\n height: 100%;\n width: 100%;\n }\n :host > io-navigator {\n flex: 1 1 auto;\n overflow: hidden;\n }\n `;\n }\n\n static supabaseUrl = 'https://gwodhwyvuftyrvbymmvc.supabase.co';\n static supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Imd3b2Rod3l2dWZ0eXJ2YnltbXZjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NDIzNDkyMDMsImV4cCI6MjA1NzkyNTIwM30.APVpyOupY84gQ7c0vBZkY-GqoJRPhb4oD4Lcj9CEzlc';\n static clientChannelName = 'spacecraft';\n static clientType = 'controller';\n\n declare clientId: string;\n declare clientName: string;\n declare supabaseClient: any;\n declare clientChannel: any;\n declare clientConnected: boolean;\n declare currentSimulatorId: string | null;\n declare currentSimulators: Map;\n\n declare magnetViewMetadata: Array;\n\n @ReactiveProperty({type: SimulatorState, init: null})\n declare simulatorState: SimulatorState;\n\n @ReactiveProperty({type: Number})\n declare simulatorRosterTick: number;\n\n constructor(props: IoElementProps) {\n super(props);\n // Client Identity\n this.clientId = generateClientId();\n this.clientName = generateClientName();\n\n // Connection State\n this.supabaseClient = null;\n this.clientChannel = null;\n this.clientConnected = false;\n this.currentSimulatorId = null;\n this.currentSimulators = new Map();\n this.magnetViewMetadata = [];\n this.simulatorRosterTick = 0;\n this.connect();\n }\n\n connect() {\n if (typeof supabase === 'undefined') {\n console.error('Supabase library missing!');\n return;\n }\n try {\n const params = new URLSearchParams(window.location.search);\n const channelName = params.get('channel') || SpacetimeController.clientChannelName;\n this.supabaseClient = supabase.createClient(SpacetimeController.supabaseUrl, SpacetimeController.supabaseAnonKey);\n this.clientChannel = this.supabaseClient.channel(channelName, {\n config: { presence: { key: this.clientId } }\n });\n this.setupPresenceHandlers();\n this.subscribeToChannel();\n // Only honor simulatorIndex param (no legacy)\n const simIndexFromUrl = params.get('simulatorIndex');\n if (simIndexFromUrl) {\n (this as any)._preselectSimulatorIndex = parseInt(simIndexFromUrl, 10);\n }\n } catch (error) {\n console.error('Controller connection failed:', error);\n console.error('[Controller] Connection failed:', error);\n }\n }\n\n \n ready() {\n this.render([\n ioNavigator({\n menu: 'top',\n caching: 'proactive',\n option: new MenuOption({\n id: 'root',\n options: [\n {id: 'About', icon: '📖'},\n {id: 'Navigate', icon: '🧭'},\n {id: 'Select', icon: '👆'},\n {id: 'Inspect', icon: '🔍'},\n {id: 'Magnet', icon: '🧲'},\n {id: 'Adjust', icon: '⚙️'},\n ],\n selectedID: $({key: 'page', storage: 'hash', value: 'About'})\n }),\n elements: [\n ioMarkdown({id: 'About', src: './docs/About.md'}),\n tabNavigate({id: 'Navigate', controller: this, simulatorState: this.simulatorState}),\n tabSelect({id: 'Select', controller: this, simulatorState: this.simulatorState}),\n tabInspect({id: 'Inspect', controller: this, simulatorState: this.simulatorState}),\n tabMagnet({id: 'Magnet', controller: this, simulatorState: this.simulatorState}),\n tabAdjust({id: 'Adjust', controller: this, simulatorState: this.simulatorState}),\n ]\n })\n ]);\n }\n\n // === UNITY COMMUNICATION ===\n\n sendPanEvent(deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pan', { panXDelta: deltaX, panYDelta: deltaY });\n }\n\n sendZoomEvent(zoomDelta: number) {\n this.sendEventToSimulator('zoom', { zoomDelta });\n }\n\n sendSelectEvent(action: string) {\n this.sendEventToSimulator('select', { action });\n }\n\n sendCreateMagnetEvent(magnetData: Magnet) {\n try {\n console.log('[Controller] sendCreateMagnetEvent magnetData:', JSON.parse(JSON.stringify(magnetData)));\n } catch (e) {\n console.log('[Controller] sendCreateMagnetEvent magnetData (raw):', magnetData);\n }\n this.sendEventToSimulator('createMagnet', { magnetData });\n }\n\n sendUpdateMagnetEvent(magnetData: Magnet) {\n this.sendEventToSimulator('updateMagnet', { magnetData });\n }\n\n sendDeleteMagnetEvent(magnetId: string) {\n this.sendEventToSimulator('deleteMagnet', { magnetId });\n }\n\n sendPushMagnetEvent(magnetId: string, deltaX: number, deltaY: number) {\n this.sendEventToSimulator('pushMagnet', { magnetId, deltaX, deltaY });\n }\n\n sendEventToSimulator(eventType: string, data: any) {\n if (!this.clientChannel) {\n console.error('[Controller] Cannot send event - no client channel');\n return;\n }\n\n if (!this.currentSimulatorId) {\n console.error('[Controller] Cannot send event - no current simulator ID');\n return;\n }\n\n const payload = {\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n screenId: 'main',\n targetSimulatorId: this.currentSimulatorId,\n ...data\n };\n\n this.clientChannel.send({\n type: 'broadcast',\n event: eventType,\n payload: payload\n }).catch((err: any) => {\n console.error(`[Controller] Send '${eventType}' failed:`, err);\n });\n }\n\n setupPresenceHandlers() {\n this.clientChannel\n .on('presence', { event: 'sync' }, () => {\n const presenceState = this.clientChannel.presenceState();\n try {\n const raw = Object.values(presenceState || {}).flat();\n const sims = raw.filter((p: any) => p && p.clientType === 'simulator');\n console.log('[Controller][presence:sync] presences:', raw.length, 'simulators:', sims.length);\n console.log('[Controller][presence:sync] simulators raw:', sims.map((p: any) => ({\n clientId: p.clientId,\n nameTop: p.clientName,\n nameShared: p.shared && p.shared.clientName,\n idxTop: typeof p.simulatorIndex === 'number' ? p.simulatorIndex : 0,\n idxShared: p.shared && typeof p.shared.simulatorIndex === 'number' ? p.shared.simulatorIndex : 0\n })));\n } catch {}\n\n const simulator = this.findSimulator(presenceState);\n \n if (simulator) {\n // If preselect by index requested, override with match when available\n if ((this as any)._preselectSimulatorIndex) {\n for (const sim of this.currentSimulators.values()) {\n const idx = (sim as any).simulatorIndex || ((sim as any).shared && (sim as any).shared.simulatorIndex);\n if (idx === (this as any)._preselectSimulatorIndex) {\n this.currentSimulatorId = (sim as any).clientId;\n break;\n }\n }\n (this as any)._preselectSimulatorIndex = null;\n }\n this.magnetViewMetadata = (simulator.shared as any).unityMetaData?.MagnetView || [];\n this.currentSimulatorId = this.currentSimulatorId || simulator.clientId;\n this.simulatorState.update(simulator.shared);\n // bump tick so UI re-renders simulator menus\n this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1;\n try {\n const list = Array.from(this.currentSimulators.values()).map((s: any) => ({\n clientId: s.clientId,\n clientName: s.clientName || (s.shared && s.shared.clientName),\n simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex)\n }));\n console.log('[Controller] roster updated (tick', this.simulatorRosterTick, '):', list);\n console.log('[Controller] currentSimulatorId:', this.currentSimulatorId);\n } catch {}\n } else {\n // No simulator selected yet; still bump tick to refresh menu state\n this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1;\n }\n });\n }\n\n subscribeToChannel() {\n this.clientChannel.subscribe(async (status: string) => {\n if (status === 'SUBSCRIBED') {\n this.clientConnected = true;\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n }\n });\n }\n\n setCurrentSimulator(simId: string) {\n this.currentSimulatorId = simId;\n if (!this.clientChannel) return;\n // Pull fresh presence state from Supabase and switch to the selected simulator's state\n const presenceState = this.clientChannel.presenceState();\n const sim = this.findSimulator(presenceState);\n if (sim) {\n this.magnetViewMetadata = (sim.shared as any).unityMetaData?.MagnetView || [];\n this.simulatorState.update(sim.shared);\n }\n try {\n const sel = this.currentSimulators.get(simId) as any;\n console.log('[Controller] setCurrentSimulator:', simId, 'name=', sel && (sel.clientName || (sel.shared && sel.shared.clientName)), 'index=', sel && (sel.simulatorIndex || (sel.shared && sel.shared.simulatorIndex)));\n } catch {}\n // Persist simulatorIndex in URL\n try {\n const selected = this.currentSimulators.get(simId) as any;\n const simIndex = selected && (selected.simulatorIndex || (selected.shared && selected.shared.simulatorIndex));\n if (simIndex) {\n const url = new URL(window.location.href);\n url.searchParams.set('simulatorIndex', String(simIndex));\n window.history.replaceState({}, '', url.toString());\n console.log('[Controller] URL simulatorIndex set to', simIndex);\n }\n } catch {}\n }\n\n async updatePresenceState() {\n if (this.clientConnected && this.clientChannel) {\n try {\n await this.clientChannel.track({\n clientId: this.clientId,\n clientType: SpacetimeController.clientType,\n clientName: this.clientName,\n startTime: Date.now()\n });\n } catch (error) {\n console.error('[Connection] Failed to update presence:', error);\n }\n }\n }\n\n findSimulator(presenceState: PresenceState): SimulatorPresence | null {\n let lastSimulator: SimulatorPresence | null = null;\n let simulator: SimulatorPresence | null = null;\n const values = Object.values(presenceState);\n this.currentSimulators = new Map();\n for (const presences of values) {\n for (const presence of presences as Presence[]) {\n // Only count fully-initialized simulators (index assigned)\n const meta: any = presence as any;\n const simIndexTop = typeof meta.simulatorIndex === 'number' ? meta.simulatorIndex : 0;\n const simIndexShared = meta.shared && typeof meta.shared.simulatorIndex === 'number' ? meta.shared.simulatorIndex : 0;\n const simIndex = simIndexTop || simIndexShared;\n if (meta.clientType === 'simulator' && simIndex > 0) {\n // Prefer shared view of fields if available\n const merged: any = {\n ...meta,\n clientName: (meta.shared && meta.shared.clientName) ? meta.shared.clientName : meta.clientName,\n simulatorIndex: simIndex,\n shared: meta.shared || {}\n };\n lastSimulator = merged as SimulatorPresence;\n this.currentSimulators.set(meta.clientId, merged as SimulatorPresence);\n if (meta.clientId == this.currentSimulatorId) {\n simulator = lastSimulator;\n }\n } else if (meta.clientType === 'simulator') {\n console.log('[Controller] ignoring simulator without index yet:', {\n clientId: meta.clientId,\n nameTop: meta.clientName,\n nameShared: meta.shared && meta.shared.clientName,\n idxTop: simIndexTop,\n idxShared: simIndexShared\n });\n }\n }\n }\n if (!simulator) {\n simulator = lastSimulator;\n }\n this.currentSimulatorId = simulator?.clientId || null;\n try {\n const list = Array.from(this.currentSimulators.values()).map((s: any) => ({\n clientId: s.clientId,\n clientName: s.clientName || (s.shared && s.shared.clientName),\n simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex)\n }));\n console.log('[Controller] currentSimulators:', list);\n } catch {}\n return simulator;\n }\n}\n"]} \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.d.ts b/WebSites/controller/build/TabNavigate.d.ts index e18bcac9..a0577834 100644 --- a/WebSites/controller/build/TabNavigate.d.ts +++ b/WebSites/controller/build/TabNavigate.d.ts @@ -3,6 +3,7 @@ export declare class TabNavigate extends TabBase { static get Style(): string; onPointermove(event: PointerEvent): void; changed(): void; + onSimulatorChange(event: CustomEvent): void; } export declare const tabNavigate: (arg0: TabBaseProps) => import("io-gui").VDOMElement; //# sourceMappingURL=TabNavigate.d.ts.map \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.d.ts.map b/WebSites/controller/build/TabNavigate.d.ts.map index 61909f88..49df209a 100644 --- a/WebSites/controller/build/TabNavigate.d.ts.map +++ b/WebSites/controller/build/TabNavigate.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"TabNavigate.d.ts","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,qBACa,WAAY,SAAQ,OAAO;IACpC,MAAM,KAAK,KAAK,WAaf;IAED,aAAa,CAAC,KAAK,EAAE,YAAY;IAUjC,OAAO;CAKV;AAED,eAAO,MAAM,WAAW,GAAY,MAAM,YAAY,iCAErD,CAAC"} \ No newline at end of file +{"version":3,"file":"TabNavigate.d.ts","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,qBACa,WAAY,SAAQ,OAAO;IACpC,MAAM,KAAK,KAAK,WAaf;IAED,aAAa,CAAC,KAAK,EAAE,YAAY;IAUjC,OAAO;IAwCP,iBAAiB,CAAC,KAAK,EAAE,WAAW;CAMvC;AAED,eAAO,MAAM,WAAW,GAAY,MAAM,YAAY,iCAErD,CAAC"} \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.js b/WebSites/controller/build/TabNavigate.js index 04b5709e..7aaefea0 100644 --- a/WebSites/controller/build/TabNavigate.js +++ b/WebSites/controller/build/TabNavigate.js @@ -4,7 +4,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key, else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; -import { h2, Register } from 'io-gui'; +import { h2, Register, IoOptionSelect, MenuOption, br } from 'io-gui'; import { TabBase } from './TabBase.js'; let TabNavigate = class TabNavigate extends TabBase { static get Style() { @@ -28,10 +28,48 @@ let TabNavigate = class TabNavigate extends TabBase { } } changed() { + // Force rerender when simulator list changes by reading controller.simulatorRosterTick + void this.controller.simulatorRosterTick; + const simulators = Array.from(this.controller.currentSimulators?.values() || []); + const hasSimulators = simulators.length > 0; + const headerRow = hasSimulators + ? [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: this.controller.currentSimulatorId || '', + option: new MenuOption({ + options: simulators + .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId })) + .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' })) + }), + '@value-input': (e) => this.onSimulatorChange(e) + }) + ] + : [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: '(none)', + option: new MenuOption({ + options: [{ id: '(none)', value: '(none)' }], + disabled: true, + }), + // no handler; locked selector + 'disabled': true + }) + ]; this.render([ - h2('DRAG to pan • SCROLL to zoom'), + ...headerRow, + h2('DRAG to pan • PINCH to zoom'), ]); } + onSimulatorChange(event) { + const newId = event.detail?.value; + if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') { + this.controller.setCurrentSimulator?.(newId); + } + } }; TabNavigate = __decorate([ Register diff --git a/WebSites/controller/build/TabNavigate.js.map b/WebSites/controller/build/TabNavigate.js.map index ea357805..df0d5887 100644 --- a/WebSites/controller/build/TabNavigate.js.map +++ b/WebSites/controller/build/TabNavigate.js.map @@ -1 +1 @@ -{"version":3,"file":"TabNavigate.js","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,EAAE,EAAE,QAAQ,EAAE,MAAM,QAAQ,CAAC;AACtC,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAG9C,IAAM,WAAW,GAAjB,MAAM,WAAY,SAAQ,OAAO;IACpC,MAAM,KAAK,KAAK;QACZ,OAAO,SAAS,CAAA;;;;;;;;;;;SAWf,CAAC;IACN,CAAC;IAED,aAAa,CAAC,KAAmB;QAC7B,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,CAAC,YAAY,CACxB,KAAK,CAAC,SAAS,GAAG,IAAI,EACtB,KAAK,CAAC,SAAS,GAAG,IAAI,CACzB,CAAC;QACN,CAAC;IACL,CAAC;IAED,OAAO;QACH,IAAI,CAAC,MAAM,CAAC;YACR,EAAE,CAAC,8BAA8B,CAAC;SACrC,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AA/BY,WAAW;IADvB,QAAQ;GACI,WAAW,CA+BvB;;AAED,MAAM,CAAC,MAAM,WAAW,GAAG,UAAS,IAAkB;IAClD,OAAO,WAAW,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AAC1C,CAAC,CAAC","sourcesContent":["import { h2, Register } from 'io-gui';\nimport { TabBase, TabBaseProps } from './TabBase.js';\n\n@Register\nexport class TabNavigate extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n justify-content: center;\n }\n :host > h2 {\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n }\n `;\n }\n\n onPointermove(event: PointerEvent) {\n super.onPointermove(event);\n if (event.movementX || event.movementY) {\n this.controller.sendPanEvent(\n event.movementX * 0.03,\n event.movementY * 0.03\n );\n }\n }\n\n changed() {\n this.render([\n h2('DRAG to pan • SCROLL to zoom'),\n ]);\n }\n}\n\nexport const tabNavigate = function(arg0: TabBaseProps) {\n return TabNavigate.vConstructor(arg0);\n};"]} \ No newline at end of file +{"version":3,"file":"TabNavigate.js","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"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","sourcesContent":["import { h2, Register, IoOptionSelect, MenuOption, IoOptionSelectProps, br } from 'io-gui';\nimport { TabBase, TabBaseProps } from './TabBase.js';\n\n@Register\nexport class TabNavigate extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n justify-content: center;\n }\n :host > h2 {\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n }\n `;\n }\n\n onPointermove(event: PointerEvent) {\n super.onPointermove(event);\n if (event.movementX || event.movementY) {\n this.controller.sendPanEvent(\n event.movementX * 0.03,\n event.movementY * 0.03\n );\n }\n }\n\n changed() {\n // Force rerender when simulator list changes by reading controller.simulatorRosterTick\n void (this.controller as any).simulatorRosterTick;\n const simulators = Array.from(this.controller.currentSimulators?.values() || []);\n const hasSimulators = simulators.length > 0;\n\n const headerRow = hasSimulators\n ? [\n h2('Simulator:'),\n br(),\n IoOptionSelect.vConstructor({\n value: this.controller.currentSimulatorId || '',\n option: new MenuOption({\n options: simulators\n .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId }))\n .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' }))\n }),\n '@value-input': (e: CustomEvent) => this.onSimulatorChange(e)\n } as IoOptionSelectProps)\n ]\n : [\n h2('Simulator:'),\n br(),\n IoOptionSelect.vConstructor({\n value: '(none)',\n option: new MenuOption({\n options: [{ id: '(none)', value: '(none)' }],\n disabled: true,\n }),\n // no handler; locked selector\n 'disabled': true\n } as IoOptionSelectProps)\n ];\n\n this.render([\n ...headerRow,\n h2('DRAG to pan • PINCH to zoom'),\n ]);\n }\n\n onSimulatorChange(event: CustomEvent) {\n const newId = event.detail?.value;\n if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') {\n (this.controller as any).setCurrentSimulator?.(newId);\n }\n }\n}\n\nexport const tabNavigate = function(arg0: TabBaseProps) {\n return TabNavigate.vConstructor(arg0);\n};"]} \ No newline at end of file diff --git a/WebSites/controller/src/SimulatorState.ts b/WebSites/controller/src/SimulatorState.ts index a60748d3..493079fc 100644 --- a/WebSites/controller/src/SimulatorState.ts +++ b/WebSites/controller/src/SimulatorState.ts @@ -35,12 +35,6 @@ export class SimulatorState extends Node { @ReactiveProperty({type: String}) declare currentScreenId: string; - @ReactiveProperty({type: Number}) - declare currentSearchGravity: number; - - @ReactiveProperty({type: String}) - declare currentSearchString: string; - @ReactiveProperty({type: Object}) declare highlightedItem: any; // TODO: define type? // {collection: Array(2), coverHeight: 283, coverImage: 'https://archive.org/services/img/5thwave0000yanc', coverWidth: 180, creator: 'Yancey, Richard', …} @@ -84,8 +78,6 @@ export class SimulatorState extends Node { currentCollectionId: state.currentCollectionId || '', currentCollectionItems: state.currentCollectionItems || [], currentScreenId: state.currentScreenId || '', - currentSearchGravity: state.currentSearchGravity || 0, - currentSearchString: state.currentSearchString || '', highlightedItem: state.highlightedItem || null, highlightedItemId: state.highlightedItemId || '', highlightedItemIds: state.highlightedItemIds || [], diff --git a/WebSites/controller/src/SpacetimeController.ts b/WebSites/controller/src/SpacetimeController.ts index 64dd2b33..86641c50 100644 --- a/WebSites/controller/src/SpacetimeController.ts +++ b/WebSites/controller/src/SpacetimeController.ts @@ -89,12 +89,16 @@ export class SpacetimeController extends IoElement { declare clientChannel: any; declare clientConnected: boolean; declare currentSimulatorId: string | null; + declare currentSimulators: Map; declare magnetViewMetadata: Array; @ReactiveProperty({type: SimulatorState, init: null}) declare simulatorState: SimulatorState; + @ReactiveProperty({type: Number}) + declare simulatorRosterTick: number; + constructor(props: IoElementProps) { super(props); // Client Identity @@ -106,7 +110,9 @@ export class SpacetimeController extends IoElement { this.clientChannel = null; this.clientConnected = false; this.currentSimulatorId = null; + this.currentSimulators = new Map(); this.magnetViewMetadata = []; + this.simulatorRosterTick = 0; this.connect(); } @@ -116,19 +122,26 @@ export class SpacetimeController extends IoElement { return; } try { - const channelName = new URLSearchParams(window.location.search).get('channel') || SpacetimeController.clientChannelName; + const params = new URLSearchParams(window.location.search); + const channelName = params.get('channel') || SpacetimeController.clientChannelName; this.supabaseClient = supabase.createClient(SpacetimeController.supabaseUrl, SpacetimeController.supabaseAnonKey); this.clientChannel = this.supabaseClient.channel(channelName, { config: { presence: { key: this.clientId } } }); this.setupPresenceHandlers(); this.subscribeToChannel(); + // Only honor simulatorIndex param (no legacy) + const simIndexFromUrl = params.get('simulatorIndex'); + if (simIndexFromUrl) { + (this as any)._preselectSimulatorIndex = parseInt(simIndexFromUrl, 10); + } } catch (error) { console.error('Controller connection failed:', error); console.error('[Controller] Connection failed:', error); } } + ready() { this.render([ ioNavigator({ @@ -226,16 +239,51 @@ export class SpacetimeController extends IoElement { this.clientChannel .on('presence', { event: 'sync' }, () => { const presenceState = this.clientChannel.presenceState(); - const simulator = this.findLatestSimulator(presenceState); + try { + const raw = Object.values(presenceState || {}).flat(); + const sims = raw.filter((p: any) => p && p.clientType === 'simulator'); + console.log('[Controller][presence:sync] presences:', raw.length, 'simulators:', sims.length); + console.log('[Controller][presence:sync] simulators raw:', sims.map((p: any) => ({ + clientId: p.clientId, + nameTop: p.clientName, + nameShared: p.shared && p.shared.clientName, + idxTop: typeof p.simulatorIndex === 'number' ? p.simulatorIndex : 0, + idxShared: p.shared && typeof p.shared.simulatorIndex === 'number' ? p.shared.simulatorIndex : 0 + }))); + } catch {} + + const simulator = this.findSimulator(presenceState); if (simulator) { + // If preselect by index requested, override with match when available + if ((this as any)._preselectSimulatorIndex) { + for (const sim of this.currentSimulators.values()) { + const idx = (sim as any).simulatorIndex || ((sim as any).shared && (sim as any).shared.simulatorIndex); + if (idx === (this as any)._preselectSimulatorIndex) { + this.currentSimulatorId = (sim as any).clientId; + break; + } + } + (this as any)._preselectSimulatorIndex = null; + } this.magnetViewMetadata = (simulator.shared as any).unityMetaData?.MagnetView || []; - this.currentSimulatorId = simulator.clientId; + this.currentSimulatorId = this.currentSimulatorId || simulator.clientId; this.simulatorState.update(simulator.shared); + // bump tick so UI re-renders simulator menus + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; + try { + const list = Array.from(this.currentSimulators.values()).map((s: any) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] roster updated (tick', this.simulatorRosterTick, '):', list); + console.log('[Controller] currentSimulatorId:', this.currentSimulatorId); + } catch {} + } else { + // No simulator selected yet; still bump tick to refresh menu state + this.simulatorRosterTick = (this.simulatorRosterTick || 0) + 1; } - }) - .on('broadcast', { event: 'simulatorTakeover' }, (payload: SimulatorTakeoverPayload) => { - this.currentSimulatorId = payload.newSimulatorId; }); } @@ -253,6 +301,33 @@ export class SpacetimeController extends IoElement { }); } + setCurrentSimulator(simId: string) { + this.currentSimulatorId = simId; + if (!this.clientChannel) return; + // Pull fresh presence state from Supabase and switch to the selected simulator's state + const presenceState = this.clientChannel.presenceState(); + const sim = this.findSimulator(presenceState); + if (sim) { + this.magnetViewMetadata = (sim.shared as any).unityMetaData?.MagnetView || []; + this.simulatorState.update(sim.shared); + } + try { + const sel = this.currentSimulators.get(simId) as any; + console.log('[Controller] setCurrentSimulator:', simId, 'name=', sel && (sel.clientName || (sel.shared && sel.shared.clientName)), 'index=', sel && (sel.simulatorIndex || (sel.shared && sel.shared.simulatorIndex))); + } catch {} + // Persist simulatorIndex in URL + try { + const selected = this.currentSimulators.get(simId) as any; + const simIndex = selected && (selected.simulatorIndex || (selected.shared && selected.shared.simulatorIndex)); + if (simIndex) { + const url = new URL(window.location.href); + url.searchParams.set('simulatorIndex', String(simIndex)); + window.history.replaceState({}, '', url.toString()); + console.log('[Controller] URL simulatorIndex set to', simIndex); + } + } catch {} + } + async updatePresenceState() { if (this.clientConnected && this.clientChannel) { try { @@ -268,17 +343,54 @@ export class SpacetimeController extends IoElement { } } - findLatestSimulator(presenceState: PresenceState): SimulatorPresence | null { - let latestSimulator = null; - let latestStartTime = 0; - Object.values(presenceState).forEach(presences => { - presences.forEach((presence: Presence) => { - if (presence.clientType === 'simulator' && presence.startTime > latestStartTime) { - latestSimulator = presence; - latestStartTime = presence.startTime; + findSimulator(presenceState: PresenceState): SimulatorPresence | null { + let lastSimulator: SimulatorPresence | null = null; + let simulator: SimulatorPresence | null = null; + const values = Object.values(presenceState); + this.currentSimulators = new Map(); + for (const presences of values) { + for (const presence of presences as Presence[]) { + // Only count fully-initialized simulators (index assigned) + const meta: any = presence as any; + const simIndexTop = typeof meta.simulatorIndex === 'number' ? meta.simulatorIndex : 0; + const simIndexShared = meta.shared && typeof meta.shared.simulatorIndex === 'number' ? meta.shared.simulatorIndex : 0; + const simIndex = simIndexTop || simIndexShared; + if (meta.clientType === 'simulator' && simIndex > 0) { + // Prefer shared view of fields if available + const merged: any = { + ...meta, + clientName: (meta.shared && meta.shared.clientName) ? meta.shared.clientName : meta.clientName, + simulatorIndex: simIndex, + shared: meta.shared || {} + }; + lastSimulator = merged as SimulatorPresence; + this.currentSimulators.set(meta.clientId, merged as SimulatorPresence); + if (meta.clientId == this.currentSimulatorId) { + simulator = lastSimulator; + } + } else if (meta.clientType === 'simulator') { + console.log('[Controller] ignoring simulator without index yet:', { + clientId: meta.clientId, + nameTop: meta.clientName, + nameShared: meta.shared && meta.shared.clientName, + idxTop: simIndexTop, + idxShared: simIndexShared + }); } - }); - }); - return latestSimulator; + } + } + if (!simulator) { + simulator = lastSimulator; + } + this.currentSimulatorId = simulator?.clientId || null; + try { + const list = Array.from(this.currentSimulators.values()).map((s: any) => ({ + clientId: s.clientId, + clientName: s.clientName || (s.shared && s.shared.clientName), + simulatorIndex: s.simulatorIndex || (s.shared && s.shared.simulatorIndex) + })); + console.log('[Controller] currentSimulators:', list); + } catch {} + return simulator; } -} \ No newline at end of file +} diff --git a/WebSites/controller/src/TabNavigate.ts b/WebSites/controller/src/TabNavigate.ts index 602369d4..cebecd50 100644 --- a/WebSites/controller/src/TabNavigate.ts +++ b/WebSites/controller/src/TabNavigate.ts @@ -1,4 +1,4 @@ -import { h2, Register } from 'io-gui'; +import { h2, Register, IoOptionSelect, MenuOption, IoOptionSelectProps, br } from 'io-gui'; import { TabBase, TabBaseProps } from './TabBase.js'; @Register @@ -29,10 +29,51 @@ export class TabNavigate extends TabBase { } changed() { + // Force rerender when simulator list changes by reading controller.simulatorRosterTick + void (this.controller as any).simulatorRosterTick; + const simulators = Array.from(this.controller.currentSimulators?.values() || []); + const hasSimulators = simulators.length > 0; + + const headerRow = hasSimulators + ? [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: this.controller.currentSimulatorId || '', + option: new MenuOption({ + options: simulators + .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId })) + .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' })) + }), + '@value-input': (e: CustomEvent) => this.onSimulatorChange(e) + } as IoOptionSelectProps) + ] + : [ + h2('Simulator:'), + br(), + IoOptionSelect.vConstructor({ + value: '(none)', + option: new MenuOption({ + options: [{ id: '(none)', value: '(none)' }], + disabled: true, + }), + // no handler; locked selector + 'disabled': true + } as IoOptionSelectProps) + ]; + this.render([ - h2('DRAG to pan • SCROLL to zoom'), + ...headerRow, + h2('DRAG to pan • PINCH to zoom'), ]); } + + onSimulatorChange(event: CustomEvent) { + const newId = event.detail?.value; + if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') { + (this.controller as any).setCurrentSimulator?.(newId); + } + } } export const tabNavigate = function(arg0: TabBaseProps) { From 3da665e89d271b4c000a2dc754d5a57f39d373ff Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 17:39:56 +0200 Subject: [PATCH 02/29] committing and pushing to main to work around new github actions bug --- .github/workflows/build-unity-spacecraft.yml | 120 +++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 .github/workflows/build-unity-spacecraft.yml diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml new file mode 100644 index 00000000..9dcdbe85 --- /dev/null +++ b/.github/workflows/build-unity-spacecraft.yml @@ -0,0 +1,120 @@ +name: Build Unity SpaceCraft + +on: + workflow_dispatch: + inputs: + unityVersion: + description: "Unity Editor version (e.g. 2022.3.45f1). Use 'auto' to detect from ProjectVersion.txt" + required: false + default: "auto" + type: string + projectPath: + description: "Unity project path" + required: false + default: "Unity/SpaceCraft" + type: string + targetPlatform: + description: "Unity target platform" + required: false + default: "WebGL" + type: choice + options: + - WebGL + buildProfile: + description: "Build profile token (maps to a Unity build method)" + required: false + default: "WebGLProductionBuild" + type: choice + options: + - WebGLProductionBuild + - WebGLDevelopmentBuild + buildMethod: + description: "Explicit Unity build method (overrides buildProfile). Example: SpaceCraft.Editor.Builds.BuildWebGLProduction" + required: false + default: "" + type: string + +jobs: + build: + name: Build ${{ inputs.targetPlatform }} (${{ inputs.buildProfile }}) + runs-on: ubuntu-latest + + env: + PROJECT_PATH: ${{ inputs.projectPath }} + TARGET_PLATFORM: ${{ inputs.targetPlatform }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Detect Unity version from ProjectVersion.txt + id: detect + shell: bash + run: | + set -euo pipefail + if [[ "${{ inputs.unityVersion }}" != "auto" && -n "${{ inputs.unityVersion }}" ]]; then + echo "Using provided unityVersion: ${{ inputs.unityVersion }}" + echo "version=${{ inputs.unityVersion }}" >> "$GITHUB_OUTPUT" + exit 0 + fi + FILE="${{ github.workspace }}/${{ inputs.projectPath }}/ProjectSettings/ProjectVersion.txt" + if [[ ! -f "$FILE" ]]; then + echo "ProjectVersion.txt not found at: $FILE" >&2 + exit 1 + fi + VERSION=$(grep -E "^m_EditorVersion:\s*" "$FILE" | sed -E 's/^m_EditorVersion:\s*([^[:space:]]+).*/\1/') + if [[ -z "$VERSION" ]]; then + echo "Failed to parse Unity version from ProjectVersion.txt" >&2 + exit 1 + fi + echo "Detected Unity version: $VERSION" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + + - name: Compute build method + id: compute + shell: bash + run: | + set -euo pipefail + if [[ -n "${{ inputs.buildMethod }}" ]]; then + METHOD="${{ inputs.buildMethod }}" + else + case "${{ inputs.buildProfile }}" in + WebGLProductionBuild) + METHOD="SpaceCraft.Editor.Builds.BuildWebGLProduction" + ;; + WebGLDevelopmentBuild) + METHOD="SpaceCraft.Editor.Builds.BuildWebGLDevelopment" + ;; + *) + echo "Unknown buildProfile: ${{ inputs.buildProfile }}" >&2 + exit 1 + ;; + esac + fi + echo "buildMethod=$METHOD" >> "$GITHUB_OUTPUT" + + - name: Unity - Builder + uses: game-ci/unity-builder@v4 + env: + UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} + UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} + UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + with: + projectPath: ${{ inputs.projectPath }} + targetPlatform: ${{ inputs.targetPlatform }} + unityVersion: ${{ steps.detect.outputs.version }} + buildMethod: ${{ steps.compute.outputs.buildMethod }} + uses_custom_image: false + # Optional: enable cache + # cacheUnity: true + # cacheVersioning: semantic + + - name: Upload build artifacts + if: success() + uses: actions/upload-artifact@v4 + with: + name: SpaceCraft-${{ inputs.targetPlatform }}-${{ inputs.buildProfile }} + path: | + build + ${{ inputs.projectPath }}/Build + From f2156e897aea3254523b367fbd1f7181eccbab79 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 18:08:42 +0200 Subject: [PATCH 03/29] added build trigger script --- .github/workflows/build-unity-spacecraft.yml | 74 ++++++++++++++++- SvelteKit/BackSpace/package.json | 3 +- .../BackSpace/scripts/gh-trigger-build.ts | 79 +++++++++++++++++++ 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 SvelteKit/BackSpace/scripts/gh-trigger-build.ts diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 9dcdbe85..a2a59391 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -1,3 +1,63 @@ +################################################################################################### +# Build Unity SpaceCraft – GitHub Actions (Ubuntu runner) +# +# Overview +# - Builds the Unity SpaceCraft project for WebGL on GitHub-hosted Linux runners. +# - Auto-detects Unity version from ProjectVersion.txt (override via input). +# - Supports Production/Development profiles or a fully custom build method. +# - Caches the Unity Library folder for faster imports/compiles between runs. +# - Downloads real LFS asset blobs for the checked-out commit (required for Unity). +# - Uploads build artifacts for later download/deploy. +# +# Requirements (repo secrets) +# - UNITY_LICENSE: Unity license file contents (game-ci format) or ULF text. +# - UNITY_EMAIL: Unity account email (for legacy activation flows in builder). +# - UNITY_PASSWORD: Unity account password (for legacy activation flows in builder). +# +# Inputs (workflow_dispatch) +# - unityVersion: Unity Editor version (e.g., 2022.3.45f1). Use 'auto' to parse ProjectVersion.txt. +# - projectPath: Path to the Unity project. Defaults to Unity/SpaceCraft. +# - targetPlatform: Build target (WebGL). Extendable if needed. +# - buildProfile: Convenience selector mapping to build methods (Production/Development). +# - buildMethod: Explicit Unity C# method string, overrides buildProfile. Example: +# SpaceCraft.Editor.Builds.BuildWebGLProduction +# +# Build method expectations (Unity side) +# - The selected build method should be a static C# function callable by Unity with: +# - No arguments (or signature supported by Unity batchmode). +# - It should configure build options and write the WebGL build output into the project Build/ path +# or another deterministic directory consumed by artifact upload below. +# - Example namespace/class/method naming is provided here as a convention; implement accordingly. +# +# Caching strategy +# - Caches the Library/ folder per (OS, Unity version, Packages/manifest.json hash) to reduce reimport. +# - On cache hits, compilations and imports should be much faster. +# - We intentionally do not cache Temp/ or final Build/ outputs; those are uploaded as artifacts. +# +# LFS and checkout +# - actions/checkout is configured with lfs: true and fetch-depth: 1 +# - Ensures real LFS blobs for the current commit are available to the build. +# - Uses shallow history for speed (no need for full Git history in CI). +# +# Artifacts +# - Uploads the default game-ci build/ directory and the project Build/ directory. +# - Adjust artifact paths if your build method writes to a different location. +# +# Invocation examples +# - Production (auto-detect Unity, default project path): +# unityVersion: auto +# buildProfile: WebGLProductionBuild +# - Development: +# unityVersion: auto +# buildProfile: WebGLDevelopmentBuild +# - Explicit method (overrides profile mapping): +# buildMethod: SpaceCraft.Editor.Builds.BuildWebGLProduction +# +# Notes +# - This workflow focuses on GitHub-hosted runners (no self-hosted required). +# - For further speed-ups, consider enabling editor caching in the action if appropriate and supported +# by your environment, or splitting content generation into a separate, cacheable step. +################################################################################################### name: Build Unity SpaceCraft on: @@ -46,6 +106,9 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + lfs: true + fetch-depth: 1 - name: Detect Unity version from ProjectVersion.txt id: detect @@ -70,6 +133,15 @@ jobs: echo "Detected Unity version: $VERSION" echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Cache Unity Library (project import cache) + uses: actions/cache@v4 + with: + path: | + ${{ inputs.projectPath }}/Library + key: ${{ runner.os }}-unity-library-${{ steps.detect.outputs.version }}-${{ hashFiles(format('{0}/Packages/manifest.json', inputs.projectPath)) }} + restore-keys: | + ${{ runner.os }}-unity-library-${{ steps.detect.outputs.version }}- + - name: Compute build method id: compute shell: bash @@ -105,7 +177,7 @@ jobs: unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} uses_custom_image: false - # Optional: enable cache + # Optional: enable Unity editor cache inside action (requires action support) # cacheUnity: true # cacheVersioning: semantic diff --git a/SvelteKit/BackSpace/package.json b/SvelteKit/BackSpace/package.json index 27642bee..fdd16a37 100644 --- a/SvelteKit/BackSpace/package.json +++ b/SvelteKit/BackSpace/package.json @@ -60,7 +60,8 @@ "unity:logs": "tsx scripts/unity-automation.js check-logs", "unity:start": "tsx scripts/unity-automation.js launch", "unity:openproject": "tsx scripts/unity-automation.js run openproject", - "unity:prebuild": "tsx scripts/unity-automation.js prebuild-webgl" + "unity:prebuild": "tsx scripts/unity-automation.js prebuild-webgl", + "trigger-build-unity-spacecraft": "tsx scripts/gh-trigger-build.ts" }, "dependencies": { "async-retry": "^1.3.3", diff --git a/SvelteKit/BackSpace/scripts/gh-trigger-build.ts b/SvelteKit/BackSpace/scripts/gh-trigger-build.ts new file mode 100644 index 00000000..d41357fd --- /dev/null +++ b/SvelteKit/BackSpace/scripts/gh-trigger-build.ts @@ -0,0 +1,79 @@ +/** + * Trigger Build Unity SpaceCraft GitHub Action + * + * This CLI fires the workflow_dispatch for .github/workflows/build-unity-spacecraft.yml + * and forwards parameters provided on the command line. + * + * Usage examples: + * npm run trigger-build-unity-spacecraft -- \ + * --unityVersion auto \ + * --projectPath "Unity/SpaceCraft" \ + * --targetPlatform WebGL \ + * --buildProfile WebGLProductionBuild \ + * --ref main + * + * npm run trigger-build-unity-spacecraft -- \ + * --unityVersion 2022.3.45f1 \ + * --buildMethod SpaceCraft.Editor.Builds.BuildWebGLDevelopment \ + * --ref my-feature-branch + */ + +import { Command } from 'commander'; +import { execSync } from 'node:child_process'; + +function ensureGhCliAvailable() { + try { + execSync('gh --version', { stdio: 'ignore' }); + } catch (err) { + console.error('gh CLI is required. Install from https://cli.github.com/'); + process.exit(1); + } +} + +const program = new Command(); +program + .option('--unityVersion ', 'Unity Editor version or "auto"', 'auto') + .option('--projectPath ', 'Unity project path', 'Unity/SpaceCraft') + .option('--targetPlatform ', 'Unity target platform', 'WebGL') + .option('--buildProfile ', 'Build profile token', 'WebGLProductionBuild') + .option('--buildMethod ', 'Explicit Unity build method (overrides buildProfile)', '') + .requiredOption('--ref ', 'Git ref to run the workflow on (branch, tag, or commit)') + .option('--repo ', 'Override GitHub repo in owner/name format'); + +program.parse(process.argv); +const opts = program.opts(); + +ensureGhCliAvailable(); + +const args: string[] = ['workflow', 'run', 'build-unity-spacecraft.yml']; + +if (opts.repo) { + args.push('--repo', opts.repo); +} +args.push('--ref', opts.ref); + +function pushField(name: string, value: string | undefined) { + if (typeof value === 'string') { + args.push('-f', `${name}=${value}`); + } +} + +pushField('unityVersion', opts.unityVersion); +pushField('projectPath', opts.projectPath); +pushField('targetPlatform', opts.targetPlatform); +pushField('buildProfile', opts.buildProfile); +if (opts.buildMethod) { + pushField('buildMethod', opts.buildMethod); +} + +try { + console.log('[gh] Running:', 'gh', args.join(' ')); + const out = execSync(`gh ${args.map(a => (a.includes(' ') ? `'${a.replace(/'/g, "'\\''")}'` : a)).join(' ')}`, { + stdio: 'inherit', + }); +} catch (err: any) { + const code = typeof err?.status === 'number' ? err.status : 1; + process.exit(code); +} + + From d2b894b06ed6d1ed8df2b5e66ea9b996ab06c4a4 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 21:07:04 +0200 Subject: [PATCH 04/29] tweaks --- .github/workflows/build-unity-spacecraft.yml | 45 ++++++++++++++------ 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index a2a59391..a043b05c 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -110,22 +110,41 @@ jobs: lfs: true fetch-depth: 1 - - name: Detect Unity version from ProjectVersion.txt + - name: Detect Unity version and resolve project path id: detect shell: bash run: | set -euo pipefail - if [[ "${{ inputs.unityVersion }}" != "auto" && -n "${{ inputs.unityVersion }}" ]]; then - echo "Using provided unityVersion: ${{ inputs.unityVersion }}" - echo "version=${{ inputs.unityVersion }}" >> "$GITHUB_OUTPUT" - exit 0 + # If version provided, honor it and still resolve project path for later steps + REQ_VER="${{ inputs.unityVersion }}" + PROJ_PATH_IN="${{ inputs.projectPath }}" + CANDIDATE_FILE="${{ github.workspace }}/${PROJ_PATH_IN}/ProjectSettings/ProjectVersion.txt" + + if [[ ! -f "$CANDIDATE_FILE" ]]; then + echo "Input projectPath not found: $CANDIDATE_FILE" >&2 + echo "Searching for ProjectVersion.txt under workspace..." + MAPFILE -t FOUND < <(find "${{ github.workspace }}" -type f -path "*/ProjectSettings/ProjectVersion.txt" | sort) + if [[ ${#FOUND[@]} -eq 0 ]]; then + echo "No ProjectVersion.txt found in repository" >&2 + exit 1 + fi + CANDIDATE_FILE="${FOUND[0]}" + echo "Using detected ProjectVersion.txt: $CANDIDATE_FILE" fi - FILE="${{ github.workspace }}/${{ inputs.projectPath }}/ProjectSettings/ProjectVersion.txt" - if [[ ! -f "$FILE" ]]; then - echo "ProjectVersion.txt not found at: $FILE" >&2 - exit 1 + + # Derive resolved project path (folder containing ProjectSettings) + RESOLVED_PROJECT_PATH="$(cd "$(dirname "$CANDIDATE_FILE")/.." && pwd)" + # Convert to path relative to workspace for downstream steps + RESOLVED_PROJECT_PATH_REL="${RESOLVED_PROJECT_PATH#"${{ github.workspace }}/"}" + echo "projectPath=$RESOLVED_PROJECT_PATH_REL" >> "$GITHUB_OUTPUT" + + if [[ "$REQ_VER" != "auto" && -n "$REQ_VER" ]]; then + echo "Using provided unityVersion: $REQ_VER" + echo "version=$REQ_VER" >> "$GITHUB_OUTPUT" + exit 0 fi - VERSION=$(grep -E "^m_EditorVersion:\s*" "$FILE" | sed -E 's/^m_EditorVersion:\s*([^[:space:]]+).*/\1/') + + VERSION=$(grep -E "^m_EditorVersion:\s*" "$CANDIDATE_FILE" | sed -E 's/^m_EditorVersion:\s*([^[:space:]]+).*/\1/') if [[ -z "$VERSION" ]]; then echo "Failed to parse Unity version from ProjectVersion.txt" >&2 exit 1 @@ -137,8 +156,8 @@ jobs: uses: actions/cache@v4 with: path: | - ${{ inputs.projectPath }}/Library - key: ${{ runner.os }}-unity-library-${{ steps.detect.outputs.version }}-${{ hashFiles(format('{0}/Packages/manifest.json', inputs.projectPath)) }} + ${{ steps.detect.outputs.projectPath }}/Library + key: ${{ runner.os }}-unity-library-${{ steps.detect.outputs.version }}-${{ hashFiles(format('{0}/Packages/manifest.json', steps.detect.outputs.projectPath)) }} restore-keys: | ${{ runner.os }}-unity-library-${{ steps.detect.outputs.version }}- @@ -172,7 +191,7 @@ jobs: UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} with: - projectPath: ${{ inputs.projectPath }} + projectPath: ${{ steps.detect.outputs.projectPath }} targetPlatform: ${{ inputs.targetPlatform }} unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} From e3787f363c018354aa71232e9e8c4f51ddeaa059 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 21:11:07 +0200 Subject: [PATCH 05/29] tweaks --- .github/workflows/build-unity-spacecraft.yml | 39 ++++++++++++++++---- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index a043b05c..b426eb0e 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -115,21 +115,31 @@ jobs: shell: bash run: | set -euo pipefail + echo "[detect] GITHUB_WORKSPACE=${{ github.workspace }}" + echo "[detect] inputs.projectPath='${{ inputs.projectPath }}' inputs.unityVersion='${{ inputs.unityVersion }}'" + echo "[detect] Workspace root listing:" + ls -la "${{ github.workspace }}" || true + # If version provided, honor it and still resolve project path for later steps REQ_VER="${{ inputs.unityVersion }}" PROJ_PATH_IN="${{ inputs.projectPath }}" CANDIDATE_FILE="${{ github.workspace }}/${PROJ_PATH_IN}/ProjectSettings/ProjectVersion.txt" + echo "[detect] Initial candidate file: $CANDIDATE_FILE" + echo "[detect] Listing input project path: '${PROJ_PATH_IN}'" + ls -la "${{ github.workspace }}/${PROJ_PATH_IN}" || true if [[ ! -f "$CANDIDATE_FILE" ]]; then - echo "Input projectPath not found: $CANDIDATE_FILE" >&2 - echo "Searching for ProjectVersion.txt under workspace..." + echo "[detect] Input projectPath not found: $CANDIDATE_FILE" >&2 + echo "[detect] Searching for ProjectVersion.txt under workspace..." MAPFILE -t FOUND < <(find "${{ github.workspace }}" -type f -path "*/ProjectSettings/ProjectVersion.txt" | sort) + echo "[detect] Found ${#FOUND[@]} candidates" if [[ ${#FOUND[@]} -eq 0 ]]; then - echo "No ProjectVersion.txt found in repository" >&2 + echo "[detect] No ProjectVersion.txt found in repository" >&2 exit 1 fi CANDIDATE_FILE="${FOUND[0]}" - echo "Using detected ProjectVersion.txt: $CANDIDATE_FILE" + echo "[detect] Using detected ProjectVersion.txt: $CANDIDATE_FILE" + echo "[detect] Candidates (up to 10):" && printf '%s\n' "${FOUND[@]:0:10}" fi # Derive resolved project path (folder containing ProjectSettings) @@ -137,21 +147,33 @@ jobs: # Convert to path relative to workspace for downstream steps RESOLVED_PROJECT_PATH_REL="${RESOLVED_PROJECT_PATH#"${{ github.workspace }}/"}" echo "projectPath=$RESOLVED_PROJECT_PATH_REL" >> "$GITHUB_OUTPUT" + echo "[detect] RESOLVED_PROJECT_PATH=$RESOLVED_PROJECT_PATH" + echo "[detect] RESOLVED_PROJECT_PATH_REL=$RESOLVED_PROJECT_PATH_REL" + echo "[detect] ProjectSettings listing:" && ls -la "$RESOLVED_PROJECT_PATH/ProjectSettings" || true + echo "[detect] cat ProjectVersion.txt:" && cat "$CANDIDATE_FILE" || true if [[ "$REQ_VER" != "auto" && -n "$REQ_VER" ]]; then - echo "Using provided unityVersion: $REQ_VER" + echo "[detect] Using provided unityVersion: $REQ_VER" echo "version=$REQ_VER" >> "$GITHUB_OUTPUT" exit 0 fi VERSION=$(grep -E "^m_EditorVersion:\s*" "$CANDIDATE_FILE" | sed -E 's/^m_EditorVersion:\s*([^[:space:]]+).*/\1/') if [[ -z "$VERSION" ]]; then - echo "Failed to parse Unity version from ProjectVersion.txt" >&2 + echo "[detect] Failed to parse Unity version from ProjectVersion.txt" >&2 exit 1 fi - echo "Detected Unity version: $VERSION" + echo "[detect] Detected Unity version: $VERSION" echo "version=$VERSION" >> "$GITHUB_OUTPUT" + - name: Log build configuration + shell: bash + run: | + echo "[config] projectPath='${{ steps.detect.outputs.projectPath }}'" + echo "[config] unityVersion='${{ steps.detect.outputs.version }}'" + echo "[config] targetPlatform='${{ inputs.targetPlatform }}'" + echo "[config] buildProfile='${{ inputs.buildProfile }}'" + - name: Cache Unity Library (project import cache) uses: actions/cache@v4 with: @@ -183,6 +205,7 @@ jobs: esac fi echo "buildMethod=$METHOD" >> "$GITHUB_OUTPUT" + echo "[compute] Selected build method: $METHOD" - name: Unity - Builder uses: game-ci/unity-builder@v4 @@ -207,5 +230,5 @@ jobs: name: SpaceCraft-${{ inputs.targetPlatform }}-${{ inputs.buildProfile }} path: | build - ${{ inputs.projectPath }}/Build + ${{ steps.detect.outputs.projectPath }}/Build From 699ca24580e0afec1c2019da1337ca357d9b78ce Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 21:12:53 +0200 Subject: [PATCH 06/29] tweaks --- .github/workflows/build-unity-spacecraft.yml | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index b426eb0e..3edd4809 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -110,6 +110,16 @@ jobs: lfs: true fetch-depth: 1 + - name: Ensure LFS files are materialized (smudged) + shell: bash + run: | + set -euo pipefail + echo "[lfs] git lfs version:" && git lfs version || true + echo "[lfs] Pulling LFS objects (broad)" + git lfs pull --exclude="" --include="" || true + echo "[lfs] Forcing checkout (smudge) of LFS pointers" + git lfs checkout || true + - name: Detect Unity version and resolve project path id: detect shell: bash @@ -150,7 +160,15 @@ jobs: echo "[detect] RESOLVED_PROJECT_PATH=$RESOLVED_PROJECT_PATH" echo "[detect] RESOLVED_PROJECT_PATH_REL=$RESOLVED_PROJECT_PATH_REL" echo "[detect] ProjectSettings listing:" && ls -la "$RESOLVED_PROJECT_PATH/ProjectSettings" || true - echo "[detect] cat ProjectVersion.txt:" && cat "$CANDIDATE_FILE" || true + echo "[detect] cat ProjectVersion.txt (pre-smudge):" && cat "$CANDIDATE_FILE" || true + + # If file still looks like an LFS pointer, try to smudge just this file and re-check + if head -n 1 "$CANDIDATE_FILE" | grep -q '^version https://git-lfs.github.com/spec/v1'; then + echo "[detect] ProjectVersion.txt appears to be an LFS pointer; attempting smudge" + git lfs checkout -- "$CANDIDATE_FILE" || true + git lfs pull --include="$RESOLVED_PROJECT_PATH_REL/ProjectSettings/ProjectVersion.txt" --exclude="" || true + echo "[detect] cat ProjectVersion.txt (post-smudge):" && cat "$CANDIDATE_FILE" || true + fi if [[ "$REQ_VER" != "auto" && -n "$REQ_VER" ]]; then echo "[detect] Using provided unityVersion: $REQ_VER" From 7d1049cfba538cee5b39603b2e48a24f9fc76560 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 21:17:27 +0200 Subject: [PATCH 07/29] Stop tracking ProjectVersion.txt via LFS; commit real text --- .gitattributes | 1 + Unity/SpaceCraft/ProjectSettings/ProjectVersion.txt | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitattributes b/.gitattributes index b93aa36b..8d98d174 100644 --- a/.gitattributes +++ b/.gitattributes @@ -48,3 +48,4 @@ *.mp3 filter=lfs diff=lfs merge=lfs -text *.wav filter=lfs diff=lfs merge=lfs -text *.ogg filter=lfs diff=lfs merge=lfs -text +Unity/**/ProjectSettings/ProjectVersion.txt text eol=lf -filter -diff -merge diff --git a/Unity/SpaceCraft/ProjectSettings/ProjectVersion.txt b/Unity/SpaceCraft/ProjectSettings/ProjectVersion.txt index 3c3dbfe7..7f0a5364 100644 --- a/Unity/SpaceCraft/ProjectSettings/ProjectVersion.txt +++ b/Unity/SpaceCraft/ProjectSettings/ProjectVersion.txt @@ -1,3 +1,2 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:2152feb1c6edf0221070c3ab8f52d9de45be39c979b719ca4f7f3b612b69a4e7 -size 85 +m_EditorVersion: 6000.0.36f1 +m_EditorVersionWithRevision: 6000.0.36f1 (9fe3b5f71dbb) From 60bfebfa05e800112363d23c392fee1c2353d5a0 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 22:15:48 +0200 Subject: [PATCH 08/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 15 ++++++++++++++- .../Assets/Libraries/Bridge/BridgePlugin.cs | 4 +++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 3edd4809..a5742e92 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -98,6 +98,7 @@ jobs: build: name: Build ${{ inputs.targetPlatform }} (${{ inputs.buildProfile }}) runs-on: ubuntu-latest + timeout-minutes: 90 env: PROJECT_PATH: ${{ inputs.projectPath }} @@ -192,6 +193,19 @@ jobs: echo "[config] targetPlatform='${{ inputs.targetPlatform }}'" echo "[config] buildProfile='${{ inputs.buildProfile }}'" + - name: Validate Unity activation secrets + shell: bash + run: | + set -euo pipefail + missing=0 + if [[ -z "${{ secrets.UNITY_LICENSE }}" ]]; then echo "[secrets] UNITY_LICENSE is missing" >&2; missing=1; fi + if [[ -z "${{ secrets.UNITY_EMAIL }}" ]]; then echo "[secrets] UNITY_EMAIL is missing" >&2; missing=1; fi + if [[ -z "${{ secrets.UNITY_PASSWORD }}" ]]; then echo "[secrets] UNITY_PASSWORD is missing" >&2; missing=1; fi + if [[ "$missing" -ne 0 ]]; then + echo "[secrets] One or more required secrets are missing. Per GameCI v4 Personal license activation, provide UNITY_LICENSE, UNITY_EMAIL, UNITY_PASSWORD." >&2 + exit 1 + fi + - name: Cache Unity Library (project import cache) uses: actions/cache@v4 with: @@ -236,7 +250,6 @@ jobs: targetPlatform: ${{ inputs.targetPlatform }} unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} - uses_custom_image: false # Optional: enable Unity editor cache inside action (requires action support) # cacheUnity: true # cacheVersioning: semantic diff --git a/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs b/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs index c89dc3ef..abfb8bf6 100644 --- a/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs +++ b/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs @@ -772,10 +772,12 @@ private IntPtr GetRenderEventFunc() renderEventFunc = #if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE (IntPtr)_CBridgePlugin_GetRenderEventFunc(); -#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX (IntPtr)0; // TODO #elif UNITY_ANDROID (IntPtr)plugin.CallStatic("GetRenderEventFunc"); +#else + (IntPtr)0; // Fallback #endif //Debug.Log("BridgePlugin: GetRenderEventFunc: Got renderEventFunc: " + renderEventFunc); } From 3bfcbdbaf5f326f9ab239eb6df9799369b6674f5 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 22:52:29 +0200 Subject: [PATCH 09/29] tweak --- .../Assets/Libraries/Bridge/BridgePlugin.cs | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs b/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs index abfb8bf6..255e238c 100644 --- a/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs +++ b/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs @@ -770,14 +770,16 @@ private IntPtr GetRenderEventFunc() { if (renderEventFunc == (IntPtr)0) { renderEventFunc = -#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS (IntPtr)_CBridgePlugin_GetRenderEventFunc(); -#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX - (IntPtr)0; // TODO +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_WEBGL + (IntPtr)0; // TODO (Windows) #elif UNITY_ANDROID (IntPtr)plugin.CallStatic("GetRenderEventFunc"); +#elif UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_WEBGL + (IntPtr)0; // Fallback for Linux/WebGL where native plugin call is not available #else - (IntPtr)0; // Fallback + (IntPtr)0; // Other platforms fallback #endif //Debug.Log("BridgePlugin: GetRenderEventFunc: Got renderEventFunc: " + renderEventFunc); } @@ -790,12 +792,14 @@ public void RenderIntoTexture(int width, int height) { //Debug.Log("BridgePlugin: RenderIntoTexture: time: " + Time.time + " width: " + width + " height: " + height); -#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS _CBridgePlugin_RenderIntoTextureSetup(plugin, width, height); -#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_WEBGL // TODO #elif UNITY_ANDROID plugin.Call("RenderIntoTextureSetup", width, height); +#else + // TODO (fallback) #endif } @@ -803,7 +807,7 @@ public void RenderIntoTexture(int width, int height) public void FlushCaches() { -#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS if (plugin == IntPtr.Zero) { return; @@ -811,7 +815,7 @@ public void FlushCaches() _CBridgePlugin_FlushCaches(plugin); -#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN +#elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN || UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_WEBGL // TODO @@ -855,7 +859,7 @@ public void IssuePluginRenderEvent() { //Debug.Log("BridgePlugin: IssuePluginRenderEvent: time: " + Time.time + " pluginID: " + pluginID + " this: " + this); -#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE || UNITY_ANDROID +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS || UNITY_ANDROID GL.IssuePluginEvent(GetRenderEventFunc(), 2); #endif } From 8294ab73a24f415da7cd09146e1db95d028ac3a8 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 23:03:57 +0200 Subject: [PATCH 10/29] tweak --- .../Assets/Libraries/Bridge/BridgePlugin.cs | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs b/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs index 255e238c..3c3d6dfb 100644 --- a/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs +++ b/Unity/SpaceCraft/Assets/Libraries/Bridge/BridgePlugin.cs @@ -60,7 +60,7 @@ public class BridgePlugin : MonoBehaviour { public bool pluginRenderEventIssued; public List messageQueue = new List(); -#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS IntPtr plugin; #elif UNITY_EDITOR_WIN || UNITY_STANDALONE_WIN // TODO @@ -77,7 +77,7 @@ public bool IsKeyboardVisible { get { #if UNITY_ANDROID return isKeyboardVisible; -#elif UNITY_IPHONE +#elif UNITY_IOS return TouchScreenKeyboard.visible; #else return false; @@ -103,11 +103,11 @@ public void SetKeyboardVisible(string isVisible) private const string PLUGIN_DLL = "Bridge_Editor"; #elif UNITY_STANDALONE_WIN private const string PLUGIN_DLL = "Bridge"; -#elif UNITY_IPHONE +#elif UNITY_IOS private const string PLUGIN_DLL = "__Internal"; #endif -#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS [DllImport(PLUGIN_DLL)] private static extern void _CBridgePlugin_SetUnitySendMessageCallback(IntPtr unitySendMessageCallback); @@ -558,7 +558,7 @@ public void EvaluateJSReturnResult(string js) public bool CanGoBack() { -#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IPHONE +#if UNITY_EDITOR_OSX || UNITY_STANDALONE_OSX || UNITY_IOS if (plugin == IntPtr.Zero) { return false; @@ -579,6 +579,11 @@ public bool CanGoBack() return plugin.Get("canGoBack"); +#else + + // Fallback for Linux/WebGL/other platforms + return false; + #endif } @@ -608,6 +613,11 @@ public bool CanGoForward() return plugin.Get("canGoForward"); +#else + + // Fallback for Linux/WebGL/other platforms + return false; + #endif } @@ -729,6 +739,8 @@ public void CallOnTexture() long newTextureHandle = 0; #elif UNITY_ANDROID long newTextureHandle = plugin.Call("GetRenderTextureHandle"); +#elif UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_WEBGL + long newTextureHandle = 0; #endif //Debug.Log("BridgePlugin: CallOnTexture: newTextureHandle: " + newTextureHandle + " textureHandle: " + textureHandle); @@ -750,6 +762,9 @@ public void CallOnTexture() #elif UNITY_ANDROID textureWidth = plugin.Call("GetRenderTextureWidth"); textureHeight = plugin.Call("GetRenderTextureHeight"); +#elif UNITY_EDITOR_LINUX || UNITY_STANDALONE_LINUX || UNITY_WEBGL + textureWidth = 0; + textureHeight = 0; #endif texture = Texture2D.CreateExternalTexture(textureWidth, textureHeight, TextureFormat.RGBA32, false, true, (IntPtr)textureHandle); From 7cd4b09ec932c454e58b46f5d0bdd3656f95bc41 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 23:17:10 +0200 Subject: [PATCH 11/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index a5742e92..5ae77602 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -225,10 +225,10 @@ jobs: else case "${{ inputs.buildProfile }}" in WebGLProductionBuild) - METHOD="SpaceCraft.Editor.Builds.BuildWebGLProduction" + METHOD="Build.WebGL_Prod" ;; WebGLDevelopmentBuild) - METHOD="SpaceCraft.Editor.Builds.BuildWebGLDevelopment" + METHOD="Build.WebGL_Dev" ;; *) echo "Unknown buildProfile: ${{ inputs.buildProfile }}" >&2 From dd8e4cde0c6559c47dd65d813b92c01452e64cf7 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 23:27:39 +0200 Subject: [PATCH 12/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 16 +++++++++ Unity/SpaceCraft/Assets/Editor/Build.cs | 36 +++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 5ae77602..340586e0 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -239,6 +239,12 @@ jobs: echo "buildMethod=$METHOD" >> "$GITHUB_OUTPUT" echo "[compute] Selected build method: $METHOD" + - name: Prepare logs directory + shell: bash + run: | + mkdir -p Logs + mkdir -p "${{ steps.detect.outputs.projectPath }}/Logs" + - name: Unity - Builder uses: game-ci/unity-builder@v4 env: @@ -250,6 +256,7 @@ jobs: targetPlatform: ${{ inputs.targetPlatform }} unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} + customParameters: -logFile Logs/Editor.log -stackTraceLogType Full # Optional: enable Unity editor cache inside action (requires action support) # cacheUnity: true # cacheVersioning: semantic @@ -263,3 +270,12 @@ jobs: build ${{ steps.detect.outputs.projectPath }}/Build + - name: Upload Unity logs (always) + if: always() + uses: actions/upload-artifact@v4 + with: + name: Unity-Logs + path: | + Logs + ${{ steps.detect.outputs.projectPath }}/Logs + diff --git a/Unity/SpaceCraft/Assets/Editor/Build.cs b/Unity/SpaceCraft/Assets/Editor/Build.cs index 74df9ad4..3c223b0e 100644 --- a/Unity/SpaceCraft/Assets/Editor/Build.cs +++ b/Unity/SpaceCraft/Assets/Editor/Build.cs @@ -196,12 +196,40 @@ private static void RemoveSymlinksRecursive(string path) private static string[] GetBuildScenes() { - // Get all enabled scenes from the build settings - var scenes = new string[EditorBuildSettings.scenes.Length]; - for (int i = 0; i < scenes.Length; i++) + // Prefer enabled scenes from Build Settings + var enabledScenes = System.Array.FindAll(EditorBuildSettings.scenes, s => s.enabled); + string[] scenes = new string[enabledScenes.Length]; + for (int i = 0; i < enabledScenes.Length; i++) { - scenes[i] = EditorBuildSettings.scenes[i].path; + scenes[i] = enabledScenes[i].path; } + + // Fallback: if none configured/enabled, include all .unity scenes under Assets + if (scenes.Length == 0) + { + Debug.LogWarning("[Build] No enabled scenes in Build Settings; falling back to discover all .unity scenes under Assets/"); + try + { + var found = Directory.GetFiles("Assets", "*.unity", SearchOption.AllDirectories); + scenes = found; + foreach (var scene in scenes) + { + Debug.Log($"[Build] Including scene: {scene}"); + } + } + catch (System.Exception ex) + { + Debug.LogError($"[Build] Failed to enumerate scenes: {ex.Message}"); + } + } + + // Validate + if (scenes == null || scenes.Length == 0) + { + Debug.LogError("[Build] No scenes found to build. Configure Build Settings or add scenes under Assets/."); + if (IsCommandLineBuild()) EditorApplication.Exit(1); + } + return scenes; } From e623b3dba5a0ab07dc5942ca496ec592bc2c16ab Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Mon, 15 Sep 2025 23:53:20 +0200 Subject: [PATCH 13/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 340586e0..a4b988a2 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -269,6 +269,8 @@ jobs: path: | build ${{ steps.detect.outputs.projectPath }}/Build + ${{ steps.detect.outputs.projectPath }}/Builds/SpaceCraft + WebSites/SpaceCraft - name: Upload Unity logs (always) if: always() From e8bb1870aa5b71f010a975da04e70112b599d338 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 00:10:17 +0200 Subject: [PATCH 14/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index a4b988a2..59b21bd1 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -267,10 +267,7 @@ jobs: with: name: SpaceCraft-${{ inputs.targetPlatform }}-${{ inputs.buildProfile }} path: | - build - ${{ steps.detect.outputs.projectPath }}/Build ${{ steps.detect.outputs.projectPath }}/Builds/SpaceCraft - WebSites/SpaceCraft - name: Upload Unity logs (always) if: always() From be5c9ac95051736bfa36c5cdc9c0f361267b58c5 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 00:37:48 +0200 Subject: [PATCH 15/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 31 +++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 59b21bd1..f071d149 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -261,13 +261,42 @@ jobs: # cacheUnity: true # cacheVersioning: semantic + - name: Setup Node.js + if: success() + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install controller dependencies + if: success() + working-directory: WebSites/controller + run: npm ci + + - name: Build controller + if: success() + working-directory: WebSites/controller + run: npm run build + + - name: Assemble website bundle + if: success() + shell: bash + run: | + set -euo pipefail + OUT_DIR="${{ steps.detect.outputs.projectPath }}/Builds" + echo "[assemble] Output: $OUT_DIR" + mkdir -p "$OUT_DIR/controller" + cp -f WebSites/index.html "$OUT_DIR/index.html" + cp -f WebSites/controller/index.html "$OUT_DIR/controller/index.html" + cp -R WebSites/controller/lib "$OUT_DIR/controller/lib" + cp -R WebSites/controller/build "$OUT_DIR/controller/build" + - name: Upload build artifacts if: success() uses: actions/upload-artifact@v4 with: name: SpaceCraft-${{ inputs.targetPlatform }}-${{ inputs.buildProfile }} path: | - ${{ steps.detect.outputs.projectPath }}/Builds/SpaceCraft + ${{ steps.detect.outputs.projectPath }}/Builds - name: Upload Unity logs (always) if: always() From ff04e4470efe3392e2bbf0244129a0281fc96be9 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 00:51:02 +0200 Subject: [PATCH 16/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index f071d149..06a0ca31 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -270,7 +270,17 @@ jobs: - name: Install controller dependencies if: success() working-directory: WebSites/controller - run: npm ci + shell: bash + run: | + set -euo pipefail + echo "[controller] PWD=$(pwd) contents:" && ls -la + if [[ -f package-lock.json ]]; then + echo "[controller] Using npm ci" + npm ci + else + echo "[controller] package-lock.json not found; using npm install" + npm install + fi - name: Build controller if: success() From 9cbbca2fd103f3b3feaf6abc326681c57ea35cd8 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 01:06:33 +0200 Subject: [PATCH 17/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 06a0ca31..935fd398 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -282,10 +282,21 @@ jobs: npm install fi + - name: Install pnpm for build script + if: success() + working-directory: WebSites/controller + shell: bash + run: | + echo "[controller] Ensuring pnpm is available in $(pwd)" + npm i -g pnpm@8 + - name: Build controller if: success() working-directory: WebSites/controller - run: npm run build + shell: bash + run: | + echo "[controller] Building in $(pwd)" + npm run build - name: Assemble website bundle if: success() From d2754e143a670a920a8929834ede5617f907b177 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 01:25:10 +0200 Subject: [PATCH 18/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 935fd398..31c38f27 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -257,9 +257,9 @@ jobs: unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} customParameters: -logFile Logs/Editor.log -stackTraceLogType Full - # Optional: enable Unity editor cache inside action (requires action support) - # cacheUnity: true - # cacheVersioning: semantic + runAsHostUser: true + cacheUnity: true + cacheVersioning: semantic - name: Setup Node.js if: success() From fb2826ff10f0d5c3695afc52a85dbf496f168467 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 01:32:26 +0200 Subject: [PATCH 19/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 41 +++++++++++--------- Unity/SpaceCraft/Assets/Editor/Build.cs | 18 ++++++++- 2 files changed, 38 insertions(+), 21 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 31c38f27..5e374427 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -251,6 +251,7 @@ jobs: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} + SC_BUILD_OUTPUT: dist/SpaceCraft with: projectPath: ${{ steps.detect.outputs.projectPath }} targetPlatform: ${{ inputs.targetPlatform }} @@ -298,18 +299,30 @@ jobs: echo "[controller] Building in $(pwd)" npm run build - - name: Assemble website bundle + - name: Assemble flat artifact (no wrapper directory) if: success() shell: bash run: | set -euo pipefail - OUT_DIR="${{ steps.detect.outputs.projectPath }}/Builds" - echo "[assemble] Output: $OUT_DIR" - mkdir -p "$OUT_DIR/controller" - cp -f WebSites/index.html "$OUT_DIR/index.html" - cp -f WebSites/controller/index.html "$OUT_DIR/controller/index.html" - cp -R WebSites/controller/lib "$OUT_DIR/controller/lib" - cp -R WebSites/controller/build "$OUT_DIR/controller/build" + STAGE="dist" + echo "[assemble] Using Unity output already at $STAGE/SpaceCraft" + mkdir -p "$STAGE/controller" + # Top-level site index + if [[ -f WebSites/index.html ]]; then + cp -f WebSites/index.html "$STAGE/index.html" + fi + # Controller site + if [[ -f WebSites/controller/index.html ]]; then + cp -f WebSites/controller/index.html "$STAGE/controller/index.html" + fi + if [[ -d WebSites/controller/lib ]]; then + cp -R WebSites/controller/lib "$STAGE/controller/lib" + fi + if [[ -d WebSites/controller/build ]]; then + cp -R WebSites/controller/build "$STAGE/controller/build" + fi + echo "[assemble] Final artifact layout:" + (cd "$STAGE" && find . -maxdepth 2 -type f | sed 's#^./##') - name: Upload build artifacts if: success() @@ -317,14 +330,4 @@ jobs: with: name: SpaceCraft-${{ inputs.targetPlatform }}-${{ inputs.buildProfile }} path: | - ${{ steps.detect.outputs.projectPath }}/Builds - - - name: Upload Unity logs (always) - if: always() - uses: actions/upload-artifact@v4 - with: - name: Unity-Logs - path: | - Logs - ${{ steps.detect.outputs.projectPath }}/Logs - + dist/** diff --git a/Unity/SpaceCraft/Assets/Editor/Build.cs b/Unity/SpaceCraft/Assets/Editor/Build.cs index 3c223b0e..0a002b96 100644 --- a/Unity/SpaceCraft/Assets/Editor/Build.cs +++ b/Unity/SpaceCraft/Assets/Editor/Build.cs @@ -44,9 +44,15 @@ public static void WebGL_Dev() PlayerSettings.WebGL.template = templateName; } - //string buildPath = Path.Combine("Builds", "SpaceCraft"); - // Build directly into WebSites/SpaceCraft at top level of repo. + // Default dev output (existing behavior) string buildPath = Path.Combine("..", "..", "WebSites", "SpaceCraft"); + // Allow CI override via env var + var envOut = Environment.GetEnvironmentVariable("SC_BUILD_OUTPUT"); + if (!string.IsNullOrEmpty(envOut)) + { + buildPath = envOut; + Debug.Log($"[Build] Overriding output via SC_BUILD_OUTPUT => {buildPath}"); + } BuildPlayerOptions options = new BuildPlayerOptions { @@ -87,7 +93,15 @@ public static void WebGL_Prod() } // --- End Force --- + // Default prod output (existing behavior) string buildPath = Path.Combine("Builds", "SpaceCraft"); + // Allow CI override via env var + var envOut = Environment.GetEnvironmentVariable("SC_BUILD_OUTPUT"); + if (!string.IsNullOrEmpty(envOut)) + { + buildPath = envOut; + Debug.Log($"[Build] Overriding output via SC_BUILD_OUTPUT => {buildPath}"); + } BuildPlayerOptions options = new BuildPlayerOptions { From f56c2720862b3cc54a752f9b4ff801ce6ba7d6fb Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 01:47:59 +0200 Subject: [PATCH 20/29] built --- WebSites/SpaceCraft/Build/SpaceCraft.data | 2 +- WebSites/controller/build/TabBase.d.ts | 12 +-- WebSites/controller/build/TabBase.d.ts.map | 2 +- WebSites/controller/build/TabBase.js | 24 ------ WebSites/controller/build/TabBase.js.map | 2 +- WebSites/controller/build/TabNavigate.d.ts | 9 --- .../controller/build/TabNavigate.d.ts.map | 1 - WebSites/controller/build/TabNavigate.js | 81 ------------------- WebSites/controller/build/TabNavigate.js.map | 1 - WebSites/controller/build/TabSelect.d.ts | 8 ++ WebSites/controller/build/TabSelect.d.ts.map | 2 +- WebSites/controller/build/TabSelect.js | 19 ++++- WebSites/controller/build/TabSelect.js.map | 2 +- WebSites/controller/build/TabView.d.ts | 11 +++ WebSites/controller/build/TabView.d.ts.map | 2 +- WebSites/controller/build/TabView.js | 27 ++++++- WebSites/controller/build/TabView.js.map | 2 +- WebSites/controller/src/TabNavigate.ts | 81 ------------------- 18 files changed, 70 insertions(+), 218 deletions(-) delete mode 100644 WebSites/controller/build/TabNavigate.d.ts delete mode 100644 WebSites/controller/build/TabNavigate.d.ts.map delete mode 100644 WebSites/controller/build/TabNavigate.js delete mode 100644 WebSites/controller/build/TabNavigate.js.map delete mode 100644 WebSites/controller/src/TabNavigate.ts diff --git a/WebSites/SpaceCraft/Build/SpaceCraft.data b/WebSites/SpaceCraft/Build/SpaceCraft.data index ae7656fe..f24ef404 100644 --- a/WebSites/SpaceCraft/Build/SpaceCraft.data +++ b/WebSites/SpaceCraft/Build/SpaceCraft.data @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:c72299555009649e126a47545753a9c1463962439e6c69ea8acb3386b0e7db45 +oid sha256:60188a18e076ba81338b864f7080e8eaa521cf0fb864223c2093d35f5bc4294a size 10230368 diff --git a/WebSites/controller/build/TabBase.d.ts b/WebSites/controller/build/TabBase.d.ts index 8ce4dac3..e8e7d5ce 100644 --- a/WebSites/controller/build/TabBase.d.ts +++ b/WebSites/controller/build/TabBase.d.ts @@ -1,4 +1,4 @@ -import { IoElement, IoElementProps, ListenerDefinition } from 'io-gui'; +import { IoElement, IoElementProps } from 'io-gui'; import { SpacetimeController } from './SpacetimeController.js'; import { SimulatorState } from './SimulatorState.js'; export type TabBaseProps = IoElementProps & { @@ -9,17 +9,7 @@ export declare class TabBase extends IoElement { static get Style(): string; controller: SpacetimeController; simulatorState: SimulatorState; - static get Listeners(): { - contextmenu: string; - pointerdown: string; - touchstart: ListenerDefinition; - touchmove: ListenerDefinition; - }; constructor(props: TabBaseProps); - preventDefault(event: Event): void; - onPointerdown(event: PointerEvent): void; - onPointermove(event: PointerEvent): void; - onPointerup(event: PointerEvent): void; ready(): void; simulatorStateMutated(): void; changed(): void; diff --git a/WebSites/controller/build/TabBase.d.ts.map b/WebSites/controller/build/TabBase.d.ts.map index 91ed93f0..1191e4de 100644 --- a/WebSites/controller/build/TabBase.d.ts.map +++ b/WebSites/controller/build/TabBase.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"TabBase.d.ts","sourceRoot":"","sources":["../src/TabBase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,cAAc,EAA8B,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AACjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG;IAC1C,UAAU,EAAE,mBAAmB,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;CAChC,CAAC;AAEF,qBACa,OAAQ,SAAQ,SAAS;IAClC,MAAM,KAAK,KAAK,WAYf;IAGO,UAAU,EAAE,mBAAmB,CAAC;IAGhC,cAAc,EAAE,cAAc,CAAC;IAEvC,MAAM,KAAK,SAAS;;;oBAI0C,kBAAkB;mBACnB,kBAAkB;MAE9E;gBAEW,KAAK,EAAE,YAAY;IAI/B,cAAc,CAAC,KAAK,EAAE,KAAK;IAG3B,aAAa,CAAC,KAAK,EAAE,YAAY;IAMjC,aAAa,CAAC,KAAK,EAAE,YAAY;IACjC,WAAW,CAAC,KAAK,EAAE,YAAY;IAO/B,KAAK;IAIL,qBAAqB;IAIrB,OAAO;CAKV;AAED,eAAO,MAAM,OAAO,GAAY,MAAM,YAAY,iCAEjD,CAAC"} \ No newline at end of file +{"version":3,"file":"TabBase.d.ts","sourceRoot":"","sources":["../src/TabBase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAgB,cAAc,EAAkD,MAAM,QAAQ,CAAC;AACjH,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAErD,MAAM,MAAM,YAAY,GAAG,cAAc,GAAG;IAC1C,UAAU,EAAE,mBAAmB,CAAC;IAChC,cAAc,EAAE,cAAc,CAAC;CAChC,CAAC;AAEF,qBACa,OAAQ,SAAQ,SAAS;IAClC,MAAM,KAAK,KAAK,WAYf;IAGO,UAAU,EAAE,mBAAmB,CAAC;IAGhC,cAAc,EAAE,cAAc,CAAC;gBAE3B,KAAK,EAAE,YAAY;IAI/B,KAAK;IAIL,qBAAqB;IAIrB,OAAO;CAKV;AAED,eAAO,MAAM,OAAO,GAAY,MAAM,YAAY,iCAEjD,CAAC"} \ No newline at end of file diff --git a/WebSites/controller/build/TabBase.js b/WebSites/controller/build/TabBase.js index f73bf27d..d82549c3 100644 --- a/WebSites/controller/build/TabBase.js +++ b/WebSites/controller/build/TabBase.js @@ -20,33 +20,9 @@ let TabBase = class TabBase extends IoElement { } `; } - static get Listeners() { - return { - 'contextmenu': 'preventDefault', - 'pointerdown': 'onPointerdown', - 'touchstart': ['preventDefault', { passive: false }], - 'touchmove': ['preventDefault', { passive: false }], - }; - } constructor(props) { super(props); } - preventDefault(event) { - event.preventDefault(); - } - onPointerdown(event) { - this.setPointerCapture(event.pointerId); - this.addEventListener('pointerup', this.onPointerup); - this.addEventListener('pointermove', this.onPointermove); - this.addEventListener('pointercancel', this.onPointerup); - } - onPointermove(event) { } - onPointerup(event) { - this.releasePointerCapture(event.pointerId); - this.removeEventListener('pointerup', this.onPointerup); - this.removeEventListener('pointermove', this.onPointermove); - this.removeEventListener('pointercancel', this.onPointerup); - } ready() { this.changed(); } diff --git a/WebSites/controller/build/TabBase.js.map b/WebSites/controller/build/TabBase.js.map index f1ecb418..2025b3a4 100644 --- a/WebSites/controller/build/TabBase.js.map +++ b/WebSites/controller/build/TabBase.js.map @@ -1 +1 @@ -{"version":3,"file":"TabBase.js","sourceRoot":"","sources":["../src/TabBase.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAkB,QAAQ,EAAE,gBAAgB,EAAsB,MAAM,QAAQ,CAAC;AAEjH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQ9C,IAAM,OAAO,GAAb,MAAM,OAAQ,SAAQ,SAAS;IAClC,MAAM,KAAK,KAAK;QACZ,OAAO,SAAS,CAAA;;;;;;;;;;SAUf,CAAC;IACN,CAAC;IAQD,MAAM,KAAK,SAAS;QAChB,OAAO;YACH,aAAa,EAAE,gBAAgB;YAC/B,aAAa,EAAE,eAAe;YAC9B,YAAY,EAAE,CAAC,gBAAgB,EAAE,EAAC,OAAO,EAAE,KAAK,EAAC,CAAuB;YACxE,WAAW,EAAE,CAAC,gBAAgB,EAAE,EAAC,OAAO,EAAE,KAAK,EAAC,CAAuB;SAC1E,CAAC;IACN,CAAC;IAED,YAAY,KAAmB;QAC3B,KAAK,CAAC,KAAK,CAAC,CAAC;IACjB,CAAC;IAED,cAAc,CAAC,KAAY;QACvB,KAAK,CAAC,cAAc,EAAE,CAAC;IAC3B,CAAC;IACD,aAAa,CAAC,KAAmB;QAC7B,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACxC,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACrD,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QACzD,IAAI,CAAC,gBAAgB,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7D,CAAC;IACD,aAAa,CAAC,KAAmB,IAAG,CAAC;IACrC,WAAW,CAAC,KAAmB;QAC3B,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACxD,IAAI,CAAC,mBAAmB,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5D,IAAI,CAAC,mBAAmB,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAChE,CAAC;IAED,KAAK;QACD,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAED,qBAAqB;QACjB,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAED,OAAO;QACH,IAAI,CAAC,MAAM,CAAC;YACR,EAAE,CAAC,SAAS,CAAC;SAChB,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAhDW;IADP,QAAQ,EAAE;2CAC6B;AAGhC;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,cAAc,EAAC,CAAC;+CACF;AAnB9B,OAAO;IADnB,QAAQ;GACI,OAAO,CAgEnB;;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,UAAS,IAAkB;IAC9C,OAAO,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC,CAAC","sourcesContent":["import { IoElement, h2, Register, IoElementProps, Property, ReactiveProperty, ListenerDefinition } from 'io-gui';\nimport { SpacetimeController } from './SpacetimeController.js';\nimport { SimulatorState } from './SimulatorState.js';\n\nexport type TabBaseProps = IoElementProps & {\n controller: SpacetimeController;\n simulatorState: SimulatorState;\n};\n\n@Register\nexport class TabBase extends IoElement {\n static get Style() {\n return /* css */`\n :host {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: flex-start;\n padding: 2em;\n overflow-y: auto;\n }\n `;\n }\n\n @Property()\n declare controller: SpacetimeController;\n\n @ReactiveProperty({type: SimulatorState})\n declare simulatorState: SimulatorState;\n\n static get Listeners() {\n return {\n 'contextmenu': 'preventDefault',\n 'pointerdown': 'onPointerdown',\n 'touchstart': ['preventDefault', {passive: false}] as ListenerDefinition,\n 'touchmove': ['preventDefault', {passive: false}] as ListenerDefinition,\n };\n }\n\n constructor(props: TabBaseProps) {\n super(props);\n }\n\n preventDefault(event: Event) {\n event.preventDefault();\n }\n onPointerdown(event: PointerEvent) {\n this.setPointerCapture(event.pointerId);\n this.addEventListener('pointerup', this.onPointerup);\n this.addEventListener('pointermove', this.onPointermove);\n this.addEventListener('pointercancel', this.onPointerup);\n }\n onPointermove(event: PointerEvent) {}\n onPointerup(event: PointerEvent) {\n this.releasePointerCapture(event.pointerId);\n this.removeEventListener('pointerup', this.onPointerup);\n this.removeEventListener('pointermove', this.onPointermove);\n this.removeEventListener('pointercancel', this.onPointerup);\n }\n\n ready() {\n this.changed();\n }\n\n simulatorStateMutated() {\n this.changed();\n }\n\n changed() {\n this.render([\n h2('TabBase'),\n ]);\n }\n}\n\nexport const tabBase = function(arg0: TabBaseProps) {\n return TabBase.vConstructor(arg0);\n};"]} \ No newline at end of file +{"version":3,"file":"TabBase.js","sourceRoot":"","sources":["../src/TabBase.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,QAAQ,EAAkB,QAAQ,EAAE,gBAAgB,EAAsB,MAAM,QAAQ,CAAC;AAEjH,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAQ9C,IAAM,OAAO,GAAb,MAAM,OAAQ,SAAQ,SAAS;IAClC,MAAM,KAAK,KAAK;QACZ,OAAO,SAAS,CAAA;;;;;;;;;;SAUf,CAAC;IACN,CAAC;IAQD,YAAY,KAAmB;QAC3B,KAAK,CAAC,KAAK,CAAC,CAAC;IACjB,CAAC;IAED,KAAK;QACD,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAED,qBAAqB;QACjB,IAAI,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;IAED,OAAO;QACH,IAAI,CAAC,MAAM,CAAC;YACR,EAAE,CAAC,SAAS,CAAC;SAChB,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAtBW;IADP,QAAQ,EAAE;2CAC6B;AAGhC;IADP,gBAAgB,CAAC,EAAC,IAAI,EAAE,cAAc,EAAC,CAAC;+CACF;AAnB9B,OAAO;IADnB,QAAQ;GACI,OAAO,CAsCnB;;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,UAAS,IAAkB;IAC9C,OAAO,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC,CAAC","sourcesContent":["import { IoElement, h2, Register, IoElementProps, Property, ReactiveProperty, ListenerDefinition } from 'io-gui';\nimport { SpacetimeController } from './SpacetimeController.js';\nimport { SimulatorState } from './SimulatorState.js';\n\nexport type TabBaseProps = IoElementProps & {\n controller: SpacetimeController;\n simulatorState: SimulatorState;\n};\n\n@Register\nexport class TabBase extends IoElement {\n static get Style() {\n return /* css */`\n :host {\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: flex-start;\n padding: 2em;\n overflow-y: auto;\n }\n `;\n }\n\n @Property()\n declare controller: SpacetimeController;\n\n @ReactiveProperty({type: SimulatorState})\n declare simulatorState: SimulatorState;\n\n constructor(props: TabBaseProps) {\n super(props);\n }\n\n ready() {\n this.changed();\n }\n\n simulatorStateMutated() {\n this.changed();\n }\n\n changed() {\n this.render([\n h2('TabBase'),\n ]);\n }\n}\n\nexport const tabBase = function(arg0: TabBaseProps) {\n return TabBase.vConstructor(arg0);\n};"]} \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.d.ts b/WebSites/controller/build/TabNavigate.d.ts deleted file mode 100644 index a0577834..00000000 --- a/WebSites/controller/build/TabNavigate.d.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { TabBase, TabBaseProps } from './TabBase.js'; -export declare class TabNavigate extends TabBase { - static get Style(): string; - onPointermove(event: PointerEvent): void; - changed(): void; - onSimulatorChange(event: CustomEvent): void; -} -export declare const tabNavigate: (arg0: TabBaseProps) => import("io-gui").VDOMElement; -//# sourceMappingURL=TabNavigate.d.ts.map \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.d.ts.map b/WebSites/controller/build/TabNavigate.d.ts.map deleted file mode 100644 index 49df209a..00000000 --- a/WebSites/controller/build/TabNavigate.d.ts.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"TabNavigate.d.ts","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,qBACa,WAAY,SAAQ,OAAO;IACpC,MAAM,KAAK,KAAK,WAaf;IAED,aAAa,CAAC,KAAK,EAAE,YAAY;IAUjC,OAAO;IAwCP,iBAAiB,CAAC,KAAK,EAAE,WAAW;CAMvC;AAED,eAAO,MAAM,WAAW,GAAY,MAAM,YAAY,iCAErD,CAAC"} \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.js b/WebSites/controller/build/TabNavigate.js deleted file mode 100644 index 7aaefea0..00000000 --- a/WebSites/controller/build/TabNavigate.js +++ /dev/null @@ -1,81 +0,0 @@ -var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { - var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; - if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); - else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; - return c > 3 && r && Object.defineProperty(target, key, r), r; -}; -import { h2, Register, IoOptionSelect, MenuOption, br } from 'io-gui'; -import { TabBase } from './TabBase.js'; -let TabNavigate = class TabNavigate extends TabBase { - static get Style() { - return /* css */ ` - :host { - justify-content: center; - } - :host > h2 { - pointer-events: none; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - } - `; - } - onPointermove(event) { - super.onPointermove(event); - if (event.movementX || event.movementY) { - this.controller.sendPanEvent(event.movementX * 0.03, event.movementY * 0.03); - } - } - changed() { - // Force rerender when simulator list changes by reading controller.simulatorRosterTick - void this.controller.simulatorRosterTick; - const simulators = Array.from(this.controller.currentSimulators?.values() || []); - const hasSimulators = simulators.length > 0; - const headerRow = hasSimulators - ? [ - h2('Simulator:'), - br(), - IoOptionSelect.vConstructor({ - value: this.controller.currentSimulatorId || '', - option: new MenuOption({ - options: simulators - .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId })) - .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' })) - }), - '@value-input': (e) => this.onSimulatorChange(e) - }) - ] - : [ - h2('Simulator:'), - br(), - IoOptionSelect.vConstructor({ - value: '(none)', - option: new MenuOption({ - options: [{ id: '(none)', value: '(none)' }], - disabled: true, - }), - // no handler; locked selector - 'disabled': true - }) - ]; - this.render([ - ...headerRow, - h2('DRAG to pan • PINCH to zoom'), - ]); - } - onSimulatorChange(event) { - const newId = event.detail?.value; - if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') { - this.controller.setCurrentSimulator?.(newId); - } - } -}; -TabNavigate = __decorate([ - Register -], TabNavigate); -export { TabNavigate }; -export const tabNavigate = function (arg0) { - return TabNavigate.vConstructor(arg0); -}; -//# sourceMappingURL=TabNavigate.js.map \ No newline at end of file diff --git a/WebSites/controller/build/TabNavigate.js.map b/WebSites/controller/build/TabNavigate.js.map deleted file mode 100644 index df0d5887..00000000 --- a/WebSites/controller/build/TabNavigate.js.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"file":"TabNavigate.js","sourceRoot":"","sources":["../src/TabNavigate.ts"],"names":[],"mappings":"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","sourcesContent":["import { h2, Register, IoOptionSelect, MenuOption, IoOptionSelectProps, br } from 'io-gui';\nimport { TabBase, TabBaseProps } from './TabBase.js';\n\n@Register\nexport class TabNavigate extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n justify-content: center;\n }\n :host > h2 {\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n }\n `;\n }\n\n onPointermove(event: PointerEvent) {\n super.onPointermove(event);\n if (event.movementX || event.movementY) {\n this.controller.sendPanEvent(\n event.movementX * 0.03,\n event.movementY * 0.03\n );\n }\n }\n\n changed() {\n // Force rerender when simulator list changes by reading controller.simulatorRosterTick\n void (this.controller as any).simulatorRosterTick;\n const simulators = Array.from(this.controller.currentSimulators?.values() || []);\n const hasSimulators = simulators.length > 0;\n\n const headerRow = hasSimulators\n ? [\n h2('Simulator:'),\n br(),\n IoOptionSelect.vConstructor({\n value: this.controller.currentSimulatorId || '',\n option: new MenuOption({\n options: simulators\n .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId }))\n .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' }))\n }),\n '@value-input': (e: CustomEvent) => this.onSimulatorChange(e)\n } as IoOptionSelectProps)\n ]\n : [\n h2('Simulator:'),\n br(),\n IoOptionSelect.vConstructor({\n value: '(none)',\n option: new MenuOption({\n options: [{ id: '(none)', value: '(none)' }],\n disabled: true,\n }),\n // no handler; locked selector\n 'disabled': true\n } as IoOptionSelectProps)\n ];\n\n this.render([\n ...headerRow,\n h2('DRAG to pan • PINCH to zoom'),\n ]);\n }\n\n onSimulatorChange(event: CustomEvent) {\n const newId = event.detail?.value;\n if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') {\n (this.controller as any).setCurrentSimulator?.(newId);\n }\n }\n}\n\nexport const tabNavigate = function(arg0: TabBaseProps) {\n return TabNavigate.vConstructor(arg0);\n};"]} \ No newline at end of file diff --git a/WebSites/controller/build/TabSelect.d.ts b/WebSites/controller/build/TabSelect.d.ts index f4e6a72d..84cf72e2 100644 --- a/WebSites/controller/build/TabSelect.d.ts +++ b/WebSites/controller/build/TabSelect.d.ts @@ -1,8 +1,16 @@ +import { ListenerDefinition } from 'io-gui'; import { TabBase, TabBaseProps } from './TabBase.js'; export declare class TabSelect extends TabBase { static get Style(): string; startX: number; startY: number; + static get Listeners(): { + pointerdown: string; + touchstart: ListenerDefinition; + touchmove: ListenerDefinition; + wheel: string; + }; + preventDefault(event: Event): void; onPointerdown(event: PointerEvent): void; onPointerup(event: PointerEvent): void; changed(): void; diff --git a/WebSites/controller/build/TabSelect.d.ts.map b/WebSites/controller/build/TabSelect.d.ts.map index 6b5743f8..3cd49a4e 100644 --- a/WebSites/controller/build/TabSelect.d.ts.map +++ b/WebSites/controller/build/TabSelect.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"TabSelect.d.ts","sourceRoot":"","sources":["../src/TabSelect.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKrD,qBACa,SAAU,SAAQ,OAAO;IAClC,MAAM,KAAK,KAAK,WAWf;IAEO,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IAEvB,aAAa,CAAC,KAAK,EAAE,YAAY;IAKjC,WAAW,CAAC,KAAK,EAAE,YAAY;IAyB/B,OAAO;CA0BV;AAED,eAAO,MAAM,SAAS,GAAY,MAAM,YAAY,iCAEnD,CAAC"} \ No newline at end of file +{"version":3,"file":"TabSelect.d.ts","sourceRoot":"","sources":["../src/TabSelect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAiC,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAKrD,qBACa,SAAU,SAAQ,OAAO;IAClC,MAAM,KAAK,KAAK,WAWf;IAEO,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IAEvB,MAAM,KAAK,SAAS;;oBAG0C,kBAAkB;mBACnB,kBAAkB;;MAG9E;IAED,cAAc,CAAC,KAAK,EAAE,KAAK;IAG3B,aAAa,CAAC,KAAK,EAAE,YAAY;IAOjC,WAAW,CAAC,KAAK,EAAE,YAAY;IA0B/B,OAAO;CA0BV;AAED,eAAO,MAAM,SAAS,GAAY,MAAM,YAAY,iCAEnD,CAAC"} \ No newline at end of file diff --git a/WebSites/controller/build/TabSelect.js b/WebSites/controller/build/TabSelect.js index 74c35a59..3415702b 100644 --- a/WebSites/controller/build/TabSelect.js +++ b/WebSites/controller/build/TabSelect.js @@ -21,13 +21,28 @@ let TabSelect = class TabSelect extends TabBase { } `; } + static get Listeners() { + return { + 'pointerdown': 'onPointerdown', + 'touchstart': ['preventDefault', { passive: false }], + 'touchmove': ['preventDefault', { passive: false }], + 'wheel': 'onScroll', + }; + } + preventDefault(event) { + event.preventDefault(); + } onPointerdown(event) { - super.onPointerdown(event); + this.setPointerCapture(event.pointerId); + this.addEventListener('pointerup', this.onPointerup); + this.addEventListener('pointercancel', this.onPointerup); this.startX = event.clientX; this.startY = event.clientY; } onPointerup(event) { - super.onPointerup(event); + this.releasePointerCapture(event.pointerId); + this.removeEventListener('pointerup', this.onPointerup); + this.removeEventListener('pointercancel', this.onPointerup); const dx = event.clientX - this.startX; const dy = event.clientY - this.startY; const distance = Math.sqrt(dx * dx + dy * dy); diff --git a/WebSites/controller/build/TabSelect.js.map b/WebSites/controller/build/TabSelect.js.map index 6fe504a5..835b8e9e 100644 --- a/WebSites/controller/build/TabSelect.js.map +++ b/WebSites/controller/build/TabSelect.js.map @@ -1 +1 @@ -{"version":3,"file":"TabSelect.js","sourceRoot":"","sources":["../src/TabSelect.ts"],"names":[],"mappings":"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","sourcesContent":["import { h2, p, Register, div, img, h4 } from 'io-gui';\nimport { TabBase, TabBaseProps } from './TabBase.js';\nimport { contentStore } from './services/ContentStore.js';\n\nconst GESTURE_THRESHOLD = 20;\n\n@Register\nexport class TabSelect extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n text-align: justify;\n }\n :host .cover-image {\n pointer-events: none;\n float: right;\n margin: 4.2em 0 0.5em 1em;\n }\n `;\n }\n\n declare startX: number;\n declare startY: number;\n\n onPointerdown(event: PointerEvent) {\n super.onPointerdown(event);\n this.startX = event.clientX;\n this.startY = event.clientY;\n }\n onPointerup(event: PointerEvent) {\n super.onPointerup(event);\n\n const dx = event.clientX - this.startX;\n const dy = event.clientY - this.startY;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n let gesture = 'tap';\n if (distance > GESTURE_THRESHOLD) {\n if (Math.abs(dx) > Math.abs(dy)) {\n gesture = dx > 0 ? 'east' : 'west';\n } else {\n gesture = dy > 0 ? 'south' : 'north';\n }\n }\n\n switch (gesture) {\n case 'tap': this.controller.sendSelectEvent('tap'); break;\n case 'north': this.controller.sendSelectEvent('north'); break;\n case 'south': this.controller.sendSelectEvent('south'); break;\n case 'east': this.controller.sendSelectEvent('east'); break;\n case 'west': this.controller.sendSelectEvent('west'); break;\n }\n }\n\n changed() {\n let description = 'No description available';\n const selected = this.simulatorState?.selectedItem;\n if (selected?.description) {\n description = selected.description\n .split(/\\n+/) // Split on any number of newlines\n .filter((line: string) => line.trim().length > 0) // Remove empty lines\n .map((line: string) => `

${line.trim()}

`) // Create paragraph for each line\n .join('');\n }\n\n // Resolve cover base from content store (built-in StreamingAssets path or external)\n const shared: any = (this.controller as any).simulatorState || {};\n const contentKey = (shared as any).contentKey || (shared as any).contentIndexUrl;\n const contentRec = contentStore.getContent(contentKey);\n const assetsBase = (shared as any).assetsBaseUrl || '../SpaceCraft/StreamingAssets/Content/';\n\n this.render([\n h2('TAP or SWIPE to select items'),\n selected ? div([\n img({src: `${assetsBase}collections/scifi/items/${selected.id}/cover.jpg`, alt: `Cover for ${selected.title}`, class: 'cover-image'}),\n h4(selected.title || 'Untitled'),\n div({class: 'description', innerHTML: description}),\n ]) : p('No item selected'),\n ]);\n }\n}\n\nexport const tabSelect = function(arg0: TabBaseProps) {\n return TabSelect.vConstructor(arg0);\n};"]} \ No newline at end of file +{"version":3,"file":"TabSelect.js","sourceRoot":"","sources":["../src/TabSelect.ts"],"names":[],"mappings":"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","sourcesContent":["import { h2, p, Register, div, img, h4, ListenerDefinition } from 'io-gui';\nimport { TabBase, TabBaseProps } from './TabBase.js';\nimport { contentStore } from './services/ContentStore.js';\n\nconst GESTURE_THRESHOLD = 20;\n\n@Register\nexport class TabSelect extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n text-align: justify;\n }\n :host .cover-image {\n pointer-events: none;\n float: right;\n margin: 4.2em 0 0.5em 1em;\n }\n `;\n }\n\n declare startX: number;\n declare startY: number;\n\n static get Listeners() {\n return {\n 'pointerdown': 'onPointerdown',\n 'touchstart': ['preventDefault', {passive: false}] as ListenerDefinition,\n 'touchmove': ['preventDefault', {passive: false}] as ListenerDefinition,\n 'wheel': 'onScroll',\n };\n }\n\n preventDefault(event: Event) {\n event.preventDefault();\n }\n onPointerdown(event: PointerEvent) {\n this.setPointerCapture(event.pointerId);\n this.addEventListener('pointerup', this.onPointerup);\n this.addEventListener('pointercancel', this.onPointerup);\n this.startX = event.clientX;\n this.startY = event.clientY;\n }\n onPointerup(event: PointerEvent) {\n this.releasePointerCapture(event.pointerId);\n this.removeEventListener('pointerup', this.onPointerup);\n this.removeEventListener('pointercancel', this.onPointerup);\n const dx = event.clientX - this.startX;\n const dy = event.clientY - this.startY;\n const distance = Math.sqrt(dx * dx + dy * dy);\n\n let gesture = 'tap';\n if (distance > GESTURE_THRESHOLD) {\n if (Math.abs(dx) > Math.abs(dy)) {\n gesture = dx > 0 ? 'east' : 'west';\n } else {\n gesture = dy > 0 ? 'south' : 'north';\n }\n }\n\n switch (gesture) {\n case 'tap': this.controller.sendSelectEvent('tap'); break;\n case 'north': this.controller.sendSelectEvent('north'); break;\n case 'south': this.controller.sendSelectEvent('south'); break;\n case 'east': this.controller.sendSelectEvent('east'); break;\n case 'west': this.controller.sendSelectEvent('west'); break;\n }\n }\n\n changed() {\n let description = 'No description available';\n const selected = this.simulatorState?.selectedItem;\n if (selected?.description) {\n description = selected.description\n .split(/\\n+/) // Split on any number of newlines\n .filter((line: string) => line.trim().length > 0) // Remove empty lines\n .map((line: string) => `

${line.trim()}

`) // Create paragraph for each line\n .join('');\n }\n\n // Resolve cover base from content store (built-in StreamingAssets path or external)\n const shared: any = (this.controller as any).simulatorState || {};\n const contentKey = (shared as any).contentKey || (shared as any).contentIndexUrl;\n const contentRec = contentStore.getContent(contentKey);\n const assetsBase = (shared as any).assetsBaseUrl || '../SpaceCraft/StreamingAssets/Content/';\n\n this.render([\n h2('TAP or SWIPE to select items'),\n selected ? div([\n img({src: `${assetsBase}collections/scifi/items/${selected.id}/cover.jpg`, alt: `Cover for ${selected.title}`, class: 'cover-image'}),\n h4(selected.title || 'Untitled'),\n div({class: 'description', innerHTML: description}),\n ]) : p('No item selected'),\n ]);\n }\n}\n\nexport const tabSelect = function(arg0: TabBaseProps) {\n return TabSelect.vConstructor(arg0);\n};"]} \ No newline at end of file diff --git a/WebSites/controller/build/TabView.d.ts b/WebSites/controller/build/TabView.d.ts index 66189b71..984ff5df 100644 --- a/WebSites/controller/build/TabView.d.ts +++ b/WebSites/controller/build/TabView.d.ts @@ -1,7 +1,18 @@ +import { ListenerDefinition } from 'io-gui'; import { TabBase, TabBaseProps } from './TabBase.js'; export declare class TabView extends TabBase { static get Style(): string; + static get Listeners(): { + pointerdown: string; + touchstart: ListenerDefinition; + touchmove: ListenerDefinition; + wheel: string; + }; + preventDefault(event: Event): void; + onPointerdown(event: PointerEvent): void; onPointermove(event: PointerEvent): void; + onPointerup(event: PointerEvent): void; + onWheel(event: WheelEvent): void; onViewModeChange(event: CustomEvent): void; changed(): void; } diff --git a/WebSites/controller/build/TabView.d.ts.map b/WebSites/controller/build/TabView.d.ts.map index 8886efd6..33bcb12b 100644 --- a/WebSites/controller/build/TabView.d.ts.map +++ b/WebSites/controller/build/TabView.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"TabView.d.ts","sourceRoot":"","sources":["../src/TabView.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,qBACa,OAAQ,SAAQ,OAAO;IAChC,MAAM,KAAK,KAAK,WAuBf;IAED,aAAa,CAAC,KAAK,EAAE,YAAY;IAUjC,gBAAgB,CAAC,KAAK,EAAE,WAAW;IAUnC,OAAO;CAcV;AAED,eAAO,MAAM,OAAO,GAAY,MAAM,YAAY,iCAEjD,CAAC"} \ No newline at end of file +{"version":3,"file":"TabView.d.ts","sourceRoot":"","sources":["../src/TabView.ts"],"names":[],"mappings":"AAAA,OAAO,EAAsE,kBAAkB,EAAE,MAAM,QAAQ,CAAC;AAEhH,OAAO,EAAE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAErD,qBACa,OAAQ,SAAQ,OAAO;IAChC,MAAM,KAAK,KAAK,WAuBf;IAED,MAAM,KAAK,SAAS;;oBAG0C,kBAAkB;mBACnB,kBAAkB;;MAG9E;IAED,cAAc,CAAC,KAAK,EAAE,KAAK;IAG3B,aAAa,CAAC,KAAK,EAAE,YAAY;IAMjC,aAAa,CAAC,KAAK,EAAE,YAAY;IAQjC,WAAW,CAAC,KAAK,EAAE,YAAY;IAO/B,OAAO,CAAC,KAAK,EAAE,UAAU;IAIzB,gBAAgB,CAAC,KAAK,EAAE,WAAW;IAUnC,OAAO;CAcV;AAED,eAAO,MAAM,OAAO,GAAY,MAAM,YAAY,iCAEjD,CAAC"} \ No newline at end of file diff --git a/WebSites/controller/build/TabView.js b/WebSites/controller/build/TabView.js index 762362f8..7415924e 100644 --- a/WebSites/controller/build/TabView.js +++ b/WebSites/controller/build/TabView.js @@ -32,12 +32,37 @@ let TabView = class TabView extends TabBase { } `; } + static get Listeners() { + return { + 'pointerdown': 'onPointerdown', + 'touchstart': ['preventDefault', { passive: false }], + 'touchmove': ['preventDefault', { passive: false }], + 'wheel': 'onWheel', + }; + } + preventDefault(event) { + event.preventDefault(); + } + onPointerdown(event) { + this.setPointerCapture(event.pointerId); + this.addEventListener('pointerup', this.onPointerup); + this.addEventListener('pointermove', this.onPointermove); + this.addEventListener('pointercancel', this.onPointerup); + } onPointermove(event) { - super.onPointermove(event); if (event.movementX || event.movementY) { this.controller.sendPanEvent(event.movementX * 0.03, event.movementY * 0.03); } } + onPointerup(event) { + this.releasePointerCapture(event.pointerId); + this.removeEventListener('pointerup', this.onPointerup); + this.removeEventListener('pointermove', this.onPointermove); + this.removeEventListener('pointercancel', this.onPointerup); + } + onWheel(event) { + this.controller.sendZoomEvent(event.deltaY * 0.01); + } onViewModeChange(event) { const newMode = event.detail?.value; if (!newMode || !this.controller?.clientChannel) diff --git a/WebSites/controller/build/TabView.js.map b/WebSites/controller/build/TabView.js.map index e5b1d71e..e83f0b0e 100644 --- a/WebSites/controller/build/TabView.js.map +++ b/WebSites/controller/build/TabView.js.map @@ -1 +1 @@ -{"version":3,"file":"TabView.js","sourceRoot":"","sources":["../src/TabView.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,cAAc,EAAE,UAAU,EAAuB,MAAM,QAAQ,CAAC;AAC5F,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAG9C,IAAM,OAAO,GAAb,MAAM,OAAQ,SAAQ,OAAO;IAChC,MAAM,KAAK,KAAK;QACZ,OAAO,SAAS,CAAA;;;;;;;;;;;;;;;;;;;;;SAqBf,CAAC;IACN,CAAC;IAED,aAAa,CAAC,KAAmB;QAC7B,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC;QAC3B,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,CAAC,YAAY,CACxB,KAAK,CAAC,SAAS,GAAG,IAAI,EACtB,KAAK,CAAC,SAAS,GAAG,IAAI,CACzB,CAAC;QACN,CAAC;IACL,CAAC;IAED,gBAAgB,CAAC,KAAkB;QAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC;QACpC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa;YAAE,OAAO;QACxD,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC;YAC/B,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,aAAa;YACpB,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,CAAC,UAAU,CAAC,kBAAkB,EAAE;SACpF,CAAC,CAAC,KAAK,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,GAAG,CAAC,CAAC,CAAC;IACxF,CAAC;IAED,OAAO;QACH,MAAM,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,QAAQ,IAAI,iBAAiB,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC;YACR,GAAG,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE;gBAC5B,EAAE,CAAC,YAAY,CAAC;gBAChB,cAAc,CAAC,YAAY,CAAC;oBACxB,KAAK,EAAE,EAAE;oBACT,MAAM,EAAE,IAAI,UAAU,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC;oBACtD,cAAc,EAAE,CAAC,CAAc,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;iBACxC,CAAC;aAC5B,CAAC;YACF,EAAE,CAAC,8BAA8B,CAAC;SACrC,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AA5DY,OAAO;IADnB,QAAQ;GACI,OAAO,CA4DnB;;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,UAAS,IAAkB;IAC9C,OAAO,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC,CAAC","sourcesContent":["import { h2, div, Register, IoOptionSelect, MenuOption, IoOptionSelectProps } from 'io-gui';\nimport { VIEW_MODE_OPTIONS, DEFAULT_VIEW_MODE } from './types/ViewMode.js';\nimport { TabBase, TabBaseProps } from './TabBase.js';\n\n@Register\nexport class TabView extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n }\n :host > h2 {\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n margin: 0 0 6px 0;\n }\n :host .view-controls {\n align-self: flex-start;\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 2px 8px 4px 8px;\n }\n `;\n }\n\n onPointermove(event: PointerEvent) {\n super.onPointermove(event);\n if (event.movementX || event.movementY) {\n this.controller.sendPanEvent(\n event.movementX * 0.03,\n event.movementY * 0.03\n );\n }\n }\n\n onViewModeChange(event: CustomEvent) {\n const newMode = event.detail?.value;\n if (!newMode || !this.controller?.clientChannel) return;\n this.controller.clientChannel.send({\n type: 'broadcast',\n event: 'setViewMode',\n payload: { mode: newMode, targetSimulatorId: this.controller.currentSimulatorId }\n }).catch((err: any) => console.error('[Controller] setViewMode send failed:', err));\n }\n\n changed() {\n const vm = this.simulatorState?.viewMode || DEFAULT_VIEW_MODE;\n this.render([\n div({ class: 'view-controls' }, [\n h2('View Mode:'),\n IoOptionSelect.vConstructor({\n value: vm,\n option: new MenuOption({ options: VIEW_MODE_OPTIONS }),\n '@value-input': (e: CustomEvent) => this.onViewModeChange(e)\n } as IoOptionSelectProps),\n ]),\n h2('DRAG to pan • SCROLL to zoom'),\n ]);\n }\n}\n\nexport const tabView = function(arg0: TabBaseProps) {\n return TabView.vConstructor(arg0);\n};\n"]} \ No newline at end of file +{"version":3,"file":"TabView.js","sourceRoot":"","sources":["../src/TabView.ts"],"names":[],"mappings":";;;;;;AAAA,OAAO,EAAE,EAAE,EAAE,GAAG,EAAE,QAAQ,EAAE,cAAc,EAAE,UAAU,EAA2C,MAAM,QAAQ,CAAC;AAChH,OAAO,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AAC3E,OAAO,EAAE,OAAO,EAAgB,MAAM,cAAc,CAAC;AAG9C,IAAM,OAAO,GAAb,MAAM,OAAQ,SAAQ,OAAO;IAChC,MAAM,KAAK,KAAK;QACZ,OAAO,SAAS,CAAA;;;;;;;;;;;;;;;;;;;;;SAqBf,CAAC;IACN,CAAC;IAED,MAAM,KAAK,SAAS;QAChB,OAAO;YACH,aAAa,EAAE,eAAe;YAC9B,YAAY,EAAE,CAAC,gBAAgB,EAAE,EAAC,OAAO,EAAE,KAAK,EAAC,CAAuB;YACxE,WAAW,EAAE,CAAC,gBAAgB,EAAE,EAAC,OAAO,EAAE,KAAK,EAAC,CAAuB;YACvE,OAAO,EAAE,SAAS;SACrB,CAAC;IACN,CAAC;IAED,cAAc,CAAC,KAAY;QACvB,KAAK,CAAC,cAAc,EAAE,CAAC;IAC3B,CAAC;IACD,aAAa,CAAC,KAAmB;QAC7B,IAAI,CAAC,iBAAiB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QACxC,IAAI,CAAC,gBAAgB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACrD,IAAI,CAAC,gBAAgB,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QACzD,IAAI,CAAC,gBAAgB,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAC7D,CAAC;IACD,aAAa,CAAC,KAAmB;QAC7B,IAAI,KAAK,CAAC,SAAS,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACrC,IAAI,CAAC,UAAU,CAAC,YAAY,CACxB,KAAK,CAAC,SAAS,GAAG,IAAI,EACtB,KAAK,CAAC,SAAS,GAAG,IAAI,CACzB,CAAC;QACN,CAAC;IACL,CAAC;IACD,WAAW,CAAC,KAAmB;QAC3B,IAAI,CAAC,qBAAqB,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC;QAC5C,IAAI,CAAC,mBAAmB,CAAC,WAAW,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;QACxD,IAAI,CAAC,mBAAmB,CAAC,aAAa,EAAE,IAAI,CAAC,aAAa,CAAC,CAAC;QAC5D,IAAI,CAAC,mBAAmB,CAAC,eAAe,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;IAChE,CAAC;IAED,OAAO,CAAC,KAAiB;QACvB,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,CAAC;IAED,gBAAgB,CAAC,KAAkB;QAC/B,MAAM,OAAO,GAAG,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC;QACpC,IAAI,CAAC,OAAO,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,aAAa;YAAE,OAAO;QACxD,IAAI,CAAC,UAAU,CAAC,aAAa,CAAC,IAAI,CAAC;YAC/B,IAAI,EAAE,WAAW;YACjB,KAAK,EAAE,aAAa;YACpB,OAAO,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,iBAAiB,EAAE,IAAI,CAAC,UAAU,CAAC,kBAAkB,EAAE;SACpF,CAAC,CAAC,KAAK,CAAC,CAAC,GAAQ,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,GAAG,CAAC,CAAC,CAAC;IACxF,CAAC;IAED,OAAO;QACH,MAAM,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,QAAQ,IAAI,iBAAiB,CAAC;QAC9D,IAAI,CAAC,MAAM,CAAC;YACR,GAAG,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,EAAE;gBAC5B,EAAE,CAAC,YAAY,CAAC;gBAChB,cAAc,CAAC,YAAY,CAAC;oBACxB,KAAK,EAAE,EAAE;oBACT,MAAM,EAAE,IAAI,UAAU,CAAC,EAAE,OAAO,EAAE,iBAAiB,EAAE,CAAC;oBACtD,cAAc,EAAE,CAAC,CAAc,EAAE,EAAE,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC;iBACxC,CAAC;aAC5B,CAAC;YACF,EAAE,CAAC,8BAA8B,CAAC;SACrC,CAAC,CAAC;IACP,CAAC;CACJ,CAAA;AAvFY,OAAO;IADnB,QAAQ;GACI,OAAO,CAuFnB;;AAED,MAAM,CAAC,MAAM,OAAO,GAAG,UAAS,IAAkB;IAC9C,OAAO,OAAO,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;AACtC,CAAC,CAAC","sourcesContent":["import { h2, div, Register, IoOptionSelect, MenuOption, IoOptionSelectProps, ListenerDefinition } from 'io-gui';\nimport { VIEW_MODE_OPTIONS, DEFAULT_VIEW_MODE } from './types/ViewMode.js';\nimport { TabBase, TabBaseProps } from './TabBase.js';\n\n@Register\nexport class TabView extends TabBase {\n static get Style() {\n return /* css */`\n :host {\n display: flex;\n flex-direction: column;\n align-items: stretch;\n }\n :host > h2 {\n pointer-events: none;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n margin: 0 0 6px 0;\n }\n :host .view-controls {\n align-self: flex-start;\n display: flex;\n align-items: center;\n gap: 8px;\n margin: 2px 8px 4px 8px;\n }\n `;\n }\n\n static get Listeners() {\n return {\n 'pointerdown': 'onPointerdown',\n 'touchstart': ['preventDefault', {passive: false}] as ListenerDefinition,\n 'touchmove': ['preventDefault', {passive: false}] as ListenerDefinition,\n 'wheel': 'onWheel',\n };\n }\n\n preventDefault(event: Event) {\n event.preventDefault();\n }\n onPointerdown(event: PointerEvent) {\n this.setPointerCapture(event.pointerId);\n this.addEventListener('pointerup', this.onPointerup);\n this.addEventListener('pointermove', this.onPointermove);\n this.addEventListener('pointercancel', this.onPointerup);\n }\n onPointermove(event: PointerEvent) {\n if (event.movementX || event.movementY) {\n this.controller.sendPanEvent(\n event.movementX * 0.03,\n event.movementY * 0.03\n );\n }\n }\n onPointerup(event: PointerEvent) {\n this.releasePointerCapture(event.pointerId);\n this.removeEventListener('pointerup', this.onPointerup);\n this.removeEventListener('pointermove', this.onPointermove);\n this.removeEventListener('pointercancel', this.onPointerup);\n }\n\n onWheel(event: WheelEvent) {\n this.controller.sendZoomEvent(event.deltaY * 0.01);\n }\n\n onViewModeChange(event: CustomEvent) {\n const newMode = event.detail?.value;\n if (!newMode || !this.controller?.clientChannel) return;\n this.controller.clientChannel.send({\n type: 'broadcast',\n event: 'setViewMode',\n payload: { mode: newMode, targetSimulatorId: this.controller.currentSimulatorId }\n }).catch((err: any) => console.error('[Controller] setViewMode send failed:', err));\n }\n\n changed() {\n const vm = this.simulatorState?.viewMode || DEFAULT_VIEW_MODE;\n this.render([\n div({ class: 'view-controls' }, [\n h2('View Mode:'),\n IoOptionSelect.vConstructor({\n value: vm,\n option: new MenuOption({ options: VIEW_MODE_OPTIONS }),\n '@value-input': (e: CustomEvent) => this.onViewModeChange(e)\n } as IoOptionSelectProps),\n ]),\n h2('DRAG to pan • SCROLL to zoom'),\n ]);\n }\n}\n\nexport const tabView = function(arg0: TabBaseProps) {\n return TabView.vConstructor(arg0);\n};\n"]} \ No newline at end of file diff --git a/WebSites/controller/src/TabNavigate.ts b/WebSites/controller/src/TabNavigate.ts deleted file mode 100644 index cebecd50..00000000 --- a/WebSites/controller/src/TabNavigate.ts +++ /dev/null @@ -1,81 +0,0 @@ -import { h2, Register, IoOptionSelect, MenuOption, IoOptionSelectProps, br } from 'io-gui'; -import { TabBase, TabBaseProps } from './TabBase.js'; - -@Register -export class TabNavigate extends TabBase { - static get Style() { - return /* css */` - :host { - justify-content: center; - } - :host > h2 { - pointer-events: none; - user-select: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - } - `; - } - - onPointermove(event: PointerEvent) { - super.onPointermove(event); - if (event.movementX || event.movementY) { - this.controller.sendPanEvent( - event.movementX * 0.03, - event.movementY * 0.03 - ); - } - } - - changed() { - // Force rerender when simulator list changes by reading controller.simulatorRosterTick - void (this.controller as any).simulatorRosterTick; - const simulators = Array.from(this.controller.currentSimulators?.values() || []); - const hasSimulators = simulators.length > 0; - - const headerRow = hasSimulators - ? [ - h2('Simulator:'), - br(), - IoOptionSelect.vConstructor({ - value: this.controller.currentSimulatorId || '', - option: new MenuOption({ - options: simulators - .map(s => ({ id: (s.clientName || s.clientId), value: s.clientId })) - .sort((a, b) => a.id.localeCompare(b.id, undefined, { sensitivity: 'base' })) - }), - '@value-input': (e: CustomEvent) => this.onSimulatorChange(e) - } as IoOptionSelectProps) - ] - : [ - h2('Simulator:'), - br(), - IoOptionSelect.vConstructor({ - value: '(none)', - option: new MenuOption({ - options: [{ id: '(none)', value: '(none)' }], - disabled: true, - }), - // no handler; locked selector - 'disabled': true - } as IoOptionSelectProps) - ]; - - this.render([ - ...headerRow, - h2('DRAG to pan • PINCH to zoom'), - ]); - } - - onSimulatorChange(event: CustomEvent) { - const newId = event.detail?.value; - if (newId && newId !== this.controller.currentSimulatorId && newId !== '(none)') { - (this.controller as any).setCurrentSimulator?.(newId); - } - } -} - -export const tabNavigate = function(arg0: TabBaseProps) { - return TabNavigate.vConstructor(arg0); -}; \ No newline at end of file From 8f81789cf947a0002ee88961fac6cb91cc6cee61 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 02:21:46 +0200 Subject: [PATCH 21/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 5e374427..429cba24 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -259,8 +259,7 @@ jobs: buildMethod: ${{ steps.compute.outputs.buildMethod }} customParameters: -logFile Logs/Editor.log -stackTraceLogType Full runAsHostUser: true - cacheUnity: true - cacheVersioning: semantic + - name: Setup Node.js if: success() @@ -324,10 +323,18 @@ jobs: echo "[assemble] Final artifact layout:" (cd "$STAGE" && find . -maxdepth 2 -type f | sed 's#^./##') - - name: Upload build artifacts + - name: Package site.zip (flat contents) + if: success() + shell: bash + run: | + set -euo pipefail + rm -f site.zip + (cd dist && zip -r ../site.zip .) + echo "[package] Created site.zip" + + - name: Upload site.zip artifact if: success() uses: actions/upload-artifact@v4 with: - name: SpaceCraft-${{ inputs.targetPlatform }}-${{ inputs.buildProfile }} - path: | - dist/** + name: site.zip + path: site.zip From 44da23c3b9dacf6aa84bc181c43ecfaac5032ac9 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 02:47:23 +0200 Subject: [PATCH 22/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 23 +++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 429cba24..2c3f6f0d 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -251,7 +251,7 @@ jobs: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - SC_BUILD_OUTPUT: dist/SpaceCraft + SC_BUILD_OUTPUT: ${{ github.workspace }}/dist/SpaceCraft with: projectPath: ${{ steps.detect.outputs.projectPath }} targetPlatform: ${{ inputs.targetPlatform }} @@ -304,8 +304,25 @@ jobs: run: | set -euo pipefail STAGE="dist" - echo "[assemble] Using Unity output already at $STAGE/SpaceCraft" + echo "[assemble] Expecting Unity output at $STAGE/SpaceCraft" + test -d "$STAGE/SpaceCraft" || { echo "[assemble] ERROR: Unity output missing at $STAGE/SpaceCraft" >&2; exit 1; } mkdir -p "$STAGE/controller" + # If Unity output missing, try common fallback paths + if [[ ! -d "$STAGE/SpaceCraft" ]]; then + CAND1="${{ steps.detect.outputs.projectPath }}/Builds/SpaceCraft" + CAND2="WebSites/SpaceCraft" + if [[ -d "$CAND1" ]]; then + echo "[assemble] Fallback: copying from $CAND1" + mkdir -p "$STAGE/SpaceCraft" + rsync -a "$CAND1/" "$STAGE/SpaceCraft/" + elif [[ -d "$CAND2" ]]; then + echo "[assemble] Fallback: copying from $CAND2" + mkdir -p "$STAGE/SpaceCraft" + rsync -a "$CAND2/" "$STAGE/SpaceCraft/" + else + echo "[assemble] WARNING: Unity output not found in dist/SpaceCraft, $CAND1, or $CAND2" + fi + fi # Top-level site index if [[ -f WebSites/index.html ]]; then cp -f WebSites/index.html "$STAGE/index.html" @@ -336,5 +353,5 @@ jobs: if: success() uses: actions/upload-artifact@v4 with: - name: site.zip + name: site path: site.zip From 7c8895bfd8513e243e208496023a2f68500dc860 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 02:58:15 +0200 Subject: [PATCH 23/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 1 + Unity/SpaceCraft/Assets/Editor/Build.cs | 43 +++++++++++++------- 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 2c3f6f0d..cefe85c0 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -257,6 +257,7 @@ jobs: targetPlatform: ${{ inputs.targetPlatform }} unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} + customParameters: -logFile Logs/Editor.log -stackTraceLogType Full --scOut=${{ github.workspace }}/dist/SpaceCraft customParameters: -logFile Logs/Editor.log -stackTraceLogType Full runAsHostUser: true diff --git a/Unity/SpaceCraft/Assets/Editor/Build.cs b/Unity/SpaceCraft/Assets/Editor/Build.cs index 0a002b96..3182b5c3 100644 --- a/Unity/SpaceCraft/Assets/Editor/Build.cs +++ b/Unity/SpaceCraft/Assets/Editor/Build.cs @@ -46,13 +46,9 @@ public static void WebGL_Dev() // Default dev output (existing behavior) string buildPath = Path.Combine("..", "..", "WebSites", "SpaceCraft"); - // Allow CI override via env var - var envOut = Environment.GetEnvironmentVariable("SC_BUILD_OUTPUT"); - if (!string.IsNullOrEmpty(envOut)) - { - buildPath = envOut; - Debug.Log($"[Build] Overriding output via SC_BUILD_OUTPUT => {buildPath}"); - } + // Allow CI override via env var or CLI + buildPath = ResolveOutputPath(buildPath); + Debug.Log($"[Build] Output path: {buildPath}"); BuildPlayerOptions options = new BuildPlayerOptions { @@ -95,13 +91,9 @@ public static void WebGL_Prod() // Default prod output (existing behavior) string buildPath = Path.Combine("Builds", "SpaceCraft"); - // Allow CI override via env var - var envOut = Environment.GetEnvironmentVariable("SC_BUILD_OUTPUT"); - if (!string.IsNullOrEmpty(envOut)) - { - buildPath = envOut; - Debug.Log($"[Build] Overriding output via SC_BUILD_OUTPUT => {buildPath}"); - } + // Allow CI override via env var or CLI + buildPath = ResolveOutputPath(buildPath); + Debug.Log($"[Build] Output path: {buildPath}"); BuildPlayerOptions options = new BuildPlayerOptions { @@ -247,6 +239,29 @@ private static string[] GetBuildScenes() return scenes; } + private static string ResolveOutputPath(string defaultPath) + { + // 1) Environment variable + var envOut = Environment.GetEnvironmentVariable("SC_BUILD_OUTPUT"); + if (!string.IsNullOrEmpty(envOut)) return envOut; + + // 2) Command line arg: -scOut or --scOut= + var args = Environment.GetCommandLineArgs(); + for (int i = 0; i < args.Length; i++) + { + if (args[i] == "-scOut" && i + 1 < args.Length) + { + return args[i + 1]; + } + if (args[i].StartsWith("--scOut=")) + { + var val = args[i].Substring("--scOut=".Length); + if (!string.IsNullOrEmpty(val)) return val; + } + } + return defaultPath; + } + private static bool IsCommandLineBuild() { return Environment.CommandLine.Contains("-batchmode"); From eaa98827a398efa45487d49e378871f97cc62c46 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 03:00:02 +0200 Subject: [PATCH 24/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index cefe85c0..146d93a8 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -258,7 +258,6 @@ jobs: unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} customParameters: -logFile Logs/Editor.log -stackTraceLogType Full --scOut=${{ github.workspace }}/dist/SpaceCraft - customParameters: -logFile Logs/Editor.log -stackTraceLogType Full runAsHostUser: true From 812b72bb5e4077c46c8cc49046b9a6062770d9b1 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 03:46:05 +0200 Subject: [PATCH 25/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 146d93a8..051cd69f 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -245,6 +245,14 @@ jobs: mkdir -p Logs mkdir -p "${{ steps.detect.outputs.projectPath }}/Logs" + - name: Prepare dist path (clean, recreate, set perms) + shell: bash + run: | + set -euo pipefail + rm -rf dist + mkdir -p dist/SpaceCraft + chmod -R 777 dist + - name: Unity - Builder uses: game-ci/unity-builder@v4 env: From ac163591d7415f8d2d8f4b5e99e39f2322755712 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 04:30:26 +0200 Subject: [PATCH 26/29] tweak --- Unity/SpaceCraft/Assets/Editor/Build.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Unity/SpaceCraft/Assets/Editor/Build.cs b/Unity/SpaceCraft/Assets/Editor/Build.cs index 3182b5c3..5b7427f4 100644 --- a/Unity/SpaceCraft/Assets/Editor/Build.cs +++ b/Unity/SpaceCraft/Assets/Editor/Build.cs @@ -241,11 +241,7 @@ private static string[] GetBuildScenes() private static string ResolveOutputPath(string defaultPath) { - // 1) Environment variable - var envOut = Environment.GetEnvironmentVariable("SC_BUILD_OUTPUT"); - if (!string.IsNullOrEmpty(envOut)) return envOut; - - // 2) Command line arg: -scOut or --scOut= + // 1) Command line arg: -scOut or --scOut= var args = Environment.GetCommandLineArgs(); for (int i = 0; i < args.Length; i++) { @@ -259,6 +255,10 @@ private static string ResolveOutputPath(string defaultPath) if (!string.IsNullOrEmpty(val)) return val; } } + + // 2) Environment variable fallback + var envOut = Environment.GetEnvironmentVariable("SC_BUILD_OUTPUT"); + if (!string.IsNullOrEmpty(envOut)) return envOut; return defaultPath; } From d1816f24a4ff2d34e925b63ee217db3d84741ef7 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 04:46:42 +0200 Subject: [PATCH 27/29] tweak --- Unity/SpaceCraft/Assets/Editor/Build.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Unity/SpaceCraft/Assets/Editor/Build.cs b/Unity/SpaceCraft/Assets/Editor/Build.cs index 5b7427f4..9420610c 100644 --- a/Unity/SpaceCraft/Assets/Editor/Build.cs +++ b/Unity/SpaceCraft/Assets/Editor/Build.cs @@ -112,8 +112,7 @@ private static void PerformBuild(BuildPlayerOptions options) { // Ensure build directory exists string buildPath = Path.GetFullPath(options.locationPathName); // Get full path - string buildDir = Path.GetDirectoryName(buildPath); - Directory.CreateDirectory(buildDir); + Directory.CreateDirectory(buildPath); // --- PRE-BUILD STEP: Remove symlinks from build target directory --- Debug.Log($"[Build Pre-Clean] Cleaning symlinks from: {buildPath}"); From 2740a1f90821a40bfecc9bbf83a23b00688a4e05 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 06:26:47 +0200 Subject: [PATCH 28/29] tweak --- Unity/SpaceCraft/Assets/Editor/Build.cs | 64 ++++++++++++++----------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/Unity/SpaceCraft/Assets/Editor/Build.cs b/Unity/SpaceCraft/Assets/Editor/Build.cs index 9420610c..be623417 100644 --- a/Unity/SpaceCraft/Assets/Editor/Build.cs +++ b/Unity/SpaceCraft/Assets/Editor/Build.cs @@ -49,10 +49,13 @@ public static void WebGL_Dev() // Allow CI override via env var or CLI buildPath = ResolveOutputPath(buildPath); Debug.Log($"[Build] Output path: {buildPath}"); + + // Ensure scenes are configured + var scenes = EnsureScenesConfigured(); BuildPlayerOptions options = new BuildPlayerOptions { - scenes = GetBuildScenes(), + scenes = scenes, locationPathName = buildPath, target = BuildTarget.WebGL, options = BuildOptions.Development // Add development flag if needed @@ -94,10 +97,13 @@ public static void WebGL_Prod() // Allow CI override via env var or CLI buildPath = ResolveOutputPath(buildPath); Debug.Log($"[Build] Output path: {buildPath}"); + + // Ensure scenes are configured + var scenes = EnsureScenesConfigured(); BuildPlayerOptions options = new BuildPlayerOptions { - scenes = GetBuildScenes(), + scenes = scenes, locationPathName = buildPath, target = BuildTarget.WebGL, options = BuildOptions.None @@ -199,43 +205,43 @@ private static void RemoveSymlinksRecursive(string path) } } - private static string[] GetBuildScenes() + private static string[] EnsureScenesConfigured() { - // Prefer enabled scenes from Build Settings - var enabledScenes = System.Array.FindAll(EditorBuildSettings.scenes, s => s.enabled); - string[] scenes = new string[enabledScenes.Length]; - for (int i = 0; i < enabledScenes.Length; i++) + // If there are enabled scenes already, return them + var enabled = System.Array.FindAll(EditorBuildSettings.scenes, s => s.enabled); + if (enabled.Length > 0) { - scenes[i] = enabledScenes[i].path; + string[] paths = new string[enabled.Length]; + for (int i = 0; i < enabled.Length; i++) paths[i] = enabled[i].path; + return paths; } - // Fallback: if none configured/enabled, include all .unity scenes under Assets - if (scenes.Length == 0) + // Discover all .unity scenes under Assets and configure Build Settings + Debug.Log("[Build] No enabled scenes in Build Settings; falling back to discover all .unity scenes under Assets/"); + string[] discovered = new string[0]; + try { - Debug.LogWarning("[Build] No enabled scenes in Build Settings; falling back to discover all .unity scenes under Assets/"); - try - { - var found = Directory.GetFiles("Assets", "*.unity", SearchOption.AllDirectories); - scenes = found; - foreach (var scene in scenes) - { - Debug.Log($"[Build] Including scene: {scene}"); - } - } - catch (System.Exception ex) - { - Debug.LogError($"[Build] Failed to enumerate scenes: {ex.Message}"); - } + discovered = Directory.GetFiles("Assets", "*.unity", SearchOption.AllDirectories); + } + catch (Exception ex) + { + Debug.LogError($"[Build] Failed to enumerate scenes: {ex.Message}"); } - // Validate - if (scenes == null || scenes.Length == 0) + if (discovered.Length > 0) { - Debug.LogError("[Build] No scenes found to build. Configure Build Settings or add scenes under Assets/."); - if (IsCommandLineBuild()) EditorApplication.Exit(1); + var buildScenes = new EditorBuildSettingsScene[discovered.Length]; + for (int i = 0; i < discovered.Length; i++) + { + buildScenes[i] = new EditorBuildSettingsScene(discovered[i], true); + Debug.Log($"[Build] Including scene: {discovered[i]}"); + } + EditorBuildSettings.scenes = buildScenes; + return discovered; } - return scenes; + Debug.LogError("[Build] No scenes found to build. Configure Build Settings or add scenes under Assets/."); + return discovered; // may be empty; BuildPipeline will fail and surface error } private static string ResolveOutputPath(string defaultPath) From e5651d0a435883c37ca944d6f901e85f7b056c22 Mon Sep 17 00:00:00 2001 From: Don Hopkins Date: Tue, 16 Sep 2025 06:41:21 +0200 Subject: [PATCH 29/29] tweak --- .github/workflows/build-unity-spacecraft.yml | 36 +++++--------------- 1 file changed, 8 insertions(+), 28 deletions(-) diff --git a/.github/workflows/build-unity-spacecraft.yml b/.github/workflows/build-unity-spacecraft.yml index 051cd69f..48910d6d 100644 --- a/.github/workflows/build-unity-spacecraft.yml +++ b/.github/workflows/build-unity-spacecraft.yml @@ -245,13 +245,7 @@ jobs: mkdir -p Logs mkdir -p "${{ steps.detect.outputs.projectPath }}/Logs" - - name: Prepare dist path (clean, recreate, set perms) - shell: bash - run: | - set -euo pipefail - rm -rf dist - mkdir -p dist/SpaceCraft - chmod -R 777 dist + # Do not pre-create dist; Unity writes under project path, we stage after - name: Unity - Builder uses: game-ci/unity-builder@v4 @@ -259,13 +253,12 @@ jobs: UNITY_LICENSE: ${{ secrets.UNITY_LICENSE }} UNITY_EMAIL: ${{ secrets.UNITY_EMAIL }} UNITY_PASSWORD: ${{ secrets.UNITY_PASSWORD }} - SC_BUILD_OUTPUT: ${{ github.workspace }}/dist/SpaceCraft with: projectPath: ${{ steps.detect.outputs.projectPath }} targetPlatform: ${{ inputs.targetPlatform }} unityVersion: ${{ steps.detect.outputs.version }} buildMethod: ${{ steps.compute.outputs.buildMethod }} - customParameters: -logFile Logs/Editor.log -stackTraceLogType Full --scOut=${{ github.workspace }}/dist/SpaceCraft + customParameters: -logFile Logs/Editor.log -stackTraceLogType Full runAsHostUser: true @@ -312,25 +305,12 @@ jobs: run: | set -euo pipefail STAGE="dist" - echo "[assemble] Expecting Unity output at $STAGE/SpaceCraft" - test -d "$STAGE/SpaceCraft" || { echo "[assemble] ERROR: Unity output missing at $STAGE/SpaceCraft" >&2; exit 1; } - mkdir -p "$STAGE/controller" - # If Unity output missing, try common fallback paths - if [[ ! -d "$STAGE/SpaceCraft" ]]; then - CAND1="${{ steps.detect.outputs.projectPath }}/Builds/SpaceCraft" - CAND2="WebSites/SpaceCraft" - if [[ -d "$CAND1" ]]; then - echo "[assemble] Fallback: copying from $CAND1" - mkdir -p "$STAGE/SpaceCraft" - rsync -a "$CAND1/" "$STAGE/SpaceCraft/" - elif [[ -d "$CAND2" ]]; then - echo "[assemble] Fallback: copying from $CAND2" - mkdir -p "$STAGE/SpaceCraft" - rsync -a "$CAND2/" "$STAGE/SpaceCraft/" - else - echo "[assemble] WARNING: Unity output not found in dist/SpaceCraft, $CAND1, or $CAND2" - fi - fi + SRC_BUILDS="${{ steps.detect.outputs.projectPath }}/Builds/SpaceCraft" + echo "[assemble] Expecting Unity output at $SRC_BUILDS" + test -d "$SRC_BUILDS" || { echo "[assemble] ERROR: Unity output missing at $SRC_BUILDS" >&2; exit 1; } + rm -rf "$STAGE" + mkdir -p "$STAGE/SpaceCraft" "$STAGE/controller" + rsync -a "$SRC_BUILDS/" "$STAGE/SpaceCraft/" # Top-level site index if [[ -f WebSites/index.html ]]; then cp -f WebSites/index.html "$STAGE/index.html"