master 38e9a0a28bce cached
49 files
114.7 KB
26.9k tokens
145 symbols
1 requests
Download .txt
Repository: Goobwabber/MultiplayerExtensions
Branch: master
Commit: 38e9a0a28bce
Files: 49
Total size: 114.7 KB

Directory structure:
gitextract_99ygdq5q/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── Build.yml
│       └── PR_Build.yml
├── .gitignore
├── LICENSE
├── MultiplayerExtensions/
│   ├── Config.cs
│   ├── Directory.Build.props
│   ├── Directory.Build.targets
│   ├── Environment/
│   │   ├── MpexAvatarNameTag.cs
│   │   ├── MpexAvatarPlaceLighting.cs
│   │   ├── MpexConnectedObjectManager.cs
│   │   ├── MpexLevelEndActions.cs
│   │   ├── MpexPlayerFacadeLighting.cs
│   │   └── MpexPlayerTableCell.cs
│   ├── Installers/
│   │   ├── MpexAppInstaller.cs
│   │   ├── MpexGameInstaller.cs
│   │   ├── MpexLobbyInstaller.cs
│   │   ├── MpexLocalActivePlayerInstaller.cs
│   │   └── MpexMenuInstaller.cs
│   ├── MultiplayerExtensions.csproj
│   ├── Patchers/
│   │   ├── AvatarPlacePatcher.cs
│   │   ├── ColorSchemePatcher.cs
│   │   ├── EnvironmentPatcher.cs
│   │   ├── MenuEnvironmentPatcher.cs
│   │   └── PlayerPositionPatcher.cs
│   ├── Patches/
│   │   ├── AvatarPoseRestrictionPatch.cs
│   │   ├── PlatformMovementPatch.cs
│   │   └── ResumeSpawningPatch.cs
│   ├── Players/
│   │   ├── MpexPlayerData.cs
│   │   └── MpexPlayerManager.cs
│   ├── Plugin.cs
│   ├── UI/
│   │   ├── MpexEnvironmentViewController.bsml
│   │   ├── MpexEnvironmentViewController.cs
│   │   ├── MpexGameplaySetup.bsml
│   │   ├── MpexGameplaySetup.cs
│   │   ├── MpexMiscViewController.bsml
│   │   ├── MpexMiscViewController.cs
│   │   ├── MpexSettingsViewController.bsml
│   │   ├── MpexSettingsViewController.cs
│   │   └── MpexSetupFlowCoordinator.cs
│   ├── Utilities/
│   │   ├── ColorConverter.cs
│   │   └── SpriteManager.cs
│   └── manifest.json
├── MultiplayerExtensions.sln
├── MultiplayerExtensions.v3.ncrunchsolution
└── README.md

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
###############################################################################
# Set default behavior to automatically normalize line endings.
###############################################################################
* text=auto

###############################################################################
# Set default behavior for command prompt diff.
#
# This is need for earlier builds of msysgit that does not have it on by
# default for csharp files.
# Note: This is only used by command line
###############################################################################
#*.cs     diff=csharp

###############################################################################
# Set the merge driver for project and solution files
#
# Merging from the command prompt will add diff markers to the files if there
# are conflicts (Merging from VS is not affected by the settings below, in VS
# the diff markers are never inserted). Diff markers may cause the following 
# file extensions to fail to load in VS. An alternative would be to treat
# these files as binary and thus will always conflict and require user
# intervention with every merge. To do so, just uncomment the entries below
###############################################################################
#*.sln       merge=binary
#*.csproj    merge=binary
#*.vbproj    merge=binary
#*.vcxproj   merge=binary
#*.vcproj    merge=binary
#*.dbproj    merge=binary
#*.fsproj    merge=binary
#*.lsproj    merge=binary
#*.wixproj   merge=binary
#*.modelproj merge=binary
#*.sqlproj   merge=binary
#*.wwaproj   merge=binary

###############################################################################
# behavior for image files
#
# image files are treated as binary by default.
###############################################################################
#*.jpg   binary
#*.png   binary
#*.gif   binary

###############################################################################
# diff behavior for common document formats
# 
# Convert binary document formats to text before diffing them. This feature
# is only available from the command line. Turn it on by uncommenting the 
# entries below.
###############################################################################
#*.doc   diff=astextplain
#*.DOC   diff=astextplain
#*.docx  diff=astextplain
#*.DOCX  diff=astextplain
#*.dot   diff=astextplain
#*.DOT   diff=astextplain
#*.pdf   diff=astextplain
#*.PDF   diff=astextplain
#*.rtf   diff=astextplain
#*.RTF   diff=astextplain


================================================
FILE: .github/FUNDING.yml
================================================
patreon: goobwabber
custom: ['https://ko-fi.com/goobwabber', 'https://ko-fi.com/zingabopp']


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a bug report
title: "[BUG] "
labels: ''
assignees: ''

---
**Multiplayer Extensions Version and Download Source**
<!--- i.e. v0.4.3 from Mod Assistant -->

**Your Platform**
<!--- PC/Quest, Steam/Oculus store. Also include other information such as if you're using Revive or Oculus Link, custom launch options. -->

**Describe the bug**
<!--- A clear and concise description of what the bug is. -->

**To Reproduce**
<!--- Steps to reproduce the behavior: -->
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error

**Expected behavior**
<!--- A clear and concise description of what you expected to happen. -->

**Log**
<!--- The log file from the game session the issue occurred (restarting the game creates a new log file). 
The 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. -->

**Screenshots/Video**
<!--- If applicable, add screenshots to help explain your problem. -->

**Additional context**
<!--- Add any other context about the problem here. -->


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] "
labels: ''
assignees: ''

---

**Is your feature request related to a problem? Please describe.**
<!--- A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] -->

**Describe the solution you'd like**
<!--- A clear and concise description of what you want to happen. -->

**Additional context**
<!--- Add any other context or screenshots about the feature request here. -->


================================================
FILE: .github/workflows/Build.yml
================================================
name: Build

on:
  push:
    branches: [ master ]
    paths:
      - 'MultiplayerExtensions.sln'
      - 'MultiplayerExtensions/**'
      - '.github/workflows/Build.yml'

jobs:
  Build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup dotnet
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 5.0.x
    - name: Fetch SIRA References
      uses: ProjectSIRA/download-sira-stripped@1.0.0
      with:
        manifest: ./MultiplayerExtensions/manifest.json
        sira-server-code: ${{ secrets.SIRA_SERVER_CODE }}
    - name: Fetch Mod References
      uses: Goobwabber/download-beatmods-deps@1.1
      with:
        manifest: ./MultiplayerExtensions/manifest.json
    - name: Build
      id: Build
      env: 
        FrameworkPathOverride: /usr/lib/mono/4.8-api
      run: dotnet build --configuration Release
    - name: GitStatus
      run: git status
    - name: Echo Filename
      run: echo $BUILDTEXT \($ASSEMBLYNAME\)
      env:
        BUILDTEXT: Filename=${{ steps.Build.outputs.filename }}
        ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }}
    - name: Upload Artifact
      uses: actions/upload-artifact@v1
      with:
        name: ${{ steps.Build.outputs.filename }}
        path: ${{ steps.Build.outputs.artifactpath }}


================================================
FILE: .github/workflows/PR_Build.yml
================================================
name: Pull Request Build

on:
  pull_request:
    branches: [ main ]
    paths:
      - 'MultiplayerExtensions.sln'
      - 'MultiplayerExtensions/**'
      - '.github/workflows/PR_Build.yml'

jobs:
  Build:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Setup dotnet
      uses: actions/setup-dotnet@v1
      with:
        dotnet-version: 5.0.x
    - name: Fetch SIRA References
      uses: ProjectSIRA/download-sira-stripped@1.0.0
      with:
        manifest: ./MultiplayerExtensions/manifest.json
        sira-server-code: ${{ secrets.SIRA_SERVER_CODE }}
    - name: Fetch Mod References
      uses: Goobwabber/download-beatmods-deps@1.1
      with:
        manifest: ./MultiplayerExtensions/manifest.json
    - name: Build
      id: Build
      env: 
        FrameworkPathOverride: /usr/lib/mono/4.8-api
      run: dotnet build --configuration Release
    - name: GitStatus
      run: git status
    - name: Echo Filename
      run: echo $BUILDTEXT \($ASSEMBLYNAME\)
      env:
        BUILDTEXT: Filename=${{ steps.Build.outputs.filename }}
        ASSEMBLYNAME: AssemblyName=${{ steps.Build.outputs.assemblyname }}
    - name: Upload Artifact
      uses: actions/upload-artifact@v1
      with:
        name: ${{ steps.Build.outputs.filename }}
        path: ${{ steps.Build.outputs.artifactpath }}


================================================
FILE: .gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- Backup*.rdl

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database
healthchecksdb

