From 1a8763d679360c716c5ca46638a818f68a18da42 Mon Sep 17 00:00:00 2001 From: bd_ Date: Sat, 27 Apr 2024 13:55:46 -0700 Subject: [PATCH] feat: add DeferPostprocessAsset API Closes: #225 --- CHANGELOG.md | 17 ++++++ Editor/API/BuildContext.cs | 69 +++++++++++++++++++++++-- UnitTests~/DeferPostprocessTest.cs | 50 ++++++++++++++++++ UnitTests~/DeferPostprocessTest.cs.meta | 3 ++ UnitTests~/SerializationSweepTest.cs | 2 +- 5 files changed, 136 insertions(+), 5 deletions(-) create mode 100644 UnitTests~/DeferPostprocessTest.cs create mode 100644 UnitTests~/DeferPostprocessTest.cs.meta diff --git a/CHANGELOG.md b/CHANGELOG.md index 00eebe6e..10906bac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added +- Added `BuildContext.DeferPostprocessAsset` (#229) + +### Changed + +### Deprecated + +### Removed +- `BuildContext.Serialize()`, which was accidentally made public for use in a unit test, is now an `[Obsolete]` no-op. + +### Fixed + +### Security + + ## [1.4.0] - [2024-03-27] ### Added diff --git a/Editor/API/BuildContext.cs b/Editor/API/BuildContext.cs index 093de606..fd997588 100644 --- a/Editor/API/BuildContext.cs +++ b/Editor/API/BuildContext.cs @@ -7,6 +7,7 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using JetBrains.Annotations; using nadena.dev.ndmf.reporting; using nadena.dev.ndmf.runtime; using nadena.dev.ndmf.ui; @@ -52,17 +53,22 @@ public sealed partial class BuildContext internal readonly ObjectRegistry _registry; internal readonly ErrorReport _report; + [PublicAPI] public ObjectRegistry ObjectRegistry => _registry; + [PublicAPI] public ErrorReport ErrorReport => _report; /// /// The root GameObject of the avatar being built. /// + + [PublicAPI] public GameObject AvatarRootObject => _avatarRootObject; /// /// The root Transform of the avatar being built. /// + [PublicAPI] public Transform AvatarRootTransform => _avatarRootTransform; /// @@ -70,19 +76,27 @@ public sealed partial class BuildContext /// referenced by the avatar to this container when the build completes, but in some cases it can be necessary /// to manually save assets (e.g. when using AnimatorController builtins). /// + [PublicAPI] public UnityObject AssetContainer { get; private set; } public bool Successful => !_report.Errors.Any(e => e.TheError.Severity >= ErrorSeverity.Error); - private Dictionary _state = new Dictionary(); - private Dictionary _extensions = new Dictionary(); - private Dictionary _activeExtensions = new Dictionary(); + private readonly Dictionary _state = new Dictionary(); + private readonly Dictionary _extensions = new Dictionary(); + private readonly Dictionary _activeExtensions = new Dictionary(); + + private readonly List<(UnityEngine.Object, PostprocessAssetDelegate)> _assetPostprocessors + = new List<(UnityEngine.Object, PostprocessAssetDelegate)>(); + + public delegate void PostprocessAssetDelegate(UnityEngine.Object asset); + [PublicAPI] public T GetState() where T : new() { return GetState(_ => new T()); } + [PublicAPI] public T GetState(Func init) { if (_state.TryGetValue(typeof(T), out var value)) @@ -95,6 +109,7 @@ public T GetState(Func init) return (T)value; } + [PublicAPI] public T Extension() where T : IExtensionContext { if (!_activeExtensions.TryGetValue(typeof(T), out var value)) @@ -105,6 +120,7 @@ public T Extension() where T : IExtensionContext return (T)value; } + [PublicAPI] public BuildContext(GameObject obj, string assetRootPath, bool isClone = true) { BuildEvent.Dispatch(new BuildEvent.BuildStarted(obj)); @@ -200,13 +216,44 @@ internal static string FilterAvatarName(string avatarName) return avatarName; } + [PublicAPI] public bool IsTemporaryAsset(UnityObject obj) { return !EditorUtility.IsPersistent(obj) || AssetDatabase.GetAssetPath(obj) == AssetDatabase.GetAssetPath(AssetContainer); } + /// + /// Processes a Unity asset after the build completes, but only if it is still referenced. This can be used to + /// e.g. compress textures only if they are not replaced by other NDMF plugins. + /// + /// Postprocess callbacks are run in order of registration. Note that the set of assets to be postprocessed is + /// determined prior to invoking callbacks; as such, if you change an asset to add or remove references to + /// assets during a postprocess callback, this will not impact which subsequent callbacks are invoked. + /// + /// The asset to postprocess + /// The postprocess callback + /// + [PublicAPI] + public void DeferPostprocessAsset( + UnityEngine.Object asset, + PostprocessAssetDelegate postprocess + ) + { + _assetPostprocessors.Add((asset, postprocess)); + } + + /// + /// No-op. Retained for API compatibility. + /// + [Obsolete("Serialize() was not meant to be public in the first place")] + [PublicAPI] public void Serialize() + { + + } + + internal void SerializeInternal() { if (string.IsNullOrEmpty(AssetDatabase.GetAssetPath(AssetContainer))) { @@ -257,6 +304,16 @@ public void Serialize() } } + foreach (var pair in _assetPostprocessors) + { + var (asset, postprocess) = pair; + + if (_savedObjects.Contains(asset)) + { + postprocess(asset); + } + } + // SaveAssets to make sub-assets visible on the Project window AssetDatabase.SaveAssets(); @@ -282,11 +339,13 @@ public void Serialize() } } + [PublicAPI] public void DeactivateExtensionContext() where T : IExtensionContext { DeactivateExtensionContext(typeof(T)); } + [PublicAPI] public void DeactivateExtensionContext(Type t) { using (new ExecutionScope(this)) @@ -371,11 +430,13 @@ internal void RunPass(ConcretePass pass) } } + [PublicAPI] public T ActivateExtensionContext() where T : IExtensionContext { return (T)ActivateExtensionContext(typeof(T)); } + [PublicAPI] public IExtensionContext ActivateExtensionContext(Type ty) { using (new ExecutionScope(this)) @@ -439,7 +500,7 @@ internal void Finish() _activeExtensions.Remove(kvp.Key); } - Serialize(); + SerializeInternal(); sw.Stop(); BuildEvent.Dispatch(new BuildEvent.BuildEnded(sw.ElapsedMilliseconds, true)); diff --git a/UnitTests~/DeferPostprocessTest.cs b/UnitTests~/DeferPostprocessTest.cs new file mode 100644 index 00000000..45e16a69 --- /dev/null +++ b/UnitTests~/DeferPostprocessTest.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using nadena.dev.ndmf; +using NUnit.Framework; +using UnityEngine; + +namespace UnitTests +{ + public class DeferPostprocessTest : TestBase + { + [Test] + public void Test() + { + var root = CreateRoot("root"); + + var meshRenderer = root.AddComponent(); + var material = new Material(Shader.Find("Standard")); + var tex = new Texture2D(1, 1); + var tex2 = new Texture2D(1, 1); + + material.mainTexture = tex; + meshRenderer.material = material; + + var log = new List(); + + BuildContext bc = CreateContext(root); + bc.DeferPostprocessAsset(tex, obj => + { + Assert.AreSame(tex, obj); + log.Add("1"); + }); + bc.DeferPostprocessAsset(tex2, _ => + { + log.Add("2"); + }); + bc.DeferPostprocessAsset(material, obj => + { + Assert.AreSame(material, obj); + log.Add("3"); + }); + bc.DeferPostprocessAsset(tex, _ => + { + log.Add("4"); + }); + + bc.SerializeInternal(); + + Assert.AreEqual(new List { "1", "3", "4" }, log); + } + } +} \ No newline at end of file diff --git a/UnitTests~/DeferPostprocessTest.cs.meta b/UnitTests~/DeferPostprocessTest.cs.meta new file mode 100644 index 00000000..b8441b8a --- /dev/null +++ b/UnitTests~/DeferPostprocessTest.cs.meta @@ -0,0 +1,3 @@ +fileFormatVersion: 2 +guid: 5d9d2b1a60774a1a90aa89e237d3f23d +timeCreated: 1714249808 \ No newline at end of file diff --git a/UnitTests~/SerializationSweepTest.cs b/UnitTests~/SerializationSweepTest.cs index 8b3a1701..7c26558e 100644 --- a/UnitTests~/SerializationSweepTest.cs +++ b/UnitTests~/SerializationSweepTest.cs @@ -39,7 +39,7 @@ public void testSerialization() testScriptable3.ref2 = testScriptable4; BuildContext bc = CreateContext(root); - bc.Serialize(); + bc.SerializeInternal(); var path = AssetDatabase.GetAssetPath(testScriptable1); Assert.IsFalse(string.IsNullOrEmpty(path));