Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Assets/UnityBox/AvatarSecuritySystem/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## [Unreleased]

### Fixed

- 修正文档与实现不一致的历史遗留问题:修复前 `callbackOrder` 实际按是否存在 NDMF 动态选择(无 NDMF 为 `-1026`;有 NDMF 时晚于 NDMF Optimize `-1025`),而文档所述为固定 `-1026`;本次改为文档所述的固定值(见下方最终方案)
- 修正存在 NDMF 时"混淆所有 Playable Layer"功能实际未生效的问题:`Obfuscator` 内部另一份基于反射的 NDMF 检测从未被赋值,导致 `requireGeneratedPath` 恒为 `true`,NDMF 克隆出的控制器(不在 Generated 目录下)被直接跳过重命名
- 移除运行期反射检测 NDMF 的动态 `callbackOrder`:VRCSDK 回调固定为 `-1024`(仅用于无 NDMF 场景,与 VRCFury 自身的 `RemoveEditorOnlyObjectsHook` 相同 `callbackOrder`,但两者处理内容互不影响,相对顺序不影响结果);存在 NDMF 时改为通过 NDMF 官方 Plugin API 注册到 NDMF 概念中真正的最后一个 BuildPhase(`PlatformFinish`,在 `Optimizing` 之后),不再依赖 VRCSDK 的 `callbackOrder` 数轴
- 同步更新 README 与技术文档中的构建管线执行顺序说明

## [0.7.1] - 2026-07-05

### Changed
Expand Down
2,532 changes: 1,279 additions & 1,253 deletions Assets/UnityBox/AvatarSecuritySystem/Editor/ASS_TechnicalDoc.md

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions Assets/UnityBox/AvatarSecuritySystem/Editor/NDMF/NDMFPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using nadena.dev.ndmf;

[assembly: ExportsPlugin(typeof(UnityBox.AvatarSecuritySystem.Editor.NDMFPlugin))]

