using Godot; using System; using System.Globalization; using System.IO; using System.Text.Json; using System.Text.Json.Serialization; namespace ETPreferences { public enum BlinkMode { Blinking = 0, Winking = 1, None = 2 } public partial class ET_Preferences : Node { private static ET_Preferences _instance; public static ET_Preferences Instance { get { if (_instance == null) { _instance = new ET_Preferences(); } return _instance; } } // Smoothing filter default public const float filter_default = 0.8f; // DFR defaults public const float horizontal_focus_default = 0.6f; public const float vertical_focus_default = 0.6f; public const float peripheral_default = 0.4f; public const float focus_default = 1f; public const float transition_thickness_default = 0.3f; public const int debug_gaze_default = 0; public bool camera_enabled { get; set; } public bool submit_to_vrchat_osc { get; set; } public bool submit_to_xr { get; set; } public bool submit_to_vrcft { get; set; } public bool submit_to_buffered_capture { get; set; } public bool submit_to_viewer { get; set; } public bool smoothing_enabled { get; set; } public int blink_mode { get; set; } public bool openxr_smoothing_excluded { get; set; } public bool expecting_new_model { get; set; } public bool dfr_settings_prompted { get; set; } public bool enabled_alignment_helper { get; set; } public bool seen_alignment_guide { get; set; } public bool left_eye_enabled { get; set; } public bool right_eye_enabled { get; set; } public string user_testing_token { get; set; } public string selected_model { get; set; } public string smoothing_intensity { get; set; } public string osc_ip { get; set; } public int osc_port { get; set; } public float left_pupil_center_x { get; set; } public float left_pupil_center_y { get; set; } public float right_pupil_center_x { get; set; } public float right_pupil_center_y { get; set; } public string quad_horizontal_focus_section { get; set; } public string quad_vertical_focus_section { get; set; } public string quad_peripheral_multiplier { get; set; } public string quad_focus_multiplier { get; set; } public string quad_transition_thickness { get; set; } public int quad_debug_gaze { get; set; } public int seen_announcements { get; set; } public Godot.Collections.Array downloaded_models { get; set; } = new Godot.Collections.Array(); public Godot.Collections.Dictionary named_models { get; set; } = new Godot.Collections.Dictionary(); // Helper properties for Vector2 access /* [JsonIgnore] public Vector2 left_pupil_center { get => new Vector2(left_pupil_center_x, left_pupil_center_y); set { left_pupil_center_x = value.X; left_pupil_center_y = value.Y; } } [JsonIgnore] public Vector2 right_pupil_center { get => new Vector2(right_pupil_center_x, right_pupil_center_y); set { right_pupil_center_x = value.X; right_pupil_center_y = value.Y; } } */ } public partial class EyeTrackingPreferences : Node { static string settings_file_name = "eyetracking_settings.json"; /// /// Detects pupil centers from the camera and saves them to preferences. /// /// The camera manager to capture from /// True if detection and save was successful public static bool DetectAndSavePupilCenters(CameraManager cameraManager) { return DetectAndSavePupilCenters(cameraManager, null); } /// /// Detects pupil centers from the camera and saves them to preferences. /// Optionally saves a visualization image for debugging. /// /// The camera manager to capture from /// Optional path to save a visualization image (null to skip) /// True if detection and save was successful public static bool DetectAndSavePupilCenters(CameraManager cameraManager, string debugImagePath) { try { if (cameraManager == null || !cameraManager.IsInitialized) { GD.PrintErr("Camera manager is not initialized"); return false; } // Get a color frame for detection bool wasGrayscale = cameraManager.UseGrayscale; cameraManager.UseGrayscale = false; OpenCvSharp.Mat colorFrame = cameraManager.GetCameraFrame(); cameraManager.UseGrayscale = wasGrayscale; if (colorFrame == null || colorFrame.Empty()) { GD.PrintErr("Failed to get camera frame for pupil detection"); return false; } // Detect pupils var pupils = PupilDetector.FindPupilCenters(colorFrame); if (pupils.Count != 2) { GD.PrintErr($"Expected 2 pupils, found {pupils.Count}"); colorFrame.Dispose(); return false; } // Save debug visualization if requested if (!string.IsNullOrEmpty(debugImagePath)) { PupilDetector.SaveVisualization(colorFrame, pupils, debugImagePath); } // Normalize coordinates to 0-1 range int imageWidth = colorFrame.Width; int imageHeight = colorFrame.Height; float leftX = pupils[0].X / (float)imageWidth; float leftY = pupils[0].Y / (float)imageHeight; float rightX = pupils[1].X / (float)imageWidth; float rightY = pupils[1].Y / (float)imageHeight; colorFrame.Dispose(); // Update preferences with detected values /* ET_Preferences.Instance.left_pupil_center_x = leftX; ET_Preferences.Instance.left_pupil_center_y = leftY; ET_Preferences.Instance.right_pupil_center_x = rightX; ET_Preferences.Instance.right_pupil_center_y = rightY; GD.Print($"Pupil centers detected and set:"); GD.Print($" Left: ({leftX:F3}, {leftY:F3})"); GD.Print($" Right: ({rightX:F3}, {rightY:F3})"); */ // Save to file return WriteSettingsFile(); } catch (Exception ex) { GD.PrintErr($"Error detecting pupil centers: {ex.Message}"); GD.PrintErr(ex.StackTrace); return false; } } public static bool WriteSettingsFile() { try { // Configure JsonSerializerOptions to ignore properties that can't be serialized var options = new JsonSerializerOptions { WriteIndented = true, DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull }; // Create a simple anonymous object with just the properties we want to save var settingsData = new { ET_Preferences.Instance.camera_enabled, ET_Preferences.Instance.submit_to_vrchat_osc, ET_Preferences.Instance.submit_to_xr, ET_Preferences.Instance.submit_to_vrcft, ET_Preferences.Instance.submit_to_buffered_capture, ET_Preferences.Instance.smoothing_enabled, ET_Preferences.Instance.blink_mode, ET_Preferences.Instance.openxr_smoothing_excluded, ET_Preferences.Instance.expecting_new_model, ET_Preferences.Instance.dfr_settings_prompted, ET_Preferences.Instance.enabled_alignment_helper, ET_Preferences.Instance.seen_alignment_guide, ET_Preferences.Instance.left_eye_enabled, ET_Preferences.Instance.right_eye_enabled, ET_Preferences.Instance.smoothing_intensity, ET_Preferences.Instance.user_testing_token, ET_Preferences.Instance.selected_model, ET_Preferences.Instance.downloaded_models, ET_Preferences.Instance.osc_ip, ET_Preferences.Instance.osc_port, /* ET_Preferences.Instance.left_pupil_center_x, ET_Preferences.Instance.left_pupil_center_y, ET_Preferences.Instance.right_pupil_center_x, ET_Preferences.Instance.right_pupil_center_y, */ ET_Preferences.Instance.quad_horizontal_focus_section, ET_Preferences.Instance.quad_vertical_focus_section, ET_Preferences.Instance.quad_peripheral_multiplier, ET_Preferences.Instance.quad_focus_multiplier, ET_Preferences.Instance.quad_transition_thickness, ET_Preferences.Instance.quad_debug_gaze, ET_Preferences.Instance.seen_announcements, ET_Preferences.Instance.named_models, }; string json = JsonSerializer.Serialize(settingsData, options); string path = Path.Combine(OS.GetExecutablePath().GetBaseDir(), settings_file_name); File.WriteAllText(path, json); GD.Print("Settings saved to: " + path); return true; } catch (Exception e) { GD.PrintErr("Failed to write settings: " + e.Message); return false; } } public static bool CheckTokenChanged() { try { string path = Path.Combine(OS.GetExecutablePath().GetBaseDir(), settings_file_name); if (!File.Exists(path)) { GD.PrintErr("Settings file not found at: " + path + "\nCreating a blank file."); return false; } string json = File.ReadAllText(path); var parsed = JsonDocument.Parse(json); var root = parsed.RootElement; string new_token = root.TryGetProperty("user_testing_token", out var token) ? token.GetString() : ""; if (!string.IsNullOrWhiteSpace(new_token) && ET_Preferences.Instance.user_testing_token != new_token) { GD.Print("Found a new token from settings!"); ET_Preferences.Instance.user_testing_token = new_token; return true; } return false; } catch (Exception e) { return false; } } public static bool ReadSettingsFile() { try { string path = Path.Combine(OS.GetExecutablePath().GetBaseDir(), settings_file_name); if (!File.Exists(path)) { GD.PrintErr("Settings file not found at: " + path + "\nCreating a blank file."); File.WriteAllText(path, "{}"); } string json = File.ReadAllText(path); var parsed = JsonDocument.Parse(json); var root = parsed.RootElement; ET_Preferences.Instance.camera_enabled = root.TryGetProperty("camera_enabled", out var camEnabled) ? camEnabled.GetBoolean() : true; ET_Preferences.Instance.submit_to_vrchat_osc = root.TryGetProperty("submit_to_vrchat_osc", out var vrchatOsc) ? vrchatOsc.GetBoolean() : true; ET_Preferences.Instance.submit_to_xr = root.TryGetProperty("submit_to_xr", out var openxr) ? openxr.GetBoolean() : false; ET_Preferences.Instance.submit_to_vrcft = root.TryGetProperty("submit_to_vrcft", out var vrcft) ? vrcft.GetBoolean() : false; ET_Preferences.Instance.submit_to_buffered_capture = root.TryGetProperty("submit_to_buffered_capture", out var buffered) ? buffered.GetBoolean() : false; ET_Preferences.Instance.smoothing_enabled = root.TryGetProperty("smoothing_enabled", out var sm) ? sm.GetBoolean() : true; ET_Preferences.Instance.blink_mode = root.TryGetProperty("blink_mode", out var bm) ? bm.GetInt32() : 0; ET_Preferences.Instance.openxr_smoothing_excluded = root.TryGetProperty("openxr_smoothing_excluded", out var oxs) ? oxs.GetBoolean() : true; ET_Preferences.Instance.expecting_new_model = root.TryGetProperty("expecting_new_model", out var em) ? em.GetBoolean() : false; ET_Preferences.Instance.dfr_settings_prompted = root.TryGetProperty("dfr_settings_prompted", out var dfe) ? dfe.GetBoolean() : false; ET_Preferences.Instance.enabled_alignment_helper = root.TryGetProperty("enabled_alignment_helper", out var eh) ? eh.GetBoolean() : false; ET_Preferences.Instance.seen_alignment_guide = root.TryGetProperty("seen_alignment_guide", out var alg) ? alg.GetBoolean() : false; ET_Preferences.Instance.right_eye_enabled = root.TryGetProperty("right_eye_enabled", out var reye) ? reye.GetBoolean() : true; ET_Preferences.Instance.left_eye_enabled = root.TryGetProperty("left_eye_enabled", out var leye) ? leye.GetBoolean() : true; ET_Preferences.Instance.smoothing_intensity = root.TryGetProperty("smoothing_intensity", out var si) ? si.GetString() : ET_Preferences.filter_default.ToString(CultureInfo.InvariantCulture); ET_Preferences.Instance.user_testing_token = root.TryGetProperty("user_testing_token", out var token) ? token.GetString() : ""; ET_Preferences.Instance.selected_model = root.TryGetProperty("selected_model", out var model) ? model.GetString() : ""; ET_Preferences.Instance.osc_ip = root.TryGetProperty("osc_ip", out var o_ip) ? o_ip.GetString() : "127.0.0.1"; ET_Preferences.Instance.osc_port = root.TryGetProperty("osc_port", out var o_port) ? o_port.GetInt32() : 9000; /* ET_Preferences.Instance.left_pupil_center_x = root.TryGetProperty("left_pupil_center_x", out var lpcx) ? lpcx.GetSingle() : 0f; ET_Preferences.Instance.left_pupil_center_y = root.TryGetProperty("left_pupil_center_y", out var lpcy) ? lpcy.GetSingle() : 0f; ET_Preferences.Instance.right_pupil_center_x = root.TryGetProperty("right_pupil_center_x", out var rpcx) ? rpcx.GetSingle() : 0f; ET_Preferences.Instance.right_pupil_center_y = root.TryGetProperty("right_pupil_center_y", out var rpcy) ? rpcy.GetSingle() : 0f; */ ET_Preferences.Instance.seen_announcements = root.TryGetProperty("seen_announcements", out var a) ? a.GetInt32() : (int)Ui.feature_announcements.None; ET_Preferences.Instance.quad_horizontal_focus_section = root.TryGetProperty("quad_horizontal_focus_section", out var hfs) ? hfs.GetString() : ET_Preferences.horizontal_focus_default.ToString(CultureInfo.InvariantCulture); ET_Preferences.Instance.quad_vertical_focus_section = root.TryGetProperty("quad_vertical_focus_section", out var vfs) ? vfs.GetString() : ET_Preferences.vertical_focus_default.ToString(CultureInfo.InvariantCulture); ET_Preferences.Instance.quad_peripheral_multiplier = root.TryGetProperty("quad_peripheral_multiplier", out var pm) ? pm.GetString() : ET_Preferences.peripheral_default.ToString(CultureInfo.InvariantCulture); ET_Preferences.Instance.quad_focus_multiplier = root.TryGetProperty("quad_focus_multiplier", out var fm) ? fm.GetString() : ET_Preferences.focus_default.ToString(CultureInfo.InvariantCulture); ET_Preferences.Instance.quad_transition_thickness = root.TryGetProperty("quad_transition_thickness", out var qtt) ? qtt.GetString() : ET_Preferences.transition_thickness_default.ToString(CultureInfo.InvariantCulture); ET_Preferences.Instance.quad_debug_gaze = root.TryGetProperty("quad_debug_gaze", out var dbg) ? dbg.GetInt32() : ET_Preferences.debug_gaze_default; if (root.TryGetProperty("downloaded_models", out var stringArrayElement) && stringArrayElement.ValueKind == JsonValueKind.Array) { var array = new Godot.Collections.Array(); foreach (var item in stringArrayElement.EnumerateArray()) { if (item.ValueKind == JsonValueKind.String && File.Exists(item.GetString())) { array.Add(item.GetString()); } } ET_Preferences.Instance.downloaded_models = array; } else { ET_Preferences.Instance.downloaded_models = new Godot.Collections.Array(); } if (root.TryGetProperty("named_models", out var dictArrayElement) && dictArrayElement.ValueKind == JsonValueKind.Object) { Godot.Collections.Dictionary namedDict = new(); foreach (var prop in dictArrayElement.EnumerateObject()) { namedDict[prop.Name] = prop.Value.GetString(); } ET_Preferences.Instance.named_models = namedDict; } else { ET_Preferences.Instance.named_models = new Godot.Collections.Dictionary(); } GD.Print("Settings loaded from: " + path); return true; } catch (Exception e) { GD.PrintErr("Failed to read settings: " + e.Message); return false; } } public static BlinkMode GetBlinkMode() { int raw = ET_Preferences.Instance != null ? ET_Preferences.Instance.blink_mode : 0; return raw switch { 1 => BlinkMode.Winking, 2 => BlinkMode.None, _ => BlinkMode.Blinking }; } } }