Full Code of pythonology/BeatTogether for AI

master 82fd4a48370c cached
22 files
51.6 KB
12.9k tokens
40 symbols
1 requests
Download .txt
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
================================================
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <AssemblyName>BeatTogether</AssemblyName>
    <AssemblyVersion>2.2.1</AssemblyVersion>
    <TargetFramework>net472</TargetFramework>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>portable</DebugType>
    <LocalRefsDir Condition="Exists('..\Refs')">..\Refs</LocalRefsDir>
    <BeatSaberDir>$(LocalRefsDir)</BeatSaberDir>
    <!--<PathMap>$(AppOutputBase)=X:\$(AssemblyName)\</PathMap>-->
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <LangVersion>9.0</LangVersion>
    <Nullable>enable</Nullable>
    <VersionType>Unofficial</VersionType>
    <CommitHash>local</CommitHash>
    <GitBranch>
    </GitBranch>
    <GitModified>
    </GitModified>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
    <Optimize>false</Optimize>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
    <Optimize>true</Optimize>
  </PropertyGroup>
  <PropertyGroup Condition="$(DefineConstants.Contains('CIBuild')) OR '$(NCrunch)' == '1'">
    <DisableCopyToPlugins>True</DisableCopyToPlugins>
  </PropertyGroup>
  <PropertyGroup Condition="'$(NCrunch)' == '1'">
    <DisableCopyToPlugins>True</DisableCopyToPlugins>
    <DisableZipRelease>True</DisableZipRelease>
  </PropertyGroup>
  <ItemGroup>
    <None Remove="UI\ServerSelectionController.bsml" />
  </ItemGroup>
  <ItemGroup>
    <Reference Include="0Harmony">
      <HintPath>$(BeatSaberDir)\Libs\0Harmony.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="BeatSaber.ViewSystem" Publicize="true">
      <HintPath>..\..\..\..\..\Program Files (x86)\Steam\steamapps\common\Beat Saber\Beat Saber_Data\Managed\BeatSaber.ViewSystem.dll</HintPath>
    </Reference>
    <Reference Include="BSML, Version=1.4.2.0, Culture=neutral, processorArchitecture=MSIL">
      <Private>False</Private>
      <HintPath>$(BeatSaberDir)\Plugins\BSML.dll</HintPath>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="Main">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
      <Publicize>True</Publicize>
    </Reference>
    <Reference Include="MultiplayerCore">
      <HintPath>$(BeatSaberDir)\Plugins\MultiplayerCore.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="SiraUtil, Version=3.0.0.0, Culture=neutral, processorArchitecture=MSIL">
      <Private>False</Private>
      <HintPath>$(BeatSaberDir)\Plugins\SiraUtil.dll</HintPath>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Data" />
    <Reference Include="DataModels">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\DataModels.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="BGLib.Polyglot">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\BGLib.Polyglot.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="HMLib">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="HMUI">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll</HintPath>
      <Private>False</Private>
      <Publicize>True</Publicize>
    </Reference>
    <Reference Include="IPA.Loader">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.CoreModule">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.UI">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Zenject">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Zenject-usage">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll</HintPath>
      <Private>False</Private>
    </Reference>
  </ItemGroup>
  <ItemGroup>
    <EmbeddedResource Include="manifest.json" />
    <EmbeddedResource Include="UI\ServerSelectionController.bsml" />
  </ItemGroup>
  <ItemGroup>
    <None Include="Directory.Build.props" Condition="Exists('Directory.Build.props')" />
    <None Include="Directory.Build.targets" Condition="Exists('Directory.Build.targets')" />
    <None Include="BeatTogether.csproj.user" Condition="Exists('BeatTogether.csproj.user')" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="BeatSaberModdingTools.Tasks" Version="1.4.3">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
    <PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.2">
      <PrivateAssets>all</PrivateAssets>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
    </PackageReference>
  </ItemGroup>
  <Target Name="PreBuild" BeforeTargets="BeforeBuild" Condition="'$(NCRUNCH)' != '1'">
    <Error Text="The BeatSaberModdingTools.Tasks nuget package doesn't seem to be installed." Condition="'$(BSMTTaskAssembly)' == ''" />
    <GetCommitInfo ProjectDir="$(ProjectDir)">
      <Output TaskParameter="CommitHash" PropertyName="CommitHash" />
      <Output TaskParameter="Branch" PropertyName="GitBranch" />
      <Output TaskParameter="IsPullRequest" PropertyName="IsPullRequest" />
      <Output TaskParameter="Modified" PropertyName="GitModified" />
      <Output TaskParameter="GitUser" PropertyName="GitUser" />
    </GetCommitInfo>
    <PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true' AND ('$(GitUser)' == 'BeatTogether' OR '$(GitUser)' == 'michael-r-elp' OR '$(GitUser)' == 'roydejong' OR '$(GitUser)' == 'Goobwabber') AND '$(IsPullRequest)' != 'true'">
      <VersionType>Official</VersionType>
    </PropertyGroup>
    <PropertyGroup Condition="'$(GitModified)' != 'Modified'">
      <InformationalVersion>$(VersionType)-$(GitBranch)-$(CommitHash)</InformationalVersion>
    </PropertyGroup>
    <PropertyGroup Condition="'$(GitModified)' == 'Modified'">
      <InformationalVersion>$(VersionType)-$(GitBranch)-$(CommitHash)-$(GitModified)</InformationalVersion>
    </PropertyGroup>
    <Message Text="Version $(InformationalVersion)" Importance="high" />
  </Target>
