using UdonSharp; using UnityEngine; using VRC.SDK3.Data; using VRC.SDK3.UdonNetworkCalling; using VRC.Udon.Common.Interfaces; using VRC.SDKBase; using VRC.SDK3.StringLoading; using TMPro; using VRC.SDK3.Components; using VRC.Udon.Common; public enum QuestionType { None, MultipleChoice, LightningRound, DumpsterDive, TheChase, FinalRound, Tiebreaker } public enum MusicEventType { None, WhereInTheWorld, RockapellaIdent } public enum SFXEventType { None, Ding, Buzzer } [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class GameManager : UdonSharpBehaviour { [UdonSynced] private bool _BuzzInAllowed = false; [UdonSynced] private bool[] _PlayerBuzzInAllowed; [UdonSynced] private int _BuzzedInPlayer = 0; [UdonSynced] private bool _GameHasBegun = false; [UdonSynced] private int _QuestionIndex = 0; [UdonSynced] private int _LightningRoundQuestionIndex = 0; [UdonSynced] private int _QuestionStage = 0; [UdonSynced] private int _QuestionCorrectResponse = 0; private DataList _QuestionsList = new DataList(); private DataDictionary _CurrentQuestion; private bool _IsBeingHeld = false; private float _StoredJumpImpulse = 0.0f; [SerializeField] private PlayerPodium[] _PlayerPodiums; [UdonSynced] public VRCUrl QuestionURL; [Header("UI")] [SerializeField] private HostCardBetweenRoundsInterface _BetweenRoundsInterface; [SerializeField] private HostCardMultipleChoiceInterface _MultipleChoiceInterface; [SerializeField] private HostCardLightningRoundInterface _LightningRoundInterface; [SerializeField] private AdminPanelInterface _AdminPanelInterface; [Header("Audio")] [SerializeField] private AudioClip _Ding = null; [SerializeField] private AudioClip _Buzzer = null; [SerializeField] private AudioClip _WhereInTheWorld = null; [SerializeField] private AudioClip _RockapellaIdent = null; [SerializeField] private AudioSource _MusicPlayer = null; [SerializeField] private AudioSource _SFXPlayer = null; void Start() { _PlayerBuzzInAllowed = new bool[_PlayerPodiums.Length]; ResetBuzzers(); // Download our test question. VRCStringDownloader.LoadUrl(QuestionURL, (IUdonEventReceiver)this); } public override void OnPickup() { Networking.SetOwner(Networking.LocalPlayer, gameObject); _StoredJumpImpulse = Networking.LocalPlayer.GetJumpImpulse(); Networking.LocalPlayer.SetJumpImpulse(0.0f); _IsBeingHeld = true; base.OnPickup(); } public override void OnDrop() { Networking.SetOwner(Networking.InstanceOwner, gameObject); Networking.LocalPlayer.SetJumpImpulse(_StoredJumpImpulse); _StoredJumpImpulse = 0.0f; _IsBeingHeld = false; base.OnDrop(); } public override bool OnOwnershipRequest(VRCPlayerApi RequestingPlayer, VRCPlayerApi RequestedOwner) { Debug.LogError(RequestingPlayer.displayName + " is requesting that " + RequestedOwner.displayName + " become the new owner of the host card. No fucking way."); return false; } public override void OnOwnershipTransferred(VRCPlayerApi Player) { Debug.LogError("New owner is " + Player); } public override void InputJump(bool Value, UdonInputEventArgs Args) { if (Value && _IsBeingHeld) { _AdminPanelInterface.gameObject.SetActive(!_AdminPanelInterface.gameObject.activeSelf); } base.InputJump(Value, Args); } public override void OnStringLoadSuccess(IVRCStringDownload DownloadedString) { string JSONString = DownloadedString.Result; if (VRCJson.TryDeserializeFromJson(JSONString, out DataToken JSONResult)) { if (JSONResult.TokenType == TokenType.DataList) { _QuestionsList.Clear(); _QuestionIndex = 0; for (int i = 0; i < JSONResult.DataList.Count; i++) { if (JSONResult.DataList[i].TokenType == TokenType.DataDictionary) { _QuestionsList.Add(JSONResult.DataList[i]); } } if (_QuestionsList.Count == 0) { _BetweenRoundsInterface.HeaderUI.text = "Unable to find any questions. Ensure the root array elements are all objects."; return; } _CurrentQuestion = _QuestionsList[0].DataDictionary; _BetweenRoundsInterface.HeaderUI.text = "Found " + _QuestionsList.Count + " questions in this case file. Press 'Use' button to show scores."; // Reset podiums on a successful case load for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent( (IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "ResetPodium"); } _GameHasBegun = false; EnableInteraction("Start Game"); } else { Debug.LogError("Malformed case file. Ensure the first element is an array of objects."); } } } private void NewMultipleChoiceQuestion() { _MultipleChoiceInterface.HeaderUI.text = QuestionTypeToString((QuestionType)((int)_CurrentQuestion["Type"].Number)); DataList ClueStrings = _CurrentQuestion["Clues"].DataList; for (int i = 0; i < _MultipleChoiceInterface.CluesUI.Length && i < ClueStrings.Count; i++) { _MultipleChoiceInterface.CluesUI[i].text = ClueStrings[i].ToString(); } DataList Choices = _CurrentQuestion["Choices"].DataList; for (int i = 0; i < Choices.Count && i < _MultipleChoiceInterface.ChoiceUI.Length; i++) { _MultipleChoiceInterface.ChoiceUI[i].text = Choices[i].ToString(); } _QuestionCorrectResponse = (int)_CurrentQuestion["Correct Response"].Number; EnableHostCardDisplay(QuestionType.MultipleChoice); EnableInteraction("Reveal Choice 1"); } private void MultipleChoiceRevealChoices() { SendCustomEvent(nameof(MultipleChoiceRevealChoice1)); SendCustomEventDelayedSeconds(nameof(MultipleChoiceRevealChoice2), 1.25f); SendCustomEventDelayedSeconds(nameof(MultipleChoiceRevealChoice3), 2.5f); } public void MultipleChoiceRevealChoice1() { _MultipleChoiceInterface.ChoiceButtonImages[0].color = (_QuestionCorrectResponse == 1) ? Color.green : Color.red; SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFXAtPitch), SFXEventType.Ding, As5); EnableInteraction("Reveal Choice 2"); } public void MultipleChoiceRevealChoice2() { _MultipleChoiceInterface.ChoiceButtonImages[1].color = (_QuestionCorrectResponse == 2) ? Color.green : Color.red; SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFXAtPitch), SFXEventType.Ding, C6); EnableInteraction("Reveal Choice 3"); } public void MultipleChoiceRevealChoice3() { _MultipleChoiceInterface.ChoiceButtonImages[2].color = (_QuestionCorrectResponse == 3) ? Color.green : Color.red; DataList Choices = _CurrentQuestion["Choices"].DataList; EnableChoiceCards(); // Complex per-podium randomiser, to prevent peeking Random.InitState(Networking.GetServerTimeInMilliseconds()); for (int i = 0; i < _PlayerPodiums.Length; i++) { int[] Indices = { 0, 1, 2 }; int[] ChoiceOrder = { -1, -1, -1 }; int Choice1Index = Indices[Random.Range(0, 3)]; ChoiceOrder[0] = Choice1Index; Indices[Choice1Index] = -1; int Choice2Index = -1; while (Choice2Index == -1) { Choice2Index = Indices[Random.Range(0, 3)]; } ChoiceOrder[1] = Choice2Index; Indices[Choice2Index] = -1; int Choice3Index = -1; while (Choice3Index == -1) { Choice3Index = Indices[Random.Range(0, 3)]; } ChoiceOrder[2] = Choice3Index; Indices[Choice3Index] = -1; string[] ChoiceStrings = { Choices[0].ToString(), Choices[1].ToString(), Choices[2].ToString() }; NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "SetCardChoices", ChoiceStrings, ChoiceOrder); } SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFXAtPitch), SFXEventType.Ding, D6); EnableInteraction("Lock Answers"); } private void MultipleChoiceLockAnswers() { for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "LockInChoice"); } _MultipleChoiceInterface.HeaderUI.text = "LOCKED IN"; for (int i = 0; i < _MultipleChoiceInterface.CluesUI.Length; i++) { _MultipleChoiceInterface.CluesUI[i].text = ""; } for (int i = 0; i < _MultipleChoiceInterface.ChoiceUI.Length; i++) { if (i != (_QuestionCorrectResponse - 1)) { _MultipleChoiceInterface.ChoiceUI[i].text = ""; } } EnableInteraction("Reveal Answers And Assign Points"); } private void MultipleChoiceRevealAnswersAndAssignPoints() { _MultipleChoiceInterface.HeaderUI.text = "ANSWER REVEALED"; for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent( (IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "VerifyMultipleChoiceResponse", _QuestionCorrectResponse); } EnableInteraction("Next Question"); } private void BeginLightningRound() { _LightningRoundQuestionIndex = 0; _LightningRoundInterface.HeaderUI.text = QuestionTypeToString((QuestionType)((int)_CurrentQuestion["Type"].Number)) + " | " + _CurrentQuestion["Location"].ToString(); _LightningRoundInterface.QuestionUI.text = ""; for (int i = 0; i < _LightningRoundInterface.ChoiceUI.Length && i < _LightningRoundInterface.ChoiceButtons.Length; i++) { _LightningRoundInterface.ChoiceUI[i].text = ""; _LightningRoundInterface.ChoiceButtonImages[i].color = Color.red; } EnableHostCardDisplay(QuestionType.LightningRound); EnableBuzzers(); EnableInteraction("Next Question"); } private void NewLightningRoundQuestion(int Question) { _LightningRoundQuestionIndex = Question; DataDictionary CurrentQuestion = _CurrentQuestion["Questions"].DataList[Question - 1].DataDictionary; _LightningRoundInterface.QuestionUI.text = CurrentQuestion["Question"].ToString(); DataList Choices = CurrentQuestion["Choices"].DataList; for (int i = 0; i < Choices.Count && i < _LightningRoundInterface.ChoiceUI.Length; i++) { _LightningRoundInterface.ChoiceUI[i].text = Choices[i].ToString(); } _QuestionCorrectResponse = (int)CurrentQuestion["Correct Response"].Number; for (int i = 0; i < _LightningRoundInterface.ChoiceButtons.Length && i < _LightningRoundInterface.ChoiceButtonImages.Length; i++) { _LightningRoundInterface.ChoiceButtonImages[i].color = (_QuestionCorrectResponse == (i + 1)) ? Color.green : Color.red; _LightningRoundInterface.ChoiceButtons[i].interactable = true; } EnableBuzzInPeriodForAllPlayers(); } private void LightningRoundCheckAnswer(int Answer) { if (_QuestionCorrectResponse == Answer) { for (int i = 0; i < _LightningRoundInterface.ChoiceButtons.Length; i++) { _LightningRoundInterface.ChoiceButtons[i].interactable = false; } int PodiumIndex = _BuzzedInPlayer - 1; if (PodiumIndex >= 0 && PodiumIndex < _PlayerPodiums.Length) { NetworkCalling.SendCustomNetworkEvent( (IUdonEventReceiver)_PlayerPodiums[PodiumIndex], NetworkEventTarget.All, "IncreaseScoreBy5"); } EndBuzzInPeriod(); EnableInteraction("Next Question"); } else { WaitForBuzzInsWithoutLastPlayer(); } } public void LightningRoundCheckAnswer1() { LightningRoundCheckAnswer(1); } public void LightningRoundCheckAnswer2() { LightningRoundCheckAnswer(2); } public void LightningRoundCheckAnswer3() { LightningRoundCheckAnswer(3); } public void LightningRoundOtherOptionChosen() { WaitForBuzzInsWithoutLastPlayer(); } private void AdvanceToNextQuestion() { DisableChoiceCards(); DisableBuzzers(); _QuestionIndex++; if (_QuestionIndex >= _QuestionsList.Count) { _BetweenRoundsInterface.HeaderUI.text = "No More Questions"; EnableHostCardDisplay(QuestionType.None); DisableInteraction(); return; } _CurrentQuestion = _QuestionsList[_QuestionIndex].DataDictionary; _QuestionStage = 0; // Again, why does this work, but not just casting to an enum? _BetweenRoundsInterface.HeaderUI.text = "Upcoming Question: " + QuestionTypeToString((QuestionType)((int)_CurrentQuestion["Type"].Number)); EnableHostCardDisplay(QuestionType.None); ResetMultipleChoiceInterface(); EnableInteraction("Show Next Question"); } private void ResetMultipleChoiceInterface() { _MultipleChoiceInterface.HeaderUI.text = ""; for (int i = 0; i < _MultipleChoiceInterface.CluesUI.Length; i++) { _MultipleChoiceInterface.CluesUI[i].text = ""; } for (int i = 0; i < _MultipleChoiceInterface.ChoiceButtonImages.Length; i++) { _MultipleChoiceInterface.ChoiceButtonImages[i].color = Color.white; } for (int i = 0; i < _MultipleChoiceInterface.ChoiceUI.Length; i++) { _MultipleChoiceInterface.ChoiceUI[i].text = ""; } } private void EnableChoiceCards() { for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableChoiceCards", true); } } private void DisableChoiceCards() { for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableChoiceCards", false); } } public void EnableBuzzers() { for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableBuzzer", true); } } public void DisableBuzzers() { for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableBuzzer", false); } } public void EnableBuzzInPeriodForAllPlayers() { _BuzzInAllowed = true; ResetBuzzers(); } public void WaitForBuzzInsWithoutLastPlayer() { _BuzzInAllowed = true; int PodiumIndex = _BuzzedInPlayer - 1; if (PodiumIndex >= 0 && PodiumIndex < _PlayerPodiums.Length) { NetworkCalling.SendCustomNetworkEvent( (IUdonEventReceiver)_PlayerPodiums[_BuzzedInPlayer - 1], NetworkEventTarget.All, "EnableBuzzInEffect", false); _BuzzedInPlayer = -1; RequestSerialization(); } } [NetworkCallable] public void PlayerBuzzedIn(int PlayerNumber) { int PlayerIndex = PlayerNumber - 1; if (!_BuzzInAllowed || !_PlayerBuzzInAllowed[PlayerIndex]) { return; } // Prevent new buzz-ins and store which player is currently buzzed in. _BuzzInAllowed = false; _PlayerBuzzInAllowed[PlayerIndex] = false; _BuzzedInPlayer = PlayerNumber; RequestSerialization(); NetworkCalling.SendCustomNetworkEvent( (IUdonEventReceiver)_PlayerPodiums[PlayerIndex], NetworkEventTarget.All, "EnableBuzzInEffect", true); // Play the buzzer sound globally. SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFX), SFXEventType.Buzzer); } public void EndBuzzInPeriod() { _BuzzInAllowed = false; ResetBuzzers(); } public void ResetBuzzers() { for (int i = 0; i < _PlayerPodiums.Length; i++) { _PlayerBuzzInAllowed[i] = true; NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "EnableBuzzInEffect", false); } _BuzzedInPlayer = -1; } private void AdvanceQuestion() { // Disable interaction until we unlock it again later. DisableInteraction(); _QuestionStage++; // TO-DO: Ask someone at either Microsoft or VRChat why the VM crashes if you cast an int // to an enum in a switch parameter, but not if you cast an enum to an int in a case // statement. I'm starting to wonder if either C# or U# are just fucking terrible // languages. C++ figured this problem out in at least 1985, and it turns out the proper // solution was "it's not a problem, it's two numbers, they're the same fucking thing". switch ((int)_CurrentQuestion["Type"].Number) { case (int)QuestionType.MultipleChoice: AdvanceMultipleChoiceStage(); break; case (int)QuestionType.LightningRound: AdvanceLightningRoundQuestion(); break; } RequestSerialization(); } private void AdvanceMultipleChoiceStage() { switch(_QuestionStage) { case 1: NewMultipleChoiceQuestion(); break; case 2: MultipleChoiceRevealChoice1(); break; case 3: MultipleChoiceRevealChoice2(); break; case 4: MultipleChoiceRevealChoice3(); break; case 5: MultipleChoiceLockAnswers(); break; case 6: MultipleChoiceRevealAnswersAndAssignPoints(); break; case 7: AdvanceToNextQuestion(); break; default: break; } } private void AdvanceLightningRoundQuestion() { switch(_QuestionStage) { case 1: BeginLightningRound(); break; case 2: NewLightningRoundQuestion(1); break; case 3: NewLightningRoundQuestion(2); break; case 4: NewLightningRoundQuestion(3); break; case 5: AdvanceToNextQuestion(); break; default: break; } } [NetworkCallable] public void PlayMusic(MusicEventType MusicEvent) { _MusicPlayer.Stop(); switch (MusicEvent) { case MusicEventType.WhereInTheWorld: _MusicPlayer.clip = _WhereInTheWorld; break; case MusicEventType.RockapellaIdent: _MusicPlayer.clip = _RockapellaIdent; break; default: _MusicPlayer.clip = null; break; } if (_MusicPlayer.clip != null) _MusicPlayer.Play(); } [NetworkCallable] public void PlaySFX(SFXEventType SFXEvent) { PlaySFXInternal(SFXEvent); } [NetworkCallable] public void PlaySFXAtPitch(SFXEventType SFXEvent, float Pitch) { PlaySFXInternal(SFXEvent, Pitch); } private void PlaySFXInternal(SFXEventType SFXEvent, float Pitch = 1.0f) { _SFXPlayer.Stop(); switch (SFXEvent) { case SFXEventType.Ding: _SFXPlayer.clip = _Ding; break; case SFXEventType.Buzzer: _SFXPlayer.clip = _Buzzer; break; default: _SFXPlayer.clip = null; break; } if (_SFXPlayer.clip != null) _SFXPlayer.pitch = Pitch; _SFXPlayer.Play(); } public override void OnPickupUseDown() { if (IsInteractionDisabled()) { return; } if (!_GameHasBegun) { for (int i = 0; i < _PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent( (IUdonEventReceiver)_PlayerPodiums[i], NetworkEventTarget.All, "DisplayScore"); } SendCustomNetworkEvent(NetworkEventTarget.All, nameof(PlaySFXAtPitch), SFXEventType.Ding, D6); _BetweenRoundsInterface.HeaderUI.text = "Upcoming Question: " + QuestionTypeToString((QuestionType)((int)_CurrentQuestion["Type"].Number)); EnableHostCardDisplay(QuestionType.None); _GameHasBegun = true; return; } AdvanceQuestion(); base.OnPickupUseDown(); } private void EnableHostCardDisplay(QuestionType Type) { _BetweenRoundsInterface.gameObject.SetActive(false); _MultipleChoiceInterface.gameObject.SetActive(false); _LightningRoundInterface.gameObject.SetActive(false); switch(Type) { case QuestionType.None: _BetweenRoundsInterface.gameObject.SetActive(true); break; case QuestionType.MultipleChoice: _MultipleChoiceInterface.gameObject.SetActive(true); break; case QuestionType.LightningRound: _LightningRoundInterface.gameObject.SetActive(true); break; case QuestionType.DumpsterDive: case QuestionType.TheChase: case QuestionType.FinalRound: case QuestionType.Tiebreaker: default: break; } } private void EnableInteraction(string InteractionText = "Advance") { DisableInteractive = false; VRCPickup Pickup = GetComponent(); if (Pickup != null) { Pickup.UseText = InteractionText; } } private void DisableInteraction() { DisableInteractive = true; } private bool IsInteractionDisabled() { return DisableInteractive; } private string QuestionTypeToString(QuestionType Type) { int SwitchType = (int)Type; switch(SwitchType) { case (int)QuestionType.None: return "None"; case (int)QuestionType.MultipleChoice: return "Standard Round"; case (int)QuestionType.LightningRound: return "Lightning Round"; case (int)QuestionType.DumpsterDive: return "Dumpster Dive"; case (int)QuestionType.FinalRound: return "Final Round"; case (int)QuestionType.Tiebreaker: return "Tiebreaker"; default: return "[[ERROR]]"; } } // A messy group of variables that are used for pitch correction of sound effects // Since Udon doesn't support accessing public static values in structs, we're doing it here // As5 = 932.33 Hz, PitchShift = TargetNoteFreq / As5Freq private const float As4 = 0.49999463709201677517617152724894f; private const float B4 = 0.52972659895101519848122445915073f; private const float C5 = 0.56122832044447781364967339890382f; private const float Cs5 = 0.5946070597320691171580878015295f; private const float D5 = 0.62995934915748715583538017654693f; private const float Ds5 = 0.66741389851232932545343386998166f; private const float E5 = 0.70709941758819302178413222785923f; private const float F5 = 0.74915534199264209024701554170734f; private const float Fs5 = 0.79369965570130747696631021204938f; private const float G5 = 0.84089324595368592665687042141731f; private const float Gs5 = 0.89089699998927418403355035234305f; private const float A5 = 0.94387180504756899381120418735855f; private const float As5 = 1.0f; private const float B5 = 1.0594639237179968466101058638036f; private const float C6 = 1.1224566408889556272993467978076f; private const float Cs6 = 1.1892033936481717846685186575569f; private const float D6 = 1.2599186983149743116707603530939f; private const float Ds6 = 1.3348385228406251005545246854654f; private const float E6 = 1.4142095609923524932159214012206f; private const float F6 = 1.4982999581693177308463741379125f; private const float Fs6 = 1.5873993114026149539326204240988f; private const float G6 = 1.6817864919073718533137408428346f; private const float Gs6 = 1.7817939999785483680671007046861f; private const float A6 = 1.8877436100951379876224083747171f; private const float As6 = 2.0f; private const float B6 = 2.1183486533738054122467366704922f; private const float C7 = 2.2449132817779112545986935956153f; private const float Cs7 = 2.3784067872963435693370373151137f; }