using System;
using System.Collections;
using System.Collections.Generic;
using System.Net;
using System.Text;
using UnityEngine;
using Newtonsoft.Json;
///
/// VR Experiment Controller
/// Listens for UDP messages from the supervisor and manages avatars and objects
/// based on the current experimental condition.
///
public class VRExperimentController : MonoBehaviour
{
[Header("Network Settings")]
[SerializeField] private bool allowPortSharing = true; // For local testing with multiple components
[Header("Avatar Assignments")]
[SerializeField] private GameObject helpfulAvatar;
[SerializeField] private GameObject demotivatingAvatar;
[SerializeField] private GameObject nonInteractiveAvatar;
[SerializeField] private GameObject practiceAvatar;
[Header("Object Assignments")]
[SerializeField] private GameObject brickObject;
[SerializeField] private GameObject paperclipObject;
[SerializeField] private GameObject ropeObject;
[SerializeField] private GameObject bookObject;
[Header("Debug Settings")]
[SerializeField] private bool enableDebugLogging = true;
// Network components
private bool isListening = false;
private int udpPort;
// Current experiment state
private string currentConditionType = "";
private string currentObjectType = "";
private int currentConditionIndex = -1;
private bool practiceRoundActive = false;
// Message queue for thread-safe communication
private Queue messageQueue = new Queue();
private readonly object queueLock = new object();
// Object mapping for easy access
private Dictionary objectMap;
private Dictionary avatarMap;
void Start()
{
// Get network config from global instance
var cfg = NetworkConfig.Instance;
if (cfg != null)
{
udpPort = cfg.port;
}
else
{
Debug.LogError("NetworkConfig not found! Please ensure NetworkConfig.asset exists in Resources folder.");
udpPort = 1221;
}
InitializeObjectMaps();
StartCoroutine(WaitAndSubscribe());
// Initially deactivate all objects and avatars
DeactivateAllObjects();
DeactivateAllAvatars();
LogMessage("VR Experiment Controller started. Listening on port 1221 (shared with avatar sync). Waiting for supervisor commands...");
}
void Update()
{
// Process messages from the UDP thread in the main thread
ProcessMessageQueue();
}
void OnDestroy()
{
StopUDPListener();
}
void OnApplicationPause(bool pauseStatus)
{
if (pauseStatus)
{
LogMessage("Application paused - stopping UDP listener");
StopUDPListener();
}
else
{
LogMessage("Application unpaused - restarting UDP listener");
StartUDPListener();
}
}
void OnApplicationFocus(bool hasFocus)
{
// Don't stop UDP listener when losing focus - supervisor GUI needs to communicate
// Only restart if we had stopped for some reason
if (hasFocus && !isListening)
{
LogMessage("Application regained focus - restarting UDP listener");
StartUDPListener();
}
}
///
/// Initialize object and avatar mappings for easy access
///
private void InitializeObjectMaps()
{
// Initialize object map
objectMap = new Dictionary
{
{ "Brick", brickObject },
{ "Paperclip", paperclipObject },
{ "Rope", ropeObject },
{ "Book", bookObject }
};
// Initialize avatar map
avatarMap = new Dictionary
{
{ "Helpful", helpfulAvatar },
{ "Demotivating", demotivatingAvatar },
{ "Non-Interactive", nonInteractiveAvatar }
};
// Validate assignments
ValidateAssignments();
}
///
/// Validate that all required objects and avatars are assigned
///
private void ValidateAssignments()
{
bool hasErrors = false;
// Check objects
foreach (var kvp in objectMap)
{
if (kvp.Value == null)
{
LogError($"Object not assigned: {kvp.Key}");
hasErrors = true;
}
}
// Check avatars
foreach (var kvp in avatarMap)
{
if (kvp.Value == null)
{
LogError($"Avatar not assigned: {kvp.Key}");
hasErrors = true;
}
}
// Check practice avatar
if (practiceAvatar == null)
{
LogError("Practice avatar not assigned!");
hasErrors = true;
}
// Check non-interactive avatar
if (nonInteractiveAvatar == null)
{
LogError("Non-interactive avatar not assigned!");
hasErrors = true;
}
if (hasErrors)
{
LogError("Please assign all required objects and avatars in the inspector!");
}
else
{
LogMessage("All objects and avatars properly assigned.");
}
}
///
/// Start listening for UDP messages
///
private void StartUDPListener()
{
try
{
// Subscribe to shared listener
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived += HandlePacketReceived;
isListening = true;
LogMessage($"UDP Experiment Control Listener subscribed to shared listener on port {udpPort} (filtering JSON messages only)");
}
catch (Exception e)
{
LogError($"Failed to subscribe to shared UDP listener: {e.Message}");
}
}
private System.Collections.IEnumerator WaitAndSubscribe()
{
float timeout = 3f;
while (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null && timeout > 0f)
{
timeout -= Time.unscaledDeltaTime;
yield return null;
}
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance == null)
{
LogError("SharedUDPListener not ready after wait.");
yield break;
}
StartUDPListener();
}
///
/// Handle packet received from shared listener
///
private void HandlePacketReceived(byte[] data, IPEndPoint senderEndPoint)
{
// Check if this is an experiment control message (JSON)
if (!IsExperimentControlMessage(data)) return;
string message = Encoding.UTF8.GetString(data);
// Add message to queue for main thread processing
lock (queueLock)
{
messageQueue.Enqueue(message);
}
}
///
/// Stop listening for UDP messages
///
private void StopUDPListener()
{
isListening = false;
// Unsubscribe from shared listener
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
{
Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.OnPacketReceived -= HandlePacketReceived;
}
LogMessage("UDP Listener stopped");
}
///
/// Check if the received data is an experiment control message (JSON) vs avatar data (binary)
///
private bool IsExperimentControlMessage(byte[] data)
{
try
{
// Experiment control messages are JSON and should be reasonably small
if (data.Length > 1024) // Avatar data is typically much larger
return false;
// Try to decode as UTF-8 text
string text = Encoding.UTF8.GetString(data);
// Check if it looks like JSON (starts with '{' and contains "command")
if (text.TrimStart().StartsWith("{") && text.Contains("\"command\""))
{
return true;
}
return false;
}
catch
{
// If we can't decode as UTF-8 or any other error, it's probably binary avatar data
return false;
}
}
///
/// Process messages from the queue in the main thread
///
private void ProcessMessageQueue()
{
lock (queueLock)
{
while (messageQueue.Count > 0)
{
string message = messageQueue.Dequeue();
ProcessUDPMessage(message);
}
}
}
///
/// Process incoming UDP message
///
private void ProcessUDPMessage(string message)
{
try
{
LogMessage($"Received UDP message: {message}");
// Parse JSON message
var messageData = JsonConvert.DeserializeObject>(message);
if (messageData.ContainsKey("command"))
{
string command = messageData["command"].ToString();
switch (command)
{
case "start_condition":
case "next_condition":
HandleConditionChange(messageData);
break;
case "start_practice_round":
HandlePracticeRoundStart(messageData);
break;
case "end_practice_round":
HandlePracticeRoundEnd(messageData);
break;
case "disable_all":
HandleDisableAll(messageData);
break;
default:
LogMessage($"Unknown command received: {command}");
break;
}
}
}
catch (Exception e)
{
LogError($"Failed to process UDP message: {e.Message}");
}
}
///
/// Handle practice round start command
///
private void HandlePracticeRoundStart(Dictionary messageData)
{
try
{
int duration = messageData.ContainsKey("duration") ? Convert.ToInt32(messageData["duration"]) : 300;
LogMessage($"Practice round started. Duration: {duration} seconds");
practiceRoundActive = true;
// Deactivate all objects and regular avatars
DeactivateAllObjects();
DeactivateAllAvatars();
// Activate only the practice avatar
if (practiceAvatar != null)
{
practiceAvatar.SetActive(true);
LogMessage("Practice avatar activated");
}
else
{
LogError("Practice avatar not assigned!");
}
// Clear current condition state
currentConditionType = "";
currentObjectType = "";
currentConditionIndex = -1;
LogMessage("Practice round active - only practice avatar is present");
}
catch (Exception e)
{
LogError($"Failed to handle practice round start: {e.Message}");
}
}
///
/// Handle practice round end command
///
private void HandlePracticeRoundEnd(Dictionary messageData)
{
try
{
string reason = messageData.ContainsKey("reason") ? messageData["reason"].ToString() : "unknown";
LogMessage($"Practice round ended. Reason: {reason}");
practiceRoundActive = false;
// Deactivate practice avatar
if (practiceAvatar != null)
{
practiceAvatar.SetActive(false);
LogMessage("Practice avatar deactivated");
}
// Ensure all objects and avatars are deactivated
DeactivateAllObjects();
DeactivateAllAvatars();
LogMessage("Practice round completed - all avatars and objects disabled");
}
catch (Exception e)
{
LogError($"Failed to handle practice round end: {e.Message}");
}
}
///
/// Handle disable all command (when timer expires)
///
private void HandleDisableAll(Dictionary messageData)
{
try
{
string reason = messageData.ContainsKey("reason") ? messageData["reason"].ToString() : "unknown";
LogMessage($"Disable all command received. Reason: {reason}");
// Deactivate all objects and avatars
DeactivateAllObjects();
DeactivateAllAvatars();
// Deactivate practice avatar if active
if (practiceAvatar != null)
{
practiceAvatar.SetActive(false);
}
// Clear current condition state
currentConditionType = "";
currentObjectType = "";
practiceRoundActive = false;
LogMessage("All objects and avatars disabled - block finished");
// Trigger condition changed event
OnConditionChanged();
}
catch (Exception e)
{
LogError($"Failed to handle disable all command: {e.Message}");
}
}
///
/// Handle condition change messages
///
private void HandleConditionChange(Dictionary messageData)
{
try
{
// Extract condition data
currentConditionType = messageData["condition_type"].ToString();
currentObjectType = messageData["object_type"].ToString();
currentConditionIndex = Convert.ToInt32(messageData["condition_index"]);
LogMessage($"Condition changed to: {currentConditionType} with {currentObjectType} (Index: {currentConditionIndex})");
// End practice round if it was active
if (practiceRoundActive)
{
practiceRoundActive = false;
if (practiceAvatar != null)
{
practiceAvatar.SetActive(false);
}
}
// Update the scene based on the new condition
UpdateSceneForCondition();
}
catch (Exception e)
{
LogError($"Failed to handle condition change: {e.Message}");
}
}
///
/// Update the scene based on the current condition
///
private void UpdateSceneForCondition()
{
// First, deactivate everything
DeactivateAllObjects();
DeactivateAllAvatars();
// Deactivate practice avatar
if (practiceAvatar != null)
{
practiceAvatar.SetActive(false);
}
// Activate the required object
if (objectMap.ContainsKey(currentObjectType) && objectMap[currentObjectType] != null)
{
objectMap[currentObjectType].SetActive(true);
LogMessage($"Activated object: {currentObjectType}");
}
else
{
LogError($"Object not found or not assigned: {currentObjectType}");
}
// Activate the required avatar (only for non-control conditions)
if (currentConditionType != "Control")
{
if (avatarMap.ContainsKey(currentConditionType) && avatarMap[currentConditionType] != null)
{
avatarMap[currentConditionType].SetActive(true);
LogMessage($"Activated avatar: {currentConditionType}");
// Handle non-interactive avatar behavior
if (currentConditionType == "Non-Interactive")
{
HandleNonInteractiveAvatar();
}
}
else
{
LogError($"Avatar not found or not assigned: {currentConditionType}");
}
}
else
{
LogMessage("Control condition - no avatar activated");
}
// Trigger any additional condition-specific behavior
OnConditionChanged();
}
///
/// Handle non-interactive avatar behavior
///
private void HandleNonInteractiveAvatar()
{
// The non-interactive avatar is a dedicated avatar that should not respond to interactions
// You can add specific behavior here, such as:
// - Disabling interaction scripts
// - Setting the avatar to a passive state
// - Disabling speech or gesture systems
LogMessage("Non-interactive avatar activated - avatar will not respond to interactions");
// Example: Disable interaction components if they exist
var interactionComponents = avatarMap["Non-Interactive"].GetComponents();
foreach (var component in interactionComponents)
{
// Disable specific interaction scripts (adjust based on your actual component names)
if (component.GetType().Name.Contains("Interaction") ||
component.GetType().Name.Contains("Speech") ||
component.GetType().Name.Contains("Gesture"))
{
component.enabled = false;
LogMessage($"Disabled interaction component: {component.GetType().Name}");
}
}
}
///
/// Deactivate all objects
///
private void DeactivateAllObjects()
{
foreach (var kvp in objectMap)
{
if (kvp.Value != null)
{
kvp.Value.SetActive(false);
}
}
}
///
/// Deactivate all avatars
///
private void DeactivateAllAvatars()
{
foreach (var kvp in avatarMap)
{
if (kvp.Value != null)
{
kvp.Value.SetActive(false);
}
}
}
///
/// Called when condition changes - override this for custom behavior
///
protected virtual void OnConditionChanged()
{
// Send Unity event or trigger custom behavior here
// This can be overridden by derived classes for specific experiment needs
if (practiceRoundActive)
{
LogMessage("Practice round active - only practice avatar is present");
}
else
{
LogMessage($"Condition change complete. Current state: {currentConditionType} - {currentObjectType}");
}
}
///
/// Get the actual UDP port being used
///
private int GetActualListenPort()
{
if (Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance != null)
{
return Convai.Scripts.Runtime.Multiplayer.SharedUDPListener.Instance.ListenPort;
}
return udpPort;
}
///
/// Get current condition information
///
public string GetCurrentCondition()
{
if (practiceRoundActive)
{
return "Practice Round Active";
}
return $"{currentConditionType} - {currentObjectType} (Index: {currentConditionIndex})";
}
///
/// Check if a specific object is currently active
///
public bool IsObjectActive(string objectType)
{
if (objectMap.ContainsKey(objectType) && objectMap[objectType] != null)
{
return objectMap[objectType].activeInHierarchy;
}
return false;
}
///
/// Check if a specific avatar is currently active
///
public bool IsAvatarActive(string avatarType)
{
if (avatarMap.ContainsKey(avatarType) && avatarMap[avatarType] != null)
{
return avatarMap[avatarType].activeInHierarchy;
}
return false;
}
///
/// Check if practice round is active
///
public bool IsPracticeRoundActive()
{
return practiceRoundActive;
}
///
/// Log message with timestamp
///
private void LogMessage(string message)
{
if (enableDebugLogging)
{
Debug.Log($"[VRExperimentController] {DateTime.Now:HH:mm:ss} - {message}");
}
}
///
/// Log error message
///
private void LogError(string message)
{
Debug.LogError($"[VRExperimentController] {DateTime.Now:HH:mm:ss} - ERROR: {message}");
}
///
/// Manual test methods for debugging (can be called from inspector)
///
[ContextMenu("Test Helpful Condition")]
public void TestHelpfulCondition()
{
var testMessage = new Dictionary
{
{"command", "start_condition"},
{"condition_type", "Helpful"},
{"object_type", "Brick"},
{"condition_index", 0}
};
HandleConditionChange(testMessage);
}
[ContextMenu("Test Demotivating Condition")]
public void TestDemotivatingCondition()
{
var testMessage = new Dictionary
{
{"command", "start_condition"},
{"condition_type", "Demotivating"},
{"object_type", "Paperclip"},
{"condition_index", 1}
};
HandleConditionChange(testMessage);
}
[ContextMenu("Test Control Condition")]
public void TestControlCondition()
{
var testMessage = new Dictionary
{
{"command", "start_condition"},
{"condition_type", "Control"},
{"object_type", "Rope"},
{"condition_index", 2}
};
HandleConditionChange(testMessage);
}
[ContextMenu("Test Non-Interactive Condition")]
public void TestNonInteractiveCondition()
{
var testMessage = new Dictionary
{
{"command", "start_condition"},
{"condition_type", "Non-Interactive"},
{"object_type", "Book"},
{"condition_index", 3}
};
HandleConditionChange(testMessage);
}
[ContextMenu("Test Practice Round")]
public void TestPracticeRound()
{
var testMessage = new Dictionary
{
{"command", "start_practice_round"},
{"duration", 300}
};
HandlePracticeRoundStart(testMessage);
}
///
/// Optional debug GUI for monitoring experiment controller status
///
void OnGUI()
{
if (!enableDebugLogging) return;
GUILayout.BeginArea(new Rect(10, 10, 350, 250));
GUILayout.Label($"VR Experiment Controller (JSON Only)");
GUILayout.Label($"Status: {(isListening ? "Listening" : "Stopped")}");
GUILayout.Label($"Listen Port: {GetActualListenPort()}{(GetActualListenPort() != udpPort ? " (alt)" : "")}");
GUILayout.Label($"Port Sharing: {allowPortSharing}");
GUILayout.Label($"Practice Round: {(practiceRoundActive ? "Active" : "Inactive")}");
GUILayout.Label($"Current Condition: {currentConditionType}");
GUILayout.Label($"Current Object: {currentObjectType}");
GUILayout.Label($"Condition Index: {currentConditionIndex}");
GUILayout.Label($"Has Condition: {!string.IsNullOrEmpty(currentConditionType)}");
if (GUILayout.Button(isListening ? "Stop Listener" : "Start Listener"))
{
if (isListening)
StopUDPListener();
else
StartUDPListener();
}
GUILayout.EndArea();
}
}