</Project>

================================================
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<ServerDetails, List<ServerDetails?>>))]
        public virtual List<ServerDetails> 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
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- This file contains project properties used by the build. -->
<Project>
  <PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
    <DisableCopyToPlugins>true</DisableCopyToPlugins>
    <DisableZipRelease>true</DisableZipRelease>
  </PropertyGroup>
  <ItemGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
    <SourceRoot Include="$(MSBuildThisFileDirectory)/"/>
  </ItemGroup>
  <PropertyGroup Condition="'$(NCrunch)' == '1'">
    <ContinuousIntegrationBuild>false</ContinuousIntegrationBuild>
    <DisableCopyToPlugins>true</DisableCopyToPlugins>
    <DisableZipRelease>true</DisableZipRelease>
  </PropertyGroup>
</Project>

================================================
FILE: BeatTogether/Directory.Build.targets
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- This file contains the build tasks and targets for verifying the manifest, zipping Release builds,
     and copying the plugin to to your Beat Saber folder. Only edit this if you know what you are doing. -->
<Project>
  <PropertyGroup>
    <BuildTargetsVersion>2.0</BuildTargetsVersion>
    <!--Set this to true if you edit this file to prevent automatic updates-->
    <BuildTargetsModified>false</BuildTargetsModified>
    <!--Output assembly path without extension-->
    <OutputAssemblyName>$(OutputPath)$(AssemblyName)</OutputAssemblyName>
    <!--Path to folder to be zipped. Needs to be relative to the project directory to work without changes to the 'BuildForCI' target.-->
    <ArtifactDestination>$(OutputPath)Final</ArtifactDestination>
    <ErrorOnMismatchedVersions Condition="'$(Configuration)' == 'Release'">True</ErrorOnMismatchedVersions>
  </PropertyGroup>
  <!--Build Targets-->
  <!--Displays a warning if BeatSaberModdingTools.Tasks is not installed.-->
  <Target Name="CheckBSMTInstalled" AfterTargets="BeforeBuild" Condition="'$(BSMTTaskAssembly)' == ''">
    <Warning Text="The BeatSaberModdingTools.Tasks nuget package doesn't seem to be installed, advanced build targets will not work." />
  </Target>
  <!--Runs a build task to get info about the project used by later targets.-->
  <Target Name="GetProjectInfo" AfterTargets="CheckBSMTInstalled" DependsOnTargets="CheckBSMTInstalled" Condition="'$(BSMTTaskAssembly)' != ''">
    <GetManifestInfo FailOnError="$(ErrorOnMismatchedVersions)">
      <Output TaskParameter="PluginVersion" PropertyName="PluginVersion" />
      <Output TaskParameter="GameVersion" PropertyName="GameVersion" />
    </GetManifestInfo>
    <PropertyGroup>
      <AssemblyVersion>$(PluginVersion)</AssemblyVersion>
    </PropertyGroup>
    <!--<GetAssemblyInfo FailOnError="$(ErrorOnMismatchedVersions)" Condition="'$(AssemblyVersion)' == ''">
      <Output TaskParameter="AssemblyVersion" PropertyName="AssemblyVersion" />
    </GetAssemblyInfo>-->
    <CompareVersions PluginVersion="$(PluginVersion)" AssemblyVersion="$(AssemblyVersion)" ErrorOnMismatch="$(ErrorOnMismatchedVersions)" />
    <GetCommitInfo ProjectDir="$(ProjectDir)">
      <Output TaskParameter="CommitHash" PropertyName="CommitHash" />
      <Output TaskParameter="Branch" PropertyName="Branch" />
      <Output TaskParameter="Modified" PropertyName="GitModified" />
    </GetCommitInfo>
    <PropertyGroup>
      <AssemblyVersion>$(PluginVersion)</AssemblyVersion>
      <Version>$(PluginVersion)</Version>
      <!--Build name for artifact/zip file-->
      <ArtifactName>$(AssemblyName)</ArtifactName>
      <ArtifactName Condition="'$(PluginVersion)' != ''">$(ArtifactName)-$(PluginVersion)</ArtifactName>
      <ArtifactName Condition="'$(GameVersion)' != ''">$(ArtifactName)-bs$(GameVersion)</ArtifactName>
      <ArtifactName Condition="'$(CommitHash)' != '' AND '$(CommitHash)' != 'local'">$(ArtifactName)-$(CommitHash)</ArtifactName>
    </PropertyGroup>
  </Target>
  <!--Build target for Continuous Integration builds. Set up for GitHub Actions.-->
  <Target Name="BuildForCI" AfterTargets="Build" DependsOnTargets="GetProjectInfo" Condition="'$(ContinuousIntegrationBuild)' == 'True' AND '$(BSMTTaskAssembly)' != ''">
    <PropertyGroup>
      <!--Set 'ArtifactName' if it failed before.-->
      <ArtifactName Condition="'$(ArtifactName)' == ''">$(AssemblyName)</ArtifactName>
    </PropertyGroup>
    <Message Text="Building for CI" Importance="high" />
    <Message Text="PluginVersion: $(PluginVersion), AssemblyVersion: $(AssemblyVersion), GameVersion: $(GameVersion)" Importance="high" />
    <Message Text="::set-output name=filename::$(ArtifactName)" Importance="high" />
    <Message Text="::set-output name=assemblyname::$(AssemblyName)" Importance="high" />
    <Message Text="::set-output name=artifactpath::$(ProjectDir)$(ArtifactDestination)" Importance="high" />
    <Message Text="Copying '$(OutputAssemblyName).dll' to '$(ProjectDir)$(ArtifactDestination)\Plugins\$(AssemblyName).dll'" Importance="high" />
    <Copy SourceFiles="$(OutputAssemblyName).dll" DestinationFiles="$(ProjectDir)$(ArtifactDestination)\Plugins\$(AssemblyName).dll" />
    <Copy SourceFiles="$(OutputAssemblyName).pdb" DestinationFiles="$(ProjectDir)$(ArtifactDestination)\Plugins\$(AssemblyName).pdb" />
  </Target>
  <!--Creates a BeatMods compliant zip file with the release.-->
  <Target Name="ZipRelease" AfterTargets="Build" Condition="'$(DisableZipRelease)' != 'True' AND '$(Configuration)' == 'Release' AND '$(BSMTTaskAssembly)' != ''">
    <PropertyGroup>
      <!--Set 'ArtifactName' if it failed before.-->
      <ArtifactName Condition="'$(ArtifactName)' == ''">$(AssemblyName)</ArtifactName>
      <DestinationDirectory>$(OutDir)zip\</DestinationDirectory>
    </PropertyGroup>
    <ItemGroup>
      <OldZips Include="$(DestinationDirectory)$(AssemblyName)*.zip"/>
    </ItemGroup>
    <Copy SourceFiles="$(OutputAssemblyName).dll" DestinationFiles="$(ArtifactDestination)\Plugins\$(AssemblyName).dll" />
    <Message Text="PluginVersion: $(PluginVersion), AssemblyVersion: $(AssemblyVersion), GameVersion: $(GameVersion)" Importance="high" />
    <Delete Files="@(OldZips)" TreatErrorsAsWarnings="true" ContinueOnError="true" />
    <ZipDir SourceDirectory="$(ArtifactDestination)" DestinationFile="$(DestinationDirectory)$(ArtifactName).zip" />
  </Target>
  <!--Copies the assembly and pdb to the Beat Saber folder.-->
  <Target Name="CopyToPlugins" AfterTargets="Build" Condition="'$(DisableCopyToPlugins)' != 'True' AND '$(ContinuousIntegrationBuild)' != 'True'">
    <PropertyGroup>
      <PluginDir>$(BeatSaberDir)\Plugins</PluginDir>
      <CanCopyToPlugins>True</CanCopyToPlugins>
      <CopyToPluginsError Condition="!Exists('$(PluginDir)')">Unable to copy assembly to game folder, did you set 'BeatSaberDir' correctly in your 'csproj.user' file? Plugins folder doesn't exist: '$(PluginDir)'.</CopyToPluginsError>
      <!--Error if 'BeatSaberDir' does not have 'Beat Saber.exe'-->
      <CopyToPluginsError Condition="!Exists('$(BeatSaberDir)\Beat Saber.exe')">Unable to copy to Plugins folder, '$(BeatSaberDir)' does not appear to be a Beat Saber game install.</CopyToPluginsError>
      <!--Error if 'BeatSaberDir' is the same as 'LocalRefsDir'-->
      <CopyToPluginsError Condition="'$(BeatSaberDir)' == '$(LocalRefsDir)' OR '$(BeatSaberDir)' == ''">Unable to copy to Plugins folder, 'BeatSaberDir' has not been set in your 'csproj.user' file.</CopyToPluginsError>
      <CanCopyToPlugins Condition="'$(CopyToPluginsError)' != ''">False</CanCopyToPlugins>
    </PropertyGroup>
    <!--Check if Beat Saber is running-->
    <IsProcessRunning ProcessName="Beat Saber" Condition="'$(BSMTTaskAssembly)' != ''">
      <Output TaskParameter="IsRunning" PropertyName="IsRunning" />
    </IsProcessRunning>
    <PropertyGroup>
      <!--If Beat Saber is running, output to the Pending folder-->
      <PluginDir Condition="'$(IsRunning)' == 'True'">$(BeatSaberDir)\IPA\Pending\Plugins</PluginDir>
    </PropertyGroup>
    <Warning Text="$(CopyToPluginsError)" Condition="'$(CopyToPluginsError)' != ''" />
    <Message Text="Copying '$(OutputAssemblyName).dll' to '$(PluginDir)'." Importance="high" Condition="$(CanCopyToPlugins)" />
    <Copy SourceFiles="$(OutputAssemblyName).dll" DestinationFiles="$(PluginDir)\$(AssemblyName).dll" Condition="$(CanCopyToPlugins)"  />
    <Copy SourceFiles="$(OutputAssemblyName).pdb" DestinationFiles="$(PluginDir)\$(AssemblyName).pdb" Condition="'$(CanCopyToPlugins)' == 'True' AND Exists('$(OutputAssemblyName).pdb')"  />
    <Warning Text="Beat Saber is running, restart the game to use the latest build." Condition="'$(IsRunning)' == 'True'" />
  </Target>
