diff --git a/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Edits.TextureShader.cs b/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Edits.TextureShader.cs index d4b190b2..97d96da3 100644 --- a/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Edits.TextureShader.cs +++ b/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Edits.TextureShader.cs @@ -62,13 +62,18 @@ internal Action QueueMaterialTextureFromFile( // Track slot replacement without narrowing the caller's apply scope. var location = FindGameObject(objectType, slot); var target = new MaterialEditTarget(go, material, propertyName); - return _textureImports.Enqueue(target, - () => this != null && GetCoordinateIndex(objectType) == coordinate + var cancel = _textureImports.Enqueue(target, + () => this != null && isActiveAndEnabled && GetCoordinateIndex(objectType) == coordinate && FindGameObject(objectType, slot) == location && !CoordinateChanging && File.Exists(filePath), done => { done(MaterialEditResult.FromApplied( TrySetMaterialTextureFromFile(slot, objectType, material, propertyName, filePath, go))); return null; }, completed); + // A file-dialog callback can arrive after OnDisable stopped Update. + // Complete through the queue's normal cancellation path immediately. + if (!isActiveAndEnabled) + cancel?.Invoke(); + return cancel; } private bool TrySetMaterialTextureFromFile( diff --git a/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Events.cs b/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Events.cs index 6274675f..8eb0f237 100644 --- a/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Events.cs +++ b/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.Events.cs @@ -677,36 +677,31 @@ internal void HandleMaterialNameChange(int slot, ObjectType objectType, Renderer } } + private Coroutine _clothesMainTexRefresh; + private Coroutine _bodyMainTexRefresh; + private int _mainTexRefreshGeneration; + /// /// Refresh the clothes MainTex, typically called after editing colors in the character maker /// - private EndOfFrameRefreshGate _clothesMainTexRefreshGate; - private EndOfFrameRefreshGate _bodyMainTexRefreshGate; - public void RefreshClothesMainTex() { - if (!_clothesMainTexRefreshGate.TryRequest()) - { + if (!isActiveAndEnabled || _clothesMainTexRefresh != null) return; - } - try - { - if (StartCoroutine(RefreshClothesMainTexCoroutine()) != null) - return; - } - catch - { - _clothesMainTexRefreshGate.Complete(); - throw; - } - _clothesMainTexRefreshGate.Complete(); + + // A failed start leaves the handle null, so a later request can retry. + _clothesMainTexRefresh = StartCoroutine( + RefreshClothesMainTexCoroutine(_mainTexRefreshGeneration)); } - private IEnumerator RefreshClothesMainTexCoroutine() + private IEnumerator RefreshClothesMainTexCoroutine(int generation) { - yield return new WaitForEndOfFrame(); try { + yield return new WaitForEndOfFrame(); + if (!isActiveAndEnabled || generation != _mainTexRefreshGeneration) + yield break; + for (var i = 0; i < MaterialTexturePropertyList.Count; i++) { var property = MaterialTexturePropertyList[i]; @@ -726,7 +721,8 @@ private IEnumerator RefreshClothesMainTexCoroutine() } finally { - _clothesMainTexRefreshGate.Complete(); + if (generation == _mainTexRefreshGeneration) + _clothesMainTexRefresh = null; } } @@ -778,28 +774,21 @@ private bool SetTextureWithProperty(GameObject go, MaterialTextureProperty textu /// public void RefreshBodyMainTex() { - if (!_bodyMainTexRefreshGate.TryRequest()) - { + if (!isActiveAndEnabled || _bodyMainTexRefresh != null) return; - } - try - { - if (StartCoroutine(RefreshBodyMainTexCoroutine()) != null) - return; - } - catch - { - _bodyMainTexRefreshGate.Complete(); - throw; - } - _bodyMainTexRefreshGate.Complete(); + + _bodyMainTexRefresh = StartCoroutine( + RefreshBodyMainTexCoroutine(_mainTexRefreshGeneration)); } - private IEnumerator RefreshBodyMainTexCoroutine() + private IEnumerator RefreshBodyMainTexCoroutine(int generation) { - yield return new WaitForEndOfFrame(); try { + yield return new WaitForEndOfFrame(); + if (!isActiveAndEnabled || generation != _mainTexRefreshGeneration) + yield break; + for (var i = 0; i < MaterialTexturePropertyList.Count; i++) { var property = MaterialTexturePropertyList[i]; @@ -812,9 +801,26 @@ private IEnumerator RefreshBodyMainTexCoroutine() } finally { - _bodyMainTexRefreshGate.Complete(); + if (generation == _mainTexRefreshGeneration) + _bodyMainTexRefresh = null; } } + + private void CancelMainTexRefreshes() + { + // Invalidate before stopping: an old iterator's finally must not + // clear a refresh requested after this character is enabled again. + _mainTexRefreshGeneration++; + var clothesRefresh = _clothesMainTexRefresh; + var bodyRefresh = _bodyMainTexRefresh; + _clothesMainTexRefresh = null; + _bodyMainTexRefresh = null; + if (clothesRefresh != null) + StopCoroutine(clothesRefresh); + if (bodyRefresh != null) + StopCoroutine(bodyRefresh); + } + /// /// Reapply all edits to the body and face /// @@ -823,22 +829,5 @@ public void RefreshBodyEdits() if (CharacterLoading) return; StartCoroutine(LoadData(false, false, false)); } - private struct EndOfFrameRefreshGate - { - private bool _pending; - - internal bool TryRequest() - { - if (_pending) - return false; - _pending = true; - return true; - } - - internal void Complete() - { - _pending = false; - } - } } } diff --git a/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.cs b/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.cs index fe26e17a..22d506c5 100644 --- a/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.cs +++ b/src/MaterialEditor.Core/Core.MaterialEditor.CharaController.cs @@ -160,6 +160,7 @@ protected override void OnReload(GameMode currentGameMode, bool maintainState) /// protected override void OnDestroy() { + CancelMainTexRefreshes(); _textureImports.Dispose(); ChaControl targetControl = null; GameObject targetRoot = null; @@ -192,6 +193,12 @@ protected override void OnDestroy() } } + private void OnDisable() + { + CancelMainTexRefreshes(); + _textureImports.CancelAll(); + } + private bool CurrentUiTargetBelongsToThisController() { var retainedTarget = MaterialEditorUI.RetainedTargetGameObject;