├── .gitignore ├── .gitmodules ├── ArcaeaSim.sln ├── ArcaeaSim ├── App.config ├── ApplicationHelper.cs ├── Arcaea.ico ├── ArcaeaSim (Debug).cmd ├── ArcaeaSim.csproj ├── ArcaeaSimApplication.cs ├── Configuration │ ├── GlobalizationConfig.cs │ ├── MainAppConfig.cs │ └── PluginsConfig.cs ├── Contents │ ├── Content.mgcb │ ├── app.config.yml │ ├── game │ │ └── The Silence │ │ │ ├── 2.aff │ │ │ ├── base.jpg │ │ │ ├── base.mp3 │ │ │ ├── base_256.jpg │ │ │ └── 歌曲信息.txt │ ├── globalization.yml │ ├── plugins.yml │ └── res │ │ ├── arcaea.jpg │ │ └── translations │ │ ├── default_millisim.iv.yml │ │ ├── default_millisim.ja-JP.yml │ │ └── default_millisim.zh-CN.yml ├── Icon.ico ├── Icon.png ├── Options.cs ├── Program.cs ├── Properties │ └── AssemblyInfo.cs ├── Subsystems │ ├── Bvs │ │ ├── ArcClient.cs │ │ ├── ArcCommunication.cs │ │ ├── ArcServer.cs │ │ └── Models │ │ │ ├── EditReloadRequestParams.cs │ │ │ ├── GeneralSimInitializeRequestParams.cs │ │ │ ├── GeneralSimInitializeResponseResult.cs │ │ │ ├── GeneralSimLaunchedNotificationParams.cs │ │ │ └── Proposals │ │ │ ├── SelectedFormatDescriptor.cs │ │ │ └── SupportedFormatDescriptor.cs │ ├── Configuration │ │ └── ConfigurationHelper.cs │ ├── Globalization │ │ └── CultureSpecificInfoHelper.cs │ ├── Interactive │ │ ├── KeyEventArgs.cs │ │ ├── KeyboardStateHandler.cs │ │ └── MouseCameraControl.cs │ └── Plugin │ │ ├── ArcaeaSimPluginManager.cs │ │ └── PluginSearchMode.cs ├── app.manifest └── packages.config ├── ArcaeaView ├── ArcaeaView.csproj ├── Components │ ├── BeatmapChangedEventArgs.cs │ ├── BeatmapLoader.cs │ ├── BeatmapLoaderFactory.cs │ ├── Cameraman.cs │ ├── CameramanFactory.cs │ ├── TrackDisplay.cs │ └── TrackDisplayFactory.cs ├── Configuration │ ├── BeatmapLoaderConfig.cs │ └── TrackDisplayConfig.cs ├── Contents │ ├── config │ │ ├── beatmap_loader.yml │ │ └── track_display.yml │ └── res │ │ ├── fx │ │ ├── compile_effects.bat │ │ └── monogame.fxh │ │ └── img │ │ ├── air_input.png │ │ ├── arc_body.png │ │ ├── arc_body_hi.png │ │ ├── arc_cap.png │ │ ├── bg │ │ └── base_conflict.jpg │ │ ├── finish_line.png │ │ ├── note_dark.png │ │ ├── note_hold_dark.png │ │ ├── note_hold_dark_hi.png │ │ ├── tap_d.png │ │ ├── track_dark.png │ │ └── track_lane_divider.png ├── Core │ └── MathF.cs ├── Extensions │ ├── ColorExtensions.cs │ └── Vector3Extensions.cs ├── Properties │ └── AssemblyInfo.cs ├── Subsystems │ ├── Rendering │ │ ├── BottomlessColoredHexahedron.cs │ │ ├── BottomlessColoredTetrahedron.cs │ │ ├── BottomlessColoredTriangularPrism.cs │ │ ├── BottomlessTexturedTetrahedron.cs │ │ ├── BottomlessTexturedTriangularPrism.cs │ │ ├── Camera.cs │ │ ├── ColoredBox.cs │ │ ├── ColoredHexahedron.cs │ │ ├── ColoredParallelogram.cs │ │ ├── ColoredRectangle.cs │ │ ├── DrawableGeometryMesh.cs │ │ └── TexturedRectangle.cs │ └── Scores │ │ ├── Entities │ │ ├── ArcColor.cs │ │ ├── ArcEasing.cs │ │ ├── ArcNote.cs │ │ ├── Beatmap.cs │ │ ├── FloorNote.cs │ │ ├── LongNote.cs │ │ ├── NoteBase.cs │ │ ├── NoteType.cs │ │ ├── SkyNote.cs │ │ ├── TimingNote.cs │ │ └── Track.cs │ │ ├── IHasTick.cs │ │ ├── IHasTicks.cs │ │ ├── IPreviewNote.cs │ │ ├── IRangedPreviewNote.cs │ │ └── Visualization │ │ ├── ArcEasingHelper.cs │ │ ├── ArcVisualNote.cs │ │ ├── FloorVisualNote.cs │ │ ├── IDrawableNote.cs │ │ ├── LongVisualNote.cs │ │ ├── NoteEffects.cs │ │ ├── SkyVisualNote.cs │ │ ├── StageMetrics.cs │ │ ├── VisualBeatmap.cs │ │ └── VisualNoteBase.cs └── packages.config ├── LICENSE.txt ├── README.md ├── appveyor.yml └── docs ├── Building.md └── Starting.md /.gitignore: -------------------------------------------------------------------------------- 1 | .vs 2 | .idea 3 | _ReSharper.* 4 | 5 | packages 6 | 7 | [Bb]in 8 | [Oo]bj 9 | 10 | *.csproj.user 11 | *.sln.user 12 | *.DotSettings.user 13 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "thirdparty/MilliSim"] 2 | path = thirdparty/MilliSim 3 | url = https://github.com/hozuki/MilliSim.git 4 | -------------------------------------------------------------------------------- /ArcaeaSim.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaeaView", "ArcaeaView\ArcaeaView.csproj", "{8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "thirdparty", "thirdparty", "{17D20885-7FF8-40F5-B927-FF731322A011}" 9 | EndProject 10 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMLTD.MilliSim.Runtime", "thirdparty\MilliSim\src\OpenMLTD.MilliSim.Runtime\OpenMLTD.MilliSim.Runtime.csproj", "{2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}" 11 | EndProject 12 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.Overlay", "thirdparty\MilliSim\thirdparty\MonoGame.Extended2\Sources\MonoGame.Extended.Overlay\MonoGame.Extended.Overlay.csproj", "{8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}" 13 | EndProject 14 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended2", "thirdparty\MilliSim\thirdparty\MonoGame.Extended2\Sources\MonoGame.Extended2\MonoGame.Extended2.csproj", "{C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}" 15 | EndProject 16 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MonoGame.Extended.VideoPlayback", "thirdparty\MilliSim\thirdparty\MonoGame.Extended2\Sources\MonoGame.Extended.VideoPlayback\MonoGame.Extended.VideoPlayback.csproj", "{01835222-222F-4FA1-96CC-45C386CF9492}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SharpAL", "thirdparty\MilliSim\thirdparty\SharpAL\SharpAL\SharpAL.csproj", "{DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}" 19 | EndProject 20 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMLTD.MilliSim.Extension.Audio.StandardAudioFormats", "thirdparty\MilliSim\src\plugins\OpenMLTD.MilliSim.Extension.Audio.StandardAudioFormats\OpenMLTD.MilliSim.Extension.Audio.StandardAudioFormats.csproj", "{001C2077-8F69-4318-9096-A52C215B4C44}" 21 | EndProject 22 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMLTD.MilliSim.Extension.Components.CoreComponents", "thirdparty\MilliSim\src\plugins\OpenMLTD.MilliSim.Extension.Components.CoreComponents\OpenMLTD.MilliSim.Extension.Components.CoreComponents.csproj", "{16575A6D-F4A1-4910-A112-DB1D62EF6342}" 23 | EndProject 24 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ArcaeaSim", "ArcaeaSim\ArcaeaSim.csproj", "{0CAC6757-EF46-4157-9ED6-08E3FBE591BD}" 25 | EndProject 26 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{56A37D24-2799-4534-97A5-ED97D14232FA}" 27 | ProjectSection(SolutionItems) = preProject 28 | .gitignore = .gitignore 29 | .gitmodules = .gitmodules 30 | appveyor.yml = appveyor.yml 31 | LICENSE.txt = LICENSE.txt 32 | README.md = README.md 33 | EndProjectSection 34 | EndProject 35 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{C268C104-0856-4563-BE11-9C7CCB62180E}" 36 | ProjectSection(SolutionItems) = preProject 37 | docs\Building.md = docs\Building.md 38 | docs\Starting.md = docs\Starting.md 39 | EndProjectSection 40 | EndProject 41 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenMLTD.Piyopiyo", "thirdparty\MilliSim\thirdparty\piyopiyo\src\OpenMLTD.Piyopiyo\OpenMLTD.Piyopiyo.csproj", "{84A82E83-D04F-49B4-AD55-C6F4CC800F05}" 42 | EndProject 43 | Global 44 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 45 | Debug|Any CPU = Debug|Any CPU 46 | Debug|x86 = Debug|x86 47 | Release|Any CPU = Release|Any CPU 48 | Release|x86 = Release|x86 49 | EndGlobalSection 50 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 51 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 52 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Debug|Any CPU.Build.0 = Debug|Any CPU 53 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Debug|x86.ActiveCfg = Debug|Any CPU 54 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Debug|x86.Build.0 = Debug|Any CPU 55 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Release|x86.ActiveCfg = Release|Any CPU 58 | {8647E62C-8760-4EE3-9CDE-0F2D15FA7CB4}.Release|x86.Build.0 = Release|Any CPU 59 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 60 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Debug|Any CPU.Build.0 = Debug|Any CPU 61 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Debug|x86.ActiveCfg = Debug|Any CPU 62 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Debug|x86.Build.0 = Debug|Any CPU 63 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Release|Any CPU.ActiveCfg = Release|Any CPU 64 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Release|Any CPU.Build.0 = Release|Any CPU 65 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Release|x86.ActiveCfg = Release|Any CPU 66 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B}.Release|x86.Build.0 = Release|Any CPU 67 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 68 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Debug|Any CPU.Build.0 = Debug|Any CPU 69 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Debug|x86.ActiveCfg = Debug|Any CPU 70 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Debug|x86.Build.0 = Debug|Any CPU 71 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Release|Any CPU.ActiveCfg = Release|Any CPU 72 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Release|Any CPU.Build.0 = Release|Any CPU 73 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Release|x86.ActiveCfg = Release|Any CPU 74 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F}.Release|x86.Build.0 = Release|Any CPU 75 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 76 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Debug|Any CPU.Build.0 = Debug|Any CPU 77 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Debug|x86.ActiveCfg = Debug|Any CPU 78 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Debug|x86.Build.0 = Debug|Any CPU 79 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Release|Any CPU.ActiveCfg = Release|Any CPU 80 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Release|Any CPU.Build.0 = Release|Any CPU 81 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Release|x86.ActiveCfg = Release|Any CPU 82 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5}.Release|x86.Build.0 = Release|Any CPU 83 | {01835222-222F-4FA1-96CC-45C386CF9492}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 84 | {01835222-222F-4FA1-96CC-45C386CF9492}.Debug|Any CPU.Build.0 = Debug|Any CPU 85 | {01835222-222F-4FA1-96CC-45C386CF9492}.Debug|x86.ActiveCfg = Debug|Any CPU 86 | {01835222-222F-4FA1-96CC-45C386CF9492}.Debug|x86.Build.0 = Debug|Any CPU 87 | {01835222-222F-4FA1-96CC-45C386CF9492}.Release|Any CPU.ActiveCfg = Release|Any CPU 88 | {01835222-222F-4FA1-96CC-45C386CF9492}.Release|Any CPU.Build.0 = Release|Any CPU 89 | {01835222-222F-4FA1-96CC-45C386CF9492}.Release|x86.ActiveCfg = Release|Any CPU 90 | {01835222-222F-4FA1-96CC-45C386CF9492}.Release|x86.Build.0 = Release|Any CPU 91 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 92 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Debug|Any CPU.Build.0 = Debug|Any CPU 93 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Debug|x86.ActiveCfg = Debug|Any CPU 94 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Debug|x86.Build.0 = Debug|Any CPU 95 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Release|Any CPU.ActiveCfg = Release|Any CPU 96 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Release|Any CPU.Build.0 = Release|Any CPU 97 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Release|x86.ActiveCfg = Release|Any CPU 98 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58}.Release|x86.Build.0 = Release|Any CPU 99 | {001C2077-8F69-4318-9096-A52C215B4C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 100 | {001C2077-8F69-4318-9096-A52C215B4C44}.Debug|Any CPU.Build.0 = Debug|Any CPU 101 | {001C2077-8F69-4318-9096-A52C215B4C44}.Debug|x86.ActiveCfg = Debug|Any CPU 102 | {001C2077-8F69-4318-9096-A52C215B4C44}.Debug|x86.Build.0 = Debug|Any CPU 103 | {001C2077-8F69-4318-9096-A52C215B4C44}.Release|Any CPU.ActiveCfg = Release|Any CPU 104 | {001C2077-8F69-4318-9096-A52C215B4C44}.Release|Any CPU.Build.0 = Release|Any CPU 105 | {001C2077-8F69-4318-9096-A52C215B4C44}.Release|x86.ActiveCfg = Release|Any CPU 106 | {001C2077-8F69-4318-9096-A52C215B4C44}.Release|x86.Build.0 = Release|Any CPU 107 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 108 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Debug|Any CPU.Build.0 = Debug|Any CPU 109 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Debug|x86.ActiveCfg = Debug|Any CPU 110 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Debug|x86.Build.0 = Debug|Any CPU 111 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Release|Any CPU.ActiveCfg = Release|Any CPU 112 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Release|Any CPU.Build.0 = Release|Any CPU 113 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Release|x86.ActiveCfg = Release|Any CPU 114 | {16575A6D-F4A1-4910-A112-DB1D62EF6342}.Release|x86.Build.0 = Release|Any CPU 115 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Debug|Any CPU.ActiveCfg = Debug|x86 116 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Debug|Any CPU.Build.0 = Debug|x86 117 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Debug|x86.ActiveCfg = Debug|x86 118 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Debug|x86.Build.0 = Debug|x86 119 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Release|Any CPU.ActiveCfg = Release|x86 120 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Release|Any CPU.Build.0 = Release|x86 121 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Release|x86.ActiveCfg = Release|x86 122 | {0CAC6757-EF46-4157-9ED6-08E3FBE591BD}.Release|x86.Build.0 = Release|x86 123 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 124 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Debug|Any CPU.Build.0 = Debug|Any CPU 125 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Debug|x86.ActiveCfg = Debug|Any CPU 126 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Debug|x86.Build.0 = Debug|Any CPU 127 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Release|Any CPU.ActiveCfg = Release|Any CPU 128 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Release|Any CPU.Build.0 = Release|Any CPU 129 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Release|x86.ActiveCfg = Release|Any CPU 130 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05}.Release|x86.Build.0 = Release|Any CPU 131 | EndGlobalSection 132 | GlobalSection(SolutionProperties) = preSolution 133 | HideSolutionNode = FALSE 134 | EndGlobalSection 135 | GlobalSection(NestedProjects) = preSolution 136 | {2F7ECBA5-AA8B-4AB2-8F84-585352935A1B} = {17D20885-7FF8-40F5-B927-FF731322A011} 137 | {8A716D2A-DE8D-4886-BF8F-76B6B1A7BB8F} = {17D20885-7FF8-40F5-B927-FF731322A011} 138 | {C7C3D9DE-41A4-4C21-98FB-336ED38A99D5} = {17D20885-7FF8-40F5-B927-FF731322A011} 139 | {01835222-222F-4FA1-96CC-45C386CF9492} = {17D20885-7FF8-40F5-B927-FF731322A011} 140 | {DC0F4960-FF4E-4BF4-878C-F76CF1FDDE58} = {17D20885-7FF8-40F5-B927-FF731322A011} 141 | {001C2077-8F69-4318-9096-A52C215B4C44} = {17D20885-7FF8-40F5-B927-FF731322A011} 142 | {16575A6D-F4A1-4910-A112-DB1D62EF6342} = {17D20885-7FF8-40F5-B927-FF731322A011} 143 | {C268C104-0856-4563-BE11-9C7CCB62180E} = {56A37D24-2799-4534-97A5-ED97D14232FA} 144 | {84A82E83-D04F-49B4-AD55-C6F4CC800F05} = {17D20885-7FF8-40F5-B927-FF731322A011} 145 | EndGlobalSection 146 | GlobalSection(ExtensibilityGlobals) = postSolution 147 | SolutionGuid = {073575D6-F3DA-49D6-ABC3-568EBD34E402} 148 | EndGlobalSection 149 | EndGlobal 150 | -------------------------------------------------------------------------------- /ArcaeaSim/App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /ArcaeaSim/ApplicationHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Reflection; 4 | using JetBrains.Annotations; 5 | using OpenMLTD.MilliSim.Core; 6 | 7 | namespace Moe.Mottomo.ArcaeaSim { 8 | internal static class ApplicationHelper { 9 | 10 | [NotNull] 11 | internal static string StartupPath => LazyStartupPath.Value; 12 | 13 | [NotNull] 14 | internal static string CodeName => LazyCodeName.Value; 15 | 16 | [NotNull] 17 | private static string GetStartupPath() { 18 | var assembly = Assembly.GetEntryAssembly(); 19 | var codeBaseUri = new Uri(assembly.EscapedCodeBase); 20 | var assemblyFilePath = codeBaseUri.LocalPath; 21 | var fileInfo = new FileInfo(assemblyFilePath); 22 | 23 | return fileInfo.DirectoryName ?? Environment.CurrentDirectory; 24 | } 25 | 26 | [CanBeNull] 27 | private static string GetCodeName() { 28 | var assembly = Assembly.GetEntryAssembly(); 29 | var attr = assembly.GetCustomAttribute(); 30 | 31 | return attr?.CodeName; 32 | } 33 | 34 | [NotNull] 35 | private static readonly Lazy LazyStartupPath = new Lazy(GetStartupPath); 36 | 37 | [NotNull] 38 | private static readonly Lazy LazyCodeName = new Lazy(GetCodeName); 39 | 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /ArcaeaSim/Arcaea.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Arcaea.ico -------------------------------------------------------------------------------- /ArcaeaSim/ArcaeaSim (Debug).cmd: -------------------------------------------------------------------------------- 1 | @ArcaeaSim.exe --debug 2 | -------------------------------------------------------------------------------- /ArcaeaSim/ArcaeaSimApplication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using JetBrains.Annotations; 5 | using Microsoft.Xna.Framework; 6 | using Microsoft.Xna.Framework.Graphics; 7 | using Microsoft.Xna.Framework.Input; 8 | using Microsoft.Xna.Framework.Media; 9 | using Moe.Mottomo.ArcaeaSim.Components; 10 | using Moe.Mottomo.ArcaeaSim.Configuration; 11 | using Moe.Mottomo.ArcaeaSim.Subsystems.Bvs; 12 | using Moe.Mottomo.ArcaeaSim.Subsystems.Interactive; 13 | using Moe.Mottomo.ArcaeaSim.Subsystems.Plugin; 14 | using OpenMLTD.MilliSim.Configuration; 15 | using OpenMLTD.MilliSim.Core; 16 | using OpenMLTD.MilliSim.Extension.Components.CoreComponents; 17 | using OpenMLTD.MilliSim.Foundation; 18 | using OpenMLTD.MilliSim.Foundation.Extending; 19 | using OpenMLTD.MilliSim.Foundation.Extensions; 20 | using OpenMLTD.MilliSim.Globalization; 21 | using OpenMLTD.MilliSim.Graphics; 22 | 23 | namespace Moe.Mottomo.ArcaeaSim { 24 | /// 25 | /// 26 | /// Arcaea application. 27 | /// 28 | public sealed class ArcaeaSimApplication : BaseGame { 29 | 30 | internal ArcaeaSimApplication([NotNull] Options startupOptions, [NotNull] BasePluginManager pluginManager, [NotNull] ConfigurationStore configurationStore, [NotNull] CultureSpecificInfo cultureSpecificInfo) 31 | : base("Contents", pluginManager) { 32 | StartupOptions = startupOptions; 33 | ConfigurationStore = configurationStore; 34 | CultureSpecificInfo = cultureSpecificInfo; 35 | 36 | ApplyConfiguration(); 37 | } 38 | 39 | public override Stage Stage => _stage; 40 | 41 | public override ConfigurationStore ConfigurationStore { get; } 42 | 43 | public override CultureSpecificInfo CultureSpecificInfo { get; } 44 | 45 | internal Options StartupOptions { get; } 46 | 47 | protected override void Initialize() { 48 | AppendComponents(); 49 | AppendExtensionComponents(); 50 | 51 | CenterWindowAndSetTitle(); 52 | 53 | base.Initialize(); 54 | 55 | InitializeExtensionComponents(); 56 | } 57 | 58 | protected override void LoadContent() { 59 | base.LoadContent(); 60 | 61 | var editorServerUri = StartupOptions.EditorServerUri; 62 | 63 | if (!string.IsNullOrWhiteSpace(editorServerUri)) { 64 | _communication = new ArcCommunication(this); 65 | 66 | var b = Uri.TryCreate(editorServerUri, UriKind.RelativeOrAbsolute, out var edServerUri); 67 | 68 | if (!b) { 69 | GameLog.Error("Invalid URI format (in editor_server_uri command line param): {0}", editorServerUri); 70 | } 71 | 72 | _communication.EditorServerUri = edServerUri; 73 | 74 | var simulatorServerPort = StartupOptions.SimulatorServerPort; 75 | 76 | _communication.Server.Start(simulatorServerPort); 77 | 78 | _communication.Client.SendLaunchedNotification(); 79 | } 80 | } 81 | 82 | protected override void UnloadContent() { 83 | if (_communication != null) { 84 | _communication.Client.SendSimExitedNotification().Wait(TimeSpan.FromSeconds(2)); 85 | 86 | _communication.Server.Stop(); 87 | 88 | _communication.Dispose(); 89 | } 90 | 91 | base.UnloadContent(); 92 | } 93 | 94 | private void ApplyConfiguration() { 95 | var graphicsManager = GraphicsDeviceManager; 96 | var config = ConfigurationStore.Get(); 97 | 98 | graphicsManager.PreferredBackBufferWidth = config.Data.Window.Width; 99 | graphicsManager.PreferredBackBufferHeight = config.Data.Window.Height; 100 | graphicsManager.PreferredDepthStencilFormat = DepthFormat.Depth16; 101 | graphicsManager.SynchronizeWithVerticalRetrace = true; 102 | graphicsManager.GraphicsProfile = GraphicsProfile.HiDef; 103 | 104 | IsMouseVisible = true; 105 | } 106 | 107 | private void AppendComponents() { 108 | var keyboardStateHandler = new KeyboardStateHandler(this); 109 | 110 | Components.Add(keyboardStateHandler); 111 | 112 | Components.Add(new MouseCameraControl(this)); 113 | 114 | keyboardStateHandler.KeyHold += (s, e) => { 115 | var cam = this.FindSingleElement()?.Camera; 116 | 117 | if (cam == null) { 118 | return; 119 | } 120 | 121 | switch (e.KeyCode) { 122 | case Keys.A: 123 | cam.Strafe(-0.1f); 124 | break; 125 | case Keys.D: 126 | cam.Strafe(0.1f); 127 | break; 128 | case Keys.W: 129 | cam.Walk(0.1f); 130 | break; 131 | case Keys.S: 132 | cam.Walk(-0.1f); 133 | break; 134 | case Keys.Left: 135 | cam.Yaw(0.01f); 136 | break; 137 | case Keys.Right: 138 | cam.Yaw(-0.01f); 139 | break; 140 | case Keys.Up: 141 | cam.Pitch(-0.01f); 142 | break; 143 | case Keys.Down: 144 | cam.Pitch(0.01f); 145 | break; 146 | case Keys.R: { 147 | var camera = this.FindSingleElement()?.Camera; 148 | camera?.Reset(); 149 | } 150 | break; 151 | case Keys.F6: { 152 | var syncTimer = this.FindSingleElement(); 153 | syncTimer?.Stop(); 154 | } 155 | break; 156 | } 157 | }; 158 | 159 | keyboardStateHandler.KeyDown += (s, e) => { 160 | switch (e.KeyCode) { 161 | case Keys.Space: { 162 | var audioController = this.FindSingleElement(); 163 | var music = audioController?.Music; 164 | var syncTimer = this.FindSingleElement(); 165 | 166 | Debug.Assert(syncTimer != null, nameof(syncTimer) + " != null"); 167 | 168 | if (music == null) { 169 | break; 170 | } 171 | 172 | var isPlaying = syncTimer.IsRunning; 173 | 174 | if (!isPlaying) { 175 | syncTimer.Start(); 176 | } else { 177 | syncTimer.Pause(); 178 | } 179 | } 180 | break; 181 | } 182 | }; 183 | } 184 | 185 | private void AppendExtensionComponents() { 186 | var stage = new Stage(this, ConfigurationStore); 187 | _stage = stage; 188 | 189 | Components.Add(stage); 190 | 191 | var pluginManager = (ArcaeaSimPluginManager)PluginManager; 192 | 193 | var instantiatedIDList = new List(); 194 | 195 | foreach (var factoryID in pluginManager.InstancingFactoryIDs) { 196 | var factory = pluginManager.GetPluginByID(factoryID); 197 | var component = factory?.CreateComponent(this, stage); 198 | 199 | if (component != null) { 200 | stage.Components.Add(component); 201 | instantiatedIDList.Add(factoryID); 202 | } 203 | } 204 | 205 | if (instantiatedIDList.Count > 0) { 206 | GameLog.Debug("Instantiated component factories: {0}", string.Join(", ", instantiatedIDList)); 207 | } else { 208 | GameLog.Debug("No component factory instantiated."); 209 | } 210 | } 211 | 212 | // For Direct3D, who fails to center the window on startup. 213 | private void CenterWindowAndSetTitle() { 214 | var windowBounds = Window.ClientBounds; 215 | var displayMode = GraphicsDevice.Adapter.CurrentDisplayMode; 216 | 217 | Window.Position = new Point((displayMode.Width - windowBounds.Width) / 2, (displayMode.Height - windowBounds.Height) / 2); 218 | 219 | var config = ConfigurationStore.Get(); 220 | 221 | string songTitle; 222 | if (ConfigurationStore.TryGetValue(out var scoreLoaderConfig)) { 223 | songTitle = scoreLoaderConfig.Data.Title; 224 | } else { 225 | songTitle = null; 226 | } 227 | 228 | var appCodeName = ApplicationHelper.CodeName; 229 | var windowTitle = config.Data.Window.Title; 230 | 231 | if (!string.IsNullOrWhiteSpace(songTitle)) { 232 | windowTitle = songTitle + " - " + windowTitle; 233 | } 234 | 235 | if (!string.IsNullOrWhiteSpace(appCodeName)) { 236 | windowTitle = windowTitle + " (\"" + appCodeName + "\")"; 237 | } 238 | 239 | Window.Title = windowTitle; 240 | } 241 | 242 | private void InitializeExtensionComponents() { 243 | var helpOverlay = this.FindSingleElement(); 244 | 245 | if (helpOverlay != null) { 246 | helpOverlay.Visible = true; 247 | } 248 | 249 | var syncTimer = this.FindSingleElement(); 250 | 251 | Debug.Assert(syncTimer != null, nameof(syncTimer) + " != null"); 252 | 253 | syncTimer.StateChanged += (s, e) => { 254 | switch (e.NewState) { 255 | case MediaState.Stopped: 256 | if (helpOverlay != null) { 257 | helpOverlay.Visible = true; 258 | } 259 | break; 260 | case MediaState.Playing: 261 | if (helpOverlay != null) { 262 | helpOverlay.Visible = false; 263 | } 264 | break; 265 | case MediaState.Paused: 266 | if (helpOverlay != null) { 267 | helpOverlay.Visible = true; 268 | } 269 | break; 270 | default: 271 | throw new ArgumentOutOfRangeException(); 272 | } 273 | }; 274 | } 275 | 276 | private Stage _stage; 277 | [CanBeNull] 278 | private ArcCommunication _communication; 279 | 280 | } 281 | } 282 | -------------------------------------------------------------------------------- /ArcaeaSim/Configuration/GlobalizationConfig.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Configuration { 2 | public sealed class GlobalizationConfig { 3 | 4 | public string[] TranslationFiles { get; set; } 5 | 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /ArcaeaSim/Configuration/MainAppConfig.cs: -------------------------------------------------------------------------------- 1 | using OpenMLTD.MilliSim.Configuration.Entities; 2 | 3 | namespace Moe.Mottomo.ArcaeaSim.Configuration { 4 | public sealed class MainAppConfig : ConfigBase { 5 | 6 | public MainAppConfigData Data { get; set; } 7 | 8 | public sealed class MainAppConfigData { 9 | 10 | public WindowConfig Window { get; set; } 11 | 12 | public sealed class WindowConfig { 13 | 14 | public int Width { get; set; } 15 | 16 | public int Height { get; set; } 17 | 18 | public string Title { get; set; } 19 | 20 | } 21 | 22 | } 23 | 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ArcaeaSim/Configuration/PluginsConfig.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Moe.Mottomo.ArcaeaSim.Subsystems.Plugin; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Configuration { 5 | public sealed class PluginsConfig { 6 | 7 | public PluginsLoading Loading { get; set; } 8 | 9 | public string[] ComponentFactories { get; set; } 10 | 11 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] 12 | public sealed class PluginsLoading { 13 | 14 | public PluginSearchMode Mode { get; set; } 15 | 16 | public PluginsLoadingLists Lists { get; set; } 17 | 18 | [UsedImplicitly(ImplicitUseKindFlags.InstantiatedWithFixedConstructorSignature)] 19 | public sealed class PluginsLoadingLists { 20 | 21 | public string[] WhiteList { get; set; } 22 | 23 | public string[] BlackList { get; set; } 24 | 25 | } 26 | 27 | } 28 | 29 | } 30 | } -------------------------------------------------------------------------------- /ArcaeaSim/Contents/Content.mgcb: -------------------------------------------------------------------------------- 1 | 2 | #----------------------------- Global Properties ----------------------------# 3 | 4 | /outputDir:bin/$(Platform) 5 | /intermediateDir:obj/$(Platform) 6 | /platform:Windows 7 | /config: 8 | /profile:Reach 9 | /compress:False 10 | 11 | #-------------------------------- References --------------------------------# 12 | 13 | 14 | #---------------------------------- Content ---------------------------------# 15 | 16 | -------------------------------------------------------------------------------- /ArcaeaSim/Contents/app.config.yml: -------------------------------------------------------------------------------- 1 | # @include 2 | 3 | # Notes: 4 | # 1. Please save this configuration file with UTF-8 encoding without BOM, codepage 65001. 5 | # 2. Numeric values in 'layout' section can appear in 2 forms, integer (interpreted as pixels) 6 | # or percentage (calculated based on window size). 7 | # 3. MilliSim uses BGM time as the standard time. If BGM is not loaded, uses BGA instead. If neither is loaded, uses game time. 8 | metadata: 9 | type: Moe.Mottomo.ArcaeaSim.Configuration.MainAppConfig 10 | assembly_file: ArcaeaSim.exe 11 | for: "@main" 12 | version: 1 # format version 13 | data: 14 | window: 15 | width: 800 # width of the window's client area, in pixels 16 | height: 600 # height of the window's client area, in pixels 17 | title: "ArcaeaSim" # main title of the window 18 | -------------------------------------------------------------------------------- /ArcaeaSim/Contents/game/The Silence/base.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Contents/game/The Silence/base.jpg -------------------------------------------------------------------------------- /ArcaeaSim/Contents/game/The Silence/base.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Contents/game/The Silence/base.mp3 -------------------------------------------------------------------------------- /ArcaeaSim/Contents/game/The Silence/base_256.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Contents/game/The Silence/base_256.jpg -------------------------------------------------------------------------------- /ArcaeaSim/Contents/game/The Silence/歌曲信息.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Contents/game/The Silence/歌曲信息.txt -------------------------------------------------------------------------------- /ArcaeaSim/Contents/globalization.yml: -------------------------------------------------------------------------------- 1 | translation_files: 2 | - res/translations/**/*.iv.yml 3 | -------------------------------------------------------------------------------- /ArcaeaSim/Contents/plugins.yml: -------------------------------------------------------------------------------- 1 | loading: 2 | mode: default # default, white_list, black_list 3 | lists: 4 | # "default": Search every .dll file in the root path and plugin paths. 5 | # "white_list": ONLY plugins in white list will be loaded 6 | white_list: [ ] 7 | # "black_list": All plugins in root and "plugins" directory EXCEPT those in black list will be loaded 8 | black_list: [ ] 9 | # file paths 10 | # - xxx.dll 11 | component_factories: 12 | # the latter, the closer to the top 13 | # core: base 14 | - plugin.component_factory.sync_timer 15 | - plugin.component_factory.background_music 16 | # core: visual backgrounds 17 | - plugin.component_factory.background_video 18 | - plugin.component_factory.background_image 19 | # score-related 20 | - plugin.component_factory.arcaea.beatmap_loader 21 | - plugin.component_factory.arcaea.cameraman 22 | - plugin.component_factory.arcaea.track_display 23 | # overlays 24 | - plugin.component_factory.help_overlay 25 | - plugin.component_factory.fps_overlay 26 | - plugin.component_factory.debug_overlay 27 | - plugin.component_factory.sync_timer_overlay 28 | -------------------------------------------------------------------------------- /ArcaeaSim/Contents/res/arcaea.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Contents/res/arcaea.jpg -------------------------------------------------------------------------------- /ArcaeaSim/Contents/res/translations/default_millisim.iv.yml: -------------------------------------------------------------------------------- 1 | system_ui: 2 | help: 3 | press_space_to_start: Press space to start 4 | -------------------------------------------------------------------------------- /ArcaeaSim/Contents/res/translations/default_millisim.ja-JP.yml: -------------------------------------------------------------------------------- 1 | system_ui: 2 | help: 3 | press_space_to_start: スペースキーでスタート 4 | -------------------------------------------------------------------------------- /ArcaeaSim/Contents/res/translations/default_millisim.zh-CN.yml: -------------------------------------------------------------------------------- 1 | system_ui: 2 | help: 3 | press_space_to_start: 按空格键开始 4 | -------------------------------------------------------------------------------- /ArcaeaSim/Icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Icon.ico -------------------------------------------------------------------------------- /ArcaeaSim/Icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaSim/Icon.png -------------------------------------------------------------------------------- /ArcaeaSim/Options.cs: -------------------------------------------------------------------------------- 1 | using CommandLine; 2 | 3 | namespace Moe.Mottomo.ArcaeaSim { 4 | public sealed class Options { 5 | 6 | [Option("debug", HelpText = "Enable debug mode", Required = false, Default = false)] 7 | public bool IsDebugEnabled { get; set; } 8 | 9 | [Option("editor_server_uri", HelpText = "BVSP editor server URI", Required = false, Default = null)] 10 | public string EditorServerUri { get; set; } 11 | 12 | [Option("bvsp_port", HelpText = "BVSP simulator server port override", Required = false, Default = 0)] 13 | public int SimulatorServerPort { get; set; } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ArcaeaSim/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using CommandLine; 4 | using JetBrains.Annotations; 5 | using Moe.Mottomo.ArcaeaSim.Subsystems.Configuration; 6 | using Moe.Mottomo.ArcaeaSim.Subsystems.Globalization; 7 | using Moe.Mottomo.ArcaeaSim.Subsystems.Plugin; 8 | using OpenMLTD.MilliSim.Core; 9 | using OpenMLTD.MilliSim.Foundation; 10 | using OpenMLTD.MilliSim.Graphics; 11 | 12 | namespace Moe.Mottomo.ArcaeaSim { 13 | #if WINDOWS || LINUX 14 | /// 15 | /// The main program class. 16 | /// 17 | internal static class Program { 18 | 19 | /// 20 | /// The main entry point for the application. 21 | /// 22 | [STAThread] 23 | private static int Main([NotNull, ItemNotNull] string[] args) { 24 | BaseGame.GraphicsBackend = GraphicsBackend.Direct3D11; 25 | 26 | GameLog.Initialize("arcaea-debug"); 27 | GameLog.Enabled = true; 28 | 29 | var exitCode = -1; 30 | 31 | var parser = new Parser(settings => { 32 | settings.IgnoreUnknownArguments = true; 33 | settings.CaseInsensitiveEnumValues = true; 34 | }); 35 | 36 | var optionsParsingResult = parser.ParseArguments(args); 37 | 38 | try { 39 | if (optionsParsingResult.Tag == ParserResultType.Parsed) { 40 | var options = ((Parsed)optionsParsingResult).Value; 41 | 42 | // Enable game log if the app is launched with "--debug" switch. 43 | GameLog.Enabled = options.IsDebugEnabled; 44 | 45 | using (var pluginManager = new ArcaeaSimPluginManager()) { 46 | pluginManager.LoadPlugins(); 47 | 48 | var configurationStore = ConfigurationHelper.CreateConfigurationStore(pluginManager); 49 | var cultureSpecificInfo = CultureSpecificInfoHelper.CreateCultureSpecificInfo(); 50 | 51 | using (var game = new ArcaeaSimApplication(options, pluginManager, configurationStore, cultureSpecificInfo)) { 52 | game.Run(); 53 | } 54 | 55 | exitCode = 0; 56 | } 57 | } else { 58 | var helpText = CommandLine.Text.HelpText.AutoBuild(optionsParsingResult); 59 | 60 | GameLog.Info(helpText); 61 | } 62 | } catch (Exception ex) { 63 | GameLog.Error(ex.Message); 64 | GameLog.Error(ex.StackTrace); 65 | Debug.Print(ex.ToString()); 66 | } 67 | 68 | return exitCode; 69 | } 70 | 71 | } 72 | #endif 73 | } 74 | -------------------------------------------------------------------------------- /ArcaeaSim/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | using log4net.Config; 5 | 6 | // General Information about an assembly is controlled through the following 7 | // set of attributes. Change these attribute values to modify the information 8 | // associated with an assembly. 9 | [assembly: AssemblyTitle("ArcaeaSim")] 10 | [assembly: AssemblyProduct("ArcaeaSim")] 11 | [assembly: AssemblyConfiguration("")] 12 | [assembly: AssemblyDescription("Arcaea Simulator")] 13 | [assembly: AssemblyCompany("")] 14 | [assembly: AssemblyCopyright("Copyright © 2018")] 15 | [assembly: AssemblyTrademark("")] 16 | [assembly: AssemblyCulture("")] 17 | 18 | // Setting ComVisible to false makes the types in this assembly not visible 19 | // to COM components. If you need to access a type in this assembly from 20 | // COM, set the ComVisible attribute to true on that type. 21 | [assembly: ComVisible(false)] 22 | 23 | // The following GUID is for the ID of the typelib if this project is exposed to COM 24 | [assembly: Guid("395f104a-85df-496b-b533-0dfb62fef8a8")] 25 | 26 | // Version information for an assembly consists of the following four values: 27 | // 28 | // Major Version 29 | // Minor Version 30 | // Build Number 31 | // Revision 32 | // 33 | // You can specify all the values or you can default the Build and Revision Numbers 34 | // by using the '*' as shown below: 35 | // [assembly: AssemblyVersion("1.0.*")] 36 | [assembly: AssemblyVersion("1.0.0.0")] 37 | [assembly: AssemblyFileVersion("1.0.0.0")] 38 | 39 | [assembly: XmlConfigurator] 40 | [assembly: CLSCompliant(true)] 41 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/ArcClient.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using JetBrains.Annotations; 3 | using Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models; 4 | using OpenMLTD.Piyopiyo; 5 | using OpenMLTD.Piyopiyo.Net.JsonRpc; 6 | 7 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs { 8 | public sealed class ArcClient : JsonRpcClient { 9 | 10 | internal ArcClient([NotNull] ArcCommunication communication) { 11 | _communication = communication; 12 | } 13 | 14 | public Task SendPlayingNotification() { 15 | return SendNotificationWithEmptyBody(CommonProtocolMethodNames.Preview_Playing); 16 | } 17 | 18 | public Task SendPausedNotification() { 19 | return SendNotificationWithEmptyBody(CommonProtocolMethodNames.Preview_Paused); 20 | } 21 | 22 | public Task SendStoppedNotification() { 23 | return SendNotificationWithEmptyBody(CommonProtocolMethodNames.Preview_Stopped); 24 | } 25 | 26 | public Task SendReloadedNotification() { 27 | return SendNotificationWithEmptyBody(CommonProtocolMethodNames.Edit_Reloaded); 28 | } 29 | 30 | public Task SendLaunchedNotification() { 31 | var serverUri = _communication.EditorServerUri; 32 | 33 | if (serverUri == null) { 34 | return Task.FromResult(0); 35 | } 36 | 37 | var endPoint = _communication.Server.EndPoint; 38 | 39 | var param0Object = new GeneralSimLaunchedNotificationParams { 40 | SimulatorServerUri = $"http://{endPoint.Address}:{endPoint.Port}/" 41 | }; 42 | 43 | return SendNotificationAsync(serverUri, CommonProtocolMethodNames.General_SimLaunched, new[] { param0Object }); 44 | } 45 | 46 | public Task SendSimExitedNotification() { 47 | return SendNotificationWithEmptyBody(CommonProtocolMethodNames.General_SimExited); 48 | } 49 | 50 | private Task SendNotificationWithEmptyBody([NotNull] string method) { 51 | if (_communication.EditorServerUri == null) { 52 | return Task.FromResult(0); 53 | } 54 | 55 | return SendNotificationAsync(_communication.EditorServerUri, method); 56 | } 57 | 58 | [NotNull] 59 | private readonly ArcCommunication _communication; 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/ArcCommunication.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JetBrains.Annotations; 3 | using OpenMLTD.MilliSim.Core; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs { 6 | public sealed class ArcCommunication : DisposableBase { 7 | 8 | internal ArcCommunication([NotNull] ArcaeaSimApplication game) { 9 | Game = game; 10 | Server = new ArcServer(this); 11 | Client = new ArcClient(this); 12 | } 13 | 14 | [NotNull] 15 | public ArcaeaSimApplication Game { get; } 16 | 17 | [CanBeNull] 18 | public Uri EditorServerUri { get; internal set; } 19 | 20 | [NotNull] 21 | public ArcServer Server { get; } 22 | 23 | [NotNull] 24 | public ArcClient Client { get; } 25 | 26 | protected override void Dispose(bool disposing) { 27 | Server.Dispose(); 28 | Client.Dispose(); 29 | } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/ArcServer.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Net; 6 | using JetBrains.Annotations; 7 | using Moe.Mottomo.ArcaeaSim.Components; 8 | using Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models; 9 | using Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models.Proposals; 10 | using OpenMLTD.MilliSim.Extension.Components.CoreComponents; 11 | using OpenMLTD.MilliSim.Foundation.Extensions; 12 | using OpenMLTD.Piyopiyo.Extensions; 13 | using OpenMLTD.Piyopiyo.Net.Contributed; 14 | using OpenMLTD.Piyopiyo.Net.JsonRpc; 15 | 16 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs { 17 | public sealed class ArcServer : SimulatorServer { 18 | 19 | internal ArcServer([NotNull] ArcCommunication communication) { 20 | _communication = communication; 21 | } 22 | 23 | protected override void OnGeneralSimInitialize(object sender, JsonRpcMethodEventArgs e) { 24 | LogToScreen("Received request: general/simInitialize"); 25 | 26 | if (JsonRpcHelper.IsRequestValid(e.ParsedRequestObject, out string errorMessage)) { 27 | var requestObject = JsonRpcHelper.TranslateAsRequest(e.ParsedRequestObject); 28 | var param0 = requestObject.Params[0]; 29 | 30 | Debug.Assert(param0 != null, nameof(param0) + " != null"); 31 | 32 | var param0Object = param0.ToObject(); 33 | 34 | var selectedFormat = SelectFormat(param0Object.SupportedFormats); 35 | 36 | var responseResult = new GeneralSimInitializeResponseResult { 37 | SelectedFormat = selectedFormat 38 | }; 39 | 40 | e.Context.RpcOk(responseResult); 41 | } else { 42 | Debug.Print(errorMessage); 43 | e.Context.RpcError(JsonRpcErrorCodes.InvalidRequest, errorMessage); 44 | } 45 | } 46 | 47 | protected override void OnPreviewPlay(object sender, JsonRpcMethodEventArgs e) { 48 | LogToScreen("Received request: preview/play"); 49 | 50 | if (JsonRpcHelper.IsRequestValid(e.ParsedRequestObject, out string errorMessage)) { 51 | var syncTimer = _communication.Game.FindSingleElement(); 52 | 53 | if (syncTimer == null) { 54 | e.Context.RpcError(JsonRpcErrorCodes.InternalError, "Cannot find the " + nameof(SyncTimer) + " component.", statusCode: HttpStatusCode.InternalServerError); 55 | return; 56 | } 57 | 58 | syncTimer.Start(); 59 | 60 | e.Context.RpcOk(); 61 | 62 | _communication.Client.SendPlayingNotification(); 63 | } else { 64 | Debug.Print(errorMessage); 65 | e.Context.RpcError(JsonRpcErrorCodes.InvalidRequest, errorMessage); 66 | } 67 | } 68 | 69 | protected override void OnPreviewPause(object sender, JsonRpcMethodEventArgs e) { 70 | LogToScreen("Received request: preview/pause"); 71 | 72 | if (JsonRpcHelper.IsRequestValid(e.ParsedRequestObject, out string errorMessage)) { 73 | var syncTimer = _communication.Game.FindSingleElement(); 74 | 75 | if (syncTimer == null) { 76 | e.Context.RpcError(JsonRpcErrorCodes.InternalError, "Cannot find the " + nameof(SyncTimer) + " component.", statusCode: HttpStatusCode.InternalServerError); 77 | return; 78 | } 79 | 80 | syncTimer.Pause(); 81 | 82 | e.Context.RpcOk(); 83 | 84 | _communication.Client.SendPausedNotification(); 85 | } else { 86 | Debug.Print(errorMessage); 87 | e.Context.RpcError(JsonRpcErrorCodes.InvalidRequest, errorMessage); 88 | } 89 | } 90 | 91 | protected override void OnPreviewStop(object sender, JsonRpcMethodEventArgs e) { 92 | LogToScreen("Received request: preview/stop"); 93 | 94 | if (JsonRpcHelper.IsRequestValid(e.ParsedRequestObject, out string errorMessage)) { 95 | var syncTimer = _communication.Game.FindSingleElement(); 96 | 97 | if (syncTimer == null) { 98 | e.Context.RpcError(JsonRpcErrorCodes.InternalError, "Cannot find the " + nameof(SyncTimer) + " component.", statusCode: HttpStatusCode.InternalServerError); 99 | return; 100 | } 101 | 102 | syncTimer.Stop(); 103 | 104 | e.Context.RpcOk(); 105 | 106 | _communication.Client.SendStoppedNotification(); 107 | } else { 108 | Debug.Print(errorMessage); 109 | e.Context.RpcError(JsonRpcErrorCodes.InvalidRequest, errorMessage); 110 | } 111 | } 112 | 113 | protected override void OnPreviewGetPlaybackState(object sender, JsonRpcMethodEventArgs e) { 114 | LogToScreen("Received request: preview/getPlaybackState"); 115 | 116 | if (JsonRpcHelper.IsRequestValid(e.ParsedRequestObject, out string errorMessage)) { 117 | e.Context.RpcOk(); 118 | } else { 119 | Debug.Print(errorMessage); 120 | e.Context.RpcError(JsonRpcErrorCodes.InvalidRequest, errorMessage); 121 | } 122 | } 123 | 124 | protected override void OnPreviewSeekByTime(object sender, JsonRpcMethodEventArgs e) { 125 | LogToScreen("Received request: preview/seekByTime"); 126 | 127 | e.Context.RpcErrorNotImplemented(); 128 | } 129 | 130 | protected override void OnEditReload(object sender, JsonRpcMethodEventArgs e) { 131 | LogToScreen("Received request: edit/reload"); 132 | 133 | if (!JsonRpcHelper.IsRequestValid(e.ParsedRequestObject, out string errorMessage)) { 134 | Debug.Print(errorMessage); 135 | e.Context.RpcError(JsonRpcErrorCodes.InvalidRequest, errorMessage); 136 | 137 | return; 138 | } 139 | 140 | var beatmapLoader = _communication.Game.FindSingleElement(); 141 | 142 | if (beatmapLoader == null) { 143 | e.Context.RpcError(JsonRpcErrorCodes.InternalError, "Cannot find the " + nameof(BeatmapLoader) + " component.", statusCode: HttpStatusCode.InternalServerError); 144 | return; 145 | } 146 | 147 | var requestObject = JsonRpcHelper.TranslateAsRequest(e.ParsedRequestObject); 148 | var param0 = requestObject.Params[0]; 149 | 150 | Debug.Assert(param0 != null, nameof(param0) + " != null"); 151 | 152 | var param0Object = param0.ToObject(); 153 | 154 | if (!string.IsNullOrEmpty(param0Object.BeatmapFile)) { 155 | if (File.Exists(param0Object.BeatmapFile)) { 156 | var backgroundMusic = _communication.Game.FindSingleElement(); 157 | 158 | if (!string.IsNullOrEmpty(param0Object.BackgroundMusicFile)) { 159 | if (backgroundMusic == null) { 160 | e.Context.RpcError(JsonRpcErrorCodes.InternalError, "Cannot find the " + nameof(BackgroundMusic) + " component.", statusCode: HttpStatusCode.InternalServerError); 161 | return; 162 | } 163 | 164 | backgroundMusic.LoadMusic(param0Object.BackgroundMusicFile); 165 | } else { 166 | backgroundMusic?.LoadMusic(null); 167 | } 168 | 169 | beatmapLoader.Load(param0Object.BeatmapFile); 170 | } else { 171 | LogToScreen($"Not found: {param0Object.BeatmapFile}"); 172 | } 173 | } 174 | 175 | e.Context.RpcOk(); 176 | 177 | // DO NOT await 178 | _communication.Client.SendReloadedNotification(); 179 | } 180 | 181 | private void LogToScreen([NotNull] string message) { 182 | var debug = _communication.Game.FindSingleElement(); 183 | 184 | if (debug != null) { 185 | message = "[SimServer] " + message; 186 | debug.AddLine(message); 187 | } 188 | } 189 | 190 | [CanBeNull] 191 | private SelectedFormatDescriptor SelectFormat([NotNull, ItemNotNull] SupportedFormatDescriptor[] supportedFormats) { 192 | SelectedFormatDescriptor selectedFormat = null; 193 | 194 | foreach (var format in supportedFormats) { 195 | var locals = _supportedScoreFileFormats.Where(f => f.Game == format.Game && f.FormatId == format.FormatId); 196 | 197 | foreach (var local in locals) { 198 | if (Array.IndexOf(format.Versions, local.Version) >= 0) { 199 | selectedFormat = local; 200 | break; 201 | } 202 | } 203 | } 204 | 205 | return selectedFormat; 206 | } 207 | 208 | private readonly SelectedFormatDescriptor[] _supportedScoreFileFormats = { 209 | new SelectedFormatDescriptor {Game = "arcaea", FormatId = "aff", Version = "*"}, 210 | }; 211 | 212 | [NotNull] 213 | private readonly ArcCommunication _communication; 214 | 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/Models/EditReloadRequestParams.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Serialization; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models { 6 | [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] 7 | public sealed class EditReloadRequestParams { 8 | 9 | [JsonConstructor] 10 | internal EditReloadRequestParams() { 11 | } 12 | 13 | [JsonProperty(Required = Required.AllowNull)] 14 | [CanBeNull] 15 | public string BeatmapFile { get; set; } 16 | 17 | [JsonProperty(Required = Required.Always)] 18 | public int BeatmapIndex { get; set; } 19 | 20 | [JsonProperty(Required = Required.Always)] 21 | public float BeatmapOffset { get; set; } 22 | 23 | [JsonProperty] 24 | [CanBeNull] 25 | public string BackgroundMusicFile { get; set; } 26 | 27 | [JsonProperty] 28 | [CanBeNull] 29 | public string BackgroundVideoFile { get; set; } 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/Models/GeneralSimInitializeRequestParams.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models.Proposals; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models { 7 | [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] 8 | public sealed class GeneralSimInitializeRequestParams { 9 | 10 | [JsonConstructor] 11 | internal GeneralSimInitializeRequestParams() { 12 | } 13 | 14 | [NotNull, ItemNotNull] 15 | public SupportedFormatDescriptor[] SupportedFormats { get; set; } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/Models/GeneralSimInitializeResponseResult.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models.Proposals; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models { 7 | [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] 8 | public sealed class GeneralSimInitializeResponseResult { 9 | 10 | [JsonConstructor] 11 | internal GeneralSimInitializeResponseResult() { 12 | } 13 | 14 | [CanBeNull] 15 | public SelectedFormatDescriptor SelectedFormat { get; set; } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/Models/GeneralSimLaunchedNotificationParams.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models { 5 | [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] 6 | public sealed class GeneralSimLaunchedNotificationParams { 7 | 8 | [JsonConstructor] 9 | internal GeneralSimLaunchedNotificationParams() { 10 | } 11 | 12 | [JsonProperty("server_uri")] 13 | public string SimulatorServerUri { get; set; } 14 | 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/Models/Proposals/SelectedFormatDescriptor.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models.Proposals { 7 | [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] 8 | public sealed class SelectedFormatDescriptor { 9 | 10 | [NotNull] 11 | public string Game { get; set; } 12 | 13 | [JsonProperty("id")] 14 | [NotNull] 15 | public string FormatId { get; set; } 16 | 17 | [NotNull] 18 | public string Version { get; set; } 19 | 20 | [CanBeNull] 21 | public JToken Extra { get; set; } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Bvs/Models/Proposals/SupportedFormatDescriptor.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using Newtonsoft.Json.Serialization; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Bvs.Models.Proposals { 7 | [JsonObject(NamingStrategyType = typeof(SnakeCaseNamingStrategy))] 8 | public sealed class SupportedFormatDescriptor { 9 | 10 | [NotNull] 11 | public string Game { get; set; } 12 | 13 | [JsonProperty("id")] 14 | [NotNull] 15 | public string FormatId { get; set; } 16 | 17 | [NotNull, ItemNotNull] 18 | public string[] Versions { get; set; } 19 | 20 | [CanBeNull] 21 | public JToken Extra { get; set; } 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Configuration/ConfigurationHelper.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using OpenMLTD.MilliSim.Configuration; 3 | using OpenMLTD.MilliSim.Configuration.Extending; 4 | using OpenMLTD.MilliSim.Foundation; 5 | using YamlDotNet.Serialization; 6 | using YamlDotNet.Serialization.NamingConventions; 7 | 8 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Configuration { 9 | /// 10 | /// Helper functions about . 11 | /// 12 | internal static class ConfigurationHelper { 13 | 14 | /// 15 | /// Creates a new from default entry file using custom YAML converters. 16 | /// 17 | /// The that contains loaded plugins. 18 | /// Created . 19 | [NotNull] 20 | internal static ConfigurationStore CreateConfigurationStore([NotNull] BasePluginManager pluginManager) { 21 | return CreateConfigurationStore(DefaultConfigurationEntryFile, pluginManager); 22 | } 23 | 24 | /// 25 | /// Creates a new from specified entry file using custom YAML type converters. 26 | /// 27 | /// Path of the entry configuration file. 28 | /// The that contains loaded plugins. 29 | /// Created . 30 | [NotNull] 31 | internal static ConfigurationStore CreateConfigurationStore([NotNull] string entryFilePath, [NotNull] BasePluginManager pluginManager) { 32 | var typeConverterFactories = pluginManager.GetPluginsOfType(); 33 | 34 | var deserializerBuilder = new DeserializerBuilder() 35 | .IgnoreUnmatchedProperties() 36 | .WithNamingConvention(new UnderscoredNamingConvention()); 37 | 38 | // External converters 39 | foreach (var factory in typeConverterFactories) { 40 | var converter = factory.CreateTypeConverter(); 41 | deserializerBuilder.WithTypeConverter(converter); 42 | } 43 | 44 | var deserializer = deserializerBuilder.Build(); 45 | 46 | return ConfigurationStore.Load(entryFilePath, deserializer); 47 | } 48 | 49 | private const string DefaultConfigurationEntryFile = "Contents/app.config.yml"; 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Globalization/CultureSpecificInfoHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.IO; 4 | using JetBrains.Annotations; 5 | using Moe.Mottomo.ArcaeaSim.Configuration; 6 | using OpenMLTD.MilliSim.Globalization; 7 | using YamlDotNet.Serialization; 8 | using YamlDotNet.Serialization.NamingConventions; 9 | 10 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Globalization { 11 | /// 12 | /// Helper functions about . 13 | /// 14 | internal static class CultureSpecificInfoHelper { 15 | 16 | /// 17 | /// Creates a new from default entry file. 18 | /// 19 | /// Created . 20 | [NotNull] 21 | internal static CultureSpecificInfo CreateCultureSpecificInfo() { 22 | return CreateCultureSpecificInfo(DefaultGlobalizationEntryFile); 23 | } 24 | 25 | /// 26 | /// Creates a new from specified entry file. 27 | /// 28 | /// Path of the globalization configuration entry file. 29 | /// Created . 30 | [NotNull] 31 | internal static CultureSpecificInfo CreateCultureSpecificInfo([NotNull] string entryFilePath) { 32 | var info = new CultureSpecificInfo(CultureInfo.CurrentUICulture); 33 | 34 | var deserializer = new DeserializerBuilder() 35 | .IgnoreUnmatchedProperties() 36 | .WithNamingConvention(new UnderscoredNamingConvention()) 37 | .Build(); 38 | 39 | var globalizationConfigFileInfo = new FileInfo(entryFilePath); 40 | GlobalizationConfig config; 41 | 42 | using (var fileStream = File.Open(globalizationConfigFileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read)) { 43 | using (var reader = new StreamReader(fileStream)) { 44 | config = deserializer.Deserialize(reader); 45 | } 46 | } 47 | 48 | var globalizationConfigBaseDirectory = globalizationConfigFileInfo.Directory; 49 | 50 | if (globalizationConfigBaseDirectory == null) { 51 | throw new ApplicationException("Unexpected: base directory for globalization files is null!"); 52 | } 53 | 54 | foreach (var translationFileGlob in config.TranslationFiles) { 55 | info.TranslationManager.AddTranslationsFromGlob(globalizationConfigBaseDirectory, translationFileGlob); 56 | } 57 | 58 | return info; 59 | } 60 | 61 | private const string DefaultGlobalizationEntryFile = "Contents/globalization.yml"; 62 | 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Interactive/KeyEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework.Input; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Interactive { 5 | public sealed class KeyEventArgs : EventArgs { 6 | 7 | public KeyEventArgs(Keys keyCode, KeyState oldState, KeyState newState) { 8 | KeyCode = keyCode; 9 | OldState = oldState; 10 | NewState = newState; 11 | } 12 | 13 | public Keys KeyCode { get; } 14 | 15 | public KeyState OldState { get; } 16 | 17 | public KeyState NewState { get; } 18 | 19 | } 20 | } -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Interactive/KeyboardStateHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Input; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Interactive { 6 | internal sealed class KeyboardStateHandler : GameComponent { 7 | 8 | public KeyboardStateHandler(Game game) 9 | : base(game) { 10 | _previousState = Keyboard.GetState(); 11 | } 12 | 13 | public event EventHandler KeyDown; 14 | 15 | public event EventHandler KeyUp; 16 | 17 | public event EventHandler KeyHold; 18 | 19 | public override void Update(GameTime gameTime) { 20 | base.Update(gameTime); 21 | 22 | if (!Enabled) { 23 | return; 24 | } 25 | 26 | var state = Keyboard.GetState(); 27 | 28 | var oldPressed = _previousState.GetPressedKeys(); 29 | var newPressed = state.GetPressedKeys(); 30 | 31 | for (var i = 0; i < oldPressed.Length; ++i) { 32 | if (Array.IndexOf(newPressed, oldPressed[i]) < 0) { 33 | var e = new KeyEventArgs(oldPressed[i], KeyState.Down, KeyState.Up); 34 | 35 | KeyUp?.Invoke(this, e); 36 | } 37 | } 38 | 39 | for (var i = 0; i < newPressed.Length; ++i) { 40 | if (Array.IndexOf(oldPressed, newPressed[i]) < 0) { 41 | var e = new KeyEventArgs(newPressed[i], KeyState.Up, KeyState.Down); 42 | 43 | KeyDown?.Invoke(this, e); 44 | } else { 45 | var e = new KeyEventArgs(newPressed[i], KeyState.Down, KeyState.Down); 46 | 47 | KeyHold?.Invoke(this, e); 48 | } 49 | } 50 | 51 | _previousState = state; 52 | } 53 | 54 | private KeyboardState _previousState; 55 | 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Interactive/MouseCameraControl.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Input; 4 | using Moe.Mottomo.ArcaeaSim.Components; 5 | using Moe.Mottomo.ArcaeaSim.Subsystems.Rendering; 6 | using OpenMLTD.MilliSim.Foundation.Extensions; 7 | 8 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Interactive { 9 | public sealed class MouseCameraControl : GameComponent { 10 | 11 | public MouseCameraControl(Game game) 12 | : base(game) { 13 | } 14 | 15 | public override void Initialize() { 16 | base.Initialize(); 17 | 18 | _camera = Game.ToBaseGame().FindSingleElement()?.Camera; 19 | } 20 | 21 | public override void Update(GameTime gameTime) { 22 | base.Update(gameTime); 23 | 24 | if (_camera == null) { 25 | return; 26 | } 27 | 28 | var currentMouseState = Mouse.GetState(); 29 | 30 | var lastMouseState = _lastMoustState; 31 | _lastMoustState = currentMouseState; 32 | 33 | // When the user is holding the right key, move our camera. 34 | if (lastMouseState.RightButton == ButtonState.Pressed && currentMouseState.RightButton == ButtonState.Pressed) { 35 | float xDifference = currentMouseState.X - lastMouseState.X; 36 | float yDifference = currentMouseState.Y - lastMouseState.Y; 37 | 38 | _camera.Pitch(RotationSpeed * yDifference); 39 | _camera.Yaw(-RotationSpeed * xDifference); 40 | } 41 | } 42 | 43 | private static readonly float RotationSpeed = MathHelper.ToRadians(0.2f); 44 | 45 | [CanBeNull] 46 | private Camera _camera; 47 | private MouseState _lastMoustState; 48 | 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Plugin/ArcaeaSimPluginManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Composition.Hosting; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using JetBrains.Annotations; 9 | using Moe.Mottomo.ArcaeaSim.Configuration; 10 | using OpenMLTD.MilliSim.Core; 11 | using OpenMLTD.MilliSim.Foundation; 12 | using YamlDotNet.Serialization; 13 | using YamlDotNet.Serialization.NamingConventions; 14 | 15 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Plugin { 16 | /// 17 | /// 18 | /// Plugin manager for ArcaeaSim. 19 | /// 20 | internal sealed class ArcaeaSimPluginManager : BasePluginManager { 21 | 22 | /// 23 | /// Creates a new instance of . 24 | /// 25 | internal ArcaeaSimPluginManager() { 26 | AppDomain.CurrentDomain.AssemblyResolve += AppDomain_Resolve; 27 | InstancingFactoryIDs = new string[0]; 28 | } 29 | 30 | /// 31 | /// Loaded plugin list. Can be used in diagnostic windows, for example a listing plugin details. 32 | /// 33 | public override IReadOnlyList Plugins => _allPlugins; 34 | 35 | /// 36 | /// The IDs of the factories that will be used for instancing responsible classes. 37 | /// 38 | [NotNull, ItemNotNull] 39 | internal IReadOnlyList InstancingFactoryIDs { get; private set; } 40 | 41 | /// 42 | /// Scan and load plugins according to default plugin list file. 43 | /// 44 | internal void LoadPlugins() { 45 | LoadPlugins(DefaultPluginListFile); 46 | } 47 | 48 | /// 49 | /// Scan and load plugins according to specified plugin list file. 50 | /// 51 | /// Path to the plugin list file. 52 | internal void LoadPlugins([NotNull] string pluginListFilePath) { 53 | var deserializerBuilder = new DeserializerBuilder(); 54 | var deserializer = deserializerBuilder 55 | .IgnoreUnmatchedProperties() 56 | .WithNamingConvention(new UnderscoredNamingConvention()) 57 | .Build(); 58 | 59 | PluginsConfig pluginsConfig; 60 | 61 | using (var fileStream = File.Open(pluginListFilePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { 62 | using (var reader = new StreamReader(fileStream)) { 63 | pluginsConfig = deserializer.Deserialize(reader); 64 | } 65 | } 66 | 67 | InstancingFactoryIDs = pluginsConfig.ComponentFactories; 68 | 69 | string[] filters; 70 | 71 | switch (pluginsConfig.Loading.Mode) { 72 | case PluginSearchMode.Default: 73 | filters = null; 74 | break; 75 | case PluginSearchMode.Exclusive: 76 | filters = pluginsConfig.Loading.Lists.BlackList; 77 | break; 78 | case PluginSearchMode.Inclusive: 79 | filters = pluginsConfig.Loading.Lists.WhiteList; 80 | break; 81 | default: 82 | throw new ArgumentOutOfRangeException(); 83 | } 84 | 85 | var startupPath = ApplicationHelper.StartupPath; 86 | var searchPaths = new string[SubSearchPaths.Count + 1]; 87 | 88 | searchPaths[0] = startupPath; 89 | 90 | for (var i = 1; i <= SubSearchPaths.Count; ++i) { 91 | searchPaths[i] = Path.Combine(startupPath, SubSearchPaths[i - 1]); 92 | } 93 | 94 | LoadAssemblies(pluginsConfig.Loading.Mode, filters, searchPaths); 95 | } 96 | 97 | protected override void Dispose(bool disposing) { 98 | _extensionContainer?.Dispose(); 99 | _extensionContainer = null; 100 | _extensionConfiguration = null; 101 | 102 | if (Plugins != null) { 103 | foreach (var plugin in Plugins) { 104 | if (plugin is IDisposable d) { 105 | d.Dispose(); 106 | } 107 | } 108 | } 109 | 110 | AppDomain.CurrentDomain.AssemblyResolve -= AppDomain_Resolve; 111 | } 112 | 113 | private void LoadAssemblies(PluginSearchMode searchMode, [CanBeNull, ItemNotNull] string[] filters, [NotNull, ItemNotNull] params string[] searchPaths) { 114 | if (_extensionContainer != null) { 115 | _extensionContainer.Dispose(); 116 | _extensionContainer = null; 117 | } 118 | 119 | var allAssemblies = FindAssemblies(searchMode, filters, searchPaths); 120 | var configuration = new ContainerConfiguration().WithAssemblies(allAssemblies); 121 | var host = configuration.CreateContainer(); 122 | 123 | var loadedPlugins = host.GetExports().ToArray(); 124 | _allPlugins = loadedPlugins; 125 | 126 | _extensionConfiguration = configuration; 127 | _extensionContainer = host; 128 | } 129 | 130 | private static IReadOnlyList FindAssemblies(PluginSearchMode searchMode, [CanBeNull, ItemNotNull] string[] filters, [NotNull, ItemNotNull] params string[] searchPaths) { 131 | var allAssemblies = new List(); 132 | var fullFilters = filters?.Where(f => !string.IsNullOrEmpty(f)).Select(Path.GetFullPath).ToArray() ?? new string[0]; 133 | 134 | var isModernWindows = Environment.OSVersion.Platform == PlatformID.Win32NT; 135 | 136 | foreach (var directory in searchPaths) { 137 | if (!Directory.Exists(directory)) { 138 | continue; 139 | } 140 | 141 | var assemblyFileNames = Directory 142 | .EnumerateFiles(directory) 143 | .Where(str => str.ToLowerInvariant().EndsWith(".dll")); 144 | 145 | foreach (var assemblyFileName in assemblyFileNames) { 146 | bool PathEquals(string filter) { 147 | var compareFlag = isModernWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; 148 | return string.Equals(filter, assemblyFileName, compareFlag); 149 | } 150 | 151 | if (fullFilters.Length > 0) { 152 | switch (searchMode) { 153 | case PluginSearchMode.Default: 154 | break; 155 | case PluginSearchMode.Exclusive: 156 | if (Array.Exists(fullFilters, PathEquals)) { 157 | continue; 158 | } 159 | break; 160 | case PluginSearchMode.Inclusive: 161 | if (!Array.Exists(fullFilters, PathEquals)) { 162 | continue; 163 | } 164 | break; 165 | default: 166 | throw new ArgumentOutOfRangeException(nameof(searchMode), searchMode, null); 167 | } 168 | } 169 | 170 | try { 171 | var assembly = Assembly.LoadFrom(assemblyFileName); 172 | allAssemblies.Add(assembly); 173 | } catch (Exception ex) { 174 | Debug.Print(ex.Message); 175 | } 176 | } 177 | } 178 | 179 | return allAssemblies; 180 | } 181 | 182 | private static Assembly AppDomain_Resolve(object sender, ResolveEventArgs e) { 183 | var currentPath = ApplicationHelper.StartupPath; 184 | var assemblyFileName = e.Name + ".dll"; 185 | 186 | var assemblyPath = Path.Combine(currentPath, assemblyFileName); 187 | 188 | // Search the root directory first... 189 | if (File.Exists(assemblyPath)) { 190 | return Assembly.LoadFrom(assemblyPath); 191 | } 192 | 193 | // ... and then the specified sub directories (e.g. "plugins") if they exist. 194 | foreach (var subPath in SubSearchPaths) { 195 | assemblyPath = Path.Combine(currentPath, subPath, assemblyFileName); 196 | 197 | if (File.Exists(assemblyPath)) { 198 | return Assembly.LoadFrom(assemblyFileName); 199 | } 200 | } 201 | 202 | return null; 203 | } 204 | 205 | private static readonly IReadOnlyList SubSearchPaths = new[] { 206 | "plugins" 207 | }; 208 | 209 | private const string DefaultPluginListFile = "Contents/plugins.yml"; 210 | 211 | private ContainerConfiguration _extensionConfiguration; 212 | private CompositionHost _extensionContainer; 213 | private IReadOnlyList _allPlugins; 214 | 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /ArcaeaSim/Subsystems/Plugin/PluginSearchMode.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Plugin { 2 | public enum PluginSearchMode { 3 | 4 | Default = 0, 5 | Exclusive = 1, 6 | Inclusive = 2 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /ArcaeaSim/app.manifest: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | true/pm 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /ArcaeaSim/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /ArcaeaView/Components/BeatmapChangedEventArgs.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JetBrains.Annotations; 3 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Components { 6 | public sealed class BeatmapChangedEventArgs : EventArgs { 7 | 8 | internal BeatmapChangedEventArgs([CanBeNull] Beatmap oldBeatmap, [CanBeNull] Beatmap newBeatmap) { 9 | OldBeatmap = oldBeatmap; 10 | NewBeatmap = newBeatmap; 11 | } 12 | 13 | [CanBeNull] 14 | public Beatmap OldBeatmap { get; } 15 | 16 | [CanBeNull] 17 | public Beatmap NewBeatmap { get; } 18 | 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /ArcaeaView/Components/BeatmapLoader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using JetBrains.Annotations; 5 | using Moe.Mottomo.ArcaeaSim.Configuration; 6 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 7 | using OpenMLTD.MilliSim.Extension.Components.CoreComponents; 8 | using OpenMLTD.MilliSim.Foundation; 9 | using OpenMLTD.MilliSim.Foundation.Extensions; 10 | 11 | namespace Moe.Mottomo.ArcaeaSim.Components { 12 | /// 13 | /// Arcaea beatmap loader component. 14 | /// 15 | public sealed class BeatmapLoader : BaseGameComponent { 16 | 17 | public BeatmapLoader([NotNull] BaseGame game, [CanBeNull] IBaseGameComponentContainer parent) 18 | : base(game, parent) { 19 | } 20 | 21 | public event EventHandler BeatmapChanged; 22 | 23 | [CanBeNull] 24 | public Beatmap Beatmap { get; private set; } 25 | 26 | /// 27 | /// Loads an AFF beatmap from string. 28 | /// 29 | /// AFF beatmap content string. 30 | public void LoadFrom([NotNull] string text) { 31 | Beatmap.TryParse(text, out var newBeatmap); 32 | 33 | var oldBeatmap = Beatmap; 34 | Beatmap = newBeatmap; 35 | 36 | BeatmapChanged?.Invoke(this, new BeatmapChangedEventArgs(oldBeatmap, newBeatmap)); 37 | 38 | var game = Game.ToBaseGame(); 39 | var debugOverlay = game.FindSingleElement(); 40 | 41 | if (debugOverlay != null) { 42 | if (newBeatmap != null) { 43 | debugOverlay.AddLine($"Loaded beatmap from string."); 44 | } else { 45 | debugOverlay.AddLine($"Failed to load beatmap from string."); 46 | } 47 | } 48 | } 49 | 50 | /// 51 | /// Loads an AFF beatmap from file. 52 | /// 53 | /// Path to the file that contains AFF beatmap data. 54 | public void Load([NotNull] string filePath) { 55 | Beatmap newBeatmap; 56 | 57 | using (var fileStream = File.Open(filePath, FileMode.Open, FileAccess.Read, FileShare.Read)) { 58 | using (var reader = new StreamReader(fileStream, Encoding.UTF8)) { 59 | Beatmap.TryParse(reader, out newBeatmap); 60 | } 61 | } 62 | 63 | var oldBeatmap = Beatmap; 64 | Beatmap = newBeatmap; 65 | 66 | BeatmapChanged?.Invoke(this, new BeatmapChangedEventArgs(oldBeatmap, newBeatmap)); 67 | 68 | var game = Game.ToBaseGame(); 69 | var debugOverlay = game.FindSingleElement(); 70 | 71 | if (debugOverlay != null) { 72 | if (newBeatmap != null) { 73 | debugOverlay.AddLine($"Loaded beatmap from '{filePath}'."); 74 | } else { 75 | debugOverlay.AddLine($"Failed to load beatmap from '{filePath}'."); 76 | } 77 | } 78 | } 79 | 80 | protected override void OnLoadContents() { 81 | base.OnLoadContents(); 82 | 83 | var config = ConfigurationStore.Get(); 84 | 85 | Load(config.Data.FilePath); 86 | } 87 | 88 | } 89 | } 90 | 91 | -------------------------------------------------------------------------------- /ArcaeaView/Components/BeatmapLoaderFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenMLTD.MilliSim.Core; 3 | using OpenMLTD.MilliSim.Foundation; 4 | using OpenMLTD.MilliSim.Foundation.Extending; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Components { 7 | [MilliSimPlugin(typeof(IBaseGameComponentFactory))] 8 | public sealed class BeatmapLoaderFactory : BaseGameComponentFactory { 9 | 10 | public override string PluginID => "plugin.component_factory.arcaea.beatmap_loader"; 11 | 12 | public override string PluginName => "Beatmap Loader Factory"; 13 | 14 | public override string PluginDescription => "Arcaea Beatmap Loader Factory"; 15 | 16 | public override string PluginAuthor => "hozuki"; 17 | 18 | public override Version PluginVersion => MyVersion; 19 | 20 | public override IBaseGameComponent CreateComponent(BaseGame game, IBaseGameComponentContainer parent) { 21 | return new BeatmapLoader(game, parent); 22 | } 23 | 24 | private static readonly Version MyVersion = new Version(1, 0, 0, 0); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ArcaeaView/Components/Cameraman.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Moe.Mottomo.ArcaeaSim.Subsystems.Rendering; 4 | using OpenMLTD.MilliSim.Foundation; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Components { 7 | /// 8 | /// The component that holds the main camera. 9 | /// 10 | public sealed class Cameraman : BaseGameComponent { 11 | 12 | public Cameraman([NotNull] BaseGame game, [CanBeNull] IBaseGameComponentContainer parent) 13 | : base(game, parent) { 14 | } 15 | 16 | /// 17 | /// Gets the camera held by this component. 18 | /// 19 | public Camera Camera { get; private set; } 20 | 21 | protected override void OnUpdate(GameTime gameTime) { 22 | base.OnUpdate(gameTime); 23 | 24 | Camera.Update(); 25 | } 26 | 27 | protected override void OnInitialize() { 28 | base.OnInitialize(); 29 | 30 | var camera = new Camera(Game.GraphicsDevice); 31 | 32 | Camera = camera; 33 | 34 | camera.Reset(); 35 | } 36 | 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /ArcaeaView/Components/CameramanFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenMLTD.MilliSim.Core; 3 | using OpenMLTD.MilliSim.Foundation; 4 | using OpenMLTD.MilliSim.Foundation.Extending; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Components { 7 | [MilliSimPlugin(typeof(IBaseGameComponentFactory))] 8 | public sealed class CameramanFactory : BaseGameComponentFactory { 9 | 10 | public override string PluginID => "plugin.component_factory.arcaea.cameraman"; 11 | 12 | public override string PluginName => "Cameraman Factory"; 13 | 14 | public override string PluginDescription => "Cameraman Factory"; 15 | 16 | public override string PluginAuthor => "hozuki"; 17 | 18 | public override Version PluginVersion => MyVersion; 19 | 20 | public override IBaseGameComponent CreateComponent(BaseGame game, IBaseGameComponentContainer parent) { 21 | return new Cameraman(game, parent); 22 | } 23 | 24 | private static readonly Version MyVersion = new Version(1, 0, 0, 0); 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /ArcaeaView/Components/TrackDisplayFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using OpenMLTD.MilliSim.Core; 3 | using OpenMLTD.MilliSim.Foundation; 4 | using OpenMLTD.MilliSim.Foundation.Extending; 5 | using OpenMLTD.MilliSim.Graphics; 6 | 7 | namespace Moe.Mottomo.ArcaeaSim.Components { 8 | [MilliSimPlugin(typeof(IBaseGameComponentFactory))] 9 | public sealed class TrackDisplayFactory : BaseGameComponentFactory { 10 | 11 | public override string PluginID => "plugin.component_factory.arcaea.track_display"; 12 | 13 | public override string PluginName => "Track Display Factory"; 14 | 15 | public override string PluginDescription => "Track Display Factory"; 16 | 17 | public override string PluginAuthor => "hozuki"; 18 | 19 | public override Version PluginVersion => MyVersion; 20 | 21 | public override IBaseGameComponent CreateComponent(BaseGame game, IBaseGameComponentContainer parent) { 22 | return new TrackDisplay(game, (IVisualContainer)parent); 23 | } 24 | 25 | private static readonly Version MyVersion = new Version(1, 0, 0, 0); 26 | 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /ArcaeaView/Configuration/BeatmapLoaderConfig.cs: -------------------------------------------------------------------------------- 1 | using OpenMLTD.MilliSim.Configuration.Entities; 2 | 3 | namespace Moe.Mottomo.ArcaeaSim.Configuration { 4 | public sealed class BeatmapLoaderConfig : ConfigBase { 5 | 6 | public ScoreLoaderConfigData Data { get; set; } 7 | 8 | public sealed class ScoreLoaderConfigData { 9 | 10 | public string Title { get; set; } 11 | 12 | public string FilePath { get; set; } 13 | 14 | } 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ArcaeaView/Configuration/TrackDisplayConfig.cs: -------------------------------------------------------------------------------- 1 | using OpenMLTD.MilliSim.Configuration.Entities; 2 | 3 | namespace Moe.Mottomo.ArcaeaSim.Configuration { 4 | public sealed class TrackDisplayConfig : ConfigBase { 5 | 6 | public TrackDisplayConfigData Data { get; set; } 7 | 8 | public sealed class TrackDisplayConfigData { 9 | 10 | public string PanelTexture { get; set; } 11 | 12 | public string TrackLaneDividerTexture { get; set; } 13 | 14 | public string SkyInputTexture { get; set; } 15 | 16 | public string NoteTexture { get; set; } 17 | 18 | public string NoteHoldTexture { get; set; } 19 | 20 | public string NoteHoldHighlightedTexture { get; set; } 21 | 22 | public string NoteSkyTexture { get; set; } 23 | 24 | public string NoteArcTexture { get; set; } 25 | 26 | } 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ArcaeaView/Contents/config/beatmap_loader.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | type: Moe.Mottomo.ArcaeaSim.Configuration.BeatmapLoaderConfig 3 | assembly_file: ArcaeaView.dll 4 | for: component.score.beatmap_loader 5 | version: 1 # format version 6 | data: 7 | title: "The Silence" 8 | file_path: Contents/game/The Silence/2.aff 9 | -------------------------------------------------------------------------------- /ArcaeaView/Contents/config/track_display.yml: -------------------------------------------------------------------------------- 1 | metadata: 2 | type: Moe.Mottomo.ArcaeaSim.Configuration.TrackDisplayConfig 3 | assembly_file: ArcaeaView.dll 4 | for: component.score.track_display 5 | version: 1 # format version 6 | data: 7 | panel_texture: Contents/res/img/track_dark.png 8 | track_lane_divider_texture: Contents/res/img/track_lane_divider.png 9 | sky_input_texture: Contents/res/img/air_input.png 10 | note_texture: Contents/res/img/note_dark.png 11 | note_hold_texture: Contents/res/img/note_hold_dark.png 12 | note_hold_highlighted_texture: Contents/res/img/note_hold_dark_hi.png 13 | note_sky_texture: Contents/res/img/tap_d.png 14 | note_arc_texture: Contents/res/img/arc_body.png 15 | -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/fx/compile_effects.bat: -------------------------------------------------------------------------------- 1 | @FOR %%a IN (*.fx) DO @( 2 | "C:\Program Files (x86)\MSBuild\MonoGame\v3.0\Tools\2MGFX.exe" "%%a" "%%a.ogl.mgfxo" /Profile:OpenGL 3 | "C:\Program Files (x86)\MSBuild\MonoGame\v3.0\Tools\2MGFX.exe" "%%a" "%%a.dx11.mgfxo" /Profile:DirectX_11 4 | ) 5 | -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/fx/monogame.fxh: -------------------------------------------------------------------------------- 1 | #if OPENGL 2 | #define SV_POSITION POSITION 3 | #define VS_SHADERMODEL vs_3_0 4 | #define PS_SHADERMODEL ps_3_0 5 | #else 6 | #define VS_SHADERMODEL vs_4_0_level_9_1 7 | #define PS_SHADERMODEL ps_4_0_level_9_1 8 | #endif 9 | -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/air_input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/air_input.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/arc_body.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/arc_body.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/arc_body_hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/arc_body_hi.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/arc_cap.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/arc_cap.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/bg/base_conflict.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/bg/base_conflict.jpg -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/finish_line.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/finish_line.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/note_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/note_dark.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/note_hold_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/note_hold_dark.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/note_hold_dark_hi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/note_hold_dark_hi.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/tap_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/tap_d.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/track_dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/track_dark.png -------------------------------------------------------------------------------- /ArcaeaView/Contents/res/img/track_lane_divider.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hozuki/ArcaeaSim2/4780dcc5a284837fb17ae5083e7de93304025c2c/ArcaeaView/Contents/res/img/track_lane_divider.png -------------------------------------------------------------------------------- /ArcaeaView/Core/MathF.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Core { 5 | /// 6 | /// Floating point number helper functions. 7 | /// 8 | public static class MathF { 9 | 10 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 11 | public static float Cos(float radian) { 12 | return (float)Math.Cos(radian); 13 | } 14 | 15 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 16 | public static float Sin(float radian) { 17 | return (float)Math.Sin(radian); 18 | } 19 | 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public static float Atan(float d) { 22 | return (float)Math.Atan(d); 23 | } 24 | 25 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 26 | public static float Atan2(float y, float x) { 27 | return (float)Math.Atan2(y, x); 28 | } 29 | 30 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 31 | public static float Tan(float radian) { 32 | return (float)Math.Tan(radian); 33 | } 34 | 35 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 36 | public static float Sqrt(float value) { 37 | return (float)Math.Sqrt(value); 38 | } 39 | 40 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 41 | public static float ClampLower(float value, float lowerBound) { 42 | return value < lowerBound ? lowerBound : value; 43 | } 44 | 45 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 46 | public static float ClampUpper(float value, float upperBound) { 47 | return value > upperBound ? upperBound : value; 48 | } 49 | 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /ArcaeaView/Extensions/ColorExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework; 2 | 3 | namespace Moe.Mottomo.ArcaeaSim.Extensions { 4 | public static class ColorExtensions { 5 | 6 | /// 7 | /// Return a new color whose brighness is changed from a base color. 8 | /// 9 | /// Base color. 10 | /// A value between [-255, 255] 11 | /// 12 | public static Color ChangeBrightness(this Color color, int amount) { 13 | var a = color.A; 14 | var r = MathHelper.Clamp(color.R + amount, 0, 255); 15 | var g = MathHelper.Clamp(color.G + amount, 0, 255); 16 | var b = MathHelper.Clamp(color.B + amount, 0, 255); 17 | 18 | return new Color(r, g, b, a); 19 | } 20 | 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /ArcaeaView/Extensions/Vector3Extensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using Microsoft.Xna.Framework; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Extensions { 5 | public static class Vector3Extensions { 6 | 7 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 8 | public static Vector2 XY(this Vector3 vector) { 9 | return new Vector2(vector.X, vector.Y); 10 | } 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ArcaeaView/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using System.Runtime.InteropServices; 4 | 5 | // 有关程序集的一般信息由以下 6 | // 控制。更改这些特性值可修改 7 | // 与程序集关联的信息。 8 | [assembly: AssemblyTitle("ArcaeaView")] 9 | [assembly: AssemblyDescription("Arcaea Simulator Core")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("")] 12 | [assembly: AssemblyProduct("ArcaeaView")] 13 | [assembly: AssemblyCopyright("Copyright © 2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // 将 ComVisible 设置为 false 会使此程序集中的类型 18 | //对 COM 组件不可见。如果需要从 COM 访问此程序集中的类型 19 | //请将此类型的 ComVisible 特性设置为 true。 20 | [assembly: ComVisible(false)] 21 | 22 | // 如果此项目向 COM 公开,则下列 GUID 用于类型库的 ID 23 | [assembly: Guid("8647e62c-8760-4ee3-9cde-0f2d15fa7cb4")] 24 | 25 | // 程序集的版本信息由下列四个值组成: 26 | // 27 | // 主版本 28 | // 次版本 29 | // 生成号 30 | // 修订号 31 | // 32 | // 可以指定所有值,也可以使用以下所示的 "*" 预置版本号和修订号 33 | //通过使用 "*",如下所示: 34 | // [assembly: AssemblyVersion("1.0.*")] 35 | [assembly: AssemblyVersion("1.0.0.0")] 36 | [assembly: AssemblyFileVersion("1.0.0.0")] 37 | 38 | [assembly: CLSCompliant(true)] 39 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/BottomlessColoredHexahedron.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Actually front-less and back-less (LOL). 9 | /// Uses . 10 | /// 11 | public sealed class BottomlessColoredHexahedron : DrawableGeometryMesh { 12 | 13 | public BottomlessColoredHexahedron([NotNull] GraphicsDevice graphicsDevice) { 14 | _graphicsDevice = graphicsDevice; 15 | 16 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColor.VertexDeclaration, 8, BufferUsage.WriteOnly); 17 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 24, BufferUsage.WriteOnly); 18 | 19 | var indices = new ushort[] { 20 | // No front and back 21 | //0, 1, 2, 22 | //2, 1, 3, 23 | //4, 5, 6, 24 | //6, 5, 7, 25 | 26 | 0, 1, 4, 27 | 1, 4, 5, 28 | 2, 3, 6, 29 | 3, 6, 7, 30 | 31 | 0, 2, 4, 32 | 2, 4, 6, 33 | 1, 3, 5, 34 | 3, 5, 7 35 | }; 36 | 37 | _indexBuffer.SetData(indices); 38 | } 39 | 40 | public override void Draw(EffectTechnique technique) { 41 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 42 | _graphicsDevice.Indices = _indexBuffer; 43 | 44 | foreach (var pass in technique.Passes) { 45 | pass.Apply(); 46 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8); 47 | } 48 | } 49 | 50 | public void SetVertices(Vector3 start, Vector3 end, Color color, Vector2 sectionSize) { 51 | var vertices = new[] { 52 | new VertexPositionColor {Position = new Vector3(start.X - sectionSize.X / 2, start.Y, start.Z - sectionSize.Y / 2), Color = color}, 53 | new VertexPositionColor {Position = new Vector3(start.X + sectionSize.X / 2, start.Y, start.Z - sectionSize.Y / 2), Color = color}, 54 | new VertexPositionColor {Position = new Vector3(start.X - sectionSize.X / 2, start.Y, start.Z + sectionSize.Y / 2), Color = color}, 55 | new VertexPositionColor {Position = new Vector3(start.X + sectionSize.X / 2, start.Y, start.Z + sectionSize.Y / 2), Color = color}, 56 | 57 | new VertexPositionColor {Position = new Vector3(end.X - sectionSize.X / 2, end.Y, end.Z - sectionSize.Y / 2), Color = color}, 58 | new VertexPositionColor {Position = new Vector3(end.X + sectionSize.X / 2, end.Y, end.Z - sectionSize.Y / 2), Color = color}, 59 | new VertexPositionColor {Position = new Vector3(end.X - sectionSize.X / 2, end.Y, end.Z + sectionSize.Y / 2), Color = color}, 60 | new VertexPositionColor {Position = new Vector3(end.X + sectionSize.X / 2, end.Y, end.Z + sectionSize.Y / 2), Color = color} 61 | }; 62 | 63 | _vertexBuffer.SetData(vertices); 64 | } 65 | 66 | protected override void Dispose(bool disposing) { 67 | _vertexBuffer.Dispose(); 68 | _indexBuffer.Dispose(); 69 | 70 | _vertexBuffer = null; 71 | _indexBuffer = null; 72 | } 73 | 74 | private readonly GraphicsDevice _graphicsDevice; 75 | private VertexBuffer _vertexBuffer; 76 | private IndexBuffer _indexBuffer; 77 | 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/BottomlessColoredTetrahedron.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Actually front-less and back-less (LOL). 9 | /// Uses . 10 | /// 11 | public sealed class BottomlessColoredTetrahedron : DrawableGeometryMesh { 12 | 13 | public BottomlessColoredTetrahedron([NotNull] GraphicsDevice graphicsDevice) { 14 | _graphicsDevice = graphicsDevice; 15 | 16 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColor.VertexDeclaration, 4, BufferUsage.WriteOnly); 17 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 9, BufferUsage.WriteOnly); 18 | 19 | var indices = new ushort[] { 20 | 0, 1, 2, 21 | 0, 1, 3 22 | }; 23 | 24 | _indexBuffer.SetData(indices); 25 | } 26 | 27 | public override void Draw(EffectTechnique technique) { 28 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 29 | _graphicsDevice.Indices = _indexBuffer; 30 | 31 | foreach (var pass in technique.Passes) { 32 | pass.Apply(); 33 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 3); 34 | } 35 | } 36 | 37 | public void SetVerticesXZ(Vector3 point, Color color, float rightAngleSideWidth) { 38 | var std = rightAngleSideWidth / 2; 39 | var dx = std; 40 | var dy = std; 41 | var dzDown = std / 2; 42 | var dzUp = dzDown; 43 | 44 | var vertices = new[] { 45 | new VertexPositionColor {Position = new Vector3(point.X, point.Y, point.Z + dzUp), Color = color}, 46 | new VertexPositionColor {Position = new Vector3(point.X, point.Y - dy, point.Z - dzDown), Color = color}, 47 | new VertexPositionColor {Position = new Vector3(point.X - dx, point.Y, point.Z - dzDown), Color = color}, 48 | new VertexPositionColor {Position = new Vector3(point.X + dx, point.Y, point.Z - dzDown), Color = color}, 49 | }; 50 | 51 | _vertexBuffer.SetData(vertices); 52 | } 53 | 54 | protected override void Dispose(bool disposing) { 55 | _vertexBuffer.Dispose(); 56 | _indexBuffer.Dispose(); 57 | 58 | _vertexBuffer = null; 59 | _indexBuffer = null; 60 | } 61 | 62 | private readonly GraphicsDevice _graphicsDevice; 63 | private VertexBuffer _vertexBuffer; 64 | private IndexBuffer _indexBuffer; 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/BottomlessColoredTriangularPrism.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Moe.Mottomo.ArcaeaSim.Core; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 7 | /// 8 | /// 9 | /// Uses . 10 | /// 11 | public sealed class BottomlessColoredTriangularPrism : DrawableGeometryMesh { 12 | 13 | public BottomlessColoredTriangularPrism([NotNull] GraphicsDevice graphicsDevice) { 14 | _graphicsDevice = graphicsDevice; 15 | 16 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColor.VertexDeclaration, 6, BufferUsage.WriteOnly); 17 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 12, BufferUsage.WriteOnly); 18 | 19 | var indices = new ushort[] { 20 | // Arcs have only two sides. 21 | 0, 1, 3, 22 | 3, 1, 4, 23 | 0, 2, 3, 24 | 3, 2, 5 25 | }; 26 | 27 | _indexBuffer.SetData(indices); 28 | } 29 | 30 | public override void Draw(EffectTechnique technique) { 31 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 32 | _graphicsDevice.Indices = _indexBuffer; 33 | 34 | foreach (var pass in technique.Passes) { 35 | pass.Apply(); 36 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 4); 37 | } 38 | } 39 | 40 | public void SetVerticesXY(Vector3 start, Vector3 end, Color color, float rightAngleSideWidth) { 41 | // Half of the width of the right angle side 42 | var std = rightAngleSideWidth / 2; 43 | var dx = std; 44 | var dzDown = std / 2; 45 | var dzUp = dzDown; 46 | 47 | var vertices = new[] { 48 | new VertexPositionColor {Position = new Vector3(start.X, start.Y, start.Z + dzUp), Color = color}, 49 | new VertexPositionColor {Position = new Vector3(start.X - dx, start.Y, start.Z - dzDown), Color = color}, 50 | new VertexPositionColor {Position = new Vector3(start.X + dx, start.Y, start.Z - dzDown), Color = color}, 51 | new VertexPositionColor {Position = new Vector3(end.X, end.Y, end.Z + dzUp), Color = color}, 52 | new VertexPositionColor {Position = new Vector3(end.X - dx, end.Y, end.Z - dzDown), Color = color}, 53 | new VertexPositionColor {Position = new Vector3(end.X + dx, end.Y, end.Z - dzDown), Color = color} 54 | }; 55 | 56 | _vertexBuffer.SetData(vertices); 57 | } 58 | 59 | protected override void Dispose(bool disposing) { 60 | _vertexBuffer.Dispose(); 61 | _indexBuffer.Dispose(); 62 | 63 | _vertexBuffer = null; 64 | _indexBuffer = null; 65 | } 66 | 67 | private readonly GraphicsDevice _graphicsDevice; 68 | private VertexBuffer _vertexBuffer; 69 | private IndexBuffer _indexBuffer; 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/BottomlessTexturedTetrahedron.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Actually front-less and back-less (LOL). 9 | /// Uses . 10 | /// 11 | public sealed class BottomlessTexturedTetrahedron : DrawableGeometryMesh { 12 | 13 | public BottomlessTexturedTetrahedron([NotNull] GraphicsDevice graphicsDevice) { 14 | _graphicsDevice = graphicsDevice; 15 | 16 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, 4, BufferUsage.WriteOnly); 17 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 9, BufferUsage.WriteOnly); 18 | 19 | var indices = new ushort[] { 20 | 0, 1, 2, 21 | 0, 1, 3 22 | }; 23 | 24 | _indexBuffer.SetData(indices); 25 | } 26 | 27 | public override void Draw(EffectTechnique technique) { 28 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 29 | _graphicsDevice.Indices = _indexBuffer; 30 | 31 | foreach (var pass in technique.Passes) { 32 | pass.Apply(); 33 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 3); 34 | } 35 | } 36 | 37 | public void SetVerticesXZ(Vector3 point, Color color, float rightAngleSideWidth) { 38 | var std = rightAngleSideWidth / 2; 39 | var dx = std; 40 | var dy = std; 41 | var dzDown = std / 2; 42 | var dzUp = dzDown; 43 | 44 | var vertices = new[] { 45 | new VertexPositionColorTexture {Position = new Vector3(point.X, point.Y, point.Z + dzUp), Color = color, TextureCoordinate = new Vector2(1, 0)}, 46 | new VertexPositionColorTexture {Position = new Vector3(point.X, point.Y - dy, point.Z - dzDown), Color = color, TextureCoordinate = new Vector2(0, 1)}, 47 | new VertexPositionColorTexture {Position = new Vector3(point.X - dx, point.Y, point.Z - dzDown), Color = color, TextureCoordinate = new Vector2(0, 0)}, 48 | new VertexPositionColorTexture {Position = new Vector3(point.X + dx, point.Y, point.Z - dzDown), Color = color, TextureCoordinate = new Vector2(0, 0)}, 49 | }; 50 | 51 | _vertexBuffer.SetData(vertices); 52 | } 53 | 54 | protected override void Dispose(bool disposing) { 55 | _vertexBuffer.Dispose(); 56 | _indexBuffer.Dispose(); 57 | 58 | _vertexBuffer = null; 59 | _indexBuffer = null; 60 | } 61 | 62 | private readonly GraphicsDevice _graphicsDevice; 63 | private VertexBuffer _vertexBuffer; 64 | private IndexBuffer _indexBuffer; 65 | 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/BottomlessTexturedTriangularPrism.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Moe.Mottomo.ArcaeaSim.Core; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 7 | /// 8 | /// 9 | /// Uses . 10 | /// 11 | public sealed class BottomlessTexturedTriangularPrism : DrawableGeometryMesh { 12 | 13 | public BottomlessTexturedTriangularPrism([NotNull] GraphicsDevice graphicsDevice) { 14 | _graphicsDevice = graphicsDevice; 15 | 16 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, 6, BufferUsage.WriteOnly); 17 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 12, BufferUsage.WriteOnly); 18 | 19 | var indices = new ushort[] { 20 | // Arcs have only two sides. 21 | 0, 1, 3, 22 | 3, 1, 4, 23 | 0, 2, 3, 24 | 3, 2, 5 25 | }; 26 | 27 | _indexBuffer.SetData(indices); 28 | } 29 | 30 | public override void Draw(EffectTechnique technique) { 31 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 32 | _graphicsDevice.Indices = _indexBuffer; 33 | 34 | foreach (var pass in technique.Passes) { 35 | pass.Apply(); 36 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 4); 37 | } 38 | } 39 | 40 | public void SetVerticesXY(Vector3 start, Vector3 end, Color color, float rightAngleSideWidth) { 41 | // Half of the width of the right angle side 42 | var std = rightAngleSideWidth / 2; 43 | var dx = std; 44 | var dzDown = std / 2; 45 | var dzUp = dzDown; 46 | 47 | var vertices = new[] { 48 | new VertexPositionColorTexture {Position = new Vector3(start.X, start.Y, start.Z + dzUp), Color = color, TextureCoordinate = new Vector2(1, 1)}, 49 | new VertexPositionColorTexture {Position = new Vector3(start.X - dx, start.Y, start.Z - dzDown), Color = color, TextureCoordinate = new Vector2(0, 1)}, 50 | new VertexPositionColorTexture {Position = new Vector3(start.X + dx, start.Y, start.Z - dzDown), Color = color, TextureCoordinate = new Vector2(0, 1)}, 51 | new VertexPositionColorTexture {Position = new Vector3(end.X, end.Y, end.Z + dzUp), Color = color, TextureCoordinate = new Vector2(1, 0)}, 52 | new VertexPositionColorTexture {Position = new Vector3(end.X - dx, end.Y, end.Z - dzDown), Color = color, TextureCoordinate = new Vector2(0, 0)}, 53 | new VertexPositionColorTexture {Position = new Vector3(end.X + dx, end.Y, end.Z - dzDown), Color = color, TextureCoordinate = new Vector2(0, 0)} 54 | }; 55 | 56 | _vertexBuffer.SetData(vertices); 57 | } 58 | 59 | protected override void Dispose(bool disposing) { 60 | _vertexBuffer.Dispose(); 61 | _indexBuffer.Dispose(); 62 | 63 | _vertexBuffer = null; 64 | _indexBuffer = null; 65 | } 66 | 67 | private readonly GraphicsDevice _graphicsDevice; 68 | private VertexBuffer _vertexBuffer; 69 | private IndexBuffer _indexBuffer; 70 | 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/Camera.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Moe.Mottomo.ArcaeaSim.Core; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 7 | /// 8 | /// A simple FPS-like perspective camera. 9 | /// 10 | public sealed class Camera { 11 | 12 | /// 13 | /// Creates a new instance. 14 | /// 15 | /// 16 | public Camera([NotNull] GraphicsDevice graphicsDevice) { 17 | _graphicsDevice = graphicsDevice; 18 | 19 | Update(); 20 | } 21 | 22 | /// 23 | /// Gets/sets the position of the camera. 24 | /// 25 | public Vector3 Position { get; set; } 26 | 27 | /// 28 | /// Gets the view matrix of the camera. 29 | /// 30 | public Matrix ViewMatrix { get; private set; } 31 | 32 | /// 33 | /// Gets the projection matrix of the camera. 34 | /// 35 | public Matrix ProjectionMatrix { get; private set; } 36 | 37 | /// 38 | /// Gets/sets the target to look at. 39 | /// 40 | public Vector3 LookAtTarget { 41 | get => _lookAtTarget; 42 | set { 43 | _lookAtTarget = value; 44 | _forward = value - Position; 45 | _right = -Vector3.Normalize(Vector3.Cross(_up, _forward)); 46 | } 47 | } 48 | 49 | /// 50 | /// Gets/sets the up vector. 51 | /// 52 | public Vector3 Up { 53 | get => _up; 54 | set { 55 | _up = value; 56 | _forward = _lookAtTarget - Position; 57 | _right = -Vector3.Normalize(Vector3.Cross(value, _forward)); 58 | } 59 | } 60 | 61 | /// 62 | /// Gets/sets the field of view. 63 | /// 64 | public float FieldOfView { 65 | get => _fieldOfView; 66 | set => _fieldOfView = MathF.ClampLower(value, 0.01f); 67 | } 68 | 69 | /// 70 | /// Gets/sets the near clip distance. 71 | /// 72 | public float NearClip { 73 | get => _nearClip; 74 | set => _nearClip = MathF.ClampLower(value, 0.0001f); 75 | } 76 | 77 | /// 78 | /// Gets/sets the far clip distance. 79 | /// 80 | public float FarClip { 81 | get => _farClip; 82 | set => _farClip = MathF.ClampLower(value, 0.1f); 83 | } 84 | 85 | /// 86 | /// Gets the right vector. 87 | /// 88 | public Vector3 Right { 89 | get => _right; 90 | private set => _right = value; 91 | } 92 | 93 | /// 94 | /// Gets the forward vector. 95 | /// 96 | public Vector3 Forward { 97 | get => _forward; 98 | private set => _forward = value; 99 | } 100 | 101 | /// 102 | /// Strafe left or right. 103 | /// 104 | /// Strafe distance. Use a positive value to strafe right, and negative value to strafe left. 105 | public void Strafe(float rightDistance) { 106 | _right.Normalize(); 107 | 108 | var delta = _right * rightDistance; 109 | Position += delta; 110 | _lookAtTarget += delta; 111 | } 112 | 113 | /// 114 | /// Walk forward or backwards. 115 | /// 116 | /// Walk distance. Use a positive value to walk forward, and negative value to walk backwards. 117 | public void Walk(float frontDistance) { 118 | _forward.Normalize(); 119 | 120 | var delta = _forward * frontDistance; 121 | Position += delta; 122 | _lookAtTarget += delta; 123 | } 124 | 125 | /// 126 | /// Rotate X. 127 | /// 128 | /// The delta rotation angle, in radians. Use a positive value to rotate down, and a negative value to rotate up. 129 | public void Pitch(float amount) { 130 | _pitch += amount; 131 | 132 | _forward.Normalize(); 133 | 134 | var left = Vector3.Cross(_up, _forward); 135 | left.Normalize(); 136 | 137 | var length = (_lookAtTarget - Position).Length(); 138 | 139 | _right = -left; 140 | _forward = Vector3.Transform(_forward, Matrix.CreateFromAxisAngle(left, amount)); 141 | _up = Vector3.Transform(_up, Matrix.CreateFromAxisAngle(left, amount)); 142 | 143 | _lookAtTarget = Position + _forward * length; 144 | } 145 | 146 | /// 147 | /// Rotate Z. 148 | /// 149 | /// The delta rotation angle, in radians. Use a positive value to rotate left, and a negative value to rotate right. 150 | public void Yaw(float amount) { 151 | _yaw += amount; 152 | 153 | _forward.Normalize(); 154 | 155 | var length = (_lookAtTarget - Position).Length(); 156 | 157 | _forward = Vector3.Transform(_forward, Matrix.CreateFromAxisAngle(_up, amount)); 158 | 159 | _lookAtTarget = Position + _forward * length; 160 | } 161 | 162 | /// 163 | /// Rotate Y. 164 | /// 165 | /// The delta rotation angle, in radians. Use a positive value to rotate counterclockwise, and a negative value to rotate clockwise. 166 | public void Roll(float amount) { 167 | _roll += amount; 168 | 169 | _up.Normalize(); 170 | var left = Vector3.Cross(_up, _forward); 171 | left.Normalize(); 172 | 173 | _up = Vector3.Transform(_up, Matrix.CreateFromAxisAngle(_forward, amount)); 174 | } 175 | 176 | /// 177 | /// Zoom in or zoom out. 178 | /// 179 | /// The delta scale. Affects FOV. 180 | public void Zoom(float deltaScale) { 181 | var newFov = MathHelper.Clamp(FieldOfView + deltaScale, 0.1f, MathHelper.Pi); 182 | 183 | FieldOfView = newFov; 184 | } 185 | 186 | /// 187 | /// Update the view matrix and the projection matrix of the camera. 188 | /// 189 | public void Update() { 190 | var view = Matrix.CreateLookAt(Position, LookAtTarget, Up); 191 | 192 | var aspectRatio = _graphicsDevice.Viewport.AspectRatio; 193 | var projection = Matrix.CreatePerspectiveFieldOfView(FieldOfView, aspectRatio, NearClip, FarClip); 194 | 195 | ViewMatrix = view; 196 | ProjectionMatrix = projection; 197 | } 198 | 199 | /// 200 | /// Reset camera parameters to initial state (not default state). 201 | /// 202 | public void Reset() { 203 | Position = Vector3.UnitZ * ViewerHeight; 204 | LookAtTarget = new Vector3(0, FarClip * 0.2f, 0); 205 | FieldOfView = MathHelper.ToRadians(60); 206 | 207 | _forward = _lookAtTarget - Position; 208 | _forward.Normalize(); 209 | _right = Vector3.UnitX; 210 | _up = Vector3.Cross(_right, _forward); 211 | 212 | _pitch = 0; 213 | _yaw = 0; 214 | _roll = 0; 215 | } 216 | 217 | private const float ViewerHeight = 10.5f; 218 | 219 | private Vector3 _up = Vector3.UnitZ; 220 | private Vector3 _lookAtTarget; 221 | private Vector3 _forward = Vector3.UnitY; 222 | private Vector3 _right = Vector3.UnitX; 223 | 224 | private float _fieldOfView = MathHelper.PiOver2; 225 | 226 | private float _nearClip = 0.5f; 227 | private float _farClip = 120f; 228 | 229 | private float _pitch; 230 | private float _yaw; 231 | private float _roll; 232 | 233 | private readonly GraphicsDevice _graphicsDevice; 234 | 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/ColoredBox.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Represents a colored box. A box is defined by its bottom left back corner (near to the X axis) and its size on X, Y and Z axes. Its faces are parallel to the axes. 9 | /// Uses vertex format . 10 | /// 11 | public sealed class ColoredBox : DrawableGeometryMesh { 12 | 13 | /// 14 | /// Creates a new instance. 15 | /// 16 | /// The to use for rendering the box. 17 | public ColoredBox([NotNull] GraphicsDevice graphicsDevice) { 18 | _graphicsDevice = graphicsDevice; 19 | 20 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColor.VertexDeclaration, 8, BufferUsage.WriteOnly); 21 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 36, BufferUsage.WriteOnly); 22 | 23 | var indices = new ushort[] { 24 | 0, 1, 2, 25 | 2, 1, 3, 26 | 4, 5, 6, 27 | 6, 5, 7, 28 | 29 | 0, 1, 4, 30 | 1, 4, 5, 31 | 2, 3, 6, 32 | 3, 6, 7, 33 | 0, 2, 4, 34 | 2, 4, 6, 35 | 1, 3, 5, 36 | 3, 5, 7 37 | }; 38 | 39 | _indexBuffer.SetData(indices); 40 | } 41 | 42 | public override void Draw(EffectTechnique technique) { 43 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 44 | _graphicsDevice.Indices = _indexBuffer; 45 | 46 | foreach (var pass in technique.Passes) { 47 | pass.Apply(); 48 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 12); 49 | } 50 | } 51 | 52 | /// 53 | /// Set and update mesh vertices. 54 | /// 55 | /// The bottom left back point. 56 | /// Size of this box. 57 | /// The color of this box. 58 | public void SetVertices(Vector3 bottomNearLeft, Vector3 size, Color color) { 59 | var vertices = new[] { 60 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color}, 61 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color}, 62 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color}, 63 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color}, 64 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color}, 65 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color}, 66 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color}, 67 | new VertexPositionColor {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color} 68 | }; 69 | 70 | _vertexBuffer.SetData(vertices); 71 | } 72 | 73 | protected override void Dispose(bool disposing) { 74 | _vertexBuffer.Dispose(); 75 | _indexBuffer.Dispose(); 76 | 77 | _vertexBuffer = null; 78 | _indexBuffer = null; 79 | } 80 | 81 | private readonly GraphicsDevice _graphicsDevice; 82 | private VertexBuffer _vertexBuffer; 83 | private IndexBuffer _indexBuffer; 84 | 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/ColoredHexahedron.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Uses . 9 | /// 10 | public sealed class ColoredHexahedron : DrawableGeometryMesh { 11 | 12 | public ColoredHexahedron([NotNull] GraphicsDevice graphicsDevice) { 13 | _graphicsDevice = graphicsDevice; 14 | 15 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColor.VertexDeclaration, 8, BufferUsage.WriteOnly); 16 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 36, BufferUsage.WriteOnly); 17 | 18 | var indices = new ushort[] { 19 | 0, 1, 2, 20 | 2, 1, 3, 21 | 4, 5, 6, 22 | 6, 5, 7, 23 | 24 | 0, 1, 4, 25 | 1, 4, 5, 26 | 2, 3, 6, 27 | 3, 6, 7, 28 | 0, 2, 4, 29 | 2, 4, 6, 30 | 1, 3, 5, 31 | 3, 5, 7 32 | }; 33 | 34 | _indexBuffer.SetData(indices); 35 | } 36 | 37 | public override void Draw(EffectTechnique technique) { 38 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 39 | _graphicsDevice.Indices = _indexBuffer; 40 | 41 | foreach (var pass in technique.Passes) { 42 | pass.Apply(); 43 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 12); 44 | } 45 | } 46 | 47 | public void SetVerticesXY(Vector3 start, Vector3 end, Color color, Vector2 sectionSize) { 48 | var vertices = new[] { 49 | new VertexPositionColor {Position = new Vector3(start.X - sectionSize.X / 2, start.Y, start.Z - sectionSize.Y / 2), Color = color}, 50 | new VertexPositionColor {Position = new Vector3(start.X + sectionSize.X / 2, start.Y, start.Z - sectionSize.Y / 2), Color = color}, 51 | new VertexPositionColor {Position = new Vector3(start.X - sectionSize.X / 2, start.Y, start.Z + sectionSize.Y / 2), Color = color}, 52 | new VertexPositionColor {Position = new Vector3(start.X + sectionSize.X / 2, start.Y, start.Z + sectionSize.Y / 2), Color = color}, 53 | new VertexPositionColor {Position = new Vector3(end.X - sectionSize.X / 2, end.Y, end.Z - sectionSize.Y / 2), Color = color}, 54 | new VertexPositionColor {Position = new Vector3(end.X + sectionSize.X / 2, end.Y, end.Z - sectionSize.Y / 2), Color = color}, 55 | new VertexPositionColor {Position = new Vector3(end.X - sectionSize.X / 2, end.Y, end.Z + sectionSize.Y / 2), Color = color}, 56 | new VertexPositionColor {Position = new Vector3(end.X + sectionSize.X / 2, end.Y, end.Z + sectionSize.Y / 2), Color = color} 57 | }; 58 | 59 | _vertexBuffer.SetData(vertices); 60 | } 61 | 62 | public void SetVerticesXZ(Vector3 start, Vector3 end, Color color, Vector2 sectionSize) { 63 | var vertices = new[] { 64 | new VertexPositionColor {Position = new Vector3(start.X - sectionSize.X / 2, start.Y - sectionSize.Y / 2, start.Z), Color = color}, 65 | new VertexPositionColor {Position = new Vector3(start.X + sectionSize.X / 2, start.Y - sectionSize.Y / 2, start.Z), Color = color}, 66 | new VertexPositionColor {Position = new Vector3(start.X - sectionSize.X / 2, start.Y + sectionSize.Y / 2, start.Z), Color = color}, 67 | new VertexPositionColor {Position = new Vector3(start.X + sectionSize.X / 2, start.Y + sectionSize.Y / 2, start.Z), Color = color}, 68 | new VertexPositionColor {Position = new Vector3(end.X - sectionSize.X / 2, end.Y - sectionSize.Y / 2, end.Z), Color = color}, 69 | new VertexPositionColor {Position = new Vector3(end.X + sectionSize.X / 2, end.Y - sectionSize.Y / 2, end.Z), Color = color}, 70 | new VertexPositionColor {Position = new Vector3(end.X - sectionSize.X / 2, end.Y + sectionSize.Y / 2, end.Z), Color = color}, 71 | new VertexPositionColor {Position = new Vector3(end.X + sectionSize.X / 2, end.Y + sectionSize.Y / 2, end.Z), Color = color} 72 | }; 73 | 74 | _vertexBuffer.SetData(vertices); 75 | } 76 | 77 | protected override void Dispose(bool disposing) { 78 | _vertexBuffer.Dispose(); 79 | _indexBuffer.Dispose(); 80 | 81 | _vertexBuffer = null; 82 | _indexBuffer = null; 83 | } 84 | 85 | private readonly GraphicsDevice _graphicsDevice; 86 | private VertexBuffer _vertexBuffer; 87 | private IndexBuffer _indexBuffer; 88 | 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/ColoredParallelogram.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Moe.Mottomo.ArcaeaSim.Core; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 7 | /// 8 | /// 9 | /// Uses . 10 | /// 11 | public sealed class ColoredParallelogram : DrawableGeometryMesh { 12 | 13 | public ColoredParallelogram([NotNull] GraphicsDevice graphicsDevice) { 14 | _graphicsDevice = graphicsDevice; 15 | 16 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColor.VertexDeclaration, 4, BufferUsage.WriteOnly); 17 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 6, BufferUsage.WriteOnly); 18 | 19 | var indices = new ushort[] { 20 | 0, 1, 2, 21 | 2, 1, 3 22 | }; 23 | 24 | _indexBuffer.SetData(indices); 25 | } 26 | 27 | public override void Draw(EffectTechnique technique) { 28 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 29 | _graphicsDevice.Indices = _indexBuffer; 30 | 31 | foreach (var pass in technique.Passes) { 32 | pass.Apply(); 33 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 2); 34 | } 35 | } 36 | 37 | public void SetVerticesXY(Vector2 start, Vector2 end, float width, Color color, float z = 0) { 38 | var angle = MathF.Atan2(end.Y - start.Y, end.X - start.X); 39 | var dx = width / 2 * MathF.Sin(angle); 40 | var dy = width / 2 * MathF.Cos(angle); 41 | 42 | var vertices = new[] { 43 | new VertexPositionColor {Position = new Vector3(start.X - dx, start.Y - dy, z), Color = color}, 44 | new VertexPositionColor {Position = new Vector3(start.X + dx, start.Y + dy, z), Color = color}, 45 | new VertexPositionColor {Position = new Vector3(end.X - dx, end.Y - dy, z), Color = color}, 46 | new VertexPositionColor {Position = new Vector3(end.X + dx, end.Y + dy, z), Color = color} 47 | }; 48 | 49 | _vertexBuffer.SetData(vertices); 50 | } 51 | 52 | public void SetVerticesXYParallel(Vector2 start, Vector2 end, float width, Color color, float z = 0) { 53 | var halfWidth = width / 2; 54 | 55 | var vertices = new[] { 56 | new VertexPositionColor {Position = new Vector3(start.X - halfWidth, start.Y, z), Color = color}, 57 | new VertexPositionColor {Position = new Vector3(start.X + halfWidth, start.Y, z), Color = color}, 58 | new VertexPositionColor {Position = new Vector3(end.X - halfWidth, end.Y, z), Color = color}, 59 | new VertexPositionColor {Position = new Vector3(end.X + halfWidth, end.Y, z), Color = color} 60 | }; 61 | 62 | _vertexBuffer.SetData(vertices); 63 | } 64 | 65 | protected override void Dispose(bool disposing) { 66 | _vertexBuffer.Dispose(); 67 | _indexBuffer.Dispose(); 68 | 69 | _vertexBuffer = null; 70 | _indexBuffer = null; 71 | } 72 | 73 | private readonly GraphicsDevice _graphicsDevice; 74 | private VertexBuffer _vertexBuffer; 75 | private IndexBuffer _indexBuffer; 76 | 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/ColoredRectangle.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Uses . 9 | /// 10 | public sealed class ColoredRectangle : DrawableGeometryMesh { 11 | 12 | public ColoredRectangle([NotNull] GraphicsDevice graphicsDevice) { 13 | _graphicsDevice = graphicsDevice; 14 | 15 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColor.VertexDeclaration, 4, BufferUsage.WriteOnly); 16 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 6, BufferUsage.WriteOnly); 17 | 18 | var indices = new ushort[] { 19 | 0, 1, 2, 20 | 2, 1, 3 21 | }; 22 | 23 | _indexBuffer.SetData(indices); 24 | } 25 | 26 | public override void Draw(EffectTechnique technique) { 27 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 28 | _graphicsDevice.Indices = _indexBuffer; 29 | 30 | foreach (var pass in technique.Passes) { 31 | pass.Apply(); 32 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 2); 33 | } 34 | } 35 | 36 | public void SetVerticesXY(Vector2 bottomLeft, Vector2 size, Color color, float z = 0) { 37 | var vertices = new[] { 38 | new VertexPositionColor {Position = new Vector3(bottomLeft.X, bottomLeft.Y, z), Color = color}, 39 | new VertexPositionColor {Position = new Vector3(bottomLeft.X + size.X, bottomLeft.Y, z), Color = color}, 40 | new VertexPositionColor {Position = new Vector3(bottomLeft.X, bottomLeft.Y + size.Y, z), Color = color}, 41 | new VertexPositionColor {Position = new Vector3(bottomLeft.X + size.X, bottomLeft.Y + size.Y, z), Color = color} 42 | }; 43 | 44 | _vertexBuffer.SetData(vertices); 45 | } 46 | 47 | protected override void Dispose(bool disposing) { 48 | _vertexBuffer.Dispose(); 49 | _indexBuffer.Dispose(); 50 | 51 | _vertexBuffer = null; 52 | _indexBuffer = null; 53 | } 54 | 55 | private readonly GraphicsDevice _graphicsDevice; 56 | private VertexBuffer _vertexBuffer; 57 | private IndexBuffer _indexBuffer; 58 | 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/DrawableGeometryMesh.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework.Graphics; 3 | using OpenMLTD.MilliSim.Core; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Represents a drawable mesh of a simple geometry. 9 | /// 10 | public abstract class DrawableGeometryMesh : DisposableBase { 11 | 12 | /// 13 | /// Draws this mesh. Be sure to have its vertex buffer and index buffer ready before calling. 14 | /// 15 | /// The to use to draw this mesh. 16 | public abstract void Draw([NotNull] EffectTechnique technique); 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Rendering/TexturedRectangle.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | 5 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Rendering { 6 | /// 7 | /// 8 | /// Uses . 9 | /// 10 | public sealed class TexturedRectangle : DrawableGeometryMesh { 11 | 12 | public TexturedRectangle([NotNull] GraphicsDevice graphicsDevice) { 13 | _graphicsDevice = graphicsDevice; 14 | 15 | _vertexBuffer = new VertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, 4, BufferUsage.WriteOnly); 16 | _indexBuffer = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 6, BufferUsage.WriteOnly); 17 | 18 | var indices = new ushort[] { 19 | 0, 1, 2, 20 | 2, 1, 3 21 | }; 22 | 23 | _indexBuffer.SetData(indices); 24 | } 25 | 26 | public override void Draw(EffectTechnique technique) { 27 | _graphicsDevice.SetVertexBuffer(_vertexBuffer); 28 | _graphicsDevice.Indices = _indexBuffer; 29 | 30 | foreach (var pass in technique.Passes) { 31 | pass.Apply(); 32 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 2); 33 | } 34 | } 35 | 36 | public void SetVerticesXY(Vector2 origin, Vector2 size, Color color, float z) { 37 | var vertices = new[] { 38 | new VertexPositionColorTexture {Position = new Vector3(origin.X, origin.Y, z), Color = color, TextureCoordinate = new Vector2(0, 1)}, 39 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, origin.Y, z), Color = color, TextureCoordinate = new Vector2(1, 1)}, 40 | new VertexPositionColorTexture {Position = new Vector3(origin.X, origin.Y + size.Y, z), Color = color, TextureCoordinate = new Vector2(0, 0)}, 41 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, origin.Y + size.Y, z), Color = color, TextureCoordinate = new Vector2(1, 0)} 42 | }; 43 | 44 | _vertexBuffer.SetData(vertices); 45 | } 46 | 47 | public void SetVerticesXZ(Vector2 origin, Vector2 size, Color color, float y) { 48 | var vertices = new[] { 49 | new VertexPositionColorTexture {Position = new Vector3(origin.X, y, origin.Y), Color = color, TextureCoordinate = new Vector2(0, 1)}, 50 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, y, origin.Y), Color = color, TextureCoordinate = new Vector2(1, 1)}, 51 | new VertexPositionColorTexture {Position = new Vector3(origin.X, y, origin.Y + size.Y), Color = color, TextureCoordinate = new Vector2(0, 0)}, 52 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, y, origin.Y + size.Y), Color = color, TextureCoordinate = new Vector2(1, 0)} 53 | }; 54 | 55 | _vertexBuffer.SetData(vertices); 56 | } 57 | 58 | public void SetVerticesXY(Vector2 origin, Vector2 size, Color color, float textureYOffset, float z) { 59 | var y = size.Y + textureYOffset; 60 | 61 | var vertices = new[] { 62 | new VertexPositionColorTexture {Position = new Vector3(origin.X, origin.Y, z), Color = color, TextureCoordinate = new Vector2(0, y)}, 63 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, origin.Y, z), Color = color, TextureCoordinate = new Vector2(1, y)}, 64 | new VertexPositionColorTexture {Position = new Vector3(origin.X, origin.Y + size.Y, z), Color = color, TextureCoordinate = new Vector2(0, textureYOffset)}, 65 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, origin.Y + size.Y, z), Color = color, TextureCoordinate = new Vector2(1, textureYOffset)} 66 | }; 67 | 68 | _vertexBuffer.SetData(vertices); 69 | } 70 | 71 | // Rotated, counterclockwise, 90deg 72 | public void SetVerticesXZTextureRotated(Vector2 origin, Vector2 size, Color color, float y) { 73 | var vertices = new[] { 74 | new VertexPositionColorTexture {Position = new Vector3(origin.X, y, origin.Y), Color = color, TextureCoordinate = new Vector2(0, 0)}, 75 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, y, origin.Y), Color = color, TextureCoordinate = new Vector2(0, 1)}, 76 | new VertexPositionColorTexture {Position = new Vector3(origin.X, y, origin.Y + size.Y), Color = color, TextureCoordinate = new Vector2(1, 0)}, 77 | new VertexPositionColorTexture {Position = new Vector3(origin.X + size.X, y, origin.Y + size.Y), Color = color, TextureCoordinate = new Vector2(1, 1)} 78 | }; 79 | 80 | _vertexBuffer.SetData(vertices); 81 | } 82 | 83 | protected override void Dispose(bool disposing) { 84 | _vertexBuffer.Dispose(); 85 | _indexBuffer.Dispose(); 86 | 87 | _vertexBuffer = null; 88 | _indexBuffer = null; 89 | } 90 | 91 | private readonly GraphicsDevice _graphicsDevice; 92 | private VertexBuffer _vertexBuffer; 93 | private IndexBuffer _indexBuffer; 94 | 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/ArcColor.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// Color of an arc, if it is a playable arc (i.e. must be tracked with your fingers). 4 | /// 5 | public enum ArcColor { 6 | 7 | /// 8 | /// Light blue. 9 | /// 10 | LightBlue = 0, 11 | /// 12 | /// Magenta. 13 | /// 14 | Magenta = 1 15 | 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/ArcEasing.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// Arc easing method. 4 | /// 5 | public enum ArcEasing { 6 | 7 | /// 8 | /// Linear. 9 | /// 10 | S = 0, 11 | 12 | /// 13 | /// Cubic bezier, with control points at 1/4 and 3/4. 14 | /// 15 | CubicBezier = 1, 16 | 17 | /// 18 | /// Ease in as a quarter circle, then linear. 19 | /// 20 | Si = 2, 21 | /// 22 | /// Linear, then ease out as a quarter circle. 23 | /// 24 | So = 3, 25 | 26 | /// 27 | /// Ease in as a quarter circle. 28 | /// 29 | SiSo = 4, 30 | /// 31 | /// Ease out as a quarter circle. 32 | /// 33 | SoSi = 5, 34 | 35 | /// 36 | /// Behavior unknown. 37 | /// 38 | SiSi = 6, 39 | /// 40 | /// Behavior unknown. 41 | /// 42 | SoSo = 7 43 | 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/ArcNote.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | 3 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 4 | /// 5 | /// 6 | /// 7 | /// Represents an arc note. 8 | /// 9 | public sealed class ArcNote : NoteBase, IHasTicks { 10 | 11 | public int StartTick { get; set; } 12 | 13 | public int EndTick { get; set; } 14 | 15 | /// 16 | /// Start X position. The playable region should be [-0.5, 1.5]. 17 | /// 18 | public float StartX { get; set; } 19 | 20 | /// 21 | /// End X position. The playable region should be [-0.5, 1.5]. 22 | /// 23 | public float EndX { get; set; } 24 | 25 | /// 26 | /// Easing method. 27 | /// 28 | public ArcEasing Easing { get; set; } 29 | 30 | /// 31 | /// Start Y position. The playable region should be [0, 1]. 32 | /// 33 | public float StartY { get; set; } 34 | 35 | /// 36 | /// End Y position. The playable region should be [0, 1]. 37 | /// 38 | public float EndY { get; set; } 39 | 40 | /// 41 | /// Color of the arc. Only matters if is . 42 | /// 43 | public ArcColor Color { get; set; } 44 | 45 | /// 46 | /// Unknown field. The most common value is "none". 47 | /// 48 | public string Unknown1 { get; set; } 49 | 50 | /// 51 | /// Whether this note a trace arc (i.e. not playble, translucent). 52 | /// 53 | public bool IsTraceArc { get; set; } 54 | 55 | /// 56 | /// Whether this note a playable arc (i.e. must be tracked with your fingers). 57 | /// 58 | public bool IsPlayable => !IsTraceArc; 59 | 60 | /// 61 | /// The sky notes associated with this note. 62 | /// The array has a value (i.e. is not null) only when is . 63 | /// 64 | [CanBeNull] 65 | public SkyNote[] SkyNotes { get; set; } 66 | 67 | public override NoteType Type => NoteType.Arc; 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/Beatmap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Runtime.CompilerServices; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | using JetBrains.Annotations; 9 | 10 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 11 | /// 12 | /// Represents an Arcaea beatmap. 13 | /// 14 | public sealed class Beatmap { 15 | 16 | /// 17 | /// Audio offset, in milliseconds. 18 | /// 19 | public int AudioOffset { get; set; } 20 | 21 | /// 22 | /// Notes in the beatmap. 23 | /// 24 | [NotNull] 25 | public NoteBase[] Notes { get; set; } = new NoteBase[0]; 26 | 27 | public static bool TryParse([NotNull] StreamReader reader, [CanBeNull] out Beatmap beatmap) { 28 | try { 29 | beatmap = Parse(reader); 30 | return true; 31 | } catch (Exception) { 32 | beatmap = null; 33 | return false; 34 | } 35 | } 36 | 37 | public static bool TryParse([NotNull] string text, [CanBeNull] out Beatmap beatmap) { 38 | try { 39 | beatmap = Parse(text); 40 | return true; 41 | } catch (Exception) { 42 | beatmap = null; 43 | return false; 44 | } 45 | } 46 | 47 | [NotNull] 48 | public static Beatmap Parse([NotNull] string text) { 49 | var bytes = Encoding.UTF8.GetBytes(text); 50 | 51 | using (var memoryStream = new MemoryStream(bytes, false)) { 52 | using (var reader = new StreamReader(memoryStream, Encoding.UTF8)) { 53 | return Parse(reader); 54 | } 55 | } 56 | } 57 | 58 | [NotNull] 59 | public static Beatmap Parse([NotNull] StreamReader reader) { 60 | var line = reader.ReadLine(); 61 | var lineCounter = 1; 62 | 63 | Debug.Assert(line != null, nameof(line) + " != null"); 64 | Debug.Assert(line.StartsWith("AudioOffset:")); 65 | 66 | var audioOffset = int.Parse(line.Substring("AudioOffset:".Length)); 67 | 68 | reader.ReadLine(); // "-" 69 | ++lineCounter; 70 | 71 | var notes = new List(); 72 | 73 | while (!reader.EndOfStream) { 74 | line = reader.ReadLine(); 75 | ++lineCounter; 76 | 77 | if (string.IsNullOrWhiteSpace(line)) { 78 | continue; 79 | } 80 | 81 | if (line.StartsWith("hold")) { 82 | var match = HoldRegex.Match(line); 83 | 84 | if (!match.Success) { 85 | throw new FormatException($"Hold format error at line {lineCounter}."); 86 | } 87 | 88 | var content = match.Groups["content"].Value; 89 | var segs = content.Split(ContentSeparator); 90 | 91 | var note = new LongNote(); 92 | note.StartTick = Convert.ToInt32(segs[0]); 93 | note.EndTick = Convert.ToInt32(segs[1]); 94 | note.Track = (Track)Convert.ToInt32(segs[2]); 95 | 96 | notes.Add(note); 97 | } else if (line.StartsWith("arc")) { 98 | var match = ArcRegex.Match(line); 99 | 100 | if (!match.Success) { 101 | throw new FormatException($"Arc format error at line {lineCounter}."); 102 | } 103 | 104 | var extra = match.Groups["extra"].Value; 105 | SkyNote[] arcTapNotes; 106 | 107 | if (!string.IsNullOrWhiteSpace(extra)) { 108 | var subNoteStrings = extra.Split(ContentSeparator); 109 | var arcTapNoteList = new List(); 110 | 111 | foreach (var subNoteString in subNoteStrings) { 112 | var subMatch = ArcTapRegex.Match(subNoteString); 113 | 114 | if (!subMatch.Success) { 115 | throw new FormatException($"ArcTap format error at line {lineCounter}."); 116 | } 117 | 118 | var arcTapNote = new SkyNote(); 119 | arcTapNote.Tick = Convert.ToInt32(subMatch.Groups["content"].Value); 120 | 121 | arcTapNoteList.Add(arcTapNote); 122 | } 123 | 124 | arcTapNotes = arcTapNoteList.ToArray(); 125 | } else { 126 | arcTapNotes = null; 127 | } 128 | 129 | var content = match.Groups["content"].Value; 130 | var segs = content.Split(ContentSeparator); 131 | 132 | var note = new ArcNote(); 133 | note.StartTick = Convert.ToInt32(segs[0]); 134 | note.EndTick = Convert.ToInt32(segs[1]); 135 | note.StartX = Convert.ToSingle(segs[2]); 136 | note.EndX = Convert.ToSingle(segs[3]); 137 | note.Easing = ParseEasing(segs[4]); 138 | note.StartY = Convert.ToSingle(segs[5]); 139 | note.EndY = Convert.ToSingle(segs[6]); 140 | note.Color = (ArcColor)Convert.ToInt32(segs[7]); 141 | note.Unknown1 = segs[8]; 142 | note.IsTraceArc = Convert.ToBoolean(segs[9]); 143 | note.SkyNotes = arcTapNotes; 144 | 145 | notes.Add(note); 146 | } else if (line.StartsWith("timing")) { 147 | var match = TimingRegex.Match(line); 148 | 149 | if (!match.Success) { 150 | throw new FormatException($"Timing format error at line {lineCounter}."); 151 | } 152 | 153 | var content = match.Groups["content"].Value; 154 | var segs = content.Split(ContentSeparator); 155 | 156 | var note = new TimingNote(); 157 | note.Tick = Convert.ToInt32(segs[0]); 158 | note.Bpm = Convert.ToSingle(segs[1]); 159 | note.BeatsPerMeasure = Convert.ToSingle(segs[2]); 160 | 161 | notes.Add(note); 162 | } else { 163 | var match = TapRegex.Match(line); 164 | 165 | if (!match.Success) { 166 | throw new FormatException($"Tap format error at line {lineCounter}."); 167 | } 168 | 169 | var content = match.Groups["content"].Value; 170 | var segs = content.Split(ContentSeparator); 171 | 172 | var note = new FloorNote(); 173 | note.Tick = Convert.ToInt32(segs[0]); 174 | note.Track = (Track)Convert.ToInt32(segs[1]); 175 | 176 | notes.Add(note); 177 | } 178 | } 179 | 180 | notes.Sort(NoteBase.Compare); 181 | 182 | var beatmap = new Beatmap(); 183 | 184 | beatmap.AudioOffset = audioOffset; 185 | beatmap.Notes = notes.ToArray(); 186 | 187 | return beatmap; 188 | } 189 | 190 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 191 | private static ArcEasing ParseEasing([NotNull] string str) { 192 | if (str == null) { 193 | throw new ArgumentNullException(nameof(str)); 194 | } 195 | 196 | var s = str.Trim().ToLowerInvariant(); 197 | 198 | switch (s) { 199 | case "s": 200 | return ArcEasing.S; 201 | case "b": 202 | return ArcEasing.CubicBezier; 203 | case "si": 204 | return ArcEasing.Si; 205 | case "so": 206 | return ArcEasing.So; 207 | case "siso": 208 | return ArcEasing.SiSo; 209 | case "sosi": 210 | return ArcEasing.SoSi; 211 | case "sisi": 212 | return ArcEasing.SiSi; 213 | case "soso": 214 | return ArcEasing.SoSo; 215 | default: 216 | throw new ArgumentOutOfRangeException(nameof(str), str, $"Unknown easing type \"{str}\"."); 217 | } 218 | } 219 | 220 | private static readonly Regex TapRegex = new Regex(@"^\((?[^)]+)\);$"); 221 | private static readonly Regex HoldRegex = new Regex(@"^hold\((?[^)]+)\);$"); 222 | private static readonly Regex ArcRegex = new Regex(@"^arc\((?[^)]+)\)(\[(?[^\]]*)\])?;$"); 223 | private static readonly Regex ArcTapRegex = new Regex(@"^arctap\((?\d+)\)$"); 224 | private static readonly Regex TimingRegex = new Regex(@"^timing\((?[^)]+)\);$"); 225 | 226 | private static readonly char[] ContentSeparator = { ',' }; 227 | 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/FloorNote.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// 4 | /// 5 | /// Represents a floor note. 6 | /// 7 | public sealed class FloorNote : NoteBase, IHasTick { 8 | 9 | public int Tick { get; set; } 10 | 11 | /// 12 | /// The track that this note is on. 13 | /// 14 | public Track Track { get; set; } 15 | 16 | public override NoteType Type => NoteType.Floor; 17 | 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/LongNote.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// 4 | /// 5 | /// Represents a long note. 6 | /// 7 | public sealed class LongNote : NoteBase, IHasTicks { 8 | 9 | public int StartTick { get; set; } 10 | 11 | public int EndTick { get; set; } 12 | 13 | /// 14 | /// The track that this note is on. 15 | /// 16 | public Track Track { get; set; } 17 | 18 | public override NoteType Type => NoteType.Long; 19 | 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/NoteBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JetBrains.Annotations; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 5 | /// 6 | /// Arcaea note base. 7 | /// 8 | public abstract class NoteBase { 9 | 10 | /// 11 | /// Type of the note. 12 | /// 13 | public abstract NoteType Type { get; } 14 | 15 | /// 16 | /// Common note comparison function. It compares the notes' tick (or start tick). 17 | /// 18 | /// The first note to be compared. 19 | /// The second note to be compared. 20 | /// Comparison result. 1, 0, -1 mean greater than, equal, and less than respectively. 21 | public static int Compare([CanBeNull] NoteBase note1, [CanBeNull] NoteBase note2) { 22 | if (note1 == null) { 23 | if (note2 == null) { 24 | return 0; 25 | } else { 26 | return -1; 27 | } 28 | } else { 29 | if (note2 == null) { 30 | return 1; 31 | } else { 32 | int tick1, tick2; 33 | 34 | if (note1 is IHasTick t1) { 35 | tick1 = t1.Tick; 36 | } else if (note1 is IHasTicks ts1) { 37 | tick1 = ts1.StartTick; 38 | } else { 39 | throw new NotSupportedException(); 40 | } 41 | 42 | if (note2 is IHasTick t2) { 43 | tick2 = t2.Tick; 44 | } else if (note2 is IHasTicks ts2) { 45 | tick2 = ts2.StartTick; 46 | } else { 47 | throw new NotSupportedException(); 48 | } 49 | 50 | return tick1.CompareTo(tick2); 51 | } 52 | } 53 | } 54 | 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/NoteType.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// Note types. 4 | /// 5 | public enum NoteType { 6 | 7 | /// 8 | /// A floor note. It appears at the bottom of the game area. It should be tapped. 9 | /// 10 | Floor = 0, 11 | /// 12 | /// A long note. It appears at the bottom of the game area. It should be pressed on start tick and released on end tick. 13 | /// 14 | Long = 1, 15 | /// 16 | /// An arc note. The complicated and special note type of Arcaea. 17 | /// 18 | Arc = 2, 19 | /// 20 | /// A timing note. It changes the tempo (BPM) information. 21 | /// 22 | Timing = 3, 23 | /// 24 | /// A sky note. It does not appear in the beatmap. This value is only for convenience of development. 25 | /// 26 | Sky = 4, 27 | Max = 4 28 | 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/SkyNote.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// 4 | /// 5 | /// Represents a sky note, which is associated to an arc note. 6 | /// 7 | public sealed class SkyNote : NoteBase, IHasTick { 8 | 9 | public int Tick { get; set; } 10 | 11 | public override NoteType Type => NoteType.Sky; 12 | 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/TimingNote.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// 4 | /// 5 | /// Represents a timing note. 6 | /// 7 | public sealed class TimingNote : NoteBase, IHasTick { 8 | 9 | public int Tick { get; set; } 10 | 11 | /// 12 | /// New tempo value. 13 | /// 14 | public float Bpm { get; set; } 15 | 16 | /// 17 | /// Beats per measure. 18 | /// 19 | public float BeatsPerMeasure { get; set; } 20 | 21 | public override NoteType Type => NoteType.Timing; 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Entities/Track.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities { 2 | /// 3 | /// Arcaea track. 4 | /// 5 | public enum Track { 6 | 7 | /// 8 | /// Not an option. 9 | /// 10 | Invalid = 0, 11 | /// 12 | /// The left track. 13 | /// 14 | Track1 = 1, 15 | /// 16 | /// The center-left track. 17 | /// 18 | Track2 = 2, 19 | /// 20 | /// The center-right track. 21 | /// 22 | Track3 = 3, 23 | /// 24 | /// The right track. 25 | /// 26 | Track4 = 4 27 | 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/IHasTick.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores { 2 | /// 3 | /// A note with only one tick. 4 | /// 5 | public interface IHasTick { 6 | 7 | /// 8 | /// Tick of this note, in milliseconds. 9 | /// 10 | int Tick { get; } 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/IHasTicks.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores { 2 | /// 3 | /// A note with a start tick and an end tick. 4 | /// 5 | public interface IHasTicks { 6 | 7 | /// 8 | /// Start tick, in milliseconds. 9 | /// 10 | int StartTick { get; } 11 | 12 | /// 13 | /// End tick, in milliseconds. 14 | /// 15 | int EndTick { get; } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/IPreviewNote.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores { 2 | /// 3 | /// A preview note with its Y position precalculated. 4 | /// 5 | public interface IPreviewNote { 6 | 7 | /// 8 | /// Precalculated Y position. 9 | /// 10 | float PreviewY { get; } 11 | 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/IRangedPreviewNote.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores { 2 | /// 3 | /// A preview note with its Y positions at start and the end are precalculated. 4 | /// 5 | public interface IRangedPreviewNote { 6 | 7 | /// 8 | /// Precalculated starting Y position. 9 | /// 10 | float PreviewStartY { get; } 11 | 12 | /// 13 | /// Precalculated ending Y position. 14 | /// 15 | float PreviewEndY { get; } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/ArcEasingHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | using Microsoft.Xna.Framework; 4 | using Moe.Mottomo.ArcaeaSim.Core; 5 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 6 | 7 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 8 | /// 9 | /// Arc note easing helper functions. 10 | /// 11 | public static class ArcEasingHelper { 12 | 13 | /// 14 | /// Calculates the eased value of a pair of coordinates. 15 | /// 16 | /// Start position. 17 | /// End position. 18 | /// A value increasing from 0 to 1 while moving from to . 19 | /// Easing method. 20 | /// Eased coordinate. 21 | public static Vector3 Ease(Vector3 start, Vector3 end, float t, ArcEasing easing) { 22 | t = MathHelper.Clamp(t, 0, 1); 23 | 24 | switch (easing) { 25 | case ArcEasing.S: 26 | return EaseLinear(start, end, t); 27 | case ArcEasing.CubicBezier: 28 | return EaseCubicBezier(start, end, t); 29 | case ArcEasing.Si: 30 | case ArcEasing.So: 31 | case ArcEasing.SiSo: 32 | case ArcEasing.SoSi: 33 | case ArcEasing.SoSo: 34 | case ArcEasing.SiSi: 35 | return EaseSinus(start, end, t, easing); 36 | default: 37 | throw new ArgumentOutOfRangeException(nameof(easing), easing, null); 38 | } 39 | } 40 | 41 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 42 | private static Vector3 EaseLinear(Vector3 p1, Vector3 p2, float t) { 43 | return Vector3.Lerp(p1, p2, t); 44 | } 45 | 46 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 47 | private static Vector3 EaseCubicBezier(Vector3 p1, Vector3 p2, float t) { 48 | var mt = 1 - t; 49 | var mt2 = mt * mt; 50 | var mt3 = mt2 * mt; 51 | var t2 = t * t; 52 | var t3 = t2 * t; 53 | 54 | var x = mt3 * p1.X + 3 * mt2 * t * p1.X + 3 * mt * t2 * p2.X + t3 * p2.X; 55 | var y = MathHelper.Lerp(p1.Y, p2.Y, t); 56 | var z = mt3 * p1.Z + 3 * mt2 * t * p1.Z + 3 * mt * t2 * p2.Z + t3 * p2.Z; 57 | 58 | return new Vector3(x, y, z); 59 | } 60 | 61 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 62 | private static Vector3 EaseSinus(Vector3 p1, Vector3 p2, float t, ArcEasing easing) { 63 | var y = MathHelper.Lerp(p1.Y, p2.Y, t); 64 | 65 | float sx, sz; 66 | 67 | switch (easing) { 68 | case ArcEasing.Si: 69 | case ArcEasing.SiSi: 70 | case ArcEasing.SiSo: 71 | sx = MathF.Sin(t * MathHelper.PiOver2); 72 | break; 73 | case ArcEasing.So: 74 | case ArcEasing.SoSi: 75 | case ArcEasing.SoSo: 76 | sx = 1 - MathF.Cos(t * MathHelper.PiOver2); 77 | break; 78 | default: 79 | throw new ArgumentOutOfRangeException(nameof(easing), easing, null); 80 | } 81 | 82 | switch (easing) { 83 | case ArcEasing.Si: 84 | case ArcEasing.So: 85 | // Credit: @18111398 86 | sz = t; 87 | break; 88 | case ArcEasing.SoSi: 89 | case ArcEasing.SiSi: 90 | sz = MathF.Sin(t * MathHelper.PiOver2); 91 | break; 92 | case ArcEasing.SiSo: 93 | case ArcEasing.SoSo: 94 | sz = 1 - MathF.Cos(t * MathHelper.PiOver2); 95 | break; 96 | default: 97 | throw new ArgumentOutOfRangeException(nameof(easing), easing, null); 98 | } 99 | 100 | var dx = p2.X - p1.X; 101 | var x = p1.X + dx * sx; 102 | 103 | var dz = p2.Z - p1.Z; 104 | var z = p1.Z + dz * sz; 105 | 106 | return new Vector3(x, y, z); 107 | } 108 | 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/FloorVisualNote.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Moe.Mottomo.ArcaeaSim.Subsystems.Rendering; 5 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 6 | 7 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 8 | /// 9 | /// 10 | /// 11 | /// Represents a floor visual note. 12 | /// 13 | public sealed class FloorVisualNote : VisualNoteBase, IPreviewNote { 14 | 15 | public FloorVisualNote([NotNull] VisualBeatmap beatmap, [NotNull] FloorNote baseNote, [NotNull] StageMetrics metrics) 16 | : base(beatmap, baseNote) { 17 | _baseNote = baseNote; 18 | _metrics = metrics; 19 | _noteRectangle = new TexturedRectangle(beatmap.GraphicsDevice); 20 | _linkHexahedron = new ColoredHexahedron(beatmap.GraphicsDevice); 21 | 22 | PreviewY = beatmap.CalculateY(baseNote.Tick, metrics, metrics.FinishLineY); 23 | } 24 | 25 | public override void Draw(int beatmapTicks, float currentY) { 26 | var metrics = _metrics; 27 | var left = ((int)_baseNote.Track - 0.5f) * metrics.TrackInnerWidth / 4 - metrics.FloorNoteWidth / 2 - metrics.HalfTrackInnerWidth; 28 | var bottom = PreviewY - currentY; 29 | 30 | var bottomLeft = new Vector2(left, bottom); 31 | var size = new Vector2(metrics.FloorNoteWidth, metrics.FloorNoteHeight); 32 | 33 | _noteRectangle.SetVerticesXY(bottomLeft, size, Color.White, Z); 34 | 35 | var effect = (BasicEffect)NoteEffects.Effects[(int)_baseNote.Type]; 36 | 37 | effect.Alpha = 0.75f; 38 | 39 | effect.TextureEnabled = true; 40 | effect.VertexColorEnabled = true; 41 | 42 | effect.Texture = _texture; 43 | 44 | _noteRectangle.Draw(effect.CurrentTechnique); 45 | 46 | var synced = SynchronizedSkyVisualNote; 47 | 48 | if (synced != null) { 49 | // Draw the sync line. 50 | 51 | var n = (ArcNote)synced.Parent.BaseNote; 52 | 53 | // TODO: This calculation can be executed when loading the beatmap, and store in some fields like "PreviewX". 54 | 55 | // var skyPoint = new Vector3(skyMiddle, skyBottom, skyEdge); 56 | // Optimization for sky point calculation needed 57 | 58 | var ratio = (float)(((SkyNote)synced.BaseNote).Tick - n.StartTick) / (n.EndTick - n.StartTick); 59 | 60 | var startX = (n.StartX - -0.5f) * metrics.TrackInnerWidth / (1.5f - -0.5f) - metrics.HalfTrackInnerWidth; 61 | var startZ = metrics.SkyInputZ * n.StartY + metrics.ArcHeightLowerBorder * (1 - n.StartY); 62 | 63 | var endX = (n.EndX - -0.5f) * metrics.TrackInnerWidth / (1.5f - -0.5f) - metrics.HalfTrackInnerWidth; 64 | var endZ = metrics.SkyInputZ * n.EndY + metrics.ArcHeightLowerBorder * (1 - n.EndY); 65 | 66 | var start = new Vector3(startX, 1, startZ); 67 | var end = new Vector3(endX, 1, endZ); 68 | 69 | var skyPoint = ArcEasingHelper.Ease(start, end, ratio, n.Easing); 70 | skyPoint.Y = synced.PreviewY - currentY; 71 | 72 | var thisPoint = new Vector3(left + metrics.FloorNoteWidth / 2, bottom, Z); 73 | 74 | _linkHexahedron.SetVerticesXZ(thisPoint, skyPoint, Color.PaleVioletRed, LinkSectionSize); 75 | 76 | effect.Alpha = 0.2f; 77 | 78 | effect.TextureEnabled = false; 79 | effect.VertexColorEnabled = true; 80 | 81 | _linkHexahedron.Draw(effect.CurrentTechnique); 82 | } 83 | } 84 | 85 | public override bool IsVisible(int beatmapTicks, float currentY) { 86 | if (_baseNote.Tick < beatmapTicks - _metrics.PastTickThreshold || beatmapTicks + _metrics.FutureTickThreshold < _baseNote.Tick) { 87 | return false; 88 | } 89 | 90 | return !(PreviewY < currentY || currentY + _metrics.TrackLength < PreviewY); 91 | } 92 | 93 | public void SetTexture([NotNull] Texture2D texture) { 94 | _texture = texture; 95 | } 96 | 97 | public float PreviewY { get; } 98 | 99 | [CanBeNull] 100 | public SkyVisualNote SynchronizedSkyVisualNote { get; internal set; } 101 | 102 | protected override void Dispose(bool disposing) { 103 | _noteRectangle?.Dispose(); 104 | _noteRectangle = null; 105 | _linkHexahedron?.Dispose(); 106 | _linkHexahedron = null; 107 | } 108 | 109 | private const float Z = 0.04f; 110 | private static readonly Vector2 LinkSectionSize = new Vector2(0.25f, 0.25f); 111 | 112 | private Texture2D _texture; 113 | 114 | private readonly FloorNote _baseNote; 115 | private readonly StageMetrics _metrics; 116 | private TexturedRectangle _noteRectangle; 117 | private ColoredHexahedron _linkHexahedron; 118 | 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/IDrawableNote.cs: -------------------------------------------------------------------------------- 1 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 2 | /// 3 | /// A drawable note protocol. 4 | /// 5 | public interface IDrawableNote { 6 | 7 | /// 8 | /// Draws this note. 9 | /// 10 | /// Current beatmap time, in milliseconds. Please note that beatmap time does not equal to audio time when a beatmap's audio offset is non-zero. 11 | /// Current Y position of the ongoing beatmap. 12 | void Draw(int beatmapTicks, float currentY); 13 | 14 | /// 15 | /// Determines whether this note is visible or not. 16 | /// 17 | /// Current beatmap time, in milliseconds. Please note that beatmap time does not equal to audio time when a beatmap's audio offset is non-zero. 18 | /// Current Y position of the ongoing beatmap. 19 | /// if the note is visible, otherwise . 20 | bool IsVisible(int beatmapTicks, float currentY); 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/LongVisualNote.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Moe.Mottomo.ArcaeaSim.Subsystems.Rendering; 5 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 6 | 7 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 8 | /// 9 | /// 10 | /// 11 | /// Represents a long visual note. 12 | /// 13 | public sealed class LongVisualNote : VisualNoteBase, IRangedPreviewNote { 14 | 15 | public LongVisualNote([NotNull] VisualBeatmap beatmap, [NotNull] LongNote baseNote, [NotNull] StageMetrics metrics) 16 | : base(beatmap, baseNote) { 17 | _baseNote = baseNote; 18 | _metrics = metrics; 19 | _noteRectangle = new TexturedRectangle(beatmap.GraphicsDevice); 20 | 21 | PreviewStartY = beatmap.CalculateY(baseNote.StartTick, metrics, metrics.FinishLineY); 22 | PreviewEndY = beatmap.CalculateY(baseNote.EndTick, metrics, metrics.FinishLineY); 23 | } 24 | 25 | public override void Draw(int beatmapTicks, float currentY) { 26 | var left = ((int)_baseNote.Track - 0.5f) * _metrics.TrackInnerWidth / 4 - _metrics.FloorNoteWidth / 2 - _metrics.HalfTrackInnerWidth; 27 | var bottom = PreviewStartY - currentY; 28 | 29 | if (bottom < _metrics.FinishLineY) { 30 | bottom = _metrics.FinishLineY; 31 | } 32 | 33 | var top = PreviewEndY - currentY; 34 | 35 | if (top > _metrics.TrackLength) { 36 | top = _metrics.TrackLength; 37 | } 38 | 39 | var bottomLeft = new Vector2(left, bottom); 40 | var size = new Vector2(_metrics.FloorNoteWidth, top - bottom); 41 | 42 | _noteRectangle.SetVerticesXY(bottomLeft, size, Color.White, Z); 43 | 44 | var effect = (BasicEffect)NoteEffects.Effects[(int)_baseNote.Type]; 45 | 46 | effect.Alpha = 0.75f; 47 | 48 | effect.TextureEnabled = true; 49 | effect.VertexColorEnabled = true; 50 | 51 | // Highlighted before the time reaches this note's start tick, and dim this note after that. 52 | // Correction credit: @18111398 53 | var isHighlighted = beatmapTicks >= _baseNote.StartTick; 54 | var texture = isHighlighted ? _hightlightedTexture : _texture; 55 | 56 | effect.Texture = texture; 57 | 58 | _noteRectangle.Draw(effect.CurrentTechnique); 59 | } 60 | 61 | public override bool IsVisible(int beatmapTicks, float currentY) { 62 | if (_baseNote.EndTick < beatmapTicks - _metrics.PastTickThreshold || beatmapTicks + _metrics.FutureTickThreshold < _baseNote.StartTick) { 63 | return false; 64 | } 65 | 66 | return !(PreviewEndY < currentY || currentY + _metrics.TrackLength < PreviewStartY); 67 | } 68 | 69 | public void SetTextures([NotNull] Texture2D texture, [NotNull] Texture2D highlightedTexture) { 70 | _texture = texture; 71 | _hightlightedTexture = highlightedTexture; 72 | } 73 | 74 | public float PreviewStartY { get; } 75 | 76 | public float PreviewEndY { get; } 77 | 78 | protected override void Dispose(bool disposing) { 79 | _noteRectangle?.Dispose(); 80 | _noteRectangle = null; 81 | } 82 | 83 | private const float Z = 0.04f; 84 | 85 | private Texture2D _texture; 86 | private Texture2D _hightlightedTexture; 87 | 88 | private readonly LongNote _baseNote; 89 | private readonly StageMetrics _metrics; 90 | private TexturedRectangle _noteRectangle; 91 | 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/NoteEffects.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Xna.Framework.Graphics; 2 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 5 | internal static class NoteEffects { 6 | 7 | internal static readonly Effect[] Effects = new Effect[(int)NoteType.Max + 1]; 8 | 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/SkyVisualNote.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework; 3 | using Microsoft.Xna.Framework.Graphics; 4 | using Moe.Mottomo.ArcaeaSim.Extensions; 5 | using Moe.Mottomo.ArcaeaSim.Subsystems.Rendering; 6 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 7 | 8 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 9 | /// 10 | /// 11 | /// 12 | /// Represents a sky visual note. 13 | /// 14 | public sealed class SkyVisualNote : VisualNoteBase, IPreviewNote { 15 | 16 | public SkyVisualNote([NotNull] VisualBeatmap beatmap, [NotNull] SkyNote baseNote, [NotNull] ArcVisualNote parent, [NotNull] StageMetrics metrics) 17 | : base(beatmap, baseNote) { 18 | _metrics = metrics; 19 | 20 | Parent = parent; 21 | 22 | PreviewY = beatmap.CalculateY(baseNote.Tick, metrics, metrics.FinishLineY); 23 | 24 | var graphicsDevice = beatmap.GraphicsDevice; 25 | _graphicsDevice = graphicsDevice; 26 | 27 | _vertexBuffer1 = new VertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, 16, BufferUsage.WriteOnly); 28 | _indexBuffer1 = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 24, BufferUsage.WriteOnly); 29 | 30 | _vertexBuffer2 = new VertexBuffer(graphicsDevice, VertexPositionColorTexture.VertexDeclaration, 8, BufferUsage.WriteOnly); 31 | _indexBuffer2 = new IndexBuffer(graphicsDevice, IndexElementSize.SixteenBits, 12, BufferUsage.WriteOnly); 32 | 33 | var indices1 = new ushort[] { 34 | // bottom, top 35 | 0, 1, 2, 36 | 2, 1, 3, 37 | 4, 5, 6, 38 | 6, 5, 7, 39 | 40 | // front, back 41 | 8, 9, 10, 42 | 10, 9, 11, 43 | 12, 13, 14, 44 | 14, 13, 15 45 | }; 46 | 47 | _indexBuffer1.SetData(indices1); 48 | 49 | var indices2 = new ushort[] { 50 | 0, 1, 2, 51 | 2, 1, 3, 52 | 4, 5, 6, 53 | 6, 5, 7, 54 | }; 55 | 56 | _indexBuffer2.SetData(indices2); 57 | 58 | _shadowRectangle = new ColoredRectangle(graphicsDevice); 59 | } 60 | 61 | public ArcVisualNote Parent { get; } 62 | 63 | public float PreviewY { get; } 64 | 65 | public override bool IsVisible(int beatmapTicks, float currentY) { 66 | var baseNote = (SkyNote)BaseNote; 67 | 68 | if (baseNote.Tick < beatmapTicks - _metrics.PastTickThreshold || beatmapTicks + _metrics.FutureTickThreshold < baseNote.Tick) { 69 | return false; 70 | } 71 | 72 | return !(PreviewY < currentY || currentY + _metrics.TrackLength < PreviewY); 73 | } 74 | 75 | public override void Draw(int beatmapTicks, float currentY) { 76 | var effect = (BasicEffect)NoteEffects.Effects[(int)BaseNote.Type]; 77 | 78 | effect.TextureEnabled = true; 79 | effect.VertexColorEnabled = true; 80 | effect.Alpha = 1; 81 | 82 | effect.Texture = _texture; 83 | 84 | _graphicsDevice.SetVertexBuffer(_vertexBuffer1); 85 | _graphicsDevice.Indices = _indexBuffer1; 86 | 87 | foreach (var pass in effect.CurrentTechnique.Passes) { 88 | pass.Apply(); 89 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 8); 90 | } 91 | 92 | effect.Texture = _texture; 93 | 94 | _graphicsDevice.SetVertexBuffer(_vertexBuffer2); 95 | _graphicsDevice.Indices = _indexBuffer2; 96 | 97 | foreach (var pass in effect.CurrentTechnique.Passes) { 98 | pass.Apply(); 99 | _graphicsDevice.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, 4); 100 | } 101 | 102 | // Cast shadow 103 | effect.TextureEnabled = false; 104 | effect.VertexColorEnabled = true; 105 | effect.Alpha = 0.75f; 106 | 107 | _shadowRectangle.Draw(effect.CurrentTechnique); 108 | } 109 | 110 | public void SetVertices(Vector3 bottomNearLeft, Vector3 size) { 111 | // Tint color 112 | var color = Color.White; 113 | 114 | var vertices1 = new[] { 115 | // bottom 116 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(0, 1)}, 117 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(1, 1)}, 118 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(0, 0)}, 119 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(1, 0)}, 120 | 121 | // up 122 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(0, 1)}, 123 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(1, 1)}, 124 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(0, 0)}, 125 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(1, 0)}, 126 | 127 | // front 128 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(0, 1)}, 129 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(1, 1)}, 130 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(0, 0)}, 131 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(1, 0)}, 132 | 133 | // back 134 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(0, 1)}, 135 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(1, 1)}, 136 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(0, 0)}, 137 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(1, 0)} 138 | }; 139 | 140 | _vertexBuffer1.SetData(vertices1); 141 | 142 | var vertices2 = new[] { 143 | // left 144 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(0, 1)}, 145 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(1, 1)}, 146 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(0, 0)}, 147 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(1, 0)}, 148 | 149 | // right 150 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(0, 1)}, 151 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z), Color = color, TextureCoordinate = new Vector2(1, 1)}, 152 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(0, 0)}, 153 | new VertexPositionColorTexture {Position = new Vector3(bottomNearLeft.X + size.X, bottomNearLeft.Y + size.Y, bottomNearLeft.Z + size.Z), Color = color, TextureCoordinate = new Vector2(1, 0)} 154 | }; 155 | 156 | _vertexBuffer2.SetData(vertices2); 157 | 158 | _shadowRectangle.SetVerticesXY(bottomNearLeft.XY(), size.XY(), Color.Gray, ShadowZ); 159 | } 160 | 161 | public void SetTexture([NotNull] Texture2D texture) { 162 | _texture = texture; 163 | } 164 | 165 | protected override void Dispose(bool disposing) { 166 | _vertexBuffer1?.Dispose(); 167 | _indexBuffer1?.Dispose(); 168 | _vertexBuffer2?.Dispose(); 169 | _indexBuffer2?.Dispose(); 170 | _vertexBuffer1 = null; 171 | _indexBuffer1 = null; 172 | _vertexBuffer2 = null; 173 | _indexBuffer2 = null; 174 | 175 | _shadowRectangle?.Dispose(); 176 | _shadowRectangle = null; 177 | } 178 | 179 | private const float ShadowZ = 0.03f; 180 | 181 | private Texture2D _texture; 182 | 183 | private readonly StageMetrics _metrics; 184 | 185 | private readonly GraphicsDevice _graphicsDevice; 186 | private VertexBuffer _vertexBuffer1; 187 | private IndexBuffer _indexBuffer1; 188 | private VertexBuffer _vertexBuffer2; 189 | private IndexBuffer _indexBuffer2; 190 | 191 | private ColoredRectangle _shadowRectangle; 192 | 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/StageMetrics.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | using JetBrains.Annotations; 3 | 4 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 5 | /// 6 | /// Stage metrics that controls the general track layout. 7 | /// 8 | /// 9 | /// This type is class rather than structure is because the same instance is frequently passed between objects. 10 | /// So one reference on the heap is enough, and can avoid extra stack allocation overhead. 11 | /// 12 | [NotNull] 13 | public sealed class StageMetrics { 14 | 15 | /// 16 | /// Creates a new instance. 17 | /// 18 | public StageMetrics() { 19 | TrackLength = 100; 20 | TrackInnerWidth = 20; 21 | 22 | LaneDividerWidth = 0.2f; 23 | 24 | FloorNoteWidth = TrackInnerWidth / 48 * 11; 25 | FloorNoteHeight = TrackInnerWidth / 8; 26 | 27 | SkyNoteWidth = TrackInnerWidth / 4; 28 | SkyNoteHeight = SkyNoteWidth / 4; 29 | SkyNoteTallness = SkyNoteHeight / 2; 30 | 31 | FinishLineY = 10; 32 | FinishLineHeight = 2f; 33 | 34 | SkyInputZ = TrackInnerWidth / 3.236f; 35 | 36 | SkyInputWidth = TrackInnerWidth * 1.25f; 37 | SkyInputTallness = 0.8f; 38 | 39 | PlayableArcWidth = 2f; 40 | PlayableArcTallness = 2f; 41 | 42 | TraceArcWidth = PlayableArcWidth / 6; 43 | TraceArcTallness = PlayableArcTallness / 6; 44 | 45 | ArcHeightLowerBorder = 0.75f; 46 | 47 | 48 | PastTickThreshold = 0; 49 | FutureTickThreshold = 4000; 50 | 51 | Speed = 5; 52 | } 53 | 54 | /// 55 | /// Length of the track. 56 | /// 57 | public float TrackLength { get; set; } 58 | 59 | /// 60 | /// Full width of the track. 61 | /// 62 | public float TrackFullWidth { 63 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 64 | get => TrackInnerWidth / (1 - (72f / 1024)); // this ratio can be measured from track texture 65 | } 66 | 67 | /// 68 | /// Half of the full width of the track. 69 | /// 70 | public float HalfTrackFullWidth { 71 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 72 | get => TrackFullWidth / 2; 73 | } 74 | 75 | /// 76 | /// Inner width of the track (excluding the borders). 77 | /// 78 | public float TrackInnerWidth { get; set; } 79 | 80 | /// 81 | /// Half of the inner width of the track. 82 | /// 83 | public float HalfTrackInnerWidth { 84 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 85 | get => TrackInnerWidth / 2; 86 | } 87 | 88 | /// 89 | /// Width of a lane divider. 90 | /// 91 | public float LaneDividerWidth { get; set; } 92 | 93 | /// 94 | /// Width of a floor note. 95 | /// 96 | public float FloorNoteWidth { get; set; } 97 | 98 | /// 99 | /// Height of a floor note. 100 | /// 101 | public float FloorNoteHeight { get; set; } 102 | 103 | /// 104 | /// Width of a sky note. 105 | /// 106 | public float SkyNoteWidth { get; set; } 107 | 108 | /// 109 | /// Height of a sky note. 110 | /// 111 | public float SkyNoteHeight { get; set; } 112 | 113 | /// 114 | /// Tallness of a sky note. 115 | /// 116 | public float SkyNoteTallness { get; set; } 117 | 118 | /// 119 | /// Y position of the finish line. 120 | /// 121 | public float FinishLineY { get; set; } 122 | 123 | /// 124 | /// Height of the finish line. 125 | /// 126 | public float FinishLineHeight { get; set; } 127 | 128 | /// 129 | /// Z position of the sky input. 130 | /// 131 | public float SkyInputZ { get; set; } 132 | 133 | /// 134 | /// Width of the sky input. 135 | /// 136 | public float SkyInputWidth { get; set; } 137 | 138 | /// 139 | /// Tallness of the sky input. 140 | /// 141 | public float SkyInputTallness { get; set; } 142 | 143 | /// 144 | /// Playable arc section width. 145 | /// 146 | public float PlayableArcWidth { get; set; } 147 | 148 | /// 149 | /// Playable arc section tallness. 150 | /// 151 | public float PlayableArcTallness { get; set; } 152 | 153 | /// 154 | /// Trace arc section width. 155 | /// 156 | public float TraceArcWidth { get; set; } 157 | 158 | /// 159 | /// Trace arc tallness. 160 | /// 161 | public float TraceArcTallness { get; set; } 162 | 163 | /// 164 | /// The height of an arc when its z == 0. 165 | /// 166 | public float ArcHeightLowerBorder { get; set; } 167 | 168 | /// 169 | /// Past tick threshold, in milliseconds. Notes whose tick or ending tick is before beatmapTick + PastTickThreshold will not be drawn. 170 | /// 171 | public int PastTickThreshold { get; set; } 172 | 173 | /// 174 | /// Future tick threshold, in milliseconds. Notes whose tick or starting tick is after beatmapTick + FutureTickThreshold will not be drawn. 175 | /// 176 | /// 177 | /// Calculating this value is challenging. 178 | /// A value too large will significantly decrease the rendering efficiency. 179 | /// A value too small will cause notes after a sudden tempo change disappear. 180 | /// This value is related to . When increases, the required value of increases. 181 | /// 182 | public int FutureTickThreshold { get; set; } 183 | 184 | /// 185 | /// Note falling speed. Corresponding to the setting in Arcaea. 186 | /// 187 | public float Speed { get; set; } 188 | 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/VisualBeatmap.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using JetBrains.Annotations; 4 | using Microsoft.Xna.Framework.Graphics; 5 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 6 | using OpenMLTD.MilliSim.Core; 7 | 8 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 9 | /// 10 | /// 11 | /// A visual beatmap. 12 | /// 13 | public sealed class VisualBeatmap : DisposableBase { 14 | 15 | /// 16 | /// Creates a new instance from a using specified stage metrics. 17 | /// 18 | /// A used to draw the beatmap's notes. 19 | /// 20 | /// Stage metrics. 21 | public VisualBeatmap([NotNull] GraphicsDevice graphicsDevice, [NotNull] Beatmap baseBeatmap, [NotNull] StageMetrics metrics) { 22 | GraphicsDevice = graphicsDevice; 23 | BaseBeatmap = baseBeatmap; 24 | 25 | Timings = baseBeatmap.Notes.Where(n => n.Type == NoteType.Timing).Cast().ToArray(); 26 | 27 | var visualNotes = baseBeatmap.Notes.Where(n => n.Type != NoteType.Timing).Select(n => { 28 | switch (n.Type) { 29 | case NoteType.Floor: 30 | return new FloorVisualNote(this, (FloorNote)n, metrics); 31 | case NoteType.Long: 32 | return new LongVisualNote(this, (LongNote)n, metrics); 33 | case NoteType.Arc: 34 | return new ArcVisualNote(this, (ArcNote)n, metrics); 35 | default: 36 | throw new ArgumentOutOfRangeException(); 37 | } 38 | }).ToArray(); 39 | 40 | VisualNotes = visualNotes; 41 | 42 | // Scan sync sky notes and add lines between the floor note and sky note. 43 | // Credits: @money, @RW, @JDF, @L-F0rce 44 | 45 | var allArcVisualNotes = visualNotes.Where(note => note.Type == NoteType.Arc).Cast().ToArray(); 46 | 47 | for (var i = 0; i < visualNotes.Length; ++i) { 48 | var visualNote = visualNotes[i]; 49 | 50 | if (visualNote.Type != NoteType.Floor || i >= visualNotes.Length - 1) { 51 | continue; 52 | } 53 | 54 | var n = (FloorVisualNote)visualNote; 55 | var floorBase = (FloorNote)n.BaseNote; 56 | 57 | // TODO: Naive method. Should be optimized. 58 | foreach (var arcVisualNote in allArcVisualNotes) { 59 | if (arcVisualNote.SkyVisualNotes == null || arcVisualNote.SkyVisualNotes.Length == 0) { 60 | continue; 61 | } 62 | 63 | foreach (var skyVisualNote in arcVisualNote.SkyVisualNotes) { 64 | if (Math.Abs(((SkyNote)skyVisualNote.BaseNote).Tick - floorBase.Tick) <= 1) { 65 | n.SynchronizedSkyVisualNote = skyVisualNote; 66 | break; 67 | } 68 | } 69 | 70 | if (n.SynchronizedSkyVisualNote != null) { 71 | break; 72 | } 73 | } 74 | } 75 | 76 | // "Supports" 77 | foreach (var arcVisualNote in allArcVisualNotes) { 78 | var n = (ArcNote)arcVisualNote.BaseNote; 79 | 80 | var ancestor = allArcVisualNotes.FirstOrDefault(note => { 81 | var anotherArcNote = (ArcNote)note.BaseNote; 82 | return anotherArcNote.EndTick == n.StartTick && anotherArcNote.IsPlayable && anotherArcNote.Color == n.Color; 83 | }); 84 | 85 | if (ancestor == null) { 86 | arcVisualNote.ShouldDrawSupport = true; 87 | arcVisualNote.ShouldDrawHeader = true; 88 | continue; 89 | } 90 | 91 | if (!((ArcNote)ancestor.BaseNote).StartY.Equals(((ArcNote)ancestor.BaseNote).EndY)) { 92 | arcVisualNote.ShouldDrawSupport = true; 93 | } 94 | } 95 | } 96 | 97 | /// 98 | /// Gets the underlying . 99 | /// 100 | [NotNull] 101 | public Beatmap BaseBeatmap { get; } 102 | 103 | /// 104 | /// Gets the used to draw this beatmap's notes. 105 | /// 106 | [NotNull] 107 | public GraphicsDevice GraphicsDevice { get; } 108 | 109 | /// 110 | /// Gets all visual notes in the beatmap. 111 | /// 112 | [NotNull, ItemNotNull] 113 | public VisualNoteBase[] VisualNotes { get; } 114 | 115 | /// 116 | /// Gets all timing notes in the beatmap. 117 | /// 118 | [NotNull, ItemNotNull] 119 | public TimingNote[] Timings { get; } 120 | 121 | /// 122 | /// Gets the first tempo value of this beatmap. 123 | /// 124 | public float FirstBpm { 125 | get { 126 | if (_firstBpm == null) { 127 | if (Timings.Length == 0) { 128 | throw new FormatException("No timing note found in this beatmap."); 129 | } 130 | 131 | _firstBpm = Timings[0].Bpm; 132 | } 133 | 134 | return _firstBpm.Value; 135 | } 136 | } 137 | 138 | /// 139 | /// Calculate a beatmap time's Y distance according to timing and metrics. 140 | /// 141 | /// Current beatmap time, in milliseconds. Please note that beatmap time does not equal to audio time when a beatmap's audio offset is non-zero. 142 | /// Stage metrics. 143 | /// The final addition value. This value is directly added to the calculated value. Example usage: assign this parameter the finishing line's Y distance. 144 | /// 145 | public float CalculateY(int beatmapTicks, [NotNull] StageMetrics metrics, float addition) { 146 | const float timeScale = 0.6f; 147 | 148 | var timingNotes = Timings; 149 | 150 | if (timingNotes.Length == 0) { 151 | return 0; 152 | } 153 | 154 | var measureLength = metrics.FloorNoteHeight * 6; 155 | 156 | var result = 0f; 157 | 158 | for (var i = 0; i < timingNotes.Length; ++i) { 159 | var timingNote = timingNotes[i]; 160 | 161 | if (beatmapTicks < timingNote.Tick) { 162 | break; 163 | } 164 | 165 | if (i >= timingNotes.Length - 1 || beatmapTicks < timingNotes[i + 1].Tick) { 166 | result += (beatmapTicks - timingNote.Tick) * timingNote.Bpm; 167 | } else { 168 | result += (timingNotes[i + 1].Tick - timingNote.Tick) * timingNote.Bpm; 169 | } 170 | } 171 | 172 | result /= 100_000f; 173 | 174 | result *= measureLength * timeScale * metrics.Speed; 175 | 176 | result += addition; 177 | 178 | return result; 179 | } 180 | 181 | protected override void Dispose(bool disposing) { 182 | if (VisualNotes.Length > 0) { 183 | foreach (var note in VisualNotes) { 184 | note.Dispose(); 185 | } 186 | } 187 | } 188 | 189 | private float? _firstBpm; 190 | 191 | } 192 | } 193 | -------------------------------------------------------------------------------- /ArcaeaView/Subsystems/Scores/Visualization/VisualNoteBase.cs: -------------------------------------------------------------------------------- 1 | using JetBrains.Annotations; 2 | using Microsoft.Xna.Framework.Graphics; 3 | using Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Entities; 4 | using OpenMLTD.MilliSim.Core; 5 | 6 | namespace Moe.Mottomo.ArcaeaSim.Subsystems.Scores.Visualization { 7 | /// 8 | /// 9 | /// 10 | /// Visual note base. 11 | /// 12 | public abstract class VisualNoteBase : DisposableBase, IDrawableNote { 13 | 14 | protected VisualNoteBase([NotNull] VisualBeatmap beatmap, [NotNull] NoteBase baseNote) { 15 | Beatmap = beatmap; 16 | BaseNote = baseNote; 17 | Type = baseNote.Type; 18 | } 19 | 20 | /// 21 | /// Gets the that this note belongs to. 22 | /// 23 | public VisualBeatmap Beatmap { get; } 24 | 25 | /// 26 | /// Gets the underlying . 27 | /// 28 | public NoteBase BaseNote { get; } 29 | 30 | /// 31 | /// Gets the type of this note. 32 | /// 33 | public NoteType Type { get; } 34 | 35 | public abstract void Draw(int beatmapTicks, float currentY); 36 | 37 | public abstract bool IsVisible(int beatmapTicks, float currentY); 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /ArcaeaView/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The Clear BSD License 2 | 3 | Copyright (c) 2018 OpenMLTD 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted (subject to the limitations in the disclaimer 8 | below) provided that the following conditions are met: 9 | 10 | * Redistributions of source code must retain the above copyright notice, 11 | this list of conditions and the following disclaimer. 12 | 13 | * Redistributions in binary form must reproduce the above copyright 14 | notice, this list of conditions and the following disclaimer in the 15 | documentation and/or other materials provided with the distribution. 16 | 17 | * Neither the name of the copyright holder nor the names of its 18 | contributors may be used to endorse or promote products derived from this 19 | software without specific prior written permission. 20 | 21 | NO EXPRESS OR IMPLIED LICENSES TO ANY PARTY'S PATENT RIGHTS ARE GRANTED BY 22 | THIS LICENSE. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 23 | CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 24 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A 25 | PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 26 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 27 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 28 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR 29 | BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER 30 | IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 31 | ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 32 | POSSIBILITY OF SUCH DAMAGE. 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ArcaeaSim 2 | 3 | An [Arcaea](https://arcaea.lowiro.com/) simulator for viewing; no playing support is planned in a near future. 4 | 5 | ArcaeaSim is built on [MilliSim](https://github.com/hozuki/MilliSim). Thus this is also a demonstration of the possibility of MilliSim. 6 | 7 | ArcaeaSim is in alpha phase so please don't expect a splendid visual effect. 8 | However, neither is it expected to go beyond the line to be a clone of Arcaea. 9 | 10 | - [Demo video 1](https://www.bilibili.com/video/av19881005/) (even before the initial commit; a very early technical preview so it may not look very appealing) 11 | - [Demo video 2](https://www.bilibili.com/video/av19981419/) (that's better) 12 | - [Demo video 3](https://www.bilibili.com/video/av20162946/) (even better) 13 | 14 | **NOTICE:** ArcaeaSim was once taken down by [DMCA request](https://github.com/github/dmca/blob/master/2018/2018-03-15-Arcaea.md) because of using copyrighted materials. 15 | In newer commits (i.e. after 15th March 2018) those materials are all removed, so when you run the application you will not see a UI like in those videos. 16 | 17 | | Downloads| | 18 | |--|--| 19 | | [GitHub Releases](https://github.com/hozuki/ArcaeaSim2/releases) | ![GitHub (pre-)release](https://img.shields.io/github/release/hozuki/ArcaeaSim2/all.svg) ![Github All Releases](https://img.shields.io/github/downloads/hozuki/ArcaeaSim2/total.svg) | 20 | | [AppVeyor](https://ci.appveyor.com/api/projects/hozuki/ArcaeaSim2/artifacts/arcaeasim-appveyor-latest.zip) | (latest development build) | 21 | 22 | | Build Status | | 23 | |--|--| 24 | | AppVeyor | [![AppVeyor](https://img.shields.io/appveyor/ci/hozuki/ArcaeaSim2.svg)](https://ci.appveyor.com/project/hozuki/ArcaeaSim2) | 25 | 26 | **Stage:** alpha 27 | 28 | **Miscellaneous:** 29 | 30 | [![GitHub contributors](https://img.shields.io/github/contributors/hozuki/ArcaeaSim2.svg)](https://github.com/hozuki/ArcaeaSim2/graphs/contributors) 31 | [![Libraries.io for GitHub](https://img.shields.io/librariesio/github/hozuki/ArcaeaSim2.svg)](https://github.com/hozuki/ArcaeaSim2) 32 | [![license](https://img.shields.io/github/license/hozuki/ArcaeaSim2.svg)](LICENSE.txt) 33 | 34 | ## Usage 35 | 36 | *This section is for end users, using packaged binaries and resources.* 37 | 38 | Like MilliSim, you will need: 39 | 40 | - Operating System: Windows 7 SP1 or later 41 | - [.NET Framework 4.5](https://www.microsoft.com/en-us/download/details.aspx?id=42642) 42 | - [Visual C++ 2015 Runtime](https://www.microsoft.com/en-us/download/details.aspx?id=53587) 43 | - OpenAL (bundled OpenAL-Soft Win32 build in newer releases) 44 | 45 | Test song(s) included in the repo: 46 | 47 | | Song | Cover | Composer(s) | Beatmap Author(s) | 48 | |---|---|---|---| 49 | | The Silence | | JDF | JDF | 50 | 51 | 1. You can set the window properties (e.g. window size) in `Contents/app.config.yml`. 52 | 2. You can set the background image properties in `Contents/config/background_image.yml`. 53 | 3. You can configure which beatmap to load in `Contents/config/beatmap_loader.yml`. 54 | 4. You can set the background music properties in `Contents/config/background_music.yml`. 55 | 5. You can filter out the plugins that you don't want (e.g. the debug info overlay) by commenting them out (adding a "#" at the start of the line) in `Contents/plugins.yml`. But remember to keep the essential plugins. 56 | 57 | Run the app with `--debug` switch to enable debug mode. 58 | 59 | **Known issue(s)**: 60 | 61 | - Some native Arcaea MP3s contain bad blocks (block sizes are inconsistent). When playing these songs ArcaeaSim will stop working and exit immediately. 62 | If you turn on the debug mode you will see the MP3 format issue in error log. In this case, you need to convert these MP3 files using other tools 63 | such as Foobar2000 and Audition, and feed ArcaeaSim with converted MP3 files. 64 | - Windows 7 (w/ SP1) may raise [ACM](https://msdn.microsoft.com/en-us/library/windows/desktop/dd742945.aspx) "conversion not possible" exception if you play the MP3 files. 65 | You can try to convert them to other formats, such as Wave audio files, and modify `audio_controller.yml` to load the converted files. 66 | - [Some other possible reasons of ArcaeaSim not starting](https://github.com/hozuki/MilliSim#note-if-millisim-cannot-launch). 67 | 68 | ## Development 69 | 70 | *This section is for developers.* 71 | 72 | Before you start, please read [Starting.md](docs/Starting.md). 73 | 74 | For building the solution, please read [Building.md](docs/Building.md). 75 | 76 | ## Contributing 77 | 78 | Contributions (especially PRs) are welcome because I probably do not have much time digging too deep into this, or simply doing maintenance work. 79 | Therefore it is not possible to make it good-looking (like that in Arcaea) only by myself. 80 | 81 | Remember to fork this repo, create your own feature branch, and make a PR. Thank you. 82 | 83 | Use English wherever you can. This makes it easier for contributors from various places to collaborate. :) 84 | 85 | ## License 86 | 87 | The code is open-sourced under BSD 3-Clause Clear license. 88 | 89 | > I REMADE ALL IMAGES BY MYSELF, USED A CUSTOM SONG AND BEATMAP WHICH ARE GRANTED BY THEIR AUTHOR. 90 | > THERE SHOULD BE NO COPYRIGHT ISSUES NOW. -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # https://www.appveyor.com/docs/appveyor-yml/ 2 | 3 | version: 1.0.3.{build} 4 | image: Visual Studio 2017 5 | platform: 6 | - x86 7 | configuration: 8 | - Release 9 | environment: 10 | EnableNuGetPackageRestore: true 11 | Release_Suffix: -alpha 12 | before_build: 13 | - cmd: nuget update -self 14 | - cmd: git submodule update --init --recursive 15 | - cmd: cd thirdparty\MilliSim 16 | - cmd: npm install 17 | - cmd: cd ..\.. 18 | - cmd: npm install -g marked 19 | - cmd: node thirdparty\MilliSim\scripts\nuget_restore.js 20 | - cmd: cd thirdparty\MilliSim 21 | - cmd: node scripts\patch_asminfo.js 22 | - cmd: cd ..\.. 23 | - ps: .\thirdparty\MilliSim\scripts\install_monogame.ps1 24 | build_script: 25 | - cmd: msbuild ArcaeaSim.sln /p:Configuration=Release /verbosity:minimal /p:nowarn="1574,1591" 26 | after_build: 27 | - ps: .\thirdparty\MilliSim\scripts\extract-openal-soft.ps1 -destination $Env:APPVEYOR_BUILD_FOLDER\ArcaeaSim\bin\Windows\x86\$Env:CONFIGURATION\ 28 | - cmd: marked ./README.md -o ./README.html 29 | - cmd: cd thirdparty\MilliSim 30 | - cmd: node scripts\appveyor_copy_licenses.js 31 | - cmd: cd ..\.. 32 | - cmd: del %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\*.xml 33 | - cmd: del %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\*.pdb 34 | # Remove useless SkiaSharp native libraries (we only need the one in x86 dir) 35 | - cmd: del %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\libSkiaSharp.* 36 | - cmd: copy %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\x86\libSkiaSharp.dll %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\ 37 | # Remove useless x64 dir (Direct3D branch targets x86) 38 | - cmd: rmdir /s /q %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\x64 39 | # x86 dir is not need either (and we don't have FFmpeg here) 40 | - cmd: rmdir /s /q %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\x86 41 | - cmd: 7z a arcaeasim.zip -r %APPVEYOR_BUILD_FOLDER%\ArcaeaSim\bin\Windows\x86\%CONFIGURATION%\* 42 | - cmd: 7z a arcaeasim.zip %APPVEYOR_BUILD_FOLDER%\README.html 43 | - cmd: cd thirdparty\MilliSim 44 | - cmd: 7z a arcaeasim.zip -r docs\licenses\* 45 | - cmd: cd ..\.. 46 | - cmd: copy arcaeasim.zip arcaeasim-appveyor-latest.zip 47 | - cmd: copy arcaeasim.zip arcaeasim-appveyor-v%APPVEYOR_BUILD_VERSION%%RELEASE_SUFFIX%.zip 48 | test: off 49 | artifacts: 50 | - path: arcaeasim-appveyor-v%APPVEYOR_BUILD_VERSION%%RELEASE_SUFFIX%.zip 51 | name: WithVersion 52 | - path: arcaeasim-appveyor-latest.zip 53 | name: Static 54 | deploy: 55 | - provider: GitHub 56 | description: ArcaeaSim 57 | auth_token: 58 | secure: u+MFjjY665AT4PHJ2bFMINBLnnnwSj1jLnNeFZoWfeYbomTNSPOmdahffmZa+dRH 59 | artifact: /arcaeasim.+\.zip/ 60 | draft: false 61 | prerelease: true 62 | on: 63 | appveyor_repo_tag: true 64 | -------------------------------------------------------------------------------- /docs/Building.md: -------------------------------------------------------------------------------- 1 | # Building ArcaeaSim 2 | 3 | **Requirements:** 4 | 5 | - OS: Windows 6 | - Compiler and Toolchain: 7 | - Visual Studio 2017 Community 8 | - .NET Framework 4.5 Toolchain 9 | - [NuGet CLI](https://www.nuget.org/downloads) (≥ 4.3.0) 10 | 11 | > **Remember** to [update your NuGet version](https://docs.microsoft.com/en-us/nuget/guides/install-nuget) before building. 12 | > Otherwise you are very likely to see errors like "Too many projects specified". 13 | 14 | **Step 1**: Clone this repo: 15 | 16 | ```bash 17 | git clone https://github.com/hozuki/ArcaeaSim.git 18 | cd ArcaeaSim 19 | git submodule update --init --recursive 20 | ``` 21 | 22 | **Step 2**: Install [MonoGame](http://www.monogame.net/downloads/) (version 3.6 or later). 23 | 24 | **Step 3**: Build the solution in Visual Studio. 25 | 26 | If you encounter errors like "assembly not found", try to run `nuget restore SOLUTION_NAME.sln` in every solution directory. 27 | The automatic NuGet restore in Visual Studio does not work. 28 | 29 | **Step 4**: A little cleaning. In the directory containing built binaries, replace the `libSkiaSharp.dll` with the DLL with the same name 30 | under `x86` directory. 31 | 32 | The default `libSkiaSharp.dll` in the binaries directory is a 64-bit DLL, but MonoGame WindowsDX build only produces a 32-bit executable by default, 33 | so the executable cannot load the default DLL. 34 | -------------------------------------------------------------------------------- /docs/Starting.md: -------------------------------------------------------------------------------- 1 | # Starting to Work 2 | 3 | ## Conventions 4 | 5 | The standard vocabulary for calling notes etc. follows the names on [Wikia](http://lowiro.wikia.com/wiki/Arcaea_Wiki). 6 | 7 | The tips below are to help you understand the coding conventions in this repo. 8 | 9 | For coding style, use: 10 | 11 | - `_camelCaseWithAPrefixUnderscore` for private instance members (`private`, `private static`, and `private readonly`); 12 | - `camelCase` for parameters and local variables; 13 | - `PascalCase` for the rest, including `private static readonly`. 14 | 15 | Visibility order: `public` -> `protected` -> `private` 16 | 17 | ## Coordinate Systems 18 | 19 | Arcaea's beatmap coordinate system and Arcaea's coordinate system (in code): left hand, Y-up (OpenGL coordinate system). 20 | 21 | ```plain 22 | Y 23 | Z | 24 | \--X 25 | ``` 26 | 27 | ArcaeaSim's coordinate system (in code): right hand, Z-up (Direct3D and mathematical coordinate system). 28 | 29 | ```plain 30 | Z 31 | Y | 32 | \--X 33 | ``` 34 | --------------------------------------------------------------------------------