471 lines
12 KiB
C#
471 lines
12 KiB
C#
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<GamemodeZone>();
|
|
if (groupManager == null)
|
|
{
|
|
groupManager = ((VoxelGroupManager.Instance != null) ? VoxelGroupManager.Instance : Object.FindObjectOfType<VoxelGroupManager>());
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
}
|