using System.Collections; using System.Text; using BlockSpace.Voxels; using ExitGames.Client.Photon; using Photon.Pun; using Photon.Realtime; using TMPro; using UnityEngine; namespace BlockSpace.Gamemodes { [DisallowMultipleComponent] [RequireComponent(typeof(PhotonView))] [RequireComponent(typeof(GamemodeZone))] public sealed class BuildBattleController : MonoBehaviourPunCallbacks { private enum Phase : byte { Build = 0, Play = 1 } private const double BuildDurationSeconds = 60.0; private const double PlayDurationSeconds = 30.0; [SerializeField] private string[] themes = new string[5] { "Castle", "Rocket", "Robot", "Dragon", "Treehouse" }; [SerializeField] private TMP_Text themeText; [SerializeField] private TMP_Text timerText; [SerializeField] private VoxelGroupManager groupManager; [SerializeField] private bool enableLogs = true; private GamemodeZone zone; private string currentTheme; private bool localWasInZone; private bool cachedCreatorMode; private bool cachedEditingLocked; private Coroutine masterLoop; private bool bootstrapAttempted; private bool hasPhaseCache; private Phase cachedPhase; private double cachedStartTime; private double nextMissingPropsLogTime; private string PhaseKey => $"BB_{zone.zoneID}_phase"; private string StartKey => $"BB_{zone.zoneID}_start"; private void Awake() { zone = GetComponent(); if (groupManager == null) { groupManager = ((VoxelGroupManager.Instance != null) ? VoxelGroupManager.Instance : Object.FindObjectOfType()); } } private void Start() { if (themeText != null) { themeText.text = "Waiting for next round..."; } Log($"Start (inRoom={PhotonNetwork.InRoom}, isMaster={PhotonNetwork.IsMasterClient}, viewID={((base.photonView != null) ? base.photonView.ViewID : (-1))})"); TryStartMasterLoop(); } private new void OnEnable() { Log($"OnEnable (inRoom={PhotonNetwork.InRoom}, isMaster={PhotonNetwork.IsMasterClient})"); TryStartMasterLoop(); } private void Update() { UpdateTimerText(); UpdateMasterPhaseAdvance(); ApplyLocalToolRules(); } private IEnumerator MasterLoop() { while (true) { yield return WaitForSecondsNetwork(60.0); StartPlayPhase(); yield return WaitForSecondsNetwork(30.0); StartBuildPhase(); } } public override void OnJoinedRoom() { if (themeText != null) { themeText.text = "Waiting for next round..."; } Log($"OnJoinedRoom (isMaster={PhotonNetwork.IsMasterClient}, viewID={((base.photonView != null) ? base.photonView.ViewID : (-1))})"); TryStartMasterLoop(); } public override void OnMasterClientSwitched(Player newMasterClient) { Log($"OnMasterClientSwitched (newMaster={newMasterClient?.ActorNumber}, amMaster={PhotonNetwork.IsMasterClient})"); TryStartMasterLoop(); } private void TryStartMasterLoop() { if (!PhotonNetwork.InRoom) { Log("TryStartMasterLoop: not in room yet"); } else if (!PhotonNetwork.IsMasterClient) { Log("TryStartMasterLoop: not master"); if (masterLoop != null) { StopCoroutine(masterLoop); masterLoop = null; Log("Stopped master loop."); } } else { EnsurePhotonViewReady(); if (masterLoop == null) { Log("Starting master loop; kicking BUILD immediately."); StartBuildPhase(); masterLoop = StartCoroutine(MasterLoop()); } } } private void EnsurePhotonViewReady() { if (!(base.photonView == null) && base.photonView.ViewID == 0 && PhotonNetwork.InRoom && PhotonNetwork.IsMasterClient) { bool flag = PhotonNetwork.AllocateViewID(base.photonView); Log($"AllocateViewID -> {flag} (viewID={base.photonView.ViewID})"); } } private void StartBuildPhase() { if (PhotonNetwork.IsMasterClient && !IsFreshPhase(Phase.Build)) { EnsurePhotonViewReady(); Log($"BUILD start (zoneID={zone.zoneID})"); int num = ((groupManager != null) ? groupManager.RemoveGroupsForZone(zone) : 0); Log($"BUILD cleared {num} group(s) in zone {zone.zoneID}"); string text = PickRandomTheme(); RPC_SetTheme(text); Log("BUILD theme picked: " + text); base.photonView.RPC("RPC_SetTheme", RpcTarget.AllViaServer, text); SetPhaseProperties(Phase.Build, PhotonNetwork.Time); } } private void StartPlayPhase() { if (PhotonNetwork.IsMasterClient && !IsFreshPhase(Phase.Play)) { EnsurePhotonViewReady(); Log($"PLAY start (zoneID={zone.zoneID})"); SetPhaseProperties(Phase.Play, PhotonNetwork.Time); } } private bool IsFreshPhase(Phase phase) { if (hasPhaseCache && cachedPhase == phase) { return PhotonNetwork.Time - cachedStartTime < 1.0; } return false; } private string PickRandomTheme() { if (themes == null || themes.Length == 0) { return "Something"; } int num = Random.Range(0, themes.Length); if (!string.IsNullOrEmpty(themes[num])) { return themes[num]; } return "Something"; } private void SetPhaseProperties(Phase phase, double startTimestamp) { if (PhotonNetwork.InRoom) { if (PhotonNetwork.CurrentRoom == null) { Log("SetPhaseProperties: CurrentRoom is null"); return; } ExitGames.Client.Photon.Hashtable propertiesToSet = new ExitGames.Client.Photon.Hashtable { { PhaseKey, (int)phase }, { StartKey, startTimestamp } }; PhotonNetwork.CurrentRoom.SetCustomProperties(propertiesToSet); Log(string.Format("SetPhaseProperties: {0} start={1:F3}", (phase == Phase.Build) ? "BUILD" : "PLAY", startTimestamp)); LogRoomProps("After SetCustomProperties"); cachedPhase = phase; cachedStartTime = startTimestamp; hasPhaseCache = true; base.photonView.RPC("RPC_SyncPhase", RpcTarget.AllViaServer, (int)phase, startTimestamp); } } private void UpdateTimerText() { if (timerText == null || !PhotonNetwork.InRoom || PhotonNetwork.CurrentRoom == null) { return; } if (!TryGetSyncedPhase(out var phase, out var startTime)) { if (PhotonNetwork.Time >= nextMissingPropsLogTime) { nextMissingPropsLogTime = PhotonNetwork.Time + 2.0; Log("UpdateTimerText: missing phase/start properties"); LogRoomProps("Timer missing props"); } if (!bootstrapAttempted && PhotonNetwork.IsMasterClient) { bootstrapAttempted = true; Log("UpdateTimerText: bootstrapping BUILD phase (master detected, no phase cache present)."); StartBuildPhase(); } if (!TryGetSyncedPhase(out phase, out startTime)) { return; } } bootstrapAttempted = false; double num = ((phase == Phase.Build) ? 60.0 : 30.0); double num2 = PhotonNetwork.Time - startTime; int num3 = Mathf.Max(0, Mathf.CeilToInt((float)(num - num2))); timerText.text = ((phase == Phase.Build) ? "BUILD: " : "PLAY: ") + num3; } private void UpdateMasterPhaseAdvance() { if (!PhotonNetwork.InRoom || !PhotonNetwork.IsMasterClient || !TryGetSyncedPhase(out var phase, out var startTime)) { return; } double num = ((phase == Phase.Build) ? 60.0 : 30.0); double num2 = PhotonNetwork.Time - startTime; if (!(num2 < num)) { Log(string.Format("Master advancing phase from {0} after {1:F2}s", (phase == Phase.Build) ? "BUILD" : "PLAY", num2)); if (phase == Phase.Build) { StartPlayPhase(); } else { StartBuildPhase(); } } } private bool TryGetSyncedPhase(out Phase phase, out double startTime) { if (hasPhaseCache) { phase = cachedPhase; startTime = cachedStartTime; return true; } if (TryGetPhaseAndStart(out phase, out startTime)) { cachedPhase = phase; cachedStartTime = startTime; hasPhaseCache = true; return true; } return false; } private bool TryGetPhaseAndStart(out Phase phase, out double startTime) { phase = Phase.Build; startTime = 0.0; if (PhotonNetwork.CurrentRoom.CustomProperties == null) { return false; } PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue(PhaseKey, out var value); PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue(StartKey, out var value2); if (value == null || value2 == null) { return false; } if (value is int value3) { phase = (Phase)Mathf.Clamp(value3, 0, 1); } else if (value is byte value4) { phase = (Phase)Mathf.Clamp(value4, 0, 1); } if (value2 is double num) { startTime = num; } else if (value2 is float num2) { startTime = num2; } else if (value2 is int num3) { startTime = num3; } return true; } public override void OnRoomPropertiesUpdate(ExitGames.Client.Photon.Hashtable propertiesThatChanged) { if (propertiesThatChanged != null && (propertiesThatChanged.ContainsKey(PhaseKey) || propertiesThatChanged.ContainsKey(StartKey))) { LogRoomProps("OnRoomPropertiesUpdate (phase/start changed)"); } } private void ApplyLocalToolRules() { if (zone == null || groupManager == null) { return; } bool flag = GamemodeZoneManager.Instance != null && GamemodeZoneManager.Instance.CurrentZone == zone; if (flag != localWasInZone) { if (flag) { cachedCreatorMode = groupManager.CreatorMode; cachedEditingLocked = groupManager.EditingLocked; } else { groupManager.SetEditingLocked(cachedEditingLocked); groupManager.SetCreatorMode(cachedCreatorMode); } localWasInZone = flag; } if (flag && PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null && TryGetSyncedPhase(out var phase, out var _)) { if (phase == Phase.Build) { groupManager.SetEditingLocked(locked: false); groupManager.SetCreatorMode(enabled: true); } else { groupManager.SetEditingLocked(locked: true); groupManager.SetCreatorMode(enabled: false); } } } private IEnumerator WaitForSecondsNetwork(double seconds) { double start = PhotonNetwork.Time; while (PhotonNetwork.Time - start < seconds) { yield return null; } } [PunRPC] private void RPC_SetTheme(string theme) { currentTheme = theme; if (themeText != null) { themeText.text = "Theme: " + theme; } Log("RPC_SetTheme received: " + theme); } [PunRPC] private void RPC_SyncPhase(int phase, double startTimestamp) { cachedPhase = (Phase)Mathf.Clamp(phase, 0, 1); cachedStartTime = startTimestamp; hasPhaseCache = true; Log(string.Format("RPC_SyncPhase received: {0} start={1:F3}", (cachedPhase == Phase.Build) ? "BUILD" : "PLAY", startTimestamp)); } private void Log(string message) { if (enableLogs) { Debug.Log($"[BuildBattle zone={zone.zoneID}] {message}", this); } } private void LogRoomProps(string prefix) { if (enableLogs && PhotonNetwork.InRoom && PhotonNetwork.CurrentRoom != null) { object value = null; object value2 = null; if (PhotonNetwork.CurrentRoom.CustomProperties != null) { PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue(PhaseKey, out value); PhotonNetwork.CurrentRoom.CustomProperties.TryGetValue(StartKey, out value2); } string text = ((PhotonNetwork.CurrentRoom.CustomProperties != null) ? FormatHashtable(PhotonNetwork.CurrentRoom.CustomProperties) : "null"); Debug.Log(string.Format("[BuildBattle zone={0}] {1} keys=({2}={3}, {4}={5}) all={6}", zone.zoneID, prefix, PhaseKey, value ?? "null", StartKey, value2 ?? "null", text), this); } } private static string FormatHashtable(IDictionary table) { if (table == null) { return "null"; } StringBuilder stringBuilder = new StringBuilder(256); stringBuilder.Append("{"); bool flag = true; foreach (DictionaryEntry item in table) { if (!flag) { stringBuilder.Append(", "); } flag = false; stringBuilder.Append((item.Key != null) ? item.Key.ToString() : "null"); stringBuilder.Append("="); stringBuilder.Append((item.Value != null) ? item.Value.ToString() : "null"); } stringBuilder.Append("}"); return stringBuilder.ToString(); } } }