namespace UnityBox.AvatarSecuritySystem.Editor
{
/// <summary>
/// 存在 NDMF 时,ASS 不再依赖 VRCSDK 的 callbackOrder 数轴(其与 VRCFury 等其他
/// 直接注册在 VRCSDK 层的收尾钩子之间没有确定的相对顺序保证),而是直接使用 NDMF
/// 的 Plugin API 注册到 NDMF 概念中真正的最后一个 BuildPhase(PlatformFinish,
/// 在 Optimizing 之后、平台专属收尾校验阶段),在 MA / VRCFury 等所有 NDMF pass
/// 完成之后、NDMF 把结果写回真实资产(context.Finish())之前执行。
/// </summary>
public class NDMFPlugin : Plugin<NDMFPlugin>
{
public override string QualifiedName => "com.unitybox.avatarsecuritysystem";
public override string DisplayName => "UnityBox Avatar Security System";

protected override void Configure()
{
InPhase(BuildPhase.PlatformFinish).Run("Generate Avatar Security System", ctx =>
{
if (!Processor.ProcessAvatar(ctx.AvatarRootObject, hasNDMF: true))
{
throw new System.Exception(
"[ASS] Avatar Security System processing failed (see previous log for details)");
}
});
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
{
"name": "UnityBox.AvatarSecuritySystem.NDMF",
"rootNamespace": "UnityBox.AvatarSecuritySystem.Editor",
"references": [
"UnityBox.AvatarSecuritySystem",
"nadena.dev.ndmf"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [
"NDMF_AVAILABLE"
],
"versionDefines": [
{
"name": "nadena.dev.ndmf",
"expression": "",
"define": "NDMF_AVAILABLE"
}
],
"noEngineReferences": false
}
21 changes: 1 addition & 20 deletions Assets/UnityBox/AvatarSecuritySystem/Editor/Obfuscator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ public static class Obfuscator
private static readonly Dictionary<string, Shader> _shaderCache = new Dictionary<string, Shader>();
private static readonly HashSet<string> _generatedContentAssetPaths = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private static readonly HashSet<string> _skipSecondPassLayerNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
private static bool? _hasNDMF;
public static bool IsEnabled => _enabled;
public static bool DecoyLayersEnabled => _decoyLayersEnabled;
public static bool DecoyStatesEnabled => _decoyStatesEnabled;
Expand All @@ -36,7 +35,6 @@ public static void Initialize(string avatarName, bool disableObfuscation,
_shaderCache.Clear();
_generatedContentAssetPaths.Clear();
_skipSecondPassLayerNames.Clear();
_hasNDMF = null;
if (!_enabled)
{
_initialized = true;
Expand All @@ -58,21 +56,6 @@ private static void EnsureInitialized()
}
}

/// <summary>
/// 检测当前项目是否引用了 NDMF 程序集。
/// NDMF 存在时 ASS 利用其已克隆的控制器做 in-place 修改;
/// 不存在时 ASS 完全掌控构建管道,可复制所有控制器后自由操作。
/// </summary>
public static bool HasNDMF
{
get
{
if (_hasNDMF == null)
_hasNDMF = System.Type.GetType(
"nadena.dev.ndmf.BuildContext, nadena.dev.ndmf") != null;
return _hasNDMF.Value;
}
}
#endregion
#region 名称映射 — 内部 Key → 误导性名称 + 哈希后缀
public static string Param(string internalKey, string cleanName = null)
Expand Down Expand Up @@ -476,7 +459,7 @@ private static void PreparePlayableControllerCopies(VRCAvatarDescriptor.CustomAn
}
}

public static void ObfuscatePlayableControllers(VRCAvatarDescriptor descriptor)
public static void ObfuscatePlayableControllers(VRCAvatarDescriptor descriptor, bool hasNDMF)
{
EnsureInitialized();
if (!_enabled || descriptor == null) return;
Expand All @@ -488,8 +471,6 @@ public static void ObfuscatePlayableControllers(VRCAvatarDescriptor descriptor)
int blendTreeCount = 0;
int clipCount = 0;

bool hasNDMF = _hasNDMF ?? false;

foreach (var animLayer in descriptor.baseAnimationLayers.Concat(descriptor.specialAnimationLayers))
{
if (animLayer.isDefault) continue;
Expand Down
53 changes: 28 additions & 25 deletions Assets/UnityBox/AvatarSecuritySystem/Editor/Processor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,38 +10,40 @@ namespace UnityBox.AvatarSecuritySystem.Editor
{
public class Processor : IVRCSDKPreprocessAvatarCallback
{
private static bool? _hasNDMF;

/// <summary>
/// NDMF 存在时在 NDMF Optimize(-1025)之后执行(-1024),
/// 确保 MA / VRCFury / 其他 NDMF pass 全部完成后 ASS 才做最终处理。
/// 无 NDMF 时在默认位置(-1026,VRCFury 之后)执行。
/// 固定在 -1024:与 VRCFury 自身的 VrcfRemoveEditorOnlyObjectsHook 相同的 callbackOrder
/// 不会造成问题(两者处理的内容互不影响,谁先谁后结果一致)。
/// 无需按 NDMF 是否存在动态切换:是否存在 NDMF 在编译期已由 NDMF_AVAILABLE 决定——
/// 存在 NDMF 时,实际处理改由 Editor/NDMF 子程序集中的 NDMFPlugin 在 NDMF
/// BuildPhase.PlatformFinish(NDMF 概念中真正的最后一个阶段)内完成,此回调直接跳过;
/// 不存在 NDMF 时才由本回调处理。
/// </summary>
public int callbackOrder
{
get
{
if (_hasNDMF == null)
_hasNDMF = System.Type.GetType(
"nadena.dev.ndmf.BuildContext, nadena.dev.ndmf") != null;
return _hasNDMF.Value ? -1024 : -1026;
}
}
public int callbackOrder => -1024;

public bool OnPreprocessAvatar(GameObject avatarGameObject)
{
#if NDMF_AVAILABLE
// 存在 NDMF 时,处理已经在 NDMF BuildPhase.PlatformFinish 阶段由 NDMFPlugin 完成,
// 此处跳过,避免重复处理及依赖不确定的 VRCSDK callbackOrder 相对顺序。
return true;
#else
Debug.Log($"[ASS] OnPreprocessAvatar called (callbackOrder={callbackOrder})");
try
{
return ProcessAvatar(avatarGameObject);
return ProcessAvatar(avatarGameObject, hasNDMF: false);
}
catch (System.Exception ex)
{
Debug.LogError($"[ASS] Avatar processing failed: {ex.Message}\n{ex.StackTrace}");
return false;
}
#endif
}
private bool ProcessAvatar(GameObject avatarGameObject)
/// <summary>
/// 核心处理逻辑。由 <see cref="OnPreprocessAvatar"/>(无 NDMF 场景)
/// 或 NDMF 场景下的 NDMFPlugin(BuildPhase.PlatformFinish)调用。
/// </summary>
public static bool ProcessAvatar(GameObject avatarGameObject, bool hasNDMF)
{
var descriptor = avatarGameObject.GetComponent<VRCAvatarDescriptor>();
if (descriptor == null)
Expand Down Expand Up @@ -80,10 +82,11 @@ private bool ProcessAvatar(GameObject avatarGameObject)
return true;
}

// callbackOrder 已决定执行时机:
// NDMF 模式(-1024):在 NDMF Optimize 之后,控制器已是最终版本,无需复制
// 独立模式(-1026):ASS 自己复制所有控制器到 Generated
bool hasNDMF = _hasNDMF ?? false;
// hasNDMF 由调用方决定执行路径:
// NDMF 模式:由 NDMFPlugin 在 BuildPhase.PlatformFinish 内调用,
// 操作的是 NDMF 已克隆的虚拟控制器,无需再复制一份
// 独立模式:由 VRCSDK 回调(callbackOrder=-1024)调用,
// ASS 自己复制所有控制器到 Generated
if (!hasNDMF)
{
if (Obfuscator.IsEnabled && assConfig.enablePlayableLayerObfuscation)
Expand Down Expand Up @@ -145,15 +148,15 @@ private bool ProcessAvatar(GameObject avatarGameObject)
GenerateDecoyLayer(fxController, instructionalClips);
InjectFakeStatesIntoRealLayers(fxController, instructionalClips);
if (assConfig.enablePlayableLayerObfuscation)
Obfuscator.ObfuscatePlayableControllers(descriptor);
Obfuscator.ObfuscatePlayableControllers(descriptor, hasNDMF);
}
RegisterASSParameters(descriptor, assConfig);
Utils.SaveAndRefresh();
Utils.LogOptimizationStats(fxController);
Debug.Log("[ASS] Security system generation complete!");
return true;
}
private void RegisterASSParameters(VRCAvatarDescriptor descriptor, ASSComponent assConfig)
private static void RegisterASSParameters(VRCAvatarDescriptor descriptor, ASSComponent assConfig)
{
var expressionParameters = descriptor.expressionParameters;
if (expressionParameters == null)
Expand Down Expand Up @@ -227,7 +230,7 @@ private void RegisterASSParameters(VRCAvatarDescriptor descriptor, ASSComponent
(needTimeUp ? $", {PARAM_TIME_UP}(local)" : " (no TimeUp)") +
(Obfuscator.IsEnabled ? " + decoys" : ""));
}
private AnimatorController GetFXController(VRCAvatarDescriptor descriptor)
private static AnimatorController GetFXController(VRCAvatarDescriptor descriptor)
{
var fxLayer = descriptor.baseAnimationLayers
.FirstOrDefault(l => l.type == VRCAvatarDescriptor.AnimLayerType.FX);
Expand Down Expand Up @@ -257,7 +260,7 @@ private AnimatorController GetFXController(VRCAvatarDescriptor descriptor)
}
return controller;
}
private void LoadAudioResources(ASSComponent config)
private static void LoadAudioResources(ASSComponent config)
{
config.successSound = Resources.Load<AudioClip>(AUDIO_PASSWORD_SUCCESS);
config.warningBeep = Resources.Load<AudioClip>(AUDIO_COUNTDOWN_WARNING);
Expand Down
Loading