diff --git a/CustomNotes/CustomNotes.csproj b/CustomNotes/CustomNotes.csproj index 33e6e52..9db8e4b 100644 --- a/CustomNotes/CustomNotes.csproj +++ b/CustomNotes/CustomNotes.csproj @@ -36,6 +36,11 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\BeatmapCore.dll False + + False + $(BeatSaberDir)\Beat Saber_Data\Managed\BGNet.dll + False + $(BeatSaberDir)\Plugins\BSML.dll False @@ -65,10 +70,20 @@ $(BeatSaberDir)\Beat Saber_Data\Managed\IPA.Loader.dll False + + False + $(BeatSaberDir)\Beat Saber_Data\Managed\LiteNetLib.dll + False + $(BeatSaberDir)\Beat Saber_Data\Managed\Main.dll False + + False + $(BeatSaberDir)\Plugins\MultiplayerExtensions.dll + False + $(BeatSaberDir)\Libs\SemVer.dll False @@ -78,6 +93,7 @@ $(BeatSaberDir)\Plugins\SiraUtil.dll False + $(BeatSaberDir)\Beat Saber_Data\Managed\Unity.TextMeshPro.dll False @@ -114,18 +130,29 @@ + + + + + + + + + + + @@ -137,6 +164,7 @@ + @@ -151,6 +179,14 @@ + + + + + + NoteQuickAccessController.cs + + copy "$(TargetPath)" "$(BeatSaberDir)\Plugins" diff --git a/CustomNotes/Data/CustomNote.cs b/CustomNotes/Data/CustomNote.cs index 58d0edd..0335613 100644 --- a/CustomNotes/Data/CustomNote.cs +++ b/CustomNotes/Data/CustomNote.cs @@ -1,6 +1,7 @@ using CustomNotes.Utilities; using System; using System.IO; +using System.Security.Cryptography; using UnityEngine; namespace CustomNotes.Data @@ -15,6 +16,7 @@ public class CustomNote public GameObject NoteDotLeft { get; } public GameObject NoteDotRight { get; } public GameObject NoteBomb { get; } + public string MD5Hash { get; } = string.Empty; public string ErrorMessage { get; } = string.Empty; public CustomNote(string fileName) @@ -25,7 +27,17 @@ public CustomNote(string fileName) { try { - AssetBundle = AssetBundle.LoadFromFile(Path.Combine(Plugin.PluginAssetPath, fileName)); + string path = Path.Combine(Plugin.PluginAssetPath, fileName); + + using (var stream = File.OpenRead(path)) { + using (var md5 = MD5.Create()) { + var hash = md5.ComputeHash(stream); + // https://modelsaber.com/api/v2/get.php?type=bloq&filter=hash: + MD5Hash = BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant(); + AssetBundle = AssetBundle.LoadFromStream(stream); + } + } + GameObject note = AssetBundle.LoadAsset("assets/_customnote.prefab"); Descriptor = note.GetComponent(); diff --git a/CustomNotes/Data/MultiplayerCustomNoteData.cs b/CustomNotes/Data/MultiplayerCustomNoteData.cs new file mode 100644 index 0000000..1623c45 --- /dev/null +++ b/CustomNotes/Data/MultiplayerCustomNoteData.cs @@ -0,0 +1,22 @@ +using CustomNotes.Packets; + +namespace CustomNotes.Data +{ + class MultiplayerCustomNoteData + { + + public string NoteHash { get; private set; } = CustomNotesPacket.DEFAULT_NOTES; + public float NoteScale { get; private set; } = 1f; + + public MultiplayerCustomNoteData(string noteHash = CustomNotesPacket.DEFAULT_NOTES, float noteScale = 1f) { + NoteHash = noteHash; + NoteScale = noteScale; + } + + public void UpdateFromPacket(CustomNotesPacket packet) { + NoteHash = packet.NoteHash; + NoteScale = packet.NoteScale; + } + + } +} diff --git a/CustomNotes/INoteHashPacketManager.cs b/CustomNotes/INoteHashPacketManager.cs new file mode 100644 index 0000000..05b685b --- /dev/null +++ b/CustomNotes/INoteHashPacketManager.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CustomNotes.Interfaces +{ + internal interface INoteHashPacketManager + { + + bool HasHashFromPlayer(IConnectedPlayer connectedPlayer); + string GetHashFromPlayer(IConnectedPlayer connectedPlayer); + + } +} diff --git a/CustomNotes/Installers/CustomNotesCoreInstaller.cs b/CustomNotes/Installers/CustomNotesCoreInstaller.cs index 463a0dc..6d01f4e 100644 --- a/CustomNotes/Installers/CustomNotesCoreInstaller.cs +++ b/CustomNotes/Installers/CustomNotesCoreInstaller.cs @@ -1,8 +1,9 @@ -using Zenject; -using SiraUtil.Interfaces; -using CustomNotes.Managers; +using CustomNotes.Managers; using CustomNotes.Providers; using CustomNotes.Settings.Utilities; +using IPA.Loader; +using SiraUtil.Interfaces; +using Zenject; namespace CustomNotes.Installers { @@ -22,6 +23,18 @@ public override void InstallBindings() Container.Bind(typeof(IModelProvider), typeof(CustomGameNoteProvider)).To().AsSingle(); Container.Bind(typeof(IModelProvider), typeof(CustomBombNoteProvider)).To().AsSingle(); + + Container.Bind(typeof(IModelProvider), typeof(CustomMultiplayerNoteProvider)).To().AsSingle(); + Container.Bind(typeof(IModelProvider), typeof(CustomMultiplayerBombProvider)).To().AsSingle(); + + if (PluginManager.GetPluginFromId("MultiplayerExtensions") != null) + { + Container.BindInterfacesAndSelfTo().AsSingle(); + } + else + { + Container.BindInterfacesAndSelfTo().AsSingle(); + } } } } \ No newline at end of file diff --git a/CustomNotes/Installers/CustomNotesGameInstaller.cs b/CustomNotes/Installers/CustomNotesGameInstaller.cs index cd9bbb4..6fbb08b 100644 --- a/CustomNotes/Installers/CustomNotesGameInstaller.cs +++ b/CustomNotes/Installers/CustomNotesGameInstaller.cs @@ -1,5 +1,6 @@ -using Zenject; -using CustomNotes.Managers; +using CustomNotes.Managers; +using CustomNotes.Providers; +using Zenject; namespace CustomNotes.Installers { @@ -17,6 +18,7 @@ public override void InstallBindings() if (_noteAssetLoader.SelectedNote != 0) { Container.Bind().To().AsSingle(); + Container.BindInterfacesAndSelfTo().AsSingle(); } } } diff --git a/CustomNotes/Installers/CustomNotesMenuInstaller.cs b/CustomNotes/Installers/CustomNotesMenuInstaller.cs index 8cf6a49..dd50789 100644 --- a/CustomNotes/Installers/CustomNotesMenuInstaller.cs +++ b/CustomNotes/Installers/CustomNotesMenuInstaller.cs @@ -1,8 +1,8 @@ -using Zenject; -using SiraUtil; -using CustomNotes.Managers; +using CustomNotes.Managers; using CustomNotes.Settings; using CustomNotes.Settings.UI; +using SiraUtil; +using Zenject; namespace CustomNotes.Installers { @@ -10,13 +10,12 @@ internal class CustomNotesMenuInstaller : Installer { public override void InstallBindings() { - - Container.Bind().FromNewComponentAsViewController().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); Container.Bind().FromNewComponentAsViewController().AsSingle(); - Container.BindFlowCoordinator(); + Container.Bind().FromNewComponentOnNewGameObject($"{nameof(NotesFlowCoordinator)}HostGameObject").AsSingle(); + Container.BindInterfacesAndSelfTo().AsSingle(); Container.BindInterfacesTo().AsSingle(); } } diff --git a/CustomNotes/Interfaces/ICustomNoteNetworkPacketManager.cs b/CustomNotes/Interfaces/ICustomNoteNetworkPacketManager.cs new file mode 100644 index 0000000..a7c43c5 --- /dev/null +++ b/CustomNotes/Interfaces/ICustomNoteNetworkPacketManager.cs @@ -0,0 +1,11 @@ +using CustomNotes.Data; + +namespace CustomNotes.Interfaces +{ + internal interface ICustomNoteNetworkPacketManager + { + bool HasDataFromPlayer(IConnectedPlayer connectedPlayer); + + MultiplayerCustomNoteData GetData(IConnectedPlayer connectedPlayer); + } +} diff --git a/CustomNotes/Managers/CustomBombController.cs b/CustomNotes/Managers/CustomBombController.cs index 77dd650..ec1d5bb 100644 --- a/CustomNotes/Managers/CustomBombController.cs +++ b/CustomNotes/Managers/CustomBombController.cs @@ -1,29 +1,18 @@ -using Zenject; -using UnityEngine; -using CustomNotes.Data; +using CustomNotes.Data; using CustomNotes.Settings.Utilities; using SiraUtil.Objects; +using UnityEngine; +using Zenject; namespace CustomNotes.Managers { - internal class CustomBombController : MonoBehaviour + internal class CustomBombController : CustomBombControllerBase { - private PluginConfig _pluginConfig; - - private CustomNote _customNote; - private NoteMovement _noteMovement; - private BombNoteController _bombNoteController; - - protected Transform bombMesh; - - protected GameObject activeNote; - protected SiraPrefabContainer container; - protected SiraPrefabContainer.Pool bombPool; - [Inject] internal void Init(PluginConfig pluginConfig, NoteAssetLoader noteAssetLoader, [Inject(Id = "cn.bomb")] SiraPrefabContainer.Pool bombContainerPool) { _pluginConfig = pluginConfig; + _noteSize = pluginConfig.NoteSize; _customNote = noteAssetLoader.CustomNoteObjects[noteAssetLoader.SelectedNote]; _bombNoteController = GetComponent(); @@ -36,29 +25,46 @@ internal void Init(PluginConfig pluginConfig, NoteAssetLoader noteAssetLoader, [ MeshRenderer bm = GetComponentInChildren(); bm.enabled = false; } + } + + internal abstract class CustomBombControllerBase : MonoBehaviour + { + protected PluginConfig _pluginConfig; + + protected CustomNote _customNote; + protected NoteMovement _noteMovement; + protected NoteController _bombNoteController; + + protected float _noteSize = 1f; + + protected Transform bombMesh; + + protected GameObject activeNote; + protected SiraPrefabContainer container; + protected SiraPrefabContainer.Pool bombPool; - private void DidFinish() + protected virtual void DidFinish() { container.transform.SetParent(null); bombPool.Despawn(container); } - private void Controller_Init(NoteController noteController) + protected virtual void Controller_Init(NoteController noteController) { SpawnThenParent(bombPool); } - private void ParentNote(GameObject fakeMesh) + protected virtual void ParentNote(GameObject fakeMesh) { fakeMesh.SetActive(true); container.transform.SetParent(bombMesh); fakeMesh.transform.localPosition = container.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f); container.transform.localRotation = Quaternion.identity; - fakeMesh.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f) * _pluginConfig.NoteSize; + fakeMesh.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f) * _noteSize; container.transform.localScale = Vector3.one; } - private void SpawnThenParent(SiraPrefabContainer.Pool bombModelPool) + protected virtual void SpawnThenParent(SiraPrefabContainer.Pool bombModelPool) { container = bombModelPool.Spawn(); activeNote = container.Prefab; @@ -66,7 +72,7 @@ private void SpawnThenParent(SiraPrefabContainer.Pool bombModelPool) ParentNote(activeNote); } - protected void OnDestroy() + protected virtual void OnDestroy() { if (_bombNoteController != null) { diff --git a/CustomNotes/Managers/CustomMultiplayerBombController.cs b/CustomNotes/Managers/CustomMultiplayerBombController.cs new file mode 100644 index 0000000..175ca41 --- /dev/null +++ b/CustomNotes/Managers/CustomMultiplayerBombController.cs @@ -0,0 +1,57 @@ +using CustomNotes.Data; +using CustomNotes.Packets; +using CustomNotes.Providers; +using CustomNotes.Settings.Utilities; +using SiraUtil.Objects; +using System; +using UnityEngine; +using Zenject; + +namespace CustomNotes.Managers +{ + class CustomMultiplayerBombController : CustomBombControllerBase + { + [Inject] + internal void Init(DiContainer Container, + PluginConfig pluginConfig, + ConnectedPlayerNotePoolProvider connectedPlayerNotePoolProvider, + IConnectedPlayer connectedPlayer) + { + _pluginConfig = pluginConfig; + + string id = connectedPlayerNotePoolProvider.GetPoolIDForPlayer(connectedPlayer); + + if (id.Equals(CustomNotesPacket.DEFAULT_NOTES)) + { + return; + } + + bombPool = Container.TryResolveId($"cn.multi.{id}.bomb"); + + _customNote = Container.TryResolveId($"cn.multi.{id}.note"); + + if (bombPool == null) + { + return; + } + + try + { + _noteSize = Container.ResolveId($"cn.multi.{connectedPlayer.userId}.scale"); + } + catch (Exception ex) + { + Logger.log.Error($" ({nameof(CustomMultiplayerBombController)}) This shouldn't happen: {ex.Message}"); + } + + _bombNoteController = GetComponent(); + _noteMovement = GetComponent(); + _bombNoteController.didInitEvent += Controller_Init; + _noteMovement.noteDidFinishJumpEvent += DidFinish; + bombMesh = gameObject.transform.Find("Mesh"); + + MeshRenderer bm = GetComponentInChildren(); + bm.enabled = false; + } + } +} diff --git a/CustomNotes/Managers/CustomMultiplayerNoteController.cs b/CustomNotes/Managers/CustomMultiplayerNoteController.cs new file mode 100644 index 0000000..ca9910a --- /dev/null +++ b/CustomNotes/Managers/CustomMultiplayerNoteController.cs @@ -0,0 +1,121 @@ +using CustomNotes.Data; +using CustomNotes.Overrides; +using CustomNotes.Packets; +using CustomNotes.Providers; +using CustomNotes.Settings.Utilities; +using SiraUtil.Objects; +using System; +using UnityEngine; +using Zenject; + +namespace CustomNotes.Managers +{ + public class CustomMultiplayerNoteController : CustomNoteControllerBase + { + + [Inject] + private CustomMultiplayerNoteEventManager _customMultiplayerNoteEventManager; + + [Inject] + internal void Init(DiContainer Container, + PluginConfig pluginConfig, + ConnectedPlayerNotePoolProvider connectedPlayerNotePoolProvider, + CustomMultiplayerNoteEventManager customMultiplayerNoteEventManager, + IConnectedPlayer connectedPlayer) + { + _pluginConfig = pluginConfig; + + string id = connectedPlayerNotePoolProvider.GetPoolIDForPlayer(connectedPlayer); + + if(id.Equals(CustomNotesPacket.DEFAULT_NOTES)) + { + return; + } + + _leftArrowNotePool = Container.TryResolveId($"cn.multi.{id}.left.arrow"); + _rightArrowNotePool = Container.TryResolveId($"cn.multi.{id}.right.arrow"); + _leftDotNotePool = Container.TryResolveId($"cn.multi.{id}.left.dot") ?? _leftArrowNotePool; + _rightDotNotePool = Container.TryResolveId($"cn.multi.{id}.right.dot") ?? _rightArrowNotePool; + + _customNote = Container.TryResolveId($"cn.multi.{id}.note"); + + if(_leftArrowNotePool == null) + { + return; + } + + try + { + _noteSize = Container.ResolveId($"cn.multi.{connectedPlayer.userId}.scale"); + } + catch (Exception ex) + { + Logger.log.Error($" ({nameof(CustomMultiplayerNoteController)}) This shouldn't happen: {ex.Message}"); + } + + _customMultiplayerNoteEventManager = customMultiplayerNoteEventManager; + + _gameNoteController = GetComponent(); + _customNoteColorNoteVisuals = gameObject.AddComponent(); + + // The NoteControllers NoteWasCut & NoteWasMissed events aren't fired for Multiplayer Notes ... + _customMultiplayerNoteEventManager.onGameNoteCutEvent += WasCut; + _customMultiplayerNoteEventManager.onGameNoteMissEvent += DidFinish; + _gameNoteController.didInitEvent += Controller_DidInit; + _customNoteColorNoteVisuals.didInitEvent += Visuals_DidInit; + + noteCube = _gameNoteController.gameObject.transform.Find("NoteCube"); + + MeshRenderer noteMesh = GetComponentInChildren(); + noteMesh.forceRenderingOff = true; + } + + protected override void DidFinish(NoteController nc) { + if (nc != this._gameNoteController) return; + container.transform.SetParent(null); + switch (nc.noteData.colorType) { + case ColorType.ColorA: + case ColorType.ColorB: + if (container != null) { + activePool?.Despawn(container); + _customMultiplayerNoteEventManager?.GameNoteDespawnedCallback(_gameNoteController as MultiplayerConnectedPlayerGameNoteController); + container = null; + } + break; + default: + break; + } + } + + protected override void OnDestroy() { + if (_gameNoteController != null && _customMultiplayerNoteEventManager != null) { + _customMultiplayerNoteEventManager.onGameNoteCutEvent -= WasCut; + _customMultiplayerNoteEventManager.onGameNoteMissEvent -= DidFinish; + _gameNoteController.didInitEvent -= Controller_DidInit; + } + if (_customNoteColorNoteVisuals != null) { + _customNoteColorNoteVisuals.didInitEvent -= Visuals_DidInit; + } + } + + protected override void ParentNote(GameObject fakeMesh) + { + container.transform.SetParent(noteCube); + fakeMesh.transform.localPosition = container.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f); + fakeMesh.transform.rotation = container.transform.rotation = fakeMesh.transform.localRotation = Quaternion.Euler(0f, 0f, 0f); + container.transform.localRotation = noteCube.parent.localRotation; + container.transform.Rotate(new Vector3(0, -90, 0), Space.Self); + fakeMesh.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f) * _noteSize; + container.transform.localScale = Vector3.one; + } + + protected override void SpawnThenParent(SiraPrefabContainer.Pool noteModelPool) { + container = noteModelPool.Spawn(); + activeNote = container.Prefab; + activePool = noteModelPool; + ParentNote(activeNote); + _customMultiplayerNoteEventManager?.GameNoteSpawnedCallback(_gameNoteController as MultiplayerConnectedPlayerGameNoteController); + } + + } +} \ No newline at end of file diff --git a/CustomNotes/Managers/CustomMultiplayerNoteEventManager.cs b/CustomNotes/Managers/CustomMultiplayerNoteEventManager.cs new file mode 100644 index 0000000..709901e --- /dev/null +++ b/CustomNotes/Managers/CustomMultiplayerNoteEventManager.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using UnityEngine; + +namespace CustomNotes.Managers +{ + /// + /// Multiplayer Game Note Controllers don't emit the NoteCut and NoteMissed Events, this is a replacement + /// + public class CustomMultiplayerNoteEventManager : IDisposable + { + + private class ActiveItemTracker : IDisposable where T : MonoBehaviour + { + public HashSet activeItems { get; private set; } = new HashSet(); + + public ActiveItemTracker() {} + + public void OnSpawn(T item) + { + this.activeItems.Add(item); + } + public void OnDespawn(T item) + { + this.activeItems.Remove(item); + } + public bool Contains(T item) + { + return this.activeItems.Contains(item); + } + public void Dispose() + { + this.activeItems = null; + } + + } + + public event Action onGameNoteCutEvent; + public event Action onGameNoteMissEvent; + + private static readonly IPA.Utilities.FieldAccessor.Accessor _vUnitAccessor = IPA.Utilities.FieldAccessor.GetAccessor("_v"); + private static readonly IPA.Utilities.FieldAccessor.Accessor _vAccessor = IPA.Utilities.FieldAccessor.GetAccessor("_v"); + + private IConnectedPlayerNoteEventManager _connectedPlayerNoteEventManager; + + private ActiveItemTracker _activeMultiGameNoteControllers = new ActiveItemTracker(); + + public CustomMultiplayerNoteEventManager(IConnectedPlayerNoteEventManager connectedPlayerNoteEventManager) + { + _connectedPlayerNoteEventManager = connectedPlayerNoteEventManager; + connectedPlayerNoteEventManager.connectedPlayerNoteWasCutEvent += HandleNoteWasCutEvent; + connectedPlayerNoteEventManager.connectedPlayerNoteWasMissedEvent += HandleNoteWasMissedEvent; + } + + public void Dispose() + { + _connectedPlayerNoteEventManager.connectedPlayerNoteWasCutEvent -= HandleNoteWasCutEvent; + _connectedPlayerNoteEventManager.connectedPlayerNoteWasMissedEvent -= HandleNoteWasMissedEvent; + } + + public void GameNoteSpawnedCallback(MultiplayerConnectedPlayerGameNoteController noteController) + { + if(!_activeMultiGameNoteControllers.Contains(noteController)) + { + _activeMultiGameNoteControllers.OnSpawn(noteController); + } + } + + //private bool _isDownStackingGameNotes = false; + //private Stack _despawnGNCStack = new Stack(5); + + public void GameNoteDespawnedCallback(MultiplayerConnectedPlayerGameNoteController noteController) + { + //_despawnGNCStack.Push(noteController); + } + + /*public void ManageGameNoteDespawnStack() + { + _isDownStackingGameNotes = true; + while (_despawnGNCStack.Count > 0) + { + _activeMultiGameNoteControllers.OnDespawn(_despawnGNCStack.Pop() as MultiplayerConnectedPlayerGameNoteController); + } + _isDownStackingGameNotes = false; + }*/ + + private void HandleNoteWasCutEvent(NoteCutInfoNetSerializable ncins) + { + foreach (MultiplayerConnectedPlayerGameNoteController noteController in _activeMultiGameNoteControllers.activeItems) + { + if (CompareNotes(noteController, ncins)) + { + this.onGameNoteCutEvent?.Invoke(noteController, ToNCI(ncins)); + break; + } + } + //if (!_isDownStackingGameNotes) ManageGameNoteDespawnStack(); + } + + private void HandleNoteWasMissedEvent(NoteMissInfoNetSerializable nmins) + { + foreach (MultiplayerConnectedPlayerGameNoteController noteController in _activeMultiGameNoteControllers.activeItems) + { + if (CompareNotes(noteController, nmins)) + { + this.onGameNoteMissEvent?.Invoke(noteController); + break; + } + } + //if (!_isDownStackingGameNotes) ManageGameNoteDespawnStack(); + } + + public static bool CompareNotes(NoteController nc, NoteCutInfoNetSerializable nci) + { + NoteData noteData = nc.noteData; + return Mathf.Approximately(noteData.time, nci.noteTime) && noteData.lineIndex == nci.noteLineIndex && noteData.noteLineLayer == nci.noteLineLayer; + } + + public static bool CompareNotes(NoteController nc, NoteMissInfoNetSerializable nmi) + { + NoteData noteData = nc.noteData; + return Mathf.Approximately(noteData.time, nmi.noteTime) && noteData.lineIndex == nmi.noteLineIndex && noteData.noteLineLayer == nmi.noteLineLayer; + } + + public static NoteCutInfo ToNCI(NoteCutInfoNetSerializable i) + { + return new NoteCutInfo( + i.cutWasOk, + i.cutWasOk, + i.cutWasOk, + !i.cutWasOk, + i.saberSpeed, + _vUnitAccessor(ref i.saberDir), + ColorTypeToSaberType(i.colorType), + 0, + 0, + _vAccessor(ref i.cutPoint), + _vUnitAccessor(ref i.cutNormal), + 0, + 0, + null + ); + } + + public static SaberType ColorTypeToSaberType(ColorType ct) + { + return ct == ColorType.ColorA ? SaberType.SaberA : SaberType.SaberB; + } + + } +} diff --git a/CustomNotes/Managers/CustomNoteController.cs b/CustomNotes/Managers/CustomNoteController.cs index 0bf72d5..9bf78c4 100644 --- a/CustomNotes/Managers/CustomNoteController.cs +++ b/CustomNotes/Managers/CustomNoteController.cs @@ -1,34 +1,16 @@ -using Zenject; -using UnityEngine; -using SiraUtil.Objects; -using CustomNotes.Data; -using SiraUtil.Interfaces; +using CustomNotes.Data; using CustomNotes.Overrides; using CustomNotes.Settings.Utilities; using CustomNotes.Utilities; +using SiraUtil.Interfaces; +using SiraUtil.Objects; +using UnityEngine; +using Zenject; namespace CustomNotes.Managers { - public class CustomNoteController : MonoBehaviour, IColorable + public class CustomNoteController : CustomNoteControllerBase { - private PluginConfig _pluginConfig; - - protected Transform noteCube; - private CustomNote _customNote; - private GameNoteController _gameNoteController; - private CustomNoteColorNoteVisuals _customNoteColorNoteVisuals; - - protected GameObject activeNote; - protected SiraPrefabContainer container; - protected SiraPrefabContainer.Pool activePool; - - private SiraPrefabContainer.Pool _leftDotNotePool; - private SiraPrefabContainer.Pool _rightDotNotePool; - private SiraPrefabContainer.Pool _leftArrowNotePool; - private SiraPrefabContainer.Pool _rightArrowNotePool; - - public Color Color => _customNoteColorNoteVisuals != null ? _customNoteColorNoteVisuals.noteColor : Color.white; - [Inject] internal void Init(PluginConfig pluginConfig, NoteAssetLoader noteAssetLoader, @@ -38,6 +20,7 @@ internal void Init(PluginConfig pluginConfig, [InjectOptional(Id = "cn.right.dot")] SiraPrefabContainer.Pool rightDotNotePool) { _pluginConfig = pluginConfig; + _noteSize = pluginConfig.NoteSize; _leftArrowNotePool = leftArrowNotePool; _rightArrowNotePool = rightArrowNotePool; @@ -59,8 +42,30 @@ internal void Init(PluginConfig pluginConfig, MeshRenderer noteMesh = GetComponentInChildren(); noteMesh.forceRenderingOff = true; } + } + + public abstract class CustomNoteControllerBase : MonoBehaviour, IColorable + { + protected PluginConfig _pluginConfig; + + protected Transform noteCube; + protected CustomNote _customNote; + protected NoteController _gameNoteController; + protected CustomNoteColorNoteVisuals _customNoteColorNoteVisuals; + protected float _noteSize = 1f; + + protected GameObject activeNote; + protected SiraPrefabContainer container; + protected SiraPrefabContainer.Pool activePool; + + protected SiraPrefabContainer.Pool _leftDotNotePool; + protected SiraPrefabContainer.Pool _rightDotNotePool; + protected SiraPrefabContainer.Pool _leftArrowNotePool; + protected SiraPrefabContainer.Pool _rightArrowNotePool; + + public Color Color => _customNoteColorNoteVisuals != null ? _customNoteColorNoteVisuals.noteColor : Color.white; - private void DidFinish(NoteController nc) + protected virtual void DidFinish(NoteController nc) { container.transform.SetParent(null); switch (nc.noteData.colorType) @@ -78,12 +83,12 @@ private void DidFinish(NoteController nc) } } - private void WasCut(NoteController nc, NoteCutInfo _) + protected virtual void WasCut(NoteController nc, NoteCutInfo _) { DidFinish(nc); } - private void Controller_DidInit(NoteController noteController) + protected virtual void Controller_DidInit(NoteController noteController) { var data = noteController.noteData; SpawnThenParent(data.colorType == ColorType.ColorA @@ -91,16 +96,16 @@ private void Controller_DidInit(NoteController noteController) : data.cutDirection == NoteCutDirection.Any ? _rightDotNotePool : _rightArrowNotePool); } - private void ParentNote(GameObject fakeMesh) + protected virtual void ParentNote(GameObject fakeMesh) { container.transform.SetParent(noteCube); fakeMesh.transform.localPosition = container.transform.localPosition = new Vector3(0.0f, 0.0f, 0.0f); container.transform.localRotation = Quaternion.identity; - fakeMesh.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f) * _pluginConfig.NoteSize; + fakeMesh.transform.localScale = new Vector3(0.4f, 0.4f, 0.4f) * _noteSize; container.transform.localScale = Vector3.one; } - private void SpawnThenParent(SiraPrefabContainer.Pool noteModelPool) + protected virtual void SpawnThenParent(SiraPrefabContainer.Pool noteModelPool) { container = noteModelPool.Spawn(); activeNote = container.Prefab; @@ -108,7 +113,7 @@ private void SpawnThenParent(SiraPrefabContainer.Pool noteModelPool) ParentNote(activeNote); } - protected void SetActiveThenColor(GameObject note, Color color) + protected virtual void SetActiveThenColor(GameObject note, Color color) { note.SetActive(true); if (_customNote.Descriptor.UsesNoteColor) @@ -117,7 +122,7 @@ protected void SetActiveThenColor(GameObject note, Color color) } } - private void Visuals_DidInit(ColorNoteVisuals visuals, NoteController noteController) + protected virtual void Visuals_DidInit(ColorNoteVisuals visuals, NoteController noteController) { SetActiveThenColor(activeNote, visuals.noteColor); // Hide certain parts of the default note which is not required @@ -125,13 +130,13 @@ private void Visuals_DidInit(ColorNoteVisuals visuals, NoteController noteContro { _customNoteColorNoteVisuals.TurnOffVisuals(); } - else if(_pluginConfig.NoteSize != 1) + else if(_noteSize != 1) { - _customNoteColorNoteVisuals.ScaleVisuals(_pluginConfig.NoteSize); + _customNoteColorNoteVisuals.ScaleVisuals(_noteSize); } } - protected void OnDestroy() + protected virtual void OnDestroy() { if (_gameNoteController != null) { diff --git a/CustomNotes/Managers/CustomNoteManager.cs b/CustomNotes/Managers/CustomNoteManager.cs index d2b1c6b..257f3fc 100644 --- a/CustomNotes/Managers/CustomNoteManager.cs +++ b/CustomNotes/Managers/CustomNoteManager.cs @@ -1,7 +1,7 @@ -using Zenject; -using CustomNotes.Data; -using SiraUtil.Services; +using CustomNotes.Data; using CustomNotes.Utilities; +using SiraUtil.Services; +using Zenject; namespace CustomNotes.Managers { diff --git a/CustomNotes/Managers/CustomNotesNetworkPacketManager.cs b/CustomNotes/Managers/CustomNotesNetworkPacketManager.cs new file mode 100644 index 0000000..416e87f --- /dev/null +++ b/CustomNotes/Managers/CustomNotesNetworkPacketManager.cs @@ -0,0 +1,134 @@ +using CustomNotes.Data; +using CustomNotes.Interfaces; +using CustomNotes.Packets; +using CustomNotes.Settings.Utilities; +using MultiplayerExtensions.Packets; +using System; +using System.Collections.Generic; +using Zenject; + +namespace CustomNotes.Managers +{ + internal class DummyCustomNotesNetworkPacketManager : ICustomNoteNetworkPacketManager + { + private readonly MultiplayerCustomNoteData _defaultData = new MultiplayerCustomNoteData(); + + public DummyCustomNotesNetworkPacketManager() + { + Logger.log.Info("Disabling CustomNote sync, no MultiplayerExtensions detected."); + } + + public bool HasDataFromPlayer(IConnectedPlayer connectedPlayer) + { + return false; + } + + public MultiplayerCustomNoteData GetData(IConnectedPlayer connectedPlayer) + { + return _defaultData; + } + } + + internal class CustomNotesNetworkPacketManager : ICustomNoteNetworkPacketManager, IInitializable, IDisposable + { + private readonly PluginConfig _pluginConfig; + private readonly NoteAssetLoader _noteAssetLoader; + + private readonly PacketManager _packetManager; + private readonly IMultiplayerSessionManager _sessionManager; + + private readonly MultiplayerCustomNoteData _defaultData = new MultiplayerCustomNoteData(); + private readonly Dictionary _customNoteData = new Dictionary(); + + [Inject] + public CustomNotesNetworkPacketManager(PluginConfig pluginConfig, + NoteAssetLoader noteAssetLoader, + [InjectOptional] PacketManager packetManager, + [InjectOptional] IMultiplayerSessionManager sessionManager) + { + _pluginConfig = pluginConfig; + _noteAssetLoader = noteAssetLoader; + _packetManager = packetManager; + _sessionManager = sessionManager; + } + + public void Initialize() + { + if (_sessionManager == null || _packetManager == null) return; + + Logger.log.Info($"Initializing {nameof(CustomNotesNetworkPacketManager)}"); + + _sessionManager.connectedEvent += OnSessionConnectedEvent; + _sessionManager.playerDisconnectedEvent += OnPlayerDisconnected; + _packetManager.RegisterCallback(HandleCustomNotesPacket); + _noteAssetLoader.customNoteSelectionChangedEvent += OnCustomNoteSelectionChanged; + } + + private void OnSessionConnectedEvent() + { + Logger.log.Debug("Connected to a new Session!"); + SendUpdatePacket(); + } + + private void OnCustomNoteSelectionChanged(int index, CustomNote note) + { + if(_sessionManager.isConnected) + { + SendUpdatePacket(); + } + } + + public void Dispose() + { + if (_sessionManager != null) + { + _sessionManager.connectedEvent -= OnSessionConnectedEvent; + _sessionManager.playerDisconnectedEvent -= OnPlayerDisconnected; + } + if (_packetManager != null) + { + _packetManager.UnregisterCallback(); + } + if(_noteAssetLoader != null) + { + _noteAssetLoader.customNoteSelectionChangedEvent -= OnCustomNoteSelectionChanged; + } + } + + public bool HasDataFromPlayer(IConnectedPlayer connectedPlayer) + { + return _customNoteData.ContainsKey(connectedPlayer.userId); + } + + public MultiplayerCustomNoteData GetData(IConnectedPlayer connectedPlayer) + { + return GetCustomNoteData(connectedPlayer) ?? _defaultData; + } + + private MultiplayerCustomNoteData GetCustomNoteData(IConnectedPlayer connectedPlayer) + { + if (_customNoteData.TryGetValue(connectedPlayer.userId, out MultiplayerCustomNoteData data)) return data; + return null; + } + + private void HandleCustomNotesPacket(CustomNotesPacket packet, IConnectedPlayer connectedPlayer) + { + Logger.log.Info($"Received {nameof(packet)} from {connectedPlayer.userName} ({connectedPlayer.userId}): hash = {packet.NoteHash} , scale = {packet.NoteScale}"); + var data = GetCustomNoteData(connectedPlayer) ?? new MultiplayerCustomNoteData(); + data.UpdateFromPacket(packet); + _customNoteData[connectedPlayer.userId] = data; + } + + private void OnPlayerDisconnected(IConnectedPlayer connectedPlayer) + { + _customNoteData.Remove(connectedPlayer.userId); + } + + private void SendUpdatePacket() + { + var note = _noteAssetLoader.CustomNoteObjects[_noteAssetLoader.SelectedNote]; + Logger.log.Debug($"Sending {nameof(CustomNotesPacket)}."); + _sessionManager.Send(CustomNotesPacket.CreatePacket(note.MD5Hash, _pluginConfig.NoteSize)); + } + } +} diff --git a/CustomNotes/Managers/MenuButtonManager.cs b/CustomNotes/Managers/MenuButtonManager.cs index db78238..7315785 100644 --- a/CustomNotes/Managers/MenuButtonManager.cs +++ b/CustomNotes/Managers/MenuButtonManager.cs @@ -1,8 +1,9 @@ -using System; -using Zenject; -using BeatSaberMarkupLanguage; -using CustomNotes.Settings.UI; +using BeatSaberMarkupLanguage; +using BeatSaberMarkupLanguage.GameplaySetup; using BeatSaberMarkupLanguage.MenuButtons; +using CustomNotes.Settings.UI; +using System; +using Zenject; namespace CustomNotes.Managers { @@ -12,16 +13,20 @@ internal class MenuButtonManager : IInitializable, IDisposable private readonly MainFlowCoordinator _mainFlowCoordinator; private readonly NotesFlowCoordinator _notesFlowCoordinator; - public MenuButtonManager(MainFlowCoordinator mainFlowCoordinator, NotesFlowCoordinator notesFlowCoordinator) + private NoteQuickAccessController _noteQuickAccessController; + + public MenuButtonManager(MainFlowCoordinator mainFlowCoordinator, NotesFlowCoordinator notesFlowCoordinator, NoteQuickAccessController noteQuickAccessController) { _mainFlowCoordinator = mainFlowCoordinator; _notesFlowCoordinator = notesFlowCoordinator; _menuButton = new MenuButton("Custom Notes", "Change Custom Notes Here!", ShowNotesFlow, true); + _noteQuickAccessController = noteQuickAccessController; } public void Initialize() { MenuButtons.instance.RegisterButton(_menuButton); + GameplaySetup.instance.AddTab("Custom Notes", "CustomNotes.Settings.UI.Views.quickAccess.bsml", _noteQuickAccessController); } public void Dispose() @@ -30,6 +35,10 @@ public void Dispose() { MenuButtons.instance.UnregisterButton(_menuButton); } + if(GameplaySetup.IsSingletonAvailable) + { + GameplaySetup.instance.RemoveTab("Custom Notes"); + } } private void ShowNotesFlow() diff --git a/CustomNotes/Managers/NoteAssetLoader.cs b/CustomNotes/Managers/NoteAssetLoader.cs index d332a6b..1b7ea2c 100644 --- a/CustomNotes/Managers/NoteAssetLoader.cs +++ b/CustomNotes/Managers/NoteAssetLoader.cs @@ -1,12 +1,12 @@ -using System; -using Zenject; -using System.IO; -using System.Linq; -using CustomNotes.Data; +using CustomNotes.Data; using CustomNotes.Providers; +using CustomNotes.Settings.Utilities; using CustomNotes.Utilities; +using System; using System.Collections.Generic; -using CustomNotes.Settings.Utilities; +using System.IO; +using System.Linq; +using Zenject; namespace CustomNotes.Managers { @@ -32,9 +32,13 @@ public int SelectedNote _customGameNoteProvider.Priority = 300; _customBombNoteProvider.Priority = CustomNoteObjects[_selectedNote].NoteBomb != null ? 300 : -1; } + + customNoteSelectionChangedEvent?.Invoke(_selectedNote, CustomNoteObjects[_selectedNote]); } } + public event Action customNoteSelectionChangedEvent; + public IList CustomNoteObjects { get; private set; } public IEnumerable CustomNoteFiles { get; private set; } = Enumerable.Empty(); @@ -42,7 +46,9 @@ public int SelectedNote private readonly CustomGameNoteProvider _customGameNoteProvider; private readonly CustomBombNoteProvider _customBombNoteProvider; - internal NoteAssetLoader(PluginConfig pluginConfig, CustomGameNoteProvider customGameNoteProvider, CustomBombNoteProvider customBombNoteProvider) + internal NoteAssetLoader(PluginConfig pluginConfig, + CustomGameNoteProvider customGameNoteProvider, + CustomBombNoteProvider customBombNoteProvider) { _pluginConfig = pluginConfig; _customGameNoteProvider = customGameNoteProvider; @@ -110,6 +116,21 @@ public void Dispose() CustomNoteFiles = Enumerable.Empty(); } + /// + /// Returns the CustomNote with hash hash or null if it's not found + /// + /// The hash to look for + /// CustomNote + public CustomNote GetNoteByHash(string hash) + { + foreach(CustomNote note in CustomNoteObjects) + { + if (note.MD5Hash.Equals(hash)) + return note; + } + return null; + } + private IList LoadCustomNotes(IEnumerable customNoteFiles) { IList customNotes = new List diff --git a/CustomNotes/Packets/CustomNotesPacket.cs b/CustomNotes/Packets/CustomNotesPacket.cs new file mode 100644 index 0000000..b9d7f8c --- /dev/null +++ b/CustomNotes/Packets/CustomNotesPacket.cs @@ -0,0 +1,68 @@ +using LiteNetLib.Utils; +using UnityEngine; + +namespace CustomNotes.Packets +{ + class CustomNotesPacket : INetSerializable, IPoolablePacket + { + public const int MAX_LENGTH = 32; // MD5 Hash length as a hexadecimal string + public const string DEFAULT_NOTES = "_hello_i_am_using_default_notes_"; + public const float MIN_NOTE_SIZE = 0.4f; + public const float MAX_NOTE_SIZE = 2f; + + private string _noteHash = DEFAULT_NOTES; + public string NoteHash + { + get => _noteHash; + set + { + if (value.Length < MAX_LENGTH) + { + _noteHash = value.PadRight(MAX_LENGTH); + } + else if (value.Length > MAX_LENGTH) + { + _noteHash = value.Truncate(MAX_LENGTH); + } + else + { + _noteHash = value; + } + } + } + + private float _noteScale = 1f; + public float NoteScale + { + get => _noteScale; + set + { + _noteScale = Mathf.Max(Mathf.Min(value, MAX_NOTE_SIZE), MIN_NOTE_SIZE); + } + } + + public void Release() => ThreadStaticPacketPool.pool.Release(this); + + public void Serialize(NetDataWriter writer) + { + writer.Put(NoteHash, MAX_LENGTH); + writer.Put(NoteScale); + } + + public void Deserialize(NetDataReader reader) + { + NoteHash = reader.GetString(MAX_LENGTH); + NoteScale = reader.GetFloat(); + } + + public CustomNotesPacket Init(string hash, float noteScale = 1f) + { + NoteHash = hash; + NoteScale = noteScale; + + return this; + } + + public static CustomNotesPacket CreatePacket(string hash, float noteScale = 1f) => ThreadStaticPacketPool.pool.Obtain().Init(hash, noteScale); + } +} diff --git a/CustomNotes/Providers/ConnectedPlayerNotePoolProvider.cs b/CustomNotes/Providers/ConnectedPlayerNotePoolProvider.cs new file mode 100644 index 0000000..1128321 --- /dev/null +++ b/CustomNotes/Providers/ConnectedPlayerNotePoolProvider.cs @@ -0,0 +1,180 @@ +using CustomNotes.Data; +using CustomNotes.Interfaces; +using CustomNotes.Managers; +using CustomNotes.Settings.Utilities; +using CustomNotes.Utilities; +using SiraUtil.Objects; +using System; +using System.Collections.Generic; +using System.Security.Cryptography; +using System.Text; +using UnityEngine; +using Zenject; + +namespace CustomNotes.Providers +{ + internal class ConnectedPlayerNotePoolProvider : IInitializable, IDisposable + { + private DiContainer _container; + private PluginConfig _pluginConfig; + private NoteAssetLoader _noteAssetLoader; + private ICustomNoteNetworkPacketManager _noteHashPacketManager; + + private readonly CustomMultiplayerNoteProvider _customMultiplayerNoteProvider; + private readonly CustomMultiplayerBombProvider _customMultiplayerBombProvider; + + private IMultiplayerSessionManager _multiplayerSessionManager; + + private Dictionary _connectedPlayerPoolIDs; + + private static MD5 _staticMd5Hasher = MD5.Create(); + + [Inject] + internal ConnectedPlayerNotePoolProvider(DiContainer Container, + PluginConfig pluginConfig, + NoteAssetLoader noteAssetLoader, + ICustomNoteNetworkPacketManager noteHashPacketManager, + CustomMultiplayerNoteProvider customMultiplayerNoteProvider, + CustomMultiplayerBombProvider customMultiplayerBombProvider, + [InjectOptional] IMultiplayerSessionManager multiplayerSessionManager) + { + _container = Container; + _pluginConfig = pluginConfig; + _noteAssetLoader = noteAssetLoader; + _noteHashPacketManager = noteHashPacketManager; + + _customMultiplayerNoteProvider = customMultiplayerNoteProvider; + _customMultiplayerBombProvider = customMultiplayerBombProvider; + + _multiplayerSessionManager = multiplayerSessionManager; + } + + public void Initialize() + { + if (_multiplayerSessionManager == null) return; + + _connectedPlayerPoolIDs = new Dictionary(); + + if (_pluginConfig.OtherPlayerMultiplayerNotes) + { + _customMultiplayerNoteProvider.Priority = 300; + _customMultiplayerBombProvider.Priority = 300; + } + else + { + _customMultiplayerNoteProvider.Priority = -1; + _customMultiplayerBombProvider.Priority = -1; + return; + } + + foreach (IConnectedPlayer connectedPlayer in _multiplayerSessionManager.connectedPlayers) + { + CreateNotePoolsForPlayer(connectedPlayer); + } + } + + private void CreateNotePoolsForPlayer(IConnectedPlayer connectedPlayer) + { + Logger.log.Debug($"Creating note pools for player \"{connectedPlayer.userName}\" - \"{connectedPlayer.userId}\" ..."); + + CustomNote note = null; + float noteScale = 1f; + if (_pluginConfig.SyncNotesInMultiplayer && _noteHashPacketManager.HasDataFromPlayer(connectedPlayer)) + { + Logger.log.Debug($"Preparing note from hash for player \"{connectedPlayer.userName}\""); + + var multiplayerNoteData = _noteHashPacketManager.GetData(connectedPlayer); + + note = _noteAssetLoader.GetNoteByHash(multiplayerNoteData.NoteHash); + noteScale = multiplayerNoteData.NoteScale; + } + + if (note == null && _pluginConfig.RandomMultiplayerNotes) + { + System.Random rng; + + if (_pluginConfig.RandomnessIsConsistentPerPlayer) + { + // Set the Random seed based on player ID -> same random number every time for the same player + var hashed = _staticMd5Hasher.ComputeHash(Encoding.UTF8.GetBytes(connectedPlayer?.userId)); + var ivalue = BitConverter.ToInt32(hashed, 0); + rng = new System.Random(ivalue); + } + else + { + rng = new System.Random(); + } + + note = _pluginConfig.RandomMultiplayerNotes ? _noteAssetLoader.CustomNoteObjects[rng.Next(1, _noteAssetLoader.CustomNoteObjects.Count)] : _noteAssetLoader.CustomNoteObjects[_noteAssetLoader.SelectedNote]; + } + else if(note == null) + { + Logger.log.Debug("using same notes as local player ..."); + note = _noteAssetLoader.CustomNoteObjects[_noteAssetLoader.SelectedNote]; + } + + _container.Bind().WithId($"cn.multi.{connectedPlayer.userId}.scale").FromInstance(noteScale); + + foreach (KeyValuePair entry in _connectedPlayerPoolIDs) + { + if (entry.Value.Equals(note.MD5Hash)) + { + Logger.log.Debug($"Reusing old note pool for \"{note.FileName}\""); + _connectedPlayerPoolIDs.Add(connectedPlayer, note.MD5Hash); + return; + } + + } + + Logger.log.Debug($"Creating new pool for note \"{note.FileName}\" with hash \"{note.MD5Hash}\""); + + MaterialSwapper.GetMaterials(); + MaterialSwapper.ReplaceMaterialsForGameObject(note.NoteLeft); + MaterialSwapper.ReplaceMaterialsForGameObject(note.NoteRight); + MaterialSwapper.ReplaceMaterialsForGameObject(note.NoteDotLeft); + MaterialSwapper.ReplaceMaterialsForGameObject(note.NoteDotRight); + MaterialSwapper.ReplaceMaterialsForGameObject(note.NoteBomb); + Utils.AddMaterialPropertyBlockController(note.NoteLeft); + Utils.AddMaterialPropertyBlockController(note.NoteRight); + Utils.AddMaterialPropertyBlockController(note.NoteDotLeft); + Utils.AddMaterialPropertyBlockController(note.NoteDotRight); + + _container.Bind().WithId($"cn.multi.{note.MD5Hash}.note").FromInstance(note); + _container.BindMemoryPool().WithId($"cn.multi.{note.MD5Hash}.left.arrow").WithInitialSize(25).FromComponentInNewPrefab(NotePrefabContainer(note.NoteLeft)); + _container.BindMemoryPool().WithId($"cn.multi.{note.MD5Hash}.right.arrow").WithInitialSize(25).FromComponentInNewPrefab(NotePrefabContainer(note.NoteRight)); + if (note.NoteDotLeft != null) + { + _container.BindMemoryPool().WithId($"cn.multi.{note.MD5Hash}.left.dot").WithInitialSize(10).FromComponentInNewPrefab(NotePrefabContainer(note.NoteDotLeft)); + } + if (note.NoteDotRight != null) + { + _container.BindMemoryPool().WithId($"cn.multi.{note.MD5Hash}.right.dot").WithInitialSize(10).FromComponentInNewPrefab(NotePrefabContainer(note.NoteDotRight)); + } + if (note.NoteBomb) + { + _container.BindMemoryPool().WithId($"cn.multi.{note.MD5Hash}.bomb").WithInitialSize(10).FromComponentInNewPrefab(NotePrefabContainer(note.NoteBomb)); + } + + _connectedPlayerPoolIDs.Add(connectedPlayer, note.MD5Hash); + } + + public string GetPoolIDForPlayer(IConnectedPlayer connectedPlayer) + { + if (_connectedPlayerPoolIDs.TryGetValue(connectedPlayer, out string id)) + return id; + return string.Empty; + } + + private SiraPrefabContainer NotePrefabContainer(GameObject initialPrefab) + { + var prefab = new GameObject("CustomNotes_Multi" + initialPrefab.name).AddComponent(); + prefab.Prefab = initialPrefab; + return prefab; + } + + public void Dispose() + { + + } + } +} diff --git a/CustomNotes/Providers/CustomBombNoteProvider.cs b/CustomNotes/Providers/CustomBombNoteProvider.cs index 4dd2d9c..d4341a3 100644 --- a/CustomNotes/Providers/CustomBombNoteProvider.cs +++ b/CustomNotes/Providers/CustomBombNoteProvider.cs @@ -1,10 +1,10 @@ -using System; -using Zenject; -using UnityEngine; -using SiraUtil.Objects; -using SiraUtil.Interfaces; -using CustomNotes.Managers; +using CustomNotes.Managers; using CustomNotes.Utilities; +using SiraUtil.Interfaces; +using SiraUtil.Objects; +using System; +using UnityEngine; +using Zenject; namespace CustomNotes.Providers { diff --git a/CustomNotes/Providers/CustomGameNoteProvider.cs b/CustomNotes/Providers/CustomGameNoteProvider.cs index 4c88b10..fe00607 100644 --- a/CustomNotes/Providers/CustomGameNoteProvider.cs +++ b/CustomNotes/Providers/CustomGameNoteProvider.cs @@ -1,10 +1,10 @@ -using System; -using Zenject; -using UnityEngine; -using SiraUtil.Objects; -using SiraUtil.Interfaces; -using CustomNotes.Managers; +using CustomNotes.Managers; using CustomNotes.Utilities; +using SiraUtil.Interfaces; +using SiraUtil.Objects; +using System; +using UnityEngine; +using Zenject; namespace CustomNotes.Providers { diff --git a/CustomNotes/Providers/CustomMultiplayerBombProvider.cs b/CustomNotes/Providers/CustomMultiplayerBombProvider.cs new file mode 100644 index 0000000..aba5814 --- /dev/null +++ b/CustomNotes/Providers/CustomMultiplayerBombProvider.cs @@ -0,0 +1,36 @@ +using CustomNotes.Managers; +using CustomNotes.Settings.Utilities; +using SiraUtil.Interfaces; +using System; +using Zenject; + +namespace CustomNotes.Providers +{ + internal class CustomMultiplayerBombProvider : IModelProvider + { + public Type Type => typeof(CustomMultiplayerBombDecorator); + public int Priority { get; set; } = 300; + + internal class CustomMultiplayerBombDecorator : IPrefabProvider + { + public bool Chain => true; + public bool CanSetup { get; private set; } + + [Inject] + public void Construct(PluginConfig pluginConfig, GameplayCoreSceneSetupData sceneSetupData) + { + CanSetup = !(sceneSetupData.gameplayModifiers.ghostNotes || sceneSetupData.gameplayModifiers.disappearingArrows) && pluginConfig.OtherPlayerMultiplayerNotes; + } + + public MultiplayerConnectedPlayerBombNoteController Modify(MultiplayerConnectedPlayerBombNoteController original) + { + if (!CanSetup) return original; + original.gameObject.AddComponent(); + return original; + } + + } + + } + +} \ No newline at end of file diff --git a/CustomNotes/Providers/CustomMultiplayerNoteProvider.cs b/CustomNotes/Providers/CustomMultiplayerNoteProvider.cs new file mode 100644 index 0000000..c3cc4ee --- /dev/null +++ b/CustomNotes/Providers/CustomMultiplayerNoteProvider.cs @@ -0,0 +1,38 @@ +using CustomNotes.Managers; +using CustomNotes.Settings.Utilities; +using SiraUtil.Interfaces; +using System; +using Zenject; + +namespace CustomNotes.Providers +{ + internal class CustomMultiplayerNoteProvider : IModelProvider + { + public Type Type => typeof(CustomMultiplayerNoteDecorator); + public int Priority { get; set; } = 300; + + internal class CustomMultiplayerNoteDecorator : IPrefabProvider + { + public bool Chain => true; + public bool CanSetup { get; private set; } + + [Inject] + public void Construct(DiContainer Container, GameplayCoreSceneSetupData sceneSetupData, PluginConfig pluginConfig) + { + //bool isMultiplayer = Container.HasBinding(); + CanSetup = !(sceneSetupData.gameplayModifiers.ghostNotes || sceneSetupData.gameplayModifiers.disappearingArrows) && pluginConfig.OtherPlayerMultiplayerNotes; + + Container.BindInterfacesAndSelfTo().AsSingle(); + } + + public MultiplayerConnectedPlayerGameNoteController Modify(MultiplayerConnectedPlayerGameNoteController original) + { + if (!CanSetup) return original; + original.gameObject.AddComponent(); + return original; + } + } + + } + +} \ No newline at end of file diff --git a/CustomNotes/Settings/UI/NoteDetailsViewController.cs b/CustomNotes/Settings/UI/NoteDetailsViewController.cs index 84c112c..3d4ec43 100644 --- a/CustomNotes/Settings/UI/NoteDetailsViewController.cs +++ b/CustomNotes/Settings/UI/NoteDetailsViewController.cs @@ -8,6 +8,10 @@ using Zenject; using System; using CustomNotes.Settings.UI; +using System.Collections.Generic; +using System.Linq; +using CustomNotes.Utilities.MultiplayerCustomNoteModeExtensions; +using BeatSaberMarkupLanguage.Parser; namespace CustomNotes.Settings { @@ -18,6 +22,9 @@ internal class NoteDetailsViewController : BSMLResourceViewController private PluginConfig _pluginConfig; private NoteListViewController _listViewController; + [UIParams] + BSMLParserParams parserParams = null; + [UIComponent("note-description")] public TextPageScrollView noteDescription = null; @@ -51,5 +58,37 @@ public float noteSize _listViewController.ScalePreviewNotes(value); } } + + [UIValue("multi-note-mode-list-options")] + private List options = new object[] + { + MultiplayerCustomNoteMode.None.ToSettingsString(), + MultiplayerCustomNoteMode.SameAsLocalPlayer.ToSettingsString(), + MultiplayerCustomNoteMode.Random.ToSettingsString(), + MultiplayerCustomNoteMode.RandomConsistent.ToSettingsString() + }.ToList(); + + [UIValue("multi-note-mode-list-choice")] + protected string listChoice = null; + + [UIAction("on-multi-note-mode-change")] + protected void OnMultiNoteModeChange(string choice) + { + MultiplayerCustomNoteModeExtensions.SetMultiNoteSettingFromString(_pluginConfig, choice); + } + + [UIValue("multi-note-sync")] + protected bool syncMultiNotes + { + get => _pluginConfig.SyncNotesInMultiplayer; + set => _pluginConfig.SyncNotesInMultiplayer = value; + } + + [UIAction("#post-parse")] + public void PostParse() + { + listChoice = MultiplayerCustomNoteModeExtensions.GetMultiNoteSettingString(_pluginConfig); + parserParams.EmitEvent("multi-note-mode-update"); + } } } diff --git a/CustomNotes/Settings/UI/NoteListViewController.cs b/CustomNotes/Settings/UI/NoteListViewController.cs index 8531f6e..9e6a2be 100644 --- a/CustomNotes/Settings/UI/NoteListViewController.cs +++ b/CustomNotes/Settings/UI/NoteListViewController.cs @@ -16,9 +16,11 @@ internal class NoteListViewController : BSMLResourceViewController { public override string ResourceName => "CustomNotes.Settings.UI.Views.noteList.bsml"; + private DiContainer _diContainer; private PluginConfig _pluginConfig; private NoteAssetLoader _noteAssetLoader; private GameplaySetupViewController _gameplaySetupViewController; + private NoteQuickAccessController _noteQuickAccessController = null; private bool isGeneratingPreview = false; private GameObject preview; @@ -47,8 +49,9 @@ internal class NoteListViewController : BSMLResourceViewController public Action customNoteChanged; [Inject] - public void Construct(PluginConfig pluginConfig, NoteAssetLoader noteAssetLoader, GameplaySetupViewController gameplaySetupViewController) + public void Construct(DiContainer diContainer, PluginConfig pluginConfig, NoteAssetLoader noteAssetLoader, GameplaySetupViewController gameplaySetupViewController) { + _diContainer = diContainer; _pluginConfig = pluginConfig; _noteAssetLoader = noteAssetLoader; _gameplaySetupViewController = gameplaySetupViewController; @@ -57,9 +60,24 @@ public void Construct(PluginConfig pluginConfig, NoteAssetLoader noteAssetLoader [UIComponent("noteList")] public CustomListTableData customListTableData = null; + [UIComponent("scroll-indicator")] + protected BSMLScrollIndicator scrollIndicator = null; + + private Coroutine _scrollIndicatorCoroutine = null; + + [UIAction("update-scroll-indicator-up")] + protected void ScrollUp() => Utils.ScrollTheScrollIndicator(true, customListTableData.tableView, scrollIndicator, _scrollIndicatorCoroutine); + + [UIAction("update-scroll-indicator-down")] + protected void ScrollDown() => Utils.ScrollTheScrollIndicator(false, customListTableData.tableView, scrollIndicator, _scrollIndicatorCoroutine); + [UIAction("noteSelect")] public void Select(TableView _, int row) { + if(_noteQuickAccessController == null) { + _noteQuickAccessController = _diContainer.Resolve(); + } + _noteQuickAccessController?.ExternalNoteSelect(row); _noteAssetLoader.SelectedNote = row; _pluginConfig.LastNote = _noteAssetLoader.CustomNoteObjects[row].FileName; @@ -72,6 +90,11 @@ public void ReloadNotes() _noteAssetLoader.Reload(); SetupList(); Select(customListTableData.tableView, _noteAssetLoader.SelectedNote); + if (_noteQuickAccessController == null) { + _noteQuickAccessController = _diContainer.Resolve(); + } + _noteQuickAccessController?.ExternalReload(); + Utils.UpdateScrollIndicator(customListTableData.tableView, scrollIndicator, 10); } [UIAction("#post-parse")] @@ -111,6 +134,9 @@ protected override void DidActivate(bool firstActivation, bool addedToHierarchy, } Select(customListTableData.tableView, _noteAssetLoader.SelectedNote); + customListTableData.tableView.ScrollToCellWithIdx(_noteAssetLoader.SelectedNote, TableViewScroller.ScrollPositionType.Beginning, false); + customListTableData.tableView.SelectCellWithIdx(_noteAssetLoader.SelectedNote); + Utils.UpdateScrollIndicator(customListTableData.tableView, scrollIndicator, 10); } protected override void DidDeactivate(bool removedFromHierarchy, bool screenSystemDisabling) diff --git a/CustomNotes/Settings/UI/NoteQuickAccessController.bsml b/CustomNotes/Settings/UI/NoteQuickAccessController.bsml new file mode 100644 index 0000000..f4e8866 --- /dev/null +++ b/CustomNotes/Settings/UI/NoteQuickAccessController.bsml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/CustomNotes/Settings/UI/NoteQuickAccessController.cs b/CustomNotes/Settings/UI/NoteQuickAccessController.cs new file mode 100644 index 0000000..1ce3c1b --- /dev/null +++ b/CustomNotes/Settings/UI/NoteQuickAccessController.cs @@ -0,0 +1,123 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using System.Runtime.CompilerServices; +using BeatSaberMarkupLanguage; +using BeatSaberMarkupLanguage.Attributes; +using BeatSaberMarkupLanguage.Components; +using BeatSaberMarkupLanguage.Parser; +using BeatSaberMarkupLanguage.ViewControllers; +using CustomNotes.Data; +using CustomNotes.Managers; +using CustomNotes.Settings.Utilities; +using CustomNotes.Utilities; +using CustomNotes.Utilities.MultiplayerCustomNoteModeExtensions; +using HMUI; +using UnityEngine; +using Zenject; + +namespace CustomNotes.Settings.UI +{ + internal class NoteQuickAccessController : IInitializable, IDisposable, INotifyPropertyChanged + { + private PluginConfig _pluginConfig; + private NoteAssetLoader _noteAssetLoader; + + [UIParams] + BSMLParserParams parserParams = null; + + [Inject] + public NoteQuickAccessController(PluginConfig pluginConfig, NoteAssetLoader noteAssetLoader) { + _pluginConfig = pluginConfig; + _noteAssetLoader = noteAssetLoader; + } + + [UIValue("multi-note-mode-list-options")] + protected List options = new object[] + { + MultiplayerCustomNoteMode.None.ToSettingsString(), + MultiplayerCustomNoteMode.SameAsLocalPlayer.ToSettingsString(), + MultiplayerCustomNoteMode.Random.ToSettingsString(), + MultiplayerCustomNoteMode.RandomConsistent.ToSettingsString() + }.ToList(); + + [UIValue("multi-note-mode-list-choice")] + protected string listChoice = null; + + [UIComponent("note-list")] + protected CustomListTableData customListTableData = null; + + [UIComponent("scroll-indicator")] + protected BSMLScrollIndicator scrollIndicator = null; + + private Coroutine _scrollIndicatorCoroutine = null; + + [UIAction("update-scroll-indicator-up")] + protected void ScrollUp() => Utils.ScrollTheScrollIndicator(true, customListTableData.tableView, scrollIndicator, _scrollIndicatorCoroutine); + + [UIAction("update-scroll-indicator-down")] + protected void ScrollDown() => Utils.ScrollTheScrollIndicator(false, customListTableData.tableView, scrollIndicator, _scrollIndicatorCoroutine); + + [UIAction("on-multi-note-mode-change")] + protected void OnMultiNoteModeChange(string choice) { + MultiplayerCustomNoteModeExtensions.SetMultiNoteSettingFromString(_pluginConfig, choice); + } + + [UIAction("#post-parse")] + protected void PostParse() { + listChoice = MultiplayerCustomNoteModeExtensions.GetMultiNoteSettingString(_pluginConfig); + parserParams.EmitEvent("multi-note-mode-update"); + SetupList(); + Utils.UpdateScrollIndicator(customListTableData.tableView, scrollIndicator); + } + + protected void SetupList() { + customListTableData.data.Clear(); + + foreach (CustomNote note in _noteAssetLoader.CustomNoteObjects) { + Sprite sprite = Sprite.Create(note.Descriptor.Icon, new Rect(0, 0, note.Descriptor.Icon.width, note.Descriptor.Icon.height), new Vector2(0.5f, 0.5f)); + CustomListTableData.CustomCellInfo customCellInfo = new CustomListTableData.CustomCellInfo(note.Descriptor.NoteName, note.Descriptor.AuthorName, sprite); + customListTableData.data.Add(customCellInfo); + } + + customListTableData.tableView.ReloadData(); + int selectedNote = _noteAssetLoader.SelectedNote; + + customListTableData.tableView.ScrollToCellWithIdx(selectedNote, TableViewScroller.ScrollPositionType.Beginning, false); + customListTableData.tableView.SelectCellWithIdx(selectedNote); + } + + [UIAction("note-select")] + protected void Select(TableView _, int row) { + _noteAssetLoader.SelectedNote = row; + _pluginConfig.LastNote = _noteAssetLoader.CustomNoteObjects[row].FileName; + } + + public void ExternalNoteSelect(int row) { + customListTableData.tableView.ScrollToCellWithIdx(row, TableViewScroller.ScrollPositionType.Beginning, false); + customListTableData.tableView.SelectCellWithIdx(row); + Utils.UpdateScrollIndicator(customListTableData.tableView, scrollIndicator); + } + + public void ExternalReload() { + SetupList(); + } + + public void Initialize() { + + } + + public void Dispose() { + + } + + public event PropertyChangedEventHandler PropertyChanged; + + protected void NotifyPropertyChanged([CallerMemberName] string propertyName = "") { + try { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } catch { } + } + } +} diff --git a/CustomNotes/Settings/UI/Views/noteDetails.bsml b/CustomNotes/Settings/UI/Views/noteDetails.bsml index dcd22f6..7a7b5d9 100644 --- a/CustomNotes/Settings/UI/Views/noteDetails.bsml +++ b/CustomNotes/Settings/UI/Views/noteDetails.bsml @@ -1,9 +1,21 @@ - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/CustomNotes/Settings/UI/Views/noteList.bsml b/CustomNotes/Settings/UI/Views/noteList.bsml index 1fad335..aef7b6a 100644 --- a/CustomNotes/Settings/UI/Views/noteList.bsml +++ b/CustomNotes/Settings/UI/Views/noteList.bsml @@ -1,9 +1,16 @@  - - + + - - + + + + + + + + +