using UdonSharp; using UnityEngine; using UnityEngine.UI; 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 HarmonyLib; public enum QuestionType { None, MultipleChoice, LightningRound, DumpsterDive, TheChase, FinalRound, Tiebreaker } public enum MusicEventType { None, WhereInTheWorld, RockapellaIdent } public enum SFXEventType { None, Buzzer } [UdonBehaviourSyncMode(BehaviourSyncMode.Manual)] public class GameManager : UdonSharpBehaviour { [UdonSynced] private bool BuzzInAllowed = false; [UdonSynced] private bool[] PlayerBuzzInAllowed; [UdonSynced] private int _BuzzedInPlayer = 0; private DataList _QuestionsList = new DataList(); private int _QuestionIndex = 0; private DataDictionary _CurrentQuestion; [UdonSynced] private int _QuestionStage = 0; [UdonSynced] private int _QuestionCorrectResponse = 0; public PlayerPodium[] PlayerPodiums; [UdonSynced] public VRCUrl QuestionURL; [Header("Multiple Choice Card UI")] [SerializeField] private TextMeshProUGUI _InfoHeader; [SerializeField] private TextMeshProUGUI[] _InfoClues; [SerializeField] private TextMeshProUGUI[] _InfoChoices; [SerializeField] private Image[] _InfoChoiceButtons; [SerializeField] private TextMeshProUGUI _Answer; [Header("Audio")] [SerializeField] private AudioClip BuzzerSound = 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 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) { Debug.LogError("Unable to find any questions. Ensure the root array elements are all objects."); return; } _CurrentQuestion = _QuestionsList[0].DataDictionary; Debug.Log("Found " + _QuestionsList.Count + " questions in this case file."); } else { Debug.LogError("Malformed case file. Ensure the first element is an array of objects."); } } } private void NewMultipleChoiceQuestion() { _InfoHeader.text = _CurrentQuestion["Type"].Number.ToString(); DataList ClueStrings = _CurrentQuestion["Clues"].DataList; for (int i = 0; i < _InfoClues.Length && i < ClueStrings.Count; i++) { _InfoClues[i].text = ClueStrings[i].ToString(); } DataList Choices = _CurrentQuestion["Choices"].DataList; _InfoChoices[0].text = Choices[0].ToString(); _InfoChoices[1].text = Choices[1].ToString(); _InfoChoices[2].text = Choices[2].ToString(); _QuestionCorrectResponse = (int)_CurrentQuestion["Correct Response"].Number; _Answer.text = Choices[_QuestionCorrectResponse].ToString(); DisableInteractive = false; } private void MultipleChoiceRevealChoices() { SendCustomEvent(nameof(MultipleChoiceRevealChoice1)); SendCustomEventDelayedSeconds(nameof(MultipleChoiceRevealChoice2), 1.25f); SendCustomEventDelayedSeconds(nameof(MultipleChoiceRevealChoice3), 2.5f); } public void MultipleChoiceRevealChoice1() { _InfoChoiceButtons[0].color = (_QuestionCorrectResponse == 1) ? Color.green : Color.red; } public void MultipleChoiceRevealChoice2() { _InfoChoiceButtons[1].color = (_QuestionCorrectResponse == 2) ? Color.green : Color.red; } public void MultipleChoiceRevealChoice3() { _InfoChoiceButtons[2].color = (_QuestionCorrectResponse == 3) ? Color.green : Color.red; DataList Choices = _CurrentQuestion["Choices"].DataList; EnableChoiceCards(); 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); } DisableInteractive = false; } private void MultipleChoiceLockAnswers() { for (int i = 0; i < PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)PlayerPodiums[i], NetworkEventTarget.All, "LockInChoice"); } _InfoHeader.text = "LOCKED IN"; for (int i = 0; i < _InfoClues.Length; i++) { _InfoClues[i].text = ""; } for (int i = 0; i < _InfoChoices.Length; i++) { if (i != (_QuestionCorrectResponse - 1)) { _InfoChoices[i].text = ""; } } DisableInteractive = false; } private void MultipleChoiceConfirmAnswers() { _InfoHeader.text = "ANSWER REVEALED"; for (int i = 0; i < PlayerPodiums.Length; i++) { NetworkCalling.SendCustomNetworkEvent((IUdonEventReceiver)PlayerPodiums[i], NetworkEventTarget.All, "VerifyResponse", _QuestionCorrectResponse); } DisableInteractive = false; } private void AdvanceToNextQuestion() { DisableChoiceCards(); _QuestionIndex++; if (_QuestionIndex >= _QuestionsList.Count) { Debug.LogError("No more questions."); return; } _CurrentQuestion = _QuestionsList[_QuestionIndex].DataDictionary; _QuestionCorrectResponse = (int)_CurrentQuestion["Correct Response"].Number; _QuestionStage = 0; ResetInfoCard("Next Question"); DisableInteractive = false; } private void ResetInfoCard(string Header = "") { _InfoHeader.text = Header; for (int i = 0; i < _InfoClues.Length; i++) { _InfoClues[i].text = ""; } _InfoChoiceButtons[0].color = Color.white; _InfoChoiceButtons[1].color = Color.white; _InfoChoiceButtons[2].color = Color.white; _InfoChoices[0].text = ""; _InfoChoices[1].text = ""; _InfoChoices[2].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 EnableBuzzInPeriodForAllPlayers() { BuzzInAllowed = true; ResetBuzzers(); } [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 WaitForBuzzInsWithoutLastPlayer() { BuzzInAllowed = true; NetworkCalling.SendCustomNetworkEvent( (IUdonEventReceiver)PlayerPodiums[_BuzzedInPlayer - 1], NetworkEventTarget.All, "EnableBuzzInEffect", false); _BuzzedInPlayer = -1; RequestSerialization(); } public void EndBuzzInPeriod() { BuzzInAllowed = false; ResetBuzzers(); } public void ResetBuzzers() { for (int i = 0; i < PlayerPodiums.Length; i++) { PlayerBuzzInAllowed[i] = true; } _BuzzedInPlayer = -1; } private void AdvanceQuestion() { // Disable interaction until we unlock it again later. DisableInteractive = true; _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 just two numbers, they're the same". switch (_CurrentQuestion["Type"].Number) { case (int)QuestionType.MultipleChoice: AdvanceMultipleChoiceStage(); break; } RequestSerialization(); } private void AdvanceMultipleChoiceStage() { switch(_QuestionStage) { case 1: NewMultipleChoiceQuestion(); break; case 2: MultipleChoiceRevealChoices(); break; case 3: MultipleChoiceLockAnswers(); break; case 4: MultipleChoiceConfirmAnswers(); break; case 5: AdvanceToNextQuestion(); break; default: return; } } [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) { SFXPlayer.Stop(); switch (SFXEvent) { case SFXEventType.Buzzer: SFXPlayer.clip = BuzzerSound; break; default: SFXPlayer.clip = null; break; } if (SFXPlayer.clip != null) SFXPlayer.Play(); } public override void OnPickupUseDown() { if (DisableInteractive) { return; } AdvanceQuestion(); base.OnPickupUseDown(); } }