</Project>

================================================
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<ServerDetailsRegistry>().AsSingle();
        }
    }
}


================================================
FILE: BeatTogether/Installers/BtMenuInstaller.cs
================================================
using BeatTogether.UI;
using Zenject;

namespace BeatTogether.Installers
{
    class BtMenuInstaller : Installer
    {
        public override void InstallBindings()
        {
            Container.BindInterfacesAndSelfTo<ServerSelectionController>().AsSingle();
        }
    }
}


================================================
FILE: BeatTogether/Models/ServerDetails.cs
================================================
using System;

namespace BeatTogether.Models
{
    public class ServerDetails
    {
        /// <summary>
        /// Display name for UI
        /// </summary>
        public string ServerName { get; set; } = string.Empty;
        /// <summary>
        /// Legacy hostname for master server (no longer used, except for automatic config migrations)
        /// </summary>
        public string HostName { get; set; } = string.Empty;
        /// <summary>
        /// The multiplayer API url / graph url 
        /// </summary>
        public string ApiUrl { get; set; } = string.Empty;
        /// <summary>
        /// Optional status check URL for the server
        /// </summary>
        public string StatusUri { get; set; } = string.Empty;
        /// <summary>
        /// Max amount of players per instance 
        /// </summary>
        public int MaxPartySize { get; set; } = 5;
        /// <summary>
        /// If set: disable SSL and certificate validation for all Ignorance/ENet client connections.
        /// </summary>
        public bool DisableSsl { get; set; } = true;

