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** **Your Platform** **Describe the bug** **To Reproduce** 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** **Log** **Screenshots/Video** **Additional context** ================================================ 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.** **Describe the solution you'd like** **Additional context** ================================================ 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 ================================================  true true true false true true ================================================ FILE: MultiplayerExtensions/Directory.Build.targets ================================================  2.0 false $(OutputPath)$(AssemblyName) $(OutputPath)Final True $(PluginVersion) $(PluginVersion) $(PluginVersion) $(AssemblyName) $(ArtifactName)-$(PluginVersion) $(ArtifactName)-bs$(GameVersion) $(ArtifactName)-$(CommitHash) $(AssemblyName) $(AssemblyName) $(OutDir)zip\ $(BeatSaberDir)\Plugins True Unable to copy assembly to game folder, did you set 'BeatSaberDir' correctly in your 'csproj.user' file? Plugins folder doesn't exist: '$(PluginDir)'. Unable to copy to Plugins folder, '$(BeatSaberDir)' does not appear to be a Beat Saber game install. Unable to copy to Plugins folder, 'BeatSaberDir' has not been set in your 'csproj.user' file. False $(BeatSaberDir)\IPA\Pending\Plugins ================================================ FILE: 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 _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(); _nameText = transform.Find("Name").GetComponent(); // Enable horizontal layout on bg if (!_bg.TryGetComponent(out _)) { var hLayout = _bg.gameObject.AddComponent(); 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(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(); imageView = iconObj.AddComponent(); 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.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 _lights = new List(); 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().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.Accessor _allLightsAnimators = FieldAccessor .GetAccessor(nameof(_allLightsAnimators)); private readonly FieldAccessor.Accessor _gameplayLightsAnimators = FieldAccessor .GetAccessor(nameof(_gameplayLightsAnimators)); private readonly FieldAccessor.Accessor _activeLightsColor = FieldAccessor .GetAccessor(nameof(_activeLightsColor)); private readonly FieldAccessor.Accessor _leadingLightsColor = FieldAccessor .GetAccessor(nameof(_leadingLightsColor)); private readonly FieldAccessor.Accessor _failedLightsColor = FieldAccessor .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 _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(); _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 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 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().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().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().AsSingle(); Container.BindInterfacesAndSelfTo().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(); return original; } private MultiplayerConnectedPlayerFacade DecorateConnectedPlayerFacade(MultiplayerConnectedPlayerFacade original) { if (Plugin.Config.MissLighting && !Plugin.Config.PersonalMissLightingOnly) original.gameObject.AddComponent(); original.gameObject.AddComponent(); 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(); return original; } private MultiplayerLobbyAvatarController DecorateAvatar(MultiplayerLobbyAvatarController original) { var avatarCaption = original.transform.Find("AvatarCaption").gameObject; avatarCaption.AddComponent(); 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().AsSingle(); Container.Bind().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().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesAndSelfTo().FromNewComponentOnNewGameObject().AsSingle(); Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); Container.BindInterfacesAndSelfTo().FromNewComponentAsViewController().AsSingle(); Container.BindInterfacesAndSelfTo().AsSingle(); // needed for local player's player place var avatarPlace = Container.Resolve().transform.Find("MultiplayerLobbyEnvironment").Find("LobbyAvatarPlace").gameObject; GameObject.Destroy(avatarPlace.GetComponent()); Container.Inject(avatarPlace.AddComponent()); } } } ================================================ FILE: MultiplayerExtensions/MultiplayerExtensions.csproj ================================================ Library Properties MultiplayerExtensions 1.0.3 net472 true portable ..\Refs $(LocalRefsDir) prompt 4 9.0 enable Unofficial local false DEBUG;TRACE true True True True $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll False False $(BeatSaberDir)\Beat Saber_Data\Managed\BGNet.dll False False $(BeatSaberDir)\Beat Saber_Data\Managed\Colors.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\GameplayCore.dll False False $(BeatSaberDir)\Libs\Hive.Versioning.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\HMRendering.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\LiteNetLib.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\netstandard.dll False False $(BeatSaberDir)\Beat Saber_Data\Managed\Polyglot.dll False $(BeatSaberDir)\Libs\SemVer.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\System.IO.Compression.dll False False $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\HMLib.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\HMUI.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.Timeline.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.AudioModule.dll False False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.CoreModule.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.DirectorModule.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.ImageConversionModule.dll False False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.TextRenderingModule.dll $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UI.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIElementsModule.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.UIModule.dll $(BeatSaberDir)\Beat Saber_Data\Managed\UnityEngine.VRModule.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject.dll False $(BeatSaberDir)\Beat Saber_Data\Managed\Zenject-usage.dll False $(BeatSaberDir)\Libs\0Harmony.dll False False $(BeatSaberDir)\Plugins\BSML.dll False False $(BeatSaberDir)\Plugins\SiraUtil.dll False 1.3.2 runtime; build; native; contentfiles; analyzers; buildtransitive all Official $(VersionType)-$(GitBranch)-$(CommitHash) $(VersionType)-$(GitBranch)-$(CommitHash)-$(GitModified) ================================================ 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).GetMethod(nameof(List.Add)); private static readonly MethodInfo _setupAvatarPlaceMethod = SymbolExtensions.GetMethodInfo(() => SetupAvatarPlace(null!, 0)); [HarmonyTranspiler] [HarmonyPatch(typeof(MultiplayerLobbyAvatarPlaceManager), nameof(MultiplayerLobbyAvatarPlaceManager.SpawnAllPlaces))] private static IEnumerable SpawnAllPlaces(IEnumerable 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().SortIndex = sortIndex; return avatarPlace; } [AffinityPrefix] [AffinityPatch(typeof(MultiplayerLobbyAvatarPlaceManager), nameof(MultiplayerLobbyAvatarPlaceManager.SpawnAllPlaces))] private void SpawnAllPlacesPrefix(ILobbyStateDataModel ____lobbyStateDataModel) => _environmentManager.transform.Find("MultiplayerLobbyEnvironment").Find("LobbyAvatarPlace").gameObject.GetComponent().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 _behavioursToInject = new(); [AffinityPostfix] [AffinityPatch(typeof(SceneDecoratorContext), "GetInjectableMonoBehaviours")] private void PreventEnvironmentInjection(SceneDecoratorContext __instance, List monoBehaviours, DiContainer ____container) { var scene = __instance.gameObject.scene; if (_scenesManager.IsSceneInStack("MultiplayerEnvironment") && _config.SoloEnvironment) { _logger.Info($"Fixing bind conflicts on scene '{scene.name}'."); List 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 _normalInstallers = new(); private List _normalInstallerTypes = new(); private List _scriptableObjectInstallers = new(); private List _monoInstallers = new(); private List _installerPrefabs = new(); [AffinityPrefix] [AffinityPatch(typeof(SceneDecoratorContext), "InstallDecoratorInstallers")] private void PreventEnvironmentInstall(SceneDecoratorContext __instance, List ____normalInstallers, List ____normalInstallerTypes, List ____scriptableObjectInstallers, List ____monoInstallers, List ____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 _objectsToEnable = new(); [AffinityPrefix] [AffinityPatch(typeof(GameScenesManager), "ActivatePresentedSceneRootObjects")] private void PreventEnvironmentActivation(List 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(); } 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(); if (hud != null) hud.gameObject.SetActive(true); } } } } [AffinityPostfix] [AffinityPatch(typeof(GameObjectContext), "GetInjectableMonoBehaviours")] private void InjectEnvironment(GameObjectContext __instance, List 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), typeof(List), typeof(List), typeof(List), typeof(List))] private void InstallEnvironment(Context __instance, List normalInstallers, List normalInstallerTypes, List scriptableObjectInstallers, List installers, List 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("Container"); var hud = (CoreGameHUDController)_behavioursToInject.Find(x => x is CoreGameHUDController); container.Unbind(); container.Bind().FromInstance(hud).AsSingle(); var multihud = __instance.transform.GetComponentInChildren(); multihud.gameObject.SetActive(false); var multiPositionHud = __instance.transform.GetComponentInChildren(); 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(); var activeOnlyGameObjects = localActivePlayer.GetField("_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("Container"); return !container.HasBinding(); } [AffinityPostfix] [AffinityPatch(typeof(GameplayCoreInstaller), nameof(GameplayCoreInstaller.InstallBindings))] private void SetEnvironmentColors(GameplayCoreInstaller __instance) { if (!_config.SoloEnvironment || !_scenesManager.IsSceneInStack("MultiplayerEnvironment")) return; DiContainer container = __instance.GetProperty("Container"); var colorManager = container.Resolve(); container.Inject(colorManager); colorManager.Awake(); colorManager.Start(); foreach (var gameObject in _objectsToEnable) { var lightSwitchEventEffects = gameObject.transform.GetComponentsInChildren(); 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 { /// /// Player's color set in the plugin config. /// 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 PlayerConnectedEvent = null!; public IReadOnlyDictionary Players => _playerData; private ConcurrentDictionary _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(HandlePlayerData); _sessionManager.playerConnectedEvent += HandlePlayerConnected; } public void Dispose() { _packetSerializer.UnregisterCallback(); } 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(); _harmony = new Harmony(ID); _metadata = pluginMetadata; Logger = logger; Config = config; zenjector.UseMetadataBinder(); zenjector.UseLogger(logger); zenjector.UseSiraSync(SiraUtil.Web.SiraSync.SiraSyncServiceType.GitHub, "Goobwabber", "MultiplayerExtensions"); zenjector.Install(Location.App, config); zenjector.Install(Location.Menu); zenjector.Install(); zenjector.Install(Location.MultiplayerCore); zenjector.Install(Location.MultiPlayer); } [OnEnable] public void OnEnable() { _harmony.PatchAll(_metadata.Assembly); } [OnDisable] public void OnDisable() { _harmony.UnpatchSelf(); } } } ================================================ FILE: MultiplayerExtensions/UI/MpexEnvironmentViewController.bsml ================================================  ================================================ 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.Accessor _showModifiers = FieldAccessor.GetAccessor(nameof(_showModifiers)); private FieldAccessor.Accessor _showEnvironmentOverrideSettings = FieldAccessor.GetAccessor(nameof(_showEnvironmentOverrideSettings)); private FieldAccessor.Accessor _showColorSchemesSettings = FieldAccessor.GetAccessor(nameof(_showColorSchemesSettings)); private FieldAccessor.Accessor _showMultiplayer = FieldAccessor.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 ================================================