├── .gitattributes ├── .gitignore ├── LICENSE ├── README.md ├── osu.XR.Tests ├── DragableDrawable.cs ├── OsuTestBrowser3D.cs ├── OsuTestScene3D.cs ├── TestSceneChangelog.cs ├── TestSceneFileHierarchy.cs ├── TestSceneFormatedText.cs ├── TestSceneHierarchyInspectorV2.cs ├── TestSceneInspector.cs ├── TestSceneOBJImport.cs ├── TestSceneSampleScene.cs ├── VisualTestRunner.cs └── osu.XR.Tests.csproj ├── osu.XR ├── BindableHSVColor.cs ├── Components │ ├── BeatingScenery.cs │ ├── DustEmitter.cs │ ├── FloorGrid.cs │ ├── Foot.cs │ ├── Groups │ │ ├── BeatingGroup.cs │ │ ├── HandheldMenu.cs │ │ ├── IHasIcon.cs │ │ ├── IHasName.cs │ │ ├── MenuStack.cs │ │ ├── PopoutPanelStack.cs │ │ └── RegularPanelStack.cs │ ├── IFocusable.cs │ ├── Panels │ │ ├── CurvedPanel.cs │ │ ├── FlatPanel.cs │ │ └── InteractivePanel.cs │ ├── Player.cs │ ├── Skyboxes │ │ ├── LightsOutSkyBox.cs │ │ ├── SkyBox.cs │ │ ├── SkyBoxType.cs │ │ └── SolidSkyBox.cs │ └── TeleportVisual.cs ├── DefaultBindings │ ├── knuckles.json │ ├── oculus_touch.json │ └── vive_controller.json ├── Drawables │ ├── BeatProvider.cs │ ├── CalmOsuAnimatedButton.cs │ ├── Containers │ │ ├── AutoScrollContainer.cs │ │ ├── BeatSyncedFlashingDrawable.cs │ │ ├── ConfigurationContainer.cs │ │ ├── ExpandableSection.cs │ │ ├── FilterableContainer.cs │ │ ├── FilterableFillFlowContainer.cs │ │ ├── FormatedText.cs │ │ ├── LazyExpandableSection.cs │ │ ├── NamedContainer.cs │ │ └── OsuXrTooltipContainer.cs │ ├── FileHierarchyView.cs │ ├── HierarchyView.cs │ └── UserInterface │ │ ├── ColorPicker.cs │ │ ├── ColorPickerControl.HuePicker.cs │ │ ├── ColorPickerControl.SVPicker.cs │ │ └── ColorPickerControl.cs ├── Editor │ ├── GripGroup.cs │ ├── IGripable.cs │ ├── PropContainer.cs │ ├── SceneComponentEditor.cs │ └── SceneContainer.cs ├── Extensions.cs ├── GamePhysicsLayer.cs ├── Graphics │ └── NeonColors.cs ├── Input │ ├── Custom │ │ ├── ButtonBinding.cs │ │ ├── ClapBinding.cs │ │ ├── Components │ │ │ ├── ActivationIndicator.cs │ │ │ ├── BoundComponent.cs │ │ │ ├── JoystickVisual.cs │ │ │ ├── RulesetActionBinding.cs │ │ │ └── RulesetActionDropdown.cs │ │ ├── CustomBinding.cs │ │ ├── InjectedInput.cs │ │ ├── JoystickBinding.cs │ │ └── Persistence │ │ │ ├── ActionBinding.cs │ │ │ ├── BindingData.cs │ │ │ ├── RulesetBindings.cs │ │ │ ├── RulesetBindingsFile.cs │ │ │ ├── RulesetVariantBindings.cs │ │ │ └── SaveDataContext.cs │ ├── KeyboardLayout.cs │ ├── Pointers │ │ ├── Pointer.cs │ │ ├── RaycastPointer.cs │ │ └── TouchPointer.cs │ ├── XrController.cs │ └── XrKeyboard.cs ├── Inspector │ ├── Editors │ │ ├── Color4Editor.cs │ │ ├── EnumEditor.cs │ │ ├── TextfieldEditor.cs │ │ ├── ToggleEditor.cs │ │ ├── ValueEditor.cs │ │ ├── Vector2Editor.cs │ │ └── Vector3Editor.cs │ ├── HierarchyInspector.cs │ ├── IInspectable.cs │ ├── ReflectedValue.cs │ ├── Reflections │ │ ├── Extensions.cs │ │ ├── ReflectionsInspector.cs │ │ ├── ReflectionsInspectorComponent.cs │ │ ├── ReflectionsInspectorComposite.cs │ │ └── ReflectionsInspectorPrimitive.cs │ ├── Selection2D.cs │ └── Selection3D.cs ├── OsuGameXr.cs ├── Panels │ ├── ChangelogPanel.cs │ ├── ConfigPanel.cs │ ├── Drawables │ │ ├── ChangelogDrawable.cs │ │ ├── InspectorDrawable.cs │ │ ├── NotificationsDrawable.cs │ │ ├── RulesetInfoDrawable.cs │ │ ├── SceneManagerDrawable.cs │ │ └── VRConfigDrawable.cs │ ├── HandheldPanel.cs │ ├── InspectorPanel.cs │ ├── NotificationsPanel.cs │ ├── OsuPanel.cs │ ├── Overlays │ │ ├── FileSelectionOverlay.cs │ │ ├── PanelOverlay.cs │ │ └── PanelOverlayContainer.cs │ ├── RulesetInfoPanel.cs │ └── SceneManagerPanel.cs ├── Program.cs ├── ReflectionExtensions.cs ├── Resources │ ├── changelog.txt │ ├── keyboard.obj │ ├── keyboard_flat.obj │ ├── paw.obj │ ├── selection.obj │ ├── shoe.obj │ └── shpere.obj ├── SceneWithMirrorWarning.cs ├── Settings │ ├── Hand.cs │ ├── InputMode.cs │ ├── Sections │ │ ├── GraphicsSettingSection.cs │ │ ├── InputSettingSection.cs │ │ ├── PresetsSection.cs │ │ └── SettingsSection.cs │ ├── SettingsPreset.cs │ ├── XrConfigManager.cs │ └── XrConfigSetting.cs ├── TextureGeneration.cs ├── Textures │ └── dust.png ├── UnreachableCodeException.cs ├── XrAction.cs ├── osu.XR.csproj └── osu.XR.csproj.user └── osu.sln /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | bin/ 3 | obj/ 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Flutterish 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # osu!XR 2 | Rhythmic immersion on another level. 3 | 4 | osu!XR is a VR port of osu!lazer. 5 | 6 | If you would like to receive notifications when a new o!xr release is published and/or join the discussion about it, [join our osu ruleset discord server](discord.gg/QGqD3eBx7b) and ask for an osu-xr role at #osu-xr which is pinged when a new release is published. 7 | 8 | If you wish to support the development of o!xr, you can support me at https://ko-fi.com/perigee <3 9 | # Features 10 | osu!XR allows you to play all the official and custom rulesets osu!lazer does. 11 | 12 | You can play with one or two pointers on a big screen or use your hands to control osu with a touchscreen. 13 | 14 | Don't limit yourself to one screen resolution - you can adjust that in XR settings. 15 | 16 | # Bindings 17 | Default controller bindings exist for: 18 | * Valve Index 19 | * Oculus Quest 2 20 | * HTC Vive 21 | 22 | If you use a different controller and made good bindings, make sure you contribute them to the project! 23 | 24 | # ⚠️ WARNING ⚠️ 25 | oxr is currently not in best shape. There are bugs, naming inconsistencies and awkward limitations. This codebase will undergo an overhaul, which means na major updates for a while. 26 | 27 | The project is also currently linked to local copies of osu!lazer (as its needed to embed the 4 standard game modes) and o!f-xr (because I didn't make a nuget yet). If you wish to contribute, you will need to clone them too for the time being. 28 | -------------------------------------------------------------------------------- /osu.XR.Tests/DragableDrawable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.Framework.Input.Events; 4 | 5 | namespace osu.XR.Tests { 6 | public class DragableDrawable : CompositeDrawable { 7 | Drawable drawable; 8 | 9 | public DragableDrawable ( Drawable drawable ) { 10 | AutoSizeAxes = Axes.Both; 11 | AddInternal( this.drawable = drawable ); 12 | } 13 | 14 | protected override void Update () { 15 | base.Update(); 16 | 17 | Origin = drawable.Origin; 18 | Anchor = drawable.Anchor; 19 | } 20 | 21 | protected override bool OnDragStart ( DragStartEvent e ) { 22 | return true; 23 | } 24 | 25 | protected override void OnDrag ( DragEvent e ) { 26 | Position += e.Delta; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /osu.XR.Tests/OsuTestBrowser3D.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Cursor; 5 | using osu.Framework.IO.Stores; 6 | using osu.Framework.Platform; 7 | using osu.Framework.Testing; 8 | using osu.Framework.XR.Materials; 9 | using osu.Game.Tests; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.Diagnostics.CodeAnalysis; 13 | using System.Linq; 14 | using System.Text; 15 | using System.Threading.Tasks; 16 | 17 | namespace osu.XR.Tests { 18 | public class OsuTestBrowser3D : OsuTestBrowser { 19 | [NotNull, MaybeNull] 20 | private DependencyContainer dependencies; 21 | [NotNull, MaybeNull] 22 | private MaterialManager MaterialManager; 23 | 24 | [BackgroundDependencyLoader] 25 | private void load () { 26 | Resources.AddStore( new DllResourceStore( osu.Framework.XR.Resources.ResourceAssembly ) ); 27 | 28 | var resources = new ResourceStore(); 29 | resources.AddStore( new NamespacedResourceStore( Resources, @"Shaders" ) ); 30 | resources.AddStore( new NamespacedResourceStore( Resources, @"Shaders/Materials" ) ); 31 | MaterialManager = new MaterialManager( resources ); 32 | dependencies.CacheAs( MaterialManager ); 33 | 34 | Child = new SafeAreaContainer { 35 | RelativeSizeAxes = Axes.Both, 36 | Child = new DrawSizePreservingFillContainer { 37 | Children = new Drawable[] 38 | { 39 | new TestBrowser(), 40 | new CursorContainer(), 41 | }, 42 | } 43 | }; 44 | } 45 | 46 | public override void SetHost ( GameHost host ) { 47 | base.SetHost( host ); 48 | host.Window.CursorState |= CursorState.Hidden; 49 | } 50 | 51 | protected override IReadOnlyDependencyContainer CreateChildDependencies ( IReadOnlyDependencyContainer parent ) => 52 | dependencies = new DependencyContainer( base.CreateChildDependencies( parent ) ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /osu.XR.Tests/OsuTestScene3D.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.XR.Components; 3 | using osu.Framework.XR.Testing; 4 | using osu.Game.Tests.Visual; 5 | 6 | namespace osu.XR.Tests { 7 | public abstract class OsuTestScene3D : OsuTestScene { 8 | protected readonly Scene Scene; 9 | 10 | public OsuTestScene3D () { 11 | Add( Scene = new TestingScene() ); 12 | } 13 | 14 | public override void Add ( Drawable drawable ) { 15 | if ( drawable is Drawable3D d3 ) 16 | Scene.Add( d3 ); 17 | else 18 | base.Add( drawable ); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /osu.XR.Tests/TestSceneChangelog.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Game.Tests.Visual; 3 | using osu.XR.Drawables; 4 | using osuTK; 5 | 6 | namespace osu.XR.Tests { 7 | public class TestSceneChangelog : OsuTestScene { 8 | protected override void LoadComplete () { 9 | base.LoadComplete(); 10 | 11 | Add( new DragableDrawable( new ChangelogDrawable { 12 | Size = new Vector2( 400, 500 ), 13 | Anchor = Anchor.Centre, 14 | Origin = Anchor.Centre 15 | } ) ); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /osu.XR.Tests/TestSceneFileHierarchy.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Game.Tests.Visual; 3 | using osu.XR.Drawables; 4 | using osu.XR.Drawables.Containers; 5 | using osuTK; 6 | 7 | namespace osu.XR.Tests { 8 | public class TestSceneFileHierarchy : OsuTestScene { 9 | FileHierarchyPanel hierarchy; 10 | 11 | protected override void LoadComplete () { 12 | base.LoadComplete(); 13 | 14 | Add( new DragableDrawable( hierarchy = new FileHierarchyPanel() { 15 | Size = new Vector2( 400, 500 ), 16 | Anchor = Anchor.Centre, 17 | Origin = Anchor.Centre 18 | } ) ); 19 | 20 | AddToggleStep( "IsMultiselect", v => hierarchy.preview.IsMultiselect = v ); 21 | AddToggleStep( "SelectionNavigates", v => hierarchy.preview.SelectionNavigates = v ); 22 | } 23 | } 24 | 25 | public class FileHierarchyPanel : ConfigurationContainer { 26 | public FileHierarchyViewWithPreview preview; 27 | public FileHierarchyPanel () { 28 | Title = "Files"; 29 | Description = "select some files"; 30 | 31 | AddSection( preview = new FileHierarchyViewWithPreview { IsMultiselect = true, SelectionNavigates = false }, name: "Files" ); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /osu.XR.Tests/TestSceneFormatedText.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Testing; 3 | 4 | namespace osu.XR.Tests { 5 | public class TestSceneFormatedText : TestScene { 6 | public TestSceneFormatedText () { 7 | Add( new SceneWithMirrorWarning() { 8 | RelativeSizeAxes = Axes.Both 9 | } ); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.XR.Tests/TestSceneHierarchyInspectorV2.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.XR.Drawables.Containers; 3 | using osu.XR.Inspector; 4 | using osuTK; 5 | 6 | namespace osu.XR.Tests { 7 | public class TestSceneHierarchyInspectorV2 : OsuTestScene3D { 8 | HierarchyInspectorPanel inspector; 9 | TestComponent component; 10 | 11 | protected override void LoadComplete () { 12 | base.LoadComplete(); 13 | 14 | Scene.Add( component = new TestComponent() ); 15 | Add( new DragableDrawable( inspector = new HierarchyInspectorPanel( component ) { 16 | Size = new Vector2( 400, 500 ), 17 | Anchor = Anchor.Centre, 18 | Origin = Anchor.Centre 19 | } ) ); 20 | 21 | AddToggleStep( "IsMultiselect", v => inspector.preview.IsMultiselect = v ); 22 | AddToggleStep( "SelectionNavigates", v => inspector.preview.SelectionNavigates = v ); 23 | } 24 | } 25 | 26 | public class HierarchyInspectorPanel : ConfigurationContainer { 27 | public HierarchyInspector preview; 28 | public HierarchyInspectorPanel ( Drawable drawable ) { 29 | Title = "Hierarchy"; 30 | Description = "browse drawables"; 31 | 32 | AddSection( preview = new HierarchyInspector( drawable ), name: "Hierarchy" ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /osu.XR.Tests/TestSceneInspector.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.XR.Components; 4 | using osu.Framework.XR.Graphics; 5 | using osu.Game.Overlays.Settings; 6 | using osu.XR.Drawables.Containers; 7 | using osu.XR.Inspector; 8 | using osuTK; 9 | 10 | namespace osu.XR.Tests { 11 | public class TestSceneInspector : OsuTestScene3D { 12 | InspectorDrawable inspector; 13 | TestComponent component; 14 | 15 | protected override void LoadComplete () { 16 | base.LoadComplete(); 17 | Add( new DragableDrawable( inspector = new InspectorDrawable { 18 | Size = new Vector2( 400, 500 ), 19 | Anchor = Anchor.Centre, 20 | Origin = Anchor.Centre 21 | } ) ); 22 | 23 | Scene.Add( component = new TestComponent() ); 24 | AddStep( "Inspect element", () => inspector.InspectedElementBindable.Value = component ); 25 | } 26 | } 27 | 28 | class TestComponent : Container3D, IConfigurableInspectable, IHasInspectorVisuals, IChildrenNotInspectable, IExperimental { 29 | public TestComponent () { 30 | Add( new Model { Mesh = Mesh.UnitCube } ); 31 | X = -2; 32 | } 33 | 34 | public Drawable CreateInspectorSubsection () { 35 | return new NamedContainer { 36 | DisplayName = "Sample settings", 37 | Children = new Drawable[] { 38 | new SettingsSlider { 39 | Current = new BindableDouble { MinValue = 0, MaxValue = 10 }, 40 | LabelText = "Sample setting" 41 | } 42 | } 43 | }; 44 | } 45 | 46 | public bool AreSettingsPersistent => true; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /osu.XR.Tests/TestSceneOBJImport.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.XR.Panels.Drawables; 3 | using osu.XR.Panels.Overlays; 4 | using osuTK; 5 | 6 | namespace osu.XR.Tests { 7 | public class TestSceneOBJImport : OsuTestScene3D { 8 | SceneManagerDrawable scenePanel; 9 | 10 | protected override void LoadComplete () { 11 | base.LoadComplete(); 12 | Add( new DragableDrawable( new PanelOverlayContainer { 13 | Size = new Vector2( 400, 500 ) * 1.2f, 14 | Child = scenePanel = new SceneManagerDrawable { 15 | RelativeSizeAxes = Axes.Both, 16 | Anchor = Anchor.Centre, 17 | Origin = Anchor.Centre 18 | }, 19 | Anchor = Anchor.Centre, 20 | Origin = Anchor.Centre 21 | } ) ); 22 | 23 | scenePanel.SceneContainer = new Editor.SceneContainer(); 24 | Scene.Add( scenePanel.SceneContainer ); 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /osu.XR.Tests/TestSceneSampleScene.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.XR.Components; 2 | 3 | namespace osu.XR.Tests { 4 | public class TestSceneSampleScene : OsuTestScene3D { 5 | protected override void LoadComplete () { 6 | base.LoadComplete(); 7 | 8 | Scene.Add( new TestComponent() ); 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /osu.XR.Tests/VisualTestRunner.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework; 2 | using osu.Framework.IO.Stores; 3 | using osu.Framework.Platform; 4 | using osu.Game.Tests; 5 | using System; 6 | 7 | namespace osu.XR.Tests { 8 | public static class VisualTestRunner { 9 | [STAThread] 10 | public static int Main ( string[] args ) { 11 | using ( DesktopGameHost host = Host.GetSuitableHost( @"osu", true ) ) { 12 | var browser = new OsuTestBrowser3D(); 13 | host.Run( browser ); 14 | return 0; 15 | } 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /osu.XR.Tests/osu.XR.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | osu.XR.Tests.VisualTestRunner 4 | 5 | 6 | net6.0 7 | preview 8 | false 9 | WinExe 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /osu.XR/BindableHSVColor.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osuTK.Graphics; 3 | using System; 4 | 5 | namespace osu.XR.Drawables.UserInterface { 6 | public class BindableHSVColor { 7 | public readonly Bindable Color = new(); 8 | public readonly Bindable H = new(); 9 | public readonly Bindable S = new(); 10 | public readonly Bindable V = new(); 11 | 12 | public BindableHSVColor ( Color4 defaultValue = default ) { 13 | Color.Default = defaultValue; 14 | Color.Value = defaultValue; 15 | recomputeHSV(); 16 | H.Default = H.Value; 17 | S.Default = S.Value; 18 | V.Default = V.Value; 19 | 20 | Action> action = _ => { 21 | if ( isLocked ) return; 22 | isLocked = true; 23 | recomputeColor(); 24 | isLocked = false; 25 | }; 26 | H.ValueChanged += action; 27 | S.ValueChanged += action; 28 | V.ValueChanged += action; 29 | 30 | Color.BindValueChanged( v => { 31 | if ( isLocked ) return; 32 | isLocked = true; 33 | recomputeHSV(); 34 | isLocked = false; 35 | } ); 36 | } 37 | 38 | void recomputeColor () { 39 | var ch = TextureGeneration.FromHSV( H.Value, S.Value, V.Value ); 40 | Color.Value = new Color4( ch.R, ch.G, ch.B, 255 ); 41 | } 42 | 43 | void recomputeHSV () { 44 | var goal = TextureGeneration.RGBToHSV( new osuTK.Vector3( Color.Value.R, Color.Value.G, Color.Value.B ) ); 45 | H.Value = goal.X; 46 | S.Value = goal.Y; 47 | V.Value = goal.Z; 48 | } 49 | 50 | private bool isLocked; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /osu.XR/Components/BeatingScenery.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.XR.Components; 3 | using osu.Framework.XR.Graphics; 4 | using osu.Framework.XR.Maths; 5 | using osu.XR.Components.Groups; 6 | using osu.XR.Editor; 7 | using osuTK; 8 | using System; 9 | 10 | namespace osu.XR.Components { 11 | public class BeatingScenery : CompositeDrawable3D { 12 | public BeatingScenery ( int? seed = 0 ) { 13 | Random random = seed.HasValue ? new( seed.Value ) : new(); 14 | 15 | double next () => random.NextDouble( 0.02, 0.1 ); 16 | for ( double theta = next(); theta < Math.PI * 2; theta += next() ) { 17 | if ( random.Chance( 0.2 ) ) { 18 | double radius = random.NextDouble( 4, 7 ); 19 | double x = Math.Cos( theta ) * radius; 20 | double y = Math.Sin( theta ) * radius; 21 | 22 | AddInternal( new BeatingGroup { Position = new Vector3( (float)x, 0, (float)y ), Child = new Collider { 23 | Mesh = Mesh.UnitCube, 24 | AutoOffsetOriginY = -0.5f, 25 | Scale = new Vector3( (float)(random.NextDouble(0.05,0.2) * radius) ), 26 | Rotation = Quaternion.FromAxisAngle( Vector3.UnitY, (float)random.NextDouble( Math.PI * 2 ) ) 27 | } } ); 28 | } 29 | } 30 | } 31 | 32 | public class GripableCollider : Collider, IGripable { 33 | public Bindable CanBeGripped { get; } = new( true ); 34 | public Bindable AllowsGripMovement { get; } = new( true ); 35 | public Bindable AllowsGripScaling { get; } = new( true ); 36 | public Bindable AllowsGripRotation { get; } = new( true ); 37 | 38 | public void OnGripped ( object source, GripGroup group ) { } 39 | public void OnGripReleased ( object source, GripGroup group ) { } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /osu.XR/Components/DustEmitter.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Sprites; 5 | using osu.Framework.Graphics.Textures; 6 | using osu.Framework.Utils; 7 | using osu.Framework.XR.Components; 8 | using osu.Framework.XR.Maths; 9 | using osu.Game.Overlays.Settings; 10 | using osu.XR.Inspector; 11 | using osu.XR.Settings; 12 | using osu.XR.Settings.Sections; 13 | using osuTK; 14 | 15 | namespace osu.XR.Components { 16 | public class DustEmitter : ParticleEmitter, IConfigurableInspectable { 17 | protected override DustParticle CreateParticle () 18 | => new(); 19 | 20 | double emitTimer; 21 | double emitInterval = 5; 22 | protected override void Update () { 23 | base.Update(); 24 | 25 | if ( !showDust.Value ) return; 26 | 27 | if ( ActiveParticles < 300 ) { 28 | emitTimer += Time.Elapsed; 29 | } 30 | else emitTimer = 0; 31 | while ( ActiveParticles < 300 && emitTimer > emitInterval ) { 32 | Emit(); 33 | emitTimer -= emitInterval; 34 | } 35 | } 36 | 37 | Bindable showDust = new( true ); 38 | 39 | [BackgroundDependencyLoader] 40 | private void load ( XrConfigManager config ) { 41 | config.BindWith( XrConfigSetting.ShowDust, showDust ); 42 | } 43 | 44 | public Drawable CreateInspectorSubsection () { 45 | return new SettingsSectionContainer { 46 | Title = "Dust Particles", 47 | Icon = FontAwesome.Solid.Star, 48 | Children = new Drawable[] { 49 | new SettingsCheckbox { Current = showDust, LabelText = "Show Dust Particles" } 50 | } 51 | }; 52 | } 53 | public bool AreSettingsPersistent => true; 54 | } 55 | 56 | public class DustParticle : ParticleEmiter.Particle { 57 | public DustParticle () { 58 | ShouldBeDepthSorted = true; 59 | AlwaysPresent = true; 60 | } 61 | 62 | [Resolved] 63 | private Player player { get; set; } 64 | 65 | [BackgroundDependencyLoader] 66 | private void load ( TextureStore textures ) { 67 | MainTexture = textures.Get( "dust" ).TextureGL; 68 | } 69 | 70 | protected override void OnApply ( ParticleEmiter emmiter ) { 71 | base.OnApply( emmiter ); 72 | 73 | this.FadeInFromZero( 400, Easing.Out ).Then().FadeOut( 800, Easing.In ).Then().Schedule( () => Release() ); 74 | this.MoveTo( new Vector3( RNG.NextSingle( 0.5f, 5 ).CopySign( RNG.NextSingle(-1,1) ), RNG.NextSingle( 0, 6 ), RNG.NextSingle( 0.5f, 5 ).CopySign( RNG.NextSingle( -1, 1 ) ) ) + player.GlobalPosition.With( y: 0 ) ) 75 | .MoveToOffset( new Vector3( RNG.NextSingle( -1, 1 ), RNG.NextSingle( -1, 1 ), RNG.NextSingle( -1, 1 ) ) * 0.1f, 1200 ); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /osu.XR/Components/FloorGrid.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Framework.XR.Components; 5 | using osu.Framework.XR.Extensions; 6 | using osu.Framework.XR.Graphics; 7 | using osu.Framework.XR.Maths; 8 | using osu.Game.Overlays.Settings; 9 | using osu.XR.Drawables; 10 | using osu.XR.Drawables.UserInterface; 11 | using osu.XR.Inspector; 12 | using osu.XR.Settings.Sections; 13 | using osuTK; 14 | using osuTK.Graphics; 15 | 16 | namespace osu.XR.Components { 17 | /// 18 | /// White line grid on the floor with a fade. 19 | /// 20 | public class FloorGrid : Model, IConfigurableInspectable { 21 | public readonly Bindable TintBindable = new( Color4.White ); 22 | public readonly BindableFloat OpacityBindable = new( 1 ) { MinValue = 0, MaxValue = 1 }; 23 | public readonly BindableInt XSegmentsBindable = new( 7 ) { MinValue = 0, MaxValue = 20, Precision = 1 }; 24 | public readonly BindableInt ZSegmentsBindable = new( 7 ) { MinValue = 0, MaxValue = 20, Precision = 1 }; 25 | public readonly BindableFloat SegmentWidthBindable = new( 0.01f ) { MinValue = 0.001f, MaxValue = 0.05f }; 26 | public readonly BindableFloat SegmentSpreadBindable = new( 1 ) { MinValue = 0.1f, MaxValue = 2 }; 27 | public readonly BindableFloat SegmentLengthBindable = new( 16.7f ) { MinValue = 5, MaxValue = 50 }; 28 | 29 | public FloorGrid () { 30 | MainTexture = Textures.Vertical2SidedGradient( Color4.Transparent, Color4.White, 200 ).TextureGL; 31 | 32 | TintBindable.BindValueChanged( v => Tint = v.NewValue, true ); 33 | OpacityBindable.BindValueChanged( v => Alpha = v.NewValue, true ); 34 | 35 | (XSegmentsBindable, ZSegmentsBindable, SegmentWidthBindable, SegmentSpreadBindable, SegmentLengthBindable).BindValuesChanged( recalcualteMesh, true ); 36 | } 37 | 38 | void recalcualteMesh () { 39 | recalcualteMesh( 40 | XSegmentsBindable.Value, 41 | ZSegmentsBindable.Value, 42 | SegmentWidthBindable.Value, 43 | SegmentSpreadBindable.Value, 44 | SegmentSpreadBindable.Value, 45 | SegmentLengthBindable.Value, 46 | SegmentLengthBindable.Value 47 | ); 48 | } 49 | 50 | void recalcualteMesh ( int x_segments, int z_segments, float width, float x_spread, float z_spread, float x_length, float z_length ) { 51 | Mesh = new Mesh { IsReady = false }; 52 | 53 | for ( int x = -x_segments; x <= x_segments; x++ ) { 54 | float xFrom = x * x_spread - width / 2; 55 | float xTo = x * x_spread + width / 2; 56 | float zFrom = x_length * -0.5f; 57 | float zTo = x_length * 0.5f; 58 | Mesh.AddQuad( new Quad( 59 | new Vector3( xFrom, 0, zFrom ), new Vector3( xFrom, 0, zTo ), 60 | new Vector3( xTo, 0, zFrom ), new Vector3( xTo, 0, zTo ) 61 | ), new Vector2( 1, 0 ), new Vector2( 1, 1 ), new Vector2( 0, 0 ), new Vector2( 0, 1 ) ); 62 | } 63 | 64 | for ( int z = -z_segments; z <= z_segments; z++ ) { 65 | float xFrom = z_length * -0.5f; 66 | float xTo = z_length * 0.5f; 67 | float zFrom = z * z_spread - width / 2; 68 | float zTo = z * z_spread + width / 2; 69 | Mesh.AddQuad( new Quad( 70 | new Vector3( xFrom, 0, zFrom ), new Vector3( xFrom, 0, zTo ), 71 | new Vector3( xTo, 0, zFrom ), new Vector3( xTo, 0, zTo ) 72 | ), new Vector2( 0, 1 ), new Vector2( 1, 1 ), new Vector2( 0, 0 ), new Vector2( 1, 0 ) ); 73 | } 74 | 75 | Mesh.IsReady = true; 76 | } 77 | 78 | public Drawable CreateInspectorSubsection () { 79 | return new SettingsSectionContainer { 80 | Title = "Floor Grid", 81 | Icon = FontAwesome.Solid.ShoePrints, 82 | Children = new Drawable[] { 83 | new ColorPicker { LabelText = "Tint", Current = TintBindable }, 84 | new SettingsSlider { LabelText = "Opacity", Current = OpacityBindable }, 85 | new SettingsSlider { LabelText = "X Segments", Current = XSegmentsBindable }, 86 | new SettingsSlider { LabelText = "Z Segments", Current = ZSegmentsBindable }, 87 | new SettingsSlider { LabelText = "Segment Spread", Current = SegmentSpreadBindable }, 88 | new SettingsSlider { LabelText = "Segment Width", Current = SegmentWidthBindable }, 89 | new SettingsSlider { LabelText = "Segment Length", Current = SegmentLengthBindable } 90 | } 91 | }; 92 | } 93 | public bool AreSettingsPersistent => false; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /osu.XR/Components/Foot.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.XR.Components; 4 | using osu.Framework.XR.Extensions; 5 | using osuTK; 6 | 7 | namespace osu.XR.Components { 8 | public class Foot : Model { 9 | public readonly Bindable TargetPosition = new Bindable(); 10 | public readonly Bindable TargetRotation = new Bindable(); 11 | public readonly Bindable PositionToleranceBindable = new Bindable( 0.22f ); 12 | public readonly Bindable RotationToleranceBindable = new Bindable( 0.6f ); 13 | 14 | Vector3 goalPosition = Vector3.Zero; 15 | Quaternion goalRotation = Quaternion.Identity; 16 | 17 | public Foot () { 18 | (TargetPosition, PositionToleranceBindable).BindValuesChanged( (pos, tol) => { 19 | if ( ( goalPosition - pos ).Length > tol ) { 20 | this.MoveTo( pos, 200, Easing.Out ); 21 | 22 | goalPosition = pos; 23 | } 24 | }, true ); 25 | 26 | (TargetRotation, RotationToleranceBindable).BindValuesChanged( (rot, tol) => { 27 | var current = ( goalRotation * new Vector4( 0, 0, 1, 1 ) ).Xyz; 28 | var goal = ( rot * new Vector4( 0, 0, 1, 1 ) ).Xyz; 29 | if ( ( goal - current ).Length > tol ) { 30 | this.RotateTo( rot, 100 ); 31 | 32 | goalRotation = rot; 33 | } 34 | }, true ); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /osu.XR/Components/Groups/BeatingGroup.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.XR.Components; 5 | using osu.XR.Drawables; 6 | using osuTK; 7 | 8 | namespace osu.XR.Components.Groups { 9 | public class BeatingGroup : Container3D { 10 | public readonly Bindable BindableBeat = new(); 11 | 12 | public BeatingGroup () { 13 | BindableBeat.ValueChanged += v => { 14 | FinishTransforms(); 15 | this.ScaleTo( new Vector3( (float)( 1 - v.NewValue.AverageAmplitude * 0.06 ) ), 50 ).Then().ScaleTo( Vector3.One, 100 ); 16 | }; 17 | } 18 | 19 | [BackgroundDependencyLoader] 20 | private void load ( BeatProvider beat ) { 21 | BindableBeat.BindTo( beat.BindableBeat ); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /osu.XR/Components/Groups/HandheldMenu.cs: -------------------------------------------------------------------------------- 1 | using OpenVR.NET; 2 | using OpenVR.NET.Manifests; 3 | using osu.Framework.Allocation; 4 | using osu.Framework.Bindables; 5 | using osu.Framework.XR.Components; 6 | using osu.XR.Components.Panels; 7 | using osu.XR.Input; 8 | using osu.XR.Settings; 9 | using osuTK; 10 | using System; 11 | using System.Linq; 12 | 13 | namespace osu.XR.Components.Groups { 14 | public class HandheldMenu : PopoutPanelStack { 15 | Bindable inputModeBindable = new(); 16 | [Resolved] 17 | private OsuGameXr Game { get; set; } 18 | 19 | [BackgroundDependencyLoader] 20 | private void load ( XrConfigManager config ) { 21 | config.BindWith( XrConfigSetting.InputMode, inputModeBindable ); 22 | } 23 | 24 | private Controller openingController; 25 | private XrController previousHoldingController; 26 | public XrController HoldingController { 27 | get { 28 | if ( VR.EnabledControllerCount <= 1 ) return null; 29 | if ( inputModeBindable.Value == InputMode.SinglePointer ) return Game.SecondaryController; 30 | if ( openingController?.IsEnabled == true ) return Game.GetControllerFor( openingController ); 31 | else return null; 32 | } 33 | } 34 | 35 | public HandheldMenu () { 36 | VR.BindComponentsLoaded( () => { 37 | var toggleMenu = VR.GetControllerComponent( XrAction.ToggleMenu ); 38 | toggleMenu.BindValueChangedDetailed( v => { 39 | if ( v.NewValue ) { 40 | if ( Elements.Any( x => x.IsColliderEnabled ) ) { 41 | if ( HoldingController is null || inputModeBindable.Value == InputMode.SinglePointer || HoldingController.Source == v.Source ) { 42 | Hide(); 43 | } 44 | else { 45 | openingController = v.Source; 46 | } 47 | } 48 | else { 49 | Show(); 50 | openingController = v.Source; 51 | Position = TargetPosition; 52 | Rotation = TargetRotation; 53 | } 54 | } 55 | } ); 56 | } ); 57 | } 58 | public bool IsOpen { get; private set; } 59 | 60 | public override void Hide () { 61 | base.Hide(); 62 | IsOpen = false; 63 | openingController = null; 64 | } 65 | 66 | public override void Show () { 67 | base.Show(); 68 | IsOpen = true; 69 | } 70 | 71 | private Vector3 retainedPosition; 72 | protected Vector3 TargetPosition { 73 | get { 74 | if ( !IsOpen ) return retainedPosition; 75 | if ( HoldingController is null ) { 76 | return retainedPosition = Game.Camera.GlobalPosition + Game.Camera.GlobalForward * 0.5f; 77 | } 78 | else { 79 | return retainedPosition = HoldingController.Position + HoldingController.Forward * 0.2f + HoldingController.Up * 0.05f; 80 | } 81 | } 82 | } 83 | 84 | private Quaternion retainedRotation; 85 | protected Quaternion TargetRotation { 86 | get { 87 | if ( !IsOpen ) return retainedRotation; 88 | if ( HoldingController is null ) { 89 | return retainedRotation = Game.Camera.GlobalRotation; 90 | } 91 | else { 92 | return retainedRotation = HoldingController.Rotation * Quaternion.FromAxisAngle( Vector3.UnitX, MathF.PI * 0.25f ); 93 | } 94 | } 95 | } 96 | 97 | protected override void Update () { 98 | base.Update(); 99 | 100 | if ( HoldingController != previousHoldingController ) { 101 | if ( previousHoldingController is not null ) previousHoldingController.HeldObjects.Remove( this ); 102 | previousHoldingController = HoldingController; 103 | if ( previousHoldingController is not null ) previousHoldingController.HeldObjects.Add( this ); 104 | } 105 | if ( Elements.Any( x => x.IsColliderEnabled ) ) { 106 | if ( VR.EnabledControllerCount == 0 ) { 107 | Hide(); 108 | } 109 | foreach ( var i in Elements ) { 110 | i.RequestedInputMode = HoldingController == Game.MainController ? PanelInputMode.Inverted : PanelInputMode.Regular; 111 | } 112 | } 113 | 114 | // doing this every frame makes it have an Easing.Out-like curve 115 | this.MoveTo( TargetPosition, 100 ); 116 | this.RotateTo( TargetRotation, 100 ); 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /osu.XR/Components/Groups/IHasIcon.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | 3 | namespace osu.XR.Components.Groups { 4 | public interface IHasIcon { 5 | Drawable CreateIcon (); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /osu.XR/Components/Groups/IHasName.cs: -------------------------------------------------------------------------------- 1 | namespace osu.XR.Components.Groups { 2 | public interface IHasName { 3 | string DisplayName { get; } 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /osu.XR/Components/Groups/MenuStack.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Sprites; 5 | using osu.Framework.XR.Components; 6 | using osu.Game.Graphics.Containers; 7 | using osu.Game.Overlays; 8 | using osu.Game.Overlays.Settings; 9 | using osu.XR.Components.Panels; 10 | using osuTK; 11 | using System; 12 | using System.Reflection; 13 | 14 | namespace osu.XR.Components.Groups { 15 | public abstract class MenuStack : CompositeDrawable3D where T : Drawable3D { 16 | public readonly BindableList Elements = new(); 17 | 18 | public override bool RemoveCompletedTransforms => true; 19 | private SettingsSidebar sidebar = new(); 20 | private FlatPanel sidebarPanel = new(); 21 | 22 | [Cached] 23 | OverlayColourProvider colours = new( OverlayColourScheme.Pink ); 24 | 25 | public MenuStack () { 26 | sidebarPanel.Source.Add( sidebar ); 27 | sidebarPanel.BypassAutoSizeAxes = Axes3D.All; 28 | sidebarPanel.AutoOffsetAnchorX = 0.5f; 29 | sidebarPanel.AutoOffsetOriginX = -0.5f; 30 | sidebarPanel.PanelHeight = 0.5; 31 | sidebarPanel.Height = 500; 32 | sidebarPanel.RelativeSizeAxes = Framework.Graphics.Axes.X; 33 | sidebarPanel.AutosizeX(); 34 | sidebarPanel.PanelAutoScaleAxes = Framework.Graphics.Axes.X; 35 | AddInternal( sidebarPanel ); 36 | sidebarPanel.Position = new Vector3( 0.02f, 0, 0 ); 37 | 38 | Elements.BindCollectionChanged( (_,e) => { 39 | if ( ignorePanelListEvents ) return; 40 | if ( e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add ) { 41 | foreach ( T i in e.NewItems ) { 42 | AddInternal( i ); 43 | i.AutoOffsetOriginX = 0.5f; 44 | i.AutoOffsetAnchorX = 0.5f; 45 | // HACK we should make our own sidebar 46 | var button = new SidebarIconButton(); 47 | ( typeof( SidebarIconButton ).GetField( "iconContainer", BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( button ) as ConstrainedIconContainer ).Icon = (i as IHasIcon)?.CreateIcon() ?? new SpriteIcon { Icon = FontAwesome.Solid.QuestionCircle }; 48 | ( typeof( SidebarIconButton ).GetField( "headerText", BindingFlags.NonPublic | BindingFlags.Instance ).GetValue( button ) as SpriteText ).Text = (i as IHasName)?.DisplayName ?? "Unnamed Panel"; 49 | button.Action = () => { 50 | Focus( i ); 51 | sidebar.Expanded.Value = false; 52 | }; 53 | sidebar.Add( button ); 54 | } 55 | } 56 | else if ( e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Remove ) { 57 | // TODO remove panels from panel stack 58 | throw new InvalidOperationException( "This collection does not support this operation yet." ); 59 | } 60 | else { 61 | throw new InvalidOperationException( "This collection does not support this operation yet." ); 62 | } 63 | 64 | applyTransforms(); 65 | }, true ); 66 | } 67 | 68 | protected abstract void ApplyTransformsTo ( T element, int index, float progress ); 69 | 70 | private bool ignorePanelListEvents = false; 71 | public void Focus ( T panel ) { 72 | if ( Elements.Contains( panel ) ) { 73 | ignorePanelListEvents = true; 74 | Elements.Remove( panel ); 75 | Elements.Insert( 0, panel ); 76 | ignorePanelListEvents = false; 77 | 78 | applyTransforms(); 79 | } 80 | } 81 | 82 | void applyTransforms () { 83 | for ( int i = 0; i < Elements.Count; i++ ) { 84 | ApplyTransformsTo( Elements[ i ], i, (float)i / Elements.Count ); 85 | } 86 | } 87 | 88 | public override void Hide () { 89 | foreach ( var i in Elements ) { 90 | i.Hide(); 91 | } 92 | sidebar.FadeOut( 300 ); 93 | sidebar.Expanded.Value = false; 94 | } 95 | 96 | public override void Show () { 97 | foreach ( var i in Elements ) { 98 | i.Show(); 99 | } 100 | sidebar.FadeIn( 300 ); 101 | sidebar.Expanded.Value = true; 102 | applyTransforms(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /osu.XR/Components/Groups/PopoutPanelStack.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.XR.Components; 3 | using osu.XR.Components.Panels; 4 | using osuTK; 5 | 6 | namespace osu.XR.Components.Groups { 7 | public abstract class PopoutPanelStack : MenuStack { 8 | public double TransitionDuration => 300; 9 | public Vector3 PopoutPosition = new Vector3( 0, 0, 0.02f ); 10 | public float PopoutScale = 0.8f; 11 | protected override void ApplyTransformsTo ( FlatPanel panel, int index, float progress ) { 12 | if ( index == 0 ) { 13 | panel.FadeIn( TransitionDuration / 2 ); 14 | panel.ScaleTo( Vector3.One, TransitionDuration, Easing.Out ); 15 | panel.MoveTo( Vector3.Zero, TransitionDuration, Easing.Out ); 16 | } 17 | else { 18 | panel.FadeOut( TransitionDuration, Easing.In ); 19 | panel.ScaleTo( new Vector3( PopoutScale ), TransitionDuration, Easing.Out ); 20 | panel.MoveTo( PopoutPosition, TransitionDuration, Easing.Out ); 21 | } 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /osu.XR/Components/Groups/RegularPanelStack.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.XR.Components; 3 | using osu.XR.Components.Panels; 4 | using osuTK; 5 | 6 | namespace osu.XR.Components.Groups { 7 | public abstract class RegularPanelStack : MenuStack { 8 | public double TransitionDuration => 300; 9 | public Vector3 PanelOffset = new Vector3( 0.01f, 0, 0.02f ); 10 | protected override void ApplyTransformsTo ( FlatPanel panel, int index, float progress ) { 11 | panel.MoveTo( PanelOffset * index, TransitionDuration, Easing.Out ); 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /osu.XR/Components/IFocusable.cs: -------------------------------------------------------------------------------- 1 | namespace osu.XR.Components { 2 | public interface IFocusable { 3 | void OnControllerFocusGained ( IFocusSource controller ); 4 | void OnControllerFocusLost ( IFocusSource controller ); 5 | bool CanHaveGlobalFocus { get; } 6 | } 7 | 8 | public interface IFocusSource { 9 | 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /osu.XR/Components/Panels/CurvedPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.XR.Components; 3 | using System; 4 | 5 | namespace osu.XR.Components.Panels { 6 | /// 7 | /// A curved 3D panel that displays an image from a . 8 | /// 9 | public class CurvedPanel : InteractivePanel { 10 | public float Arc { get => ArcBindable.Value; set => ArcBindable.Value = value; } 11 | public float Radius { get => RadiusBindable.Value; set => RadiusBindable.Value = value; } 12 | public readonly BindableFloat ArcBindable = new( MathF.PI * 1.2f ) { MinValue = MathF.PI / 18, MaxValue = MathF.PI * 2 }; 13 | public readonly BindableFloat RadiusBindable = new( 1.6f ) { MinValue = 0.1f, MaxValue = 100 }; 14 | 15 | public CurvedPanel () { 16 | ArcBindable.ValueChanged += _ => IsMeshInvalidated = true; 17 | RadiusBindable.ValueChanged += _ => IsMeshInvalidated = true; 18 | } 19 | 20 | protected override void RecalculateMesh () { 21 | Mesh.Clear(); 22 | Panel.Shapes.MakeCurved( Mesh, (float)MainTexture.Width / MainTexture.Height, (float)Arc, (float)Radius, 100 ); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /osu.XR/Components/Panels/FlatPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.XR.Components; 4 | using System; 5 | 6 | namespace osu.XR.Components.Panels { 7 | /// 8 | /// A flat 3D panel that displays an image from a . 9 | /// 10 | public class FlatPanel : InteractivePanel { 11 | public readonly BindableDouble PanelWidthBindable = new( 1 ); 12 | public readonly BindableDouble PanelHeightBindable = new( 1 ); 13 | public double PanelWidth { get => PanelWidthBindable.Value; set => PanelWidthBindable.Value = value; } 14 | public double PanelHeight { get => PanelHeightBindable.Value; set => PanelHeightBindable.Value = value; } 15 | private Axes panelAutoScaleAxes = Axes.None; 16 | public Axes PanelAutoScaleAxes { 17 | get => panelAutoScaleAxes; 18 | set { 19 | panelAutoScaleAxes = value; 20 | IsMeshInvalidated = true; 21 | } 22 | } 23 | 24 | public FlatPanel () { 25 | PanelWidthBindable.ValueChanged += _ => IsMeshInvalidated = true; 26 | PanelHeightBindable.ValueChanged += _ => IsMeshInvalidated = true; 27 | } 28 | 29 | protected override void RecalculateMesh () { 30 | float width; 31 | float height; 32 | if ( PanelAutoScaleAxes == Axes.Both ) { // TODO with both, use fit fill mode 33 | throw new InvalidOperationException( $"{nameof( FlatPanel )} {nameof( PanelAutoScaleAxes )} can not be {Axes.Both}. There is no reference size." ); 34 | } 35 | else if ( PanelAutoScaleAxes == Axes.X ) { 36 | height = (float)PanelHeight; 37 | width = height * ( (float)MainTexture.Width / MainTexture.Height ); 38 | } 39 | else if ( PanelAutoScaleAxes == Axes.Y ) { 40 | width = (float)PanelWidth; 41 | height = width / ( (float)MainTexture.Width / MainTexture.Height ); 42 | } 43 | else { 44 | width = (float)PanelWidth; 45 | height = (float)PanelHeight; 46 | } 47 | 48 | Mesh.Clear(); 49 | Panel.Shapes.MakeFlat( Mesh, width, height ); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /osu.XR/Components/Panels/InteractivePanel.cs: -------------------------------------------------------------------------------- 1 | using OpenVR.NET; 2 | using osu.Framework.Allocation; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics.Cursor; 5 | using osu.Framework.XR.Components; 6 | using osu.XR.Drawables.Containers; 7 | using osu.XR.Input; 8 | using osu.XR.Settings; 9 | using osuTK; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | using static osu.Framework.XR.Physics.Raycast; 13 | 14 | namespace osu.XR.Components.Panels { 15 | public abstract class InteractivePanel : Panel, IFocusable { 16 | public bool CanHaveGlobalFocus { get; init; } = true; 17 | public PanelInputMode RequestedInputMode { get; set; } = PanelInputMode.Regular; 18 | 19 | private bool hasFocus; 20 | new public bool HasFocus { 21 | get => hasFocus; 22 | set { 23 | if ( hasFocus == value ) return; 24 | hasFocus = value; 25 | if ( !hasFocus ) { 26 | EmulatedInput.IsLeftPressed = false; 27 | EmulatedInput.IsRightPressed = false; 28 | EmulatedInput.ReleaseAllTouch(); 29 | } 30 | EmulatedInput.HasFocus = value; 31 | } 32 | } 33 | 34 | protected override TooltipContainer CreateContentWrapper () => new OsuXrTooltipContainer( null ); 35 | 36 | [BackgroundDependencyLoader] 37 | private void load ( XrConfigManager config ) { 38 | config.BindWith( XrConfigSetting.Deadzone, deadzoneBindable ); 39 | EmulatedInput.Touch.DeadzoneBindable.BindTo( deadzoneBindable ); 40 | } 41 | BindableInt deadzoneBindable = new( 20 ); 42 | 43 | bool inDeadzone = false; 44 | Vector2 deadzoneCenter; 45 | Vector2 pointerPosition; 46 | 47 | void handleButton ( bool isLeft, bool isDown ) { 48 | if ( VR.EnabledControllerCount > 1 ) { 49 | if ( RequestedInputMode == PanelInputMode.Regular == isLeft ) EmulatedInput.IsLeftPressed = isDown; 50 | else if ( RequestedInputMode == PanelInputMode.Inverted == isLeft ) EmulatedInput.IsRightPressed = isDown; 51 | } 52 | else EmulatedInput.IsLeftPressed = isDown; 53 | 54 | if ( isDown ) { 55 | inDeadzone = true; 56 | deadzoneCenter = pointerPosition; 57 | } 58 | else inDeadzone = false; 59 | } 60 | 61 | List focusedControllers = new(); 62 | IEnumerable focusedControllerSources => focusedControllers.Select( x => x.Source ); 63 | Dictionary eventUnsubs = new(); 64 | public void OnControllerFocusGained ( IFocusSource focus ) { 65 | if ( focus is not XrController controller ) return; 66 | 67 | System.Action> onScroll = v => { EmulatedInput.Scroll += v.NewValue - v.OldValue; }; 68 | System.Action> onLeft = v => { handleButton( isLeft: true, isDown: v.NewValue ); }; 69 | System.Action> onRight = v => { handleButton( isLeft: false, isDown: v.NewValue ); }; 70 | System.Action onMove = hit => { onPointerMove( controller, hit ); }; 71 | System.Action onDown = hit => { onTouchDown( controller, hit ); }; 72 | System.Action onUp = () => { onTouchUp( controller ); }; 73 | controller.PointerMove += onMove; 74 | controller.PointerDown += onDown; 75 | controller.PointerUp += onUp; 76 | controller.ScrollBindable.ValueChanged += onScroll; 77 | controller.LeftButtonBindable.ValueChanged += onLeft; 78 | controller.RightButtonBindable.ValueChanged += onRight; 79 | eventUnsubs.Add( controller, () => { 80 | controller.PointerMove -= onMove; 81 | controller.PointerDown -= onDown; 82 | controller.PointerUp -= onUp; 83 | controller.ScrollBindable.ValueChanged -= onScroll; 84 | controller.LeftButtonBindable.ValueChanged -= onLeft; 85 | controller.RightButtonBindable.ValueChanged -= onRight; 86 | } ); 87 | focusedControllers.Add( controller ); 88 | 89 | updateFocus(); 90 | } 91 | public void OnControllerFocusLost ( IFocusSource focus ) { 92 | if ( focus is not XrController controller ) return; 93 | 94 | eventUnsubs[ controller ].Invoke(); 95 | eventUnsubs.Remove( controller ); 96 | focusedControllers.Remove( controller ); 97 | 98 | updateFocus(); 99 | } 100 | void updateFocus () { 101 | HasFocus = focusedControllers.Any(); 102 | } 103 | 104 | private void onPointerMove ( XrController controller, RaycastHit hit ) { 105 | if ( hit.Collider != this ) return; 106 | 107 | var position = TexturePositionAt( hit.TrisIndex, hit.Point ); 108 | if ( controller.EmulatesTouch ) { 109 | EmulatedInput.TouchMove( controller, position ); 110 | } 111 | else { 112 | pointerPosition = position; 113 | if ( ( pointerPosition - deadzoneCenter ).Length > deadzoneBindable.Value ) inDeadzone = false; 114 | if ( !inDeadzone ) EmulatedInput.Mouse.EmulateMouseMove( position ); 115 | } 116 | } 117 | 118 | private void onTouchDown ( XrController controller, RaycastHit hit ) { 119 | if ( hit.Collider != this ) return; 120 | 121 | var position = TexturePositionAt( hit.TrisIndex, hit.Point ); 122 | EmulatedInput.TouchDown( controller, position ); 123 | } 124 | 125 | private void onTouchUp ( XrController controller ) { 126 | EmulatedInput.TouchUp( controller ); 127 | } 128 | } 129 | 130 | public enum PanelInputMode { 131 | Regular, 132 | Inverted 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /osu.XR/Components/Player.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.XR.Components; 4 | using osu.Framework.XR.Graphics; 5 | using osu.XR.Settings; 6 | using osuTK; 7 | using osuTK.Graphics; 8 | using System; 9 | using System.Threading.Tasks; 10 | 11 | namespace osu.XR.Components { 12 | public class Player : XrPlayer { // TODO move controllers here too 13 | private Container3D invariantContainer = new(); 14 | private Model shadow; 15 | private Foot footLeft; 16 | private Foot footRight; 17 | 18 | private Mesh paw; 19 | private Mesh shoe; 20 | 21 | public readonly Bindable FeetSymbols = new( Components.FeetSymbols.None ); 22 | public Player () { 23 | AddInternal( shadow = new Model() ); 24 | invariantContainer.Add( footRight = new Foot { Scale = new Vector3( -0.1f, 0.1f, 0.1f ), X = 0.1f, Z = 0.03f, Y = -0.001f, EulerRotY = 14 / 180f * MathF.PI } ); 25 | invariantContainer.Add( footLeft = new Foot { Scale = new Vector3( 0.1f, 0.1f, 0.1f ), X = -0.1f, Y = -0.001f, EulerRotY = -10 / 180f * MathF.PI } ); 26 | 27 | shadow.Mesh.AddCircle( Vector3.Zero, Vector3.UnitY, Vector3.UnitZ, 32 ); 28 | shadow.Scale = new Vector3( 0.012f ); 29 | footRight.Alpha = footLeft.Alpha = 0.2f; 30 | footRight.Tint = footLeft.Tint = Color4.AliceBlue; 31 | shadow.Alpha = 0.8f; 32 | shadow.Tint = Color4.White; 33 | } 34 | 35 | [BackgroundDependencyLoader] 36 | private void load ( XrConfigManager config ) { 37 | config.BindWith( XrConfigSetting.ShadowType, FeetSymbols ); 38 | } 39 | 40 | protected override void LoadComplete () { 41 | base.LoadComplete(); 42 | 43 | Root.Add( invariantContainer ); 44 | 45 | FeetSymbols.BindValueChanged( v => { 46 | if ( v.NewValue == Components.FeetSymbols.Shoes ) { 47 | if ( shoe is null ) { 48 | Task.Run( () => { 49 | shoe = Mesh.FromOBJFile( @".\Resources\shoe.obj" ); 50 | 51 | Schedule( () => { 52 | footLeft.Mesh = shoe; 53 | footRight.Mesh = shoe; 54 | } ); 55 | } ); 56 | } 57 | else { 58 | footLeft.Mesh = shoe; 59 | footRight.Mesh = shoe; 60 | } 61 | 62 | footLeft.IsVisible = true; 63 | footRight.IsVisible = true; 64 | 65 | footLeft.Scale = new Vector3( 0.1f, 0.1f, 0.1f ) * 1.7f; 66 | footRight.Scale = new Vector3( -0.1f, 0.1f, 0.1f ) * 1.7f; 67 | } 68 | else if ( v.NewValue == Components.FeetSymbols.Paws ) { 69 | if ( paw is null ) { 70 | Task.Run( () => { 71 | paw = Mesh.FromOBJFile( @".\Resources\paw.obj" ); 72 | 73 | Schedule( () => { 74 | footLeft.Mesh = paw; 75 | footRight.Mesh = paw; 76 | } ); 77 | } ); 78 | } 79 | else { 80 | footLeft.Mesh = paw; 81 | footRight.Mesh = paw; 82 | } 83 | 84 | footLeft.IsVisible = true; 85 | footRight.IsVisible = true; 86 | 87 | footLeft.Scale = new Vector3( -0.1f, 0.1f, 0.1f ); 88 | footRight.Scale = new Vector3( 0.1f, 0.1f, 0.1f ); 89 | } 90 | else { 91 | footLeft.IsVisible = false; 92 | footRight.IsVisible = false; 93 | } 94 | }, true ); 95 | } 96 | 97 | protected override void Update () { 98 | base.Update(); 99 | 100 | shadow.Y = -Y; 101 | footLeft.TargetPosition.Value = new Vector3( X, -0.001f, Z ) + Left * 0.1f; 102 | footRight.TargetPosition.Value = new Vector3( X, -0.001f, Z ) + Right * 0.1f; 103 | 104 | footLeft.TargetRotation.Value = Quaternion.FromAxisAngle( Vector3.UnitY, (-10 / 180f + 1) * MathF.PI ) * Rotation; 105 | footRight.TargetRotation.Value = Quaternion.FromAxisAngle( Vector3.UnitY, (14 / 180f + 1) * MathF.PI ) * Rotation; 106 | } 107 | } 108 | 109 | public enum FeetSymbols { 110 | None, 111 | Shoes, 112 | Paws 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /osu.XR/Components/Skyboxes/LightsOutSkyBox.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Utils; 5 | using osu.Framework.XR.Components; 6 | using osu.Framework.XR.Extensions; 7 | using osu.Framework.XR.Maths; 8 | using osu.XR.Drawables; 9 | using osu.XR.Graphics; 10 | using osuTK; 11 | using osuTK.Graphics; 12 | using System; 13 | using System.Linq; 14 | 15 | namespace osu.XR.Components.Skyboxes { 16 | /// 17 | /// A solid black skybox with neon rectangles orbitting the camera to the beat 18 | /// 19 | public class LightsOutSkyBox : CompositeDrawable3D { 20 | private Bindable velocity = new( 0 ); 21 | private Container3D clockwise; 22 | private Container3D counterClockwise; 23 | 24 | public LightsOutSkyBox () { 25 | AddInternal( clockwise = new Container3D() ); 26 | AddInternal( counterClockwise = new Container3D() ); 27 | 28 | recalculateChildren(); 29 | BindableBeat.ValueChanged += v => { 30 | this.TransformBindableTo( velocity, (float)v.NewValue.AverageAmplitude * (float)v.NewValue.TimingPoint.BPM / 30, 100 ); 31 | foreach ( Model child in clockwise.Concat( counterClockwise ) ) { 32 | child.FlashColour( Interpolation.ValueAt( 0.4f, child.Tint, Color4.White, 0, 1 ), v.NewValue.TimingPoint.BeatLength * 2 / 3, Easing.Out ); 33 | } 34 | }; 35 | } 36 | 37 | private void recalculateChildren () { 38 | this.clockwise.Clear(); 39 | counterClockwise.Clear(); 40 | bool clockwise = true; 41 | 42 | Random random = new Random(); 43 | var radius = 10f; 44 | for ( float y = 0; y < 4; y += random.NextSingle( 0.1f, 0.3f ) ) { 45 | var height = random.NextSingle( 0.3f, 0.6f ); 46 | y += height / 2; 47 | 48 | for ( float theta = random.NextSingle( 0, 1.2f ); theta < MathF.Tau - 0.2f; theta += random.NextSingle( 0.1f, random.NextSingle( 0.1f, 1.2f ) ) ) { 49 | var delta = Math.Min( random.NextSingle( 0.1f, random.NextSingle( 0.1f, 1.2f ) ), MathF.Tau - theta - 0.1f ); 50 | 51 | var child = new Model(); 52 | ( clockwise ? this.clockwise : counterClockwise ).Add( child ); 53 | child.Mesh.AddArcedPlane( Vector3.UnitY, Quaternion.FromAxisAngle( Vector3.UnitY, theta ).Apply( Vector3.UnitZ ), height, radius, delta, origin: new Vector3( 0, y, 0 ) ); 54 | child.UseGammaCorrection = true; 55 | child.Tint = NeonColors.NextRandom( random ); 56 | 57 | theta += delta; 58 | } 59 | 60 | clockwise = !clockwise; 61 | y += height / 2; 62 | } 63 | 64 | for ( float r = radius; r > 0.9f; r -= random.NextSingle( 0.3f, 0.7f ) * radius / 5 ) { 65 | var width = random.NextSingle( 0.2f, 0.4f ); 66 | 67 | for ( float theta = random.NextSingle( 0, 1.2f ); theta < MathF.Tau - 0.2f; theta += random.NextSingle( 0.1f, random.NextSingle( 0.1f, 1.2f ) ) ) { 68 | var delta = Math.Min( random.NextSingle( 0.1f, random.NextSingle( 0.1f, 1.2f ) ), MathF.Tau - theta - 0.1f ); 69 | 70 | var child = new Model(); 71 | ( clockwise ? this.clockwise : counterClockwise ).Add( child ); 72 | child.Mesh.AddCircularArc( Vector3.UnitY, Quaternion.FromAxisAngle( Vector3.UnitY, theta ).Apply( Vector3.UnitZ ), delta, r - width, r, origin: new Vector3( 0, 0.001f, 0 ) ); 73 | child.UseGammaCorrection = true; 74 | child.Tint = NeonColors.NextRandom( random ); 75 | 76 | theta += delta; 77 | } 78 | 79 | clockwise = !clockwise; 80 | r -= width; 81 | } 82 | } 83 | 84 | protected override void Update () { 85 | base.Update(); 86 | clockwise.Rotation = Quaternion.FromAxisAngle( Vector3.UnitY, velocity.Value * 0.1f * (float)Time.Elapsed / 1000 ) * clockwise.Rotation; 87 | counterClockwise.Rotation = Quaternion.FromAxisAngle( Vector3.UnitY, velocity.Value * -0.1f * (float)Time.Elapsed / 1000 ) * counterClockwise.Rotation; 88 | } 89 | 90 | public readonly Bindable BindableBeat = new(); 91 | [BackgroundDependencyLoader] 92 | private void load ( BeatProvider beat ) { 93 | BindableBeat.BindTo( beat.BindableBeat ); 94 | } 95 | 96 | //private class CameraTrackingModel : Model { 97 | // protected override DrawNode3D CreateDrawNode () 98 | // => new CameraPositionTrackingDrawNode( this ); 99 | //} 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /osu.XR/Components/Skyboxes/SkyBox.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Sprites; 5 | using osu.Framework.XR.Components; 6 | using osu.Game.Overlays.Settings; 7 | using osu.XR.Inspector; 8 | using osu.XR.Settings; 9 | 10 | namespace osu.XR.Components.Skyboxes { 11 | public class SkyBox : CompositeDrawable3D, IConfigurableInspectable { 12 | SolidSkyBox solid = new SolidSkyBox(); 13 | LightsOutSkyBox lightsOut = new LightsOutSkyBox(); 14 | 15 | private Bindable typeBindable = new(); 16 | private Bindable activeSkybox = new(); 17 | private Bindable _activeSkybox = new(); 18 | public SkyBox () { 19 | activeSkybox.BindValueChanged( v => { 20 | v.OldValue?.Hide(); 21 | if ( v.NewValue is null ) { 22 | ClearInternal( false ); 23 | } 24 | else { 25 | InternalChild = v.NewValue; 26 | v.NewValue.Show(); 27 | } 28 | _activeSkybox.Value = v.NewValue; 29 | } ); 30 | 31 | typeBindable.BindValueChanged( v => { 32 | activeSkybox.Value = v.NewValue switch { 33 | SkyBoxType.Solid => solid, 34 | SkyBoxType.LightsOut => lightsOut, 35 | _ => null 36 | }; 37 | }, true ); 38 | } 39 | 40 | [BackgroundDependencyLoader] 41 | private void load ( XrConfigManager config ) { 42 | config.BindWith( XrConfigSetting.SkyboxType, typeBindable ); 43 | } 44 | 45 | public Drawable CreateInspectorSubsection () { 46 | var section = new InspectorSubsectionWithCurrent { 47 | Title = "Skybox", 48 | Icon = FontAwesome.Solid.Image, 49 | Current = _activeSkybox, 50 | Children = new Drawable[] { 51 | new SettingsEnumDropdown { Current = typeBindable, LabelText = "Skybox type" } 52 | } 53 | }; 54 | 55 | return section; 56 | } 57 | 58 | public bool AreSettingsPersistent => true; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /osu.XR/Components/Skyboxes/SkyBoxType.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace osu.XR.Components.Skyboxes { 4 | public enum SkyBoxType { 5 | Solid, 6 | 7 | [Description("Lights Out")] 8 | LightsOut, 9 | //Storyboard 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /osu.XR/Components/Skyboxes/SolidSkyBox.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Framework.XR.Components; 5 | using osu.Framework.XR.Graphics; 6 | using osu.Framework.XR.Projection; 7 | using osu.Framework.XR.Rendering; 8 | using osu.Game.Overlays.Settings; 9 | using osu.XR.Drawables; 10 | using osu.XR.Drawables.UserInterface; 11 | using osu.XR.Inspector; 12 | using osu.XR.Settings.Sections; 13 | using osuTK; 14 | using osuTK.Graphics; 15 | using System; 16 | 17 | namespace osu.XR.Components.Skyboxes { 18 | /// 19 | /// A hot pink skybox that fits the osu theme. 20 | /// 21 | public class SolidSkyBox : Model, IBehindEverything, IConfigurableInspectable { 22 | public readonly Bindable TintBindable = new( new Color4( 253, 35, 115, 255 ) ); 23 | public readonly BindableFloat OpacityBindable = new( 1 ) { MinValue = 0, MaxValue = 1 }; 24 | 25 | public SolidSkyBox () { 26 | MainTexture = Textures.VerticalGradient( Color4.Black, Color4.White, 100, x => MathF.Pow( x, 2 ) ).TextureGL; 27 | Mesh.Vertices.AddRange( new[] { 28 | new Vector3( 1, 1, 1 ) * 300, 29 | new Vector3( 1, 1, -1 ) * 300, 30 | new Vector3( 1, -1, 1 ) * 300, 31 | new Vector3( 1, -1, -1 ) * 300, 32 | new Vector3( -1, 1, 1 ) * 300, 33 | new Vector3( -1, 1, -1 ) * 300, 34 | new Vector3( -1, -1, 1 ) * 300, 35 | new Vector3( -1, -1, -1 ) * 300, 36 | } ); 37 | Mesh.TextureCoordinates.AddRange( new[] { 38 | new Vector2( 0, 0 ), 39 | new Vector2( 0, 0 ), 40 | new Vector2( 0, 1 ), 41 | new Vector2( 0, 1 ), 42 | new Vector2( 0, 0 ), 43 | new Vector2( 0, 0 ), 44 | new Vector2( 0, 1 ), 45 | new Vector2( 0, 1 ), 46 | new Vector2( 0, 1 ), 47 | new Vector2( 1, 1 ), 48 | } ); 49 | Mesh.Tris.AddRange( new IndexedFace[] { 50 | new( 4, 7, 5 ), 51 | new( 4, 7, 6 ), 52 | new( 4, 2, 6 ), 53 | new( 4, 2, 0 ), 54 | new( 5, 3, 7 ), 55 | new( 5, 3, 1 ), 56 | new( 6, 3, 7 ), 57 | new( 6, 3, 2 ), 58 | new( 0, 2, 1 ), 59 | new( 1, 2, 3 ) 60 | } ); 61 | 62 | UseGammaCorrection = true; 63 | TintBindable.BindValueChanged( v => Tint = v.NewValue, true ); 64 | OpacityBindable.BindValueChanged( v => Alpha = v.NewValue, true ); 65 | } 66 | 67 | public Drawable CreateInspectorSubsection () { 68 | return new SettingsSectionContainer { 69 | Title = "Skybox", 70 | Icon = FontAwesome.Solid.Image, 71 | Children = new Drawable[] { 72 | new ColorPicker { LabelText = "Tint", Current = TintBindable }, 73 | new SettingsSlider { LabelText = "Opacity", Current = OpacityBindable }, 74 | } 75 | }; 76 | } 77 | public bool AreSettingsPersistent => false; 78 | 79 | protected override DrawNode3D CreateDrawNode () 80 | => new CameraPositionTrackingDrawNode( this ); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /osu.XR/Components/TeleportVisual.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Caching; 3 | using osu.Framework.XR.Components; 4 | using osu.Framework.XR.Extensions; 5 | using osuTK; 6 | 7 | namespace osu.XR.Components { 8 | public class TeleportVisual : CompositeDrawable3D { 9 | private Path3D path; 10 | private Model circle; 11 | Cached isPathValid = new(); 12 | 13 | public readonly BindableBool IsActive = new(); 14 | public readonly Bindable GravityBindable = new( Vector3.UnitY * -9.81f ); 15 | public readonly Bindable OriginBindable = new(); 16 | public readonly Bindable DirectionBindable = new( Vector3.UnitY ); 17 | 18 | protected override void LoadComplete () { 19 | base.LoadComplete(); 20 | 21 | Root.Add( path = new DashedPath3D() ); 22 | Root.Add( circle = new() ); 23 | circle.Scale = new Vector3( 0.1f ); 24 | circle.Alpha = 0.3f; 25 | circle.Mesh.AddCircle( Vector3.Zero, Vector3.UnitY, Vector3.UnitZ, 32 ); 26 | IsActive.BindValueChanged( v => { 27 | if ( v.NewValue ) { 28 | path.IsVisible = true; 29 | circle.IsVisible = HasHitGround; 30 | } 31 | else { 32 | path.IsVisible = false; 33 | circle.IsVisible = false; 34 | } 35 | }, true ); 36 | 37 | (GravityBindable, OriginBindable, DirectionBindable).BindValuesChanged( () => isPathValid.Invalidate(), true ); 38 | } 39 | 40 | protected override void Update () { 41 | base.Update(); 42 | if ( IsActive.Value && !isPathValid.IsValid ) { 43 | regenPath(); 44 | isPathValid.Validate(); 45 | } 46 | } 47 | 48 | public bool HasHitGround { get; private set; } 49 | public Vector3 HitPosition { get; private set; } 50 | void regenPath () { 51 | const float timeStep = 0.03f; 52 | Vector3 pos = OriginBindable.Value; 53 | Vector3 velocity = DirectionBindable.Value; 54 | path.ClearNodes(); 55 | for ( int i = 0; i < 200; i++ ) { 56 | path.AddNode( pos ); 57 | pos += velocity * timeStep; 58 | velocity += GravityBindable.Value * timeStep; 59 | 60 | if ( pos.Y <= 0 && pos.Y < OriginBindable.Value.Y ) { 61 | HasHitGround = true; 62 | circle.Position = HitPosition = pos - velocity * ( pos.Y / velocity.Y ); 63 | circle.Y = 0.01f; 64 | circle.IsVisible = true; 65 | return; 66 | } 67 | } 68 | HasHitGround = false; 69 | circle.IsVisible = false; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /osu.XR/DefaultBindings/knuckles.json: -------------------------------------------------------------------------------- 1 | { 2 | "action_manifest_version" : 0, 3 | "alias_info" : {}, 4 | "app_key" : "system.generated.osu.xr.exe", 5 | "bindings" : { 6 | "/actions/configuration" : { 7 | "sources" : [ 8 | { 9 | "inputs" : { 10 | "click" : { 11 | "output" : "/actions/configuration/in/togglemenu" 12 | } 13 | }, 14 | "mode" : "button", 15 | "path" : "/user/hand/right/input/a" 16 | }, 17 | { 18 | "inputs" : { 19 | "click" : { 20 | "output" : "/actions/configuration/in/togglemenu" 21 | } 22 | }, 23 | "mode" : "button", 24 | "path" : "/user/hand/left/input/a" 25 | } 26 | ] 27 | }, 28 | "/actions/haptics" : { 29 | "haptics" : [ 30 | { 31 | "output" : "/actions/haptics/out/feedback", 32 | "path" : "/user/hand/left/output/haptic" 33 | }, 34 | { 35 | "output" : "/actions/haptics/out/feedback", 36 | "path" : "/user/hand/right/output/haptic" 37 | } 38 | ], 39 | "sources" : [] 40 | }, 41 | "/actions/pointer" : { 42 | "sources" : [ 43 | { 44 | "inputs" : { 45 | "click" : { 46 | "output" : "/actions/pointer/in/mouseleft" 47 | } 48 | }, 49 | "mode" : "button", 50 | "path" : "/user/hand/right/input/trigger" 51 | }, 52 | { 53 | "inputs" : { 54 | "click" : { 55 | "output" : "/actions/pointer/in/mouseright" 56 | } 57 | }, 58 | "mode" : "button", 59 | "path" : "/user/hand/left/input/trigger" 60 | }, 61 | { 62 | "inputs" : { 63 | "position" : { 64 | "output" : "/actions/pointer/in/scroll" 65 | } 66 | }, 67 | "mode" : "joystick", 68 | "path" : "/user/hand/right/input/thumbstick" 69 | }, 70 | { 71 | "inputs" : { 72 | "position" : { 73 | "output" : "/actions/pointer/in/scroll" 74 | } 75 | }, 76 | "mode" : "joystick", 77 | "path" : "/user/hand/left/input/thumbstick" 78 | } 79 | ] 80 | } 81 | }, 82 | "category" : "steamvr_input", 83 | "controller_type" : "knuckles", 84 | "description" : "", 85 | "name" : "Default osu.XR configuration for Index Controller", 86 | "options" : {}, 87 | "simulated_actions" : [] 88 | } 89 | -------------------------------------------------------------------------------- /osu.XR/DefaultBindings/vive_controller.json: -------------------------------------------------------------------------------- 1 | { 2 | "action_manifest_version": 0, 3 | "alias_info": {}, 4 | "app_key": "system.generated.osu.xr.exe", 5 | "bindings": { 6 | "/actions/configuration": { 7 | "sources": [ 8 | { 9 | "inputs": { 10 | "click": { 11 | "output": "/actions/configuration/in/togglemenu" 12 | } 13 | }, 14 | "mode": "button", 15 | "path": "/user/hand/right/input/application_menu" 16 | }, 17 | { 18 | "inputs": { 19 | "click": { 20 | "output": "/actions/configuration/in/togglemenu" 21 | } 22 | }, 23 | "mode": "button", 24 | "path": "/user/hand/left/input/application_menu" 25 | } 26 | ] 27 | }, 28 | "/actions/haptics": { 29 | "haptics": [ 30 | { 31 | "output": "/actions/haptics/out/feedback", 32 | "path": "/user/hand/left/output/haptic" 33 | }, 34 | { 35 | "output": "/actions/haptics/out/feedback", 36 | "path": "/user/hand/right/output/haptic" 37 | } 38 | ], 39 | "sources": [] 40 | }, 41 | "/actions/pointer": { 42 | "sources": [ 43 | { 44 | "inputs": { 45 | "click": { 46 | "output": "/actions/pointer/in/mouseleft" 47 | } 48 | }, 49 | "mode": "button", 50 | "path": "/user/hand/right/input/trigger" 51 | }, 52 | { 53 | "inputs": { 54 | "click": { 55 | "output": "/actions/pointer/in/mouseleft" 56 | } 57 | }, 58 | "mode": "button", 59 | "path": "/user/hand/left/input/trigger" 60 | }, 61 | { 62 | "inputs": { 63 | "scroll": { 64 | "output": "/actions/pointer/in/scroll" 65 | } 66 | }, 67 | "mode": "scroll", 68 | "path": "/user/hand/right/input/trackpad" 69 | }, 70 | { 71 | "inputs": { 72 | "click": { 73 | "output": "/actions/pointer/in/mouseright" 74 | } 75 | }, 76 | "mode": "button", 77 | "path": "/user/hand/left/input/grip" 78 | }, 79 | { 80 | "inputs": { 81 | "click": { 82 | "output": "/actions/pointer/in/mouseright" 83 | } 84 | }, 85 | "mode": "button", 86 | "path": "/user/hand/right/input/grip" 87 | }, 88 | { 89 | "inputs": { 90 | "scroll": { 91 | "output": "/actions/pointer/in/scroll" 92 | } 93 | }, 94 | "mode": "scroll", 95 | "path": "/user/hand/left/input/trackpad" 96 | } 97 | ] 98 | } 99 | }, 100 | "category": "steamvr_input", 101 | "controller_type": "vive_controller", 102 | "description": "", 103 | "name": "Default osu.XR configuration for Vive Controller", 104 | "options": {}, 105 | "simulated_actions": [] 106 | } 107 | -------------------------------------------------------------------------------- /osu.XR/Drawables/BeatProvider.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Audio.Track; 2 | using osu.Framework.Bindables; 3 | using osu.Game.Beatmaps.ControlPoints; 4 | using osu.Game.Graphics.Containers; 5 | 6 | namespace osu.XR.Drawables { 7 | public class BeatProvider : BeatSyncedContainer { 8 | protected override void OnNewBeat ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ) { 9 | base.OnNewBeat( beatIndex, timingPoint, effectPoint, amplitudes ); 10 | BindableBeat.Value = new( beatIndex, timingPoint, effectPoint, amplitudes ); 11 | } 12 | 13 | public readonly Bindable BindableBeat = new(); 14 | } 15 | 16 | public record Beat ( int BeatIndex, TimingControlPoint TimingPoint, EffectControlPoint EffectPoint, ChannelAmplitudes Amplitudes ) { 17 | public readonly double AverageAmplitude = Amplitudes.Average; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /osu.XR/Drawables/CalmOsuAnimatedButton.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Extensions.Color4Extensions; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Containers; 5 | using osu.Framework.Graphics.Effects; 6 | using osu.Framework.Graphics.Shapes; 7 | using osu.Framework.Input.Events; 8 | using osu.Game.Graphics; 9 | using osu.Game.Graphics.Containers; 10 | using osu.Game.Graphics.UserInterface; 11 | using osuTK.Graphics; 12 | 13 | namespace osu.XR.Drawables { 14 | /// 15 | /// Highlight on hover, bounce on click. 16 | /// 17 | public class CalmOsuAnimatedButton : OsuClickableContainer { // TODO this is jank. Get rid of this. 18 | /// 19 | /// The colour that should be flashed when the is clicked. 20 | /// 21 | protected Color4 FlashColour = Color4.HotPink.Opacity( 1f ); 22 | 23 | private Color4 hoverColour = Color4.HotPink.Opacity( 0.6f ); 24 | 25 | /// 26 | /// The background colour of the while it is hovered. 27 | /// 28 | protected Color4 HoverColour { 29 | get => hoverColour; 30 | set { 31 | hoverColour = value; 32 | hover.Colour = value; 33 | } 34 | } 35 | 36 | protected override Container Content => content; 37 | 38 | private readonly Container content; 39 | private readonly Box hover; 40 | 41 | public CalmOsuAnimatedButton ( HoverSampleSet sampleSet = HoverSampleSet.Button ) : base( sampleSet ) { 42 | base.Content.Add( content = new Container { 43 | Origin = Anchor.Centre, 44 | Anchor = Anchor.Centre, 45 | RelativeSizeAxes = Axes.Both, 46 | CornerRadius = 5, 47 | Masking = true, 48 | EdgeEffect = new EdgeEffectParameters { 49 | Colour = Color4.Black.Opacity( 0.04f ), 50 | Type = EdgeEffectType.Shadow, 51 | Radius = 5, 52 | }, 53 | Children = new Drawable[] 54 | { 55 | hover = new Box 56 | { 57 | RelativeSizeAxes = Axes.Both, 58 | Colour = HoverColour, 59 | Alpha = 0, 60 | }, 61 | } 62 | } ); 63 | } 64 | 65 | [BackgroundDependencyLoader] 66 | private void load ( OsuColour colours ) { 67 | if ( AutoSizeAxes != Axes.None ) { 68 | content.RelativeSizeAxes = ( Axes.Both & ~AutoSizeAxes ); 69 | content.AutoSizeAxes = AutoSizeAxes; 70 | } 71 | 72 | Enabled.BindValueChanged( enabled => this.FadeColour( enabled.NewValue ? Color4.White : colours.Gray9, 200, Easing.OutQuint ), true ); 73 | } 74 | 75 | public System.Action Hovered; 76 | public System.Action HoverLost; 77 | protected override bool OnHover ( HoverEvent e ) { 78 | Schedule( () => Hovered?.Invoke() ); 79 | hover.FadeIn( 500, Easing.OutQuint ); 80 | return base.OnHover( e ); 81 | } 82 | 83 | protected override void OnHoverLost ( HoverLostEvent e ) { 84 | HoverLost?.Invoke(); 85 | hover.FadeOut( 500, Easing.OutQuint ); 86 | base.OnHoverLost( e ); 87 | } 88 | 89 | protected override bool OnClick ( ClickEvent e ) { 90 | hover.FlashColour( FlashColour, 800, Easing.OutQuint ); 91 | return base.OnClick( e ); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/AutoScrollContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace osu.XR.Drawables.Containers { 8 | public class AutoScrollContainerSyncGroup : AutoScrollContainerSyncGroup { 9 | public AutoScrollContainerSyncGroup ( Direction direction = Direction.Horizontal ) { } 10 | } 11 | public class AutoScrollContainerSyncGroup : Component where T : Drawable { 12 | public readonly List> containers = new(); 13 | 14 | public double BeginDelay = 2000; 15 | public double EndDelay = 2000; 16 | public double ScrollSpeed = 60; 17 | public double Current { get; private set; } 18 | public readonly Direction Direction; 19 | 20 | enum Stage { 21 | Start, 22 | WaitingAfterStart, 23 | Scrolling, 24 | WaitingBeforeEnd, 25 | End 26 | } 27 | Stage stage = Stage.Start; 28 | 29 | public AutoScrollContainerSyncGroup ( Direction direction = Direction.Horizontal ) { 30 | Direction = direction; 31 | } 32 | 33 | protected override void Update () { 34 | base.Update(); 35 | 36 | if ( containers.Count == 0 ) { 37 | stage = Stage.Start; 38 | Current = 0; 39 | return; 40 | } 41 | 42 | double contentSize = containers.Max( x => x.ContentSize ); 43 | double avaiableSize = containers.Max( x => x.AvaiableSize ); 44 | double maskedSize = contentSize - avaiableSize; 45 | 46 | if ( maskedSize <= 0 ) { 47 | stage = Stage.Start; 48 | Current = 0; 49 | return; 50 | } 51 | 52 | switch ( stage ) { 53 | case Stage.Start: 54 | stage = Stage.WaitingAfterStart; 55 | this.Delay( BeginDelay ).Then().Schedule( () => { 56 | stage = Stage.Scrolling; 57 | } ); 58 | break; 59 | case Stage.Scrolling: 60 | Current = Math.Clamp( Current + Time.Elapsed / 1000 * ScrollSpeed, 0, maskedSize ); 61 | if ( Current == maskedSize ) { 62 | stage = Stage.WaitingBeforeEnd; 63 | this.Delay( EndDelay ).Then().Schedule( () => { 64 | stage = Stage.End; 65 | } ); 66 | } 67 | break; 68 | case Stage.WaitingBeforeEnd: 69 | if ( Current < maskedSize ) { 70 | FinishTransforms(); 71 | stage = Stage.Scrolling; 72 | } 73 | break; 74 | case Stage.End: 75 | Current = Math.Clamp( Current + Time.Elapsed / 1000 * ScrollSpeed * -3, 0, maskedSize ); 76 | if ( Current == 0 ) { 77 | stage = Stage.Start; 78 | } 79 | break; 80 | } 81 | } 82 | } 83 | 84 | public class AutoScrollContainer : AutoScrollContainer { 85 | public AutoScrollContainer ( Direction direction = Direction.Horizontal ) : base( direction ) { } 86 | public AutoScrollContainer ( AutoScrollContainerSyncGroup syncGroup ) : base( syncGroup ) { } 87 | } 88 | 89 | public class AutoScrollContainer : Container where T : Drawable { 90 | public double BeginDelay { get => syncGroup.BeginDelay; set => syncGroup.BeginDelay = value; } 91 | public double EndDelay { get => syncGroup.EndDelay; set => syncGroup.EndDelay = value; } 92 | public double ScrollSpeed { get => syncGroup.ScrollSpeed; set => syncGroup.ScrollSpeed = value; } 93 | public Direction Direction => syncGroup.Direction; 94 | 95 | FillFlowContainer size; 96 | protected override Container Content => size; 97 | AutoScrollContainerSyncGroup syncGroup; 98 | public AutoScrollContainer ( Direction direction = Direction.Horizontal ) : this( new AutoScrollContainerSyncGroup( direction ) ) { 99 | AddInternal( syncGroup ); 100 | } 101 | public AutoScrollContainer ( AutoScrollContainerSyncGroup syncGroup ) { 102 | if ( syncGroup is null ) { 103 | AddInternal( syncGroup = new AutoScrollContainerSyncGroup( Direction.Horizontal ) ); 104 | } 105 | this.syncGroup = syncGroup; 106 | syncGroup?.containers.Add( this ); 107 | AddInternal( size = new FillFlowContainer { 108 | AutoSizeAxes = Axes.Both, 109 | Direction = syncGroup.Direction is Direction.Horizontal ? FillDirection.Horizontal : FillDirection.Vertical 110 | } ); 111 | Masking = true; 112 | } 113 | public double ContentSize => syncGroup.Direction is Direction.Horizontal ? size.Size.X : size.Size.Y; 114 | public double AvaiableSize => syncGroup.Direction is Direction.Horizontal ? DrawSize.X : DrawSize.Y; 115 | protected override void Update () { 116 | base.Update(); 117 | 118 | double maskedSize = ContentSize - AvaiableSize; 119 | double current = Math.Max( Math.Min( syncGroup.Current, maskedSize ), 0 ); 120 | if ( syncGroup.Direction is Direction.Horizontal ) 121 | size.X = -(float)current; 122 | else 123 | size.Y = -(float)current; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/BeatSyncedFlashingDrawable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Audio.Track; 2 | using osu.Framework.Graphics; 3 | using osu.Game.Beatmaps.ControlPoints; 4 | using osu.Game.Graphics.Containers; 5 | 6 | namespace osu.XR.Drawables.Containers { 7 | public class BeatSyncedFlashingDrawable : BeatSyncedContainer { 8 | protected override void OnNewBeat ( int beatIndex, TimingControlPoint timingPoint, EffectControlPoint effectPoint, ChannelAmplitudes amplitudes ) { 9 | this.FlashColour( Colour4.White, timingPoint.BeatLength * 0.8f, Easing.Out ); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/ExpandableSection.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Shapes; 5 | using osu.Framework.Graphics.Sprites; 6 | using osu.Game.Graphics; 7 | using osu.Game.Graphics.Containers; 8 | using osuTK; 9 | 10 | namespace osu.XR.Drawables.Containers { 11 | public class ExpandableSection : Container { 12 | OsuTextFlowContainer title; 13 | CalmOsuAnimatedButton button; 14 | FillFlowContainer layout; 15 | FillFlowContainer content; 16 | Drawable expandableElement; 17 | SpriteIcon chevron; 18 | protected override Container Content => content; 19 | 20 | public ExpandableSection () { 21 | AutoSizeAxes = Axes.Y; 22 | 23 | AddInternal( new Box { 24 | RelativeSizeAxes = Axes.Both, 25 | Colour = OsuColour.Gray( 0.025f ) 26 | } ); 27 | 28 | AddInternal( layout = new FillFlowContainer { 29 | RelativeSizeAxes = Axes.X, 30 | AutoSizeAxes = Axes.Y, 31 | Direction = FillDirection.Vertical, 32 | 33 | Children = new Drawable[] { 34 | button = new CalmOsuAnimatedButton { 35 | Margin = new MarginPadding { Vertical = 2 }, 36 | RelativeSizeAxes = Axes.X, 37 | AutoSizeAxes = Axes.Y 38 | }, 39 | expandableElement = new Container { 40 | Masking = true, 41 | RelativeSizeAxes = Axes.X, 42 | Child = content = new FillFlowContainer { 43 | RelativeSizeAxes = Axes.X, 44 | AutoSizeAxes = Axes.Y, 45 | Direction = FillDirection.Vertical 46 | } 47 | } 48 | } 49 | } ); 50 | 51 | Masking = true; 52 | CornerRadius = 5; 53 | BorderColour = OsuColour.Gray( 0.075f ); 54 | BorderThickness = 2; 55 | 56 | button.AddRange( new Drawable[] { 57 | new Box { 58 | Alpha = 0, 59 | AlwaysPresent = true, 60 | Height = 25, 61 | RelativeSizeAxes = Axes.X 62 | }, 63 | title = new OsuTextFlowContainer( x => x.AllowMultiline = true ) { 64 | AutoSizeAxes = Axes.Y, 65 | Anchor = Anchor.CentreLeft, 66 | Origin = Anchor.CentreLeft, 67 | TextAnchor = Anchor.CentreLeft, 68 | Margin = new MarginPadding { Left = 15, Vertical = 2 } 69 | }, 70 | chevron = new SpriteIcon { 71 | Icon = FontAwesome.Solid.ChevronDown, 72 | Origin = Anchor.CentreRight, 73 | Anchor = Anchor.CentreRight, 74 | Margin = new MarginPadding { Right = 15 }, 75 | Size = new Vector2( 16 ) 76 | } 77 | } ); 78 | 79 | button.Action = () => { 80 | IsExpanded.Value = !IsExpanded.Value; 81 | }; 82 | 83 | IsExpanded.BindValueChanged( v => { 84 | if ( v.NewValue ) { 85 | this.TransformBindableTo( size, 1, 300, Easing.Out ); 86 | chevron.FadeTo( 1, 100, Easing.Out ); 87 | } 88 | else { 89 | this.TransformBindableTo( size, 0, 300, Easing.Out ); 90 | chevron.FadeTo( 0.6f, 100, Easing.Out ); 91 | } 92 | }, true ); 93 | } 94 | 95 | public readonly BindableBool IsExpanded = new BindableBool( false ); 96 | BindableFloat size = new( 0 ); 97 | 98 | public string Title { 99 | set => title.Text = value; 100 | } 101 | 102 | protected override void Update () { 103 | base.Update(); 104 | Width = Parent.DrawWidth - Margin.Left - Margin.Right; 105 | expandableElement.Height = size.Value * ( content.LayoutSize.Y + 5 ); 106 | title.Width = button.DrawWidth - 60; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/FilterableContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osuTK; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace osu.XR.Drawables.Containers { 9 | public class FilterableContainer : Container, IFilterable, IHasFilterableChildren { 10 | public IEnumerable FilterTerms { get; set; } = Array.Empty(); 11 | public bool MatchingFilter { 12 | set { 13 | if ( !CanBeFiltered ) return; 14 | 15 | if ( value ) Show(); 16 | else Hide(); 17 | } 18 | } 19 | 20 | public override void Hide () { 21 | this.ScaleTo( new Vector2( 1, 0 ), 100, Easing.Out ); 22 | this.Delay( 50 ).FadeOut( 50 ); 23 | } 24 | 25 | public override void Show () { 26 | this.ScaleTo( new Vector2( 1, 1 ), 100, Easing.Out ); 27 | this.FadeIn( 50 ); 28 | } 29 | 30 | public bool FilteringActive { get; set; } 31 | public bool CanBeFiltered = true; 32 | 33 | public IEnumerable FilterableChildren 34 | => CanBeFiltered ? Children.OfType() : Array.Empty(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/FilterableFillFlowContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osuTK; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace osu.XR.Drawables.Containers { 9 | public class FilterableFillFlowContainer : FillFlowContainer, IFilterable, IHasFilterableChildren { 10 | public IEnumerable FilterTerms { get; set; } = Array.Empty(); 11 | public bool MatchingFilter { 12 | set { 13 | if ( !CanBeFiltered ) return; 14 | 15 | if ( value ) Show(); 16 | else Hide(); 17 | } 18 | } 19 | 20 | public override void Hide () { 21 | this.ScaleTo( new Vector2( 1, 0 ), 100, Easing.Out ); 22 | this.Delay( 50 ).FadeOut( 50 ); 23 | } 24 | 25 | public override void Show () { 26 | this.ScaleTo( new Vector2( 1, 1 ), 100, Easing.Out ); 27 | this.FadeIn( 50 ); 28 | } 29 | 30 | public bool FilteringActive { get; set; } 31 | public bool CanBeFiltered = true; 32 | 33 | public IEnumerable FilterableChildren 34 | => CanBeFiltered ? Children.OfType() : Array.Empty(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/LazyExpandableSection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace osu.XR.Drawables.Containers { 4 | public class LazyExpandableSection : ExpandableSection { 5 | Action onFirstExpand; 6 | public LazyExpandableSection ( Action onFirstExpand ) { 7 | this.onFirstExpand = onFirstExpand; 8 | 9 | IsExpanded.ValueChanged += onExpanded; 10 | } 11 | 12 | private void onExpanded ( Framework.Bindables.ValueChangedEvent obj ) { 13 | if ( !obj.NewValue ) return; 14 | 15 | IsExpanded.ValueChanged -= onExpanded; 16 | onFirstExpand?.Invoke( this ); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/NamedContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Extensions.TypeExtensions; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.XR.Components.Groups; 5 | 6 | namespace osu.XR.Drawables.Containers { 7 | public class NamedContainer : FillFlowContainer, IHasName { 8 | public NamedContainer () { 9 | RelativeSizeAxes = Axes.X; 10 | AutoSizeAxes = Axes.Y; 11 | Direction = FillDirection.Vertical; 12 | 13 | DisplayName = GetType().ReadableName(); 14 | } 15 | 16 | public string DisplayName { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /osu.XR/Drawables/Containers/OsuXrTooltipContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Extensions.Color4Extensions; 3 | using osu.Framework.Graphics; 4 | using osu.Framework.Graphics.Cursor; 5 | using osu.Framework.Graphics.Effects; 6 | using osu.Framework.Graphics.Shapes; 7 | using osu.Framework.Localisation; 8 | using osu.Game.Graphics; 9 | using osu.Game.Graphics.Containers; 10 | using osuTK; 11 | using osuTK.Graphics; 12 | 13 | namespace osu.XR.Drawables.Containers { 14 | public class OsuXrTooltipContainer : TooltipContainer { 15 | protected override ITooltip CreateTooltip () => new OsuXrTooltip(); 16 | 17 | public OsuXrTooltipContainer ( CursorContainer cursor ) 18 | : base( cursor ) { 19 | } 20 | 21 | protected override double AppearDelay => ( 1 - CurrentTooltip.Alpha ) * base.AppearDelay; // reduce appear delay if the tooltip is already partly visible. 22 | 23 | public class OsuXrTooltip : Tooltip { 24 | private readonly Box background; 25 | private readonly OsuTextFlowContainer text; 26 | LocalisableString current; 27 | private bool instantMovement = true; 28 | 29 | public override void SetContent ( LocalisableString contentString ) { 30 | if ( contentString == current ) return; 31 | 32 | current = contentString; 33 | text.Text = current.ToString(); 34 | 35 | if ( IsPresent ) { 36 | AutoSizeDuration = 250; 37 | background.FlashColour( OsuColour.Gray( 0.4f ), 1000, Easing.OutQuint ); 38 | } 39 | else 40 | AutoSizeDuration = 0; 41 | 42 | return; 43 | } 44 | 45 | public OsuXrTooltip () { 46 | AutoSizeEasing = Easing.OutQuint; 47 | BypassAutoSizeAxes = Axes.Both; 48 | 49 | CornerRadius = 5; 50 | Masking = true; 51 | EdgeEffect = new EdgeEffectParameters { 52 | Type = EdgeEffectType.Shadow, 53 | Colour = Color4.Black.Opacity( 40 ), 54 | Radius = 5, 55 | }; 56 | Children = new Drawable[] 57 | { 58 | background = new Box 59 | { 60 | RelativeSizeAxes = Axes.Both, 61 | Alpha = 0.9f, 62 | }, 63 | text = new OsuTextFlowContainer(f => f.Font = OsuFont.GetFont(weight: FontWeight.Regular)) 64 | { 65 | Padding = new MarginPadding(5), 66 | AutoSizeAxes = Axes.Both 67 | } 68 | }; 69 | } 70 | 71 | [BackgroundDependencyLoader] 72 | private void load ( OsuColour colour ) { 73 | background.Colour = colour.Gray3; 74 | } 75 | 76 | protected override void PopIn () { 77 | instantMovement |= !IsPresent; 78 | this.FadeIn( 500, Easing.OutQuint ); 79 | } 80 | 81 | protected override void PopOut () => this.Delay( 150 ).FadeOut( 500, Easing.OutQuint ); 82 | 83 | public override void Move ( Vector2 pos ) { 84 | if ( instantMovement ) { 85 | Position = pos; 86 | instantMovement = false; 87 | } 88 | else { 89 | this.MoveTo( pos, 200, Easing.OutQuint ); 90 | } 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /osu.XR/Drawables/UserInterface/ColorPicker.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.Game.Overlays.Settings; 4 | using osuTK.Graphics; 5 | 6 | namespace osu.XR.Drawables.UserInterface { 7 | public class ColorPicker : SettingsItem where T : ColorPickerControl, new() { 8 | public new ColorPickerControl Control => (ColorPickerControl)base.Control; 9 | protected override Drawable CreateControl () { 10 | return new T(); 11 | } 12 | 13 | public ColorPicker () { 14 | FlowContent.Direction = FillDirection.Vertical; 15 | } 16 | } 17 | 18 | public class ColorPicker : ColorPicker { } 19 | } 20 | -------------------------------------------------------------------------------- /osu.XR/Drawables/UserInterface/ColorPickerControl.SVPicker.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Audio.Sample; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Framework.Graphics.Shapes; 7 | using osu.Framework.Graphics.Sprites; 8 | using osu.Framework.Graphics.Textures; 9 | using osu.Framework.Input.Events; 10 | using osuTK; 11 | using osuTK.Graphics; 12 | using System; 13 | 14 | namespace osu.XR.Drawables.UserInterface { 15 | public partial class ColorPickerControl { 16 | private class SVPicker : CompositeDrawable { 17 | public Bindable HueBindable { get; init; } 18 | public Bindable SaturationBindable { get; init; } 19 | public Bindable ValueBindable { get; init; } 20 | public Bindable LockedBindable { get; init; } 21 | public Bindable ColorBindable { get; init; } 22 | 23 | Sprite svBox; 24 | SVNotch svNotch; 25 | 26 | public SVPicker () { 27 | InternalChildren = new Drawable[] { 28 | svBox = new Sprite { 29 | RelativeSizeAxes = Axes.Both, 30 | Texture = new Texture( (int)PICKER_SIZE, (int)PICKER_SIZE ) 31 | }, 32 | svNotch = new SVNotch { 33 | Origin = Anchor.Centre, 34 | RelativePositionAxes = Axes.Both 35 | } 36 | }; 37 | 38 | Size = new Vector2( PICKER_SIZE ); 39 | } 40 | 41 | protected override void LoadComplete () { 42 | base.LoadComplete(); 43 | HueBindable.BindValueChanged( v => { 44 | svBox.Texture.SetData( TextureGeneration.HSVBoxWithSetHue( (int)PICKER_SIZE, (int)PICKER_SIZE, v.NewValue ) ); 45 | }, true ); 46 | 47 | LockedBindable.BindValueChanged( v => { 48 | this.FadeColour( v.NewValue ? Colour4.Gray : Colour4.White, 70 ); 49 | }, true ); 50 | 51 | ColorBindable.BindValueChanged( v => { 52 | svNotch.box.Colour = v.NewValue; 53 | }, true ); 54 | 55 | SaturationBindable.BindValueChanged( v => { 56 | svNotch.MoveTo( new Vector2( SaturationBindable.Value, 1 - ValueBindable.Value ), 70 ); 57 | }, true ); 58 | ValueBindable.BindValueChanged( v => { 59 | svNotch.MoveTo( new Vector2( SaturationBindable.Value, 1 - ValueBindable.Value ), 70 ); 60 | }, true ); 61 | } 62 | 63 | Sample notchSample; 64 | [BackgroundDependencyLoader] 65 | private void load ( ISampleStore samples ) { 66 | notchSample = samples.Get( "UI/notch-tick" ); 67 | } 68 | 69 | protected override bool OnDragStart ( DragStartEvent e ) { 70 | return !LockedBindable.Value; 71 | } 72 | 73 | protected override void OnDrag ( DragEvent e ) { 74 | if ( LockedBindable.Value ) return; 75 | var pos = Parent.ToSpaceOfOtherDrawable( e.MousePosition, this ); 76 | updateAt( pos ); 77 | } 78 | 79 | protected override bool OnClick ( ClickEvent e ) { 80 | if ( LockedBindable.Value ) return false; 81 | var pos = Parent.ToSpaceOfOtherDrawable( e.MousePosition, this ); 82 | updateAt( pos ); 83 | return true; 84 | } 85 | 86 | double sampleDelay = 120; 87 | double sampleTimer; 88 | void updateAt ( Vector2 pos ) { 89 | SaturationBindable.Value = Math.Clamp( pos.X / DrawWidth, 0, 1 ); 90 | ValueBindable.Value = Math.Clamp( 1 - ( pos.Y / DrawHeight ), 0, 1 ); 91 | 92 | if ( sampleTimer > sampleDelay ) { 93 | notchSample.Play(); 94 | sampleTimer = 0; 95 | } 96 | } 97 | 98 | protected override void Update () { 99 | base.Update(); 100 | sampleTimer += Time.Elapsed; 101 | } 102 | 103 | private class SVNotch : CompositeDrawable { 104 | public Box box; 105 | public SVNotch () { 106 | AddInternal( box = new Box { 107 | RelativeSizeAxes = Axes.Both, 108 | AlwaysPresent = true 109 | } ); 110 | 111 | Size = new Vector2( 14 ); 112 | Masking = true; 113 | CornerRadius = Width / 2; 114 | BorderColour = Color4.Black; 115 | BorderThickness = 3; 116 | } 117 | } 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /osu.XR/Editor/IGripable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | 3 | namespace osu.XR.Editor { 4 | public interface IGripable { 5 | Bindable CanBeGripped { get; } 6 | Bindable AllowsGripMovement { get; } 7 | Bindable AllowsGripScaling { get; } 8 | Bindable AllowsGripRotation { get; } 9 | 10 | void OnGripped ( object source, GripGroup group ); 11 | void OnGripReleased ( object source, GripGroup group ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /osu.XR/Editor/PropContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Framework.XR.Components; 5 | using osu.Game.Overlays.Settings; 6 | using osu.XR.Inspector; 7 | using osu.XR.Settings.Sections; 8 | 9 | #nullable enable 10 | 11 | namespace osu.XR.Editor { 12 | public class PropContainer : CompositeDrawable3D, IConfigurableInspectable { 13 | public readonly Drawable3D Prop; 14 | new public string Name { 15 | get => NameBindable.Value; 16 | set => NameBindable.Value = value; 17 | } 18 | public readonly Bindable NameBindable; 19 | 20 | public PropContainer ( Drawable3D prop, string name = "New Prop" ) { 21 | Prop = prop; 22 | InternalChild = prop; 23 | AutoSizeAxes = Axes3D.All; 24 | NameBindable = new Bindable( name ); 25 | } 26 | 27 | public Drawable CreateInspectorSubsection () { 28 | var section = new SettingsSectionContainer { 29 | Title = "Imported Prop", 30 | Icon = FontAwesome.Solid.Cube, 31 | 32 | Children = new Drawable[] { 33 | new SettingsTextBox { 34 | LabelText = "Name", 35 | Current = NameBindable, 36 | Margin = new MarginPadding { Bottom = 5 } 37 | } 38 | } 39 | }; 40 | 41 | if ( Prop is Collider co ) { 42 | Bindable colliderEnabled = new( false ); 43 | section.Add( new SettingsCheckbox { 44 | LabelText = "Enable blocking touch pointers", 45 | TooltipText = "Touch pointers can easily get stuck inside props while gripping them.\nThis setting should be disable while doing so.", 46 | Current = colliderEnabled 47 | } ); 48 | 49 | colliderEnabled.BindValueChanged( v => { 50 | co.PhysicsLayer = v.NewValue ? GamePhysicsLayer.All : GamePhysicsLayer.Prop; 51 | }, true ); 52 | } 53 | 54 | if ( Prop is IGripable gr ) { 55 | section.AddRange( new Drawable[] { 56 | new SettingsCheckbox { 57 | LabelText = "Allow Gripping", 58 | Current = gr.CanBeGripped 59 | }, 60 | new SettingsCheckbox { 61 | LabelText = "Allow Grip Movement", 62 | Current = gr.AllowsGripMovement 63 | }, 64 | new SettingsCheckbox { 65 | LabelText = "Allow Grip Rotation", 66 | Current = gr.AllowsGripRotation 67 | }, 68 | new SettingsCheckbox { 69 | LabelText = "Allow Grip Scaling", 70 | Current = gr.AllowsGripScaling 71 | }, 72 | } ); 73 | } 74 | 75 | section.Add( new DangerousSettingsButton { 76 | Text = "Delete", 77 | Action = () => { 78 | Destroy(); 79 | }, 80 | Margin = new MarginPadding { Top = 6 } 81 | } ); 82 | 83 | return section; 84 | } 85 | 86 | public bool AreSettingsPersistent => false; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /osu.XR/Editor/SceneComponentEditor.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.XR.Settings.Sections; 3 | 4 | namespace osu.XR.Editor { 5 | /// 6 | /// Given you want to edit a component that does not implement its own inspector setting subsection, you can use this class instead 7 | /// 8 | public abstract class SceneComponentEditor : SettingsSection { 9 | public readonly Bindable EditedElementBindable = new(); 10 | public T EditedElement { 11 | get => EditedElementBindable.Value; 12 | set => EditedElementBindable.Value = value; 13 | } 14 | 15 | public SceneComponentEditor ( T editedElement = default ) { 16 | EditedElementBindable.ValueChanged += v => OnEditedElementChanged( v.OldValue, v.NewValue ); 17 | EditedElementBindable.Value = editedElement; 18 | } 19 | 20 | protected abstract void OnEditedElementChanged ( T previous, T next ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /osu.XR/Editor/SceneContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.XR.Components; 2 | 3 | namespace osu.XR.Editor { 4 | public class SceneContainer : Container3D { 5 | public SceneContainer () { 6 | Name = "Scene"; 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /osu.XR/Extensions.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.XR.Inspector; 3 | using System; 4 | #nullable enable 5 | 6 | namespace osu.XR { 7 | public static class Extensions { 8 | public static IInspectable? GetClosestInspectable ( this Drawable self ) 9 | => ( self.IsInspectable() && self is IInspectable inspectable ) ? inspectable : self.Parent?.GetClosestInspectable(); 10 | 11 | public static string GetInspectorName ( this Drawable drawable ) 12 | => string.IsNullOrWhiteSpace( drawable.Name ) ? drawable.GetType().Name : drawable.Name; 13 | 14 | public static Drawable? GetValidInspectable ( this Drawable self ) 15 | => self.IsInspectable() ? self : self.Parent?.GetValidInspectable(); 16 | 17 | public static bool DoParentsAllowInspection ( this Drawable self ) 18 | => self.Parent is null ? true : ( self.Parent is IChildrenNotInspectable ? false : self.Parent.DoParentsAllowInspection() ); 19 | 20 | public static bool IsInspectable ( this Drawable self ) 21 | => self is not ISelfNotInspectable && self.DoParentsAllowInspection(); 22 | 23 | static readonly string[] SiUnits = new[] { "B", "kiB", "MiB", "GiB", "TiB" }; 24 | public static string HumanizeSiBytes ( this int bytes ) 25 | => HumanizeSiBytes( (long)bytes ); 26 | public static string HumanizeSiBytes ( this long bytes ) { 27 | if ( bytes == 0 ) return "0B"; 28 | if ( bytes < 0 ) return '-' + HumanizeSiBytes( -bytes ); 29 | 30 | int order = (int)Math.Floor( Math.Log( (double)bytes, 1024 ) ); 31 | order = Math.Clamp( order, 0, SiUnits.Length - 1 ); 32 | double display = bytes / Math.Pow( 1024, order ); 33 | 34 | return $"{display:0.##}{SiUnits[order]}"; 35 | } 36 | 37 | public static float CopySign ( this float value, float sign ) 38 | => Math.Abs( value ) * Math.Sign( sign ); 39 | 40 | public static string Pluralize ( this int number, string name, string? plural = null ) { 41 | if ( number == 1 ) { 42 | return $"{number} {name}"; 43 | } 44 | else { 45 | if ( plural == null ) { 46 | if ( name.ToLower().EndsWith( "sh" ) ) { 47 | plural = name + "es"; 48 | } 49 | else { 50 | plural = name + "s"; 51 | } 52 | } 53 | return $"{number} {plural}"; 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /osu.XR/GamePhysicsLayer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.XR.Physics; 2 | 3 | namespace osu.XR { 4 | public static class GamePhysicsLayer { 5 | public static readonly PhysicsLayer All = PhysicsLayer.All; 6 | public static readonly PhysicsLayer Floor = PhysicsLayer.Layer0; 7 | public static readonly PhysicsLayer Prop = PhysicsLayer.Layer1; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /osu.XR/Graphics/NeonColors.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osuTK.Graphics; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | #nullable enable 7 | 8 | namespace osu.XR.Graphics { 9 | public static class NeonColors { 10 | public static Color4 NextRandom ( Random random ) { 11 | return ParseHex( hexCodes[ random.Next( 0, hexCodes.Count ) ] ); 12 | } 13 | 14 | private static Colour4 ParseHex ( string hex ) { 15 | if ( Colour4.TryParseHex( hex, out var color ) ) return color; 16 | else throw new InvalidOperationException( $"Invalid hex color `{hex}`" ); 17 | } 18 | 19 | private static List hexCodes = new() { 20 | "#dfff11", 21 | "#66ff00", 22 | "#ff08e8", 23 | "#fe01b1", 24 | "#be03fd", 25 | "#ff000d", 26 | "#ffcf09", 27 | "#fc0e34", 28 | "#01f9c6", 29 | "#ff003f", 30 | "#0ff0fc", 31 | "#fc74fd", 32 | "#21fc0d", 33 | "#6600ff", 34 | "#ccff00", 35 | "#ff3503", 36 | "#ff0490", 37 | "#bf00ff", 38 | "#e60000", 39 | "#55ffff", 40 | "#8f00f1", 41 | "#fffc00", 42 | "#08ff08", 43 | "#ffcf00", 44 | "#fe1493", 45 | "#ff5555", 46 | "#fc8427", 47 | "#00fdff", 48 | "#ccff02", 49 | "#ff11ff", 50 | "#04d9ff", 51 | "#ff9933", 52 | "#fe4164", 53 | "#39ff14", 54 | "#fe019a", 55 | "#bc13fe", 56 | "#ff073a", 57 | "#cfff04", 58 | "#ff0055" 59 | }; 60 | 61 | public static readonly Color4 BrightChartreuse = ParseHex( "#dfff11" ); 62 | public static readonly Color4 BrightGreen = ParseHex( "#66ff00" ); 63 | public static readonly Color4 BrightMagenta = ParseHex( "#ff08e8" ); 64 | public static readonly Color4 BrightPink = ParseHex( "#fe01b1" ); 65 | public static readonly Color4 BrightPurple = ParseHex( "#be03fd" ); 66 | public static readonly Color4 BrightRed = ParseHex( "#ff000d" ); 67 | public static readonly Color4 BrightSaffron = ParseHex( "#ffcf09" ); 68 | public static readonly Color4 BrightScarlet = ParseHex( "#fc0e34" ); 69 | public static readonly Color4 BrightTeal = ParseHex( "#01f9c6" ); 70 | public static readonly Color4 ElectricCrimson = ParseHex( "#ff003f" ); 71 | public static readonly Color4 ElectricCyan = ParseHex( "#0ff0fc" ); 72 | public static readonly Color4 ElectricFlamingo = ParseHex( "#fc74fd" ); 73 | public static readonly Color4 ElectricGreen = ParseHex( "#21fc0d" ); 74 | public static readonly Color4 ElectricIndigo = ParseHex( "#6600ff" ); 75 | public static readonly Color4 ElectricLime = ParseHex( "#ccff00" ); 76 | public static readonly Color4 ElectricOrange = ParseHex( "#ff3503" ); 77 | public static readonly Color4 ElectricPink = ParseHex( "#ff0490" ); 78 | public static readonly Color4 ElectricPurple = ParseHex( "#bf00ff" ); 79 | public static readonly Color4 ElectricRed = ParseHex( "#e60000" ); 80 | public static readonly Color4 ElectricSheep = ParseHex( "#55ffff" ); 81 | public static readonly Color4 ElectricViolet = ParseHex( "#8f00f1" ); 82 | public static readonly Color4 ElectricYellow = ParseHex( "#fffc00" ); 83 | public static readonly Color4 FluorescentGreen = ParseHex( "#08ff08" ); 84 | public static readonly Color4 FluorescentOrange = ParseHex( "#ffcf00" ); 85 | public static readonly Color4 FluorescentPink = ParseHex( "#fe1493" ); 86 | public static readonly Color4 FluorescentRed = ParseHex( "#ff5555" ); 87 | public static readonly Color4 FluorescentRedOrange = ParseHex( "#fc8427" ); 88 | public static readonly Color4 FluorescentTurquoise = ParseHex( "#00fdff" ); 89 | public static readonly Color4 FluorescentYellow = ParseHex( "#ccff02" ); 90 | public static readonly Color4 LightNeonPink = ParseHex( "#ff11ff" ); 91 | public static readonly Color4 NeonBlue = ParseHex( "#04d9ff" ); 92 | public static readonly Color4 NeonCarrot = ParseHex( "#ff9933" ); 93 | public static readonly Color4 NeonFuchsia = ParseHex( "#fe4164" ); 94 | public static readonly Color4 NeonGreen = ParseHex( "#39ff14" ); 95 | public static readonly Color4 NeonPink = ParseHex( "#fe019a" ); 96 | public static readonly Color4 NeonPurple = ParseHex( "#bc13fe" ); 97 | public static readonly Color4 NeonRed = ParseHex( "#ff073a" ); 98 | public static readonly Color4 NeonYellow = ParseHex( "#cfff04" ); 99 | public static readonly Color4 PinkishRedNeon = ParseHex( "#ff0055" ); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/ButtonBinding.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using OpenVR.NET.Manifests; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Game.Graphics.Sprites; 7 | using osu.XR.Input.Custom.Components; 8 | using osu.XR.Input.Custom.Persistence; 9 | using osu.XR.Settings; 10 | 11 | namespace osu.XR.Input.Custom { 12 | public class ButtonBinding : CustomBinding { 13 | public readonly Hand Hand; 14 | public override string Name => $"{Hand} Buttons"; 15 | public ButtonBinding ( Hand hand ) { 16 | Hand = hand; 17 | PrimaryAction.ValueChanged += v => OnSettingsChanged(); 18 | SecondaryAction.ValueChanged += v => OnSettingsChanged(); 19 | } 20 | 21 | public readonly Bindable PrimaryAction = new(); 22 | public readonly Bindable SecondaryAction = new(); 23 | public override CustomBindingHandler CreateHandler () 24 | => new ButtonBindingHandler( this ); 25 | 26 | public override object CreateSaveData ( SaveDataContext context ) 27 | => new { 28 | Primary = context.SaveActionBinding( PrimaryAction.Value ), 29 | Secondary = context.SaveActionBinding( SecondaryAction.Value ), 30 | }; 31 | 32 | public override void Load ( JToken data, SaveDataContext context ) { 33 | PrimaryAction.Value = context.LoadActionBinding( data, "Primary" ); 34 | SecondaryAction.Value = context.LoadActionBinding( data, "Secondary" ); 35 | } 36 | } 37 | 38 | public class ButtonBindingHandler : CustomBindingHandler { 39 | public readonly RulesetActionBinding primaryBinding = new(); 40 | public readonly RulesetActionBinding secondaryBinding = new(); 41 | 42 | BoundComponent primary; 43 | BoundComponent secondary; 44 | public ButtonBindingHandler ( ButtonBinding backing ) : base( backing ) { 45 | AddInternal( primary = new( XrAction.MouseLeft, x => x?.Role == OsuGameXr.RoleForHand( backing.Hand ) ) ); 46 | AddInternal( secondary = new( XrAction.MouseRight, x => x?.Role == OsuGameXr.RoleForHand( backing.Hand ) ) ); 47 | 48 | primaryBinding.IsActive.BindTo( primary.Current ); 49 | secondaryBinding.IsActive.BindTo( secondary.Current ); 50 | primaryBinding.RulesetAction.BindTo( backing.PrimaryAction ); 51 | secondaryBinding.RulesetAction.BindTo( backing.SecondaryAction ); 52 | 53 | primaryBinding.Press += TriggerPress; 54 | secondaryBinding.Press += TriggerPress; 55 | primaryBinding.Release += TriggerRelease; 56 | secondaryBinding.Release += TriggerRelease; 57 | } 58 | 59 | public override CustomBindingDrawable CreateSettingsDrawable () 60 | => new ButtonBindingDrawable( this ); 61 | } 62 | 63 | public class ButtonBindingDrawable : CustomBindingDrawable { 64 | ButtonSetup primary; 65 | ButtonSetup secondary; 66 | 67 | public ButtonBindingDrawable ( ButtonBindingHandler handler ) : base( handler ) { 68 | RelativeSizeAxes = Axes.X; 69 | AutoSizeAxes = Axes.Y; 70 | 71 | AddInternal( new FillFlowContainer { 72 | Direction = FillDirection.Vertical, 73 | RelativeSizeAxes = Axes.X, 74 | AutoSizeAxes = Axes.Y, 75 | Children = new Drawable[] { 76 | primary = new ButtonSetup( true ), 77 | secondary = new ButtonSetup( false ) 78 | } 79 | } ); 80 | 81 | primary.Indicator.IsActive.BindTo( handler.primaryBinding.IsActive ); 82 | primary.ActionDropdown.RulesetAction.BindTo( handler.primaryBinding.RulesetAction ); 83 | secondary.Indicator.IsActive.BindTo( handler.secondaryBinding.IsActive ); 84 | secondary.ActionDropdown.RulesetAction.BindTo( handler.secondaryBinding.RulesetAction ); 85 | } 86 | 87 | private class ButtonSetup : FillFlowContainer { 88 | public ActivationIndicator Indicator; 89 | public RulesetActionDropdown ActionDropdown; 90 | public ButtonSetup ( bool isPrimary ) { 91 | Direction = FillDirection.Vertical; 92 | RelativeSizeAxes = Axes.X; 93 | AutoSizeAxes = Axes.Y; 94 | Children = new Drawable[] { 95 | new Container { 96 | RelativeSizeAxes = Axes.X, 97 | AutoSizeAxes = Axes.Y, 98 | Children = new Drawable[] { 99 | new OsuSpriteText { Text = $"{(isPrimary ? "Primary" : "Secondary")} Button", Margin = new MarginPadding { Left = 16 } }, 100 | Indicator = new ActivationIndicator { 101 | Margin = new MarginPadding { Right = 16 }, 102 | Origin = Anchor.CentreRight, 103 | Anchor = Anchor.CentreRight 104 | } 105 | } 106 | }, 107 | ActionDropdown = new RulesetActionDropdown() 108 | }; 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Components/ActivationIndicator.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Shapes; 4 | 5 | namespace osu.XR.Input.Custom.Components { 6 | public class ActivationIndicator : Circle { 7 | public readonly BindableBool IsActive = new(); 8 | public ActivationIndicator () { 9 | Colour = Colour4.HotPink.Darken( 0.9f ); 10 | Size = new osuTK.Vector2( 30, 16 ); 11 | 12 | IsActive.BindValueChanged( v => { 13 | if ( v.NewValue ) { 14 | this.FadeColour( Colour4.HotPink ); 15 | this.FlashColour( Colour4.White, 200 ); 16 | this.ResizeWidthTo( 45, 100, Easing.Out ); 17 | } 18 | else { 19 | this.FadeColour( Colour4.HotPink.Darken( 0.9f ), 200 ); 20 | this.ResizeWidthTo( 30, 100, Easing.Out ); 21 | } 22 | } ); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Components/BoundComponent.cs: -------------------------------------------------------------------------------- 1 | using OpenVR.NET; 2 | using OpenVR.NET.Manifests; 3 | using osu.Framework.Bindables; 4 | using osu.Framework.Graphics; 5 | using System; 6 | 7 | namespace osu.XR.Input.Custom.Components { 8 | public class BoundComponent : Drawable where T : ControllerInputComponent where Tvalue : struct, IEquatable { 9 | event System.Action onDispose; 10 | protected override void Dispose ( bool isDisposing ) { 11 | base.Dispose( isDisposing ); 12 | 13 | onDispose?.Invoke(); 14 | onDispose = null; 15 | } 16 | 17 | public readonly Bindable Current = new(); 18 | 19 | public BoundComponent ( XrAction action, Func predicate ) { 20 | bool found = false; 21 | 22 | void lookForValidController ( Controller controller ) { 23 | if ( found ) return; 24 | if ( !predicate( controller ) ) return; 25 | found = true; 26 | 27 | var comp = VR.GetControllerComponent( action, controller ); 28 | System.Action> listener = v => { 29 | Current.Value = v.NewValue; 30 | }; 31 | comp.BindValueChangedDetailed( listener, true ); 32 | onDispose += () => comp.ValueChanged -= listener; 33 | VR.NewControllerAdded -= lookForValidController; 34 | } 35 | 36 | VR.BindComponentsLoaded( () => { 37 | VR.BindNewControllerAdded( lookForValidController, true ); 38 | lookForValidController( null ); 39 | } ); 40 | } 41 | } 42 | 43 | public class BoundComponent : Drawable where T : ControllerInputComponent where Tvalue : struct, IEquatable { 44 | event System.Action onDispose; 45 | protected override void Dispose ( bool isDisposing ) { 46 | base.Dispose( isDisposing ); 47 | 48 | onDispose?.Invoke(); 49 | onDispose = null; 50 | } 51 | 52 | public readonly Bindable Current = new(); 53 | 54 | public BoundComponent ( XrAction action, Func predicate, Func converter ) { 55 | bool found = false; 56 | 57 | void lookForValidController ( Controller controller ) { 58 | if ( found ) return; 59 | if ( !predicate( controller ) ) return; 60 | found = true; 61 | 62 | var comp = VR.GetControllerComponent( action, controller ); 63 | System.Action> listener = v => { 64 | Current.Value = converter( v.NewValue ); 65 | }; 66 | comp.BindValueChangedDetailed( listener, true ); 67 | onDispose += () => comp.ValueChanged -= listener; 68 | VR.NewControllerAdded -= lookForValidController; 69 | } 70 | 71 | VR.BindComponentsLoaded( () => { 72 | VR.BindNewControllerAdded( lookForValidController, true ); 73 | lookForValidController( null ); 74 | } ); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Components/RulesetActionBinding.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using System; 3 | 4 | namespace osu.XR.Input.Custom.Components { 5 | public class RulesetActionBinding { 6 | public readonly BindableBool IsActive = new(); 7 | public readonly Bindable RulesetAction = new(); 8 | public RulesetActionBinding () { 9 | IsActive.BindValueChanged( v => { 10 | if ( RulesetAction.Value == null ) return; 11 | if ( v.NewValue ) 12 | Press?.Invoke( RulesetAction.Value ); 13 | else 14 | Release?.Invoke( RulesetAction.Value ); 15 | } ); 16 | RulesetAction.BindValueChanged( v => { 17 | if ( IsActive.Value ) { 18 | if ( v.OldValue != null ) 19 | Release?.Invoke( v.OldValue ); 20 | if ( v.NewValue != null ) 21 | Press?.Invoke( v.NewValue ); 22 | } 23 | } ); 24 | } 25 | public event Action Press; 26 | public event Action Release; 27 | } 28 | } -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Components/RulesetActionDropdown.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Extensions; 4 | using osu.Game.Overlays.Settings; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace osu.XR.Input.Custom.Components { 9 | public class RulesetActionDropdown : SettingsDropdown { 10 | [Resolved] 11 | protected List rulesetActions { get; private set; } 12 | 13 | public readonly Bindable RulesetAction = new(); 14 | bool bindlock; 15 | 16 | public RulesetActionDropdown () { 17 | Current = new Bindable( "None" ); 18 | 19 | RulesetAction.ValueChanged += v => { 20 | if ( bindlock ) return; 21 | bindlock = true; 22 | Current.Value = ( v.NewValue is null ) ? Current.Default : v.NewValue.GetDescription(); 23 | bindlock = false; 24 | }; 25 | } 26 | 27 | protected override void LoadComplete () { 28 | base.LoadComplete(); 29 | Items = rulesetActions.Select( x => x.GetDescription() ).Prepend( "None" ); 30 | Current.BindValueChanged( v => { 31 | if ( bindlock ) return; 32 | bindlock = true; 33 | RulesetAction.Value = ( v.NewValue == Current.Default ) ? null : rulesetActions.FirstOrDefault( x => x.GetDescription() == v.NewValue ); 34 | bindlock = false; 35 | } ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/InjectedInput.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Input; 5 | using osu.Framework.Input.Bindings; 6 | using osu.Framework.XR.Input; 7 | using osu.Game.Rulesets.Mods; 8 | using osu.Game.Rulesets.UI; 9 | using osu.Game.Screens.Play; 10 | using osuTK; 11 | using System; 12 | using System.Collections.Generic; 13 | using System.Collections.Specialized; 14 | 15 | namespace osu.XR.Input.Custom { 16 | public struct PlayerInfo { 17 | public Player Player; 18 | public DrawableRuleset DrawableRuleset; 19 | public PassThroughInputManager InputManager; 20 | public Type RulesetActionType; 21 | public KeyBindingContainer KeyBindingContainer; 22 | public Bindable> Mods; 23 | public int Variant; 24 | } 25 | 26 | public class InjectedInput : CompositeDrawable { 27 | Dictionary handlers = new(); 28 | public readonly PlayerInfo Info; 29 | BindableList inputs; 30 | 31 | public InjectedInput ( BindableList inputs, PlayerInfo info ) { 32 | RelativeSizeAxes = Axes.Both; 33 | Info = info; 34 | this.inputs = inputs; 35 | 36 | inputs.BindCollectionChanged( inputsChanged, true ); 37 | 38 | Info.InputManager.GetMethod( "AddHandler" ).Invoke( info.InputManager, new object[] { mouseHandler } ); 39 | } 40 | 41 | private void inputsChanged ( object _, NotifyCollectionChangedEventArgs a ) { 42 | if ( a.Action == NotifyCollectionChangedAction.Add ) { 43 | if ( a.NewItems is null ) return; 44 | 45 | foreach ( CustomBinding i in a.NewItems ) { 46 | var handler = i.CreateHandler(); 47 | handlers.Add( i, handler ); 48 | AddInternal( handler ); 49 | } 50 | } 51 | else { 52 | if ( a.OldItems is null ) return; 53 | 54 | foreach ( CustomBinding i in a.OldItems ) { 55 | handlers.Remove( i, out var handler ); 56 | RemoveInternal( handler ); 57 | } 58 | } 59 | } 60 | 61 | protected override void Dispose ( bool isDisposing ) { 62 | base.Dispose( isDisposing ); 63 | inputs.CollectionChanged -= inputsChanged; 64 | } 65 | 66 | public void TriggerPress ( object action, CustomBindingHandler source ) { 67 | if ( action is null ) return; 68 | 69 | Info.KeyBindingContainer.GetMethod( nameof( KeyBindingContainer.TriggerPressed ) ).Invoke( Info.KeyBindingContainer, new object[] { action } ); 70 | } 71 | 72 | public void TriggerRelease ( object action, CustomBindingHandler source ) { 73 | if ( action is null ) return; 74 | 75 | Info.KeyBindingContainer.GetMethod( nameof( KeyBindingContainer.TriggerReleased ) ).Invoke( Info.KeyBindingContainer, new object[] { action } ); 76 | } 77 | 78 | VirtualMouseHandler mouseHandler = new(); 79 | public void MoveTo ( Vector2 position, bool isNormalized = false ) { 80 | var quad = Info.InputManager.ScreenSpaceDrawQuad; 81 | 82 | if ( Info.InputManager.UseParentInput ) { 83 | Info.InputManager.UseParentInput = false; 84 | 85 | mousePos = quad.Size / 2; 86 | } 87 | 88 | 89 | if ( isNormalized ) { 90 | var scale = Math.Min( quad.Width, quad.Height ) / 2; 91 | position *= scale; 92 | } 93 | 94 | mouseHandler.EmulateMouseMove( mousePos = position + quad.Size / 2 ); 95 | } 96 | 97 | Vector2 mousePos; 98 | public void MoveBy ( Vector2 position, bool isNormalized = false ) { 99 | var quad = Info.InputManager.ScreenSpaceDrawQuad; 100 | 101 | if ( Info.InputManager.UseParentInput ) { 102 | Info.InputManager.UseParentInput = false; 103 | 104 | mousePos = quad.Size / 2; 105 | } 106 | 107 | if ( isNormalized ) { 108 | var scale = Math.Min( quad.Width, quad.Height ) / 2; 109 | position *= scale; 110 | } 111 | 112 | mousePos += position; 113 | mousePos = new Vector2( 114 | Math.Clamp( mousePos.X, 0, quad.Width ), 115 | Math.Clamp( mousePos.Y, 0, quad.Height ) 116 | ); 117 | mouseHandler.EmulateMouseMove( mousePos ); 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Persistence/ActionBinding.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace osu.XR.Input.Custom.Persistence { 4 | public sealed class ActionBinding { 5 | [JsonProperty( Order = 1 )] 6 | public string Name { get; init; } 7 | [JsonProperty( Order = 2 )] 8 | public int ID { get; init; } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Persistence/BindingData.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | 5 | namespace osu.XR.Input.Custom.Persistence { 6 | public sealed class BindingData { 7 | [JsonProperty( Order = 1 )] 8 | public string FormatVersion { get; init; } = "[Initial]"; 9 | 10 | [JsonProperty( Order = 2 )] 11 | public string Type { get; init; } 12 | [JsonProperty( Order = 3 )] 13 | public object Data; 14 | 15 | public static BindingData Load ( JToken token ) { 16 | var (version, root) = token.GetFormatVersion(); 17 | 18 | if ( version == "[Initial]" ) { 19 | return new() { 20 | FormatVersion = version, 21 | Type = (string)root[ "Type" ], 22 | Data = root[ "Data" ] 23 | }; 24 | } 25 | else { 26 | throw new Exception( "Invalid format version" ); 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Persistence/RulesetBindings.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace osu.XR.Input.Custom.Persistence { 7 | public sealed class RulesetBindings { 8 | [JsonProperty( Order = 1 )] 9 | public string FormatVersion { get; init; } = "[Initial]"; 10 | 11 | [JsonProperty( Order = 2 )] 12 | public string Name { get; init; } 13 | 14 | [JsonProperty( Order = 3 )] 15 | public readonly Dictionary VariantNames = new(); 16 | [JsonProperty( Order = 4 )] 17 | public readonly Dictionary Variants = new(); 18 | 19 | public static RulesetBindings Load ( JToken token ) { 20 | var (version, root) = token.GetFormatVersion(); 21 | 22 | if ( version == "[Initial]" ) { 23 | RulesetBindings ruleset = new() { 24 | FormatVersion = version, 25 | Name = (string)root[ "Name" ] 26 | }; 27 | 28 | foreach ( var (index, name) in root[ "VariantNames" ].ToObject>() ) { 29 | ruleset.VariantNames.Add( index, name ); 30 | } 31 | 32 | foreach ( var (index, variant) in root[ "Variants" ].ToObject>() ) { 33 | ruleset.Variants.Add( index, RulesetVariantBindings.Load( variant as JToken ) ); 34 | } 35 | 36 | return ruleset; 37 | } 38 | else { 39 | throw new Exception( "Invalid ruleset format version" ); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Persistence/RulesetBindingsFile.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | 7 | namespace osu.XR.Input.Custom.Persistence { 8 | public sealed class RulesetBindingsFile { 9 | [JsonProperty( Order = 1 )] 10 | public string FormatVersion { get; init; } = "[Initial]"; 11 | 12 | [JsonProperty( Order = 2 )] 13 | public string Name = "Ruleset bindings for osu!XR"; 14 | [JsonProperty( Order = 3 )] 15 | public string Description = "Exported ruleset bindings"; 16 | 17 | [JsonProperty( Order = 4 )] 18 | public readonly List Rulesets = new(); 19 | 20 | /// 21 | /// Performs a shallow merge where only the missing rulesets are added. 22 | /// 23 | public RulesetBindingsFile MergeMissingFrom ( RulesetBindingsFile other ) { 24 | foreach ( var i in other.Rulesets.Where( x => !Rulesets.Any( y => y.Name == x.Name ) ) ) { 25 | Rulesets.Add( i ); 26 | } 27 | 28 | return this; 29 | } 30 | 31 | public static RulesetBindingsFile Load ( JToken token ) { 32 | var (version,root) = token.GetFormatVersion(); 33 | 34 | if ( version == "[Initial]" ) { 35 | RulesetBindingsFile file = new() { 36 | FormatVersion = version, 37 | Name = (string)root[ "Name" ], 38 | Description = (string)root[ "Description" ] 39 | }; 40 | 41 | foreach ( var ruleset in ( root[ "Rulesets" ] as JArray ) ) { 42 | file.Rulesets.Add( RulesetBindings.Load( ruleset ) ); 43 | } 44 | 45 | return file; 46 | } 47 | else { 48 | throw new Exception( "Invalid format version" ); 49 | } 50 | } 51 | } 52 | 53 | public static class JsonExtensions { 54 | public static (string version, JObject root) GetFormatVersion ( this JToken token ) { 55 | if ( token is not JObject root || root[ "FormatVersion" ] is not JToken version || version.Type != JTokenType.String ) 56 | throw new Exception( "Could not find format version" ); 57 | 58 | return ((string)version, root); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Persistence/RulesetVariantBindings.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace osu.XR.Input.Custom.Persistence { 7 | public sealed class RulesetVariantBindings { 8 | [JsonProperty( Order = 1 )] 9 | public string FormatVersion { get; init; } = "[Initial]"; 10 | 11 | [JsonProperty( Order = 2 )] 12 | public string Name { get; init; } 13 | 14 | [JsonProperty( Order = 3 )] 15 | public readonly Dictionary Actions = new(); 16 | [JsonProperty( Order = 4 )] 17 | public readonly List Bindings = new(); 18 | 19 | public static RulesetVariantBindings Load ( JToken token ) { 20 | var (version, root) = token.GetFormatVersion(); 21 | 22 | if ( version == "[Initial]" ) { 23 | RulesetVariantBindings variant = new() { 24 | FormatVersion = version, 25 | Name = (string)root[ "Name" ] 26 | }; 27 | 28 | foreach ( var (index,name) in root[ "Actions" ].ToObject>() ) { 29 | variant.Actions.Add( index, name ); 30 | } 31 | 32 | foreach ( var binding in root[ "Bindings" ] as JArray ) { 33 | variant.Bindings.Add( BindingData.Load( binding ) ); 34 | } 35 | 36 | return variant; 37 | } 38 | else { 39 | throw new Exception( "Invalid format version" ); 40 | } 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /osu.XR/Input/Custom/Persistence/SaveDataContext.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using osu.Framework.Extensions; 3 | using osu.XR.Drawables; 4 | using System.Linq; 5 | 6 | namespace osu.XR.Input.Custom.Persistence { 7 | public class SaveDataContext { 8 | RulesetVariantXrBindingsSubsection section; 9 | public SaveDataContext ( RulesetVariantXrBindingsSubsection section ) { 10 | this.section = section; 11 | } 12 | 13 | public int RulesetActionIndexOf ( object rulesetAction ) 14 | => section.RulesetActions.IndexOf( rulesetAction ); 15 | 16 | public ActionBinding SaveActionBinding ( object action ) 17 | => new ActionBinding { 18 | ID = RulesetActionIndexOf( action ), 19 | Name = action?.GetDescription() 20 | }; 21 | 22 | public object LoadActionBinding ( JToken token, string name ) { 23 | var id = ( token as JObject )[ name ].ToObject().ID; 24 | 25 | return section.RulesetActions.ElementAtOrDefault( id ); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /osu.XR/Input/Pointers/Pointer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.XR.Components; 4 | using osu.Framework.XR.Physics; 5 | using osu.Framework.XR.Projection; 6 | using System; 7 | using static osu.Framework.XR.Physics.Raycast; 8 | 9 | namespace osu.XR.Input.Pointers { 10 | public abstract class Pointer : Model, IRenderedLast { 11 | [Resolved] 12 | protected PhysicsSystem PhysicsSystem { get; private set; } 13 | 14 | protected RaycastHit RaycastHit; 15 | private IHasCollider currentHit; 16 | /// 17 | /// The this pointer points at. Might be null. 18 | /// 19 | public IHasCollider CurrentHit { 20 | get => currentHit; 21 | protected set { 22 | if ( value != currentHit ) { 23 | var prev = currentHit; 24 | currentHit = value; 25 | if ( currentHit != null ) CurrentFocus = value; 26 | HitChanged?.Invoke( new( prev, currentHit ) ); 27 | } 28 | if ( value != null ) NewHit?.Invoke( RaycastHit ); 29 | else NoHit?.Invoke(); 30 | } 31 | } 32 | 33 | private IHasCollider currentFocus; 34 | /// 35 | /// The last non-null . 36 | /// 37 | public IHasCollider CurrentFocus { 38 | get => currentFocus; 39 | private set { 40 | if ( value == currentFocus ) return; 41 | var prev = currentFocus; 42 | currentFocus = value; 43 | 44 | FocusChanged?.Invoke( new( prev, currentFocus ) ); 45 | } 46 | } 47 | 48 | private bool wasActive = false; 49 | public abstract bool IsActive { get; } 50 | protected sealed override void Update () { 51 | base.Update(); 52 | if ( !IsActive ) { 53 | if ( wasActive ) { 54 | RaycastHit = default; 55 | wasActive = false; 56 | 57 | var oldFocus = currentFocus; 58 | var oldHit = currentHit; 59 | currentHit = null; 60 | currentFocus = null; 61 | FocusChanged?.Invoke( new( oldFocus, null ) ); 62 | HitChanged?.Invoke( new( oldHit, null ) ); 63 | NewHit?.Invoke( default ); 64 | } 65 | return; 66 | } 67 | 68 | wasActive = true; 69 | UpdatePointer(); 70 | } 71 | 72 | protected abstract void UpdatePointer (); 73 | 74 | public event Action> FocusChanged; 75 | public event Action> HitChanged; 76 | 77 | public delegate void PointerUpdate ( RaycastHit hit ); 78 | public event PointerUpdate NewHit; 79 | public event Action NoHit; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /osu.XR/Input/Pointers/RaycastPointer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.XR.Graphics; 3 | using osu.Framework.XR.Maths; 4 | using osuTK; 5 | 6 | namespace osu.XR.Input.Pointers { 7 | /// 8 | /// A 3D cursor. 9 | /// 10 | public class RaycastPointer : Pointer { 11 | public Transform Source; 12 | 13 | public double HitDistance { get => HitDistanceBindable.Value; set => HitDistanceBindable.Value = value; } 14 | public readonly BindableDouble HitDistanceBindable = new( 5 ); 15 | 16 | public RaycastPointer () { // TODO warp towards the held location 17 | Mesh = new(); 18 | Mesh.AddCircle( new Vector3( 0, 0, -0.01f ), Vector3.UnitZ, Vector3.UnitX * 0.04f, 30 ); 19 | Mesh.AddCircle( new Vector3( 0, 0, -0.02f ), Vector3.UnitZ, Vector3.UnitX * 0.014f, 30 ); 20 | 21 | ShouldBeDepthSorted = true; 22 | } 23 | protected override void UpdatePointer () { 24 | if ( PhysicsSystem.TryHit( Source.Position, Source.Forward, out var hit ) && hit.Distance < HitDistance ) { 25 | Position = hit.Point; 26 | 27 | Rotation = hit.Normal.LookRotation(); 28 | 29 | RaycastHit = hit; 30 | CurrentHit = hit.Collider; 31 | } 32 | else { 33 | Position = Source.Position + Source.Forward * (float)HitDistance; 34 | Rotation = Source.Rotation; 35 | 36 | RaycastHit = hit; 37 | CurrentHit = null; 38 | } 39 | 40 | Scale = new Vector3( ( Position - Source.Position ).Length ); 41 | } 42 | 43 | public override bool IsActive => Source is not null && IsVisible; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /osu.XR/Input/Pointers/TouchPointer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.XR.Graphics; 3 | using osu.Framework.XR.Maths; 4 | using osu.Framework.XR.Physics; 5 | using osuTK; 6 | using System.Threading.Tasks; 7 | 8 | namespace osu.XR.Input.Pointers { 9 | public class TouchPointer : Pointer { 10 | public Transform Source; 11 | public double Radius { get => RadiusBindable.Value; set => RadiusBindable.Value = value; } 12 | public readonly BindableDouble RadiusBindable = new( 0.023 ); 13 | 14 | public TouchPointer () { 15 | Task.Run( () => { 16 | var mesh = Mesh.FromOBJFile( "./Resources/shpere.obj" ); 17 | Schedule( () => Mesh = mesh ); 18 | } ); 19 | ShouldBeDepthSorted = true; 20 | } 21 | 22 | protected override void UpdatePointer () { // TODO back and forward motion should trigger a tap even while blocked 23 | var targetPos = Source.Position; // ISSUE this can get stuck behind the screen 24 | 25 | var direction = ( targetPos - Position ).Normalized(); 26 | if ( PhysicsSystem.TryHit( Position, direction, out var rayHit, layer: GamePhysicsLayer.All.Except( GamePhysicsLayer.Floor | GamePhysicsLayer.Prop ) ) && rayHit.Distance - Radius / 2 < ( Position - targetPos ).Length ) { 27 | Position = rayHit.Point + rayHit.Normal * (float)Radius / 2; 28 | } 29 | else { 30 | Position = targetPos; 31 | } 32 | 33 | Scale = new Vector3( (float)Radius ); 34 | if ( PhysicsSystem.TryHit( Position, Radius, out var hit, layer: GamePhysicsLayer.All.Except( GamePhysicsLayer.Floor ) ) ) { 35 | RaycastHit = new Raycast.RaycastHit( 36 | point: hit.Point, 37 | origin: hit.Origin, 38 | normal: ( hit.Origin - hit.Point ).Normalized(), 39 | direction: hit.Point - hit.Origin, 40 | distance: hit.Distance, 41 | trisIndex: hit.TrisIndex, 42 | collider: hit.Collider 43 | ); 44 | CurrentHit = hit.Collider; 45 | } 46 | else { 47 | RaycastHit = default; 48 | CurrentHit = null; 49 | } 50 | } 51 | 52 | public override bool IsActive => Source is not null && IsVisible; 53 | } 54 | } 55 | 56 | /* 57 | TODO handheld/movable screen (grip binding) 58 | TODO cursor warping (at least figure out which vertices to warp) 59 | */ -------------------------------------------------------------------------------- /osu.XR/Inspector/Editors/Color4Editor.cs: -------------------------------------------------------------------------------- 1 | using osu.XR.Drawables.UserInterface; 2 | using osuTK.Graphics; 3 | 4 | namespace osu.XR.Inspector.Editors { 5 | public class Color4Editor : ValueEditor { 6 | ColorPicker colorPicker; 7 | public Color4Editor ( Color4 defaultValue = default ) : base( defaultValue ) { 8 | Add( colorPicker = new ColorPicker { } ); 9 | colorPicker.Current.BindTo( Current ); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Editors/EnumEditor.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Game.Overlays.Settings; 3 | using System; 4 | using System.Collections.Generic; 5 | 6 | namespace osu.XR.Inspector.Editors { 7 | public class EnumEditor : ValueEditor where T : struct, Enum { 8 | public EnumEditor ( T defaultValue ) : base( defaultValue ) { 9 | if ( typeof( T ).IsDefined( typeof( FlagsAttribute ), false ) ) { 10 | makeFlags(); 11 | } 12 | else { 13 | makeChoice(); 14 | } 15 | } 16 | 17 | void makeFlags () { 18 | List<(T flag, Bindable isActive)> flags = new(); 19 | foreach ( T i in Enum.GetValues() ) { 20 | dynamic value = i; 21 | if ( value != 0 && ( value & value - 1 ) == 0 ) { 22 | SettingsCheckbox cb; 23 | Add( cb = new SettingsCheckbox { 24 | LabelText = i.ToString(), 25 | Current = new BindableBool( Current.Value.HasFlag( i ) ) 26 | } ); 27 | flags.Add( (i, cb.Current) ); 28 | cb.Current.ValueChanged += _ => flagChanged( flags ); 29 | } 30 | else { 31 | // TODO multiflags 32 | } 33 | } 34 | } 35 | 36 | private void flagChanged ( List<(T flag, Bindable isActive)> flags ) { 37 | dynamic value = default( T ); 38 | foreach ( var (flag, isActive) in flags ) { 39 | if ( isActive.Value ) 40 | value |= flag; 41 | } 42 | Current.Value = value; 43 | } 44 | 45 | void makeChoice () { 46 | Add( new SettingsEnumDropdown { 47 | Current = Current 48 | } ); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Editors/TextfieldEditor.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Game.Graphics.UserInterface; 3 | using osuTK.Graphics; 4 | using System; 5 | 6 | namespace osu.XR.Inspector.Editors { 7 | public class TextfieldEditor : ValueEditor { 8 | OsuTextBox textbox; 9 | Func parser; 10 | 11 | public TextfieldEditor ( Func parser, T defaultValue = default ) : base( defaultValue ) { 12 | this.parser = parser; 13 | Add( textbox = new OsuTextBox { 14 | Margin = new MarginPadding { Horizontal = 15 } 15 | } ); 16 | textbox.OnUpdate += _ => textbox.Width = DrawWidth - 30; 17 | 18 | textbox.Current.Value = Current.Value?.ToString() ?? ""; 19 | textbox.Current.ValueChanged += textChanged; 20 | } 21 | 22 | private void textChanged ( Framework.Bindables.ValueChangedEvent v ) { 23 | var (couldParse, result) = parser( v.NewValue ); 24 | if ( couldParse ) { 25 | Current.Value = result; 26 | textbox.FadeColour( Color4.White, 200 ); 27 | } 28 | else { 29 | textbox.FadeColour( Color4.Red, 200 ); 30 | } 31 | } 32 | 33 | protected override void HandleException ( Exception e ) { 34 | base.HandleException( e ); 35 | textbox.FadeColour( Color4.Red, 200 ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Editors/ToggleEditor.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Overlays.Settings; 2 | 3 | namespace osu.XR.Inspector.Editors { 4 | public class ToggleEditor : ValueEditor { 5 | SettingsCheckbox checkbox; 6 | public ToggleEditor ( bool value = default ) : base( value ) { 7 | Add( checkbox = new SettingsCheckbox { 8 | Current = Current 9 | } ); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Editors/ValueEditor.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osuTK; 5 | using osuTK.Graphics; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Reflection; 9 | 10 | namespace osu.XR.Inspector.Editors { 11 | public abstract class ValueEditor : FillFlowContainer { 12 | public readonly Bindable Current = new(); 13 | 14 | public ValueEditor () { 15 | RelativeSizeAxes = Axes.X; 16 | AutoSizeAxes = Axes.Y; 17 | Direction = FillDirection.Vertical; 18 | } 19 | 20 | public void OnException ( Exception e ) { 21 | if ( e is TargetInvocationException ) 22 | e = e.InnerException; 23 | HandleException( e ); 24 | } 25 | 26 | protected virtual void HandleException ( Exception e ) { 27 | OpenVR.NET.Events.Exception( e, e.Message ); 28 | } 29 | 30 | private static readonly Dictionary> editors = new() { 31 | [ typeof( int ) ] = 32 | v => new TextfieldEditor( s => { 33 | if ( int.TryParse( s, out var v ) ) 34 | return (true, v); 35 | return (false, 0); 36 | }, (int)v ), 37 | 38 | [ typeof( float ) ] = 39 | v => new TextfieldEditor( s => { 40 | if ( float.TryParse( s, out var v ) ) 41 | return (true, v); 42 | return (false, 0); 43 | }, (float)v ), 44 | 45 | [ typeof( double ) ] = 46 | v => new TextfieldEditor( s => { 47 | if ( double.TryParse( s, out var v ) ) 48 | return (true, v); 49 | return (false, 0); 50 | }, (double)v ), 51 | 52 | [ typeof( string ) ] = 53 | v => new TextfieldEditor( s => (true, s), v as string ), 54 | 55 | [ typeof( bool ) ] = 56 | v => new ToggleEditor( (bool)v ), 57 | 58 | [ typeof( Vector2 ) ] = 59 | v => new Vector2Editor( (Vector2)v ), 60 | 61 | [ typeof( Vector3 ) ] = 62 | v => new Vector3Editor( (Vector3)v ), 63 | 64 | [ typeof( Color4 ) ] = 65 | v => new Color4Editor( (Color4)v ) 66 | }; 67 | 68 | public static bool HasEditorFor ( Type t ) { 69 | return t.IsEnum || editors.ContainsKey( t ); 70 | } 71 | public static ValueEditor GetEditorFor ( Type t, object value = default ) { 72 | return t.IsEnum 73 | ? Activator.CreateInstance( typeof( EnumEditor<> ).MakeGenericType( t ), new object[] { value } ) as ValueEditor 74 | : editors[ t ]( value ); 75 | } 76 | } 77 | 78 | public abstract class ValueEditor : ValueEditor { 79 | public ValueEditor ( T defaultValue = default ) { 80 | Current = new Bindable( defaultValue ); 81 | Current.BindValueChanged( v => base.Current.Value = v.NewValue ); 82 | } 83 | 84 | new public readonly Bindable Current; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Editors/Vector2Editor.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Game.Graphics.UserInterface; 3 | using osuTK; 4 | using osuTK.Graphics; 5 | 6 | namespace osu.XR.Inspector.Editors { 7 | public class Vector2Editor : ValueEditor { 8 | OsuTextBox textboxX; 9 | OsuTextBox textboxY; 10 | public Vector2Editor ( Vector2 defaultValue = default ) : base( defaultValue ) { 11 | Add( textboxX = new OsuTextBox { 12 | Margin = new MarginPadding { Horizontal = 15 }, 13 | PlaceholderText = "X" 14 | } ); 15 | textboxX.OnUpdate += _ => textboxX.Width = DrawWidth - 30; 16 | Add( textboxY = new OsuTextBox { 17 | Margin = new MarginPadding { Horizontal = 15 }, 18 | PlaceholderText = "Y" 19 | } ); 20 | textboxY.OnUpdate += _ => textboxY.Width = DrawWidth - 30; 21 | 22 | textboxX.Current.Value = Current.Value.X.ToString(); 23 | textboxY.Current.Value = Current.Value.Y.ToString(); 24 | 25 | textboxX.Current.ValueChanged += _ => textChanged(); 26 | textboxY.Current.ValueChanged += _ => textChanged(); 27 | } 28 | 29 | private void textChanged () { 30 | bool ok = true; 31 | 32 | if ( !float.TryParse( textboxX.Current.Value, out var x ) ) { 33 | textboxX.FadeColour( Color4.Red, 200 ); 34 | ok = false; 35 | } 36 | else { 37 | textboxX.FadeColour( Color4.White, 200 ); 38 | } 39 | 40 | if ( !float.TryParse( textboxY.Current.Value, out var y ) ) { 41 | textboxY.FadeColour( Color4.Red, 200 ); 42 | ok = false; 43 | } 44 | else { 45 | textboxY.FadeColour( Color4.White, 200 ); 46 | } 47 | 48 | if ( ok ) { 49 | Current.Value = new Vector2( x, y ); 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Editors/Vector3Editor.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Game.Graphics.UserInterface; 3 | using osuTK; 4 | using osuTK.Graphics; 5 | 6 | namespace osu.XR.Inspector.Editors { 7 | public class Vector3Editor : ValueEditor { 8 | OsuTextBox textboxX; 9 | OsuTextBox textboxY; 10 | OsuTextBox textboxZ; 11 | public Vector3Editor ( Vector3 defaultValue = default ) : base( defaultValue ) { 12 | Add( textboxX = new OsuTextBox { 13 | Margin = new MarginPadding { Horizontal = 15 }, 14 | PlaceholderText = "X" 15 | } ); 16 | textboxX.OnUpdate += _ => textboxX.Width = DrawWidth - 30; 17 | Add( textboxY = new OsuTextBox { 18 | Margin = new MarginPadding { Horizontal = 15 }, 19 | PlaceholderText = "Y" 20 | } ); 21 | textboxY.OnUpdate += _ => textboxY.Width = DrawWidth - 30; 22 | Add( textboxZ = new OsuTextBox { 23 | Margin = new MarginPadding { Horizontal = 15 }, 24 | PlaceholderText = "Z" 25 | } ); 26 | textboxY.OnUpdate += _ => textboxZ.Width = DrawWidth - 30; 27 | 28 | textboxX.Current.Value = Current.Value.X.ToString(); 29 | textboxY.Current.Value = Current.Value.Y.ToString(); 30 | textboxZ.Current.Value = Current.Value.Z.ToString(); 31 | 32 | textboxX.Current.ValueChanged += _ => textChanged(); 33 | textboxY.Current.ValueChanged += _ => textChanged(); 34 | textboxZ.Current.ValueChanged += _ => textChanged(); 35 | } 36 | 37 | private void textChanged () { 38 | bool ok = true; 39 | 40 | if ( !float.TryParse( textboxX.Current.Value, out var x ) ) { 41 | textboxX.FadeColour( Color4.Red, 200 ); 42 | ok = false; 43 | } 44 | else { 45 | textboxX.FadeColour( Color4.White, 200 ); 46 | } 47 | 48 | if ( !float.TryParse( textboxY.Current.Value, out var y ) ) { 49 | textboxY.FadeColour( Color4.Red, 200 ); 50 | ok = false; 51 | } 52 | else { 53 | textboxY.FadeColour( Color4.White, 200 ); 54 | } 55 | 56 | if ( !float.TryParse( textboxZ.Current.Value, out var z ) ) { 57 | textboxZ.FadeColour( Color4.Red, 200 ); 58 | ok = false; 59 | } 60 | else { 61 | textboxZ.FadeColour( Color4.White, 200 ); 62 | } 63 | 64 | if ( ok ) { 65 | Current.Value = new Vector3( x, y, z ); 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /osu.XR/Inspector/IInspectable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.XR.Drawables.Containers; 5 | using osu.XR.Settings.Sections; 6 | using osuTK; 7 | 8 | #nullable enable 9 | 10 | namespace osu.XR.Inspector { 11 | /// 12 | /// An object whose properties can be inspected. 13 | /// 14 | public interface IInspectable { } 15 | 16 | /// 17 | /// An object whose properties can be inspected and that has custom inspector subsections. 18 | /// 19 | public interface IConfigurableInspectable { 20 | Drawable CreateInspectorSubsection (); 21 | bool AreSettingsPersistent { get; } 22 | } 23 | 24 | /// 25 | /// An object that should never be seen in the inspector. Its children will still be visible. 26 | /// 27 | public interface ISelfNotInspectable { } 28 | /// 29 | /// An object whose children should never be seen in the inspector. 30 | /// 31 | public interface IChildrenNotInspectable { } 32 | /// 33 | /// An object fully invisible to the inspector. 34 | /// 35 | public interface INotInspectable : ISelfNotInspectable, IChildrenNotInspectable { } 36 | 37 | // TODO IHasInspectorVisuals will be able to render things when selected by the inspector. 38 | public interface IHasInspectorVisuals { } 39 | 40 | /// 41 | /// This object is experimental. 42 | /// 43 | public interface IExperimental { } 44 | 45 | 46 | public static class IConfigurableInspectableExtensions { 47 | public static Drawable? CreateWarnings ( this IConfigurableInspectable self ) { 48 | if ( !self.AreSettingsPersistent ) { 49 | return new BasicFormatedText { 50 | RelativeSizeAxes = Axes.X, 51 | AutoSizeAxes = Axes.Y, 52 | Text = "||Warning:|| Some or all of these settings are **not** persistent\n~~They will not be saved after you close the game~~", 53 | TextAnchor = Anchor.Centre 54 | }; 55 | } 56 | else return null; 57 | } 58 | } 59 | 60 | public class InspectorSubsectionWithCurrent : SettingsSectionContainer { 61 | BindableWithCurrent current = new(); 62 | public readonly BindableBool ShowWarningsBindable = new( true ); 63 | public Bindable Current { 64 | get => current; 65 | set => current.Current = value; 66 | } 67 | public bool ShowWarnings { 68 | get => ShowWarningsBindable.Value; 69 | set => ShowWarningsBindable.Value = value; 70 | } 71 | 72 | private FillFlowContainer content; 73 | protected override Container Content => content; 74 | private Drawable? subsection; 75 | private Drawable? warning; 76 | public InspectorSubsectionWithCurrent () { 77 | AddInternal( content = new FillFlowContainer { 78 | RelativeSizeAxes = Axes.X, 79 | AutoSizeAxes = Axes.Y, 80 | Direction = FillDirection.Vertical 81 | } ); 82 | } 83 | 84 | protected override void LoadComplete () { 85 | base.LoadComplete(); 86 | 87 | current.BindValueChanged( v => { 88 | if ( subsection is not null ) RemoveInternal( subsection ); 89 | if ( warning is not null ) RemoveInternal( warning ); 90 | subsection = null; 91 | this.warning = null; 92 | if ( v.NewValue is IConfigurableInspectable config ) { 93 | if ( config.CreateWarnings() is Drawable warning ) { 94 | AddInternal( this.warning = warning ); 95 | if ( !ShowWarnings ) warning.Scale = Vector2.UnitX; 96 | } 97 | AddInternal( subsection = config.CreateInspectorSubsection() ); 98 | } 99 | }, true ); 100 | 101 | ShowWarningsBindable.BindValueChanged( v => { 102 | warning?.ScaleTo( v.NewValue ? Vector2.One : Vector2.UnitX, 100 ); 103 | } ); 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Reflections/Extensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace osu.XR.Inspector.Reflections { 7 | public static class Extensions { 8 | 9 | static readonly Type[] simpleTypes = new Type[] { 10 | typeof(string), 11 | typeof(decimal), 12 | typeof(DateTime), 13 | typeof(DateTimeOffset), 14 | typeof(TimeSpan), 15 | typeof(Guid) 16 | }; 17 | public static bool IsSimpleType ( this Type type ) { 18 | return type.IsPrimitive 19 | || type.IsEnum 20 | || type.IsValueType // these can be decomposed but its meh 21 | || simpleTypes.Contains( type ) 22 | || type.IsGenericType && type.GetGenericTypeDefinition() == typeof( Nullable<> ) && type.GetGenericArguments()[ 0 ].IsSimpleType(); 23 | } 24 | 25 | static IEnumerable> getDeclaredValues ( object obj, Type type ) { 26 | foreach ( var i in type.GetProperties( BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly ) ) { 27 | if ( i.GetGetMethod() != null ) yield return new ReflectedValue( obj, i ); 28 | } 29 | 30 | foreach ( var i in type.GetFields( BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly ) ) { 31 | yield return new ReflectedValue( obj, i ); 32 | } 33 | } 34 | 35 | public static IEnumerable<(Type type, IEnumerable> values)> GetDeclaredSections ( this object obj ) { 36 | Type type = obj.GetType(); 37 | 38 | while ( type != null && type != typeof( object ) ) { 39 | var declared = getDeclaredValues( obj, type ); 40 | if ( declared.Any() ) yield return (type, declared); 41 | 42 | type = type.BaseType; 43 | } 44 | } 45 | 46 | public static Type GetBaseType ( this Type type, Type targetType ) { 47 | do { 48 | if ( targetType.IsGenericTypeDefinition && type.IsGenericType && targetType == type.GetGenericTypeDefinition() ) { 49 | return type; 50 | } 51 | if ( type == targetType ) return type; 52 | 53 | type = type.BaseType; 54 | } while ( type != null ); 55 | 56 | return null; 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Reflections/ReflectionsInspector.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.XR.Components.Groups; 4 | using System; 5 | using System.Linq; 6 | 7 | namespace osu.XR.Inspector.Reflections { 8 | public class ReflectionsInspector : CompositeDrawable, IHasName { 9 | public Func ValueGetter { get; private set; } 10 | public object TargetValue { 11 | get { 12 | try { 13 | return ValueGetter(); 14 | } 15 | catch ( Exception e ) { 16 | ShouldUpdate = false; 17 | return e; 18 | } 19 | } 20 | } 21 | public Action ValueSetter { get; private set; } 22 | public Func TypeGetter { get; private set; } 23 | public Type TargetType => TypeGetter( TargetValue ); 24 | public Func NameGetter; 25 | public string TargetName => NameGetter( TargetValue ); 26 | public bool IsValueEditable { get; private set; } 27 | 28 | public ReflectionsInspector ( object value = null, string name = null ) { 29 | RelativeSizeAxes = Axes.X; 30 | AutoSizeAxes = Axes.Y; 31 | 32 | IsValueEditable = false; 33 | SetValue( value, name ); 34 | } 35 | public ReflectionsInspector ( ReflectedValue reflectedValue ) { 36 | RelativeSizeAxes = Axes.X; 37 | AutoSizeAxes = Axes.Y; 38 | 39 | IsValueEditable = !reflectedValue.IsReadonly; 40 | SetSource( reflectedValue ); 41 | } 42 | 43 | public void SetValue ( object value, string name = null ) { 44 | ValueGetter = () => value; 45 | ValueSetter = null; 46 | TypeGetter = v => v?.GetType() ?? typeof( object ); 47 | NameGetter = _ => name ?? "Value"; 48 | ShouldUpdate = true; 49 | updateValue(); 50 | } 51 | 52 | public void SetSource ( ReflectedValue reflectedValue ) { 53 | ValueGetter = reflectedValue.Getter; 54 | ValueSetter = reflectedValue.Setter; 55 | TypeGetter = _ => reflectedValue.DeclaredType; 56 | NameGetter = _ => reflectedValue.DeclaredName; 57 | ShouldUpdate = true; 58 | updateValue(); 59 | } 60 | 61 | public double UpdateInterval = 100; 62 | public bool ShouldUpdate = true; 63 | double time; 64 | 65 | protected override void Update () { 66 | base.Update(); 67 | if ( ShouldUpdate ) { 68 | time += Time.Elapsed; 69 | if ( time >= UpdateInterval ) { 70 | time = 0; 71 | updateValue(); 72 | } 73 | } 74 | } 75 | 76 | ReflectionsInspectorComponent current { 77 | get => ( InternalChildren.Any() ? InternalChild : null ) as ReflectionsInspectorComponent; 78 | set => InternalChild = value; 79 | } 80 | 81 | object previousValue = null; 82 | void updateValue () { 83 | var value = TargetValue; 84 | if ( current is not null && ( value?.Equals( previousValue ) == true || previousValue == value ) ) return; 85 | previousValue = value; 86 | 87 | if ( value is null or Exception ) { 88 | if ( current is not ReflectionsInspectorPrimitive ) 89 | current = new ReflectionsInspectorPrimitive( this ); 90 | else current.UpdateValue(); 91 | return; 92 | } 93 | 94 | var type = value.GetType(); 95 | 96 | if ( type.IsSimpleType() || ReflectionsInspectorPrimitive.CanEdit( type ) ) { 97 | if ( current is not ReflectionsInspectorPrimitive ) 98 | current = new ReflectionsInspectorPrimitive( this ); 99 | else current.UpdateValue(); 100 | return; 101 | } 102 | 103 | if ( current is not ReflectionsInspectorComposite ) 104 | current = new ReflectionsInspectorComposite( this ); 105 | else current.UpdateValue(); 106 | return; 107 | } 108 | 109 | public string DisplayName => "Reflections"; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Reflections/ReflectionsInspectorComponent.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using System; 4 | using System.Reflection; 5 | 6 | namespace osu.XR.Inspector.Reflections { 7 | public abstract class ReflectionsInspectorComponent : CompositeDrawable { 8 | protected readonly ReflectionsInspector Source; 9 | public ReflectionsInspectorComponent ( ReflectionsInspector source ) { 10 | RelativeSizeAxes = Axes.X; 11 | AutoSizeAxes = Axes.Y; 12 | Source = source; 13 | } 14 | 15 | public abstract void UpdateValue (); 16 | 17 | protected string StringifiedValue { 18 | get { 19 | var value = Source.TargetValue; 20 | 21 | if ( value is Exception e ) { 22 | if ( e is TargetInvocationException ) 23 | return e.InnerException.Message; 24 | else 25 | return e.Message; 26 | } 27 | else if ( value is null ) 28 | return "Null"; 29 | else 30 | return value.ToString(); 31 | } 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Reflections/ReflectionsInspectorComposite.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Extensions.TypeExtensions; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.XR.Drawables.Containers; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace osu.XR.Inspector.Reflections { 9 | public class ReflectionsInspectorComposite : ReflectionsInspectorComponent { 10 | public ReflectionsInspectorComposite ( ReflectionsInspector source ) : base( source ) { 11 | UpdateValue(); 12 | } 13 | 14 | public override void UpdateValue () { 15 | ClearInternal( true ); 16 | var sections = Source.TargetValue.GetDeclaredSections(); 17 | LazyExpandableSection section; 18 | AddInternal( section = new LazyExpandableSection( v => { 19 | var addNames = sections.Count() > 1; 20 | 21 | void addSection ( IEnumerable> values, Container container ) { 22 | foreach ( var i in values.OrderBy( v => v.DeclaredType.IsSimpleType() ? 1 : 2 ).ThenBy( v => v.DeclaredName ) ) { 23 | container.Add( new ReflectionsInspector( i ) ); 24 | } 25 | } 26 | 27 | foreach ( var (type, values) in sections ) { 28 | if ( addNames ) { 29 | v.Add( new LazyExpandableSection( d => { 30 | addSection( values, d ); 31 | } ) { 32 | Title = type.ReadableName() 33 | } ); 34 | } 35 | else { 36 | addSection( values, v ); 37 | } 38 | } 39 | } ) { 40 | Title = $"{Source.TargetName} [{Source.TargetType.ReadableName()}]: {StringifiedValue}", 41 | Margin = new MarginPadding { Horizontal = 5 } 42 | } ); 43 | 44 | section.OnUpdate += _ => { 45 | section.Title = $"{Source.TargetName} [{Source.TargetType.ReadableName()}]: {StringifiedValue}"; 46 | }; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Reflections/ReflectionsInspectorPrimitive.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Extensions.Color4Extensions; 3 | using osu.Framework.Extensions.TypeExtensions; 4 | using osu.Framework.Graphics; 5 | using osu.Framework.Graphics.Containers; 6 | using osu.Framework.Graphics.Shapes; 7 | using osu.Framework.Graphics.Sprites; 8 | using osu.Game.Graphics.Containers; 9 | using osu.Game.Graphics.UserInterface; 10 | using osu.XR.Inspector.Editors; 11 | using osuTK; 12 | using osuTK.Graphics; 13 | using System; 14 | 15 | namespace osu.XR.Inspector.Reflections { 16 | public class ReflectionsInspectorPrimitive : ReflectionsInspectorComponent { 17 | OsuTextFlowContainer text; 18 | Circle background; 19 | 20 | FillFlowContainer flow; 21 | Container container; 22 | public ReflectionsInspectorPrimitive ( ReflectionsInspector source ) : base( source ) { 23 | AddInternal( flow = new FillFlowContainer { 24 | RelativeSizeAxes = Axes.X, 25 | AutoSizeAxes = Axes.Y, 26 | Direction = FillDirection.Vertical, 27 | Children = new Drawable[] { 28 | container = new Container { 29 | RelativeSizeAxes = Axes.X, 30 | AutoSizeAxes = Axes.Y, 31 | Children = new Drawable[] { 32 | background = new Circle { 33 | RelativeSizeAxes = Axes.Y, 34 | AlwaysPresent = true, 35 | Colour = Color4.Transparent, 36 | Margin = new MarginPadding { Left = 10 } 37 | }, 38 | text = new OsuTextFlowContainer { 39 | Margin = new MarginPadding { Horizontal = 15 }, 40 | AutoSizeAxes = Axes.Y 41 | } 42 | } 43 | } 44 | } 45 | } ); 46 | 47 | if ( source.IsValueEditable && source.TargetValue is not Exception ) { 48 | if ( source.ValueSetter != null && ValueEditor.HasEditorFor( source.TargetType ) ) { 49 | makeEditable( ValueEditor.GetEditorFor( source.TargetType, source.TargetValue ), v => source.ValueSetter( v ) ); 50 | } 51 | else { 52 | var bindableType = source.TargetType.GetBaseType( typeof( Bindable<> ) ); 53 | if ( bindableType is not null && ValueEditor.HasEditorFor( bindableType.GenericTypeArguments[ 0 ] ) ) { 54 | object value = source.TargetValue.GetProperty( "Value" ); 55 | makeEditable( ValueEditor.GetEditorFor( bindableType.GenericTypeArguments[ 0 ], value ), v => source.TargetValue.SetProperty( "Value", v ) ); 56 | 57 | OnUpdate += _ => { 58 | object newValue = source.TargetValue.GetProperty( "Value" ); 59 | if ( value is null ? newValue is not null : !value.Equals( newValue ) ) { 60 | value = newValue; 61 | UpdateValue(); 62 | } 63 | }; 64 | } 65 | } 66 | } 67 | 68 | UpdateValue(); 69 | } 70 | 71 | public static bool CanEdit ( Type t ) { 72 | if ( ValueEditor.HasEditorFor( t ) ) return true; 73 | var bindableType = t.GetBaseType( typeof( Bindable<> ) ); 74 | if ( bindableType is not null && ValueEditor.HasEditorFor( bindableType.GenericTypeArguments[ 0 ] ) ) return true; 75 | return false; 76 | } 77 | 78 | void makeEditable ( ValueEditor editor, Action action ) { 79 | flow.Add( editor ); 80 | editor.Current.ValueChanged += v => { 81 | try { 82 | action( v.NewValue ); 83 | } 84 | catch { } 85 | }; 86 | 87 | editor.Alpha = 0; 88 | editor.Scale = new Vector2( 1, 0 ); 89 | bool editorVisible = false; 90 | 91 | container.Add( new OsuButton { 92 | Height = 15, 93 | Width = 60, 94 | Text = "Edit", 95 | Anchor = Anchor.CentreRight, 96 | Origin = Anchor.CentreRight, 97 | Margin = new MarginPadding { Right = 15 }, 98 | Action = () => { 99 | editorVisible = !editorVisible; 100 | if ( editorVisible ) { 101 | editor.ScaleTo( new Vector2( 1, 1 ), 200 ); 102 | editor.FadeIn( 200 ); 103 | } 104 | else { 105 | editor.ScaleTo( new Vector2( 1, 0 ), 200 ); 106 | editor.FadeOut( 200 ); 107 | } 108 | } 109 | } ); 110 | } 111 | 112 | protected override void Update () { 113 | base.Update(); 114 | 115 | text.Width = flow.DrawWidth - 30; 116 | } 117 | 118 | public override void UpdateValue () { 119 | text.Clear( true ); 120 | text.AddText( $"{Source.TargetName} ", v => v.Colour = Color4.GreenYellow ); 121 | text.AddText( $"[{Source.TargetType.ReadableName()}]: ", v => v.Colour = Color4.LimeGreen ); 122 | if ( Source.TargetValue is Exception ) { 123 | text.AddText( StringifiedValue, v => v.Colour = Color4.Red ); 124 | } 125 | else { 126 | if ( Source.TargetType == typeof( Color4 ) ) { 127 | text.AddIcon( FontAwesome.Solid.Palette, v => v.Colour = (Color4)Source.TargetValue ); 128 | } 129 | else { 130 | text.AddText( StringifiedValue ); 131 | } 132 | } 133 | 134 | background.FlashColour( Color4.HotPink.Opacity( 0.6f ), 500, Easing.In ); 135 | background.Width = DrawWidth - 20; 136 | } 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Selection2D.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.Framework.Graphics.Primitives; 4 | using osu.Framework.Graphics.Shaders; 5 | using osu.Framework.Graphics.Shapes; 6 | using osu.Framework.XR.Components; 7 | using osuTK; 8 | 9 | namespace osu.XR.Inspector { 10 | public class Selection2D : Box, INotInspectable { 11 | public Selection2D () { 12 | Colour = Colour4.HotPink; 13 | Alpha = 0.3f; 14 | } 15 | 16 | Drawable selected; 17 | public Drawable Selected { 18 | get => selected; 19 | set => Select( value ); 20 | } 21 | public void Select ( Drawable drawable ) { 22 | if ( selected == drawable ) return; 23 | 24 | selected = drawable; 25 | 26 | ( Parent as Container )?.Remove( this ); 27 | if ( selected is null ) return; 28 | Container container = null; 29 | while ( drawable.Parent is not null and not Drawable3D ) { 30 | drawable = drawable.Parent; 31 | if ( drawable is Container c && ( IsLoaded || c.Dependencies.Get( typeof( ShaderManager ) ) is not null ) ) container = c; 32 | } 33 | 34 | container?.Add( this ); 35 | this.FlashColour( Colour4.White, 200, Easing.In ); 36 | this.FadeTo( 0.6f ).FadeTo( 0.3f, 200, Easing.In ); 37 | } 38 | 39 | public override bool ReceivePositionalInputAt ( Vector2 screenSpacePos ) 40 | => false; 41 | 42 | protected override Quad ComputeScreenSpaceDrawQuad () 43 | => selected?.ScreenSpaceDrawQuad ?? base.ComputeScreenSpaceDrawQuad(); 44 | 45 | protected override void Update () { 46 | base.Update(); 47 | Invalidate( Invalidation.DrawInfo ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /osu.XR/Inspector/Selection3D.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.XR.Components; 4 | using osu.Framework.XR.Graphics; 5 | using osu.Framework.XR.Projection; 6 | using osuTK; 7 | using osuTK.Graphics; 8 | 9 | namespace osu.XR.Inspector { 10 | public class Selection3D : Model, INotInspectable, IRenderedLast { 11 | public Selection3D () { 12 | Mesh = Mesh.FromOBJFile( "./Resources/selection.obj" ); 13 | Tint = Color4.Lime; 14 | 15 | AutoOffsetOrigin = Vector3.Zero; 16 | } 17 | 18 | private BindableFloat animationProgress = new( 1 ); 19 | Drawable3D selected; 20 | Drawable3D over; 21 | public Drawable3D Selected { 22 | get => selected; 23 | set => Select( value ); 24 | } 25 | public void Select ( Drawable3D drawable ) { 26 | if ( selected == drawable ) return; 27 | 28 | selected = drawable; 29 | if ( selected is null ) { 30 | this.TransformBindableTo( animationProgress, 0, 140, Easing.Out ); 31 | } 32 | else { 33 | over = selected; 34 | animationProgress.Value = 0; 35 | this.TransformBindableTo( animationProgress, 1, 140, Easing.Out ); 36 | ( Parent as Container3D )?.Remove( this ); 37 | selected.Root.Add( this ); 38 | Transform.SetParent( selected.Transform, transformKey ); 39 | } 40 | } 41 | 42 | protected override void Update () { 43 | base.Update(); 44 | 45 | if ( over is null ) { 46 | return; 47 | } 48 | 49 | Scale = over.RequiredParentSizeToFit * 1.03f * ( 1 + ( 1 - animationProgress.Value ) * 0.3f ); 50 | Alpha = animationProgress.Value; 51 | Position = over.Centre; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /osu.XR/Panels/ChangelogPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Sprites; 3 | using osu.XR.Drawables; 4 | using osu.XR.Panels; 5 | 6 | namespace osu.XR.Components.Panels { 7 | public class ChangelogPanel : HandheldPanel { 8 | protected override ChangelogDrawable CreateContent () 9 | => new(); 10 | 11 | public override string DisplayName => "Changelog"; 12 | public override Drawable CreateIcon () 13 | => new SpriteIcon { Icon = FontAwesome.Solid.ClipboardList }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /osu.XR/Panels/ConfigPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Sprites; 3 | using osu.XR.Drawables; 4 | using osu.XR.Panels; 5 | 6 | namespace osu.XR.Components.Panels { 7 | public class ConfigPanel : HandheldPanel { 8 | protected override VRConfigDrawable CreateContent () 9 | => new(); 10 | 11 | public override string DisplayName => "Settings"; 12 | public override Drawable CreateIcon () 13 | => new SpriteIcon { Icon = FontAwesome.Solid.Cog }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /osu.XR/Panels/Drawables/ChangelogDrawable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Sprites; 3 | using osu.Game.Graphics; 4 | using osu.Game.Graphics.Containers.Markdown; 5 | using osu.Game.Graphics.Sprites; 6 | using osu.XR.Drawables.Containers; 7 | using System; 8 | using System.IO; 9 | using System.Threading.Tasks; 10 | 11 | namespace osu.XR.Drawables { 12 | public class ChangelogDrawable : ConfigurationContainer { 13 | public ChangelogDrawable () { 14 | Title = "Changelog"; 15 | Description = "check out what's new"; 16 | } 17 | 18 | protected override void LoadComplete () { 19 | base.LoadComplete(); 20 | 21 | Task.Run( () => { 22 | var raw = File.ReadAllText( @".\Resources\changelog.txt" ); 23 | Schedule( () => loadChangelog( raw ) ); 24 | } ); 25 | } 26 | 27 | void loadChangelog ( string raw ) { 28 | ClearSections(); 29 | OsuMarkdownContainer createContainer () { 30 | return new BiggerOsuMarkdownContainer { 31 | RelativeSizeAxes = Axes.X, 32 | AutoSizeAxes = Axes.Y 33 | }; 34 | } 35 | OsuMarkdownContainer container = null; 36 | string text = ""; 37 | 38 | void finalizeSection () { 39 | if ( container is null ) return; 40 | 41 | container.Text = text; 42 | 43 | text = ""; 44 | } 45 | 46 | foreach ( var line in raw.Replace( "\r", "" ).Split( "\n", StringSplitOptions.RemoveEmptyEntries ) ) { 47 | var trimmed = line.Trim(); 48 | if ( trimmed.StartsWith( "[" ) && trimmed.EndsWith( "]" ) ) { 49 | finalizeSection(); 50 | 51 | container = createContainer(); 52 | 53 | AddSection( container, name: trimmed.Substring( 1, trimmed.Length - 2 ) ); 54 | } 55 | else { 56 | if ( container is null ) { 57 | container = createContainer(); 58 | 59 | AddSection( container, name: "Untitled Section" ); 60 | } 61 | 62 | text += line + "\n"; 63 | } 64 | } 65 | finalizeSection(); 66 | } 67 | 68 | private class BiggerOsuMarkdownContainer : OsuMarkdownContainer { 69 | public override SpriteText CreateSpriteText () => new OsuSpriteText { 70 | Font = OsuFont.GetFont( Typeface.Inter, size: 18, weight: FontWeight.Regular ), 71 | }; 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /osu.XR/Panels/Drawables/InspectorDrawable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.XR.Components; 5 | using osu.Game.Graphics.UserInterface; 6 | using osu.Game.Overlays.Settings; 7 | using osu.XR.Drawables.Containers; 8 | using osu.XR.Inspector.Reflections; 9 | using osuTK.Graphics; 10 | using System.Collections.Generic; 11 | using System.Linq; 12 | 13 | namespace osu.XR.Inspector { 14 | public class InspectorDrawable : ConfigurationContainer, INotInspectable { 15 | public readonly Bindable SelectedElementBindable = new(); 16 | public readonly Bindable InspectedElementBindable = new(); 17 | public readonly BindableBool IsSelectingBindable = new( false ); 18 | public readonly BindableBool Targets2DDrawables = new( false ); 19 | public readonly BindableBool GranularSelectionBindable = new( false ); 20 | BasicFormatedText selectedName; 21 | BasicFormatedText inspectedName; 22 | 23 | Selection3D selection3d = new(); 24 | Selection3D helperSelection3d = new() { Tint = Color4.Yellow }; 25 | 26 | Selection2D selection2d = new(); 27 | Selection2D helperSelection2d = new(); 28 | 29 | public InspectorDrawable () { 30 | Title = "Inspector"; 31 | Description = "inspect and modify properties"; 32 | 33 | SelectedElementBindable.BindValueChanged( v => { 34 | if ( v.NewValue is Drawable3D d3 ) { 35 | helperSelection3d.Select( d3 ); 36 | helperSelection2d.Select( null ); 37 | } 38 | else { 39 | helperSelection3d.Select( null ); 40 | helperSelection2d.Select( v.NewValue ); 41 | } 42 | selectedName.Text = $"Selected: **{v.NewValue?.GetInspectorName() ?? "Nothing"}**"; 43 | }, true ); 44 | 45 | InspectedElementBindable.BindValueChanged( v => { 46 | if ( v.NewValue is Drawable3D d3 ) { 47 | selection3d.Select( d3 ); 48 | selection2d.Select( null ); 49 | } 50 | else { 51 | selection3d.Select( null ); 52 | selection2d.Select( v.NewValue ); 53 | } 54 | inspectedName.Text = $"Inspected: ||{v.NewValue?.GetInspectorName() ?? "Nothing"}||"; 55 | 56 | HierarchyInspector hierarchy = keepSections.OfType().FirstOrDefault(); 57 | if ( v.NewValue is null ) { 58 | ClearSections(); 59 | } 60 | else { 61 | ClearSections( x => !keepSections.Contains( x ), true ); 62 | ClearSections( false ); 63 | } 64 | if ( v.NewValue is not null ) { 65 | if ( hierarchy is null ) { 66 | AddSection( hierarchy = new HierarchyInspector( v.NewValue ) ); 67 | hierarchy.StepHovered += s => { 68 | SelectedElementBindable.Value = s.Value; 69 | }; 70 | hierarchy.StepHoverLost += s => { 71 | SelectedElementBindable.Value = null; 72 | }; 73 | hierarchy.StepSelected += s => { 74 | keepSections.Add( hierarchy ); 75 | InspectedElementBindable.Value = s.Value; 76 | }; 77 | } 78 | else { 79 | AddSection( hierarchy ); 80 | hierarchy.FocusOn( v.NewValue ); 81 | } 82 | } 83 | 84 | if ( v.NewValue is IConfigurableInspectable config ) { 85 | if ( config.CreateWarnings() is Drawable warning ) { 86 | AddSection( warning ); 87 | } 88 | AddSection( config.CreateInspectorSubsection() ); 89 | } 90 | 91 | if ( System.Diagnostics.Debugger.IsAttached ) { 92 | AddSection( new ReflectionsInspector( v.NewValue, "Inspected" ) ); 93 | } 94 | 95 | keepSections.Clear(); 96 | }, true ); 97 | } 98 | List keepSections = new(); 99 | 100 | protected override Drawable CreateStickyHeader ( SearchTextBox search ) { 101 | return new FillFlowContainer { 102 | RelativeSizeAxes = Axes.X, 103 | AutoSizeAxes = Axes.Y, 104 | Direction = FillDirection.Vertical, 105 | Children = new Drawable[] { 106 | new SettingsCheckbox { LabelText = "Select element to inspect", Current = IsSelectingBindable }, 107 | new SettingsCheckbox { LabelText = "Granular selection", Current = GranularSelectionBindable }, 108 | new SettingsCheckbox { LabelText = "Target 2D elements", Current = Targets2DDrawables }, 109 | selectedName = new BasicFormatedText { 110 | RelativeSizeAxes = Axes.X, 111 | AutoSizeAxes = Axes.Y, 112 | Margin = new MarginPadding { Left = 15 } 113 | }, 114 | inspectedName = new BasicFormatedText { 115 | RelativeSizeAxes = Axes.X, 116 | AutoSizeAxes = Axes.Y, 117 | Margin = new MarginPadding { Left = 15 } 118 | }, 119 | search 120 | } 121 | }; 122 | } 123 | 124 | Drawable getSelected ( Drawable drawable ) { 125 | if ( GranularSelectionBindable.Value ) { 126 | return drawable?.GetValidInspectable(); 127 | } 128 | else { 129 | return ( drawable?.GetClosestInspectable() as Drawable ) ?? drawable?.GetValidInspectable(); 130 | } 131 | } 132 | 133 | public void Select ( Drawable drawable ) { 134 | SelectedElementBindable.Value = getSelected( drawable ); 135 | } 136 | public void Inspect ( Drawable drawable ) { 137 | InspectedElementBindable.Value = getSelected( drawable ); 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /osu.XR/Panels/Drawables/NotificationsDrawable.cs: -------------------------------------------------------------------------------- 1 | using osu.Game.Overlays.Notifications; 2 | using osu.XR.Drawables.Containers; 3 | 4 | namespace osu.XR.Drawables { 5 | public class NotificationsDrawable : ConfigurationContainer { 6 | public NotificationsDrawable () { 7 | Title = "Notifications"; 8 | Description = "messages and stuff"; 9 | 10 | messageContainer = new NotificationSection( "Messages", "Clear all" ); 11 | errorContainer = new NotificationSection( "Errors", "Clear all" ); 12 | 13 | AddSection( errorContainer ); 14 | AddSection( messageContainer ); 15 | } 16 | 17 | NotificationSection messageContainer; 18 | NotificationSection errorContainer; 19 | 20 | public void PostMessage ( Notification notification ) { 21 | //messageContainer.Add( notification, 0 ); 22 | } 23 | 24 | public void PostError ( Notification notification ) { 25 | //errorContainer.Add( notification, 0 ); 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /osu.XR/Panels/Drawables/VRConfigDrawable.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Localisation; 2 | using osu.Game.Graphics.UserInterface; 3 | using osu.XR.Drawables.Containers; 4 | using osu.XR.Settings.Sections; 5 | using System; 6 | 7 | namespace osu.XR.Drawables { 8 | public class VRConfigDrawable : ConfigurationContainer { 9 | public VRConfigDrawable () : base() { 10 | Title = "VR Settings"; 11 | Description = "change the way osu!XR behaves"; 12 | 13 | AddSection( new InputSettingSection() ); 14 | AddSection( new GraphicsSettingSection() ); 15 | AddSection( new PresetsSection() ); 16 | } 17 | } 18 | 19 | public class PxSliderBar : OsuSliderBar { 20 | public override LocalisableString TooltipText => $"{Current.Value}px"; 21 | } 22 | 23 | public class RadToDegreeSliderBar : OsuSliderBar { 24 | public override LocalisableString TooltipText => $"{Current.Value / Math.PI * 180:N0}°"; 25 | } 26 | 27 | public class MetersSliderBar : OsuSliderBar { 28 | public override LocalisableString TooltipText => $"{Current.Value:N2}m"; 29 | } 30 | 31 | public class PercentSliderBar : OsuSliderBar { 32 | public override LocalisableString TooltipText => $"{Current.Value:0%}"; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /osu.XR/Panels/HandheldPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.XR.Components.Groups; 4 | using osu.XR.Components.Panels; 5 | using osuTK; 6 | 7 | namespace osu.XR.Panels { 8 | /// 9 | /// A panel with a standard size 10 | /// 11 | public abstract class HandheldPanel : FlatPanel, IHasName, IHasIcon { 12 | public Drawable Content { get; private set; } 13 | public readonly Bindable IsVisibleBindable = new(); 14 | public HandheldPanel () { 15 | Content = CreateContent(); 16 | Content.Size = new Vector2( 400, 500 ) * 1.2f; 17 | 18 | PanelAutoScaleAxes = Axes.X; 19 | AutosizeBoth(); 20 | PanelHeight = 0.5; 21 | Source.Add( Content ); 22 | } 23 | 24 | protected override void Update () { 25 | base.Update(); 26 | IsVisibleBindable.Value = IsVisible = Content.IsPresent; 27 | } 28 | 29 | protected abstract Drawable CreateContent (); 30 | 31 | public abstract string DisplayName { get; } 32 | public abstract Drawable CreateIcon (); 33 | } 34 | 35 | public abstract class HandheldPanel : HandheldPanel where T : Drawable { 36 | new public T Content => (T)base.Content; 37 | protected override abstract T CreateContent (); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /osu.XR/Panels/InspectorPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Sprites; 3 | using osu.XR.Inspector; 4 | using osu.XR.Panels; 5 | 6 | namespace osu.XR.Components.Panels { 7 | public class InspectorPanel : HandheldPanel { 8 | protected override InspectorDrawable CreateContent () 9 | => new(); 10 | 11 | public override string DisplayName => "Inspector"; 12 | public override Drawable CreateIcon () 13 | => new SpriteIcon { Icon = FontAwesome.Solid.Search }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /osu.XR/Panels/NotificationsPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Sprites; 3 | using osu.Game.Overlays.Notifications; 4 | using osu.XR.Drawables; 5 | using osu.XR.Panels; 6 | 7 | namespace osu.XR.Components.Panels { 8 | public class NotificationsPanel : HandheldPanel { 9 | public void PostMessage ( Notification notification ) 10 | => Content.PostMessage( notification ); 11 | 12 | public void PostError ( Notification notification ) 13 | => Content.PostError( notification ); 14 | 15 | protected override NotificationsDrawable CreateContent () 16 | => new(); 17 | 18 | public override string DisplayName => "Notifications"; 19 | public override Drawable CreateIcon () 20 | => new SpriteIcon { Icon = FontAwesome.Solid.ExclamationCircle }; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /osu.XR/Panels/OsuPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Bindables; 3 | using osu.Framework.Graphics; 4 | using osu.Game; 5 | using osu.Game.Overlays.Settings; 6 | using osu.XR.Drawables; 7 | using osu.XR.Drawables.Containers; 8 | using osu.XR.Inspector; 9 | using osu.XR.Settings; 10 | 11 | namespace osu.XR.Components.Panels { 12 | class OsuPanel : CurvedPanel, IConfigurableInspectable { 13 | public OsuPanel () { 14 | Y = 1.8f; 15 | } 16 | 17 | OsuGame osuGame; 18 | public void SetSource ( OsuGame osuGame ) { 19 | Source.Add( this.osuGame = osuGame ); 20 | AutosizeBoth(); 21 | } 22 | 23 | Bindable screenHeightBindable = new( 1.8f ); 24 | 25 | Bindable screenResX = new( 1920 * 2 ); 26 | Bindable screenResY = new( 1080 ); 27 | 28 | [Resolved] 29 | private XrConfigManager Config { get; set; } 30 | 31 | [BackgroundDependencyLoader] 32 | private void load () { 33 | Config.BindWith( XrConfigSetting.ScreenHeight, screenHeightBindable ); 34 | screenHeightBindable.BindValueChanged( v => Y = v.NewValue, true ); 35 | 36 | screenResX.BindValueChanged( v => Width = osuGame.Width = v.NewValue, true ); 37 | screenResY.BindValueChanged( v => Height = osuGame.Height = v.NewValue, true ); 38 | 39 | Config.BindWith( XrConfigSetting.ScreenRadius, RadiusBindable ); 40 | Config.BindWith( XrConfigSetting.ScreenArc, ArcBindable ); 41 | 42 | Config.BindWith( XrConfigSetting.ScreenResolutionX, screenResX ); 43 | Config.BindWith( XrConfigSetting.ScreenResolutionY, screenResY ); 44 | } 45 | 46 | public Drawable CreateInspectorSubsection () { 47 | return new NamedContainer { 48 | DisplayName = "Panel Properties", 49 | Children = new Drawable[] { 50 | new SettingsSlider { Current = Config.GetBindable( XrConfigSetting.ScreenArc ), LabelText = "Screen arc" }, 51 | new SettingsSlider { Current = Config.GetBindable( XrConfigSetting.ScreenRadius ), LabelText = "Screen radius" }, 52 | new SettingsSlider { Current = Config.GetBindable( XrConfigSetting.ScreenHeight ), LabelText = "Screen height" }, 53 | 54 | new SettingsSlider { Current = Config.GetBindable( XrConfigSetting.ScreenResolutionX ), LabelText = "Screen resolution X" }, 55 | new SettingsSlider { Current = Config.GetBindable( XrConfigSetting.ScreenResolutionY ), LabelText = "Screen resolution Y" }, 56 | } 57 | }; 58 | } 59 | 60 | public bool AreSettingsPersistent => true; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /osu.XR/Panels/Overlays/FileSelectionOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using osu.Framework.Graphics.Shapes; 5 | using osu.Game.Graphics.Containers; 6 | using osu.Game.Graphics.Sprites; 7 | using osu.XR.Drawables; 8 | using System; 9 | using System.Collections.Generic; 10 | using System.IO; 11 | using System.Linq; 12 | 13 | namespace osu.XR.Panels.Overlays { 14 | public class FileSelectionOverlay : PanelOverlay { // TODO better design for this 15 | FileHierarchyViewWithPreview hiererchy; 16 | OsuTextFlowContainer selected; 17 | FillFlowContainer header; 18 | 19 | BindableList> selectedSteps = new(); 20 | 21 | public event Action> Confirmed; 22 | public event Action Cancelled; 23 | 24 | public FileSelectionOverlay () { 25 | FillFlowContainer container; 26 | AddInternal( new OsuScrollContainer( Direction.Vertical ) { 27 | RelativeSizeAxes = Axes.Both, 28 | ScrollbarVisible = false, 29 | Child = container = new FillFlowContainer { 30 | AutoSizeAxes = Axes.Y, 31 | RelativeSizeAxes = Axes.X, 32 | Direction = FillDirection.Vertical 33 | } 34 | } ); 35 | 36 | CalmOsuAnimatedButton buttonA; 37 | CalmOsuAnimatedButton buttonB; 38 | 39 | container.Add( header = new FillFlowContainer { 40 | AutoSizeAxes = Axes.Y, 41 | RelativeSizeAxes = Axes.X, 42 | Direction = FillDirection.Horizontal, 43 | Margin = new MarginPadding { Left = 15, Vertical = 20 }, 44 | Children = new Drawable[] { 45 | buttonA = new CalmOsuAnimatedButton { 46 | Width = 100, 47 | Height = 20, 48 | Action = () => { 49 | Confirmed?.Invoke( selectedSteps.Select( x => x.Value ).ToArray() ); 50 | Confirmed = null; 51 | Cancelled = null; 52 | Hide(); 53 | } 54 | }, 55 | buttonB = new CalmOsuAnimatedButton { 56 | Width = 100, 57 | Height = 20, 58 | Action = () => { 59 | Hide(); 60 | } 61 | } 62 | } 63 | } ); 64 | 65 | buttonA.Add( new Box { 66 | RelativeSizeAxes = Axes.Both, 67 | Alpha = 0.6f, 68 | Colour = Colour4.HotPink 69 | } ); 70 | buttonB.Add( new Box { 71 | RelativeSizeAxes = Axes.Both, 72 | Alpha = 0.6f, 73 | Colour = Colour4.HotPink 74 | } ); 75 | 76 | buttonA.Add( new OsuSpriteText { 77 | Origin = Anchor.Centre, 78 | Anchor = Anchor.Centre, 79 | UseFullGlyphHeight = true, 80 | Text = "Confirm" 81 | } ); 82 | buttonB.Add( new OsuSpriteText { 83 | Origin = Anchor.Centre, 84 | Anchor = Anchor.Centre, 85 | UseFullGlyphHeight = true, 86 | Text = "Cancel" 87 | } ); 88 | 89 | container.Add( selected = new OsuTextFlowContainer { 90 | AutoSizeAxes = Axes.Y, 91 | RelativeSizeAxes = Axes.X, 92 | Width = 0.9f, 93 | Origin = Anchor.TopCentre, 94 | Anchor = Anchor.TopCentre 95 | } ); 96 | 97 | container.Add( hiererchy = new FileHierarchyViewWithPreview() { 98 | IsMultiselect = true, 99 | Margin = new Framework.Graphics.MarginPadding { Vertical = 20 } 100 | } ); 101 | 102 | selectedSteps.BindTo( hiererchy.MultiselectSelection ); 103 | 104 | selectedSteps.BindCollectionChanged( (e,o) => { 105 | selected.Clear(); 106 | selected.AddText( "Selected: ", s => s.Colour = Colour4.HotPink ); 107 | if ( selectedSteps.Any() ) { 108 | selected.AddText( string.Join( ", ", selectedSteps.Select( x => Path.GetFileName( x.Value ) ) ) ); 109 | } 110 | else { 111 | selected.AddText( "Nothing" ); 112 | } 113 | }, true ); 114 | } 115 | 116 | public override void Hide () { 117 | Cancelled?.Invoke(); 118 | Confirmed = null; 119 | Cancelled = null; 120 | selectedSteps.Clear(); 121 | base.Hide(); 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /osu.XR/Panels/Overlays/PanelOverlay.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.Framework.Graphics.Shapes; 4 | using osu.Game.Graphics; 5 | using osuTK; 6 | 7 | namespace osu.XR.Panels.Overlays { 8 | public abstract class PanelOverlay : CompositeDrawable { 9 | public PanelOverlay () { 10 | Origin = Anchor.TopCentre; 11 | Anchor = Anchor.BottomCentre; 12 | 13 | RelativeSizeAxes = Axes.Both; 14 | AddInternal( new Box { RelativeSizeAxes = Axes.Both, Colour = OsuColour.Gray( 0.04f ) } ); 15 | } 16 | 17 | public override void Hide () { 18 | this.TransformTo( nameof( RelativeAnchorPosition ), new Vector2( 0.5f, 1 ), 500, Easing.Out ); 19 | } 20 | 21 | public override void Show () { 22 | this.TransformTo( nameof( RelativeAnchorPosition ), new Vector2( 0.5f, 0 ), 500, Easing.Out ); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /osu.XR/Panels/Overlays/PanelOverlayContainer.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Containers; 4 | using System.Linq; 5 | 6 | namespace osu.XR.Panels.Overlays { 7 | [Cached] 8 | public class PanelOverlayContainer : Container { 9 | private PanelOverlay activeOverlay; 10 | 11 | private Container overlays; 12 | private Container content; 13 | protected override Container Content => content; 14 | 15 | public PanelOverlayContainer () { 16 | AddInternal( content = new Container { 17 | RelativeSizeAxes = Axes.Both 18 | } ); 19 | AddInternal( overlays = new Container { 20 | RelativeSizeAxes = Axes.Both 21 | } ); 22 | Masking = true; 23 | } 24 | 25 | public T RequestOverlay () where T : PanelOverlay, new() { 26 | activeOverlay?.Hide(); 27 | 28 | var find = overlays.OfType().FirstOrDefault(); 29 | if ( find is null ) { 30 | find = new T(); 31 | overlays.Add( find ); 32 | } 33 | activeOverlay = find; 34 | activeOverlay.Show(); 35 | return find; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /osu.XR/Panels/RulesetInfoPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Bindables; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.XR.Drawables; 5 | using osu.XR.Input.Custom; 6 | using osu.XR.Panels; 7 | 8 | namespace osu.XR.Components.Panels { 9 | public class RulesetInfoPanel : HandheldPanel { 10 | public BindableList GetBindingsForVariant ( int variant ) 11 | => Content.GetBindingsForVariant( variant ); 12 | 13 | protected override RulesetInfoDrawable CreateContent () 14 | => new(); 15 | 16 | public override string DisplayName => "Ruleset"; 17 | public override Drawable CreateIcon () 18 | => new SpriteIcon { Icon = FontAwesome.Solid.Gamepad }; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /osu.XR/Panels/SceneManagerPanel.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Sprites; 3 | using osu.XR.Components; 4 | using osu.XR.Components.Skyboxes; 5 | using osu.XR.Editor; 6 | using osu.XR.Panels.Drawables; 7 | using osu.XR.Panels.Overlays; 8 | 9 | namespace osu.XR.Panels { 10 | public class SceneManagerPanel : HandheldPanel { 11 | SceneManagerDrawable content; 12 | 13 | protected override Drawable CreateContent () { 14 | return new PanelOverlayContainer { 15 | Child = content = new() { 16 | RelativeSizeAxes = Axes.Both 17 | } 18 | }; 19 | } 20 | 21 | protected override void LoadComplete () { 22 | base.LoadComplete(); 23 | content.SceneContainer = new SceneContainer(); 24 | Root.Add( content.SceneContainer ); 25 | 26 | var skybox = new SkyBox(); 27 | var floorgrid = new FloorGrid(); 28 | var dust = new DustEmitter(); 29 | 30 | content.SceneContainer.Add( skybox ); 31 | content.SceneContainer.Add( floorgrid ); 32 | content.SceneContainer.Add( dust ); 33 | } 34 | 35 | public override string DisplayName => "Scene"; 36 | public override Drawable CreateIcon () 37 | => new SpriteIcon { Icon = FontAwesome.Solid.Image }; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /osu.XR/Program.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) ppy Pty Ltd . Licensed under the MIT Licence. 2 | // See the LICENCE file in the repository root for full licence text. 3 | 4 | using osu.Framework.Development; 5 | using osu.Framework.Logging; 6 | using osu.Game.IPC; 7 | using osu.XR.GameHosts; 8 | using System; 9 | using System.IO; 10 | using System.Linq; 11 | using System.Threading; 12 | using System.Threading.Tasks; 13 | 14 | namespace osu.XR { 15 | public static class Program { 16 | [STAThread] 17 | public static int Main ( string[] args ) { 18 | // Back up the cwd before DesktopGameHost changes it 19 | var cwd = Environment.CurrentDirectory; 20 | bool useOsuTK = args.Contains( "--tk" ); 21 | 22 | using ( var host = new ExtendedRealityWindowsGameHost( @"osu", true, false, false ) ) { 23 | host.ExceptionThrown += handleException; 24 | 25 | if ( !host.IsPrimaryInstance ) { 26 | if ( args.Length > 0 && args[ 0 ].Contains( '.' ) ) // easy way to check for a file import in args 27 | { 28 | var importer = new ArchiveImportIPCChannel( host ); 29 | 30 | foreach ( var file in args ) { 31 | Console.WriteLine( @"Importing {0}", file ); 32 | if ( !importer.ImportAsync( Path.GetFullPath( file, cwd ) ).Wait( 3000 ) ) 33 | throw new TimeoutException( @"IPC took too long to send" ); 34 | } 35 | 36 | return 0; 37 | } 38 | 39 | // we want to allow multiple instances to be started when in debug. 40 | if ( !DebugUtils.IsDebugBuild ) 41 | return 0; 42 | } 43 | 44 | switch ( args.FirstOrDefault() ?? string.Empty ) { 45 | default: 46 | host.Run( new OsuGameXr( args ) ); 47 | break; 48 | } 49 | 50 | return 0; 51 | } 52 | } 53 | 54 | private static int allowableExceptions = DebugUtils.IsDebugBuild ? 0 : 1; 55 | 56 | /// 57 | /// Allow a maximum of one unhandled exception, per second of execution. 58 | /// 59 | /// 60 | /// 61 | private static bool handleException ( Exception arg ) { 62 | bool continueExecution = Interlocked.Decrement( ref allowableExceptions ) >= 0; 63 | 64 | Logger.Log( $"Unhandled exception has been {( continueExecution ? $"allowed with {allowableExceptions} more allowable exceptions" : "denied" )} ." ); 65 | 66 | // restore the stock of allowable exceptions after a short delay. 67 | Task.Delay( 1000 ).ContinueWith( _ => Interlocked.Increment( ref allowableExceptions ) ); 68 | 69 | return continueExecution; 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /osu.XR/ReflectionExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | 4 | namespace osu.XR { 5 | public static class ReflectionExtensions { 6 | public static T GetField ( this object self ) 7 | => (T)self.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public ).First( x => x.FieldType.IsAssignableTo( typeof( T ) ) ).GetValue( self ); 8 | 9 | public static T GetField ( this object self, string name ) 10 | => (T)self.GetType().GetFields( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public ).First( x => x.Name == name && x.FieldType.IsAssignableTo( typeof( T ) ) ).GetValue( self ); 11 | 12 | public static T GetProperty ( this object self ) 13 | => (T)self.GetType().GetProperties( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public ).First( x => x.PropertyType.IsAssignableTo( typeof( T ) ) ).GetValue( self ); 14 | 15 | public static T GetProperty ( this object self, string name ) 16 | => (T)self.GetType().GetProperties( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public ).First( x => x.Name == name && x.PropertyType.IsAssignableTo( typeof( T ) ) ).GetValue( self ); 17 | 18 | public static void SetProperty ( this object self, string name, T value ) 19 | => self.GetType().GetProperties( BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public ).First( x => x.Name == name && x.PropertyType.IsAssignableTo( typeof( T ) ) ).SetValue( self, value ); 20 | 21 | public static MethodInfo GetMethod ( this object self, string name ) 22 | => self.GetType().GetMethod( name, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /osu.XR/Resources/changelog.txt: -------------------------------------------------------------------------------- 1 | [Current] 2 | ## User Experience 3 | Inspector hierarchy now has its own searchbox. 4 | Thanks to jjbeaniguess for providing new default bindings for oculus touch controllers <3 5 | Can no longer accidentally adjust volume while playing with scroll bindings. 6 | 7 | ## Bugfixes 8 | Pointers have the correct tint again. 9 | Fixed "Screen mirroring" formatting after updating farmatting function last update. 10 | 11 | ## Framework 12 | Improved physics performance. 13 | You can now open files and URLs externally. 14 | Materials can now be created. 15 | 16 | 17 | [2021.08.26] 18 | ## New Features 19 | Added a scene management panel. 20 | You can now customize the skybox and the floor grid through the inspector. 21 | You can now teleport. 22 | Added a setting to disable teleporting temporarily. 23 | Added a shadow which can be toggled between none, footprints and paws. 24 | Added dust particles. 25 | You can now grip certain objects in edit mode (which is not yet implemented). 26 | Added a new skybox type - "Lights Out". 27 | Added ability to import props in the scene management panel. You are able to transform, rename and delete them. They do not support ruleset bindings yet. Imported props will not be saved yet. 28 | 29 | ## User Experience 30 | Inspector selections now have an animation. 31 | Made all panels the same size and look. 32 | The handheld menu now only shows the top panel and transitions between with a "popout" animation. 33 | Panels now have tooltips. 34 | Updated the look of this changelog! 35 | Other minor UX changes. 36 | Model loading ( such as controllers ) is now done in the background which reduces load-lag. 37 | 38 | ## Bugfixes 39 | Fixed a bug where some drawables would render twice (the issue was that they would get loaded before being added to the 3d hierarchy and anything that worked on the hierarchy in the load method would cause this). 40 | Notification panel now doesnt open on startup. 41 | Keyboard is now focused correctly. 42 | Fixed a bug where touch pointers would register collisions where there were none. 43 | 44 | ## Framework 45 | Models' tint now is multiplied with alpha. 46 | Added ability to fully parse OBJ and MTL files, however not all shader properties are supported. 47 | Added ability to fully parse BLEND files and extract meshes, however no material information can be extracted yet. 48 | 49 | ## Debugging 50 | The reflections inspector is now avaiable only in debug builds. 51 | 52 | 53 | [2021.06.03] 54 | ## New Features 55 | Implemented saving and loading ruleset bindings. 56 | Ruleset bindings now support rulesets with variants like osu!mania. 57 | Added this changelog! 58 | 59 | ## User Expreience 60 | Improved transparency rendering. The keyboard will no longer have jank visuals when pressing a key and the pointers will not make panels see-through. 61 | The default preset has a better screen size now. 62 | The inspector panel got a fleshed out look and functionality: 63 | A new hierarchy inspector. 64 | A new reflections inspector: you can edit: int, float, double, string, enum, color4, vector2, vector3 and bindables of these types. 65 | Custom sections for elements with settings 66 | The keyboard now flashes the pressed key. 67 | 68 | ## Bugfixes 69 | The inspector selection box now correctly selects objects with an offset. 70 | Fixed collisions not working on flat Y-planes. There is a floor collider now! 71 | 72 | ## Framework 73 | Improved physics performance. 74 | 75 | ## Debugging 76 | You can now inspect 2D drawables. 77 | There is also a secondary selection box for previewed elements. 78 | 79 | 80 | [2021.05.19] 81 | ## New features 82 | Added ruleset bindings: 83 | Clap - based on how close your primary and secondary controllers are 84 | Buttons - primary and secondary buttons on each controller 85 | Joystick: 86 | Zone - an arc with a deadzone 87 | Movement - you can move the cursor with the joystick 88 | 89 | ## User Expreience 90 | When in 2 pointer mode and one hand is occupied (for example, holding a menu), the other pointer will act as if it was in 1 pointer mode. 91 | The menu now sticks to your offhand even in 1 pointer mode, as long as you have a secondary controller. -------------------------------------------------------------------------------- /osu.XR/SceneWithMirrorWarning.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Framework.XR.Components; 5 | using osu.XR.Drawables.Containers; 6 | using osuTK; 7 | using osuTK.Graphics; 8 | 9 | namespace osu.XR { 10 | public class SceneWithMirrorWarning : Scene { 11 | BasicFormatedText text; 12 | SpriteIcon iconA; 13 | SpriteIcon iconB; 14 | 15 | public SceneWithMirrorWarning () { 16 | Add( new Container { 17 | Children = new Drawable[] { 18 | text = new BasicFormatedText { 19 | Origin = Anchor.Centre, 20 | Anchor = Anchor.Centre, 21 | TextAnchor = Anchor.TopCentre, 22 | AutoSizeAxes = Axes.Both, 23 | Text = "^^**Warning**^^\n" + 24 | "Screen mirroring is turned ||off||\n" + 25 | "You can enable it in XrSettings\n" + 26 | "~~If the screen jitters, alt-enter a few times~~", 27 | Scale = new Vector2( 1.5f ) 28 | }, 29 | 30 | iconA = new SpriteIcon { 31 | Icon = FontAwesome.Solid.ExclamationTriangle, 32 | Origin = Anchor.CentreRight, 33 | Anchor = Anchor.CentreLeft, 34 | BypassAutoSizeAxes = Axes.Both, 35 | Size = new Vector2( 50 ), 36 | Colour = Color4.Yellow, 37 | Position = new Vector2( -20, 0 ) 38 | }, 39 | 40 | iconB = new SpriteIcon { 41 | Icon = FontAwesome.Solid.ExclamationTriangle, 42 | Origin = Anchor.CentreLeft, 43 | Anchor = Anchor.CentreRight, 44 | BypassAutoSizeAxes = Axes.Both, 45 | Size = new Vector2( 50 ), 46 | Colour = Color4.Yellow, 47 | Position = new Vector2( 20, 0 ) 48 | } 49 | }, 50 | AutoSizeAxes = Axes.Both, 51 | Origin = Anchor.Centre, 52 | Anchor = Anchor.Centre 53 | } ); 54 | 55 | RenderToScreenBindable.BindValueChanged( v => { 56 | if ( v.NewValue ) { 57 | text.FadeOut( 1000 ); 58 | iconA.FadeOut( 1000 ); 59 | iconB.FadeOut( 1000 ); 60 | } 61 | else { 62 | text.FadeIn( 1000 ); 63 | iconA.FadeIn( 1000 ); 64 | iconB.FadeIn( 1000 ); 65 | } 66 | }, true ); 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /osu.XR/Settings/Hand.cs: -------------------------------------------------------------------------------- 1 | namespace osu.XR.Settings { 2 | public enum Hand { 3 | Auto, 4 | Left, 5 | Right 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /osu.XR/Settings/InputMode.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace osu.XR.Settings { 4 | public enum InputMode { 5 | [Description( "Single Pointer" )] 6 | SinglePointer, 7 | [Description( "Two Pointers" )] 8 | DoublePointer, 9 | [Description( "Touchscreen" )] 10 | TouchScreen 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /osu.XR/Settings/Sections/GraphicsSettingSection.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Game.Overlays.Settings; 5 | using osu.XR.Components; 6 | using osu.XR.Drawables; 7 | 8 | namespace osu.XR.Settings.Sections { 9 | public class GraphicsSettingSection : SettingsSection { 10 | public override string DisplayName => "Graphics"; 11 | public override Drawable CreateIcon () => new SpriteIcon { 12 | Icon = FontAwesome.Solid.Laptop 13 | }; 14 | 15 | [BackgroundDependencyLoader] 16 | private void load ( XrConfigManager config ) { 17 | Children = new Drawable[] { 18 | new SettingsSlider { Current = config.GetBindable( XrConfigSetting.ScreenArc ), LabelText = "Screen arc" }, 19 | new SettingsSlider { Current = config.GetBindable( XrConfigSetting.ScreenRadius ), LabelText = "Screen radius" }, 20 | new SettingsSlider { Current = config.GetBindable( XrConfigSetting.ScreenHeight ), LabelText = "Screen height" }, 21 | 22 | new SettingsSlider { Current = config.GetBindable( XrConfigSetting.ScreenResolutionX ), LabelText = "Screen resolution X" }, 23 | new SettingsSlider { Current = config.GetBindable( XrConfigSetting.ScreenResolutionY ), LabelText = "Screen resolution Y" }, 24 | 25 | new SettingsEnumDropdown { LabelText = "Shadow type", Current = config.GetBindable( XrConfigSetting.ShadowType ) }, 26 | 27 | new SettingsCheckbox { Current = config.GetBindable( XrConfigSetting.RenderToScreen ), LabelText = "Render to screen", TooltipText = "If checked, renders to your computer screen" }, 28 | }; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /osu.XR/Settings/Sections/InputSettingSection.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Framework.Platform; 5 | using osu.Framework.XR.GameHosts; 6 | using osu.Game.Overlays.Settings; 7 | using osu.XR.Drawables; 8 | 9 | namespace osu.XR.Settings.Sections { 10 | public class InputSettingSection : SettingsSection { 11 | public override string DisplayName => "Input"; 12 | public override Drawable CreateIcon () => new SpriteIcon { 13 | Icon = FontAwesome.Solid.Keyboard 14 | }; 15 | 16 | [BackgroundDependencyLoader] 17 | private void load ( XrConfigManager config, GameHost host ) { 18 | Children = new Drawable[] { 19 | new SettingsEnumDropdown { LabelText = "Input mode", Current = config.GetBindable( XrConfigSetting.InputMode ) }, 20 | // TODO these settings should only be shown when in appropriate input modes 21 | new SettingsCheckbox { LabelText = "Emulate touch with single pointer", Current = config.GetBindable( XrConfigSetting.SinglePointerTouch ), TooltipText = "Act as if a single pointer is a touch source.\nTwo pointers already behave this way." }, 22 | new SettingsCheckbox { LabelText = "Tap only on press", Current = config.GetBindable( XrConfigSetting.TapOnPress ), TooltipText = "In touchscreen mode, press a button to tap the screen" }, 23 | new SettingsSlider { LabelText = "Deadzone", Current = config.GetBindable( XrConfigSetting.Deadzone ), TooltipText = "Pointer deadzone after touching the screen or pressing a button" }, 24 | new SettingsSlider { LabelText = "Player Height Offset", Current = (host as ExtendedRealityGameHost).PlayerHeightOffsetBindable, TooltipText = "Some devices might not send OXR a correct viewspace.\nUse this to correct this behaviour by offsetting your height." }, 25 | new SettingsEnumDropdown { LabelText = "Dominant hand", Current = config.GetBindable( XrConfigSetting.DominantHand ) }, 26 | new SettingsCheckbox { LabelText = "Disable teleporting", Current = config.GetBindable( XrConfigSetting.DisableTeleport ) }, 27 | }; 28 | } 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /osu.XR/Settings/Sections/PresetsSection.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Allocation; 2 | using osu.Framework.Graphics; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.Framework.Logging; 5 | using osu.Game.Overlays.Settings; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | 9 | namespace osu.XR.Settings.Sections { 10 | public class PresetsSection : SettingsSection { 11 | public override string DisplayName => "Presets"; 12 | public override Drawable CreateIcon () => new SpriteIcon { 13 | Icon = FontAwesome.Solid.BoxOpen 14 | }; 15 | 16 | SettingsPreset lastPreset; 17 | private List<(string name, SettingsPreset preset)> presets = new() { 18 | ("Default", XrConfigManager.DefaultPreset), 19 | ("Touchscreen Big", XrConfigManager.PresetTouchscreenBig), 20 | ("Touchscreen Small", XrConfigManager.PresetTouchscreenSmall) 21 | }; 22 | 23 | // TODO allow users to save their own presets 24 | [BackgroundDependencyLoader] 25 | private void load ( XrConfigManager config ) { 26 | AddRange( presets.Select( x => new SettingsButton { 27 | Text = x.name, 28 | Action = () => { 29 | lastPreset = new( config, XrConfigManager.TypeLookpuPreset ); 30 | x.preset.Load( config, XrConfigManager.TypeLookpuPreset ); 31 | } 32 | } ).Append( new SettingsButton { 33 | Text = "Previous", 34 | Action = () => { 35 | lastPreset?.Load( config, XrConfigManager.TypeLookpuPreset ); 36 | } 37 | } ) ); 38 | 39 | if ( System.Diagnostics.Debugger.IsAttached ) { 40 | Add( new SettingsButton { 41 | Text = "Print Current (Runtime Logs)", 42 | Action = () => { 43 | var preset = new SettingsPreset( config, XrConfigManager.TypeLookpuPreset ); 44 | foreach ( var (k, v) in preset.values ) { 45 | Logger.Log( $"{k}: {v}" ); 46 | } 47 | } 48 | } ); 49 | } 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /osu.XR/Settings/Sections/SettingsSection.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics; 2 | using osu.Framework.Graphics.Containers; 3 | using osu.Framework.Graphics.Sprites; 4 | using osu.XR.Components.Groups; 5 | 6 | namespace osu.XR.Settings.Sections { 7 | public abstract class SettingsSection : FillFlowContainer, IHasName, IHasIcon { 8 | public SettingsSection () { 9 | Direction = FillDirection.Vertical; 10 | AutoSizeAxes = Axes.Y; 11 | RelativeSizeAxes = Axes.X; 12 | } 13 | 14 | public abstract string DisplayName { get; } 15 | public abstract Drawable CreateIcon (); 16 | } 17 | 18 | public class SettingsSectionContainer : SettingsSection { 19 | public string Title { get; set; } = "SettingsSection"; 20 | public IconUsage Icon = FontAwesome.Regular.QuestionCircle; 21 | 22 | public override string DisplayName => Title; 23 | public override Drawable CreateIcon () 24 | => new SpriteIcon { Icon = Icon }; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /osu.XR/Settings/SettingsPreset.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Configuration; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Reflection; 5 | 6 | namespace osu.XR.Settings { 7 | public class SettingsPreset where T : struct, Enum { 8 | public Dictionary values = new(); 9 | 10 | static Ti CastTo ( dynamic o ) { 11 | return (Ti)o; 12 | } 13 | static dynamic CastToReflected ( dynamic o, Type type ) { 14 | if ( type.IsEnum ) o = CastToReflected( o, type.GetEnumUnderlyingType() ); 15 | 16 | return typeof( SettingsPreset ).GetMethod( nameof( CastTo ), BindingFlags.NonPublic | BindingFlags.Static ).MakeGenericMethod( type ).Invoke( null, new object[] { o } ); 17 | } 18 | 19 | public void Load ( ConfigManager config, SettingsPreset typeLookup ) { 20 | foreach ( var (k,v) in values ) { 21 | ( (dynamic)config ).SetValue( k, CastToReflected( v, typeLookup.values[ k ].GetType() ) ); 22 | } 23 | } 24 | 25 | public SettingsPreset () { } 26 | public SettingsPreset ( ConfigManager config, SettingsPreset typeLookup ) { 27 | foreach ( var (k,v) in typeLookup.values ) { 28 | values.Add( k, typeof( ConfigManager ).GetMethod( nameof( ConfigManager.Get ) ).MakeGenericMethod( v.GetType() as Type ).Invoke( config, new object[] { k } ) ); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /osu.XR/Settings/XrConfigManager.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Platform; 2 | using osu.Game.Configuration; 3 | using osu.XR.Components; 4 | using osu.XR.Components.Skyboxes; 5 | using System; 6 | using System.IO; 7 | 8 | namespace osu.XR.Settings { 9 | public class XrConfigManager : InMemoryConfigManager { 10 | Storage storage; 11 | public XrConfigManager ( Storage storage ) { 12 | this.storage = storage.GetStorageForDirectory( "XR" ); 13 | 14 | Load(); 15 | } 16 | 17 | protected override void InitialiseDefaults () { 18 | base.InitialiseDefaults(); 19 | SetDefault( XrConfigSetting.InputMode, InputMode.SinglePointer ); 20 | SetDefault( XrConfigSetting.SinglePointerTouch, false ); 21 | SetDefault( XrConfigSetting.TapOnPress, false ); 22 | SetDefault( XrConfigSetting.Deadzone, 20, 0, 100 ); 23 | 24 | SetDefault( XrConfigSetting.ScreenArc, 2.5534892f, MathF.PI / 18, MathF.PI * 2 ); 25 | SetDefault( XrConfigSetting.ScreenRadius, 1.6f, 0.4f, 4 ); 26 | SetDefault( XrConfigSetting.ScreenHeight, 1.8f, 0f, 3 ); 27 | 28 | SetDefault( XrConfigSetting.ScreenResolutionX, 2512, 500, 7680 ); 29 | SetDefault( XrConfigSetting.ScreenResolutionY, 1080, 400, 4320 ); 30 | 31 | SetDefault( XrConfigSetting.RenderToScreen, false ); 32 | SetDefault( XrConfigSetting.DisableTeleport, false ); 33 | SetDefault( XrConfigSetting.DominantHand, Hand.Auto ); 34 | SetDefault( XrConfigSetting.ShadowType, FeetSymbols.None ); 35 | SetDefault( XrConfigSetting.ShowDust, true ); 36 | SetDefault( XrConfigSetting.SkyboxType, SkyBoxType.Solid ); 37 | } 38 | 39 | public static readonly SettingsPreset TypeLookpuPreset = new() { 40 | values = new() { 41 | [ XrConfigSetting.InputMode ] = InputMode.SinglePointer, 42 | [ XrConfigSetting.SinglePointerTouch ] = false, 43 | [ XrConfigSetting.TapOnPress ] = false, 44 | [ XrConfigSetting.Deadzone ] = 20, 45 | [ XrConfigSetting.ScreenArc ] = MathF.PI * 1.2f, 46 | [ XrConfigSetting.ScreenRadius ] = 1.6f, 47 | [ XrConfigSetting.ScreenHeight ] = 1.8f, 48 | [ XrConfigSetting.ScreenResolutionX ] = 2512, 49 | [ XrConfigSetting.ScreenResolutionY ] = 1080, 50 | [ XrConfigSetting.RenderToScreen ] = false, 51 | [ XrConfigSetting.DominantHand ] = Hand.Auto, 52 | [ XrConfigSetting.DisableTeleport ] = false, 53 | [ XrConfigSetting.ShadowType ] = FeetSymbols.None, 54 | [ XrConfigSetting.ShowDust ] = true, 55 | [ XrConfigSetting.SkyboxType ] = SkyBoxType.Solid 56 | } 57 | }; 58 | 59 | public static readonly SettingsPreset DefaultPreset = new() { 60 | values = new() { 61 | [XrConfigSetting.InputMode] = InputMode.SinglePointer, 62 | [XrConfigSetting.SinglePointerTouch] = false, 63 | [XrConfigSetting.TapOnPress] = false, 64 | [XrConfigSetting.Deadzone] = 20, 65 | [XrConfigSetting.ScreenArc] = 2.5534892f, 66 | [XrConfigSetting.ScreenRadius] = 1.6f, 67 | [XrConfigSetting.ScreenHeight] = 1.8f, 68 | [XrConfigSetting.ScreenResolutionX] = 2512, 69 | [XrConfigSetting.ScreenResolutionY] = 1080 70 | } 71 | }; 72 | 73 | public static readonly SettingsPreset PresetTouchscreenBig = new() { 74 | values = new() { 75 | [XrConfigSetting.InputMode] = InputMode.TouchScreen, 76 | [XrConfigSetting.ScreenArc] = 1.2f, 77 | [XrConfigSetting.ScreenRadius] = 1.01f, 78 | [XrConfigSetting.ScreenHeight] = 1.47f, 79 | [XrConfigSetting.ScreenResolutionX] = 3840 / 2, 80 | [XrConfigSetting.ScreenResolutionY] = 2552 / 2 81 | } 82 | }; 83 | 84 | public static readonly SettingsPreset PresetTouchscreenSmall = new() { 85 | values = new() { 86 | [XrConfigSetting.InputMode] = InputMode.TouchScreen, 87 | [XrConfigSetting.ScreenArc] = 1.2f, 88 | [XrConfigSetting.ScreenRadius] = 0.69f /*nice*/, 89 | [XrConfigSetting.ScreenHeight] = 1.58f, 90 | [XrConfigSetting.ScreenResolutionX] = 3840 / 2, 91 | [XrConfigSetting.ScreenResolutionY] = 2552 / 2 92 | } 93 | }; 94 | 95 | const string saveFilePath = "XrSettings.json"; 96 | protected override void PerformLoad () { 97 | try { 98 | if ( storage.Exists( saveFilePath ) ) { 99 | using var s = storage.GetStream( saveFilePath ); 100 | var reader = new StreamReader( s ); 101 | Newtonsoft.Json.JsonConvert.DeserializeObject>( reader.ReadToEnd() ).Load( this, TypeLookpuPreset ); 102 | } 103 | } 104 | catch { } 105 | } 106 | 107 | protected override bool PerformSave () { 108 | var preset = new SettingsPreset( this, TypeLookpuPreset ); 109 | using var s = storage.GetStream( saveFilePath, FileAccess.Write, mode: FileMode.Create ); 110 | var writer = new StreamWriter( s ); 111 | writer.Write( Newtonsoft.Json.JsonConvert.SerializeObject( preset, Newtonsoft.Json.Formatting.Indented ) ); 112 | writer.Flush(); 113 | return true; 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /osu.XR/Settings/XrConfigSetting.cs: -------------------------------------------------------------------------------- 1 | namespace osu.XR.Settings { 2 | public enum XrConfigSetting { 3 | InputMode, 4 | SinglePointerTouch, 5 | TapOnPress, 6 | Deadzone, 7 | 8 | ScreenArc, 9 | ScreenRadius, 10 | ScreenHeight, 11 | 12 | ScreenResolutionX, 13 | ScreenResolutionY, 14 | 15 | RenderToScreen, 16 | DominantHand, 17 | 18 | DisableTeleport, 19 | ShadowType, 20 | 21 | ShowDust, 22 | SkyboxType 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /osu.XR/TextureGeneration.cs: -------------------------------------------------------------------------------- 1 | using osu.Framework.Graphics.Textures; 2 | using osuTK; 3 | using SixLabors.ImageSharp; 4 | using SixLabors.ImageSharp.PixelFormats; 5 | using System; 6 | 7 | namespace osu.XR { 8 | public static class TextureGeneration { 9 | public static TextureUpload Generate ( int width, int height, Func generator ) { 10 | Image image = new Image( width, height ); 11 | image.ProcessPixelRows( rows => { 12 | for ( int y = 0; y < height; y++ ) { 13 | var span = rows.GetRowSpan( y ); 14 | for ( int x = 0; x < width; x++ ) { 15 | span[x] = generator( x, height - y - 1 ); 16 | } 17 | } 18 | } ); 19 | 20 | return new TextureUpload( image ); 21 | } 22 | 23 | public static Rgba32 FromHSV ( float hueDeg, float S, float V, float A = 1 ) { 24 | // https://www.rapidtables.com/convert/color/hsv-to-rgb.html 25 | hueDeg %= 360; 26 | if ( hueDeg < 0 ) hueDeg += 360; 27 | 28 | float C = V * S; 29 | float X = C * ( 1 - Math.Abs( hueDeg / 60 % 2 - 1 ) ); 30 | float m = V - C; 31 | 32 | float r, g, b; 33 | (r, g, b) = hueDeg switch { 34 | < 60 => (C, X, 0f), 35 | < 120 => (X, C, 0), 36 | < 180 => (0, C, X), 37 | < 240 => (0, X, C), 38 | < 300 => (X, 0, C), 39 | _ => (C, 0, X) 40 | }; 41 | 42 | return new Rgba32( r + m, g + m, b + m, A ); // NOTE this can be faster if we switch to working with bytes as soon as possible but might result in accuracy loss 43 | } 44 | 45 | public static Vector3 RGBToHSV ( Vector3 rgb ) { 46 | // https://www.rapidtables.com/convert/color/rgb-to-hsv.html 47 | float cmax = MathF.Max( MathF.Max( rgb.X, rgb.Y ), rgb.Z ); 48 | float cmin = MathF.Min( MathF.Min( rgb.X, rgb.Y ), rgb.Z ); 49 | float delta = cmax - cmin; 50 | 51 | float hue; 52 | if ( delta == 0 ) hue = 0; 53 | else if ( cmax == rgb.X ) hue = 60 * ( ( rgb.Y - rgb.Z ) / delta % 6 ); 54 | else if ( cmax == rgb.Y ) hue = 60 * ( ( rgb.Z - rgb.X ) / delta + 2 ); 55 | else hue = 60 * ( ( rgb.X - rgb.Y ) / delta + 4 ); 56 | 57 | float saturation = cmax == 0 ? 0 : delta / cmax; 58 | return new Vector3( hue, saturation, cmax ); 59 | } 60 | 61 | public static TextureUpload HSVBoxWithSetHue ( int width, int height, float hueDeg ) 62 | => Generate( width, height, ( x, y ) => FromHSV( hueDeg, (float)x / width, (float)y / height ) ); 63 | 64 | public static TextureUpload HSVCircle ( int size, float innerRadius, float outerRadius, float saturation = 1, float value = 1, float fadeDistance = 0, int? discreteCount = null ) { 65 | if ( innerRadius > outerRadius ) { 66 | var temp = innerRadius; 67 | innerRadius = outerRadius; 68 | outerRadius = temp; 69 | } 70 | 71 | float half = size / 2; 72 | 73 | return Generate( size, size, ( x, y ) => { 74 | float angleDeg = MathF.Atan2( y - half, x - half ) / MathF.PI * 180; 75 | if ( discreteCount.HasValue ) { 76 | float step = 360 / discreteCount.Value; 77 | angleDeg = MathF.Round( angleDeg / step ) * step; 78 | } 79 | float distance = MathF.Sqrt( ( x - half ) * ( x - half ) + ( y - half ) * ( y - half ) ); 80 | float alpha; 81 | if ( distance < innerRadius ) { 82 | alpha = fadeDistance == 0 ? 0 : MathF.Max( 0, 1 - ( innerRadius - distance ) / fadeDistance ); 83 | } 84 | else if ( distance > outerRadius ) { 85 | alpha = fadeDistance == 0 ? 0 : MathF.Max( 0, 1 - ( distance - outerRadius ) / fadeDistance ); 86 | } 87 | else { 88 | alpha = 1; 89 | } 90 | return FromHSV( angleDeg, saturation, value, alpha ); 91 | } ); 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /osu.XR/Textures/dust.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Flutterish/osu-XR/bada6c3406009ad295e555ac24b88a1c1e2f18ed/osu.XR/Textures/dust.png -------------------------------------------------------------------------------- /osu.XR/UnreachableCodeException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace osu.XR { 4 | [Serializable] 5 | internal class UnreachableCodeException : Exception { 6 | public UnreachableCodeException () { 7 | } 8 | } 9 | } -------------------------------------------------------------------------------- /osu.XR/XrAction.cs: -------------------------------------------------------------------------------- 1 | namespace osu.XR { 2 | public enum XrActionGroup { 3 | Pointer, 4 | Configuration, 5 | Haptics 6 | } 7 | 8 | public enum XrAction { 9 | // Pointer 10 | MouseLeft, 11 | MouseRight, 12 | Scroll, 13 | 14 | Grip, 15 | Move, 16 | 17 | // Configuration 18 | ToggleMenu, 19 | 20 | // Haptics 21 | Feedback 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /osu.XR/osu.XR.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | WinExe 5 | net6.0 6 | D:\Main\Solutions\Git\osu!lazer\osu\app.manifest 7 | preview 8 | false 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Always 33 | 34 | 35 | Always 36 | 37 | 38 | Always 39 | 40 | 41 | Always 42 | 43 | 44 | Always 45 | 46 | 47 | Always 48 | 49 | 50 | Always 51 | 52 | 53 | Always 54 | 55 | 56 | Always 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /osu.XR/osu.XR.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | --------------------------------------------------------------------------------