        public bool IsOfficial => ServerName == Config.OfficialServerName;

        /// <summary>
        /// Gets whether this server matches against a given override URL.
        /// Only checks whether the hostname and port match.
        /// </summary>
        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<Config>();

            _harmony = new Harmony(ID);
            _metadata = metadata;

            zenjector.UseLogger(logger);
            zenjector.Install<BtAppInstaller>(Location.App, config);
            zenjector.Install<BtMenuInstaller>(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<ServerDetails> Servers
            => _config.Servers.Concat(_servers).Append(OfficialServer).ToList();

        private readonly Config _config;
        private readonly List<ServerDetails> _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
================================================
<horizontal>
  <list-setting text='Playing on' id='server-list' options='server-options' value='server' apply-on-change='true' bind-value='true' rich-text='true' overflow-mode='Ellipsis' size-delta-x='-75' anchor-pos-y='0'></list-setting>
</horizontal>

================================================
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<object> _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<CurvedCanvasSettings>().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`).
Download .txt
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
Download .txt
SYMBOL INDEX (40 symbols across 8 files)

FILE: BeatTogether/Config.cs
  class Config (line 9) | public class Config
    method OnReload (line 27) | public virtual void OnReload()
    method CopyFrom (line 55) | public virtual void CopyFrom(Config other)

FILE: BeatTogether/Installers/BtAppInstaller.cs
  class BtAppInstaller (line 6) | class BtAppInstaller : Installer
    method BtAppInstaller (line 10) | public BtAppInstaller(
    method InstallBindings (line 16) | public override void InstallBindings()

FILE: BeatTogether/Installers/BtMenuInstaller.cs
  class BtMenuInstaller (line 6) | class BtMenuInstaller : Installer
    method InstallBindings (line 8) | public override void InstallBindings()

FILE: BeatTogether/Models/ServerDetails.cs
  class ServerDetails (line 5) | public class ServerDetails
    method MatchesApiUrl (line 38) | public bool MatchesApiUrl(string? apiUrl)
    method ToString (line 62) | public override string ToString() => ServerName;

FILE: BeatTogether/Models/TemporaryServerDetails.cs
  class TemporaryServerDetails (line 5) | public class TemporaryServerDetails : ServerDetails
    method TemporaryServerDetails (line 7) | public TemporaryServerDetails(string graphApiUrl, string? statusUrl)

FILE: BeatTogether/Plugin.cs
  class Plugin (line 12) | [Plugin(RuntimeOptions.SingleStartInit)]
    method Plugin (line 19) | [Init]
    method OnEnable (line 32) | [OnEnable]
    method OnDisable (line 38) | [OnDisable]

FILE: BeatTogether/Registries/ServerDetailsRegistry.cs
  class ServerDetailsRegistry (line 8) | public class ServerDetailsRegistry
    method ServerDetailsRegistry (line 28) | internal ServerDetailsRegistry(
    method AddServer (line 34) | public void AddServer(ServerDetails server)
    method SetSelectedServer (line 41) | public void SetSelectedServer(ServerDetails server)

FILE: BeatTogether/UI/ServerSelectionController.cs
  class ServerSelectionController (line 28) | internal class ServerSelectionController : IInitializable, IAffinity, IN...
    method ServerSelectionController (line 53) | internal ServerSelectionController(
    method Initialize (line 74) | public void Initialize()
    method ApplySelectedServer (line 87) | private void ApplySelectedServer(ServerDetails server)
    method SyncSelectedServer (line 102) | private void SyncSelectedServer()
    method ApplyNetworkConfig (line 140) | private void ApplyNetworkConfig(ServerDetails server)
    method SyncTemporarySelectedServer (line 149) | private void SyncTemporarySelectedServer()
    method HandleMpStatusUpdateForUrl (line 173) | private void HandleMpStatusUpdateForUrl(string statusUrl, MpStatusData...
    method DidActivate (line 199) | [AffinityPrefix]
    method PresentMasterServerUnavailableErrorDialog (line 218) | [AffinityPostfix]
    method DidDeactivate (line 225) | [AffinityPrefix]
    method TransitionDidStart (line 233) | [AffinityPostfix]
    method TransitionDidFinish (line 241) | [AffinityPostfix]
    method DoPresentTransition (line 249) | [AffinityPrefix]
    method TopViewControllerWillChange (line 259) | [AffinityPrefix]
    method SetTitle (line 280) | [AffinityPrefix]
    method RefreshSwitchInteractable (line 296) | private void RefreshSwitchInteractable()
    method SetGlobalUserInteraction (line 314) | [AffinityPrefix]
    method OnPropertyChanged (line 328) | [NotifyPropertyChangedInvocator]
Condensed preview — 22 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (57K chars).
[
  {
    "path": ".github/FUNDING.yml",
    "chars": 22,
    "preview": "patreon: BeatTogether\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/Bug_Report.yml",
    "chars": 1857,
    "preview": "name: \"Bug report 🐛\"\ndescription: Report issues, errors or unexpected behavior\ntitle: \"[Bug]: \"\nlabels: [bug]\nassignees:"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "chars": 233,
    "preview": "blank_issues_enabled: true # Enabled for now until all issue templates are added\ncontact_links:\n  - name: BeatTogether C"
  },
  {
    "path": ".github/workflows/Build.yml",
    "chars": 1284,
    "preview": "name: Build\n\non:\n  push:\n    branches: [ master ]\n    paths:\n      - 'BeatTogether.sln'\n      - 'BeatTogether/**'\n      "
  },
  {
    "path": ".github/workflows/PR_Build.yml",
    "chars": 1306,
    "preview": "name: Pull Request Build\n\non:\n  pull_request:\n    branches: [ master ]\n    paths:\n      - 'BeatTogether.sln'\n      - 'Be"
  },
  {
    "path": ".gitignore",
    "chars": 6022,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": "BeatTogether/BeatTogether.csproj",
    "chars": 7093,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Library</Out"
  },
  {
    "path": "BeatTogether/Config.cs",
    "chars": 2253,
    "preview": "using System.Collections.Generic;\nusing System.Linq;\nusing BeatTogether.Models;\nusing IPA.Config.Stores.Attributes;\nusi"
  },
  {
    "path": "BeatTogether/Directory.Build.props",
    "chars": 739,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- This file contains project properties used by the build. -->\n<Project>\n  <P"
  },
  {
    "path": "BeatTogether/Directory.Build.targets",
    "chars": 7801,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- This file contains the build tasks and targets for verifying the manifest, "
  },
  {
    "path": "BeatTogether/Installers/BtAppInstaller.cs",
    "chars": 492,
    "preview": "using BeatTogether.Registries;\nusing Zenject;\n\nnamespace BeatTogether.Installers\n{\n    class BtAppInstaller : Installer"
  },
  {
    "path": "BeatTogether/Installers/BtMenuInstaller.cs",
    "chars": 282,
    "preview": "using BeatTogether.UI;\nusing Zenject;\n\nnamespace BeatTogether.Installers\n{\n    class BtMenuInstaller : Installer\n    {\n"
  },
  {
    "path": "BeatTogether/Models/ServerDetails.cs",
    "chars": 2074,
    "preview": "using System;\n\nnamespace BeatTogether.Models\n{\n    public class ServerDetails\n    {\n        /// <summary>\n        /// D"
  },
  {
    "path": "BeatTogether/Models/TemporaryServerDetails.cs",
    "chars": 727,
    "preview": "using System;\n\nnamespace BeatTogether.Models\n{\n    public class TemporaryServerDetails : ServerDetails\n    {\n        pu"
  },
  {
    "path": "BeatTogether/Plugin.cs",
    "chars": 1121,
    "preview": "using BeatTogether.Installers;\nusing HarmonyLib;\nusing IPA;\nusing IPA.Config.Stores;\nusing IPA.Loader;\nusing SiraUtil.Z"
  },
  {
    "path": "BeatTogether/Registries/ServerDetailsRegistry.cs",
    "chars": 1722,
    "preview": "using BeatTogether.Models;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace BeatTogether.R"
  },
  {
    "path": "BeatTogether/UI/ServerSelectionController.bsml",
    "chars": 253,
    "preview": "<horizontal>\n  <list-setting text='Playing on' id='server-list' options='server-options' value='server' apply-on-change"
  },
  {
    "path": "BeatTogether/UI/ServerSelectionController.cs",
    "chars": 13343,
    "preview": "using BeatSaberMarkupLanguage;\r\nusing BeatSaberMarkupLanguage.Attributes;\r\nusing BeatSaberMarkupLanguage.Components.Set"
  },
  {
    "path": "BeatTogether/manifest.json",
    "chars": 489,
    "preview": "{\n  \"$schema\": \"https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json\",\n  \"id\": \"BeatTogeth"
  },
  {
    "path": "BeatTogether.sln",
    "chars": 1115,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.2992"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2020 Pythonology\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 1527,
    "preview": "# BeatTogether\nA multiplayer private server for the modding community. Supports crossplay between PC and Quest. **This i"
  }
]

About this extraction

This page contains the full source code of the pythonology/BeatTogether GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 22 files (51.6 KB), approximately 12.9k tokens, and a symbol index with 40 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!