[
  {
    "path": ".gitattributes",
    "content": "###############################################################################\n# Set default behavior to automatically normalize line endings.\n###############################################################################\n* text=auto\n\n###############################################################################\n# Set default behavior for command prompt diff.\n#\n# This is need for earlier builds of msysgit that does not have it on by\n# default for csharp files.\n# Note: This is only used by command line\n###############################################################################\n#*.cs     diff=csharp\n\n###############################################################################\n# Set the merge driver for project and solution files\n#\n# Merging from the command prompt will add diff markers to the files if there\n# are conflicts (Merging from VS is not affected by the settings below, in VS\n# the diff markers are never inserted). Diff markers may cause the following \n# file extensions to fail to load in VS. An alternative would be to treat\n# these files as binary and thus will always conflict and require user\n# intervention with every merge. To do so, just uncomment the entries below\n###############################################################################\n#*.sln       merge=binary\n#*.csproj    merge=binary\n#*.vbproj    merge=binary\n#*.vcxproj   merge=binary\n#*.vcproj    merge=binary\n#*.dbproj    merge=binary\n#*.fsproj    merge=binary\n#*.lsproj    merge=binary\n#*.wixproj   merge=binary\n#*.modelproj merge=binary\n#*.sqlproj   merge=binary\n#*.wwaproj   merge=binary\n\n###############################################################################\n# behavior for image files\n#\n# image files are treated as binary by default.\n###############################################################################\n#*.jpg   binary\n#*.png   binary\n#*.gif   binary\n\n###############################################################################\n# diff behavior for common document formats\n# \n# Convert binary document formats to text before diffing them. This feature\n# is only available from the command line. Turn it on by uncommenting the \n# entries below.\n###############################################################################\n#*.doc   diff=astextplain\n#*.DOC   diff=astextplain\n#*.docx  diff=astextplain\n#*.DOCX  diff=astextplain\n#*.dot   diff=astextplain\n#*.DOT   diff=astextplain\n#*.pdf   diff=astextplain\n#*.PDF   diff=astextplain\n#*.rtf   diff=astextplain\n#*.RTF   diff=astextplain\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "patreon: goobwabber\ncustom: ['https://ko-fi.com/goobwabber', 'https://ko-fi.com/zingabopp']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "content": "---\nname: Bug report\nabout: Create a bug report\ntitle: \"[BUG] \"\nlabels: ''\nassignees: ''\n\n---\n**Multiplayer Extensions Version and Download Source**\n<!--- i.e. v0.4.3 from Mod Assistant -->\n\n**Your Platform**\n<!--- PC/Quest, Steam/Oculus store. Also include other information such as if you're using Revive or Oculus Link, custom launch options. -->\n\n**Describe the bug**\n<!--- A clear and concise description of what the bug is. -->\n\n**To Reproduce**\n<!--- Steps to reproduce the behavior: -->\n1. Go to '...'\n2. Click on '....'\n3. Scroll down to '....'\n4. See error\n\n**Expected behavior**\n<!--- A clear and concise description of what you expected to happen. -->\n\n**Log**\n<!--- The log file from the game session the issue occurred (restarting the game creates a new log file). \nThe log file can be found at `Beat Saber\\Logs\\_latest.log` (`Beat Saber` being the folder `Beat Saber.exe` is in). You can drag-and-drop it into the Issue. -->\n\n**Screenshots/Video**\n<!--- If applicable, add screenshots to help explain your problem. -->\n\n**Additional context**\n<!--- Add any other context about the problem here. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "content": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[FEATURE] \"\nlabels: ''\nassignees: ''\n\n---\n\n**Is your feature request related to a problem? Please describe.**\n<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->\n\n**Describe the solution you'd like**\n<!--- A clear and concise description of what you want to happen. -->\n\n**Additional context**\n<!--- Add any other context or screenshots about the feature request here. -->\n"
  },
  {
    "path": ".github/workflows/Build.yml",
    "content": "name: Build\n\non:\n  push:\n    branches: [ master ]\n    paths:\n      - 'MultiplayerExtensions.sln'\n      - 'MultiplayerExtensions/**'\n      - '.github/workflows/Build.yml'\n\njobs:\n  Build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - name: Setup dotnet\n      uses: actions/setup-dotnet@v1\n      with:\n        dotnet-version: 5.0.x\n    - name: Fetch SIRA References\n      uses: ProjectSIRA/download-sira-stripped@1.0.0\n      with:\n        manifest: ./MultiplayerExtensions/manifest.json\n        sira-server-code: ${{ secrets.SIRA_SERVER_CODE }}\n    - name: Fetch Mod References\n      uses: Goobwabber/download-beatmods-deps@1.1\n      with:\n        manifest: ./MultiplayerExtensions/manifest.json\n    - name: Build\n      id: Build\n      env: \n        FrameworkPathOverride: /usr/lib/mono/4.8-api\n      run: dotnet build --configuration Release\n    - name: GitStatus\n      run: git status\n    - name: Echo Filename\n      run: echo $BUILDTEXT \\($ASSEMBLYNAME\\)\n      env:\n        BUILDTEXT: Filename=${{ steps.Build.outputs.filename }}\n        ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }}\n    - name: Upload Artifact\n      uses: actions/upload-artifact@v1\n      with:\n        name: ${{ steps.Build.outputs.filename }}\n        path: ${{ steps.Build.outputs.artifactpath }}\n"
  },
  {
    "path": ".github/workflows/PR_Build.yml",
    "content": "name: Pull Request Build\n\non:\n  pull_request:\n    branches: [ main ]\n    paths:\n      - 'MultiplayerExtensions.sln'\n      - 'MultiplayerExtensions/**'\n      - '.github/workflows/PR_Build.yml'\n\njobs:\n  Build:\n    runs-on: ubuntu-latest\n    steps:\n    - uses: actions/checkout@v2\n    - name: Setup dotnet\n      uses: actions/setup-dotnet@v1\n      with:\n        dotnet-version: 5.0.x\n    - name: Fetch SIRA References\n      uses: ProjectSIRA/download-sira-stripped@1.0.0\n      with:\n        manifest: ./MultiplayerExtensions/manifest.json\n        sira-server-code: ${{ secrets.SIRA_SERVER_CODE }}\n    - name: Fetch Mod References\n      uses: Goobwabber/download-beatmods-deps@1.1\n      with:\n        manifest: ./MultiplayerExtensions/manifest.json\n    - name: Build\n      id: Build\n      env: \n        FrameworkPathOverride: /usr/lib/mono/4.8-api\n      run: dotnet build --configuration Release\n    - name: GitStatus\n      run: git status\n    - name: Echo Filename\n      run: echo $BUILDTEXT \\($ASSEMBLYNAME\\)\n      env:\n        BUILDTEXT: Filename=${{ steps.Build.outputs.filename }}\n        ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }}\n    - name: Upload Artifact\n      uses: actions/upload-artifact@v1\n      with:\n        name: ${{ steps.Build.outputs.filename }}\n        path: ${{ steps.Build.outputs.artifactpath }}\n"
  },
  {
    "path": ".gitignore",
    "content": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore\n\n# User-specific files\n*.rsuser\n*.suo\n*.user\n*.userosscache\n*.sln.docstates\n\n# User-specific files (MonoDevelop/Xamarin Studio)\n*.userprefs\n\n# Build results\n[Dd]ebug/\n[Dd]ebugPublic/\n[Rr]elease/\n[Rr]eleases/\nx64/\nx86/\n[Aa][Rr][Mm]/\n[Aa][Rr][Mm]64/\nbld/\n[Bb]in/\n[Oo]bj/\n[Ll]og/\n\n# Visual Studio 2015/2017 cache/options directory\n.vs/\n# Uncomment if you have tasks that create the project's static files in wwwroot\n#wwwroot/\n\n# Visual Studio 2017 auto generated files\nGenerated\\ Files/\n\n# MSTest test Results\n[Tt]est[Rr]esult*/\n[Bb]uild[Ll]og.*\n\n# NUNIT\n*.VisualState.xml\nTestResult.xml\n\n# Build Results of an ATL Project\n[Dd]ebugPS/\n[Rr]eleasePS/\ndlldata.c\n\n# Benchmark Results\nBenchmarkDotNet.Artifacts/\n\n# .NET Core\nproject.lock.json\nproject.fragment.lock.json\nartifacts/\n\n# StyleCop\nStyleCopReport.xml\n\n# Files built by Visual Studio\n*_i.c\n*_p.c\n*_h.h\n*.ilk\n*.meta\n*.obj\n*.iobj\n*.pch\n*.pdb\n*.ipdb\n*.pgc\n*.pgd\n*.rsp\n*.sbr\n*.tlb\n*.tli\n*.tlh\n*.tmp\n*.tmp_proj\n*_wpftmp.csproj\n*.log\n*.vspscc\n*.vssscc\n.builds\n*.pidb\n*.svclog\n*.scc\n\n# Chutzpah Test files\n_Chutzpah*\n\n# Visual C++ cache files\nipch/\n*.aps\n*.ncb\n*.opendb\n*.opensdf\n*.sdf\n*.cachefile\n*.VC.db\n*.VC.VC.opendb\n\n# Visual Studio profiler\n*.psess\n*.vsp\n*.vspx\n*.sap\n\n# Visual Studio Trace Files\n*.e2e\n\n# TFS 2012 Local Workspace\n$tf/\n\n# Guidance Automation Toolkit\n*.gpState\n\n# ReSharper is a .NET coding add-in\n_ReSharper*/\n*.[Rr]e[Ss]harper\n*.DotSettings.user\n\n# JustCode is a .NET coding add-in\n.JustCode\n\n# TeamCity is a build add-in\n_TeamCity*\n\n# DotCover is a Code Coverage Tool\n*.dotCover\n\n# AxoCover is a Code Coverage Tool\n.axoCover/*\n!.axoCover/settings.json\n\n# Visual Studio code coverage results\n*.coverage\n*.coveragexml\n\n# NCrunch\n_NCrunch_*\n.*crunch*.local.xml\nnCrunchTemp_*\n\n# MightyMoose\n*.mm.*\nAutoTest.Net/\n\n# Web workbench (sass)\n.sass-cache/\n\n# Installshield output folder\n[Ee]xpress/\n\n# DocProject is a documentation generator add-in\nDocProject/buildhelp/\nDocProject/Help/*.HxT\nDocProject/Help/*.HxC\nDocProject/Help/*.hhc\nDocProject/Help/*.hhk\nDocProject/Help/*.hhp\nDocProject/Help/Html2\nDocProject/Help/html\n\n# Click-Once directory\npublish/\n\n# Publish Web Output\n*.[Pp]ublish.xml\n*.azurePubxml\n# Note: Comment the next line if you want to checkin your web deploy settings,\n# but database connection strings (with potential passwords) will be unencrypted\n*.pubxml\n*.publishproj\n\n# Microsoft Azure Web App publish settings. Comment the next line if you want to\n# checkin your Azure Web App publish settings, but sensitive information contained\n# in these scripts will be unencrypted\nPublishScripts/\n\n# NuGet Packages\n*.nupkg\n# The packages folder can be ignored because of Package Restore\n**/[Pp]ackages/*\n# except build/, which is used as an MSBuild target.\n!**/[Pp]ackages/build/\n# Uncomment if necessary however generally it will be regenerated when needed\n#!**/[Pp]ackages/repositories.config\n# NuGet v3's project.json files produces more ignorable files\n*.nuget.props\n*.nuget.targets\n\n# Microsoft Azure Build Output\ncsx/\n*.build.csdef\n\n# Microsoft Azure Emulator\necf/\nrcf/\n\n# Windows Store app package directories and files\nAppPackages/\nBundleArtifacts/\nPackage.StoreAssociation.xml\n_pkginfo.txt\n*.appx\n\n# Visual Studio cache files\n# files ending in .cache can be ignored\n*.[Cc]ache\n# but keep track of directories ending in .cache\n!?*.[Cc]ache/\n\n# Others\nClientBin/\n~$*\n*~\n*.dbmdl\n*.dbproj.schemaview\n*.jfm\n*.pfx\n*.publishsettings\norleans.codegen.cs\n\n# Including strong name files can present a security risk\n# (https://github.com/github/gitignore/pull/2483#issue-259490424)\n#*.snk\n\n# Since there are multiple workflows, uncomment next line to ignore bower_components\n# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)\n#bower_components/\n\n# RIA/Silverlight projects\nGenerated_Code/\n\n# Backup & report files from converting an old project file\n# to a newer Visual Studio version. Backup files are not needed,\n# because we have git ;-)\n_UpgradeReport_Files/\nBackup*/\nUpgradeLog*.XML\nUpgradeLog*.htm\nServiceFabricBackup/\n*.rptproj.bak\n\n# SQL Server files\n*.mdf\n*.ldf\n*.ndf\n\n# Business Intelligence projects\n*.rdl.data\n*.bim.layout\n*.bim_*.settings\n*.rptproj.rsuser\n*- Backup*.rdl\n\n# Microsoft Fakes\nFakesAssemblies/\n\n# GhostDoc plugin setting file\n*.GhostDoc.xml\n\n# Node.js Tools for Visual Studio\n.ntvs_analysis.dat\nnode_modules/\n\n# Visual Studio 6 build log\n*.plg\n\n# Visual Studio 6 workspace options file\n*.opt\n\n# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)\n*.vbw\n\n# Visual Studio LightSwitch build output\n**/*.HTMLClient/GeneratedArtifacts\n**/*.DesktopClient/GeneratedArtifacts\n**/*.DesktopClient/ModelManifest.xml\n**/*.Server/GeneratedArtifacts\n**/*.Server/ModelManifest.xml\n_Pvt_Extensions\n\n# Paket dependency manager\n.paket/paket.exe\npaket-files/\n\n# FAKE - F# Make\n.fake/\n\n# JetBrains Rider\n.idea/\n*.sln.iml\n\n# CodeRush personal settings\n.cr/personal\n\n# Python Tools for Visual Studio (PTVS)\n__pycache__/\n*.pyc\n\n# Cake - Uncomment if you are using it\n# tools/**\n# !tools/packages.config\n\n# Tabs Studio\n*.tss\n\n# Telerik's JustMock configuration file\n*.jmconfig\n\n# BizTalk build output\n*.btp.cs\n*.btm.cs\n*.odx.cs\n*.xsd.cs\n\n# OpenCover UI analysis results\nOpenCover/\n\n# Azure Stream Analytics local run output\nASALocalRun/\n\n# MSBuild Binary and Structured Log\n*.binlog\n\n# NVidia Nsight GPU debugger configuration file\n*.nvuser\n\n# MFractors (Xamarin productivity tool) working folder\n.mfractor/\n\n# Local History for Visual Studio\n.localhistory/\n\n# BeatPulse healthcheck temp database\nhealthchecksdb\n\nRefs/Beat Saber_Data/Managed/*\nRefs/Plugins/*\nRefs/Libs/Mono*\n!Refs/Beat Saber_Data/Managed/IPA.Loader.dll\n/bsinstalldir.txt\n\nMultiplayerExtensions/Properties/launchSettings.json\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2020 Zingabopp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\nAs an exception, all contents of the \"Refs\" directory, or any subdirectory named\n\"Refs\", are not part of the Software and are therefore not subject to the License,\nand remain the exclusive property of their copyright holders."
  },
  {
    "path": "MultiplayerExtensions/Config.cs",
    "content": "using IPA.Config.Stores.Attributes;\nusing MultiplayerExtensions.Utilities;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions\n{\n    public class Config\n    {\n        public static readonly Color DefaultPlayerColor = new Color(0.031f, 0.752f, 1f);\n\n        public virtual bool SoloEnvironment { get; set; } = false;\n        public virtual bool SideBySide { get; set; } = false;\n        public virtual float SideBySideDistance { get; set; } = 4f;\n        public virtual bool DisableAvatarConstraints { get; set; } = false;\n        public virtual bool DisableMultiplayerPlatforms { get; set; } = false;\n        public virtual bool DisableMultiplayerLights { get; set; } = false;\n        public virtual bool DisableMultiplayerObjects { get; set; } = false;\n        public virtual bool DisableMultiplayerColors { get; set; } = false;\n        public virtual bool DisablePlatformMovement { get; set; } = false;\n        public virtual bool MissLighting { get; set; } = false;\n        public virtual bool PersonalMissLightingOnly { get; set; } = false;\n        [UseConverter(typeof(ColorConverter))]\n        public virtual Color PlayerColor { get; set; } = DefaultPlayerColor;\n        [UseConverter(typeof(ColorConverter))]\n        public virtual Color MissColor { get; set; } = new Color(1, 0, 0);\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Directory.Build.props",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- This file contains project properties used by the build. -->\n<Project>\n  <PropertyGroup Condition=\"'$(GITHUB_ACTIONS)' == 'true'\">\n    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>\n    <DisableCopyToPlugins>true</DisableCopyToPlugins>\n    <DisableZipRelease>true</DisableZipRelease>\n  </PropertyGroup>\n  <ItemGroup Condition=\"'$(GITHUB_ACTIONS)' == 'true'\">\n    <SourceRoot Include=\"$(MSBuildThisFileDirectory)/\"/>\n  </ItemGroup>\n  <PropertyGroup Condition=\"'$(NCrunch)' == '1'\">\n    <ContinuousIntegrationBuild>false</ContinuousIntegrationBuild>\n    <DisableCopyToPlugins>true</DisableCopyToPlugins>\n    <DisableZipRelease>true</DisableZipRelease>\n  </PropertyGroup>\n</Project>"
  },
  {
    "path": "MultiplayerExtensions/Directory.Build.targets",
    "content": "﻿<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- This file contains the build tasks and targets for verifying the manifest, zipping Release builds,\n     and copying the plugin to to your Beat Saber folder. Only edit this if you know what you are doing. -->\n<Project>\n  <PropertyGroup>\n    <BuildTargetsVersion>2.0</BuildTargetsVersion>\n    <!--Set this to true if you edit this file to prevent automatic updates-->\n    <BuildTargetsModified>false</BuildTargetsModified>\n    <!--Output assembly path without extension-->\n    <OutputAssemblyName>$(OutputPath)$(AssemblyName)</OutputAssemblyName>\n    <!--Path to folder to be zipped. Needs to be relative to the project directory to work without changes to the 'BuildForCI' target.-->\n    <ArtifactDestination>$(OutputPath)Final</ArtifactDestination>\n    <ErrorOnMismatchedVersions Condition=\"'$(Configuration)' == 'Release'\">True</ErrorOnMismatchedVersions>\n  </PropertyGroup>\n  <!--Build Targets-->\n  <!--Displays a warning if BeatSaberModdingTools.Tasks is not installed.-->\n  <Target Name=\"CheckBSMTInstalled\" AfterTargets=\"BeforeBuild\" Condition=\"'$(BSMTTaskAssembly)' == ''\">\n    <Warning Text=\"The BeatSaberModdingTools.Tasks nuget package doesn't seem to be installed, advanced build targets will not work.\" />\n  </Target>\n  <!--Runs a build task to get info about the project used by later targets.-->\n  <Target Name=\"GetProjectInfo\" AfterTargets=\"CheckBSMTInstalled\" DependsOnTargets=\"CheckBSMTInstalled\" Condition=\"'$(BSMTTaskAssembly)' != ''\">\n    <GetManifestInfo FailOnError=\"$(ErrorOnMismatchedVersions)\">\n      <Output TaskParameter=\"PluginVersion\" PropertyName=\"PluginVersion\" />\n      <Output TaskParameter=\"GameVersion\" PropertyName=\"GameVersion\" />\n    </GetManifestInfo>\n    <PropertyGroup>\n      <AssemblyVersion>$(PluginVersion)</AssemblyVersion>\n    </PropertyGroup>\n    <!--<GetAssemblyInfo FailOnError=\"$(ErrorOnMismatchedVersions)\" Condition=\"'$(AssemblyVersion)' == ''\">\n      <Output TaskParameter=\"AssemblyVersion\" PropertyName=\"AssemblyVersion\" />\n    </GetAssemblyInfo>-->\n    <CompareVersions PluginVersion=\"$(PluginVersion)\" AssemblyVersion=\"$(AssemblyVersion)\" ErrorOnMismatch=\"$(ErrorOnMismatchedVersions)\" />\n    <GetCommitInfo ProjectDir=\"$(ProjectDir)\">\n      <Output TaskParameter=\"CommitHash\" PropertyName=\"CommitHash\" />\n      <Output TaskParameter=\"Branch\" PropertyName=\"Branch\" />\n      <Output TaskParameter=\"Modified\" PropertyName=\"GitModified\" />\n    </GetCommitInfo>\n    <PropertyGroup>\n      <AssemblyVersion>$(PluginVersion)</AssemblyVersion>\n      <Version>$(PluginVersion)</Version>\n      <!--Build name for artifact/zip file-->\n      <ArtifactName>$(AssemblyName)</ArtifactName>\n      <ArtifactName Condition=\"'$(PluginVersion)' != ''\">$(ArtifactName)-$(PluginVersion)</ArtifactName>\n      <ArtifactName Condition=\"'$(GameVersion)' != ''\">$(ArtifactName)-bs$(GameVersion)</ArtifactName>\n      <ArtifactName Condition=\"'$(CommitHash)' != '' AND '$(CommitHash)' != 'local'\">$(ArtifactName)-$(CommitHash)</ArtifactName>\n    </PropertyGroup>\n  </Target>\n  <!--Build target for Continuous Integration builds. Set up for GitHub Actions.-->\n  <Target Name=\"BuildForCI\" AfterTargets=\"Build\" DependsOnTargets=\"GetProjectInfo\" Condition=\"'$(ContinuousIntegrationBuild)' == 'True' AND '$(BSMTTaskAssembly)' != ''\">\n    <PropertyGroup>\n      <!--Set 'ArtifactName' if it failed before.-->\n      <ArtifactName Condition=\"'$(ArtifactName)' == ''\">$(AssemblyName)</ArtifactName>\n    </PropertyGroup>\n    <Message Text=\"Building for CI\" Importance=\"high\" />\n    <Message Text=\"PluginVersion: $(PluginVersion), AssemblyVersion: $(AssemblyVersion), GameVersion: $(GameVersion)\" Importance=\"high\" />\n    <Message Text=\"::set-output name=filename::$(ArtifactName)\" Importance=\"high\" />\n    <Message Text=\"::set-output name=assemblyname::$(AssemblyName)\" Importance=\"high\" />\n    <Message Text=\"::set-output name=artifactpath::$(ProjectDir)$(ArtifactDestination)\" Importance=\"high\" />\n    <Message Text=\"Copying '$(OutputAssemblyName).dll' to '$(ProjectDir)$(ArtifactDestination)\\Plugins\\$(AssemblyName).dll'\" Importance=\"high\" />\n    <Copy SourceFiles=\"$(OutputAssemblyName).dll\" DestinationFiles=\"$(ProjectDir)$(ArtifactDestination)\\Plugins\\$(AssemblyName).dll\" />\n    <Copy SourceFiles=\"$(OutputAssemblyName).pdb\" DestinationFiles=\"$(ProjectDir)$(ArtifactDestination)\\Plugins\\$(AssemblyName).pdb\" />\n  </Target>\n  <!--Creates a BeatMods compliant zip file with the release.-->\n  <Target Name=\"ZipRelease\" AfterTargets=\"Build\" Condition=\"'$(DisableZipRelease)' != 'True' AND '$(Configuration)' == 'Release' AND '$(BSMTTaskAssembly)' != ''\">\n    <PropertyGroup>\n      <!--Set 'ArtifactName' if it failed before.-->\n      <ArtifactName Condition=\"'$(ArtifactName)' == ''\">$(AssemblyName)</ArtifactName>\n      <DestinationDirectory>$(OutDir)zip\\</DestinationDirectory>\n    </PropertyGroup>\n    <ItemGroup>\n      <OldZips Include=\"$(DestinationDirectory)$(AssemblyName)*.zip\"/>\n    </ItemGroup>\n    <Copy SourceFiles=\"$(OutputAssemblyName).dll\" DestinationFiles=\"$(ArtifactDestination)\\Plugins\\$(AssemblyName).dll\" />\n    <Message Text=\"PluginVersion: $(PluginVersion), AssemblyVersion: $(AssemblyVersion), GameVersion: $(GameVersion)\" Importance=\"high\" />\n    <Delete Files=\"@(OldZips)\" TreatErrorsAsWarnings=\"true\" ContinueOnError=\"true\" />\n    <ZipDir SourceDirectory=\"$(ArtifactDestination)\" DestinationFile=\"$(DestinationDirectory)$(ArtifactName).zip\" />\n  </Target>\n  <!--Copies the assembly and pdb to the Beat Saber folder.-->\n  <Target Name=\"CopyToPlugins\" AfterTargets=\"Build\" Condition=\"'$(DisableCopyToPlugins)' != 'True' AND '$(ContinuousIntegrationBuild)' != 'True'\">\n    <PropertyGroup>\n      <PluginDir>$(BeatSaberDir)\\Plugins</PluginDir>\n      <CanCopyToPlugins>True</CanCopyToPlugins>\n      <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>\n      <!--Error if 'BeatSaberDir' does not have 'Beat Saber.exe'-->\n      <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>\n      <!--Error if 'BeatSaberDir' is the same as 'LocalRefsDir'-->\n      <CopyToPluginsError Condition=\"'$(BeatSaberDir)' == '$(LocalRefsDir)' OR '$(BeatSaberDir)' == ''\">Unable to copy to Plugins folder, 'BeatSaberDir' has not been set in your 'csproj.user' file.</CopyToPluginsError>\n      <CanCopyToPlugins Condition=\"'$(CopyToPluginsError)' != ''\">False</CanCopyToPlugins>\n    </PropertyGroup>\n    <!--Check if Beat Saber is running-->\n    <IsProcessRunning ProcessName=\"Beat Saber\" Condition=\"'$(BSMTTaskAssembly)' != ''\">\n      <Output TaskParameter=\"IsRunning\" PropertyName=\"IsRunning\" />\n    </IsProcessRunning>\n    <PropertyGroup>\n      <!--If Beat Saber is running, output to the Pending folder-->\n      <PluginDir Condition=\"'$(IsRunning)' == 'True'\">$(BeatSaberDir)\\IPA\\Pending\\Plugins</PluginDir>\n    </PropertyGroup>\n    <Warning Text=\"$(CopyToPluginsError)\" Condition=\"'$(CopyToPluginsError)' != ''\" />\n    <Message Text=\"Copying '$(OutputAssemblyName).dll' to '$(PluginDir)'.\" Importance=\"high\" Condition=\"$(CanCopyToPlugins)\" />\n    <Copy SourceFiles=\"$(OutputAssemblyName).dll\" DestinationFiles=\"$(PluginDir)\\$(AssemblyName).dll\" Condition=\"$(CanCopyToPlugins)\"  />\n    <Copy SourceFiles=\"$(OutputAssemblyName).pdb\" DestinationFiles=\"$(PluginDir)\\$(AssemblyName).pdb\" Condition=\"'$(CanCopyToPlugins)' == 'True' AND Exists('$(OutputAssemblyName).pdb')\"  />\n    <Warning Text=\"Beat Saber is running, restart the game to use the latest build.\" Condition=\"'$(IsRunning)' == 'True'\" />\n  </Target>\n</Project>"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexAvatarNameTag.cs",
    "content": "﻿using System;\nusing System.Collections.Generic;\nusing HMUI;\nusing MultiplayerCore.Players;\nusing MultiplayerExtensions.Players;\nusing MultiplayerExtensions.Utilities;\nusing UnityEngine;\nusing UnityEngine.UI;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Environments.Lobby\n{\n    public class MpexAvatarNameTag : MonoBehaviour\n    {\n        enum PlayerIconSlot\n        {\n            Platform = 0\n        }\n        \n        private readonly Dictionary<PlayerIconSlot, ImageView> _playerIcons = new();\n\n        private IConnectedPlayer _player = null!;\n        private MpPlayerManager _playerManager = null!;\n        private MpexPlayerManager _mpexPlayerManager = null!;\n        private SpriteManager _spriteManager = null!;\n        private ImageView _bg = null!;\n        private CurvedTextMeshPro _nameText = null!;\n\n        [Inject]\n        internal void Construct(\n            IConnectedPlayer player,\n            MpPlayerManager playerManager,\n            MpexPlayerManager mpexPlayerManager,\n            SpriteManager spriteManager)\n        {\n            _player = player;\n            _playerManager = playerManager;\n            _mpexPlayerManager = mpexPlayerManager;\n            _spriteManager = spriteManager;\n        }\n\n        private void Awake()\n        {\n            // Get references\n            _bg = transform.Find(\"BG\").GetComponent<ImageView>();\n            _nameText = transform.Find(\"Name\").GetComponent<CurvedTextMeshPro>();\n            \n            // Enable horizontal layout on bg\n            if (!_bg.TryGetComponent<HorizontalLayoutGroup>(out _))\n            {\n                var hLayout = _bg.gameObject.AddComponent<HorizontalLayoutGroup>();\n                hLayout.childAlignment = TextAnchor.MiddleCenter;\n                hLayout.childForceExpandWidth = false;\n                hLayout.childForceExpandHeight = false;\n                hLayout.childScaleWidth = false;\n                hLayout.childScaleHeight = false;\n                hLayout.spacing = 4f;\n            }\n\n            // Re-nest name onto bg\n            _nameText.transform.SetParent(_bg.transform, false);\n            \n            // Take control of name tag\n            if (_nameText.TryGetComponent<ConnectedPlayerName>(out var nativeNameScript))\n                Destroy(nativeNameScript);\n            _nameText.text = \"Player\";\n\n            // Set player data\n            _nameText.text = _player.userName;\n            _nameText.color = Color.white;\n            if (_mpexPlayerManager.TryGetPlayer(_player.userId, out var mpexData))\n                _nameText.color = mpexData.Color;\n            if (_playerManager.TryGetPlayer(_player.userId, out var data))\n                SetPlatformData(data);\n        }\n\n        private void OnEnable()\n        {\n            _playerManager.PlayerConnectedEvent += HandlePlatformData;\n            _mpexPlayerManager.PlayerConnectedEvent += HandleMpexData;\n        }\n\n        private void OnDisable()\n        {\n            _playerManager.PlayerConnectedEvent -= HandlePlatformData;\n            _mpexPlayerManager.PlayerConnectedEvent -= HandleMpexData;\n        }\n\n        private void HandlePlatformData(IConnectedPlayer player, MpPlayerData data)\n        {\n            if (player == _player)\n                SetPlatformData(data);\n        }\n\n        private void HandleMpexData(IConnectedPlayer player, MpexPlayerData data)\n        {\n            if (player == _player)\n                _nameText.color = data.Color;\n        }\n\n        private void SetPlatformData(MpPlayerData data)\n        {\n            Sprite icon = null;\n            switch (data.Platform)\n            {\n                case Platform.Steam:\n                    icon = _spriteManager.IconSteam64;\n                    break;\n                case Platform.OculusQuest:\n                    icon = _spriteManager.IconMeta64;\n                    break;\n                case Platform.OculusPC:\n                    icon = _spriteManager.IconOculus64;\n                    break;\n                default:\n                    icon = _spriteManager.IconToaster64;\n                    break;\n            }\n            SetIcon(PlayerIconSlot.Platform, icon);\n        }\n\n        private void SetIcon(PlayerIconSlot slot, Sprite sprite)\n        {\n            if (!_playerIcons.TryGetValue(slot, out ImageView imageView))\n            {\n                var iconObj = new GameObject($\"MpexPlayerIcon({slot})\");\n                iconObj.transform.SetParent(_bg.transform, false);\n                iconObj.transform.SetSiblingIndex((int)slot);\n                iconObj.layer = 5;\n\n                iconObj.AddComponent<CanvasRenderer>();\n                \n                imageView = iconObj.AddComponent<ImageView>();\n                imageView.maskable = true;\n                imageView.fillCenter = true;\n                imageView.preserveAspect = true;\n                imageView.material = _bg.material; // No Glow Billboard material\n                _playerIcons[slot] = imageView;\n\n                var rectTransform = iconObj.GetComponent<RectTransform>();\n                rectTransform.localScale = new Vector3(3.2f, 3.2f);\n            }\n\n            imageView.sprite = sprite;\n            \n            _nameText.transform.SetSiblingIndex(999);\n        }\n    }\n}"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexAvatarPlaceLighting.cs",
    "content": "﻿using IPA.Utilities;\nusing MultiplayerExtensions.Players;\nusing System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Environments\n{\n    public class MpexAvatarPlaceLighting : MonoBehaviour\n    {\n        public const float SmoothTime = 2f;\n        public Color TargetColor { get; private set; } = Color.black;\n        public int SortIndex { get; internal set; }\n\n        private List<TubeBloomPrePassLight> _lights = new List<TubeBloomPrePassLight>();\n\n        private IMultiplayerSessionManager _sessionManager = null!;\n        private MenuLightsManager _lightsManager = null!;\n        private MpexPlayerManager _mpexPlayerManager = null!;\n        private Config _config = null!;\n\n        [Inject]\n        internal void Construct(\n            IMultiplayerSessionManager sessionManager,\n            MenuLightsManager lightsManager,\n            MpexPlayerManager mpexPlayerManager,\n            Config config)\n        {\n            _sessionManager = sessionManager;\n            _lightsManager = lightsManager;\n            _mpexPlayerManager = mpexPlayerManager;\n            _config = config;\n        }\n\n        private void Start()\n        {\n            _lights = GetComponentsInChildren<TubeBloomPrePassLight>().ToList();\n\n            if (_sessionManager == null || _lightsManager == null || _mpexPlayerManager == null || _sessionManager.localPlayer == null)\n                return;\n\n            if (_sessionManager.localPlayer.sortIndex == SortIndex)\n            {\n                SetColor(_config.PlayerColor, true);\n                return;\n            }\n\n            foreach (var player in _sessionManager.connectedPlayers)\n                if (player.sortIndex == SortIndex)\n                {\n                    SetColor(_mpexPlayerManager.GetPlayer(player.userId)?.Color ?? Config.DefaultPlayerColor, true);\n                    return;\n                }\n\n            SetColor(Color.black);\n        }\n\n        private void OnEnable()\n        {\n            _mpexPlayerManager.PlayerConnectedEvent += HandlePlayerData;\n            _sessionManager.playerConnectedEvent += HandlePlayerConnected;\n            _sessionManager.playerDisconnectedEvent += HandlePlayerDisconnected;\n        }\n\n        private void OnDisable()\n        {\n            _mpexPlayerManager.PlayerConnectedEvent -= HandlePlayerData;\n            _sessionManager.playerConnectedEvent -= HandlePlayerConnected;\n            _sessionManager.playerDisconnectedEvent -= HandlePlayerDisconnected;\n        }\n\n        private void HandlePlayerData(IConnectedPlayer player, MpexPlayerData data)\n        {\n            if (player.sortIndex == SortIndex)\n                SetColor(data.Color, false);\n        }\n\n        private void HandlePlayerConnected(IConnectedPlayer player)\n        {\n            if (player.sortIndex != SortIndex)\n                return;\n            if (_mpexPlayerManager.TryGetPlayer(player.userId, out MpexPlayerData data))\n                SetColor(data.Color, false);\n            else\n                SetColor(Config.DefaultPlayerColor, false);\n        }\n\n        private void HandlePlayerDisconnected(IConnectedPlayer player)\n        {\n            if (player.sortIndex == SortIndex)\n                SetColor(Color.black, false);\n        }\n\n        private void Update()\n        {\n            Color current = GetColor();\n            if (current == TargetColor)\n                return;\n            if (_lightsManager.IsColorVeryCloseToColor(current, TargetColor))\n                SetColor(TargetColor);\n            else\n                SetColor(Color.Lerp(current, TargetColor, Time.deltaTime * SmoothTime));\n        }\n\n        public void SetColor(Color color, bool immediate)\n        {\n            TargetColor = color;\n            if (immediate)\n                SetColor(color);\n        }\n\n        public Color GetColor()\n        {\n            if (_lights.Count > 0)\n                return _lights[0].color;\n            return Color.black;\n        }\n\n        private void SetColor(Color color)\n        {\n            foreach(TubeBloomPrePassLight light in _lights)\n            {\n                light.color = color;\n                light.Refresh();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexConnectedObjectManager.cs",
    "content": "﻿using UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Environment\n{\n    public class MpexConnectedObjectManager : MonoBehaviour\n    {\n        private MultiplayerConnectedPlayerSpectatingSpot _playerSpectatingSpot = null!;\n        private IConnectedPlayerBeatmapObjectEventManager _beatmapObjectEventManager = null!;\n        private BeatmapObjectManager _beatmapObjectManager = null!;\n        private Config _config = null!;\n\n        [Inject]\n        internal void Construct(\n            MultiplayerConnectedPlayerSpectatingSpot playerSpectatingSpot,\n            IConnectedPlayerBeatmapObjectEventManager beatmapObjectEventManager,\n            BeatmapObjectManager beatmapObjectManager,\n            Config config)\n        {\n            _playerSpectatingSpot = playerSpectatingSpot;\n            _beatmapObjectEventManager = beatmapObjectEventManager;\n            _beatmapObjectManager = beatmapObjectManager;\n            _config = config;\n        }\n\n        private void Start()\n        {\n            _playerSpectatingSpot.isObservedChangedEvent += HandleIsObservedChangedEvent;\n            if (_config.DisableMultiplayerObjects)\n                _beatmapObjectEventManager.Pause();\n        }\n\n        private void OnDestroy()\n        {\n            if (_playerSpectatingSpot != null)\n                _playerSpectatingSpot.isObservedChangedEvent -= HandleIsObservedChangedEvent;\n        }\n\n        private void HandleIsObservedChangedEvent(bool isObserved)\n        {\n            if (_config.DisableMultiplayerPlatforms)\n                transform.Find(\"Construction\").gameObject.SetActive(isObserved);\n            if (_config.DisableMultiplayerLights)\n                transform.Find(\"Lasers\").gameObject.SetActive(isObserved);\n            if (!_config.DisableMultiplayerObjects)\n                return;\n            if (isObserved)\n            {\n                _beatmapObjectEventManager.Resume();\n                return;\n            }\n            _beatmapObjectEventManager.Pause();\n            _beatmapObjectManager.DissolveAllObjects();\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexLevelEndActions.cs",
    "content": "﻿using SiraUtil.Affinity;\nusing System;\n\nnamespace MultiplayerExtensions.Environment\n{\n    public class MpexLevelEndActions : IAffinity, ILevelEndActions\n    {\n        public event Action levelFailedEvent = null!;\n        public event Action levelFinishedEvent = null!;\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(MultiplayerLocalActivePlayerFacade), \"ReportPlayerDidFinish\")]\n        private void PlayerDidFinish() =>\n            levelFinishedEvent?.Invoke();\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(MultiplayerLocalActivePlayerFacade), \"ReportPlayerNetworkDidFailed\")]\n        private void PlayerDidFail() =>\n            levelFailedEvent?.Invoke();\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexPlayerFacadeLighting.cs",
    "content": "﻿using IPA.Utilities;\nusing System;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Environment\n{\n    class MpexPlayerFacadeLighting : MonoBehaviour\n    {\n        private readonly FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>.Accessor _allLightsAnimators\n            = FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>\n                .GetAccessor(nameof(_allLightsAnimators));\n        private readonly FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>.Accessor _gameplayLightsAnimators\n            = FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>\n                .GetAccessor(nameof(_gameplayLightsAnimators));\n\n        private readonly FieldAccessor<MultiplayerGameplayAnimator, ColorSO>.Accessor _activeLightsColor\n            = FieldAccessor<MultiplayerGameplayAnimator, ColorSO>\n                .GetAccessor(nameof(_activeLightsColor));\n        private readonly FieldAccessor<MultiplayerGameplayAnimator, ColorSO>.Accessor _leadingLightsColor\n            = FieldAccessor<MultiplayerGameplayAnimator, ColorSO>\n                .GetAccessor(nameof(_leadingLightsColor));\n        private readonly FieldAccessor<MultiplayerGameplayAnimator, ColorSO>.Accessor _failedLightsColor\n            = FieldAccessor<MultiplayerGameplayAnimator, ColorSO>\n                .GetAccessor(nameof(_failedLightsColor));\n\n        private LightsAnimator[] _allLights => _allLightsAnimators(ref _gameplayAnimator);\n        private LightsAnimator[] _gameplayLights => _gameplayLightsAnimators(ref _gameplayAnimator);\n\n        private ColorSO _activeColor => _activeLightsColor(ref _gameplayAnimator);\n        private ColorSO _leadingColor => _leadingLightsColor(ref _gameplayAnimator);\n        private ColorSO _failedColor => _failedLightsColor(ref _gameplayAnimator);\n\n        private bool _isLeading = false;\n        private int _highestCombo = 0;\n\n        private IConnectedPlayer _connectedPlayer = null!;\n        private MultiplayerController _multiplayerController = null!;\n        private IScoreSyncStateManager _scoreProvider = null!;\n        private MultiplayerLeadPlayerProvider _leadPlayerProvider = null!;\n        private MultiplayerGameplayAnimator _gameplayAnimator = null!;\n        private MultiplayerSyncState<StandardScoreSyncState, StandardScoreSyncState.Score, int> _syncState = null!;\n        private Config _config = null!;\n\n        [Inject]\n        internal void Construct(\n            IConnectedPlayer connectedPlayer, \n            MultiplayerController multiplayerController, \n            IScoreSyncStateManager scoreProvider, \n            MultiplayerLeadPlayerProvider leadPlayerProvider,\n            Config config)\n        {\n            _connectedPlayer = connectedPlayer;\n            _multiplayerController = multiplayerController;\n            _scoreProvider = scoreProvider;\n            _leadPlayerProvider = leadPlayerProvider;\n            _config = config;\n        }\n\n        public void OnEnable()\n        {\n            _gameplayAnimator = GetComponentInChildren<MultiplayerGameplayAnimator>();\n            _syncState = _scoreProvider.GetSyncStateForPlayer(_connectedPlayer);\n            _leadPlayerProvider.newLeaderWasSelectedEvent += HandleNewLeaderWasSelected;\n        }\n\n        public void OnDisable()\n        {\n            _leadPlayerProvider.newLeaderWasSelectedEvent -= HandleNewLeaderWasSelected;\n        }\n\n        private void HandleNewLeaderWasSelected(string userId)\n        {\n            _isLeading = userId == _connectedPlayer.userId;\n        }\n\n        private void Update()\n        {\n            if (_multiplayerController.state == MultiplayerController.State.Gameplay\n                && !_connectedPlayer.IsFailed())\n            {\n                int combo = _syncState.GetState(StandardScoreSyncState.Score.Combo, _syncState.player.offsetSyncTime);\n                if (combo > _highestCombo)\n                    _highestCombo = combo;\n\n                Color baseColor = _isLeading ? _leadingColor : _activeColor;\n                float failPercentage = (Mathf.Min(_highestCombo, 20f) - combo) / 20f;\n                Color color = _config.MissColor;\n                color.a = baseColor.a;\n                SetLights(Color.Lerp(baseColor, color, failPercentage));\n            }\n        }\n\n        public void SetLights(Color color)\n        {\n            foreach (LightsAnimator light in _gameplayLightsAnimators(ref _gameplayAnimator))\n                light.SetColor(color);\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexPlayerTableCell.cs",
    "content": "﻿using MultiplayerCore.Objects;\nusing SiraUtil.Affinity;\nusing System;\nusing System.Threading.Tasks;\nusing UnityEngine;\nusing UnityEngine.UI;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Objects\n{\n    public class MpexPlayerTableCell : IInitializable, IDisposable, IAffinity\n    {\n        private readonly ServerPlayerListViewController _playerListView;\n        private readonly MpEntitlementChecker _entitlementChecker;\n        private readonly ILobbyPlayersDataModel _playersDataModel;\n        private readonly IMenuRpcManager _menuRpcManager;\n\n        private static float alphaIsMe = 0.4f;\n        private static float alphaIsNotMe = 0.2f;\n\n        private static Color green = new Color(0f, 1f, 0f, 1f);\n        private static Color yellow = new Color(0.125f, 0.75f, 1f, 1f);\n        private static Color red = new Color(1f, 0f, 0f, 1f);\n        private static Color normal = new Color(0.125f, 0.75f, 1f, 0.1f);\n\n        internal MpexPlayerTableCell(\n            ServerPlayerListViewController playerListView,\n            NetworkPlayerEntitlementChecker entitlementChecker,\n            ILobbyPlayersDataModel playersDataModel,\n            IMenuRpcManager menuRpcManager)\n        {\n            _playerListView = playerListView;\n            _entitlementChecker = (entitlementChecker as MpEntitlementChecker)!;\n            _playersDataModel = playersDataModel;\n            _menuRpcManager = menuRpcManager;\n        }\n\n        public void Initialize() \n        {\n            _menuRpcManager.setIsEntitledToLevelEvent += HandleSetIsEntitledToLevel;\n        }\n\n        public void Dispose()\n        {\n            _menuRpcManager.setIsEntitledToLevelEvent -= HandleSetIsEntitledToLevel;\n        }\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(GameServerPlayerTableCell), nameof(GameServerPlayerTableCell.SetData))]\n        public void SetDataPrefix(IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData, bool hasKickPermissions, bool allowSelection, Task<AdditionalContentModel.EntitlementStatus> getLevelEntitlementTask, Image ____localPlayerBackgroundImage)\n        {\n            if (getLevelEntitlementTask != null)\n                getLevelEntitlementTask = Task.FromResult(AdditionalContentModel.EntitlementStatus.Owned);\n        }\n\n        [AffinityPostfix]\n        [AffinityPatch(typeof(GameServerPlayerTableCell), nameof(GameServerPlayerTableCell.SetData))]\n        public void SetDataPostfix(IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData, bool hasKickPermissions, bool allowSelection, Task<AdditionalContentModel.EntitlementStatus> getLevelEntitlementTask, Image ____localPlayerBackgroundImage)\n        {\n            ____localPlayerBackgroundImage.enabled = true;\n            string? hostSelectedLevel = _playersDataModel[_playersDataModel.partyOwnerId].beatmapLevel?.beatmapLevel?.levelID;\n            if (hostSelectedLevel == null)\n            {\n                SetLevelEntitlement(____localPlayerBackgroundImage, EntitlementsStatus.Unknown);\n                return;\n            }\n\n            EntitlementsStatus entitlement = EntitlementsStatus.Unknown;\n            if (!connectedPlayer.isMe)\n                entitlement = _entitlementChecker.GetUserEntitlementStatusWithoutRequest(connectedPlayer.userId, hostSelectedLevel);\n            // TODO: change color for local player\n            if (entitlement != EntitlementsStatus.Unknown)\n                SetLevelEntitlement(____localPlayerBackgroundImage, entitlement);\n            else if (!connectedPlayer.isMe)\n            {\n                // This might be a bad idea, race condition can cause packets that scale with the amount of players\n                _entitlementChecker.GetUserEntitlementStatus(connectedPlayer.userId, hostSelectedLevel);\n            }\n        }\n\n        private void SetLevelEntitlement(Image backgroundImage, EntitlementsStatus status)\n        {\n            Color backgroundColor = status switch\n            {\n                EntitlementsStatus.Ok => green,\n                EntitlementsStatus.NotOwned => red,\n                _ => normal,\n            };\n\n            backgroundColor.a = alphaIsNotMe;\n            backgroundImage.color = backgroundColor;\n        }\n\n        private void HandleSetIsEntitledToLevel(string userId, string levelId, EntitlementsStatus status)\n            => _playerListView.SetDataToTable();\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexAppInstaller.cs",
    "content": "﻿using IPA.Loader;\nusing MultiplayerExtensions.Patchers;\nusing MultiplayerExtensions.Players;\nusing MultiplayerExtensions.Utilities;\nusing SiraUtil.Zenject;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Installers\n{\n\tclass MpexAppInstaller : Installer\n\t{\n\t\tprivate readonly Config _config;\n\n\t\tpublic MpexAppInstaller(\n\t\t\tConfig config)\n        {\n\t\t\t_config = config;\n        }\n\n        public override void InstallBindings()\n        {\n\t\t\tContainer.BindInstance(_config).AsSingle();\n\t\t\tContainer.BindInterfacesAndSelfTo<SpriteManager>().AsSingle();\n\t\t\tContainer.BindInterfacesAndSelfTo<MpexPlayerManager>().AsSingle();\n\t\t\tContainer.BindInterfacesAndSelfTo<EnvironmentPatcher>().AsSingle();\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexGameInstaller.cs",
    "content": "﻿using IPA.Utilities;\nusing MultiplayerExtensions.Environment;\nusing MultiplayerExtensions.Patchers;\nusing MultiplayerExtensions.Players;\nusing SiraUtil.Extras;\nusing SiraUtil.Objects.Multiplayer;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Installers\n{\n    class MpexGameInstaller : Installer\n    {\n        public override void InstallBindings()\n        {\n            Container.BindInterfacesAndSelfTo<PlayerPositionPatcher>().AsSingle();\n            Container.BindInterfacesAndSelfTo<ColorSchemePatcher>().AsSingle();\n            Container.RegisterRedecorator(new LocalActivePlayerRegistration(DecorateLocalActivePlayerFacade));\n            Container.RegisterRedecorator(new LocalActivePlayerDuelRegistration(DecorateLocalActivePlayerFacade));\n            Container.RegisterRedecorator(new ConnectedPlayerRegistration(DecorateConnectedPlayerFacade));\n            Container.RegisterRedecorator(new ConnectedPlayerDuelRegistration(DecorateConnectedPlayerFacade));\n        }\n\n        private MultiplayerLocalActivePlayerFacade DecorateLocalActivePlayerFacade(MultiplayerLocalActivePlayerFacade original)\n        {\n            if (Plugin.Config.MissLighting)\n            original.gameObject.AddComponent<MpexPlayerFacadeLighting>();\n            return original;\n        }\n\n        private MultiplayerConnectedPlayerFacade DecorateConnectedPlayerFacade(MultiplayerConnectedPlayerFacade original)\n        {\n            if (Plugin.Config.MissLighting && !Plugin.Config.PersonalMissLightingOnly)\n                original.gameObject.AddComponent<MpexPlayerFacadeLighting>();\n            original.gameObject.AddComponent<MpexConnectedObjectManager>();\n            return original;\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexLobbyInstaller.cs",
    "content": "﻿using MultiplayerExtensions.Environments;\nusing MultiplayerExtensions.Environments.Lobby;\nusing SiraUtil.Extras;\nusing SiraUtil.Objects.Multiplayer;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Installers\n{\n    class MpexLobbyInstaller : Installer\n    {\n        public override void InstallBindings()\n        {\n            Container.RegisterRedecorator(new LobbyAvatarPlaceRegistration(DecorateAvatarPlace));\n            Container.RegisterRedecorator(new LobbyAvatarRegistration(DecorateAvatar));\n        }\n\n        private MultiplayerLobbyAvatarPlace DecorateAvatarPlace(MultiplayerLobbyAvatarPlace original)\n        {\n            original.gameObject.AddComponent<MpexAvatarPlaceLighting>();\n            return original;\n        }\n\n        private MultiplayerLobbyAvatarController DecorateAvatar(MultiplayerLobbyAvatarController original)\n        {\n            var avatarCaption = original.transform.Find(\"AvatarCaption\").gameObject;\n            avatarCaption.AddComponent<MpexAvatarNameTag>();\n\n            return original;\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexLocalActivePlayerInstaller.cs",
    "content": "﻿using MultiplayerExtensions.Environment;\nusing MultiplayerExtensions.Patchers;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Installers\n{\n    public class MpexLocalActivePlayerInstaller : MonoInstaller\n    {\n        public override void InstallBindings()\n        {\n            // stuff needed for solo environments to work\n            Container.BindInterfacesAndSelfTo<MpexLevelEndActions>().AsSingle();\n            Container.Bind<EnvironmentContext>().FromInstance(EnvironmentContext.Gameplay).AsSingle();\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexMenuInstaller.cs",
    "content": "﻿using MultiplayerExtensions.Environments;\nusing MultiplayerExtensions.Objects;\nusing MultiplayerExtensions.Patchers;\nusing MultiplayerExtensions.UI;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Installers\n{\n    class MpexMenuInstaller : Installer\n    {\n        public override void InstallBindings()\n        {\n            //Container.BindInterfacesAndSelfTo<MpexPlayerTableCell>().AsSingle();\n            Container.BindInterfacesAndSelfTo<AvatarPlacePatcher>().AsSingle();\n            Container.BindInterfacesAndSelfTo<MenuEnvironmentPatcher>().AsSingle();\n\n            Container.BindInterfacesAndSelfTo<MpexSetupFlowCoordinator>().FromNewComponentOnNewGameObject().AsSingle();\n            Container.BindInterfacesAndSelfTo<MpexSettingsViewController>().FromNewComponentAsViewController().AsSingle();\n            Container.BindInterfacesAndSelfTo<MpexEnvironmentViewController>().FromNewComponentAsViewController().AsSingle();\n            Container.BindInterfacesAndSelfTo<MpexMiscViewController>().FromNewComponentAsViewController().AsSingle();\n            Container.BindInterfacesAndSelfTo<MpexGameplaySetup>().AsSingle();\n\n            // needed for local player's player place\n            var avatarPlace = Container.Resolve<MenuEnvironmentManager>().transform.Find(\"MultiplayerLobbyEnvironment\").Find(\"LobbyAvatarPlace\").gameObject;\n            GameObject.Destroy(avatarPlace.GetComponent<MpexAvatarPlaceLighting>());\n            Container.Inject(avatarPlace.AddComponent<MpexAvatarPlaceLighting>());\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/MultiplayerExtensions.csproj",
    "content": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Library</OutputType>\n    <AppDesignerFolder>Properties</AppDesignerFolder>\n    <AssemblyName>MultiplayerExtensions</AssemblyName>\n    <AssemblyVersion>1.0.3</AssemblyVersion>\n    <TargetFramework>net472</TargetFramework>\n    <DebugSymbols>true</DebugSymbols>\n    <DebugType>portable</DebugType>\n    <LocalRefsDir Condition=\"Exists('..\\Refs')\">..\\Refs</LocalRefsDir>\n    <BeatSaberDir>$(LocalRefsDir)</BeatSaberDir>\n    <!--<PathMap>$(AppOutputBase)=X:\\$(AssemblyName)\\</PathMap>-->\n    <ErrorReport>prompt</ErrorReport>\n    <WarningLevel>4</WarningLevel>\n    <LangVersion>9.0</LangVersion>\n    <Nullable>enable</Nullable>\n    <VersionType>Unofficial</VersionType>\n    <CommitHash>local</CommitHash>\n    <GitBranch></GitBranch>\n    <GitModified></GitModified>\n  </PropertyGroup>\n  <PropertyGroup Condition=\" '$(Configuration)' == 'Debug' \">\n    <Optimize>false</Optimize>\n    <DefineConstants>DEBUG;TRACE</DefineConstants>\n  </PropertyGroup>\n  <PropertyGroup Condition=\" '$(Configuration)' == 'Release' \">\n    <Optimize>true</Optimize>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"$(DefineConstants.Contains('CIBuild')) OR '$(NCrunch)' == '1'\">\n    <DisableCopyToPlugins>True</DisableCopyToPlugins>\n  </PropertyGroup>\n  <PropertyGroup Condition=\"'$(NCrunch)' == '1'\">\n    <DisableCopyToPlugins>True</DisableCopyToPlugins>\n    <DisableZipRelease>True</DisableZipRelease>\n  </PropertyGroup>\n  <ItemGroup>\n    <Reference Include=\"BeatmapCore\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\BeatmapCore.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"BGNet\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\BGNet.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"Colors\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\Colors.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"GameplayCore\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\GameplayCore.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"Hive.Versioning\">\n      <HintPath>$(BeatSaberDir)\\Libs\\Hive.Versioning.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"HMRendering\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\HMRendering.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"LiteNetLib\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\LiteNetLib.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"netstandard\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\netstandard.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"Polyglot\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\Polyglot.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"SemVer\">\n      <HintPath>$(BeatSaberDir)\\Libs\\SemVer.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"System.IO.Compression\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\System.IO.Compression.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"Main\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\Main.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"HMLib\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\HMLib.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"HMUI\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\HMUI.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"IPA.Loader\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\IPA.Loader.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"Unity.TextMeshPro\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\Unity.TextMeshPro.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"Unity.Timeline\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\Unity.Timeline.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"UnityEngine\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"UnityEngine.AudioModule\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.AudioModule.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"UnityEngine.CoreModule\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.CoreModule.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"UnityEngine.DirectorModule\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.DirectorModule.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"UnityEngine.ImageConversionModule\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.ImageConversionModule.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"UnityEngine.TextRenderingModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.TextRenderingModule.dll</HintPath>\n    </Reference>\n    <Reference Include=\"UnityEngine.UI\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.UI.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"UnityEngine.UIElementsModule\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.UIElementsModule.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"UnityEngine.UIModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.UIModule.dll</HintPath>\n    </Reference>\n    <Reference Include=\"UnityEngine.VRModule\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\UnityEngine.VRModule.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"Zenject\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\Zenject.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"Zenject-usage\">\n      <HintPath>$(BeatSaberDir)\\Beat Saber_Data\\Managed\\Zenject-usage.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"0Harmony\">\n      <HintPath>$(BeatSaberDir)\\Libs\\0Harmony.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"BSML\">\n      <HintPath>$(BeatSaberDir)\\Plugins\\BSML.dll</HintPath>\n      <Private>False</Private>\n      <SpecificVersion>False</SpecificVersion>\n    </Reference>\n    <Reference Include=\"SiraUtil\">\n      <HintPath>$(BeatSaberDir)\\Plugins\\SiraUtil.dll</HintPath>\n      <Private>False</Private>\n    </Reference>\n    <Reference Include=\"System\" />\n    <Reference Include=\"System.Core\" />\n    <Reference Include=\"System.Xml.Linq\" />\n    <Reference Include=\"System.Data.DataSetExtensions\" />\n    <Reference Include=\"System.Data\" />\n    <Reference Include=\"System.Xml\" />\n  </ItemGroup>\n  <ItemGroup>\n    <EmbeddedResource Include=\"Assets\\IconMeta64.png\" />\n    <EmbeddedResource Include=\"Assets\\IconToaster64.png\" />\n    <EmbeddedResource Include=\"manifest.json\" />\n    <None Remove=\"Assets\\IconMeta64.png\" />\n    <None Remove=\"Assets\\IconSteam64.png\" />\n    <None Remove=\"Assets\\IconToaster64.png\" />\n    <None Remove=\"UI\\MpexEnvironmentViewController.bsml\" />\n    <None Remove=\"UI\\MpexGameplaySetup.bsml\" />\n    <None Remove=\"UI\\MpexMiscViewController.bsml\" />\n    <None Remove=\"UI\\MpexSettingsViewController.bsml\" />\n    <EmbeddedResource Include=\"Assets\\IconSteam64.png\" />\n    <None Remove=\"Assets\\IconOculus64.png\" />\n    <EmbeddedResource Include=\"Assets\\IconOculus64.png\" />\n    <EmbeddedResource Include=\"UI\\MpexGameplaySetup.bsml\" />\n    <EmbeddedResource Include=\"UI\\MpexMiscViewController.bsml\" />\n    <EmbeddedResource Include=\"UI\\MpexEnvironmentViewController.bsml\" />\n    <EmbeddedResource Include=\"UI\\MpexSettingsViewController.bsml\" />\n  </ItemGroup>\n  <ItemGroup>\n    <None Include=\"Directory.Build.props\" Condition=\"Exists('Directory.Build.props')\" />\n    <None Include=\"Directory.Build.targets\" Condition=\"Exists('Directory.Build.targets')\" />\n    <None Include=\"MultiplayerExtensions.csproj.user\" Condition=\"Exists('MultiplayerExtensions.csproj.user')\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"BeatSaberModdingTools.Tasks\">\n      <Version>1.3.2</Version>\n      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>\n      <PrivateAssets>all</PrivateAssets>\n    </PackageReference>\n    <PackageReference Include=\"Newtonsoft.Json\" Version=\"12.0.3\" />\n  </ItemGroup>\n  <ItemGroup>\n    <PackageReference Include=\"BepInEx.AssemblyPublicizer.MSBuild\" Version=\"0.4.1\" PrivateAssets=\"all\" />\n    <!-- Publicize directly when referencing -->\n    <Reference Include=\"$(BeatSaberDir)\\Plugins\\MultiplayerCore.dll\" Publicize=\"true\" />\n  </ItemGroup>\n  <ItemGroup>\n    <Folder Include=\"Assets\" />\n  </ItemGroup>\n  <Target Name=\"PreBuild\" BeforeTargets=\"BeforeBuild\" Condition=\"'$(NCRUNCH)' != '1'\">\n    <Error Text=\"The BeatSaberModdingTools.Tasks nuget package doesn't seem to be installed.\" Condition=\"'$(BSMTTaskAssembly)' == ''\" />\n    <GetCommitInfo ProjectDir=\"$(ProjectDir)\">\n      <Output TaskParameter=\"CommitHash\" PropertyName=\"CommitHash\" />\n      <Output TaskParameter=\"Branch\" PropertyName=\"GitBranch\" />\n      <Output TaskParameter=\"IsPullRequest\" PropertyName=\"IsPullRequest\" />\n      <Output TaskParameter=\"Modified\" PropertyName=\"GitModified\" />\n      <Output TaskParameter=\"GitUser\" PropertyName=\"GitUser\" />\n    </GetCommitInfo>\n    <PropertyGroup Condition=\"'$(GITHUB_ACTIONS)' == 'true' AND '$(GitUser)' == 'Zingabopp' AND '$(IsPullRequest)' != 'true'\">\n      <VersionType>Official</VersionType>\n    </PropertyGroup>\n    <PropertyGroup Condition=\"'$(GitModified)' != 'Modified'\">\n      <InformationalVersion>$(VersionType)-$(GitBranch)-$(CommitHash)</InformationalVersion>\n    </PropertyGroup>\n    <PropertyGroup Condition=\"'$(GitModified)' == 'Modified'\">\n      <InformationalVersion>$(VersionType)-$(GitBranch)-$(CommitHash)-$(GitModified)</InformationalVersion>\n    </PropertyGroup>\n    <Message Text=\"Version $(InformationalVersion)\" Importance=\"high\" />\n  </Target>\n</Project>\n"
  },
  {
    "path": "MultiplayerExtensions/Patchers/AvatarPlacePatcher.cs",
    "content": "﻿using HarmonyLib;\nusing MultiplayerExtensions.Environments;\nusing SiraUtil.Affinity;\nusing System.Collections.Generic;\nusing System.Reflection;\nusing System.Reflection.Emit;\n\nnamespace MultiplayerExtensions.Patchers\n{\n    [HarmonyPatch]\n    public class AvatarPlacePatcher : IAffinity\n    {\n        private readonly MenuEnvironmentManager _environmentManager;\n\n        internal AvatarPlacePatcher(\n            MenuEnvironmentManager environmentManager)\n        {\n            _environmentManager = environmentManager;\n        }\n\n        private static readonly MethodInfo _addMethod = typeof(List<MultiplayerLobbyAvatarPlace>).GetMethod(nameof(List<MultiplayerLobbyAvatarPlace>.Add));\n        private static readonly MethodInfo _setupAvatarPlaceMethod = SymbolExtensions.GetMethodInfo(() => SetupAvatarPlace(null!, 0));\n\n        [HarmonyTranspiler]\n        [HarmonyPatch(typeof(MultiplayerLobbyAvatarPlaceManager), nameof(MultiplayerLobbyAvatarPlaceManager.SpawnAllPlaces))]\n        private static IEnumerable<CodeInstruction> SpawnAllPlaces(IEnumerable<CodeInstruction> instructions) =>\n            new CodeMatcher(instructions)\n                .MatchForward(false, new CodeMatch(OpCodes.Callvirt, _addMethod))\n                .Insert(new CodeInstruction(OpCodes.Ldloc_3), new CodeInstruction(OpCodes.Callvirt, _setupAvatarPlaceMethod))\n                .InstructionEnumeration();\n\n        private static MultiplayerLobbyAvatarPlace SetupAvatarPlace(MultiplayerLobbyAvatarPlace avatarPlace, int sortIndex)\n        {\n            avatarPlace.gameObject.GetComponent<MpexAvatarPlaceLighting>().SortIndex = sortIndex;\n            return avatarPlace;\n        }\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(MultiplayerLobbyAvatarPlaceManager), nameof(MultiplayerLobbyAvatarPlaceManager.SpawnAllPlaces))]\n        private void SpawnAllPlacesPrefix(ILobbyStateDataModel ____lobbyStateDataModel)\n            => _environmentManager.transform.Find(\"MultiplayerLobbyEnvironment\").Find(\"LobbyAvatarPlace\").gameObject.GetComponent<MpexAvatarPlaceLighting>().SortIndex = ____lobbyStateDataModel.localPlayer.sortIndex;\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Patchers/ColorSchemePatcher.cs",
    "content": "﻿using SiraUtil.Affinity;\n\nnamespace MultiplayerExtensions.Patchers\n{\n    public class ColorSchemePatcher : IAffinity\n    {\n        private readonly GameplayCoreSceneSetupData _sceneSetupData;\n        private readonly Config _config;\n\n        internal ColorSchemePatcher(\n            GameplayCoreSceneSetupData sceneSetupData,\n            Config config)\n        {\n            _sceneSetupData = sceneSetupData;\n            _config = config;\n        }\n\n        [AffinityPostfix]\n        [AffinityPatch(typeof(PlayersSpecificSettingsAtGameStartModel), nameof(PlayersSpecificSettingsAtGameStartModel.GetPlayerSpecificSettingsForUserId))]\n        private void SetConnectedPlayerColorScheme(ref PlayerSpecificSettingsNetSerializable __result)\n        {\n            var colorscheme = _sceneSetupData.colorScheme;\n            if (_config.DisableMultiplayerColors)\n                __result.colorScheme = new ColorSchemeNetSerializable(colorscheme.saberAColor, colorscheme.saberBColor, colorscheme.obstaclesColor, colorscheme.environmentColor0, colorscheme.environmentColor1, colorscheme.environmentColor0Boost, colorscheme.environmentColor1Boost);\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Patchers/EnvironmentPatcher.cs",
    "content": "﻿using HarmonyLib;\nusing IPA.Utilities;\nusing SiraUtil.Affinity;\nusing SiraUtil.Logging;\nusing System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Linq;\nusing UnityEngine;\nusing UnityEngine.SceneManagement;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Patchers\n{\n    [HarmonyPatch]\n    public class EnvironmentPatcher : IAffinity\n    {\n        private readonly GameScenesManager _scenesManager;\n        private readonly Config _config;\n        private readonly SiraLog _logger;\n\n        internal EnvironmentPatcher(\n            GameScenesManager scenesManager,\n            Config config,\n            SiraLog logger)\n        {\n            _scenesManager = scenesManager;\n            _config = config;\n            _logger = logger;\n        }\n\n        private List<MonoBehaviour> _behavioursToInject = new();\n\n        [AffinityPostfix]\n        [AffinityPatch(typeof(SceneDecoratorContext), \"GetInjectableMonoBehaviours\")]\n        private void PreventEnvironmentInjection(SceneDecoratorContext __instance, List<MonoBehaviour> monoBehaviours, DiContainer ____container)\n        {\n            var scene = __instance.gameObject.scene;\n            if (_scenesManager.IsSceneInStack(\"MultiplayerEnvironment\") && _config.SoloEnvironment)\n            {\n                _logger.Info($\"Fixing bind conflicts on scene '{scene.name}'.\");\n                List<MonoBehaviour> removedBehaviours = new();\n\n                //if (scene.name == \"MultiplayerEnvironment\")\n                //    removedBehaviours = monoBehaviours.FindAll(behaviour => behaviour is ZenjectBinding binding && binding.Components.Any(c => c is LightWithIdManager));\n                if (scene.name.Contains(\"Environment\") && !scene.name.Contains(\"Multiplayer\"))\n                    removedBehaviours = monoBehaviours.FindAll(behaviour => (behaviour is ZenjectBinding binding && binding.Components.Any(c => c is LightWithIdManager)));\n\n                if (removedBehaviours.Any())\n                {\n                    _logger.Info($\"Removing behaviours '{string.Join(\", \", removedBehaviours.Select(behaviour => behaviour.GetType()))}' from scene '{scene.name}'.\");\n                    monoBehaviours.RemoveAll(monoBehaviour => removedBehaviours.Contains(monoBehaviour));\n                }\n\n                if (scene.name.Contains(\"Environment\") && !scene.name.Contains(\"Multiplayer\"))\n                {\n                    _logger.Info($\"Preventing environment injection.\");\n                    _behavioursToInject = new(monoBehaviours);\n                    monoBehaviours.Clear();\n                }\n            }\n            else\n            {\n                _behavioursToInject.Clear();\n            }\n        }\n\n        private List<InstallerBase> _normalInstallers = new();\n        private List<Type> _normalInstallerTypes = new();\n        private List<ScriptableObjectInstaller> _scriptableObjectInstallers = new();\n        private List<MonoInstaller> _monoInstallers = new();\n        private List<MonoInstaller> _installerPrefabs = new();\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(SceneDecoratorContext), \"InstallDecoratorInstallers\")]\n        private void PreventEnvironmentInstall(SceneDecoratorContext __instance, List<InstallerBase> ____normalInstallers, List<Type> ____normalInstallerTypes, List<ScriptableObjectInstaller> ____scriptableObjectInstallers, List<MonoInstaller> ____monoInstallers, List<MonoInstaller> ____installerPrefabs)\n        {\n            var scene = __instance.gameObject.scene;\n            if (_scenesManager.IsSceneInStack(\"MultiplayerEnvironment\") && _config.SoloEnvironment && scene.name.Contains(\"Environment\") && !scene.name.Contains(\"Multiplayer\"))\n            {\n                _logger.Info($\"Preventing environment installation.\");\n\n                _normalInstallers = new(____normalInstallers);\n                _normalInstallerTypes = new(____normalInstallerTypes);\n                _scriptableObjectInstallers = new(____scriptableObjectInstallers);\n                _monoInstallers = new(____monoInstallers);\n                _installerPrefabs = new(____installerPrefabs);\n\n                ____normalInstallers.Clear();\n                ____normalInstallerTypes.Clear();\n                ____scriptableObjectInstallers.Clear();\n                ____monoInstallers.Clear();\n                ____installerPrefabs.Clear();\n            }\n            else if (!_scenesManager.IsSceneInStack(\"MultiplayerEnvironment\"))\n            {\n                _normalInstallers.Clear();\n                _normalInstallerTypes.Clear();\n                _scriptableObjectInstallers.Clear();\n                _monoInstallers.Clear();\n                _installerPrefabs.Clear();\n            }\n        }\n\n        private List<GameObject> _objectsToEnable = new();\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(GameScenesManager), \"ActivatePresentedSceneRootObjects\")]\n        private void PreventEnvironmentActivation(List<string> scenesToPresent)\n        {\n            string defaultScene = scenesToPresent.FirstOrDefault(scene => scene.Contains(\"Environment\") && !scene.Contains(\"Multiplayer\"));\n            if (defaultScene != null)\n            {\n                if (scenesToPresent.Contains(\"MultiplayerEnvironment\"))\n                {\n                    _logger.Info($\"Preventing environment activation. ({defaultScene})\");\n                    _objectsToEnable = SceneManager.GetSceneByName(defaultScene).GetRootGameObjects().ToList();\n                    scenesToPresent.Remove(defaultScene);\n\n                    // fix ring lighting dogshit\n                    var trackLaneRingManagers = _objectsToEnable[0].transform.GetComponentsInChildren<TrackLaneRingsManager>();\n                } \n                else\n                {\n                    // Make sure hud is enabled in solo\n                    var sceneObjects = SceneManager.GetSceneByName(defaultScene).GetRootGameObjects().ToList();\n                    foreach (GameObject gameObject in sceneObjects)\n                    {\n                        var hud = gameObject.transform.GetComponentInChildren<CoreGameHUDController>();\n                        if (hud != null)\n                            hud.gameObject.SetActive(true);\n                    }\n                }\n            }\n        }\n\n        [AffinityPostfix]\n        [AffinityPatch(typeof(GameObjectContext), \"GetInjectableMonoBehaviours\")]\n        private void InjectEnvironment(GameObjectContext __instance, List<MonoBehaviour> monoBehaviours)\n        {\n            if (__instance.transform.name.Contains(\"LocalActivePlayer\") && _config.SoloEnvironment)\n            {\n                _logger.Info($\"Injecting environment.\");\n                monoBehaviours.AddRange(_behavioursToInject);\n            }\n        }\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(Context), \"InstallInstallers\", AffinityMethodType.Normal, null, typeof(List<InstallerBase>), typeof(List<Type>), typeof(List<ScriptableObjectInstaller>), typeof(List<MonoInstaller>), typeof(List<MonoInstaller>))]\n        private void InstallEnvironment(Context __instance, List<InstallerBase> normalInstallers, List<Type> normalInstallerTypes, List<ScriptableObjectInstaller> scriptableObjectInstallers, List<MonoInstaller> installers, List<MonoInstaller> installerPrefabs)\n        {\n            if (__instance is GameObjectContext instance && __instance.transform.name.Contains(\"LocalActivePlayer\") && _config.SoloEnvironment)\n            {\n                _logger.Info($\"Installing environment.\");\n                normalInstallers.AddRange(_normalInstallers);\n                normalInstallerTypes.AddRange(_normalInstallerTypes);\n                scriptableObjectInstallers.AddRange(_scriptableObjectInstallers);\n                installers.AddRange(_monoInstallers);\n                installerPrefabs.AddRange(_installerPrefabs);\n            }\n        }\n\n       \n        [AffinityPrefix]\n        [AffinityPatch(typeof(GameObjectContext), \"InstallInstallers\")]\n        private void LoveYouCountersPlus(GameObjectContext __instance)\n        {\n            if (__instance.transform.name.Contains(\"LocalActivePlayer\") && _config.SoloEnvironment)\n            {\n                DiContainer container = __instance.GetProperty<DiContainer, GameObjectContext>(\"Container\");\n                var hud = (CoreGameHUDController)_behavioursToInject.Find(x => x is CoreGameHUDController);\n                container.Unbind<CoreGameHUDController>();\n                container.Bind<CoreGameHUDController>().FromInstance(hud).AsSingle();\n                var multihud = __instance.transform.GetComponentInChildren<CoreGameHUDController>();\n                multihud.gameObject.SetActive(false);\n                var multiPositionHud = __instance.transform.GetComponentInChildren<MultiplayerPositionHUDController>();\n                multiPositionHud.transform.position += new Vector3(0, 0.01f, 0);\n            }\n        }\n\n        [AffinityPostfix] \n        [AffinityPatch(typeof(GameObjectContext), \"InstallSceneBindings\")]\n        private void ActivateEnvironment(GameObjectContext __instance)\n        {\n            if (__instance.transform.name.Contains(\"LocalActivePlayer\") && _config.SoloEnvironment)\n            {\n                _logger.Info($\"Activating environment.\");\n                foreach (GameObject gameObject in _objectsToEnable)\n                    gameObject.SetActive(true);\n\n                var activeObjects = __instance.transform.Find(\"IsActiveObjects\");\n                activeObjects.Find(\"Lasers\").gameObject.SetActive(false);\n                activeObjects.Find(\"Construction\").gameObject.SetActive(false);\n                activeObjects.Find(\"BigSmokePS\").gameObject.SetActive(false);\n                activeObjects.Find(\"DustPS\").gameObject.SetActive(false);\n                activeObjects.Find(\"DirectionalLights\").gameObject.SetActive(false);\n\n                var localActivePlayer = __instance.transform.GetComponent<MultiplayerLocalActivePlayerFacade>();\n                var activeOnlyGameObjects = localActivePlayer.GetField<GameObject[], MultiplayerLocalActivePlayerFacade>(\"_activeOnlyGameObjects\");\n                var newActiveOnlyGameObjects = activeOnlyGameObjects.Concat(_objectsToEnable);\n                localActivePlayer.SetField(\"_activeOnlyGameObjects\", newActiveOnlyGameObjects.ToArray());\n            }\n        }\n\n        [HarmonyPostfix]\n        [HarmonyPatch(typeof(Context), \"InstallSceneBindings\")]\n        private static void HideOtherPlayerPlatforms(Context __instance)\n        {\n            if (__instance.transform.name.Contains(\"ConnectedPlayer\"))\n            {\n                if (Plugin.Config.DisableMultiplayerPlatforms)\n                    __instance.transform.Find(\"Construction\").gameObject.SetActive(false);\n                if (Plugin.Config.DisableMultiplayerLights)\n                    __instance.transform.Find(\"Lasers\").gameObject.SetActive(false);\n            }\n        }\n\n        [HarmonyPrefix]\n        [HarmonyPatch(typeof(EnvironmentSceneSetup), nameof(EnvironmentSceneSetup.InstallBindings))]\n        private static bool RemoveDuplicateInstalls(EnvironmentSceneSetup __instance)\n        {\n            DiContainer container = __instance.GetProperty<DiContainer, MonoInstallerBase>(\"Container\");\n            return !container.HasBinding<EnvironmentBrandingManager.InitData>();\n        }\n\n        [AffinityPostfix]\n        [AffinityPatch(typeof(GameplayCoreInstaller), nameof(GameplayCoreInstaller.InstallBindings))]\n        private void SetEnvironmentColors(GameplayCoreInstaller __instance)\n        {\n            if (!_config.SoloEnvironment || !_scenesManager.IsSceneInStack(\"MultiplayerEnvironment\"))\n                return;\n\n            DiContainer container = __instance.GetProperty<DiContainer, MonoInstallerBase>(\"Container\");\n            var colorManager = container.Resolve<EnvironmentColorManager>();\n            container.Inject(colorManager);\n            colorManager.Awake();\n            colorManager.Start();\n\n            foreach (var gameObject in _objectsToEnable)\n            {\n                var lightSwitchEventEffects = gameObject.transform.GetComponentsInChildren<LightSwitchEventEffect>();\n                foreach (var component in lightSwitchEventEffects)\n                    component.Awake();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Patchers/MenuEnvironmentPatcher.cs",
    "content": "﻿using HarmonyLib;\nusing SiraUtil.Affinity;\nusing SiraUtil.Logging;\nusing System.Linq;\n\nnamespace MultiplayerExtensions.Patchers\n{\n    [HarmonyPatch]\n    public class MenuEnvironmentPatcher : IAffinity\n    {\n        private readonly GameplaySetupViewController _gameplaySetup;\n        private readonly Config _config;\n        private readonly SiraLog _logger;\n\n        internal MenuEnvironmentPatcher(\n            GameplaySetupViewController gameplaySetup,\n            Config config,\n            SiraLog logger)\n        {\n            _gameplaySetup = gameplaySetup;\n            _config = config;\n            _logger = logger;\n        }\n\n        [HarmonyPrefix]\n        [HarmonyPatch(typeof(GameplaySetupViewController), nameof(GameplaySetupViewController.Setup))]\n        private static void EnableEnvironmentTab(bool showModifiers, ref bool showEnvironmentOverrideSettings, bool showColorSchemesSettings, bool showMultiplayer, PlayerSettingsPanelController.PlayerSettingsPanelLayout playerSettingsPanelLayout)\n        {\n            if (showMultiplayer)\n                showEnvironmentOverrideSettings = Plugin.Config.SoloEnvironment;\n        }\n\n        private EnvironmentInfoSO _originalEnvironmentInfo = null!;\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(MultiplayerLevelScenesTransitionSetupDataSO), \"Init\")]\n        private void SetEnvironmentScene(IDifficultyBeatmap difficultyBeatmap, ref EnvironmentInfoSO ____multiplayerEnvironmentInfo)\n        {\n            if (!_config.SoloEnvironment)\n                return;\n\n            _originalEnvironmentInfo = ____multiplayerEnvironmentInfo;\n            ____multiplayerEnvironmentInfo = difficultyBeatmap.GetEnvironmentInfo();\n            if (_gameplaySetup.environmentOverrideSettings.overrideEnvironments)\n                ____multiplayerEnvironmentInfo = _gameplaySetup.environmentOverrideSettings.GetOverrideEnvironmentInfoForType(____multiplayerEnvironmentInfo.environmentType);\n        }\n\n        [AffinityPostfix]\n        [AffinityPatch(typeof(MultiplayerLevelScenesTransitionSetupDataSO), \"Init\")]\n        private void ResetEnvironmentScene(IDifficultyBeatmap difficultyBeatmap, ref EnvironmentInfoSO ____multiplayerEnvironmentInfo)\n        {\n            if (_config.SoloEnvironment)\n                ____multiplayerEnvironmentInfo = _originalEnvironmentInfo;\n        }\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(ScenesTransitionSetupDataSO), \"Init\")]\n        private void AddEnvironmentOverrides(ref SceneInfo[] scenes)\n        {\n            if (_config.SoloEnvironment && scenes.Any(scene => scene.name.Contains(\"Multiplayer\")))\n            {\n                scenes = scenes.AddItem(_originalEnvironmentInfo.sceneInfo).ToArray();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Patchers/PlayerPositionPatcher.cs",
    "content": "﻿using HarmonyLib;\nusing SiraUtil.Affinity;\nusing System.Collections.Generic;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions.Patchers\n{\n    [HarmonyPatch]\n    public class PlayerPositionPatcher : IAffinity\n    {\n        private readonly Config _config;\n\n        internal PlayerPositionPatcher(\n            Config config)\n        {\n            _config = config;\n        }\n\n        // these are affinity patches because they only apply to one container\n        [AffinityPrefix]\n        [AffinityPatch(typeof(MultiplayerLayoutProvider), nameof(MultiplayerLayoutProvider.CalculateLayout))]\n        private bool SideBySideLayout(ref MultiplayerPlayerLayout __result)\n        {;\n            __result = MultiplayerPlayerLayout.Duel;\n            return !_config.SideBySide;\n        }\n\n        [HarmonyPrefix]\n        [HarmonyPatch(typeof(MultiplayerConditionalActiveByLayout), nameof(MultiplayerConditionalActiveByLayout.Start))]\n        private static void SideBySideLayoutConfirm(MultiplayerConditionalActiveByLayout __instance, MultiplayerLayoutProvider ____layoutProvider)\n        {\n            if (!Plugin.Config.SideBySide)\n                return;\n            if (____layoutProvider.layout == MultiplayerPlayerLayout.NotDetermined)\n                __instance.HandlePlayersLayoutWasCalculated(MultiplayerPlayerLayout.Duel, 2);\n        }\n\n        [HarmonyPrefix]\n        [HarmonyPatch(typeof(MultiplayerConditionalActiveByLayout), nameof(MultiplayerConditionalActiveByLayout.HandlePlayersLayoutWasCalculated))]\n        private static void SideBySideObjectDisable(ref MultiplayerPlayerLayout layout)\n        {\n            if (Plugin.Config.SideBySide)\n                layout = MultiplayerPlayerLayout.Duel;\n        }\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(MultiplayerPlayerPlacement), nameof(MultiplayerPlayerPlacement.GetOuterCirclePositionAngleForPlayer))]\n        private bool SideBySideAngle(int playerIndex, int localPlayerIndex, ref float __result)\n        {\n            __result = (playerIndex - localPlayerIndex) * 0.01f;\n            return !_config.SideBySide;\n        }\n\n        [AffinityPrefix]\n        [AffinityPatch(typeof(MultiplayerPlayerPlacement), nameof(MultiplayerPlayerPlacement.GetPlayerWorldPosition))]\n        private bool SoloEnvironmentPosition(float outerCirclePositionAngle, ref Vector3 __result)\n        {\n            var sortIndex = outerCirclePositionAngle ;\n            __result = new Vector3(sortIndex * 100f * _config.SideBySideDistance, 0, 0);\n            return !_config.SideBySide;\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Patches/AvatarPoseRestrictionPatch.cs",
    "content": "﻿using HarmonyLib;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions.Patches\n{\n    [HarmonyPatch]\n    public class AvatarPoseRestrictionPatch\n    {\n        [HarmonyPrefix]\n        [HarmonyPatch(typeof(AvatarPoseRestrictions), nameof(AvatarPoseRestrictions.HandleAvatarPoseControllerPositionsWillBeSet))]\n        private static bool DisableAvatarRestrictions(AvatarPoseRestrictions __instance, Vector3 headPosition, Vector3 leftHandPosition, Vector3 rightHandPosition, out Vector3 newHeadPosition, out Vector3 newLeftHandPosition, out Vector3 newRightHandPosition)\n        {\n            newHeadPosition = headPosition;\n            newLeftHandPosition = leftHandPosition;\n            newRightHandPosition = rightHandPosition;\n            if (!Plugin.Config.DisableAvatarConstraints)\n                return true;\n            newLeftHandPosition = __instance.LimitHandPositionRelativeToHead(leftHandPosition, headPosition);\n            newRightHandPosition = __instance.LimitHandPositionRelativeToHead(rightHandPosition, headPosition);\n            return false;\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Patches/PlatformMovementPatch.cs",
    "content": "﻿using HarmonyLib;\n\nnamespace MultiplayerExtensions.Patches\n{\n    [HarmonyPatch]\n    public class PlatformMovementPatch\n    {\n        [HarmonyPrefix]\n        [HarmonyPatch(typeof(MultiplayerVerticalPlayerMovementManager), nameof(MultiplayerVerticalPlayerMovementManager.Update))]\n        private static bool DisableVerticalPlayerMovement()\n        {\n            return !Plugin.Config.DisablePlatformMovement;\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Patches/ResumeSpawningPatch.cs",
    "content": "﻿using HarmonyLib;\n\nnamespace MultiplayerExtensions.Patches\n{\n    [HarmonyPatch]\n    public class ResumeSpawningPatch\n    {\n        [HarmonyPrefix]\n        [HarmonyPatch(typeof(MultiplayerConnectedPlayerFacade), nameof(MultiplayerConnectedPlayerFacade.ResumeSpawning))]\n        private static bool DisableAvatarRestrictions()\n        {\n            if (Plugin.Config.DisableMultiplayerObjects)\n                return false;\n            return true;\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Players/MpexPlayerData.cs",
    "content": "﻿using LiteNetLib.Utils;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions.Players\n{\n    public class MpexPlayerData : INetSerializable\n    {\n        /// <summary>\n        /// Player's color set in the plugin config.\n        /// </summary>\n        public Color Color { get; set; }\n\n        public void Serialize(NetDataWriter writer)\n        {\n            writer.Put(\"#\" + ColorUtility.ToHtmlStringRGB(Color));\n        }\n\n        public void Deserialize(NetDataReader reader)\n        {\n            Color color;\n            if (!ColorUtility.TryParseHtmlString(reader.GetString(), out color))\n                color = Config.DefaultPlayerColor;\n            Color = color;\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Players/MpexPlayerManager.cs",
    "content": "﻿using MultiplayerCore.Networking;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Players\n{\n\tpublic class MpexPlayerManager : IInitializable\n\t{\n\t\tpublic event Action<IConnectedPlayer, MpexPlayerData> PlayerConnectedEvent = null!;\n\n\t\tpublic IReadOnlyDictionary<string, MpexPlayerData> Players => _playerData;\n\n\t\tprivate ConcurrentDictionary<string, MpexPlayerData> _playerData = new();\n\n\t\tprivate readonly MpPacketSerializer _packetSerializer;\n\t\tprivate readonly IMultiplayerSessionManager _sessionManager;\n\t\tprivate readonly Config _config;\n\n\t\tinternal MpexPlayerManager(\n\t\t\tMpPacketSerializer packetSerializer,\n\t\t\tIMultiplayerSessionManager sessionManager,\n\t\t\tConfig config)\n\t\t{\n\t\t\t_packetSerializer = packetSerializer;\n\t\t\t_sessionManager = sessionManager;\n\t\t\t_config = config;\n\t\t}\n\n\t\tpublic void Initialize()\n\t\t{\n\t\t\t_sessionManager.SetLocalPlayerState(\"modded\", true);\n\t\t\t_packetSerializer.RegisterCallback<MpexPlayerData>(HandlePlayerData);\n\t\t\t_sessionManager.playerConnectedEvent += HandlePlayerConnected;\n\t\t}\n\n\t\tpublic void Dispose()\n\t\t{\n\t\t\t_packetSerializer.UnregisterCallback<MpexPlayerData>();\n\t\t}\n\n\t\tprivate void HandlePlayerConnected(IConnectedPlayer player)\n\t\t{\n\t\t\t_sessionManager.Send(new MpexPlayerData\n\t\t\t{\n\t\t\t\tColor = _config.PlayerColor\n\t\t\t});\n\t\t}\n\n\t\tprivate void HandlePlayerData(MpexPlayerData packet, IConnectedPlayer player)\n\t\t{\n\t\t\t_playerData[player.userId] = packet;\n\t\t\tPlayerConnectedEvent(player, packet);\n\t\t}\n\n\t\tpublic bool TryGetPlayer(string userId, out MpexPlayerData player)\n\t\t\t=> _playerData.TryGetValue(userId, out player);\n\n\t\tpublic MpexPlayerData? GetPlayer(string userId)\n\t\t\t=> _playerData.ContainsKey(userId) ? _playerData[userId] : null;\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Plugin.cs",
    "content": "﻿using HarmonyLib;\nusing IPA;\nusing IPA.Config.Stores;\nusing IPA.Loader;\nusing MultiplayerExtensions.Installers;\nusing SiraUtil.Zenject;\nusing IPALogger = IPA.Logging.Logger;\nusing Conf = IPA.Config.Config;\n\nnamespace MultiplayerExtensions\n{\n    [Plugin(RuntimeOptions.DynamicInit)]\n    public class Plugin\n    {\n        public const string ID = \"com.goobwabber.multiplayerextensions\";\n\n        internal static IPALogger Logger = null!;\n        internal static Config Config = null!;\n\n        private readonly Harmony _harmony;\n        private readonly PluginMetadata _metadata;\n\n        [Init]\n        public Plugin(IPALogger logger, Conf conf, Zenjector zenjector, PluginMetadata pluginMetadata)\n        {\n            Config config = conf.Generated<Config>();\n            _harmony = new Harmony(ID);\n            _metadata = pluginMetadata;\n            Logger = logger;\n            Config = config;\n\n            zenjector.UseMetadataBinder<Plugin>();\n            zenjector.UseLogger(logger);\n            zenjector.UseSiraSync(SiraUtil.Web.SiraSync.SiraSyncServiceType.GitHub, \"Goobwabber\", \"MultiplayerExtensions\");\n            zenjector.Install<MpexAppInstaller>(Location.App, config);\n            zenjector.Install<MpexMenuInstaller>(Location.Menu);\n            zenjector.Install<MpexLobbyInstaller, MultiplayerLobbyInstaller>();\n            zenjector.Install<MpexGameInstaller>(Location.MultiplayerCore);\n            zenjector.Install<MpexLocalActivePlayerInstaller>(Location.MultiPlayer);\n        }\n\n        [OnEnable]\n        public void OnEnable()\n        {\n            _harmony.PatchAll(_metadata.Assembly);\n        }\n\n        [OnDisable]\n        public void OnDisable()\n        {\n            _harmony.UnpatchSelf();\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexEnvironmentViewController.bsml",
    "content": "﻿<bg xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/ https://raw.githubusercontent.com/monkeymanboy/BSML-Docs/gh-pages/BSMLSchema.xsd'>\n\t<horizontal anchor-min-y='1'  bg='panel-top' pad-left='10' pad-right='10' horizontal-fit='PreferredSize'>\n    <text text='Environment' align='Center' font-size='8' />\n\t</horizontal>\n\t<settings-container>\n    <toggle-setting value='solo-environment' apply-on-change='true' text='Solo Environment' hover-hint='Uses the solo environment instead of the multiplayer environment. Chroma and Noodle compatible!' bind-value='true'/>\n    <toggle-setting value='side-by-side' apply-on-change='true' text='Side By Side' hover-hint=\"Places players side by side in gameplay, similar to old multiplayer!\" bind-value='true'/>\n    <increment-setting id='side-by-side-distance-increment' value='side-by-side-distance' apply-on-change='true' text='Side By Side Distance' hover-hint=\"Distance between players in gameplay when using side by side\" increment='0.1' bind-value='true'/>\n\t</settings-container>\n</bg>"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexEnvironmentViewController.cs",
    "content": "﻿using BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.Components.Settings;\nusing BeatSaberMarkupLanguage.ViewControllers;\nusing IPA.Utilities;\nusing Zenject;\n\nnamespace MultiplayerExtensions.UI\n{\n    [ViewDefinition(\"MultiplayerExtensions.UI.MpexEnvironmentViewController.bsml\")]\n    public class MpexEnvironmentViewController : BSMLAutomaticViewController\n    {\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showModifiers\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showModifiers));\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showEnvironmentOverrideSettings\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showEnvironmentOverrideSettings));\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showColorSchemesSettings\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showColorSchemesSettings));\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showMultiplayer\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showMultiplayer));\n\n        private GameplaySetupViewController _gameplaySetup = null!;\n        private Config _config = null!;\n\n        [Inject]\n        private void Construct(\n            GameplaySetupViewController gameplaySetup,\n            Config config)\n        {\n            _gameplaySetup = gameplaySetup;\n            _config = config;\n        }\n\n        [UIAction(\"#post-parse\")]\n        private void PostParse()\n        {\n            _sideBySideDistanceIncrement.interactable = _sideBySide;\n        }\n\n        [UIComponent(\"side-by-side-distance-increment\")]\n        private GenericInteractableSetting _sideBySideDistanceIncrement = null!;\n\n        [UIValue(\"solo-environment\")]\n        private bool _soloEnvironment\n        {\n            get => _config.SoloEnvironment;\n            set\n            {\n                _config.SoloEnvironment = value;\n                _gameplaySetup.Setup(\n                    _showModifiers(ref _gameplaySetup),\n                    _showEnvironmentOverrideSettings(ref _gameplaySetup),\n                    _showColorSchemesSettings(ref _gameplaySetup),\n                    _showMultiplayer(ref _gameplaySetup),\n                    PlayerSettingsPanelController.PlayerSettingsPanelLayout.Multiplayer\n                );\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"side-by-side\")]\n        private bool _sideBySide\n        {\n            get => _config.SideBySide;\n            set\n            {\n                _config.SideBySide = value;\n                if (_sideBySideDistanceIncrement != null)\n                    _sideBySideDistanceIncrement.interactable = value;\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"side-by-side-distance\")]\n        private float _sideBySideDistance\n        {\n            get => _config.SideBySideDistance;\n            set\n            {\n                _config.SideBySideDistance = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexGameplaySetup.bsml",
    "content": "﻿<vertical id='vert'>\n\t<text text='MultiplayerExtensions' pad-bottom='0' align='Bottom' size-delta-y='10'/>\n\t<toggle-setting value='solo-environment' apply-on-change='true' text='Solo Environment' hover-hint='Uses the solo environment instead of the multiplayer environment. Chroma and Noodle compatible!' bind-value='true'/>\n  <button text='Preferences' on-click='preferences-click'/>\n</vertical>"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexGameplaySetup.cs",
    "content": "﻿using BeatSaberMarkupLanguage;\nusing BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.Components;\nusing HMUI;\nusing IPA.Utilities;\nusing SiraUtil.Logging;\nusing System;\nusing System.Reflection;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.UI\n{\n    public class MpexGameplaySetup : NotifiableBase, IInitializable, IDisposable\n    {\n        public const string ResourcePath = \"MultiplayerExtensions.UI.MpexGameplaySetup.bsml\";\n\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showModifiers\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showModifiers));\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showEnvironmentOverrideSettings\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showEnvironmentOverrideSettings));\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showColorSchemesSettings\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showColorSchemesSettings));\n        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showMultiplayer\n            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showMultiplayer));\n\n        private GameplaySetupViewController _gameplaySetup;\n        private MultiplayerSettingsPanelController _multiplayerSettingsPanel;\n        private MainFlowCoordinator _mainFlowCoordinator;\n        private MpexSetupFlowCoordinator _setupFlowCoordinator;\n        private Config _config;\n        private SiraLog _logger;\n\n        internal MpexGameplaySetup(\n            GameplaySetupViewController gameplaySetup,\n            MainFlowCoordinator mainFlowCoordinator,\n            MpexSetupFlowCoordinator setupFlowCoordinator,\n            Config config,\n            SiraLog logger)\n        {\n            _gameplaySetup = gameplaySetup;\n            _multiplayerSettingsPanel = gameplaySetup.GetField<MultiplayerSettingsPanelController, GameplaySetupViewController>(\"_multiplayerSettingsPanelController\");\n            _mainFlowCoordinator = mainFlowCoordinator;\n            _setupFlowCoordinator = setupFlowCoordinator;\n            _config = config;\n            _logger = logger;\n        }\n\n        public void Initialize()\n        {\n            BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _multiplayerSettingsPanel.gameObject, this);\n            while (0 < _vert.transform.childCount)\n                _vert.transform.GetChild(0).SetParent(_multiplayerSettingsPanel.transform);\n        }\n\n        public void Dispose()\n        {\n            \n        }\n\n        [UIAction(\"preferences-click\")]\n        private void PresentPreferences()\n        {\n            FlowCoordinator deepestChildFlowCoordinator = DeepestChildFlowCoordinator(_mainFlowCoordinator);\n            _setupFlowCoordinator.parentFlowCoordinator = deepestChildFlowCoordinator;\n            deepestChildFlowCoordinator.PresentFlowCoordinator(_setupFlowCoordinator);\n        }\n\n        private FlowCoordinator DeepestChildFlowCoordinator(FlowCoordinator root)\n        {\n            var flow = root.childFlowCoordinator;\n            if (flow == null) return root;\n            if (flow.childFlowCoordinator == null || flow.childFlowCoordinator == flow)\n            {\n                return flow;\n            }\n            return DeepestChildFlowCoordinator(flow);\n        }\n\n        [UIObject(\"vert\")]\n        private GameObject _vert = null!;\n\n        [UIValue(\"solo-environment\")]\n        private bool _soloEnvironment\n        {\n            get => _config.SoloEnvironment;\n            set \n            {\n                _config.SoloEnvironment = value;\n                _gameplaySetup.Setup(\n                    _showModifiers(ref _gameplaySetup),\n                    _showEnvironmentOverrideSettings(ref _gameplaySetup),\n                    _showColorSchemesSettings(ref _gameplaySetup),\n                    _showMultiplayer(ref _gameplaySetup),\n                    PlayerSettingsPanelController.PlayerSettingsPanelLayout.Multiplayer\n                );\n                NotifyPropertyChanged();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexMiscViewController.bsml",
    "content": "﻿<bg xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/ https://raw.githubusercontent.com/monkeymanboy/BSML-Docs/gh-pages/BSMLSchema.xsd'>\n\t<horizontal anchor-min-y='1'  bg='panel-top' pad-left='10' pad-right='10' horizontal-fit='PreferredSize'>\n\t\t<text text='Misc' align='Center' font-size='8' />\n\t</horizontal>\n\t<settings-container>\n    <toggle-setting value='disable-avatar-constraints' apply-on-change='true' text='Disable Avatar Position Restrictions' hover-hint='Allows players to move outside of their bounds.' bind-value='true'/>\n    <toggle-setting value='disable-player-colors' apply-on-change='true' text='Disable Player Colors' hover-hint=\"Sets other player's colors to yours.\" bind-value='true'/>\n    <toggle-setting value='disable-platform-movement' apply-on-change='true' text='Disable Platform Movement' hover-hint=\"Disables vertical platform movement.\" bind-value='true'/>\n\t</settings-container>\n</bg>"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexMiscViewController.cs",
    "content": "﻿using BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.ViewControllers;\nusing Zenject;\n\nnamespace MultiplayerExtensions.UI\n{\n    [ViewDefinition(\"MultiplayerExtensions.UI.MpexMiscViewController.bsml\")]\n    public class MpexMiscViewController : BSMLAutomaticViewController\n    {\n        private Config _config = null!;\n\n        [Inject]\n        private void Construct(\n            Config config)\n        {\n            _config = config;\n        }\n\n        [UIValue(\"disable-avatar-constraints\")]\n        private bool _disableAvatarConstraints\n        {\n            get => _config.DisableAvatarConstraints;\n            set\n            {\n                _config.DisableAvatarConstraints = value;\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"disable-player-colors\")]\n        private bool _disablePlayerColors\n        {\n            get => _config.DisableMultiplayerColors;\n            set\n            {\n                _config.DisableMultiplayerColors = value;\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"disable-platform-movement\")]\n        private bool _disablePlatformMovement\n        {\n            get => _config.DisablePlatformMovement;\n            set\n            {\n                _config.DisablePlatformMovement = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexSettingsViewController.bsml",
    "content": "﻿<bg xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/ https://raw.githubusercontent.com/monkeymanboy/BSML-Docs/gh-pages/BSMLSchema.xsd'>\n\t<horizontal anchor-min-y='1'  bg='panel-top' pad-left='10' pad-right='10' horizontal-fit='PreferredSize'>\n\t</horizontal>\n\t<settings-container>\n    <toggle-setting value='hide-player-platforms' apply-on-change='true' text='Hide Player Platforms' hover-hint=\"Hides other player's platforms in game.\" bind-value='true'/>\n    <toggle-setting value='hide-player-lights' apply-on-change='true' text='Hide Player Lights' hover-hint=\"Hides other player's platform lights in game.\" bind-value='true'/>\n    <toggle-setting value='hide-player-objects' apply-on-change='true' text='Hide Player Objects' hover-hint=\"Hides other player's notes and walls in game.\" bind-value='true'/>\n    <toggle-setting value='miss-lighting' apply-on-change='true' text='Miss Lighting' hover-hint=\"Turns a player's platform red when they miss a note.\" bind-value='true'/>\n    <toggle-setting id='personal-miss-lighting-toggle' value='personal-miss-lighting-only' apply-on-change='true' text='Personal Only Miss Lighting' hover-hint=\"Makes miss lighting only apply to your platform.\" bind-value='true'/>\n\t</settings-container>\n</bg>"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexSettingsViewController.cs",
    "content": "﻿using BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.Components.Settings;\nusing BeatSaberMarkupLanguage.ViewControllers;\nusing Zenject;\n\nnamespace MultiplayerExtensions.UI\n{\n    [ViewDefinition(\"MultiplayerExtensions.UI.MpexSettingsViewController.bsml\")]\n    public class MpexSettingsViewController : BSMLAutomaticViewController\n    {\n        private Config _config = null!;\n\n        [Inject]\n        private void Construct(\n            Config config)\n        {\n            _config = config;\n        }\n\n        [UIAction(\"#post-parse\")]\n        private void PostParse()\n        {\n            _personalMissLightingToggle.interactable = _missLighting;\n        }\n\n        [UIComponent(\"personal-miss-lighting-toggle\")]\n        private GenericInteractableSetting _personalMissLightingToggle = null!;\n\n        [UIValue(\"hide-player-platforms\")]\n        private bool _hidePlayerPlatforms\n        {\n            get => _config.DisableMultiplayerPlatforms;\n            set\n            {\n                _config.DisableMultiplayerPlatforms = value;\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"hide-player-lights\")]\n        private bool _hidePlayerLights\n        {\n            get => _config.DisableMultiplayerLights;\n            set\n            {\n                _config.DisableMultiplayerLights = value;\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"hide-player-objects\")]\n        private bool _hidePlayerObjects\n        {\n            get => _config.DisableMultiplayerObjects;\n            set\n            {\n                _config.DisableMultiplayerObjects = value;\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"miss-lighting\")]\n        private bool _missLighting\n        {\n            get => _config.MissLighting;\n            set\n            {\n                _config.MissLighting = value;\n                if (_personalMissLightingToggle != null)\n                    _personalMissLightingToggle.interactable = value;\n                NotifyPropertyChanged();\n            }\n        }\n\n        [UIValue(\"personal-miss-lighting-only\")]\n        private bool _personalMissLightingOnly\n        {\n            get => _config.PersonalMissLightingOnly;\n            set\n            {\n                _config.PersonalMissLightingOnly = value;\n                NotifyPropertyChanged();\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexSetupFlowCoordinator.cs",
    "content": "﻿using HMUI;\nusing Zenject;\nusing BeatSaberMarkupLanguage;\nusing SiraUtil.Affinity;\nusing System;\n\nnamespace MultiplayerExtensions.UI\n{\n    public class MpexSetupFlowCoordinator : FlowCoordinator\n    {\n        internal FlowCoordinator parentFlowCoordinator = null!;\n        private MpexSettingsViewController _settingsViewController = null!;\n        private MpexEnvironmentViewController _environmentViewController = null!;\n        private MpexMiscViewController _miscViewController = null!;\n        private ILobbyGameStateController _gameStateController = null!;\n\n        [Inject]\n        public void Construct(\n            MainFlowCoordinator mainFlowCoordinator, \n            MpexSettingsViewController settingsViewController,\n            MpexEnvironmentViewController environmentViewController,\n            MpexMiscViewController miscViewController,\n            ILobbyGameStateController gameStateController)\n        {\n            parentFlowCoordinator = mainFlowCoordinator;\n            _settingsViewController = settingsViewController;\n            _environmentViewController = environmentViewController;\n            _miscViewController = miscViewController;\n            _gameStateController = gameStateController;\n        }\n\n        protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling)\n        {\n            if (firstActivation)\n            {\n                SetTitle(\"Multiplayer Preferences\");\n                showBackButton = true;\n            }\n            if (addedToHierarchy)\n            {\n                ProvideInitialViewControllers(_settingsViewController, _environmentViewController, _miscViewController);\n                _gameStateController.gameStartedEvent += DismissGameStartedEvent;\n            }\n        }\n\n        protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling)\n        {\n            if (removedFromHierarchy)\n                _gameStateController.gameStartedEvent -= DismissGameStartedEvent;\n        }\n\n        private void DismissGameStartedEvent(ILevelGameplaySetupData obj)\n        {\n            parentFlowCoordinator.DismissFlowCoordinator(this, null, ViewController.AnimationDirection.Horizontal, true);\n        }\n\n        protected override void BackButtonWasPressed(ViewController topViewController)\n        {\n            parentFlowCoordinator.DismissFlowCoordinator(this);\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Utilities/ColorConverter.cs",
    "content": "﻿using IPA.Config.Data;\nusing IPA.Config.Stores;\nusing System;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions.Utilities\n{\n    public class ColorConverter : ValueConverter<Color>\n    {\n        public override Color FromValue(Value? value, object parent)\n        {\n            if (value is not Text text) \n                throw new ArgumentException(\"Argument not Text\", nameof(value));\n            if (!ColorUtility.TryParseHtmlString(text.Value, out var color))\n                throw new ArgumentException(\"Could not parse HtmlString\", nameof(value));\n            return color;\n        }\n\n        public override Value? ToValue(Color obj, object parent)\n            => Value.Text($\"#{ColorUtility.ToHtmlStringRGB(obj)}\");\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/Utilities/SpriteManager.cs",
    "content": "﻿using SiraUtil.Logging;\nusing System;\nusing System.Reflection;\nusing System.Security.Policy;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Utilities\n{\n    public class SpriteManager : IInitializable, IDisposable\n    {\n        public Sprite IconOculus64 { get; private set; } = null!;\n        public Sprite IconSteam64 { get; private set; } = null!;\n        public Sprite IconMeta64 { get; private set; } = null!;\n        public Sprite IconToaster64 { get; private set; } = null!;\n\n        private readonly SiraLog _logger;\n\n        internal SpriteManager(\n            SiraLog logger)\n        {\n            _logger = logger;\n        }\n\n        public void Initialize()\n        {\n            IconOculus64 = GetSpriteFromResources(\"MultiplayerExtensions.Assets.IconOculus64.png\");\n            IconSteam64 = GetSpriteFromResources(\"MultiplayerExtensions.Assets.IconSteam64.png\");\n            IconMeta64 = GetSpriteFromResources(\"MultiplayerExtensions.Assets.IconMeta64.png\");\n            IconToaster64 = GetSpriteFromResources(\"MultiplayerExtensions.Assets.IconToaster64.png\");\n        }\n\n        public void Dispose()\n        {\n            if (IconOculus64 != null) Sprite.Destroy(IconOculus64);\n            IconOculus64 = null;\n            if (IconSteam64 != null) Sprite.Destroy(IconSteam64);\n            IconSteam64 = null;\n            if (IconMeta64 != null) Sprite.Destroy(IconMeta64);\n            IconMeta64 = null;\n            if (IconToaster64 != null) Sprite.Destroy(IconToaster64);\n            IconToaster64 = null;\n        }\n\n        private Sprite GetSpriteFromResources(string resourcePath, float pixelsPerUnit = 10.0f)\n        {\n            Sprite? sprite = GetSprite(GetResource(Assembly.GetCallingAssembly(), resourcePath), pixelsPerUnit);\n            if (sprite == null)\n                return null!;\n            sprite.name = resourcePath;\n            return sprite;\n        }\n\n        private byte[] GetResource(Assembly asm, string resourceName)\n        {\n            System.IO.Stream stream = asm.GetManifestResourceStream(resourceName);\n            byte[] data = new byte[stream.Length];\n            stream.Read(data, 0, (int)stream.Length);\n            return data;\n        }\n\n        public Sprite? GetSprite(byte[]? data, float pixelsPerUnit = 100.0f, bool returnDefaultOnFail = true)\n        {\n            Sprite? ReturnDefault(bool useDefault)\n            {\n                return null;\n            }\n            try\n            {\n                Texture2D texture = new Texture2D(2, 2);\n                if (data == null || data.Length == 0)\n                {\n                    //Logger?.Invoke($\"data seems to be null or empty.\", null);\n                    return ReturnDefault(returnDefaultOnFail);\n                }\n                texture.LoadImage(data);\n                return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0, 0), pixelsPerUnit);\n            }\n            catch (Exception ex)\n            {\n                _logger.Warn($\"Caught unhandled exception {ex.Message}\");\n                return ReturnDefault(returnDefaultOnFail);\n            }\n        }\n    }\n}\n"
  },
  {
    "path": "MultiplayerExtensions/manifest.json",
    "content": "{\n  \"$schema\": \"https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json\",\n  \"id\": \"MultiplayerExtensions\",\n  \"name\": \"MultiplayerExtensions\",\n  \"author\": \"Goobwabber\",\n  \"version\": \"1.0.3\",\n  \"description\": \"Expands the functionality of Beat Saber Multiplayer.\",\n  \"gameVersion\": \"1.24.0\",\n  \"dependsOn\": {\n    \"BSIPA\": \"^4.1.4\",\n    \"BeatSaberMarkupLanguage\": \"^1.5.1\",\n    \"SiraUtil\": \"^3.1.0\",\n    \"MultiplayerCore\": \"^1.0.0\"\n  },\n  \"links\": {\n    \"project-home\": \"https://github.com/Goobwabber/MultiplayerExtensions\",\n    \"donate\": \"https://github.com/Goobwabber/MultiplayerExtensions#donate\"\n  }\n}\n"
  },
  {
    "path": "MultiplayerExtensions.sln",
    "content": "﻿\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.30517.126\nMinimumVisualStudioVersion = 10.0.40219.1\nProject(\"{9A19103F-16F7-4668-BE54-9A1E7A4F7556}\") = \"MultiplayerExtensions\", \"MultiplayerExtensions\\MultiplayerExtensions.csproj\", \"{4641BE95-C3F7-4636-BA24-67188A38D94C}\"\nEndProject\nGlobal\n\tGlobalSection(SolutionConfigurationPlatforms) = preSolution\n\t\tDebug|Any CPU = Debug|Any CPU\n\t\tRelease|Any CPU = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(ProjectConfigurationPlatforms) = postSolution\n\t\t{4641BE95-C3F7-4636-BA24-67188A38D94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU\n\t\t{4641BE95-C3F7-4636-BA24-67188A38D94C}.Debug|Any CPU.Build.0 = Debug|Any CPU\n\t\t{4641BE95-C3F7-4636-BA24-67188A38D94C}.Release|Any CPU.ActiveCfg = Release|Any CPU\n\t\t{4641BE95-C3F7-4636-BA24-67188A38D94C}.Release|Any CPU.Build.0 = Release|Any CPU\n\tEndGlobalSection\n\tGlobalSection(SolutionProperties) = preSolution\n\t\tHideSolutionNode = FALSE\n\tEndGlobalSection\n\tGlobalSection(ExtensibilityGlobals) = postSolution\n\t\tSolutionGuid = {E8F0586B-E2CD-47E7-B61F-73A798A37ADA}\n\tEndGlobalSection\nEndGlobal\n"
  },
  {
    "path": "MultiplayerExtensions.v3.ncrunchsolution",
    "content": "﻿<SolutionConfiguration>\n  <Settings>\n    <AllowParallelTestExecution>True</AllowParallelTestExecution>\n    <SolutionConfigured>True</SolutionConfigured>\n  </Settings>\n</SolutionConfiguration>"
  },
  {
    "path": "README.md",
    "content": "# MultiplayerExtensions (Steam/PC-Only) [![Build](https://github.com/Goobwabber/MultiplayerExtensions/workflows/Build/badge.svg?event=push)](https://github.com/Goobwabber/MultiplayerExtensions/actions?query=workflow%3ABuild+branch%3Amaster)\nA Beat Saber mod which aims to improve the multiplayer experience.\n\n**THIS MOD DOES NOT ENABLE CUSTOM SONGS.** \n\n## Features\n* Fancy platform lighting\n* Fancy in-game lighting\n**Looking for more features, feel free to contribute your own.**\n\n## Installation\n1. Ensure you have the [required mods](https://github.com/Goobwabber/MultiplayerExtensions#requirements).\n2. Download the `MultiplayerExtensions` file listed under `Assets` **[Here](https://github.com/Goobwabber/MultiplayerExtensions/releases)**.\n   * Optionally, you can get a development build by downloading the file listed under `Artifacts`  **[Here](https://github.com/Goobwabber/MultiplayerExtensions/actions?query=workflow%3ABuild+branch%3Amaster)** (pick the topmost successful build).\n   * You must be logged into GitHub to download a development build.\n3. Extract the zip file to your Beat Saber game directory (the one `Beat Saber.exe` is in).\n   * The `MultiplayerExtensions.dll` (and `MultiplayerExtensions.pdb` if it exists) should end up in your `Plugins` folder (**NOT** the one in `Beat Saber_Data`).\n4. **Optional**: Edit `Beat Saber IPA.json` (in your `UserData` folder) and change `Debug` -> `ShowCallSource` to `true`. This will enable BSIPA to get file and line numbers from the `PDB` file where errors occur, which is very useful when reading the log files. This may have a *slight* impact on performance.\n\nLastly, check out [other mods](https://github.com/Goobwabber/MultiplayerExtensions#related-mods) that work well with MultiplayerExtensions!\n\n## Requirements\nThese can be downloaded from [BeatMods](https://beatmods.com/#/mods) or using Mod Assistant. **Do NOT use any of the DLLs in the `Refs` folder, they have been stripped of code and will not work.**\n* MultiplayerCore v1.0.0+\n* BeatSaberMarkupLanguage v1.5.1+\n* SiraUtil 3.0.0+\n\n## Reporting Issues\n* The best way to report issues is to click on the `Issues` tab at the top of the GitHub page. This allows any contributor to see the problem and attempt to fix it, and others with the same issue can contribute more information. **Please try the troubleshooting steps before reporting the issues listed there. Please only report issues after using the latest build, your problem may have already been fixed.**\n* Include in your issue:\n  * A detailed explanation of your problem (you can also attach videos/screenshots)\n  * **Important**: The log file from the game session the issue occurred (restarting the game creates a new log file).\n    * The log file can be found at `Beat Saber\\Logs\\_latest.log` (`Beat Saber` being the folder `Beat Saber.exe` is in).\n* If you ask for help on Discord, at least include your `_latest.log` file in your help request.\n\n## Contributing\nAnyone can feel free to contribute bug fixes or enhancements to MultiplayerExtensions. GitHub Actions for Pull Requests made from GitHub accounts that don't have direct access to the repository will fail. This is normal because the Action requires a `Secret` to download dependencies.\n### Building\nVisual Studio 2019 with the [BeatSaberModdingTools](https://github.com/Zingabopp/BeatSaberModdingTools) extension is the recommended development environment.\n1. Check out the repository\n2. Open `MultiplayerExtensions.sln`\n3. Right-click the `MultiplayerExtensions` project, go to `Beat Saber Modding Tools` -> `Set Beat Saber Directory`\n   * This assumes you have already set the directory for your Beat Saber game folder in `Extensions` -> `Beat Saber Modding Tools` -> `Settings...`\n   * If you do not have the BeatSaberModdingTools extension, you will need to manually create a `MultiplayerExtensions.csproj.user` file to set the location of your game install. An example is showing below.\n4. The project should now build.\n### Testing\nMultiplayerExtensions and other multiplayer mods may not work without a compatible private server to play on. The only one at this point in time is [BeatTogether](https://github.com/pythonology/BeatTogether), which comes in the form of it's [Master](https://github.com/pythonology/BeatTogether.MasterServer) and [Dedicated](https://github.com/pythonology/BeatTogether.DedicatedServer) servers. If you are looking to update this mod to a newer version, these servers will also need to be up to date and working for that version. You can gain access to their private beta by donating on their [patreon](https://www.patreon.com/BeatTogether). Alternatively, you can set up your own cluster.\n\n**Example csproj.user File:**\n```xml\n<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project ToolsVersion=\"Current\" xmlns=\"http://schemas.microsoft.com/developer/msbuild/2003\">\n  <PropertyGroup>\n    <BeatSaberDir>Full\\Path\\To\\Beat Saber</BeatSaberDir>\n  </PropertyGroup>\n</Project>\n```\n## Donate\nYou can support development of MultiplayerExtensions by donating at the following links:\n* https://www.patreon.com/goobwabber\n* https://ko-fi.com/goobwabber\n* https://ko-fi.com/zingabopp\n\n## Related Mods\n* [MultiplayerCore](https://github.com/Goobwabber/MultiplayerCore)\n* BeatTogether for [PC](https://github.com/pythonology/BeatTogether) or [Quest](https://github.com/pythonology/BeatTogether.Quest)\n* [BeatSaberServerBrowser](https://github.com/roydejong/BeatSaberServerBrowser)\n"
  }
]