Refs/Beat Saber_Data/Managed/*
Refs/Plugins/*
Refs/Libs/Mono*
!Refs/Beat Saber_Data/Managed/IPA.Loader.dll
/bsinstalldir.txt

MultiplayerExtensions/Properties/launchSettings.json


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2020 Zingabopp

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

As an exception, all contents of the "Refs" directory, or any subdirectory named
"Refs", are not part of the Software and are therefore not subject to the License,
and remain the exclusive property of their copyright holders.

================================================
FILE: MultiplayerExtensions/Config.cs
================================================
using IPA.Config.Stores.Attributes;
using MultiplayerExtensions.Utilities;
using UnityEngine;

namespace MultiplayerExtensions
{
    public class Config
    {
        public static readonly Color DefaultPlayerColor = new Color(0.031f, 0.752f, 1f);

        public virtual bool SoloEnvironment { get; set; } = false;
        public virtual bool SideBySide { get; set; } = false;
        public virtual float SideBySideDistance { get; set; } = 4f;
        public virtual bool DisableAvatarConstraints { get; set; } = false;
        public virtual bool DisableMultiplayerPlatforms { get; set; } = false;
        public virtual bool DisableMultiplayerLights { get; set; } = false;
        public virtual bool DisableMultiplayerObjects { get; set; } = false;
        public virtual bool DisableMultiplayerColors { get; set; } = false;
        public virtual bool DisablePlatformMovement { get; set; } = false;
        public virtual bool MissLighting { get; set; } = false;
        public virtual bool PersonalMissLightingOnly { get; set; } = false;
        [UseConverter(typeof(ColorConverter))]
        public virtual Color PlayerColor { get; set; } = DefaultPlayerColor;
        [UseConverter(typeof(ColorConverter))]
        public virtual Color MissColor { get; set; } = new Color(1, 0, 0);
    }
}


================================================
FILE: MultiplayerExtensions/Directory.Build.props
================================================
<?xml version="1.0" encoding="utf-8"?>
<!-- This file contains project properties used by the build. -->
<Project>
  <PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
    <ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
    <DisableCopyToPlugins>true</DisableCopyToPlugins>
    <DisableZipRelease>true</DisableZipRelease>
  </PropertyGroup>
  <ItemGroup Condition="'$(GITHUB_ACTIONS)' == 'true'">
    <SourceRoot Include="$(MSBuildThisFileDirectory)/"/>
  </ItemGroup>
  <PropertyGroup Condition="'$(NCrunch)' == '1'">
    <ContinuousIntegrationBuild>false</ContinuousIntegrationBuild>
    <DisableCopyToPlugins>true</DisableCopyToPlugins>
    <DisableZipRelease>true</DisableZipRelease>
  </PropertyGroup>
</Project>

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

================================================
FILE: MultiplayerExtensions/Environment/MpexAvatarNameTag.cs
================================================
using System;
using System.Collections.Generic;
using HMUI;
using MultiplayerCore.Players;
using MultiplayerExtensions.Players;
using MultiplayerExtensions.Utilities;
using UnityEngine;
using UnityEngine.UI;
using Zenject;

namespace MultiplayerExtensions.Environments.Lobby
{
    public class MpexAvatarNameTag : MonoBehaviour
    {
        enum PlayerIconSlot
        {
            Platform = 0
        }
        
        private readonly Dictionary<PlayerIconSlot, ImageView> _playerIcons = new();

        private IConnectedPlayer _player = null!;
        private MpPlayerManager _playerManager = null!;
        private MpexPlayerManager _mpexPlayerManager = null!;
        private SpriteManager _spriteManager = null!;
        private ImageView _bg = null!;
        private CurvedTextMeshPro _nameText = null!;

        [Inject]
        internal void Construct(
            IConnectedPlayer player,
            MpPlayerManager playerManager,
            MpexPlayerManager mpexPlayerManager,
            SpriteManager spriteManager)
        {
            _player = player;
            _playerManager = playerManager;
            _mpexPlayerManager = mpexPlayerManager;
            _spriteManager = spriteManager;
        }

        private void Awake()
        {
            // Get references
            _bg = transform.Find("BG").GetComponent<ImageView>();
            _nameText = transform.Find("Name").GetComponent<CurvedTextMeshPro>();
            
            // Enable horizontal layout on bg
            if (!_bg.TryGetComponent<HorizontalLayoutGroup>(out _))
            {
                var hLayout = _bg.gameObject.AddComponent<HorizontalLayoutGroup>();
                hLayout.childAlignment = TextAnchor.MiddleCenter;
                hLayout.childForceExpandWidth = false;
                hLayout.childForceExpandHeight = false;
                hLayout.childScaleWidth = false;
                hLayout.childScaleHeight = false;
                hLayout.spacing = 4f;
            }

            // Re-nest name onto bg
            _nameText.transform.SetParent(_bg.transform, false);
            
            // Take control of name tag
            if (_nameText.TryGetComponent<ConnectedPlayerName>(out var nativeNameScript))
                Destroy(nativeNameScript);
            _nameText.text = "Player";

            // Set player data
            _nameText.text = _player.userName;
            _nameText.color = Color.white;
            if (_mpexPlayerManager.TryGetPlayer(_player.userId, out var mpexData))
                _nameText.color = mpexData.Color;
            if (_playerManager.TryGetPlayer(_player.userId, out var data))
                SetPlatformData(data);
        }

        private void OnEnable()
        {
            _playerManager.PlayerConnectedEvent += HandlePlatformData;
            _mpexPlayerManager.PlayerConnectedEvent += HandleMpexData;
        }

        private void OnDisable()
        {
            _playerManager.PlayerConnectedEvent -= HandlePlatformData;
            _mpexPlayerManager.PlayerConnectedEvent -= HandleMpexData;
        }

        private void HandlePlatformData(IConnectedPlayer player, MpPlayerData data)
        {
            if (player == _player)
                SetPlatformData(data);
        }

        private void HandleMpexData(IConnectedPlayer player, MpexPlayerData data)
        {
            if (player == _player)
                _nameText.color = data.Color;
        }

        private void SetPlatformData(MpPlayerData data)
        {
            Sprite icon = null;
            switch (data.Platform)
            {
                case Platform.Steam:
                    icon = _spriteManager.IconSteam64;
                    break;
                case Platform.OculusQuest:
                    icon = _spriteManager.IconMeta64;
                    break;
                case Platform.OculusPC:
                    icon = _spriteManager.IconOculus64;
                    break;
                default:
                    icon = _spriteManager.IconToaster64;
                    break;
            }
            SetIcon(PlayerIconSlot.Platform, icon);
        }

        private void SetIcon(PlayerIconSlot slot, Sprite sprite)
        {
            if (!_playerIcons.TryGetValue(slot, out ImageView imageView))
            {
                var iconObj = new GameObject($"MpexPlayerIcon({slot})");
                iconObj.transform.SetParent(_bg.transform, false);
                iconObj.transform.SetSiblingIndex((int)slot);
                iconObj.layer = 5;

                iconObj.AddComponent<CanvasRenderer>();
                
                imageView = iconObj.AddComponent<ImageView>();
                imageView.maskable = true;
                imageView.fillCenter = true;
                imageView.preserveAspect = true;
                imageView.material = _bg.material; // No Glow Billboard material
                _playerIcons[slot] = imageView;

                var rectTransform = iconObj.GetComponent<RectTransform>();
                rectTransform.localScale = new Vector3(3.2f, 3.2f);
            }

            imageView.sprite = sprite;
            
            _nameText.transform.SetSiblingIndex(999);
        }
    }
}

================================================
FILE: MultiplayerExtensions/Environment/MpexAvatarPlaceLighting.cs
================================================
using IPA.Utilities;
using MultiplayerExtensions.Players;
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.Environments
{
    public class MpexAvatarPlaceLighting : MonoBehaviour
    {
        public const float SmoothTime = 2f;
        public Color TargetColor { get; private set; } = Color.black;
        public int SortIndex { get; internal set; }

        private List<TubeBloomPrePassLight> _lights = new List<TubeBloomPrePassLight>();

        private IMultiplayerSessionManager _sessionManager = null!;
        private MenuLightsManager _lightsManager = null!;
        private MpexPlayerManager _mpexPlayerManager = null!;
        private Config _config = null!;

        [Inject]
        internal void Construct(
            IMultiplayerSessionManager sessionManager,
            MenuLightsManager lightsManager,
            MpexPlayerManager mpexPlayerManager,
            Config config)
        {
            _sessionManager = sessionManager;
            _lightsManager = lightsManager;
            _mpexPlayerManager = mpexPlayerManager;
            _config = config;
        }

        private void Start()
        {
            _lights = GetComponentsInChildren<TubeBloomPrePassLight>().ToList();

            if (_sessionManager == null || _lightsManager == null || _mpexPlayerManager == null || _sessionManager.localPlayer == null)
                return;

            if (_sessionManager.localPlayer.sortIndex == SortIndex)
            {
                SetColor(_config.PlayerColor, true);
                return;
            }

            foreach (var player in _sessionManager.connectedPlayers)
                if (player.sortIndex == SortIndex)
                {
                    SetColor(_mpexPlayerManager.GetPlayer(player.userId)?.Color ?? Config.DefaultPlayerColor, true);
                    return;
                }

            SetColor(Color.black);
        }

        private void OnEnable()
        {
            _mpexPlayerManager.PlayerConnectedEvent += HandlePlayerData;
            _sessionManager.playerConnectedEvent += HandlePlayerConnected;
            _sessionManager.playerDisconnectedEvent += HandlePlayerDisconnected;
        }

        private void OnDisable()
        {
            _mpexPlayerManager.PlayerConnectedEvent -= HandlePlayerData;
            _sessionManager.playerConnectedEvent -= HandlePlayerConnected;
            _sessionManager.playerDisconnectedEvent -= HandlePlayerDisconnected;
        }

        private void HandlePlayerData(IConnectedPlayer player, MpexPlayerData data)
        {
            if (player.sortIndex == SortIndex)
                SetColor(data.Color, false);
        }

        private void HandlePlayerConnected(IConnectedPlayer player)
        {
            if (player.sortIndex != SortIndex)
                return;
            if (_mpexPlayerManager.TryGetPlayer(player.userId, out MpexPlayerData data))
                SetColor(data.Color, false);
            else
                SetColor(Config.DefaultPlayerColor, false);
        }

        private void HandlePlayerDisconnected(IConnectedPlayer player)
        {
            if (player.sortIndex == SortIndex)
                SetColor(Color.black, false);
        }

        private void Update()
        {
            Color current = GetColor();
            if (current == TargetColor)
                return;
            if (_lightsManager.IsColorVeryCloseToColor(current, TargetColor))
                SetColor(TargetColor);
            else
                SetColor(Color.Lerp(current, TargetColor, Time.deltaTime * SmoothTime));
        }

        public void SetColor(Color color, bool immediate)
        {
            TargetColor = color;
            if (immediate)
                SetColor(color);
        }

        public Color GetColor()
        {
            if (_lights.Count > 0)
                return _lights[0].color;
            return Color.black;
        }

        private void SetColor(Color color)
        {
            foreach(TubeBloomPrePassLight light in _lights)
            {
                light.color = color;
                light.Refresh();
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/Environment/MpexConnectedObjectManager.cs
================================================
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.Environment
{
    public class MpexConnectedObjectManager : MonoBehaviour
    {
        private MultiplayerConnectedPlayerSpectatingSpot _playerSpectatingSpot = null!;
        private IConnectedPlayerBeatmapObjectEventManager _beatmapObjectEventManager = null!;
        private BeatmapObjectManager _beatmapObjectManager = null!;
        private Config _config = null!;

        [Inject]
        internal void Construct(
            MultiplayerConnectedPlayerSpectatingSpot playerSpectatingSpot,
            IConnectedPlayerBeatmapObjectEventManager beatmapObjectEventManager,
            BeatmapObjectManager beatmapObjectManager,
            Config config)
        {
            _playerSpectatingSpot = playerSpectatingSpot;
            _beatmapObjectEventManager = beatmapObjectEventManager;
            _beatmapObjectManager = beatmapObjectManager;
            _config = config;
        }

        private void Start()
        {
            _playerSpectatingSpot.isObservedChangedEvent += HandleIsObservedChangedEvent;
            if (_config.DisableMultiplayerObjects)
                _beatmapObjectEventManager.Pause();
        }

        private void OnDestroy()
        {
            if (_playerSpectatingSpot != null)
                _playerSpectatingSpot.isObservedChangedEvent -= HandleIsObservedChangedEvent;
        }

        private void HandleIsObservedChangedEvent(bool isObserved)
        {
            if (_config.DisableMultiplayerPlatforms)
                transform.Find("Construction").gameObject.SetActive(isObserved);
            if (_config.DisableMultiplayerLights)
                transform.Find("Lasers").gameObject.SetActive(isObserved);
            if (!_config.DisableMultiplayerObjects)
                return;
            if (isObserved)
            {
                _beatmapObjectEventManager.Resume();
                return;
            }
            _beatmapObjectEventManager.Pause();
            _beatmapObjectManager.DissolveAllObjects();
        }
    }
}


================================================
FILE: MultiplayerExtensions/Environment/MpexLevelEndActions.cs
================================================
using SiraUtil.Affinity;
using System;

namespace MultiplayerExtensions.Environment
{
    public class MpexLevelEndActions : IAffinity, ILevelEndActions
    {
        public event Action levelFailedEvent = null!;
        public event Action levelFinishedEvent = null!;

        [AffinityPrefix]
        [AffinityPatch(typeof(MultiplayerLocalActivePlayerFacade), "ReportPlayerDidFinish")]
        private void PlayerDidFinish() =>
            levelFinishedEvent?.Invoke();

        [AffinityPrefix]
        [AffinityPatch(typeof(MultiplayerLocalActivePlayerFacade), "ReportPlayerNetworkDidFailed")]
        private void PlayerDidFail() =>
            levelFailedEvent?.Invoke();
    }
}


================================================
FILE: MultiplayerExtensions/Environment/MpexPlayerFacadeLighting.cs
================================================
using IPA.Utilities;
using System;
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.Environment
{
    class MpexPlayerFacadeLighting : MonoBehaviour
    {
        private readonly FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>.Accessor _allLightsAnimators
            = FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>
                .GetAccessor(nameof(_allLightsAnimators));
        private readonly FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>.Accessor _gameplayLightsAnimators
            = FieldAccessor<MultiplayerGameplayAnimator, LightsAnimator[]>
                .GetAccessor(nameof(_gameplayLightsAnimators));

        private readonly FieldAccessor<MultiplayerGameplayAnimator, ColorSO>.Accessor _activeLightsColor
            = FieldAccessor<MultiplayerGameplayAnimator, ColorSO>
                .GetAccessor(nameof(_activeLightsColor));
        private readonly FieldAccessor<MultiplayerGameplayAnimator, ColorSO>.Accessor _leadingLightsColor
            = FieldAccessor<MultiplayerGameplayAnimator, ColorSO>
                .GetAccessor(nameof(_leadingLightsColor));
        private readonly FieldAccessor<MultiplayerGameplayAnimator, ColorSO>.Accessor _failedLightsColor
            = FieldAccessor<MultiplayerGameplayAnimator, ColorSO>
                .GetAccessor(nameof(_failedLightsColor));

        private LightsAnimator[] _allLights => _allLightsAnimators(ref _gameplayAnimator);
        private LightsAnimator[] _gameplayLights => _gameplayLightsAnimators(ref _gameplayAnimator);

        private ColorSO _activeColor => _activeLightsColor(ref _gameplayAnimator);
        private ColorSO _leadingColor => _leadingLightsColor(ref _gameplayAnimator);
        private ColorSO _failedColor => _failedLightsColor(ref _gameplayAnimator);

        private bool _isLeading = false;
        private int _highestCombo = 0;

        private IConnectedPlayer _connectedPlayer = null!;
        private MultiplayerController _multiplayerController = null!;
        private IScoreSyncStateManager _scoreProvider = null!;
        private MultiplayerLeadPlayerProvider _leadPlayerProvider = null!;
        private MultiplayerGameplayAnimator _gameplayAnimator = null!;
        private MultiplayerSyncState<StandardScoreSyncState, StandardScoreSyncState.Score, int> _syncState = null!;
        private Config _config = null!;

        [Inject]
        internal void Construct(
            IConnectedPlayer connectedPlayer, 
            MultiplayerController multiplayerController, 
            IScoreSyncStateManager scoreProvider, 
            MultiplayerLeadPlayerProvider leadPlayerProvider,
            Config config)
        {
            _connectedPlayer = connectedPlayer;
            _multiplayerController = multiplayerController;
            _scoreProvider = scoreProvider;
            _leadPlayerProvider = leadPlayerProvider;
            _config = config;
        }

        public void OnEnable()
        {
            _gameplayAnimator = GetComponentInChildren<MultiplayerGameplayAnimator>();
            _syncState = _scoreProvider.GetSyncStateForPlayer(_connectedPlayer);
            _leadPlayerProvider.newLeaderWasSelectedEvent += HandleNewLeaderWasSelected;
        }

        public void OnDisable()
        {
            _leadPlayerProvider.newLeaderWasSelectedEvent -= HandleNewLeaderWasSelected;
        }

        private void HandleNewLeaderWasSelected(string userId)
        {
            _isLeading = userId == _connectedPlayer.userId;
        }

        private void Update()
        {
            if (_multiplayerController.state == MultiplayerController.State.Gameplay
                && !_connectedPlayer.IsFailed())
            {
                int combo = _syncState.GetState(StandardScoreSyncState.Score.Combo, _syncState.player.offsetSyncTime);
                if (combo > _highestCombo)
                    _highestCombo = combo;

                Color baseColor = _isLeading ? _leadingColor : _activeColor;
                float failPercentage = (Mathf.Min(_highestCombo, 20f) - combo) / 20f;
                Color color = _config.MissColor;
                color.a = baseColor.a;
                SetLights(Color.Lerp(baseColor, color, failPercentage));
            }
        }

        public void SetLights(Color color)
        {
            foreach (LightsAnimator light in _gameplayLightsAnimators(ref _gameplayAnimator))
                light.SetColor(color);
        }
    }
}


================================================
FILE: MultiplayerExtensions/Environment/MpexPlayerTableCell.cs
================================================
using MultiplayerCore.Objects;
using SiraUtil.Affinity;
using System;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.UI;
using Zenject;

namespace MultiplayerExtensions.Objects
{
    public class MpexPlayerTableCell : IInitializable, IDisposable, IAffinity
    {
        private readonly ServerPlayerListViewController _playerListView;
        private readonly MpEntitlementChecker _entitlementChecker;
        private readonly ILobbyPlayersDataModel _playersDataModel;
        private readonly IMenuRpcManager _menuRpcManager;

        private static float alphaIsMe = 0.4f;
        private static float alphaIsNotMe = 0.2f;

        private static Color green = new Color(0f, 1f, 0f, 1f);
        private static Color yellow = new Color(0.125f, 0.75f, 1f, 1f);
        private static Color red = new Color(1f, 0f, 0f, 1f);
        private static Color normal = new Color(0.125f, 0.75f, 1f, 0.1f);

        internal MpexPlayerTableCell(
            ServerPlayerListViewController playerListView,
            NetworkPlayerEntitlementChecker entitlementChecker,
            ILobbyPlayersDataModel playersDataModel,
            IMenuRpcManager menuRpcManager)
        {
            _playerListView = playerListView;
            _entitlementChecker = (entitlementChecker as MpEntitlementChecker)!;
            _playersDataModel = playersDataModel;
            _menuRpcManager = menuRpcManager;
        }

        public void Initialize() 
        {
            _menuRpcManager.setIsEntitledToLevelEvent += HandleSetIsEntitledToLevel;
        }

        public void Dispose()
        {
            _menuRpcManager.setIsEntitledToLevelEvent -= HandleSetIsEntitledToLevel;
        }

        [AffinityPrefix]
        [AffinityPatch(typeof(GameServerPlayerTableCell), nameof(GameServerPlayerTableCell.SetData))]
        public void SetDataPrefix(IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData, bool hasKickPermissions, bool allowSelection, Task<AdditionalContentModel.EntitlementStatus> getLevelEntitlementTask, Image ____localPlayerBackgroundImage)
        {
            if (getLevelEntitlementTask != null)
                getLevelEntitlementTask = Task.FromResult(AdditionalContentModel.EntitlementStatus.Owned);
        }

        [AffinityPostfix]
        [AffinityPatch(typeof(GameServerPlayerTableCell), nameof(GameServerPlayerTableCell.SetData))]
        public void SetDataPostfix(IConnectedPlayer connectedPlayer, ILobbyPlayerData playerData, bool hasKickPermissions, bool allowSelection, Task<AdditionalContentModel.EntitlementStatus> getLevelEntitlementTask, Image ____localPlayerBackgroundImage)
        {
            ____localPlayerBackgroundImage.enabled = true;
            string? hostSelectedLevel = _playersDataModel[_playersDataModel.partyOwnerId].beatmapLevel?.beatmapLevel?.levelID;
            if (hostSelectedLevel == null)
            {
                SetLevelEntitlement(____localPlayerBackgroundImage, EntitlementsStatus.Unknown);
                return;
            }

            EntitlementsStatus entitlement = EntitlementsStatus.Unknown;
            if (!connectedPlayer.isMe)
                entitlement = _entitlementChecker.GetUserEntitlementStatusWithoutRequest(connectedPlayer.userId, hostSelectedLevel);
            // TODO: change color for local player
            if (entitlement != EntitlementsStatus.Unknown)
                SetLevelEntitlement(____localPlayerBackgroundImage, entitlement);
            else if (!connectedPlayer.isMe)
            {
                // This might be a bad idea, race condition can cause packets that scale with the amount of players
                _entitlementChecker.GetUserEntitlementStatus(connectedPlayer.userId, hostSelectedLevel);
            }
        }

        private void SetLevelEntitlement(Image backgroundImage, EntitlementsStatus status)
        {
            Color backgroundColor = status switch
            {
                EntitlementsStatus.Ok => green,
                EntitlementsStatus.NotOwned => red,
                _ => normal,
            };

            backgroundColor.a = alphaIsNotMe;
            backgroundImage.color = backgroundColor;
        }

        private void HandleSetIsEntitledToLevel(string userId, string levelId, EntitlementsStatus status)
            => _playerListView.SetDataToTable();
    }
}


================================================
FILE: MultiplayerExtensions/Installers/MpexAppInstaller.cs
================================================
using IPA.Loader;
using MultiplayerExtensions.Patchers;
using MultiplayerExtensions.Players;
using MultiplayerExtensions.Utilities;
using SiraUtil.Zenject;
using Zenject;

namespace MultiplayerExtensions.Installers
{
	class MpexAppInstaller : Installer
	{
		private readonly Config _config;

		public MpexAppInstaller(
			Config config)
        {
			_config = config;
        }

        public override void InstallBindings()
        {
			Container.BindInstance(_config).AsSingle();
			Container.BindInterfacesAndSelfTo<SpriteManager>().AsSingle();
			Container.BindInterfacesAndSelfTo<MpexPlayerManager>().AsSingle();
			Container.BindInterfacesAndSelfTo<EnvironmentPatcher>().AsSingle();
		}
	}
}


================================================
FILE: MultiplayerExtensions/Installers/MpexGameInstaller.cs
================================================
using IPA.Utilities;
using MultiplayerExtensions.Environment;
using MultiplayerExtensions.Patchers;
using MultiplayerExtensions.Players;
using SiraUtil.Extras;
using SiraUtil.Objects.Multiplayer;
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.Installers
{
    class MpexGameInstaller : Installer
    {
        public override void InstallBindings()
        {
            Container.BindInterfacesAndSelfTo<PlayerPositionPatcher>().AsSingle();
            Container.BindInterfacesAndSelfTo<ColorSchemePatcher>().AsSingle();
            Container.RegisterRedecorator(new LocalActivePlayerRegistration(DecorateLocalActivePlayerFacade));
            Container.RegisterRedecorator(new LocalActivePlayerDuelRegistration(DecorateLocalActivePlayerFacade));
            Container.RegisterRedecorator(new ConnectedPlayerRegistration(DecorateConnectedPlayerFacade));
            Container.RegisterRedecorator(new ConnectedPlayerDuelRegistration(DecorateConnectedPlayerFacade));
        }

        private MultiplayerLocalActivePlayerFacade DecorateLocalActivePlayerFacade(MultiplayerLocalActivePlayerFacade original)
        {
            if (Plugin.Config.MissLighting)
            original.gameObject.AddComponent<MpexPlayerFacadeLighting>();
            return original;
        }

        private MultiplayerConnectedPlayerFacade DecorateConnectedPlayerFacade(MultiplayerConnectedPlayerFacade original)
        {
            if (Plugin.Config.MissLighting && !Plugin.Config.PersonalMissLightingOnly)
                original.gameObject.AddComponent<MpexPlayerFacadeLighting>();
            original.gameObject.AddComponent<MpexConnectedObjectManager>();
            return original;
        }
    }
}


================================================
FILE: MultiplayerExtensions/Installers/MpexLobbyInstaller.cs
================================================
using MultiplayerExtensions.Environments;
using MultiplayerExtensions.Environments.Lobby;
using SiraUtil.Extras;
using SiraUtil.Objects.Multiplayer;
using Zenject;

namespace MultiplayerExtensions.Installers
{
    class MpexLobbyInstaller : Installer
    {
        public override void InstallBindings()
        {
            Container.RegisterRedecorator(new LobbyAvatarPlaceRegistration(DecorateAvatarPlace));
            Container.RegisterRedecorator(new LobbyAvatarRegistration(DecorateAvatar));
        }

        private MultiplayerLobbyAvatarPlace DecorateAvatarPlace(MultiplayerLobbyAvatarPlace original)
        {
            original.gameObject.AddComponent<MpexAvatarPlaceLighting>();
            return original;
        }

        private MultiplayerLobbyAvatarController DecorateAvatar(MultiplayerLobbyAvatarController original)
        {
            var avatarCaption = original.transform.Find("AvatarCaption").gameObject;
            avatarCaption.AddComponent<MpexAvatarNameTag>();

            return original;
        }
    }
}


================================================
FILE: MultiplayerExtensions/Installers/MpexLocalActivePlayerInstaller.cs
================================================
using MultiplayerExtensions.Environment;
using MultiplayerExtensions.Patchers;
using Zenject;

namespace MultiplayerExtensions.Installers
{
    public class MpexLocalActivePlayerInstaller : MonoInstaller
    {
        public override void InstallBindings()
        {
            // stuff needed for solo environments to work
            Container.BindInterfacesAndSelfTo<MpexLevelEndActions>().AsSingle();
            Container.Bind<EnvironmentContext>().FromInstance(EnvironmentContext.Gameplay).AsSingle();
        }
    }
}


================================================
FILE: MultiplayerExtensions/Installers/MpexMenuInstaller.cs
================================================
using MultiplayerExtensions.Environments;
using MultiplayerExtensions.Objects;
using MultiplayerExtensions.Patchers;
using MultiplayerExtensions.UI;
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.Installers
{
    class MpexMenuInstaller : Installer
    {
        public override void InstallBindings()
        {
            //Container.BindInterfacesAndSelfTo<MpexPlayerTableCell>().AsSingle();
            Container.BindInterfacesAndSelfTo<AvatarPlacePatcher>().AsSingle();
            Container.BindInterfacesAndSelfTo<MenuEnvironmentPatcher>().AsSingle();

            Container.BindInterfacesAndSelfTo<MpexSetupFlowCoordinator>().FromNewComponentOnNewGameObject().AsSingle();
            Container.BindInterfacesAndSelfTo<MpexSettingsViewController>().FromNewComponentAsViewController().AsSingle();
            Container.BindInterfacesAndSelfTo<MpexEnvironmentViewController>().FromNewComponentAsViewController().AsSingle();
            Container.BindInterfacesAndSelfTo<MpexMiscViewController>().FromNewComponentAsViewController().AsSingle();
            Container.BindInterfacesAndSelfTo<MpexGameplaySetup>().AsSingle();

            // needed for local player's player place
            var avatarPlace = Container.Resolve<MenuEnvironmentManager>().transform.Find("MultiplayerLobbyEnvironment").Find("LobbyAvatarPlace").gameObject;
            GameObject.Destroy(avatarPlace.GetComponent<MpexAvatarPlaceLighting>());
            Container.Inject(avatarPlace.AddComponent<MpexAvatarPlaceLighting>());
        }
    }
}


================================================
FILE: MultiplayerExtensions/MultiplayerExtensions.csproj
================================================
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Library</OutputType>
    <AppDesignerFolder>Properties</AppDesignerFolder>
    <AssemblyName>MultiplayerExtensions</AssemblyName>
    <AssemblyVersion>1.0.3</AssemblyVersion>
    <TargetFramework>net472</TargetFramework>
    <DebugSymbols>true</DebugSymbols>
    <DebugType>portable</DebugType>
    <LocalRefsDir Condition="Exists('..\Refs')">..\Refs</LocalRefsDir>
    <BeatSaberDir>$(LocalRefsDir)</BeatSaberDir>
    <!--<PathMap>$(AppOutputBase)=X:\$(AssemblyName)\</PathMap>-->
    <ErrorReport>prompt</ErrorReport>
    <WarningLevel>4</WarningLevel>
    <LangVersion>9.0</LangVersion>
    <Nullable>enable</Nullable>
    <VersionType>Unofficial</VersionType>
    <CommitHash>local</CommitHash>
    <GitBranch></GitBranch>
    <GitModified></GitModified>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
    <Optimize>false</Optimize>
    <DefineConstants>DEBUG;TRACE</DefineConstants>
  </PropertyGroup>
  <PropertyGroup Condition=" '$(Configuration)' == 'Release' ">
    <Optimize>true</Optimize>
  </PropertyGroup>
  <PropertyGroup Condition="$(DefineConstants.Contains('CIBuild')) OR '$(NCrunch)' == '1'">
    <DisableCopyToPlugins>True</DisableCopyToPlugins>
  </PropertyGroup>
  <PropertyGroup Condition="'$(NCrunch)' == '1'">
    <DisableCopyToPlugins>True</DisableCopyToPlugins>
    <DisableZipRelease>True</DisableZipRelease>
  </PropertyGroup>
  <ItemGroup>
    <Reference Include="BeatmapCore">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="BGNet">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\BGNet.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="Colors">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="GameplayCore">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="Hive.Versioning">
      <HintPath>$(BeatSaberDir)\Libs\Hive.Versioning.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="HMRendering">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\HMRendering.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="LiteNetLib">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\LiteNetLib.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="netstandard">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\netstandard.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="Polyglot">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Polyglot.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="SemVer">
      <HintPath>$(BeatSaberDir)\Libs\SemVer.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="System.IO.Compression">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\System.IO.Compression.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="Main">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="HMLib">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="HMUI">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="IPA.Loader">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Unity.TextMeshPro">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Unity.Timeline">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Unity.Timeline.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.AudioModule">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.AudioModule.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="UnityEngine.CoreModule">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.DirectorModule">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.DirectorModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.ImageConversionModule">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.ImageConversionModule.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="UnityEngine.TextRenderingModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll</HintPath>
    </Reference>
    <Reference Include="UnityEngine.UI">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.UIElementsModule">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="UnityEngine.UIModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll</HintPath>
    </Reference>
    <Reference Include="UnityEngine.VRModule">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Zenject">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="Zenject-usage">
      <HintPath>$(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="0Harmony">
      <HintPath>$(BeatSaberDir)\Libs\0Harmony.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="BSML">
      <HintPath>$(BeatSaberDir)\Plugins\BSML.dll</HintPath>
      <Private>False</Private>
      <SpecificVersion>False</SpecificVersion>
    </Reference>
    <Reference Include="SiraUtil">
      <HintPath>$(BeatSaberDir)\Plugins\SiraUtil.dll</HintPath>
      <Private>False</Private>
    </Reference>
    <Reference Include="System" />
    <Reference Include="System.Core" />
    <Reference Include="System.Xml.Linq" />
    <Reference Include="System.Data.DataSetExtensions" />
    <Reference Include="System.Data" />
    <Reference Include="System.Xml" />
  </ItemGroup>
  <ItemGroup>
    <EmbeddedResource Include="Assets\IconMeta64.png" />
    <EmbeddedResource Include="Assets\IconToaster64.png" />
    <EmbeddedResource Include="manifest.json" />
    <None Remove="Assets\IconMeta64.png" />
    <None Remove="Assets\IconSteam64.png" />
    <None Remove="Assets\IconToaster64.png" />
    <None Remove="UI\MpexEnvironmentViewController.bsml" />
    <None Remove="UI\MpexGameplaySetup.bsml" />
    <None Remove="UI\MpexMiscViewController.bsml" />
    <None Remove="UI\MpexSettingsViewController.bsml" />
    <EmbeddedResource Include="Assets\IconSteam64.png" />
    <None Remove="Assets\IconOculus64.png" />
    <EmbeddedResource Include="Assets\IconOculus64.png" />
    <EmbeddedResource Include="UI\MpexGameplaySetup.bsml" />
    <EmbeddedResource Include="UI\MpexMiscViewController.bsml" />
    <EmbeddedResource Include="UI\MpexEnvironmentViewController.bsml" />
    <EmbeddedResource Include="UI\MpexSettingsViewController.bsml" />
  </ItemGroup>
  <ItemGroup>
    <None Include="Directory.Build.props" Condition="Exists('Directory.Build.props')" />
    <None Include="Directory.Build.targets" Condition="Exists('Directory.Build.targets')" />
    <None Include="MultiplayerExtensions.csproj.user" Condition="Exists('MultiplayerExtensions.csproj.user')" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="BeatSaberModdingTools.Tasks">
      <Version>1.3.2</Version>
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
      <PrivateAssets>all</PrivateAssets>
    </PackageReference>
    <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
  </ItemGroup>
  <ItemGroup>
    <PackageReference Include="BepInEx.AssemblyPublicizer.MSBuild" Version="0.4.1" PrivateAssets="all" />
    <!-- Publicize directly when referencing -->
    <Reference Include="$(BeatSaberDir)\Plugins\MultiplayerCore.dll" Publicize="true" />
  </ItemGroup>
  <ItemGroup>
    <Folder Include="Assets" />
  </ItemGroup>
  <Target Name="PreBuild" BeforeTargets="BeforeBuild" Condition="'$(NCRUNCH)' != '1'">
    <Error Text="The BeatSaberModdingTools.Tasks nuget package doesn't seem to be installed." Condition="'$(BSMTTaskAssembly)' == ''" />
    <GetCommitInfo ProjectDir="$(ProjectDir)">
      <Output TaskParameter="CommitHash" PropertyName="CommitHash" />
      <Output TaskParameter="Branch" PropertyName="GitBranch" />
      <Output TaskParameter="IsPullRequest" PropertyName="IsPullRequest" />
      <Output TaskParameter="Modified" PropertyName="GitModified" />
      <Output TaskParameter="GitUser" PropertyName="GitUser" />
    </GetCommitInfo>
    <PropertyGroup Condition="'$(GITHUB_ACTIONS)' == 'true' AND '$(GitUser)' == 'Zingabopp' AND '$(IsPullRequest)' != 'true'">
      <VersionType>Official</VersionType>
    </PropertyGroup>
    <PropertyGroup Condition="'$(GitModified)' != 'Modified'">
      <InformationalVersion>$(VersionType)-$(GitBranch)-$(CommitHash)</InformationalVersion>
    </PropertyGroup>
    <PropertyGroup Condition="'$(GitModified)' == 'Modified'">
      <InformationalVersion>$(VersionType)-$(GitBranch)-$(CommitHash)-$(GitModified)</InformationalVersion>
    </PropertyGroup>
    <Message Text="Version $(InformationalVersion)" Importance="high" />
  </Target>
</Project>


================================================
FILE: MultiplayerExtensions/Patchers/AvatarPlacePatcher.cs
================================================
using HarmonyLib;
using MultiplayerExtensions.Environments;
using SiraUtil.Affinity;
using System.Collections.Generic;
using System.Reflection;
using System.Reflection.Emit;

namespace MultiplayerExtensions.Patchers
{
    [HarmonyPatch]
    public class AvatarPlacePatcher : IAffinity
    {
        private readonly MenuEnvironmentManager _environmentManager;

        internal AvatarPlacePatcher(
            MenuEnvironmentManager environmentManager)
        {
            _environmentManager = environmentManager;
        }

        private static readonly MethodInfo _addMethod = typeof(List<MultiplayerLobbyAvatarPlace>).GetMethod(nameof(List<MultiplayerLobbyAvatarPlace>.Add));
        private static readonly MethodInfo _setupAvatarPlaceMethod = SymbolExtensions.GetMethodInfo(() => SetupAvatarPlace(null!, 0));

        [HarmonyTranspiler]
        [HarmonyPatch(typeof(MultiplayerLobbyAvatarPlaceManager), nameof(MultiplayerLobbyAvatarPlaceManager.SpawnAllPlaces))]
        private static IEnumerable<CodeInstruction> SpawnAllPlaces(IEnumerable<CodeInstruction> instructions) =>
            new CodeMatcher(instructions)
                .MatchForward(false, new CodeMatch(OpCodes.Callvirt, _addMethod))
                .Insert(new CodeInstruction(OpCodes.Ldloc_3), new CodeInstruction(OpCodes.Callvirt, _setupAvatarPlaceMethod))
                .InstructionEnumeration();

        private static MultiplayerLobbyAvatarPlace SetupAvatarPlace(MultiplayerLobbyAvatarPlace avatarPlace, int sortIndex)
        {
            avatarPlace.gameObject.GetComponent<MpexAvatarPlaceLighting>().SortIndex = sortIndex;
            return avatarPlace;
        }

        [AffinityPrefix]
        [AffinityPatch(typeof(MultiplayerLobbyAvatarPlaceManager), nameof(MultiplayerLobbyAvatarPlaceManager.SpawnAllPlaces))]
        private void SpawnAllPlacesPrefix(ILobbyStateDataModel ____lobbyStateDataModel)
            => _environmentManager.transform.Find("MultiplayerLobbyEnvironment").Find("LobbyAvatarPlace").gameObject.GetComponent<MpexAvatarPlaceLighting>().SortIndex = ____lobbyStateDataModel.localPlayer.sortIndex;
    }
}


================================================
FILE: MultiplayerExtensions/Patchers/ColorSchemePatcher.cs
================================================
using SiraUtil.Affinity;

namespace MultiplayerExtensions.Patchers
{
    public class ColorSchemePatcher : IAffinity
    {
        private readonly GameplayCoreSceneSetupData _sceneSetupData;
        private readonly Config _config;

        internal ColorSchemePatcher(
            GameplayCoreSceneSetupData sceneSetupData,
            Config config)
        {
            _sceneSetupData = sceneSetupData;
            _config = config;
        }

        [AffinityPostfix]
        [AffinityPatch(typeof(PlayersSpecificSettingsAtGameStartModel), nameof(PlayersSpecificSettingsAtGameStartModel.GetPlayerSpecificSettingsForUserId))]
        private void SetConnectedPlayerColorScheme(ref PlayerSpecificSettingsNetSerializable __result)
        {
            var colorscheme = _sceneSetupData.colorScheme;
            if (_config.DisableMultiplayerColors)
                __result.colorScheme = new ColorSchemeNetSerializable(colorscheme.saberAColor, colorscheme.saberBColor, colorscheme.obstaclesColor, colorscheme.environmentColor0, colorscheme.environmentColor1, colorscheme.environmentColor0Boost, colorscheme.environmentColor1Boost);
        }
    }
}


================================================
FILE: MultiplayerExtensions/Patchers/EnvironmentPatcher.cs
================================================
using HarmonyLib;
using IPA.Utilities;
using SiraUtil.Affinity;
using SiraUtil.Logging;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using Zenject;

namespace MultiplayerExtensions.Patchers
{
    [HarmonyPatch]
    public class EnvironmentPatcher : IAffinity
    {
        private readonly GameScenesManager _scenesManager;
        private readonly Config _config;
        private readonly SiraLog _logger;

        internal EnvironmentPatcher(
            GameScenesManager scenesManager,
            Config config,
            SiraLog logger)
        {
            _scenesManager = scenesManager;
            _config = config;
            _logger = logger;
        }

        private List<MonoBehaviour> _behavioursToInject = new();

        [AffinityPostfix]
        [AffinityPatch(typeof(SceneDecoratorContext), "GetInjectableMonoBehaviours")]
        private void PreventEnvironmentInjection(SceneDecoratorContext __instance, List<MonoBehaviour> monoBehaviours, DiContainer ____container)
        {
            var scene = __instance.gameObject.scene;
            if (_scenesManager.IsSceneInStack("MultiplayerEnvironment") && _config.SoloEnvironment)
            {
                _logger.Info($"Fixing bind conflicts on scene '{scene.name}'.");
                List<MonoBehaviour> removedBehaviours = new();

                //if (scene.name == "MultiplayerEnvironment")
                //    removedBehaviours = monoBehaviours.FindAll(behaviour => behaviour is ZenjectBinding binding && binding.Components.Any(c => c is LightWithIdManager));
                if (scene.name.Contains("Environment") && !scene.name.Contains("Multiplayer"))
                    removedBehaviours = monoBehaviours.FindAll(behaviour => (behaviour is ZenjectBinding binding && binding.Components.Any(c => c is LightWithIdManager)));

                if (removedBehaviours.Any())
                {
                    _logger.Info($"Removing behaviours '{string.Join(", ", removedBehaviours.Select(behaviour => behaviour.GetType()))}' from scene '{scene.name}'.");
                    monoBehaviours.RemoveAll(monoBehaviour => removedBehaviours.Contains(monoBehaviour));
                }

                if (scene.name.Contains("Environment") && !scene.name.Contains("Multiplayer"))
                {
                    _logger.Info($"Preventing environment injection.");
                    _behavioursToInject = new(monoBehaviours);
                    monoBehaviours.Clear();
                }
            }
            else
            {
                _behavioursToInject.Clear();
            }
        }

        private List<InstallerBase> _normalInstallers = new();
        private List<Type> _normalInstallerTypes = new();
        private List<ScriptableObjectInstaller> _scriptableObjectInstallers = new();
        private List<MonoInstaller> _monoInstallers = new();
        private List<MonoInstaller> _installerPrefabs = new();

        [AffinityPrefix]
        [AffinityPatch(typeof(SceneDecoratorContext), "InstallDecoratorInstallers")]
        private void PreventEnvironmentInstall(SceneDecoratorContext __instance, List<InstallerBase> ____normalInstallers, List<Type> ____normalInstallerTypes, List<ScriptableObjectInstaller> ____scriptableObjectInstallers, List<MonoInstaller> ____monoInstallers, List<MonoInstaller> ____installerPrefabs)
        {
            var scene = __instance.gameObject.scene;
            if (_scenesManager.IsSceneInStack("MultiplayerEnvironment") && _config.SoloEnvironment && scene.name.Contains("Environment") && !scene.name.Contains("Multiplayer"))
            {
                _logger.Info($"Preventing environment installation.");

                _normalInstallers = new(____normalInstallers);
                _normalInstallerTypes = new(____normalInstallerTypes);
                _scriptableObjectInstallers = new(____scriptableObjectInstallers);
                _monoInstallers = new(____monoInstallers);
                _installerPrefabs = new(____installerPrefabs);

                ____normalInstallers.Clear();
                ____normalInstallerTypes.Clear();
                ____scriptableObjectInstallers.Clear();
                ____monoInstallers.Clear();
                ____installerPrefabs.Clear();
            }
            else if (!_scenesManager.IsSceneInStack("MultiplayerEnvironment"))
            {
                _normalInstallers.Clear();
                _normalInstallerTypes.Clear();
                _scriptableObjectInstallers.Clear();
                _monoInstallers.Clear();
                _installerPrefabs.Clear();
            }
        }

        private List<GameObject> _objectsToEnable = new();

        [AffinityPrefix]
        [AffinityPatch(typeof(GameScenesManager), "ActivatePresentedSceneRootObjects")]
        private void PreventEnvironmentActivation(List<string> scenesToPresent)
        {
            string defaultScene = scenesToPresent.FirstOrDefault(scene => scene.Contains("Environment") && !scene.Contains("Multiplayer"));
            if (defaultScene != null)
            {
                if (scenesToPresent.Contains("MultiplayerEnvironment"))
                {
                    _logger.Info($"Preventing environment activation. ({defaultScene})");
                    _objectsToEnable = SceneManager.GetSceneByName(defaultScene).GetRootGameObjects().ToList();
                    scenesToPresent.Remove(defaultScene);

                    // fix ring lighting dogshit
                    var trackLaneRingManagers = _objectsToEnable[0].transform.GetComponentsInChildren<TrackLaneRingsManager>();
                } 
                else
                {
                    // Make sure hud is enabled in solo
                    var sceneObjects = SceneManager.GetSceneByName(defaultScene).GetRootGameObjects().ToList();
                    foreach (GameObject gameObject in sceneObjects)
                    {
                        var hud = gameObject.transform.GetComponentInChildren<CoreGameHUDController>();
                        if (hud != null)
                            hud.gameObject.SetActive(true);
                    }
                }
            }
        }

        [AffinityPostfix]
        [AffinityPatch(typeof(GameObjectContext), "GetInjectableMonoBehaviours")]
        private void InjectEnvironment(GameObjectContext __instance, List<MonoBehaviour> monoBehaviours)
        {
            if (__instance.transform.name.Contains("LocalActivePlayer") && _config.SoloEnvironment)
            {
                _logger.Info($"Injecting environment.");
                monoBehaviours.AddRange(_behavioursToInject);
            }
        }

        [AffinityPrefix]
        [AffinityPatch(typeof(Context), "InstallInstallers", AffinityMethodType.Normal, null, typeof(List<InstallerBase>), typeof(List<Type>), typeof(List<ScriptableObjectInstaller>), typeof(List<MonoInstaller>), typeof(List<MonoInstaller>))]
        private void InstallEnvironment(Context __instance, List<InstallerBase> normalInstallers, List<Type> normalInstallerTypes, List<ScriptableObjectInstaller> scriptableObjectInstallers, List<MonoInstaller> installers, List<MonoInstaller> installerPrefabs)
        {
            if (__instance is GameObjectContext instance && __instance.transform.name.Contains("LocalActivePlayer") && _config.SoloEnvironment)
            {
                _logger.Info($"Installing environment.");
                normalInstallers.AddRange(_normalInstallers);
                normalInstallerTypes.AddRange(_normalInstallerTypes);
                scriptableObjectInstallers.AddRange(_scriptableObjectInstallers);
                installers.AddRange(_monoInstallers);
                installerPrefabs.AddRange(_installerPrefabs);
            }
        }

       
        [AffinityPrefix]
        [AffinityPatch(typeof(GameObjectContext), "InstallInstallers")]
        private void LoveYouCountersPlus(GameObjectContext __instance)
        {
            if (__instance.transform.name.Contains("LocalActivePlayer") && _config.SoloEnvironment)
            {
                DiContainer container = __instance.GetProperty<DiContainer, GameObjectContext>("Container");
                var hud = (CoreGameHUDController)_behavioursToInject.Find(x => x is CoreGameHUDController);
                container.Unbind<CoreGameHUDController>();
                container.Bind<CoreGameHUDController>().FromInstance(hud).AsSingle();
                var multihud = __instance.transform.GetComponentInChildren<CoreGameHUDController>();
                multihud.gameObject.SetActive(false);
                var multiPositionHud = __instance.transform.GetComponentInChildren<MultiplayerPositionHUDController>();
                multiPositionHud.transform.position += new Vector3(0, 0.01f, 0);
            }
        }

        [AffinityPostfix] 
        [AffinityPatch(typeof(GameObjectContext), "InstallSceneBindings")]
        private void ActivateEnvironment(GameObjectContext __instance)
        {
            if (__instance.transform.name.Contains("LocalActivePlayer") && _config.SoloEnvironment)
            {
                _logger.Info($"Activating environment.");
                foreach (GameObject gameObject in _objectsToEnable)
                    gameObject.SetActive(true);

                var activeObjects = __instance.transform.Find("IsActiveObjects");
                activeObjects.Find("Lasers").gameObject.SetActive(false);
                activeObjects.Find("Construction").gameObject.SetActive(false);
                activeObjects.Find("BigSmokePS").gameObject.SetActive(false);
                activeObjects.Find("DustPS").gameObject.SetActive(false);
                activeObjects.Find("DirectionalLights").gameObject.SetActive(false);

                var localActivePlayer = __instance.transform.GetComponent<MultiplayerLocalActivePlayerFacade>();
                var activeOnlyGameObjects = localActivePlayer.GetField<GameObject[], MultiplayerLocalActivePlayerFacade>("_activeOnlyGameObjects");
                var newActiveOnlyGameObjects = activeOnlyGameObjects.Concat(_objectsToEnable);
                localActivePlayer.SetField("_activeOnlyGameObjects", newActiveOnlyGameObjects.ToArray());
            }
        }

        [HarmonyPostfix]
        [HarmonyPatch(typeof(Context), "InstallSceneBindings")]
        private static void HideOtherPlayerPlatforms(Context __instance)
        {
            if (__instance.transform.name.Contains("ConnectedPlayer"))
            {
                if (Plugin.Config.DisableMultiplayerPlatforms)
                    __instance.transform.Find("Construction").gameObject.SetActive(false);
                if (Plugin.Config.DisableMultiplayerLights)
                    __instance.transform.Find("Lasers").gameObject.SetActive(false);
            }
        }

        [HarmonyPrefix]
        [HarmonyPatch(typeof(EnvironmentSceneSetup), nameof(EnvironmentSceneSetup.InstallBindings))]
        private static bool RemoveDuplicateInstalls(EnvironmentSceneSetup __instance)
        {
            DiContainer container = __instance.GetProperty<DiContainer, MonoInstallerBase>("Container");
            return !container.HasBinding<EnvironmentBrandingManager.InitData>();
        }

        [AffinityPostfix]
        [AffinityPatch(typeof(GameplayCoreInstaller), nameof(GameplayCoreInstaller.InstallBindings))]
        private void SetEnvironmentColors(GameplayCoreInstaller __instance)
        {
            if (!_config.SoloEnvironment || !_scenesManager.IsSceneInStack("MultiplayerEnvironment"))
                return;

            DiContainer container = __instance.GetProperty<DiContainer, MonoInstallerBase>("Container");
            var colorManager = container.Resolve<EnvironmentColorManager>();
            container.Inject(colorManager);
            colorManager.Awake();
            colorManager.Start();

            foreach (var gameObject in _objectsToEnable)
            {
                var lightSwitchEventEffects = gameObject.transform.GetComponentsInChildren<LightSwitchEventEffect>();
                foreach (var component in lightSwitchEventEffects)
                    component.Awake();
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/Patchers/MenuEnvironmentPatcher.cs
================================================
using HarmonyLib;
using SiraUtil.Affinity;
using SiraUtil.Logging;
using System.Linq;

namespace MultiplayerExtensions.Patchers
{
    [HarmonyPatch]
    public class MenuEnvironmentPatcher : IAffinity
    {
        private readonly GameplaySetupViewController _gameplaySetup;
        private readonly Config _config;
        private readonly SiraLog _logger;

        internal MenuEnvironmentPatcher(
            GameplaySetupViewController gameplaySetup,
            Config config,
            SiraLog logger)
        {
            _gameplaySetup = gameplaySetup;
            _config = config;
            _logger = logger;
        }

        [HarmonyPrefix]
        [HarmonyPatch(typeof(GameplaySetupViewController), nameof(GameplaySetupViewController.Setup))]
        private static void EnableEnvironmentTab(bool showModifiers, ref bool showEnvironmentOverrideSettings, bool showColorSchemesSettings, bool showMultiplayer, PlayerSettingsPanelController.PlayerSettingsPanelLayout playerSettingsPanelLayout)
        {
            if (showMultiplayer)
                showEnvironmentOverrideSettings = Plugin.Config.SoloEnvironment;
        }

        private EnvironmentInfoSO _originalEnvironmentInfo = null!;

        [AffinityPrefix]
        [AffinityPatch(typeof(MultiplayerLevelScenesTransitionSetupDataSO), "Init")]
        private void SetEnvironmentScene(IDifficultyBeatmap difficultyBeatmap, ref EnvironmentInfoSO ____multiplayerEnvironmentInfo)
        {
            if (!_config.SoloEnvironment)
                return;

            _originalEnvironmentInfo = ____multiplayerEnvironmentInfo;
            ____multiplayerEnvironmentInfo = difficultyBeatmap.GetEnvironmentInfo();
            if (_gameplaySetup.environmentOverrideSettings.overrideEnvironments)
                ____multiplayerEnvironmentInfo = _gameplaySetup.environmentOverrideSettings.GetOverrideEnvironmentInfoForType(____multiplayerEnvironmentInfo.environmentType);
        }

        [AffinityPostfix]
        [AffinityPatch(typeof(MultiplayerLevelScenesTransitionSetupDataSO), "Init")]
        private void ResetEnvironmentScene(IDifficultyBeatmap difficultyBeatmap, ref EnvironmentInfoSO ____multiplayerEnvironmentInfo)
        {
            if (_config.SoloEnvironment)
                ____multiplayerEnvironmentInfo = _originalEnvironmentInfo;
        }

        [AffinityPrefix]
        [AffinityPatch(typeof(ScenesTransitionSetupDataSO), "Init")]
        private void AddEnvironmentOverrides(ref SceneInfo[] scenes)
        {
            if (_config.SoloEnvironment && scenes.Any(scene => scene.name.Contains("Multiplayer")))
            {
                scenes = scenes.AddItem(_originalEnvironmentInfo.sceneInfo).ToArray();
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/Patchers/PlayerPositionPatcher.cs
================================================
using HarmonyLib;
using SiraUtil.Affinity;
using System.Collections.Generic;
using UnityEngine;

namespace MultiplayerExtensions.Patchers
{
    [HarmonyPatch]
    public class PlayerPositionPatcher : IAffinity
    {
        private readonly Config _config;

        internal PlayerPositionPatcher(
            Config config)
        {
            _config = config;
        }

        // these are affinity patches because they only apply to one container
        [AffinityPrefix]
        [AffinityPatch(typeof(MultiplayerLayoutProvider), nameof(MultiplayerLayoutProvider.CalculateLayout))]
        private bool SideBySideLayout(ref MultiplayerPlayerLayout __result)
        {;
            __result = MultiplayerPlayerLayout.Duel;
            return !_config.SideBySide;
        }

        [HarmonyPrefix]
        [HarmonyPatch(typeof(MultiplayerConditionalActiveByLayout), nameof(MultiplayerConditionalActiveByLayout.Start))]
        private static void SideBySideLayoutConfirm(MultiplayerConditionalActiveByLayout __instance, MultiplayerLayoutProvider ____layoutProvider)
        {
            if (!Plugin.Config.SideBySide)
                return;
            if (____layoutProvider.layout == MultiplayerPlayerLayout.NotDetermined)
                __instance.HandlePlayersLayoutWasCalculated(MultiplayerPlayerLayout.Duel, 2);
        }

        [HarmonyPrefix]
        [HarmonyPatch(typeof(MultiplayerConditionalActiveByLayout), nameof(MultiplayerConditionalActiveByLayout.HandlePlayersLayoutWasCalculated))]
        private static void SideBySideObjectDisable(ref MultiplayerPlayerLayout layout)
        {
            if (Plugin.Config.SideBySide)
                layout = MultiplayerPlayerLayout.Duel;
        }

        [AffinityPrefix]
        [AffinityPatch(typeof(MultiplayerPlayerPlacement), nameof(MultiplayerPlayerPlacement.GetOuterCirclePositionAngleForPlayer))]
        private bool SideBySideAngle(int playerIndex, int localPlayerIndex, ref float __result)
        {
            __result = (playerIndex - localPlayerIndex) * 0.01f;
            return !_config.SideBySide;
        }

        [AffinityPrefix]
        [AffinityPatch(typeof(MultiplayerPlayerPlacement), nameof(MultiplayerPlayerPlacement.GetPlayerWorldPosition))]
        private bool SoloEnvironmentPosition(float outerCirclePositionAngle, ref Vector3 __result)
        {
            var sortIndex = outerCirclePositionAngle ;
            __result = new Vector3(sortIndex * 100f * _config.SideBySideDistance, 0, 0);
            return !_config.SideBySide;
        }
    }
}


================================================
FILE: MultiplayerExtensions/Patches/AvatarPoseRestrictionPatch.cs
================================================
using HarmonyLib;
using UnityEngine;

namespace MultiplayerExtensions.Patches
{
    [HarmonyPatch]
    public class AvatarPoseRestrictionPatch
    {
        [HarmonyPrefix]
        [HarmonyPatch(typeof(AvatarPoseRestrictions), nameof(AvatarPoseRestrictions.HandleAvatarPoseControllerPositionsWillBeSet))]
        private static bool DisableAvatarRestrictions(AvatarPoseRestrictions __instance, Vector3 headPosition, Vector3 leftHandPosition, Vector3 rightHandPosition, out Vector3 newHeadPosition, out Vector3 newLeftHandPosition, out Vector3 newRightHandPosition)
        {
            newHeadPosition = headPosition;
            newLeftHandPosition = leftHandPosition;
            newRightHandPosition = rightHandPosition;
            if (!Plugin.Config.DisableAvatarConstraints)
                return true;
            newLeftHandPosition = __instance.LimitHandPositionRelativeToHead(leftHandPosition, headPosition);
            newRightHandPosition = __instance.LimitHandPositionRelativeToHead(rightHandPosition, headPosition);
            return false;
        }
    }
}


================================================
FILE: MultiplayerExtensions/Patches/PlatformMovementPatch.cs
================================================
using HarmonyLib;

namespace MultiplayerExtensions.Patches
{
    [HarmonyPatch]
    public class PlatformMovementPatch
    {
        [HarmonyPrefix]
        [HarmonyPatch(typeof(MultiplayerVerticalPlayerMovementManager), nameof(MultiplayerVerticalPlayerMovementManager.Update))]
        private static bool DisableVerticalPlayerMovement()
        {
            return !Plugin.Config.DisablePlatformMovement;
        }
    }
}


================================================
FILE: MultiplayerExtensions/Patches/ResumeSpawningPatch.cs
================================================
using HarmonyLib;

namespace MultiplayerExtensions.Patches
{
    [HarmonyPatch]
    public class ResumeSpawningPatch
    {
        [HarmonyPrefix]
        [HarmonyPatch(typeof(MultiplayerConnectedPlayerFacade), nameof(MultiplayerConnectedPlayerFacade.ResumeSpawning))]
        private static bool DisableAvatarRestrictions()
        {
            if (Plugin.Config.DisableMultiplayerObjects)
                return false;
            return true;
        }
    }
}


================================================
FILE: MultiplayerExtensions/Players/MpexPlayerData.cs
================================================
using LiteNetLib.Utils;
using UnityEngine;

namespace MultiplayerExtensions.Players
{
    public class MpexPlayerData : INetSerializable
    {
        /// <summary>
        /// Player's color set in the plugin config.
        /// </summary>
        public Color Color { get; set; }

        public void Serialize(NetDataWriter writer)
        {
            writer.Put("#" + ColorUtility.ToHtmlStringRGB(Color));
        }

        public void Deserialize(NetDataReader reader)
        {
            Color color;
            if (!ColorUtility.TryParseHtmlString(reader.GetString(), out color))
                color = Config.DefaultPlayerColor;
            Color = color;
        }
    }
}


================================================
FILE: MultiplayerExtensions/Players/MpexPlayerManager.cs
================================================
using MultiplayerCore.Networking;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.Players
{
	public class MpexPlayerManager : IInitializable
	{
		public event Action<IConnectedPlayer, MpexPlayerData> PlayerConnectedEvent = null!;

		public IReadOnlyDictionary<string, MpexPlayerData> Players => _playerData;

		private ConcurrentDictionary<string, MpexPlayerData> _playerData = new();

		private readonly MpPacketSerializer _packetSerializer;
		private readonly IMultiplayerSessionManager _sessionManager;
		private readonly Config _config;

		internal MpexPlayerManager(
			MpPacketSerializer packetSerializer,
			IMultiplayerSessionManager sessionManager,
			Config config)
		{
			_packetSerializer = packetSerializer;
			_sessionManager = sessionManager;
			_config = config;
		}

		public void Initialize()
		{
			_sessionManager.SetLocalPlayerState("modded", true);
			_packetSerializer.RegisterCallback<MpexPlayerData>(HandlePlayerData);
			_sessionManager.playerConnectedEvent += HandlePlayerConnected;
		}

		public void Dispose()
		{
			_packetSerializer.UnregisterCallback<MpexPlayerData>();
		}

		private void HandlePlayerConnected(IConnectedPlayer player)
		{
			_sessionManager.Send(new MpexPlayerData
			{
				Color = _config.PlayerColor
			});
		}

		private void HandlePlayerData(MpexPlayerData packet, IConnectedPlayer player)
		{
			_playerData[player.userId] = packet;
			PlayerConnectedEvent(player, packet);
		}

		public bool TryGetPlayer(string userId, out MpexPlayerData player)
			=> _playerData.TryGetValue(userId, out player);

		public MpexPlayerData? GetPlayer(string userId)
			=> _playerData.ContainsKey(userId) ? _playerData[userId] : null;
    }
}


================================================
FILE: MultiplayerExtensions/Plugin.cs
================================================
using HarmonyLib;
using IPA;
using IPA.Config.Stores;
using IPA.Loader;
using MultiplayerExtensions.Installers;
using SiraUtil.Zenject;
using IPALogger = IPA.Logging.Logger;
using Conf = IPA.Config.Config;

namespace MultiplayerExtensions
{
    [Plugin(RuntimeOptions.DynamicInit)]
    public class Plugin
    {
        public const string ID = "com.goobwabber.multiplayerextensions";

        internal static IPALogger Logger = null!;
        internal static Config Config = null!;

        private readonly Harmony _harmony;
        private readonly PluginMetadata _metadata;

        [Init]
        public Plugin(IPALogger logger, Conf conf, Zenjector zenjector, PluginMetadata pluginMetadata)
        {
            Config config = conf.Generated<Config>();
            _harmony = new Harmony(ID);
            _metadata = pluginMetadata;
            Logger = logger;
            Config = config;

            zenjector.UseMetadataBinder<Plugin>();
            zenjector.UseLogger(logger);
            zenjector.UseSiraSync(SiraUtil.Web.SiraSync.SiraSyncServiceType.GitHub, "Goobwabber", "MultiplayerExtensions");
            zenjector.Install<MpexAppInstaller>(Location.App, config);
            zenjector.Install<MpexMenuInstaller>(Location.Menu);
            zenjector.Install<MpexLobbyInstaller, MultiplayerLobbyInstaller>();
            zenjector.Install<MpexGameInstaller>(Location.MultiplayerCore);
            zenjector.Install<MpexLocalActivePlayerInstaller>(Location.MultiPlayer);
        }

        [OnEnable]
        public void OnEnable()
        {
            _harmony.PatchAll(_metadata.Assembly);
        }

        [OnDisable]
        public void OnDisable()
        {
            _harmony.UnpatchSelf();
        }
    }
}


================================================
FILE: MultiplayerExtensions/UI/MpexEnvironmentViewController.bsml
================================================
<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'>
	<horizontal anchor-min-y='1'  bg='panel-top' pad-left='10' pad-right='10' horizontal-fit='PreferredSize'>
    <text text='Environment' align='Center' font-size='8' />
	</horizontal>
	<settings-container>
    <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'/>
    <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'/>
    <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'/>
	</settings-container>
</bg>

================================================
FILE: MultiplayerExtensions/UI/MpexEnvironmentViewController.cs
================================================
using BeatSaberMarkupLanguage.Attributes;
using BeatSaberMarkupLanguage.Components.Settings;
using BeatSaberMarkupLanguage.ViewControllers;
using IPA.Utilities;
using Zenject;

namespace MultiplayerExtensions.UI
{
    [ViewDefinition("MultiplayerExtensions.UI.MpexEnvironmentViewController.bsml")]
    public class MpexEnvironmentViewController : BSMLAutomaticViewController
    {
        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showModifiers
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showModifiers));
        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showEnvironmentOverrideSettings
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showEnvironmentOverrideSettings));
        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showColorSchemesSettings
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showColorSchemesSettings));
        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showMultiplayer
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showMultiplayer));

        private GameplaySetupViewController _gameplaySetup = null!;
        private Config _config = null!;

        [Inject]
        private void Construct(
            GameplaySetupViewController gameplaySetup,
            Config config)
        {
            _gameplaySetup = gameplaySetup;
            _config = config;
        }

        [UIAction("#post-parse")]
        private void PostParse()
        {
            _sideBySideDistanceIncrement.interactable = _sideBySide;
        }

        [UIComponent("side-by-side-distance-increment")]
        private GenericInteractableSetting _sideBySideDistanceIncrement = null!;

        [UIValue("solo-environment")]
        private bool _soloEnvironment
        {
            get => _config.SoloEnvironment;
            set
            {
                _config.SoloEnvironment = value;
                _gameplaySetup.Setup(
                    _showModifiers(ref _gameplaySetup),
                    _showEnvironmentOverrideSettings(ref _gameplaySetup),
                    _showColorSchemesSettings(ref _gameplaySetup),
                    _showMultiplayer(ref _gameplaySetup),
                    PlayerSettingsPanelController.PlayerSettingsPanelLayout.Multiplayer
                );
                NotifyPropertyChanged();
            }
        }

        [UIValue("side-by-side")]
        private bool _sideBySide
        {
            get => _config.SideBySide;
            set
            {
                _config.SideBySide = value;
                if (_sideBySideDistanceIncrement != null)
                    _sideBySideDistanceIncrement.interactable = value;
                NotifyPropertyChanged();
            }
        }

        [UIValue("side-by-side-distance")]
        private float _sideBySideDistance
        {
            get => _config.SideBySideDistance;
            set
            {
                _config.SideBySideDistance = value;
                NotifyPropertyChanged();
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/UI/MpexGameplaySetup.bsml
================================================
<vertical id='vert'>
	<text text='MultiplayerExtensions' pad-bottom='0' align='Bottom' size-delta-y='10'/>
	<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'/>
  <button text='Preferences' on-click='preferences-click'/>
</vertical>

================================================
FILE: MultiplayerExtensions/UI/MpexGameplaySetup.cs
================================================
using BeatSaberMarkupLanguage;
using BeatSaberMarkupLanguage.Attributes;
using BeatSaberMarkupLanguage.Components;
using HMUI;
using IPA.Utilities;
using SiraUtil.Logging;
using System;
using System.Reflection;
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.UI
{
    public class MpexGameplaySetup : NotifiableBase, IInitializable, IDisposable
    {
        public const string ResourcePath = "MultiplayerExtensions.UI.MpexGameplaySetup.bsml";

        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showModifiers
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showModifiers));
        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showEnvironmentOverrideSettings
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showEnvironmentOverrideSettings));
        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showColorSchemesSettings
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showColorSchemesSettings));
        private FieldAccessor<GameplaySetupViewController, bool>.Accessor _showMultiplayer
            = FieldAccessor<GameplaySetupViewController, bool>.GetAccessor(nameof(_showMultiplayer));

        private GameplaySetupViewController _gameplaySetup;
        private MultiplayerSettingsPanelController _multiplayerSettingsPanel;
        private MainFlowCoordinator _mainFlowCoordinator;
        private MpexSetupFlowCoordinator _setupFlowCoordinator;
        private Config _config;
        private SiraLog _logger;

        internal MpexGameplaySetup(
            GameplaySetupViewController gameplaySetup,
            MainFlowCoordinator mainFlowCoordinator,
            MpexSetupFlowCoordinator setupFlowCoordinator,
            Config config,
            SiraLog logger)
        {
            _gameplaySetup = gameplaySetup;
            _multiplayerSettingsPanel = gameplaySetup.GetField<MultiplayerSettingsPanelController, GameplaySetupViewController>("_multiplayerSettingsPanelController");
            _mainFlowCoordinator = mainFlowCoordinator;
            _setupFlowCoordinator = setupFlowCoordinator;
            _config = config;
            _logger = logger;
        }

        public void Initialize()
        {
            BSMLParser.instance.Parse(BeatSaberMarkupLanguage.Utilities.GetResourceContent(Assembly.GetExecutingAssembly(), ResourcePath), _multiplayerSettingsPanel.gameObject, this);
            while (0 < _vert.transform.childCount)
                _vert.transform.GetChild(0).SetParent(_multiplayerSettingsPanel.transform);
        }

        public void Dispose()
        {
            
        }

        [UIAction("preferences-click")]
        private void PresentPreferences()
        {
            FlowCoordinator deepestChildFlowCoordinator = DeepestChildFlowCoordinator(_mainFlowCoordinator);
            _setupFlowCoordinator.parentFlowCoordinator = deepestChildFlowCoordinator;
            deepestChildFlowCoordinator.PresentFlowCoordinator(_setupFlowCoordinator);
        }

        private FlowCoordinator DeepestChildFlowCoordinator(FlowCoordinator root)
        {
            var flow = root.childFlowCoordinator;
            if (flow == null) return root;
            if (flow.childFlowCoordinator == null || flow.childFlowCoordinator == flow)
            {
                return flow;
            }
            return DeepestChildFlowCoordinator(flow);
        }

        [UIObject("vert")]
        private GameObject _vert = null!;

        [UIValue("solo-environment")]
        private bool _soloEnvironment
        {
            get => _config.SoloEnvironment;
            set 
            {
                _config.SoloEnvironment = value;
                _gameplaySetup.Setup(
                    _showModifiers(ref _gameplaySetup),
                    _showEnvironmentOverrideSettings(ref _gameplaySetup),
                    _showColorSchemesSettings(ref _gameplaySetup),
                    _showMultiplayer(ref _gameplaySetup),
                    PlayerSettingsPanelController.PlayerSettingsPanelLayout.Multiplayer
                );
                NotifyPropertyChanged();
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/UI/MpexMiscViewController.bsml
================================================
<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'>
	<horizontal anchor-min-y='1'  bg='panel-top' pad-left='10' pad-right='10' horizontal-fit='PreferredSize'>
		<text text='Misc' align='Center' font-size='8' />
	</horizontal>
	<settings-container>
    <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'/>
    <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'/>
    <toggle-setting value='disable-platform-movement' apply-on-change='true' text='Disable Platform Movement' hover-hint="Disables vertical platform movement." bind-value='true'/>
	</settings-container>
</bg>

================================================
FILE: MultiplayerExtensions/UI/MpexMiscViewController.cs
================================================
using BeatSaberMarkupLanguage.Attributes;
using BeatSaberMarkupLanguage.ViewControllers;
using Zenject;

namespace MultiplayerExtensions.UI
{
    [ViewDefinition("MultiplayerExtensions.UI.MpexMiscViewController.bsml")]
    public class MpexMiscViewController : BSMLAutomaticViewController
    {
        private Config _config = null!;

        [Inject]
        private void Construct(
            Config config)
        {
            _config = config;
        }

        [UIValue("disable-avatar-constraints")]
        private bool _disableAvatarConstraints
        {
            get => _config.DisableAvatarConstraints;
            set
            {
                _config.DisableAvatarConstraints = value;
                NotifyPropertyChanged();
            }
        }

        [UIValue("disable-player-colors")]
        private bool _disablePlayerColors
        {
            get => _config.DisableMultiplayerColors;
            set
            {
                _config.DisableMultiplayerColors = value;
                NotifyPropertyChanged();
            }
        }

        [UIValue("disable-platform-movement")]
        private bool _disablePlatformMovement
        {
            get => _config.DisablePlatformMovement;
            set
            {
                _config.DisablePlatformMovement = value;
                NotifyPropertyChanged();
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/UI/MpexSettingsViewController.bsml
================================================
<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'>
	<horizontal anchor-min-y='1'  bg='panel-top' pad-left='10' pad-right='10' horizontal-fit='PreferredSize'>
	</horizontal>
	<settings-container>
    <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'/>
    <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'/>
    <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'/>
    <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'/>
    <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'/>
	</settings-container>
</bg>

================================================
FILE: MultiplayerExtensions/UI/MpexSettingsViewController.cs
================================================
using BeatSaberMarkupLanguage.Attributes;
using BeatSaberMarkupLanguage.Components.Settings;
using BeatSaberMarkupLanguage.ViewControllers;
using Zenject;

namespace MultiplayerExtensions.UI
{
    [ViewDefinition("MultiplayerExtensions.UI.MpexSettingsViewController.bsml")]
    public class MpexSettingsViewController : BSMLAutomaticViewController
    {
        private Config _config = null!;

        [Inject]
        private void Construct(
            Config config)
        {
            _config = config;
        }

        [UIAction("#post-parse")]
        private void PostParse()
        {
            _personalMissLightingToggle.interactable = _missLighting;
        }

        [UIComponent("personal-miss-lighting-toggle")]
        private GenericInteractableSetting _personalMissLightingToggle = null!;

        [UIValue("hide-player-platforms")]
        private bool _hidePlayerPlatforms
        {
            get => _config.DisableMultiplayerPlatforms;
            set
            {
                _config.DisableMultiplayerPlatforms = value;
                NotifyPropertyChanged();
            }
        }

        [UIValue("hide-player-lights")]
        private bool _hidePlayerLights
        {
            get => _config.DisableMultiplayerLights;
            set
            {
                _config.DisableMultiplayerLights = value;
                NotifyPropertyChanged();
            }
        }

        [UIValue("hide-player-objects")]
        private bool _hidePlayerObjects
        {
            get => _config.DisableMultiplayerObjects;
            set
            {
                _config.DisableMultiplayerObjects = value;
                NotifyPropertyChanged();
            }
        }

        [UIValue("miss-lighting")]
        private bool _missLighting
        {
            get => _config.MissLighting;
            set
            {
                _config.MissLighting = value;
                if (_personalMissLightingToggle != null)
                    _personalMissLightingToggle.interactable = value;
                NotifyPropertyChanged();
            }
        }

        [UIValue("personal-miss-lighting-only")]
        private bool _personalMissLightingOnly
        {
            get => _config.PersonalMissLightingOnly;
            set
            {
                _config.PersonalMissLightingOnly = value;
                NotifyPropertyChanged();
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/UI/MpexSetupFlowCoordinator.cs
================================================
using HMUI;
using Zenject;
using BeatSaberMarkupLanguage;
using SiraUtil.Affinity;
using System;

namespace MultiplayerExtensions.UI
{
    public class MpexSetupFlowCoordinator : FlowCoordinator
    {
        internal FlowCoordinator parentFlowCoordinator = null!;
        private MpexSettingsViewController _settingsViewController = null!;
        private MpexEnvironmentViewController _environmentViewController = null!;
        private MpexMiscViewController _miscViewController = null!;
        private ILobbyGameStateController _gameStateController = null!;

        [Inject]
        public void Construct(
            MainFlowCoordinator mainFlowCoordinator, 
            MpexSettingsViewController settingsViewController,
            MpexEnvironmentViewController environmentViewController,
            MpexMiscViewController miscViewController,
            ILobbyGameStateController gameStateController)
        {
            parentFlowCoordinator = mainFlowCoordinator;
            _settingsViewController = settingsViewController;
            _environmentViewController = environmentViewController;
            _miscViewController = miscViewController;
            _gameStateController = gameStateController;
        }

        protected override void DidActivate(bool firstActivation, bool addedToHierarchy, bool screenSystemEnabling)
        {
            if (firstActivation)
            {
                SetTitle("Multiplayer Preferences");
                showBackButton = true;
            }
            if (addedToHierarchy)
            {
                ProvideInitialViewControllers(_settingsViewController, _environmentViewController, _miscViewController);
                _gameStateController.gameStartedEvent += DismissGameStartedEvent;
            }
        }

        protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling)
        {
            if (removedFromHierarchy)
                _gameStateController.gameStartedEvent -= DismissGameStartedEvent;
        }

        private void DismissGameStartedEvent(ILevelGameplaySetupData obj)
        {
            parentFlowCoordinator.DismissFlowCoordinator(this, null, ViewController.AnimationDirection.Horizontal, true);
        }

        protected override void BackButtonWasPressed(ViewController topViewController)
        {
            parentFlowCoordinator.DismissFlowCoordinator(this);
        }
    }
}


================================================
FILE: MultiplayerExtensions/Utilities/ColorConverter.cs
================================================
using IPA.Config.Data;
using IPA.Config.Stores;
using System;
using UnityEngine;

namespace MultiplayerExtensions.Utilities
{
    public class ColorConverter : ValueConverter<Color>
    {
        public override Color FromValue(Value? value, object parent)
        {
            if (value is not Text text) 
                throw new ArgumentException("Argument not Text", nameof(value));
            if (!ColorUtility.TryParseHtmlString(text.Value, out var color))
                throw new ArgumentException("Could not parse HtmlString", nameof(value));
            return color;
        }

        public override Value? ToValue(Color obj, object parent)
            => Value.Text($"#{ColorUtility.ToHtmlStringRGB(obj)}");
    }
}


================================================
FILE: MultiplayerExtensions/Utilities/SpriteManager.cs
================================================
using SiraUtil.Logging;
using System;
using System.Reflection;
using System.Security.Policy;
using UnityEngine;
using Zenject;

namespace MultiplayerExtensions.Utilities
{
    public class SpriteManager : IInitializable, IDisposable
    {
        public Sprite IconOculus64 { get; private set; } = null!;
        public Sprite IconSteam64 { get; private set; } = null!;
        public Sprite IconMeta64 { get; private set; } = null!;
        public Sprite IconToaster64 { get; private set; } = null!;

        private readonly SiraLog _logger;

        internal SpriteManager(
            SiraLog logger)
        {
            _logger = logger;
        }

        public void Initialize()
        {
            IconOculus64 = GetSpriteFromResources("MultiplayerExtensions.Assets.IconOculus64.png");
            IconSteam64 = GetSpriteFromResources("MultiplayerExtensions.Assets.IconSteam64.png");
            IconMeta64 = GetSpriteFromResources("MultiplayerExtensions.Assets.IconMeta64.png");
            IconToaster64 = GetSpriteFromResources("MultiplayerExtensions.Assets.IconToaster64.png");
        }

        public void Dispose()
        {
            if (IconOculus64 != null) Sprite.Destroy(IconOculus64);
            IconOculus64 = null;
            if (IconSteam64 != null) Sprite.Destroy(IconSteam64);
            IconSteam64 = null;
            if (IconMeta64 != null) Sprite.Destroy(IconMeta64);
            IconMeta64 = null;
            if (IconToaster64 != null) Sprite.Destroy(IconToaster64);
            IconToaster64 = null;
        }

        private Sprite GetSpriteFromResources(string resourcePath, float pixelsPerUnit = 10.0f)
        {
            Sprite? sprite = GetSprite(GetResource(Assembly.GetCallingAssembly(), resourcePath), pixelsPerUnit);
            if (sprite == null)
                return null!;
            sprite.name = resourcePath;
            return sprite;
        }

        private byte[] GetResource(Assembly asm, string resourceName)
        {
            System.IO.Stream stream = asm.GetManifestResourceStream(resourceName);
            byte[] data = new byte[stream.Length];
            stream.Read(data, 0, (int)stream.Length);
            return data;
        }

        public Sprite? GetSprite(byte[]? data, float pixelsPerUnit = 100.0f, bool returnDefaultOnFail = true)
        {
            Sprite? ReturnDefault(bool useDefault)
            {
                return null;
            }
            try
            {
                Texture2D texture = new Texture2D(2, 2);
                if (data == null || data.Length == 0)
                {
                    //Logger?.Invoke($"data seems to be null or empty.", null);
                    return ReturnDefault(returnDefaultOnFail);
                }
                texture.LoadImage(data);
                return Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), new Vector2(0, 0), pixelsPerUnit);
            }
            catch (Exception ex)
            {
                _logger.Warn($"Caught unhandled exception {ex.Message}");
                return ReturnDefault(returnDefaultOnFail);
            }
        }
    }
}


================================================
FILE: MultiplayerExtensions/manifest.json
================================================
{
  "$schema": "https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json",
  "id": "MultiplayerExtensions",
  "name": "MultiplayerExtensions",
  "author": "Goobwabber",
  "version": "1.0.3",
  "description": "Expands the functionality of Beat Saber Multiplayer.",
  "gameVersion": "1.24.0",
  "dependsOn": {
    "BSIPA": "^4.1.4",
    "BeatSaberMarkupLanguage": "^1.5.1",
    "SiraUtil": "^3.1.0",
    "MultiplayerCore": "^1.0.0"
  },
  "links": {
    "project-home": "https://github.com/Goobwabber/MultiplayerExtensions",
    "donate": "https://github.com/Goobwabber/MultiplayerExtensions#donate"
  }
}


================================================
FILE: MultiplayerExtensions.sln
================================================

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 16.0.30517.126
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "MultiplayerExtensions", "MultiplayerExtensions\MultiplayerExtensions.csproj", "{4641BE95-C3F7-4636-BA24-67188A38D94C}"
EndProject
Global
	GlobalSection(SolutionConfigurationPlatforms) = preSolution
		Debug|Any CPU = Debug|Any CPU
		Release|Any CPU = Release|Any CPU
	EndGlobalSection
	GlobalSection(ProjectConfigurationPlatforms) = postSolution
		{4641BE95-C3F7-4636-BA24-67188A38D94C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
		{4641BE95-C3F7-4636-BA24-67188A38D94C}.Debug|Any CPU.Build.0 = Debug|Any CPU
		{4641BE95-C3F7-4636-BA24-67188A38D94C}.Release|Any CPU.ActiveCfg = Release|Any CPU
		{4641BE95-C3F7-4636-BA24-67188A38D94C}.Release|Any CPU.Build.0 = Release|Any CPU
	EndGlobalSection
	GlobalSection(SolutionProperties) = preSolution
		HideSolutionNode = FALSE
	EndGlobalSection
	GlobalSection(ExtensibilityGlobals) = postSolution
		SolutionGuid = {E8F0586B-E2CD-47E7-B61F-73A798A37ADA}
	EndGlobalSection
EndGlobal


================================================
FILE: MultiplayerExtensions.v3.ncrunchsolution
================================================
<SolutionConfiguration>
  <Settings>
    <AllowParallelTestExecution>True</AllowParallelTestExecution>
    <SolutionConfigured>True</SolutionConfigured>
  </Settings>
</SolutionConfiguration>

================================================
FILE: README.md
================================================
# 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)
A Beat Saber mod which aims to improve the multiplayer experience.

**THIS MOD DOES NOT ENABLE CUSTOM SONGS.** 

## Features
* Fancy platform lighting
* Fancy in-game lighting
**Looking for more features, feel free to contribute your own.**

## Installation
1. Ensure you have the [required mods](https://github.com/Goobwabber/MultiplayerExtensions#requirements).
2. Download the `MultiplayerExtensions` file listed under `Assets` **[Here](https://github.com/Goobwabber/MultiplayerExtensions/releases)**.
   * 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).
   * You must be logged into GitHub to download a development build.
3. Extract the zip file to your Beat Saber game directory (the one `Beat Saber.exe` is in).
   * The `MultiplayerExtensions.dll` (and `MultiplayerExtensions.pdb` if it exists) should end up in your `Plugins` folder (**NOT** the one in `Beat Saber_Data`).
4. **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.

Lastly, check out [other mods](https://github.com/Goobwabber/MultiplayerExtensions#related-mods) that work well with MultiplayerExtensions!

## Requirements
These 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.**
* MultiplayerCore v1.0.0+
* BeatSaberMarkupLanguage v1.5.1+
* SiraUtil 3.0.0+

## Reporting Issues
* 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.**
* Include in your issue:
  * A detailed explanation of your problem (you can also attach videos/screenshots)
  * **Important**: The log file from the game session the issue occurred (restarting the game creates a new log file).
    * The log file can be found at `Beat Saber\Logs\_latest.log` (`Beat Saber` being the folder `Beat Saber.exe` is in).
* If you ask for help on Discord, at least include your `_latest.log` file in your help request.

## Contributing
Anyone 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.
### Building
Visual Studio 2019 with the [BeatSaberModdingTools](https://github.com/Zingabopp/BeatSaberModdingTools) extension is the recommended development environment.
1. Check out the repository
2. Open `MultiplayerExtensions.sln`
3. Right-click the `MultiplayerExtensions` project, go to `Beat Saber Modding Tools` -> `Set Beat Saber Directory`
   * This assumes you have already set the directory for your Beat Saber game folder in `Extensions` -> `Beat Saber Modding Tools` -> `Settings...`
   * 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.
4. The project should now build.
### Testing
MultiplayerExtensions 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.

**Example csproj.user File:**
```xml
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
  <PropertyGroup>
    <BeatSaberDir>Full\Path\To\Beat Saber</BeatSaberDir>
  </PropertyGroup>
</Project>
```
## Donate
You can support development of MultiplayerExtensions by donating at the following links:
* https://www.patreon.com/goobwabber
* https://ko-fi.com/goobwabber
* https://ko-fi.com/zingabopp

## Related Mods
* [MultiplayerCore](https://github.com/Goobwabber/MultiplayerCore)
* BeatTogether for [PC](https://github.com/pythonology/BeatTogether) or [Quest](https://github.com/pythonology/BeatTogether.Quest)
* [BeatSaberServerBrowser](https://github.com/roydejong/BeatSaberServerBrowser)
Download .txt
gitextract_99ygdq5q/

├── .gitattributes
├── .github/
│   ├── FUNDING.yml
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   └── feature_request.md
│   └── workflows/
│       ├── Build.yml
│       └── PR_Build.yml
├── .gitignore
├── LICENSE
├── MultiplayerExtensions/
│   ├── Config.cs
│   ├── Directory.Build.props
│   ├── Directory.Build.targets
│   ├── Environment/
│   │   ├── MpexAvatarNameTag.cs
│   │   ├── MpexAvatarPlaceLighting.cs
│   │   ├── MpexConnectedObjectManager.cs
│   │   ├── MpexLevelEndActions.cs
│   │   ├── MpexPlayerFacadeLighting.cs
│   │   └── MpexPlayerTableCell.cs
│   ├── Installers/
│   │   ├── MpexAppInstaller.cs
│   │   ├── MpexGameInstaller.cs
│   │   ├── MpexLobbyInstaller.cs
│   │   ├── MpexLocalActivePlayerInstaller.cs
│   │   └── MpexMenuInstaller.cs
│   ├── MultiplayerExtensions.csproj
│   ├── Patchers/
│   │   ├── AvatarPlacePatcher.cs
│   │   ├── ColorSchemePatcher.cs
│   │   ├── EnvironmentPatcher.cs
│   │   ├── MenuEnvironmentPatcher.cs
│   │   └── PlayerPositionPatcher.cs
│   ├── Patches/
│   │   ├── AvatarPoseRestrictionPatch.cs
│   │   ├── PlatformMovementPatch.cs
│   │   └── ResumeSpawningPatch.cs
│   ├── Players/
│   │   ├── MpexPlayerData.cs
│   │   └── MpexPlayerManager.cs
│   ├── Plugin.cs
│   ├── UI/
│   │   ├── MpexEnvironmentViewController.bsml
│   │   ├── MpexEnvironmentViewController.cs
│   │   ├── MpexGameplaySetup.bsml
│   │   ├── MpexGameplaySetup.cs
│   │   ├── MpexMiscViewController.bsml
│   │   ├── MpexMiscViewController.cs
│   │   ├── MpexSettingsViewController.bsml
│   │   ├── MpexSettingsViewController.cs
│   │   └── MpexSetupFlowCoordinator.cs
│   ├── Utilities/
│   │   ├── ColorConverter.cs
│   │   └── SpriteManager.cs
│   └── manifest.json
├── MultiplayerExtensions.sln
├── MultiplayerExtensions.v3.ncrunchsolution
└── README.md
Download .txt
SYMBOL INDEX (145 symbols across 30 files)

FILE: MultiplayerExtensions/Config.cs
  class Config (line 7) | public class Config

FILE: MultiplayerExtensions/Environment/MpexAvatarNameTag.cs
  class MpexAvatarNameTag (line 13) | public class MpexAvatarNameTag : MonoBehaviour
    type PlayerIconSlot (line 15) | enum PlayerIconSlot
    method Construct (line 29) | [Inject]
    method Awake (line 42) | private void Awake()
    method OnEnable (line 77) | private void OnEnable()
    method OnDisable (line 83) | private void OnDisable()
    method HandlePlatformData (line 89) | private void HandlePlatformData(IConnectedPlayer player, MpPlayerData ...
    method HandleMpexData (line 95) | private void HandleMpexData(IConnectedPlayer player, MpexPlayerData data)
    method SetPlatformData (line 101) | private void SetPlatformData(MpPlayerData data)
    method SetIcon (line 122) | private void SetIcon(PlayerIconSlot slot, Sprite sprite)

FILE: MultiplayerExtensions/Environment/MpexAvatarPlaceLighting.cs
  class MpexAvatarPlaceLighting (line 11) | public class MpexAvatarPlaceLighting : MonoBehaviour
    method Construct (line 24) | [Inject]
    method Start (line 37) | private void Start()
    method OnEnable (line 60) | private void OnEnable()
    method OnDisable (line 67) | private void OnDisable()
    method HandlePlayerData (line 74) | private void HandlePlayerData(IConnectedPlayer player, MpexPlayerData ...
    method HandlePlayerConnected (line 80) | private void HandlePlayerConnected(IConnectedPlayer player)
    method HandlePlayerDisconnected (line 90) | private void HandlePlayerDisconnected(IConnectedPlayer player)
    method Update (line 96) | private void Update()
    method SetColor (line 107) | public void SetColor(Color color, bool immediate)
    method GetColor (line 114) | public Color GetColor()
    method SetColor (line 121) | private void SetColor(Color color)

FILE: MultiplayerExtensions/Environment/MpexConnectedObjectManager.cs
  class MpexConnectedObjectManager (line 6) | public class MpexConnectedObjectManager : MonoBehaviour
    method Construct (line 13) | [Inject]
    method Start (line 26) | private void Start()
    method OnDestroy (line 33) | private void OnDestroy()
    method HandleIsObservedChangedEvent (line 39) | private void HandleIsObservedChangedEvent(bool isObserved)

FILE: MultiplayerExtensions/Environment/MpexLevelEndActions.cs
  class MpexLevelEndActions (line 6) | public class MpexLevelEndActions : IAffinity, ILevelEndActions
    method PlayerDidFinish (line 11) | [AffinityPrefix]
    method PlayerDidFail (line 16) | [AffinityPrefix]

FILE: MultiplayerExtensions/Environment/MpexPlayerFacadeLighting.cs
  class MpexPlayerFacadeLighting (line 8) | class MpexPlayerFacadeLighting : MonoBehaviour
    method Construct (line 45) | [Inject]
    method OnEnable (line 60) | public void OnEnable()
    method OnDisable (line 67) | public void OnDisable()
    method HandleNewLeaderWasSelected (line 72) | private void HandleNewLeaderWasSelected(string userId)
    method Update (line 77) | private void Update()
    method SetLights (line 94) | public void SetLights(Color color)

FILE: MultiplayerExtensions/Environment/MpexPlayerTableCell.cs
  class MpexPlayerTableCell (line 11) | public class MpexPlayerTableCell : IInitializable, IDisposable, IAffinity
    method MpexPlayerTableCell (line 26) | internal MpexPlayerTableCell(
    method Initialize (line 38) | public void Initialize()
    method Dispose (line 43) | public void Dispose()
    method SetDataPrefix (line 48) | [AffinityPrefix]
    method SetDataPostfix (line 56) | [AffinityPostfix]
    method SetLevelEntitlement (line 81) | private void SetLevelEntitlement(Image backgroundImage, EntitlementsSt...
    method HandleSetIsEntitledToLevel (line 94) | private void HandleSetIsEntitledToLevel(string userId, string levelId,...

FILE: MultiplayerExtensions/Installers/MpexAppInstaller.cs
  class MpexAppInstaller (line 10) | class MpexAppInstaller : Installer
    method MpexAppInstaller (line 14) | public MpexAppInstaller(
    method InstallBindings (line 20) | public override void InstallBindings()

FILE: MultiplayerExtensions/Installers/MpexGameInstaller.cs
  class MpexGameInstaller (line 12) | class MpexGameInstaller : Installer
    method InstallBindings (line 14) | public override void InstallBindings()
    method DecorateLocalActivePlayerFacade (line 24) | private MultiplayerLocalActivePlayerFacade DecorateLocalActivePlayerFa...
    method DecorateConnectedPlayerFacade (line 31) | private MultiplayerConnectedPlayerFacade DecorateConnectedPlayerFacade...

FILE: MultiplayerExtensions/Installers/MpexLobbyInstaller.cs
  class MpexLobbyInstaller (line 9) | class MpexLobbyInstaller : Installer
    method InstallBindings (line 11) | public override void InstallBindings()
    method DecorateAvatarPlace (line 17) | private MultiplayerLobbyAvatarPlace DecorateAvatarPlace(MultiplayerLob...
    method DecorateAvatar (line 23) | private MultiplayerLobbyAvatarController DecorateAvatar(MultiplayerLob...

FILE: MultiplayerExtensions/Installers/MpexLocalActivePlayerInstaller.cs
  class MpexLocalActivePlayerInstaller (line 7) | public class MpexLocalActivePlayerInstaller : MonoInstaller
    method InstallBindings (line 9) | public override void InstallBindings()

FILE: MultiplayerExtensions/Installers/MpexMenuInstaller.cs
  class MpexMenuInstaller (line 10) | class MpexMenuInstaller : Installer
    method InstallBindings (line 12) | public override void InstallBindings()

FILE: MultiplayerExtensions/Patchers/AvatarPlacePatcher.cs
  class AvatarPlacePatcher (line 10) | [HarmonyPatch]
    method AvatarPlacePatcher (line 15) | internal AvatarPlacePatcher(
    method SpawnAllPlaces (line 24) | [HarmonyTranspiler]
    method SetupAvatarPlace (line 32) | private static MultiplayerLobbyAvatarPlace SetupAvatarPlace(Multiplaye...
    method SpawnAllPlacesPrefix (line 38) | [AffinityPrefix]

FILE: MultiplayerExtensions/Patchers/ColorSchemePatcher.cs
  class ColorSchemePatcher (line 5) | public class ColorSchemePatcher : IAffinity
    method ColorSchemePatcher (line 10) | internal ColorSchemePatcher(
    method SetConnectedPlayerColorScheme (line 18) | [AffinityPostfix]

FILE: MultiplayerExtensions/Patchers/EnvironmentPatcher.cs
  class EnvironmentPatcher (line 15) | [HarmonyPatch]
    method EnvironmentPatcher (line 22) | internal EnvironmentPatcher(
    method PreventEnvironmentInjection (line 34) | [AffinityPostfix]
    method PreventEnvironmentInstall (line 74) | [AffinityPrefix]
    method PreventEnvironmentActivation (line 107) | [AffinityPrefix]
    method InjectEnvironment (line 137) | [AffinityPostfix]
    method InstallEnvironment (line 148) | [AffinityPrefix]
    method LoveYouCountersPlus (line 164) | [AffinityPrefix]
    method ActivateEnvironment (line 181) | [AffinityPostfix]
    method HideOtherPlayerPlatforms (line 205) | [HarmonyPostfix]
    method RemoveDuplicateInstalls (line 218) | [HarmonyPrefix]
    method SetEnvironmentColors (line 226) | [AffinityPostfix]

FILE: MultiplayerExtensions/Patchers/MenuEnvironmentPatcher.cs
  class MenuEnvironmentPatcher (line 8) | [HarmonyPatch]
    method MenuEnvironmentPatcher (line 15) | internal MenuEnvironmentPatcher(
    method EnableEnvironmentTab (line 25) | [HarmonyPrefix]
    method SetEnvironmentScene (line 35) | [AffinityPrefix]
    method ResetEnvironmentScene (line 48) | [AffinityPostfix]
    method AddEnvironmentOverrides (line 56) | [AffinityPrefix]

FILE: MultiplayerExtensions/Patchers/PlayerPositionPatcher.cs
  class PlayerPositionPatcher (line 8) | [HarmonyPatch]
    method PlayerPositionPatcher (line 13) | internal PlayerPositionPatcher(
    method SideBySideLayout (line 20) | [AffinityPrefix]
    method SideBySideLayoutConfirm (line 28) | [HarmonyPrefix]
    method SideBySideObjectDisable (line 38) | [HarmonyPrefix]
    method SideBySideAngle (line 46) | [AffinityPrefix]
    method SoloEnvironmentPosition (line 54) | [AffinityPrefix]

FILE: MultiplayerExtensions/Patches/AvatarPoseRestrictionPatch.cs
  class AvatarPoseRestrictionPatch (line 6) | [HarmonyPatch]
    method DisableAvatarRestrictions (line 9) | [HarmonyPrefix]

FILE: MultiplayerExtensions/Patches/PlatformMovementPatch.cs
  class PlatformMovementPatch (line 5) | [HarmonyPatch]
    method DisableVerticalPlayerMovement (line 8) | [HarmonyPrefix]

FILE: MultiplayerExtensions/Patches/ResumeSpawningPatch.cs
  class ResumeSpawningPatch (line 5) | [HarmonyPatch]
    method DisableAvatarRestrictions (line 8) | [HarmonyPrefix]

FILE: MultiplayerExtensions/Players/MpexPlayerData.cs
  class MpexPlayerData (line 6) | public class MpexPlayerData : INetSerializable
    method Serialize (line 13) | public void Serialize(NetDataWriter writer)
    method Deserialize (line 18) | public void Deserialize(NetDataReader reader)

FILE: MultiplayerExtensions/Players/MpexPlayerManager.cs
  class MpexPlayerManager (line 10) | public class MpexPlayerManager : IInitializable
    method MpexPlayerManager (line 22) | internal MpexPlayerManager(
    method Initialize (line 32) | public void Initialize()
    method Dispose (line 39) | public void Dispose()
    method HandlePlayerConnected (line 44) | private void HandlePlayerConnected(IConnectedPlayer player)
    method HandlePlayerData (line 52) | private void HandlePlayerData(MpexPlayerData packet, IConnectedPlayer ...
    method TryGetPlayer (line 58) | public bool TryGetPlayer(string userId, out MpexPlayerData player)
    method GetPlayer (line 61) | public MpexPlayerData? GetPlayer(string userId)

FILE: MultiplayerExtensions/Plugin.cs
  class Plugin (line 12) | [Plugin(RuntimeOptions.DynamicInit)]
    method Plugin (line 23) | [Init]
    method OnEnable (line 42) | [OnEnable]
    method OnDisable (line 48) | [OnDisable]

FILE: MultiplayerExtensions/UI/MpexEnvironmentViewController.cs
  class MpexEnvironmentViewController (line 9) | [ViewDefinition("MultiplayerExtensions.UI.MpexEnvironmentViewController....
    method Construct (line 24) | [Inject]
    method PostParse (line 33) | [UIAction("#post-parse")]

FILE: MultiplayerExtensions/UI/MpexGameplaySetup.cs
  class MpexGameplaySetup (line 14) | public class MpexGameplaySetup : NotifiableBase, IInitializable, IDispos...
    method MpexGameplaySetup (line 34) | internal MpexGameplaySetup(
    method Initialize (line 49) | public void Initialize()
    method Dispose (line 56) | public void Dispose()
    method PresentPreferences (line 61) | [UIAction("preferences-click")]
    method DeepestChildFlowCoordinator (line 69) | private FlowCoordinator DeepestChildFlowCoordinator(FlowCoordinator root)

FILE: MultiplayerExtensions/UI/MpexMiscViewController.cs
  class MpexMiscViewController (line 7) | [ViewDefinition("MultiplayerExtensions.UI.MpexMiscViewController.bsml")]
    method Construct (line 12) | [Inject]

FILE: MultiplayerExtensions/UI/MpexSettingsViewController.cs
  class MpexSettingsViewController (line 8) | [ViewDefinition("MultiplayerExtensions.UI.MpexSettingsViewController.bsm...
    method Construct (line 13) | [Inject]
    method PostParse (line 20) | [UIAction("#post-parse")]

FILE: MultiplayerExtensions/UI/MpexSetupFlowCoordinator.cs
  class MpexSetupFlowCoordinator (line 9) | public class MpexSetupFlowCoordinator : FlowCoordinator
    method Construct (line 17) | [Inject]
    method DidActivate (line 32) | protected override void DidActivate(bool firstActivation, bool addedTo...
    method DidDeactivate (line 46) | protected override void DidDeactivate(bool removedFromHierarchy, bool ...
    method DismissGameStartedEvent (line 52) | private void DismissGameStartedEvent(ILevelGameplaySetupData obj)
    method BackButtonWasPressed (line 57) | protected override void BackButtonWasPressed(ViewController topViewCon...

FILE: MultiplayerExtensions/Utilities/ColorConverter.cs
  class ColorConverter (line 8) | public class ColorConverter : ValueConverter<Color>
    method FromValue (line 10) | public override Color FromValue(Value? value, object parent)
    method ToValue (line 19) | public override Value? ToValue(Color obj, object parent)

FILE: MultiplayerExtensions/Utilities/SpriteManager.cs
  class SpriteManager (line 10) | public class SpriteManager : IInitializable, IDisposable
    method SpriteManager (line 19) | internal SpriteManager(
    method Initialize (line 25) | public void Initialize()
    method Dispose (line 33) | public void Dispose()
    method GetSpriteFromResources (line 45) | private Sprite GetSpriteFromResources(string resourcePath, float pixel...
    method GetResource (line 54) | private byte[] GetResource(Assembly asm, string resourceName)
    method GetSprite (line 62) | public Sprite? GetSprite(byte[]? data, float pixelsPerUnit = 100.0f, b...
Condensed preview — 49 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (125K chars).
[
  {
    "path": ".gitattributes",
    "chars": 2518,
    "preview": "###############################################################################\n# Set default behavior to automatically "
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 92,
    "preview": "patreon: goobwabber\ncustom: ['https://ko-fi.com/goobwabber', 'https://ko-fi.com/zingabopp']\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 1114,
    "preview": "---\nname: Bug report\nabout: Create a bug report\ntitle: \"[BUG] \"\nlabels: ''\nassignees: ''\n\n---\n**Multiplayer Extensions V"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 498,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: \"[FEATURE] \"\nlabels: ''\nassignees: ''\n\n---\n\n**I"
  },
  {
    "path": ".github/workflows/Build.yml",
    "chars": 1318,
    "preview": "name: Build\n\non:\n  push:\n    branches: [ master ]\n    paths:\n      - 'MultiplayerExtensions.sln'\n      - 'MultiplayerExt"
  },
  {
    "path": ".github/workflows/PR_Build.yml",
    "chars": 1340,
    "preview": "name: Pull Request Build\n\non:\n  pull_request:\n    branches: [ main ]\n    paths:\n      - 'MultiplayerExtensions.sln'\n    "
  },
  {
    "path": ".gitignore",
    "chars": 5926,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": "LICENSE",
    "chars": 1292,
    "preview": "MIT License\n\nCopyright (c) 2020 Zingabopp\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\n"
  },
  {
    "path": "MultiplayerExtensions/Config.cs",
    "chars": 1299,
    "preview": "using IPA.Config.Stores.Attributes;\nusing MultiplayerExtensions.Utilities;\nusing UnityEngine;\n\nnamespace MultiplayerExte"
  },
  {
    "path": "MultiplayerExtensions/Directory.Build.props",
    "chars": 739,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- This file contains project properties used by the build. -->\n<Project>\n  <P"
  },
  {
    "path": "MultiplayerExtensions/Directory.Build.targets",
    "chars": 7801,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<!-- This file contains the build tasks and targets for verifying the manifest, "
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexAvatarNameTag.cs",
    "chars": 5244,
    "preview": "using System;\nusing System.Collections.Generic;\nusing HMUI;\nusing MultiplayerCore.Players;\nusing MultiplayerExtensions."
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexAvatarPlaceLighting.cs",
    "chars": 4238,
    "preview": "using IPA.Utilities;\nusing MultiplayerExtensions.Players;\nusing System;\nusing System.Collections.Generic;\nusing System."
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexConnectedObjectManager.cs",
    "chars": 2067,
    "preview": "using UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Environment\n{\n    public class MpexConnectedObjectMa"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexLevelEndActions.cs",
    "chars": 687,
    "preview": "using SiraUtil.Affinity;\nusing System;\n\nnamespace MultiplayerExtensions.Environment\n{\n    public class MpexLevelEndActi"
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexPlayerFacadeLighting.cs",
    "chars": 4488,
    "preview": "using IPA.Utilities;\nusing System;\nusing UnityEngine;\nusing Zenject;\n\nnamespace MultiplayerExtensions.Environment\n{\n   "
  },
  {
    "path": "MultiplayerExtensions/Environment/MpexPlayerTableCell.cs",
    "chars": 4351,
    "preview": "using MultiplayerCore.Objects;\nusing SiraUtil.Affinity;\nusing System;\nusing System.Threading.Tasks;\nusing UnityEngine;\n"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexAppInstaller.cs",
    "chars": 700,
    "preview": "using IPA.Loader;\nusing MultiplayerExtensions.Patchers;\nusing MultiplayerExtensions.Players;\nusing MultiplayerExtension"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexGameInstaller.cs",
    "chars": 1714,
    "preview": "using IPA.Utilities;\nusing MultiplayerExtensions.Environment;\nusing MultiplayerExtensions.Patchers;\nusing MultiplayerEx"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexLobbyInstaller.cs",
    "chars": 1048,
    "preview": "using MultiplayerExtensions.Environments;\nusing MultiplayerExtensions.Environments.Lobby;\nusing SiraUtil.Extras;\nusing "
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexLocalActivePlayerInstaller.cs",
    "chars": 528,
    "preview": "using MultiplayerExtensions.Environment;\nusing MultiplayerExtensions.Patchers;\nusing Zenject;\n\nnamespace MultiplayerExt"
  },
  {
    "path": "MultiplayerExtensions/Installers/MpexMenuInstaller.cs",
    "chars": 1546,
    "preview": "using MultiplayerExtensions.Environments;\nusing MultiplayerExtensions.Objects;\nusing MultiplayerExtensions.Patchers;\nus"
  },
  {
    "path": "MultiplayerExtensions/MultiplayerExtensions.csproj",
    "chars": 11031,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<Project Sdk=\"Microsoft.NET.Sdk\">\n  <PropertyGroup>\n    <OutputType>Library</Outp"
  },
  {
    "path": "MultiplayerExtensions/Patchers/AvatarPlacePatcher.cs",
    "chars": 2121,
    "preview": "using HarmonyLib;\nusing MultiplayerExtensions.Environments;\nusing SiraUtil.Affinity;\nusing System.Collections.Generic;\n"
  },
  {
    "path": "MultiplayerExtensions/Patchers/ColorSchemePatcher.cs",
    "chars": 1157,
    "preview": "using SiraUtil.Affinity;\n\nnamespace MultiplayerExtensions.Patchers\n{\n    public class ColorSchemePatcher : IAffinity\n  "
  },
  {
    "path": "MultiplayerExtensions/Patchers/EnvironmentPatcher.cs",
    "chars": 12316,
    "preview": "using HarmonyLib;\nusing IPA.Utilities;\nusing SiraUtil.Affinity;\nusing SiraUtil.Logging;\nusing System;\nusing System.Coll"
  },
  {
    "path": "MultiplayerExtensions/Patchers/MenuEnvironmentPatcher.cs",
    "chars": 2747,
    "preview": "using HarmonyLib;\nusing SiraUtil.Affinity;\nusing SiraUtil.Logging;\nusing System.Linq;\n\nnamespace MultiplayerExtensions."
  },
  {
    "path": "MultiplayerExtensions/Patchers/PlayerPositionPatcher.cs",
    "chars": 2553,
    "preview": "using HarmonyLib;\nusing SiraUtil.Affinity;\nusing System.Collections.Generic;\nusing UnityEngine;\n\nnamespace MultiplayerE"
  },
  {
    "path": "MultiplayerExtensions/Patches/AvatarPoseRestrictionPatch.cs",
    "chars": 1078,
    "preview": "using HarmonyLib;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions.Patches\n{\n    [HarmonyPatch]\n    public class Ava"
  },
  {
    "path": "MultiplayerExtensions/Patches/PlatformMovementPatch.cs",
    "chars": 427,
    "preview": "using HarmonyLib;\n\nnamespace MultiplayerExtensions.Patches\n{\n    [HarmonyPatch]\n    public class PlatformMovementPatch\n"
  },
  {
    "path": "MultiplayerExtensions/Patches/ResumeSpawningPatch.cs",
    "chars": 466,
    "preview": "using HarmonyLib;\n\nnamespace MultiplayerExtensions.Patches\n{\n    [HarmonyPatch]\n    public class ResumeSpawningPatch\n  "
  },
  {
    "path": "MultiplayerExtensions/Players/MpexPlayerData.cs",
    "chars": 690,
    "preview": "using LiteNetLib.Utils;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions.Players\n{\n    public class MpexPlayerData :"
  },
  {
    "path": "MultiplayerExtensions/Players/MpexPlayerManager.cs",
    "chars": 1787,
    "preview": "using MultiplayerCore.Networking;\nusing System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\n"
  },
  {
    "path": "MultiplayerExtensions/Plugin.cs",
    "chars": 1743,
    "preview": "using HarmonyLib;\nusing IPA;\nusing IPA.Config.Stores;\nusing IPA.Loader;\nusing MultiplayerExtensions.Installers;\nusing S"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexEnvironmentViewController.bsml",
    "chars": 1096,
    "preview": "<bg xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexEnvironmentViewController.cs",
    "chars": 3189,
    "preview": "using BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.Components.Settings;\nusing BeatSaberMarkupLangu"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexGameplaySetup.bsml",
    "chars": 397,
    "preview": "<vertical id='vert'>\n\t<text text='MultiplayerExtensions' pad-bottom='0' align='Bottom' size-delta-y='10'/>\n\t<toggle-set"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexGameplaySetup.cs",
    "chars": 4255,
    "preview": "using BeatSaberMarkupLanguage;\nusing BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.Components;\nusin"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexMiscViewController.bsml",
    "chars": 983,
    "preview": "<bg xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexMiscViewController.cs",
    "chars": 1393,
    "preview": "using BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.ViewControllers;\nusing Zenject;\n\nnamespace Mult"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexSettingsViewController.bsml",
    "chars": 1306,
    "preview": "<bg xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance' xsi:schemaLocation='https://monkeymanboy.github.io/BSML-Docs/"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexSettingsViewController.cs",
    "chars": 2431,
    "preview": "using BeatSaberMarkupLanguage.Attributes;\nusing BeatSaberMarkupLanguage.Components.Settings;\nusing BeatSaberMarkupLangu"
  },
  {
    "path": "MultiplayerExtensions/UI/MpexSetupFlowCoordinator.cs",
    "chars": 2424,
    "preview": "using HMUI;\nusing Zenject;\nusing BeatSaberMarkupLanguage;\nusing SiraUtil.Affinity;\nusing System;\n\nnamespace Multiplayer"
  },
  {
    "path": "MultiplayerExtensions/Utilities/ColorConverter.cs",
    "chars": 735,
    "preview": "using IPA.Config.Data;\nusing IPA.Config.Stores;\nusing System;\nusing UnityEngine;\n\nnamespace MultiplayerExtensions.Utili"
  },
  {
    "path": "MultiplayerExtensions/Utilities/SpriteManager.cs",
    "chars": 3162,
    "preview": "using SiraUtil.Logging;\nusing System;\nusing System.Reflection;\nusing System.Security.Policy;\nusing UnityEngine;\nusing Z"
  },
  {
    "path": "MultiplayerExtensions/manifest.json",
    "chars": 630,
    "preview": "{\n  \"$schema\": \"https://raw.githubusercontent.com/bsmg/BSIPA-MetadataFileSchema/master/Schema.json\",\n  \"id\": \"Multiplaye"
  },
  {
    "path": "MultiplayerExtensions.sln",
    "chars": 1142,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio Version 16\nVisualStudioVersion = 16.0.3051"
  },
  {
    "path": "MultiplayerExtensions.v3.ncrunchsolution",
    "chars": 192,
    "preview": "<SolutionConfiguration>\n  <Settings>\n    <AllowParallelTestExecution>True</AllowParallelTestExecution>\n    <SolutionCon"
  },
  {
    "path": "README.md",
    "chars": 5450,
    "preview": "# MultiplayerExtensions (Steam/PC-Only) [![Build](https://github.com/Goobwabber/MultiplayerExtensions/workflows/Build/ba"
  }
]

About this extraction

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

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

Copied to clipboard!