using System; using System.Collections; using System.IO; using System.Text; using System.Xml; using TMPro; using UnityEngine; using UnityEngine.Networking; using UnityEngine.UI; public class RenderJobManager : MonoBehaviour { [Header("Serveur")] [SerializeField] private string nextJobUrl = "https://ukitchenit.com/aidesign/render/next-job.php"; [SerializeField] [Min(1f)] private float verificationInterval = 3f; [SerializeField] [Min(0f)] private float delayBeforeOpening = 2f; [Header("Démarrage")] [SerializeField] private bool startAutomatically = false; [Header("Interface")] [SerializeField] private Button searchButton; [SerializeField] private TMP_Text searchButtonText; [SerializeField] private TMP_Text statusText; private bool searchEnabled; private bool requestInProgress; private bool jobFound; /// Job en cours de rendu ("" si aucun). Lu par PDFCreate pour téléverser le PDF /// et appeler CompleteJob une fois le design terminé. public static string CurrentJobId = ""; private Coroutine searchCoroutine; private string downloadFolder; private void Awake() { downloadFolder = Path.Combine( Application.persistentDataPath, "RenderJobs" ); try { Directory.CreateDirectory(downloadFolder); } catch (Exception exception) { Debug.LogException(exception); SetStatus( "Erreur : impossible de créer le dossier RenderJobs." ); } if (searchButton != null) { searchButton.onClick.AddListener( ToggleJobSearch ); } UpdateButtonText(); } private void Start() { if (startAutomatically) { StartJobSearch(); } else { SetStatus("Recherche de jobs arrêtée."); } } private void OnDestroy() { if (searchButton != null) { searchButton.onClick.RemoveListener( ToggleJobSearch ); } } // Source de vérité : la coroutine tourne-t-elle réellement ? On ne se fie PAS à searchEnabled // seul, qui peut rester à true si Unity tue la coroutine (objet désactivé) sans passer par // StopJobSearch — c'est ce désync qui obligeait à cliquer deux fois. private bool IsSearching => searchCoroutine != null; public void ToggleJobSearch() { if (IsSearching) { StopJobSearch(); } else { StartJobSearch(); } } public void StartJobSearch() { if (IsSearching) { return; // déjà réellement en cours } // Repart d'un état propre, quels que soient les drapeaux laissés par le job précédent. jobFound = false; requestInProgress = false; searchEnabled = true; searchCoroutine = StartCoroutine( SearchJobsLoop() ); UpdateButtonText(); SetStatus("Recherche de jobs active."); } public void StopJobSearch() { searchEnabled = false; jobFound = false; if (searchCoroutine != null) { StopCoroutine(searchCoroutine); searchCoroutine = null; } requestInProgress = false; UpdateButtonText(); SetStatus("Recherche de jobs arrêtée."); } // Relance propre après un job (appelée par PDFCreate) : garantit une recherche réellement // active en un seul appel, sans dépendre de l'état antérieur. public void RestartJobSearch() { StopJobSearch(); StartJobSearch(); } private IEnumerator SearchJobsLoop() { while (searchEnabled && !jobFound) { if (!requestInProgress) { yield return CheckNextJob(); } if (searchEnabled && !jobFound) { yield return new WaitForSeconds( verificationInterval ); } } searchCoroutine = null; } private IEnumerator CheckNextJob() { requestInProgress = true; string computerName = UnityWebRequest.EscapeURL( Environment.MachineName ); string requestUrl = nextJobUrl + "?computer=" + computerName + "&nocache=" + DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); Debug.Log( "[RenderJobManager] Vérification : " + requestUrl ); using UnityWebRequest request = UnityWebRequest.Get(requestUrl); request.timeout = 30; request.SetRequestHeader( "Cache-Control", "no-cache" ); yield return request.SendWebRequest(); Debug.Log("==========================="); Debug.Log(request.downloadHandler.text); Debug.Log("==========================="); string responseText = request.downloadHandler != null ? request.downloadHandler.text : string.Empty; Debug.Log( "[RenderJobManager] Réponse PHP : " + responseText ); if ( request.result != UnityWebRequest.Result.Success ) { SetStatus( "Erreur serveur : " + request.error ); Debug.LogError( "[RenderJobManager] HTTP " + request.responseCode + " | " + request.error + " | " + responseText ); requestInProgress = false; yield break; } NextJobResponse response = null; string parsingError = null; try { response = JsonUtility.FromJson( responseText ); } catch (Exception exception) { parsingError = exception.Message; Debug.LogException(exception); } if (response == null) { SetStatus( "Réponse JSON invalide : " + parsingError ); requestInProgress = false; yield break; } if (!response.success) { SetStatus( "Erreur serveur : " + response.error ); requestInProgress = false; yield break; } if ( response.job == null || string.IsNullOrWhiteSpace( response.job.fileName ) || string.IsNullOrWhiteSpace( response.job.downloadUrl ) ) { SetStatus("Aucun job en attente."); requestInProgress = false; yield break; } requestInProgress = false; jobFound = true; searchEnabled = false; CurrentJobId = response.job.jobId; UpdateButtonText(); SetStatus( "JOB TROUVÉ : " + response.job.fileName ); Debug.Log( "[RenderJobManager] Job trouvé : " + response.job.jobId + " | Fichier : " + response.job.fileName + " | URL : " + response.job.downloadUrl ); if (delayBeforeOpening > 0f) { yield return new WaitForSeconds( delayBeforeOpening ); } yield return DownloadAndOpenJob( response.job ); } private IEnumerator DownloadAndOpenJob( RenderJob job ) { string safeFileName = Path.GetFileName(job.fileName); if ( string.IsNullOrWhiteSpace(safeFileName) || !safeFileName.EndsWith( ".udt", StringComparison.OrdinalIgnoreCase ) ) { SetStatus( "Erreur : nom de fichier UDT invalide." ); yield break; } string localFilePath = Path.Combine( downloadFolder, safeFileName ); string temporaryFilePath = localFilePath + ".download"; DeleteFileIfExists( temporaryFilePath ); SetStatus( "Téléchargement : " + safeFileName ); using UnityWebRequest downloadRequest = UnityWebRequest.Get( job.downloadUrl ); downloadRequest.downloadHandler = new DownloadHandlerFile( temporaryFilePath ); downloadRequest.timeout = 120; downloadRequest.SetRequestHeader( "Cache-Control", "no-cache" ); yield return downloadRequest.SendWebRequest(); if ( downloadRequest.result != UnityWebRequest.Result.Success ) { DeleteFileIfExists( temporaryFilePath ); SetStatus( "Erreur de téléchargement : " + downloadRequest.error ); Debug.LogError( "[RenderJobManager] Téléchargement échoué : " + downloadRequest.responseCode + " | " + downloadRequest.error ); yield break; } bool fileSaved = false; string saveError = null; try { if (File.Exists(localFilePath)) { File.Delete(localFilePath); } File.Move( temporaryFilePath, localFilePath ); fileSaved = true; } catch (Exception exception) { saveError = exception.Message; Debug.LogException(exception); } if (!fileSaved) { DeleteFileIfExists( temporaryFilePath ); SetStatus( "Erreur de sauvegarde : " + saveError ); yield break; } Debug.Log( "[RenderJobManager] Fichier téléchargé : " + localFilePath ); SetStatus( "Ouverture : " + safeFileName ); bool fileOpened = false; string openingError = null; try { OpenUdtFile( localFilePath, safeFileName ); fileOpened = true; } catch (Exception exception) { openingError = exception.Message; Debug.LogException(exception); } if (!fileOpened) { SetStatus( "Erreur d’ouverture : " + openingError ); yield break; } SetStatus( "Fichier ouvert : " + safeFileName ); Debug.Log( "[RenderJobManager] Fichier ouvert avec succès : " + safeFileName ); } private void OpenUdtFile( string localFilePath, string fileName ) { Debug.Log( "[RenderJobManager] Chemin local : " + localFilePath ); Debug.Log( "[RenderJobManager] Nom du fichier : " + fileName ); // Même principe que Load_WEB.LoadfromServer : lire le XML puis LoadXML.SetGLOBAL. // Les exceptions remontent au try/catch de DownloadAndOpenJob. XmlDocument xmlDoc = new XmlDocument(); xmlDoc.LoadXml( File.ReadAllText(localFilePath, Encoding.UTF8) ); GameObject loadPnl = Get.o2("HIDER", "LOADpnl"); if (loadPnl != null) { loadPnl.GetComponent().alpha = 0; } WaitCircle.Setting(true, TRANS.This("M_Download")); // Le nom passé à LoadXML doit contenir "ai_" : c'est lui qui pose _G.FlieCategory="ai", // ce qui déclenche ensuite ProcessAi -> PDF -> upload -> email -> CompleteJob. LoadXML.SetGLOBAL( xmlDoc, AiFileName(fileName) ); } // Force le mode AI pour les jobs de rendu. private static string AiFileName(string fileName) { string name = Path.GetFileNameWithoutExtension(fileName); return name.IndexOf( "ai_", StringComparison.OrdinalIgnoreCase ) >= 0 ? name : "ai_" + name; } private void UpdateButtonText() { if (searchButtonText == null) { return; } if (jobFound) { searchButtonText.text = "JOB TROUVÉ"; return; } searchButtonText.text = IsSearching ? "ARRÊTER LA RECHERCHE" : "ACTIVER LA RECHERCHE"; } private void SetStatus( string message ) { if (statusText != null) { statusText.text = message; } Debug.Log( "[RenderJobManager] " + message ); } private static void DeleteFileIfExists( string filePath ) { try { if (File.Exists(filePath)) { File.Delete(filePath); } } catch (Exception exception) { Debug.LogWarning( "[RenderJobManager] Impossible de supprimer " + filePath + " : " + exception.Message ); } } [Serializable] private class NextJobResponse { public bool success; public RenderJob job; public string error; public string message; } [Serializable] private class RenderJob { public string jobId; public string fileName; public string downloadUrl; } }