Repository: pythonology/BeatTogether Branch: master Commit: 82fd4a48370c Files: 22 Total size: 51.6 KB Directory structure: gitextract_ln948ubu/ ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── Bug_Report.yml │ │ └── config.yml │ └── workflows/ │ ├── Build.yml │ └── PR_Build.yml ├── .gitignore ├── BeatTogether/ │ ├── BeatTogether.csproj │ ├── Config.cs │ ├── Directory.Build.props │ ├── Directory.Build.targets │ ├── Installers/ │ │ ├── BtAppInstaller.cs │ │ └── BtMenuInstaller.cs │ ├── Models/ │ │ ├── ServerDetails.cs │ │ └── TemporaryServerDetails.cs │ ├── Plugin.cs │ ├── Registries/ │ │ └── ServerDetailsRegistry.cs │ ├── UI/ │ │ ├── ServerSelectionController.bsml │ │ └── ServerSelectionController.cs │ └── manifest.json ├── BeatTogether.sln ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/FUNDING.yml ================================================ patreon: BeatTogether ================================================ FILE: .github/ISSUE_TEMPLATE/Bug_Report.yml ================================================ name: "Bug report 🐛" description: Report issues, errors or unexpected behavior title: "[Bug]: " labels: [bug] assignees: - michael-r-elp body: - type: markdown attributes: value: | Please make sure to search for existing issues before making a new one! - type: textarea attributes: label: Describe the bug placeholder: "Thing 'x' isn't working when I do 'y', etc." description: | A clear and concise description of what the bug is. validations: required: true - type: textarea attributes: label: Steps to reproduce placeholder: Steps to reproduce the behavior. validations: required: true - type: textarea attributes: label: Expected Behavior description: If applicable, add screenshots to help explain your problem. placeholder: What were you expecting? validations: required: false - type: textarea attributes: label: Actual Behavior placeholder: What happened instead? validations: required: true - type: input attributes: label: Game Version placeholder: "1.29.0" description: | The version of the game you were running validations: required: true - type: input attributes: label: Mod Version placeholder: "0.1.0" description: | The version of the mod you were using validations: required: true - type: textarea attributes: label: Additional context description: Add any other context about the problem here. validations: required: false #- type: checkboxes # id: terms # attributes: # label: Code of Conduct # description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/BeatTogether/.github/blob/main/CODE_OF_CONDUCT.md) # options: # - label: I agree to follow this project's Code of Conduct # required: true ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: true # Enabled for now until all issue templates are added contact_links: - name: BeatTogether Community Discord url: https://discord.com/invite/gezGrFG4tz about: Please ask and answer questions here. ================================================ FILE: .github/workflows/Build.yml ================================================ name: Build on: push: branches: [ master ] paths: - 'BeatTogether.sln' - 'BeatTogether/**' - '.github/workflows/Build.yml' jobs: Build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup dotnet uses: actions/setup-dotnet@v3 with: dotnet-version: '5.0.x' - name: Fetch SIRA References uses: ProjectSIRA/download-sira-stripped@1.0.0 with: manifest: ./BeatTogether/manifest.json sira-server-code: ${{ secrets.SIRA_SERVER_CODE }} - name: Fetch Mod References uses: Goobwabber/download-beatmods-deps@1.1 with: manifest: ./BeatTogether/manifest.json - name: Build id: Build env: FrameworkPathOverride: /usr/lib/mono/4.8-api run: dotnet build --configuration Release - name: GitStatus run: git status - name: Echo Filename run: echo $BUILDTEXT \($ASSEMBLYNAME\) env: BUILDTEXT: Filename=${{ steps.Build.outputs.filename }} ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }} - name: Upload Artifact uses: actions/upload-artifact@v1 with: name: ${{ steps.Build.outputs.filename }} path: ${{ steps.Build.outputs.artifactpath }} ================================================ FILE: .github/workflows/PR_Build.yml ================================================ name: Pull Request Build on: pull_request: branches: [ master ] paths: - 'BeatTogether.sln' - 'BeatTogether/**' - '.github/workflows/PR_Build.yml' jobs: Build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup dotnet uses: actions/setup-dotnet@v3 with: dotnet-version: 5.0.x - name: Fetch SIRA References uses: ProjectSIRA/download-sira-stripped@1.0.0 with: manifest: ./BeatTogether/manifest.json sira-server-code: ${{ secrets.SIRA_SERVER_CODE }} - name: Fetch Mod References uses: Goobwabber/download-beatmods-deps@1.1 with: manifest: ./BeatTogether/manifest.json - name: Build id: Build env: FrameworkPathOverride: /usr/lib/mono/4.8-api run: dotnet build --configuration Release - name: GitStatus run: git status - name: Echo Filename run: echo $BUILDTEXT \($ASSEMBLYNAME\) env: BUILDTEXT: Filename=${{ steps.Build.outputs.filename }} ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }} - name: Upload Artifact uses: actions/upload-artifact@v1 with: name: ${{ steps.Build.outputs.filename }} path: ${{ steps.Build.outputs.artifactpath }} ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore # User-specific files *.rsuser *.suo *.user *.userosscache *.sln.docstates # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Mono auto generated files mono_crash.* # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ [Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # Visual Studio 2017 auto generated files Generated\ Files/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUnit *.VisualState.xml TestResult.xml nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # Benchmark Results BenchmarkDotNet.Artifacts/ # .NET Core project.lock.json project.fragment.lock.json artifacts/ # StyleCop StyleCopReport.xml # Files built by Visual Studio *_i.c *_p.c *_h.h *.ilk *.meta *.obj *.iobj *.pch *.pdb *.ipdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *_wpftmp.csproj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile *.VC.db *.VC.VC.opendb # Visual Studio profiler *.psess *.vsp *.vspx *.sap # Visual Studio Trace Files *.e2e # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # AxoCover is a Code Coverage Tool .axoCover/* !.axoCover/settings.json # Visual Studio code coverage results *.coverage *.coveragexml # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # Note: Comment the next line if you want to checkin your web deploy settings, # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # Microsoft Azure Web App publish settings. Comment the next line if you want to # checkin your Azure Web App publish settings, but sensitive information contained # in these scripts will be unencrypted PublishScripts/ # NuGet Packages *.nupkg # NuGet Symbol Packages *.snupkg # The packages folder can be ignored because of Package Restore **/[Pp]ackages/* # except build/, which is used as an MSBuild target. !**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Windows Store app package directories and files AppPackages/ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx *.appxbundle *.appxupload # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !?*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.jfm *.pfx *.publishsettings orleans.codegen.cs # Including strong name files can present a security risk # (https://github.com/github/gitignore/pull/2483#issue-259490424) #*.snk # Since there are multiple workflows, uncomment next line to ignore bower_components # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) #bower_components/ # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm ServiceFabricBackup/ *.rptproj.bak # SQL Server files *.mdf *.ldf *.ndf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings *.rptproj.rsuser *- [Bb]ackup.rdl *- [Bb]ackup ([0-9]).rdl *- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat node_modules/ # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe paket-files/ # FAKE - F# Make .fake/ # CodeRush personal settings .cr/personal # Python Tools for Visual Studio (PTVS) __pycache__/ *.pyc # Cake - Uncomment if you are using it # tools/** # !tools/packages.config # Tabs Studio *.tss # Telerik's JustMock configuration file *.jmconfig # BizTalk build output *.btp.cs *.btm.cs *.odx.cs *.xsd.cs # OpenCover UI analysis results OpenCover/ # Azure Stream Analytics local run output ASALocalRun/ # MSBuild Binary and Structured Log *.binlog # NVidia Nsight GPU debugger configuration file *.nvuser # MFractors (Xamarin productivity tool) working folder .mfractor/ # Local History for Visual Studio .localhistory/ # BeatPulse healthcheck temp database healthchecksdb # Backup folder for Package Reference Convert tool in Visual Studio 2017 MigrationBackup/ # Ionide (cross platform F# VS Code tools) working folder .ionide/ launchSettings.json ================================================ FILE: BeatTogether/BeatTogether.csproj ================================================  Library Properties BeatTogether 2.2.1 net472 true portable ..\Refs $(LocalRefsDir) prompt 4 9.0 enable Unofficial local false DEBUG;TRACE true True True True $(BeatSaberDir)\Libs\0Harmony.dll False ..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\BeatSaber.ViewSystem.dll False $(BeatSaberDir)\Plugins\BSML.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False False True $(BeatSaberDir)\Plugins\MultiplayerCore.dll False False $(BeatSaberDir)\Plugins\SiraUtil.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\DataModels.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.Polyglot.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll False True $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll False all runtime; build; native; contentfiles; analyzers; buildtransitive all runtime; build; native; contentfiles; analyzers; buildtransitive Official $(VersionType)-$(GitBranch)-$(CommitHash) $(VersionType)-$(GitBranch)-$(CommitHash)-$(GitModified) ================================================ FILE: BeatTogether/Config.cs ================================================ using System.Collections.Generic; using System.Linq; using BeatTogether.Models; using IPA.Config.Stores.Attributes; using IPA.Config.Stores.Converters; namespace BeatTogether { public class Config { // Official master server name that will be seen by players public const string OfficialServerName = "Official Servers"; // BeatTogether master server config public const int DefaultApiPort = 8989; public const string BeatTogetherServerName = "BeatTogether"; public const string BeatTogetherHostName = "master.beattogether.systems"; public const string BeatTogetherApiUri = "http://master.beattogether.systems:8989"; public const string BeatTogetherStatusUri = "http://master.beattogether.systems/status"; public const int BeatTogetherMaxPartySize = 100; public virtual string SelectedServer { get; set; } = BeatTogetherServerName; [NonNullable, UseConverter(typeof(CollectionConverter>))] public virtual List Servers { get; set; } = new(); public virtual void OnReload() { var haveBtServer = false; foreach (var server in Servers) { if (server.ServerName == BeatTogetherServerName) haveBtServer = true; // Try to auto migrate API URL if missing from older configs if (string.IsNullOrEmpty(server.ApiUrl)) server.ApiUrl = $"http://{server.HostName}:{DefaultApiPort}"; } if (!haveBtServer) { Servers.Insert(0, new ServerDetails { ServerName = BeatTogetherServerName, HostName = BeatTogetherHostName, ApiUrl = BeatTogetherApiUri, StatusUri = BeatTogetherStatusUri, MaxPartySize = BeatTogetherMaxPartySize, DisableSsl = true }); } } public virtual void CopyFrom(Config other) { SelectedServer = other.SelectedServer; Servers = other.Servers; } } } ================================================ FILE: BeatTogether/Directory.Build.props ================================================  true true true false true true ================================================ FILE: BeatTogether/Directory.Build.targets ================================================  2.0 false $(OutputPath)$(AssemblyName) $(OutputPath)Final True $(PluginVersion) $(PluginVersion) $(PluginVersion) $(AssemblyName) $(ArtifactName)-$(PluginVersion) $(ArtifactName)-bs$(GameVersion) $(ArtifactName)-$(CommitHash) $(AssemblyName) $(AssemblyName) $(OutDir)zip\ $(BeatSaberDir)\Plugins True Unable to copy assembly to game folder, did you set 'BeatSaberDir' correctly in your 'csproj.user' file? Plugins folder doesn't exist: '$(PluginDir)'. Unable to copy to Plugins folder, '$(BeatSaberDir)' does not appear to be a Beat Saber game install. Unable to copy to Plugins folder, 'BeatSaberDir' has not been set in your 'csproj.user' file. False $(BeatSaberDir)\IPA\Pending\Plugins ================================================ FILE: BeatTogether/Installers/BtAppInstaller.cs ================================================ using BeatTogether.Registries; using Zenject; namespace BeatTogether.Installers { class BtAppInstaller : Installer { private readonly Config _config; public BtAppInstaller( Config config) { _config = config; } public override void InstallBindings() { Container.BindInstance(_config).AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); } } } ================================================ FILE: BeatTogether/Installers/BtMenuInstaller.cs ================================================ using BeatTogether.UI; using Zenject; namespace BeatTogether.Installers { class BtMenuInstaller : Installer { public override void InstallBindings() { Container.BindInterfacesAndSelfTo().AsSingle(); } } } ================================================ FILE: BeatTogether/Models/ServerDetails.cs ================================================ using System; namespace BeatTogether.Models { public class ServerDetails { /// /// Display name for UI /// public string ServerName { get; set; } = string.Empty; /// /// Legacy hostname for master server (no longer used, except for automatic config migrations) /// public string HostName { get; set; } = string.Empty; /// /// The multiplayer API url / graph url /// public string ApiUrl { get; set; } = string.Empty; /// /// Optional status check URL for the server /// public string StatusUri { get; set; } = string.Empty; /// /// Max amount of players per instance /// public int MaxPartySize { get; set; } = 5; /// /// If set: disable SSL and certificate validation for all Ignorance/ENet client connections. /// public bool DisableSsl { get; set; } = true; public bool IsOfficial => ServerName == Config.OfficialServerName; /// /// Gets whether this server matches against a given override URL. /// Only checks whether the hostname and port match. /// public bool MatchesApiUrl(string? apiUrl) { if (apiUrl == ApiUrl) // Exact match return true; if (string.IsNullOrEmpty(apiUrl)) return false; // Loose match try { var urlOurs = new Uri(ApiUrl); var urlTheirs = new Uri(apiUrl); return urlOurs.Host == urlTheirs.Host && urlOurs.Port == urlTheirs.Port; } catch (UriFormatException) { return false; } } public override string ToString() => ServerName; } } ================================================ FILE: BeatTogether/Models/TemporaryServerDetails.cs ================================================ using System; namespace BeatTogether.Models { public class TemporaryServerDetails : ServerDetails { public TemporaryServerDetails(string graphApiUrl, string? statusUrl) { try { var urlParsed = new Uri(graphApiUrl); ServerName = urlParsed.Host; HostName = urlParsed.Host; } catch (UriFormatException) { ServerName = graphApiUrl; HostName = graphApiUrl; } ApiUrl = graphApiUrl; StatusUri = statusUrl ?? graphApiUrl; MaxPartySize = IsOfficial ? 5 : 128; DisableSsl = true; } } } ================================================ FILE: BeatTogether/Plugin.cs ================================================ using BeatTogether.Installers; using HarmonyLib; using IPA; using IPA.Config.Stores; using IPA.Loader; using SiraUtil.Zenject; using Conf = IPA.Config.Config; using IPALogger = IPA.Logging.Logger; namespace BeatTogether { [Plugin(RuntimeOptions.SingleStartInit)] class Plugin { private readonly Harmony _harmony; private readonly PluginMetadata _metadata; public const string ID = "com.Python.BeatTogether"; [Init] public Plugin(IPALogger logger, Conf conf, PluginMetadata metadata, Zenjector zenjector) { Config config = conf.Generated(); _harmony = new Harmony(ID); _metadata = metadata; zenjector.UseLogger(logger); zenjector.Install(Location.App, config); zenjector.Install(Location.Menu); } [OnEnable] public void OnEnable() { _harmony.PatchAll(_metadata.Assembly); } [OnDisable] public void OnDisable() { _harmony.UnpatchSelf(); } } } ================================================ FILE: BeatTogether/Registries/ServerDetailsRegistry.cs ================================================ using BeatTogether.Models; using System; using System.Collections.Generic; using System.Linq; namespace BeatTogether.Registries { public class ServerDetailsRegistry { public ServerDetails SelectedServer => TemporarySelectedServer ?? Servers.FirstOrDefault(details => details.ServerName == _config.SelectedServer) ?? Servers.FirstOrDefault(details => details.ServerName == Config.BeatTogetherServerName); public IReadOnlyList Servers => _config.Servers.Concat(_servers).Append(OfficialServer).ToList(); private readonly Config _config; private readonly List _servers = new(); public readonly ServerDetails OfficialServer = new() { ServerName = Config.OfficialServerName }; public TemporaryServerDetails? TemporarySelectedServer { get; private set; } internal ServerDetailsRegistry( Config config) { _config = config; } public void AddServer(ServerDetails server) { if (Servers.Any(details => details.ServerName == server.ServerName)) throw new ArgumentException($"A server already exists with the name {server.ServerName}."); _servers.Add(server); } public void SetSelectedServer(ServerDetails server) { if (server is TemporaryServerDetails tmpServer) { TemporarySelectedServer = tmpServer; } else { _config.SelectedServer = server.ServerName; TemporarySelectedServer = null; } } } } ================================================ FILE: BeatTogether/UI/ServerSelectionController.bsml ================================================  ================================================ FILE: BeatTogether/UI/ServerSelectionController.cs ================================================ using BeatSaberMarkupLanguage; using BeatSaberMarkupLanguage.Attributes; using BeatSaberMarkupLanguage.Components.Settings; using BeatSaberMarkupLanguage.FloatingScreen; using BeatTogether.Models; using BeatTogether.Registries; using HMUI; using IPA.Utilities; using MultiplayerCore.Patchers; using SiraUtil.Affinity; using SiraUtil.Logging; using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using JetBrains.Annotations; using UnityEngine; using Zenject; using System.Threading; using BGLib.Polyglot; using MultiplayerCore.Models; using MultiplayerCore.Repositories; namespace BeatTogether.UI { internal class ServerSelectionController : IInitializable, IAffinity, INotifyPropertyChanged { public const string ResourcePath = "BeatTogether.UI.ServerSelectionController.bsml"; private FloatingScreen _screen = null!; private readonly MultiplayerModeSelectionFlowCoordinator _modeSelectionFlow; private readonly JoiningLobbyViewController _joiningLobbyView; private readonly NetworkConfigPatcher _networkConfig; private readonly MpStatusRepository _mpStatusRepository; private readonly ServerDetailsRegistry _serverRegistry; private readonly SiraLog _logger; private bool _isFirstActivation; private uint _allowSelectionOnce; [UIComponent("server-list")] private ListSetting _serverList = null!; [UIValue("server")] private ServerDetails _serverValue { get => _serverRegistry.SelectedServer; set => ApplySelectedServer(value); } [UIValue("server-options")] private List _serverOptions; internal ServerSelectionController( MultiplayerModeSelectionFlowCoordinator modeSelectionFlow, JoiningLobbyViewController joiningLobbyView, NetworkConfigPatcher networkConfig, MpStatusRepository mpStatusRepository, ServerDetailsRegistry serverRegistry, SiraLog logger) { _modeSelectionFlow = modeSelectionFlow; _joiningLobbyView = joiningLobbyView; _networkConfig = networkConfig; _mpStatusRepository = mpStatusRepository; _serverRegistry = serverRegistry; _logger = logger; _isFirstActivation = true; _serverOptions = new(_serverRegistry.Servers); _mpStatusRepository.statusUpdatedForUrlEvent += HandleMpStatusUpdateForUrl; } public void Initialize() { _screen = FloatingScreen.CreateFloatingScreen(new Vector2(90, 90), false, new Vector3(0, 3f, 4.35f), new Quaternion(0, 0, 0, 0)); BSMLParser.Instance.Parse(Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _screen.gameObject, this); (_serverList.gameObject.transform.GetChild(1) as RectTransform)!.sizeDelta = new Vector2(60, 0); _screen.GetComponent().SetRadius(140); _screen.gameObject.SetActive(false); } #region Server selection private void ApplySelectedServer(ServerDetails server) { if (server is TemporaryServerDetails) return; ApplyNetworkConfig(server); SyncTemporarySelectedServer(); RefreshSwitchInteractable(); _modeSelectionFlow.DidDeactivate(false, false); _modeSelectionFlow.DidActivate(false, true, false); _modeSelectionFlow.ReplaceTopViewController(_joiningLobbyView, animationDirection: ViewController.AnimationDirection.Vertical); } private void SyncSelectedServer() { ServerDetails selectedServer; if (_networkConfig.IsOverridingApi) { // Master server is being patched by MpCore, sync our selection var knownServer = _serverRegistry.Servers.FirstOrDefault(serverDetails => serverDetails.MatchesApiUrl(_networkConfig.GraphUrl)); if (knownServer != null) { // Selected server is in our config selectedServer = knownServer; } else { // Selected server is not in our config, set temporary value _logger.Debug($"Setting temporary server details (GraphUrl={_networkConfig.GraphUrl})"); selectedServer = new TemporaryServerDetails(_networkConfig.GraphUrl!, _networkConfig.MasterServerStatusUrl); } } else { selectedServer = _serverRegistry.OfficialServer; } _serverRegistry.SetSelectedServer(selectedServer); SyncTemporarySelectedServer(); OnPropertyChanged(nameof(_serverValue)); // for BSML binding } #endregion #region Server config private void ApplyNetworkConfig(ServerDetails server) { if (server.IsOfficial) _networkConfig.UseOfficialServer(); else _networkConfig.UseCustomApiServer(server.ApiUrl, server.StatusUri, server.MaxPartySize, null, server.DisableSsl); } private void SyncTemporarySelectedServer() { var didChange = false; if (_serverRegistry.TemporarySelectedServer is not null) { var temporaryServer = _serverRegistry.TemporarySelectedServer!; if (!_serverOptions.Contains(temporaryServer)) { _serverOptions.Add(temporaryServer); didChange = true; } } else { if (_serverOptions.RemoveAll(so => so is TemporaryServerDetails) > 0) didChange = true; } if (didChange) OnPropertyChanged(nameof(_serverOptions)); // for BSML binding } private void HandleMpStatusUpdateForUrl(string statusUrl, MpStatusData statusData) { // Automatically set disableSsl setting from mp status data var targetServers = _serverRegistry.Servers .Where((server) => server.StatusUri.Equals(statusUrl)); foreach (var targetServer in targetServers) { var disableSsl = !statusData.useSsl; if (disableSsl == targetServer.DisableSsl) continue; _logger.Info($"Config update for \"{targetServer.ServerName}\": disableSsl={disableSsl}"); targetServer.DisableSsl = disableSsl; if (_serverRegistry.SelectedServer == targetServer) ApplyNetworkConfig(targetServer); } } #endregion #region Affinity patches [AffinityPrefix] [AffinityPatch(typeof(MultiplayerModeSelectionFlowCoordinator), nameof(MultiplayerModeSelectionFlowCoordinator.DidActivate))] private void DidActivate() { if (_isFirstActivation) { // First activation: apply the currently selected server (from our config) ApplyNetworkConfig(_serverRegistry.SelectedServer); _isFirstActivation = false; } else { // Secondary activation: server selection may have been externally modified, sync it now SyncSelectedServer(); _screen.gameObject.SetActive(true); } } [AffinityPostfix] [AffinityPatch(typeof(MultiplayerModeSelectionFlowCoordinator), nameof(MultiplayerModeSelectionFlowCoordinator.PresentMasterServerUnavailableErrorDialog))] private void PresentMasterServerUnavailableErrorDialog() { _allowSelectionOnce = 2; } [AffinityPrefix] [AffinityPatch(typeof(MultiplayerModeSelectionFlowCoordinator), nameof(MultiplayerModeSelectionFlowCoordinator.DidDeactivate))] private void DidDeactivate(bool removedFromHierarchy) { _screen.gameObject.SetActive(false); } [AffinityPostfix] [AffinityPatch(typeof(MultiplayerModeSelectionFlowCoordinator), nameof(MultiplayerModeSelectionFlowCoordinator.TransitionDidStart))] private void TransitionDidStart() { RefreshSwitchInteractable(); } [AffinityPostfix] [AffinityPatch(typeof(MultiplayerModeSelectionFlowCoordinator), nameof(MultiplayerModeSelectionFlowCoordinator.TransitionDidFinish))] private void TransitionDidFinish() { RefreshSwitchInteractable(); } [AffinityPrefix] [AffinityPatch(typeof(ViewControllerTransitionHelpers), nameof(ViewControllerTransitionHelpers.DoPresentTransition))] private void DoPresentTransition(ViewController toPresentViewController, ViewController toDismissViewController, ref ViewController.AnimationDirection animationDirection) { if (toDismissViewController is JoiningLobbyViewController) animationDirection = ViewController.AnimationDirection.Vertical; } [AffinityPrefix] [AffinityPatch(typeof(MultiplayerModeSelectionFlowCoordinator), nameof(MultiplayerModeSelectionFlowCoordinator.TopViewControllerWillChange))] private bool TopViewControllerWillChange(ViewController oldViewController, ViewController newViewController, ViewController.AnimationType animationType) { var screenContainer = oldViewController != null ? oldViewController.transform.parent.parent : newViewController.transform.parent.parent; var screenSystem = screenContainer.parent; _screen.gameObject.transform.localScale = screenContainer.localScale * screenSystem.localScale.y; _screen.transform.position = screenContainer.position + new Vector3(0, screenSystem.localScale.y * 1.15f, 0); _screen.gameObject.SetActive(true); RefreshSwitchInteractable(); if (newViewController is JoiningLobbyViewController && animationType == ViewController.AnimationType.None) return false; return true; } [AffinityPrefix] [AffinityPatch(typeof(FlowCoordinator), nameof(FlowCoordinator.SetTitle))] private void SetTitle(ref string value, ref string ____title) { // Keep "Multiplayer Mode Selection" as a title when the server status check is happening // This makes it more obvious what is going on and it looks less goofy (duplicate text) if (value == Localization.Get("LABEL_CHECKING_SERVER_STATUS")) value = Localization.Get("LABEL_MULTIPLAYER_MODE_SELECTION"); } #endregion #region SetInteraction private bool _globalInteraction = true; private void RefreshSwitchInteractable() { if (_serverList == null) return; // Only allow interactions when the main view controller is active and not transitioning var interactable = _globalInteraction && _modeSelectionFlow.topViewController is MultiplayerModeSelectionViewController && !_modeSelectionFlow.topViewController.isInTransition; // We have _allowSelectionOnce set to 2 and only enable the actual toggle // the second time this runs as the first will be the status check and // on the second time this runs we'll have the actual error pop-up _serverList.Interactable = interactable || _allowSelectionOnce == 1; if (_allowSelectionOnce > 0) _allowSelectionOnce -= 1; } [AffinityPrefix] [AffinityPatch(typeof(FlowCoordinator), nameof(FlowCoordinator.SetGlobalUserInteraction))] private void SetGlobalUserInteraction(bool value) { _globalInteraction = value; RefreshSwitchInteractable(); } #endregion #region INotifyPropertyChanged public event PropertyChangedEventHandler? PropertyChanged; [NotifyPropertyChangedInvocator] private void OnPropertyChanged([CallerMemberName] string? propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } #endregion } } ================================================ FILE: BeatTogether/manifest.json ================================================ { "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json", "id": "BeatTogether", "name": "BeatTogether", "author": "Goobwabber", "version": "2.2.1", "description": "A multiplayer private server for the modding community. Supports crossplay between PC and Quest.", "gameVersion": "1.37.5", "dependsOn": { "BSIPA": "^4.3.3", "BeatSaberMarkupLanguage": "^1.12.0", "SiraUtil": "^3.1.7", "MultiplayerCore": "^1.5.0" } } ================================================ FILE: BeatTogether.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.29926.136 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BeatTogether", "BeatTogether/BeatTogether.csproj", "{8421DD7B-8755-425F-9F6F-7EE4FDB74312}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8421DD7B-8755-425F-9F6F-7EE4FDB74312}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8421DD7B-8755-425F-9F6F-7EE4FDB74312}.Debug|Any CPU.Build.0 = Debug|Any CPU {8421DD7B-8755-425F-9F6F-7EE4FDB74312}.Release|Any CPU.ActiveCfg = Release|Any CPU {8421DD7B-8755-425F-9F6F-7EE4FDB74312}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {AC359C44-03E2-4E4F-9302-35ED1B2D7D31} EndGlobalSection EndGlobal ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2020 Pythonology Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # BeatTogether A multiplayer private server for the modding community. Supports crossplay between PC and Quest. **This is the PC Plugin.** Feel free to join our Discord! https://discord.com/invite/gezGrFG4tz (Support, Coordinating games with friends, etc) Want to support development and server costs? [Click Here](https://www.patreon.com/BeatTogether) ## Features * Private server free from Beat Saber official; allowing Quest to play freely on modded installs * Crossplay between all compatible platforms * Custom songs between platforms * 10 Player lobbies ## Requirements This version supports Beat Saber 1.37.0+ with mods: * BSIPA v4.1.5+ * BeatSaberMarkupLanguage v1.11.4+ * [MultiplayerCore v1.5.0+](https://github.com/Goobwabber/MultiplayerCore#installation) These can be downloaded from [BeatMods](https://beatmods.com/#/mods) or using [BSManager](https://github.com/Zagrios/bs-manager?tab=readme-ov-file#download-and-installation) ## Installation **Recommended Install:** The easiest way to install is through [BSManager](https://github.com/Zagrios/bs-manager?tab=readme-ov-file#download-and-installation)! (Only available for 1.37.3) **Manual Install (For any other version than 1.37.3)** To install, Download the latest mod from our releases. [Click Here](https://github.com/BeatTogether/BeatTogether/releases) Extract the zip file to your Beat Saber game directory (the one `Beat Saber.exe` is in). The `BeatTogether.dll` should end up in your `Plugins` folder (**NOT** the one in `Beat Saber_Data`).