diff --git a/CharaStudioVR/CharaStudioVR.csproj b/CharaStudioVR/CharaStudioVR.csproj index 6d8b86b..74432dc 100644 --- a/CharaStudioVR/CharaStudioVR.csproj +++ b/CharaStudioVR/CharaStudioVR.csproj @@ -185,6 +185,7 @@ + diff --git a/CharaStudioVR/Settings/CharaStudioContext.cs b/CharaStudioVR/Settings/CharaStudioContext.cs index 86b9dd6..1a0067e 100644 --- a/CharaStudioVR/Settings/CharaStudioContext.cs +++ b/CharaStudioVR/Settings/CharaStudioContext.cs @@ -1,5 +1,4 @@ using System; -using System.IO; using System.Xml.Serialization; using UnityEngine; using VRGIN.Core; @@ -10,41 +9,13 @@ namespace KKS_VR.Settings [XmlRoot("Context")] public class CharaStudioContext : IVRManagerContext { - private static string _contextSavePath = Path.Combine(BepInEx.Paths.ConfigPath, "KKS_CharaStudioVRContext.xml"); - private static string _settingsSavePath = Path.Combine(BepInEx.Paths.ConfigPath, "KKS_CharaStudioVRSettings.xml"); + private readonly DefaultMaterialPalette _Materials; - private DefaultMaterialPalette _Materials; - - public CharaStudioContext() - { - _Materials = new DefaultMaterialPalette(); - Settings = CharaStudioSettings.Load(_settingsSavePath); - ConfineMouse = true; - EnforceDefaultGUIMaterials = false; - GUIAlternativeSortingMode = false; - GuiLayer = "Default"; - GuiFarClipPlane = 1000f; - GuiNearClipPlane = -1000f; - IgnoreMask = 0; - InvisibleLayer = "Ignore Raycast"; - PrimaryColor = Color.cyan; - SimulateCursor = true; - UILayer = "UI"; - UILayerMask = LayerMask.GetMask(UILayer); - UnitToMeter = 1f; - NearClipPlane = 0.001f; - PreferredGUI = GUIType.uGUI; - } - - public string Version { get; set; } - - public float MaxFarClipPlane { get; set; } - - public int GuiMaterialRenderQueue { get; set; } + private readonly VRSettings _Settings; [XmlIgnore] public IMaterialPalette Materials => _Materials; - [XmlIgnore] public VRSettings Settings { get; } + [XmlIgnore] public VRSettings Settings => _Settings; public bool ConfineMouse { get; set; } @@ -76,40 +47,36 @@ public CharaStudioContext() public GUIType PreferredGUI { get; set; } + public string Version { get; set; } + + public float MaxFarClipPlane { get; set; } + + public int GuiMaterialRenderQueue { get; set; } + Type IVRManagerContext.VoiceCommandType { get; } public bool ForceIMGUIOnScreen { get; set; } - public static IVRManagerContext GetContext() + + public CharaStudioContext(CharaStudioSettings settings) { - var path = _contextSavePath; - var xmlSerializer = new XmlSerializer(typeof(CharaStudioContext)); - if (File.Exists(path)) - { - using var stream = File.OpenRead(path); - try - { - return xmlSerializer.Deserialize(stream) as CharaStudioContext; - } - catch (Exception) - { - VRLog.Error("Failed to deserialize {0} -- using default", path); - } - } - - var configurableContext = new CharaStudioContext(); - try - { - using var streamWriter = new StreamWriter(path); - streamWriter.BaseStream.SetLength(0L); - xmlSerializer.Serialize(streamWriter, configurableContext); - return configurableContext; - } - catch (Exception) - { - VRLog.Error("Failed to write {0}", path); - return configurableContext; - } + _Materials = new DefaultMaterialPalette(); + _Settings = settings; + ConfineMouse = true; + EnforceDefaultGUIMaterials = false; + GUIAlternativeSortingMode = false; + GuiLayer = "Default"; + GuiFarClipPlane = 1000f; + GuiNearClipPlane = -1000f; + IgnoreMask = 0; + InvisibleLayer = "Ignore Raycast"; + PrimaryColor = Color.cyan; + SimulateCursor = true; + UILayer = "UI"; + UILayerMask = LayerMask.GetMask(UILayer); + UnitToMeter = 1f; + NearClipPlane = 0.001f; + PreferredGUI = GUIType.uGUI; } } } diff --git a/CharaStudioVR/Settings/CharaStudioSettings.cs b/CharaStudioVR/Settings/CharaStudioSettings.cs index 4d5b683..000bb5e 100644 --- a/CharaStudioVR/Settings/CharaStudioSettings.cs +++ b/CharaStudioVR/Settings/CharaStudioSettings.cs @@ -52,15 +52,21 @@ public override Shortcuts Shortcuts protected set => base.Shortcuts = value; } - public CharaStudioSettings() + public float NearClipPlane { - IPDScale = 1f; - GrabRotationImmediateMode = false; + get => _NearClipPlane; + set + { + _NearClipPlane = value; + TriggerPropertyChanged("NearClipPlane"); + } } - public static CharaStudioSettings Load(string path) + private float _NearClipPlane; + + public CharaStudioSettings() { - return VRSettings.Load(path); + GrabRotationImmediateMode = false; } } } diff --git a/CharaStudioVR/Settings/CharaStudioSettingsManager.cs b/CharaStudioVR/Settings/CharaStudioSettingsManager.cs new file mode 100644 index 0000000..ba1447f --- /dev/null +++ b/CharaStudioVR/Settings/CharaStudioSettingsManager.cs @@ -0,0 +1,93 @@ +using System; +using BepInEx.Configuration; +using KKAPI.Utilities; +using VRGIN.Core; + +namespace KKS_VR.Settings +{ + /// + /// Manages configuration and keeps it up to date. + /// + /// BepInEx wants us to store the config in a bunch of ConfigEntry objects, + /// but VRGIN wants it stored inside a class inheriting VRSettings. So + /// our plan is: + /// + /// * We have both ConfigEntry objects and CharaStudioSettings around. + /// * The ConfigEntry objects are the master copy and the CharaStudioSettings + /// object is a mirror. + /// * CharaStudioSettingsManager is responsible for keeping CharaStudioSettings up to date. + /// * No other parts of code should modify CharaStudioSettings. In fact, there + /// are code paths where VRGIN tries to modify it. We simply attempt + /// to avoid executing those code paths. + /// + internal class CharaStudioSettingsManager + { + public const string SectionGeneral = "0. General"; + + /// + /// Create config entries under the given ConfigFile. Also create a fresh + /// CharaStudioSettings object and arrange that it be synced with the config + /// entries. + /// + /// The new CharaStudioSettings object. + public static CharaStudioSettings Create(ConfigFile config) + { + var settings = new CharaStudioSettings(); + + var ipdScale = config.Bind(SectionGeneral, "IPD Scale", 1f, + new ConfigDescription( + "Scale of the camera. The higher, the more gigantic the player is.", + new AcceptableValueRange(0.25f, 4f))); + Tie(ipdScale, v => settings.IPDScale = v); + + var rumble = config.Bind(SectionGeneral, "Rumble", true, + "Whether or not rumble is activated."); + Tie(rumble, v => settings.Rumble = v); + + var rotationMultiplier = config.Bind(SectionGeneral, "Rotation multiplier", 1f, + new ConfigDescription( + "How quickly the the view should rotate when doing so with the controllers.", + new AcceptableValueRange(-4f, 4f), + new ConfigurationManagerAttributes { Order = -1 })); + Tie(rotationMultiplier, v => settings.RotationMultiplier = v); + + var logLevel = config.Bind(SectionGeneral, "Log level", VRLog.LogMode.Info, + new ConfigDescription( + "The minimum severity for a message to be logged.", + null, + new ConfigurationManagerAttributes { IsAdvanced = true })); + Tie(logLevel, v => VRLog.Level = v); + + var nearClipPlane = config.Bind(SectionGeneral, "Near clip plane", 0.002f, + new ConfigDescription( + "Minimum distance from camera for an object to be shown (causes visual glitches on some maps when set too small)", + new AcceptableValueRange(0.001f, 0.2f))); + Tie(nearClipPlane, v => settings.NearClipPlane = v); + + // not used for anything + var lockRotXZ = config.Bind(SectionGeneral, "Lock XZ Axis rotation", true, + new ConfigDescription("Lock XZ Axis (pitch / roll) rotation.")); + Tie(lockRotXZ, v => settings.LockRotXZ = v); + + var maxVoiceDistance = config.Bind(SectionGeneral, "Max Voice distance", 300f, + new ConfigDescription( + "Max Voice distance (in unit. 300 = 30m in real (HS2 uses 10 unit = 1m scale).", + new AcceptableValueRange(100f, 600f))); + Tie(maxVoiceDistance, v => settings.MaxVoiceDistance = v); + + var minVoiceDistance = config.Bind(SectionGeneral, "Min Voice distance", 7f, + new ConfigDescription( + "Min Voice distance (in unit. 7 = 70 cm in real (HS2 uses 10 unit = 1m scale).", + new AcceptableValueRange(1f, 70f))); + Tie(minVoiceDistance, v => settings.MinVoiceDistance = v); + + return settings; + } + + private static void Tie(ConfigEntry entry, Action set) + { + set(entry.Value); + entry.SettingChanged += (_, _1) => set(entry.Value); + } + } +} diff --git a/CharaStudioVR/VRPlugin.cs b/CharaStudioVR/VRPlugin.cs index 0f5fd55..725d03b 100644 --- a/CharaStudioVR/VRPlugin.cs +++ b/CharaStudioVR/VRPlugin.cs @@ -39,11 +39,12 @@ public void Awake() { BepInExVrLogBackend.ApplyYourself(); OpenVRHelperTempfixHook.Patch(); - StartCoroutine(LoadDevice()); + var settings = CharaStudioSettingsManager.Create(Config); + StartCoroutine(LoadDevice(settings)); } } - private IEnumerator LoadDevice() + private IEnumerator LoadDevice(CharaStudioSettings settings) { // For some reason using Scene.LoadSceneName instead of SceneManager will break the background color, probably some timing issue yield return new WaitUntil(() => Manager.Scene.initialized && SceneManager.GetActiveScene().name == "Studio"); @@ -110,7 +111,11 @@ private IEnumerator LoadDevice() TopmostToolIcons.Patch(); - VRManager.Create(CharaStudioContext.GetContext()); + VRManager.Create(new CharaStudioContext(settings)); + + // VRGIN doesn't update the near clip plane until a first "main" camera is created, so we set it here. + UpdateNearClipPlane(settings); + settings.AddListener("NearClipPlane", (_, _1) => UpdateNearClipPlane(settings)); VR.Manager.SetMode(); @@ -129,6 +134,11 @@ private IEnumerator LoadDevice() base.Logger.LogInfo("Finished loading into VR mode!"); } + private void UpdateNearClipPlane(CharaStudioSettings settings) + { + VR.Camera.gameObject.GetComponent().nearClipPlane = settings.NearClipPlane; + } + private static class NativeMethods { [DllImport("user32.dll")]