using Firebase.Database; using Firebase.Extensions; using System; using System.Collections.Generic; using TMPro; using UnityEngine; using Newtonsoft.Json; using System.Linq; using System.Collections; using UnityEngine.Localization.Settings; using UnityEngine.Localization.Tables; /// /// This is the game state manager on the PC side /// public class RoomManager : MonoBehaviour { public static RoomManager Instance; [Header("Waiting For Players Page")] public GameObject waitingForPlayersPage; /// /// TextMeshPro that show the value of the current rooom code /// public TextMeshProUGUI roomCodeLabel; public AudioClip playerJoinSFX; public List playerStickers = new List(); [Header("Explanation Page")] public GameObject explanationPage; public TextMeshProUGUI explanationCounter; public float explanationTime; private DateTime endOfExplanationDate = DateTime.MinValue; public AudioClip counterSFX; [Header("Waiting For Proposition Page")] public WaitForPropositionsPage waitForPropositionsPage; [Header("Waiting For Proposition Page")] public VotingPage votingPage; [Header("Scoring Page")] public ScoringPage scoringPage; [Header("Other")] public PromptList promptList; public Room myRoom { get; private set; } = null; public event Action OnRoomCreated; DatabaseReference realtimeDB; #region Unity Methods private void Awake() { Instance = this; FirebaseInitializer.Instance.onFirebaseReady += Initialize; } private void Start() { InitializeFirstPage(); } private void Update() { if (myRoom == null) return; //While Explanation State if (myRoom.currentState == (int)GameState.Explanation && endOfExplanationDate != DateTime.MinValue) { TimeSpan duration = endOfExplanationDate - DateTime.Now; explanationCounter.text = ((int)duration.TotalSeconds).ToString("D1"); if (duration.TotalMilliseconds <= 0) { SendRoomState(GameState.MakeProposition); } } } private bool deleteRoomFilesCompleted = false; private bool deleteRealtimeDBCompleted = false; private void OnApplicationQuit() { StartCoroutine(Coroutine_ClearCurrentRoom(() => { Debug.Log("You can go. Bye bye..."); })); } #endregion #region Public Methods /// /// Automatically called at start of game /// [ContextMenu("Create New Room")] public void CreateNewRoom() { WhichCodesAreAlreadyUsed(codes => { Room newRoom = new Room(GenerateRandomAvailableCode(codes).ToString("D4")); myRoom = newRoom; try { string JSON = JsonConvert.SerializeObject(newRoom); realtimeDB.Child("rooms").Child(newRoom.code).SetRawJsonValueAsync(JSON).ContinueWithOnMainThread(task => { //then subscribe to it realtimeDB.Child("rooms").Child(newRoom.code).ValueChanged += OnRoomUpdate; roomCodeLabel.text = myRoom.code; OnRoomCreated?.Invoke(); Debug.Log($"room {myRoom.code} has been created on the server"); }); } catch (Exception e) { Debug.LogException(e); } }); } public void GeneratePrompts() { System.Random rnd = new(); List prompts = promptList.prompts.OrderBy(x => rnd.Next()).Take(myRoom.players.Count()).ToList(); List players = myRoom.players.Values.ToList().OrderBy(x => rnd.Next()).ToList(); Dictionary questions = new(); for (int i = 0; i < players.Count(); i++) { Dictionary propositions = new(); for (int j = 0; j < 2; j++) { propositions.Add(j, new Proposition(players[i + j < players.Count() ? i + j : 0])); } questions.Add(i, new Question() { index = i, promptId = prompts[i].id, propositions = propositions, creationDate = DateTime.Now.ToOADate(), }); } string JSON = JsonConvert.SerializeObject(questions); realtimeDB.Child("rooms").Child(myRoom.code).Child("questions").SetRawJsonValueAsync(JSON); } /// /// /// /// can be "en" or "fr" public void SetPromptsLanguage(string _language) { if (myRoom == null) return; if(_language == "en" ||_language == "fr") realtimeDB.Child("rooms").Child(myRoom.code).Child("promptsLanguage").SetValueAsync(_language); } #endregion #region Private Methods private void Initialize() { FirebaseInitializer.Instance.onFirebaseReady -= Initialize; realtimeDB = FirebaseDatabase.DefaultInstance.RootReference; Debug.Log("Realtime DB initialized"); ClearOldAndCreateNewRoom(); } private void InitializeFirstPage() { explanationPage.SetActive(false); waitForPropositionsPage.gameObject.SetActive(false); votingPage.gameObject.SetActive(false); scoringPage.gameObject.SetActive(false); waitingForPlayersPage.SetActive(true); ResetAllPlayerLabels(); } private void SendRoomState(GameState _newState) { //Debug.Log($"sending to RTDB that we are now in the {_newState} state", this); realtimeDB.Child("rooms").Child(myRoom.code).Child("currentState").SetValueAsync((int)_newState); } private void ResetAllPlayerLabels() { string label = LanguageManager.Instance.currentStringTable.GetTable().GetEntry("ComputerView/Canvas/WaitingForPlayersPage/WaitingForP").LocalizedValue; for (int i = 0; i < playerStickers.Count; i++) { playerStickers[i].Initialize($"{label}{i + 1}", i); } } private void ClearOldAndCreateNewRoom() { CleanOldRooms(); StartCoroutine(Coroutine_ClearCurrentRoom(() => { CreateNewRoom(); })); } private IEnumerator Coroutine_ClearCurrentRoom(Action callback_OnRoomClear = null) { if (myRoom == null || string.IsNullOrEmpty(myRoom.code)) { yield return null; Debug.Log("There is no last room to clean", this); callback_OnRoomClear?.Invoke(); } else { realtimeDB.Child("rooms").Child(myRoom.code).ValueChanged -= OnRoomUpdate; Debug.Log($"delete files of room {myRoom.code} from storage", this); StorageManager.Instance.DeleteFileOfRoom(myRoom, () => { Debug.Log($"cleaning photo files of room({myRoom.code})finish", this); deleteRoomFilesCompleted = true; }); Debug.Log($"delete realtimeDB room {myRoom.code}"); realtimeDB.Child("rooms").Child(myRoom.code).RemoveValueAsync().ContinueWithOnMainThread(task => { Debug.Log($"room's({myRoom.code}) data has been deleted for database", this); myRoom = null; deleteRealtimeDBCompleted = true; }); while (!deleteRoomFilesCompleted || !deleteRealtimeDBCompleted) { yield return null; // Yield until both tasks are completed } callback_OnRoomClear?.Invoke(); Debug.Log("Everything is clean."); } } /// /// Automatically called when something change in your room /// private void OnRoomUpdate(object sender, ValueChangedEventArgs value) { if (value.DatabaseError != null) { Debug.LogError(value.DatabaseError.Message); return; } if (value.Snapshot.Value == null) { Debug.Log("Trying to update room, but it's empty. Maybe you are exiting the app, so it's ok", this); return; } string JSON = value.Snapshot.GetRawJsonValue(); Debug.Log($"your room has been updated :\n{JSON}"); GameState lastState = (GameState)myRoom.currentState; try { myRoom = JsonConvert.DeserializeObject(JSON); } catch (Exception ex) { Debug.LogException(ex); } //this is done only when entering a new state if (lastState != (GameState)myRoom.currentState) { OnNewGameStateStarted(); } else { //this is done each time something change switch (myRoom.currentState) { case (int)GameState.WaitingForOtherPlayersToJoin: UpdateConnectedPlayerList(myRoom.GetPlayerList()); break; case (int)GameState.MakeProposition: waitForPropositionsPage.OnRoomUpdate(myRoom); break; default: break; } } } /// /// Check all the rooms in the server and give back the number already taken /// private void WhichCodesAreAlreadyUsed(Action> callback_OnCodesChecked) { QueryRoomsInDatabase(onlineRooms => { List alreadyUsedCodes = onlineRooms.Select(room => int.Parse(room.code)).ToList(); Debug.Log($"Codes {string.Join(", ", alreadyUsedCodes)} are already used by other parties", this); callback_OnCodesChecked?.Invoke(alreadyUsedCodes); }); } /// /// Generate a code between 0 and 1000 that is not in the list /// /// the list of code you don"t want to get /// private int GenerateRandomAvailableCode(List _impossibleCodes) { int random = UnityEngine.Random.Range(0, 1000); while (_impossibleCodes.Contains(random)) { Debug.Log($"{random} is already taken, choosing another room code", this); random = UnityEngine.Random.Range(0, 1000); } return random; } /// /// Called when we enter in a new game state /// private void OnNewGameStateStarted() { switch (myRoom.currentState) { case (int)GameState.EnteringName: //if game has been reset by a player on mobile ClearOldAndCreateNewRoom(); InitializeFirstPage(); break; case (int)GameState.WaitingForOtherPlayersToJoin: Debug.Log("New State : WaitingForOtherPlayersToJoin"); break; case (int)GameState.Explanation: Debug.Log("New State : Explanation"); scoringPage.gameObject.SetActive(false); //if we come back from a new game waitingForPlayersPage.SetActive(false); explanationPage.SetActive(true); endOfExplanationDate = DateTime.Now.AddSeconds(explanationTime); AudioManager.Instance.PlaySFX(counterSFX); AudioManager.Instance.StopMusic(); //generate all the questions during the explanation GeneratePrompts(); break; case (int)GameState.MakeProposition: Debug.Log("New State : MakeProposition"); AudioManager.Instance.ChangeMusic(MusicTitle.TakingPicture); waitForPropositionsPage.Initialize(myRoom, () => SendRoomState(GameState.MakeVote)); explanationPage.SetActive(false); break; case (int)GameState.MakeVote: AudioManager.Instance.ChangeMusic(MusicTitle.VotingSession); votingPage.ShowVotingPage( realtimeDB.Child("rooms").Child(myRoom.code) , myRoom.players , myRoom.questions , () => SendRoomState(GameState.Score)); break; case (int)GameState.Score: Debug.Log("It's score time !"); scoringPage.Initialize(myRoom); break; default: break; } } /// /// Update the player labels on the WaitingForPlayer page /// /// private void UpdateConnectedPlayerList(List _players) { AudioManager.Instance.PlaySFX(playerJoinSFX); ResetAllPlayerLabels(); //Debug.Log($"players count = {_players.Count}"); List orderedPlayers = _players.OrderBy(x => x.creationDate).ToList(); for (int i = 0; i < orderedPlayers.Count; i++) { Debug.Log($"player {i} = {orderedPlayers[i].name}"); playerStickers[i].Initialize(orderedPlayers[i].name, i); } } private void QueryRoomsInDatabase(Action> callback) { //Debug.Log("Querying rooms from the database", this); realtimeDB.Child("rooms").GetValueAsync().ContinueWithOnMainThread(task => { if (task.IsFaulted) { Debug.LogException(task.Exception); callback?.Invoke(new List()); return; } DataSnapshot snapshot = task.Result; if (snapshot.Value != null) { string JSON = snapshot.GetRawJsonValue(); Debug.Log($"Found some rooms:\n{JSON}", this); try { Dictionary onlineRooms = JsonConvert.DeserializeObject>(JSON); callback?.Invoke(onlineRooms.Values.ToList()); } catch (Exception e) { Debug.LogException(e); Debug.Log($"Room {JSON} is broken, deleting it", this); snapshot.Reference.RemoveValueAsync().ContinueWithOnMainThread(task => { if (task.IsFaulted) { Debug.LogException(task.Exception); return; } Debug.Log($"Broken room has been deleted. Checking again", this); QueryRoomsInDatabase(callback); }); } } else { Debug.Log("No rooms found in the database", this); callback?.Invoke(new List()); } }); } private void CleanOldRooms() { QueryRoomsInDatabase(onlineRooms => { DateTime oneHourLater = DateTime.Now.AddHours(1); foreach (Room r in onlineRooms) { if (DateTime.FromOADate(r.creationDate) < oneHourLater) { Debug.Log($"Room {r.code} has been created at {DateTime.FromOADate(r.creationDate)} and it's more than one hour old."); DeleteRoom(r.code); } } }); } private void DeleteRoom(string _roomCode) { realtimeDB.Child("rooms").Child(_roomCode).RemoveValueAsync().ContinueWithOnMainThread(task => { if (task.IsFaulted) { Debug.LogException(task.Exception); return; } Debug.Log($"Room {_roomCode} has been deleted"); }); } #endregion }