feat: 音频参数保存功能
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using AibisDream.Kit;
|
||||
using AibisDream.SaveSystem;
|
||||
using FMOD;
|
||||
using FMOD.Studio;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using NUnit.Framework;
|
||||
|
||||
namespace AibisDream.SystemEditor.Tests
|
||||
{
|
||||
public sealed class AudioGlobalParameterSnapshotTests
|
||||
{
|
||||
[Test]
|
||||
public void SetFromYarn_TracksOnlySuccessfulUniqueNames()
|
||||
{
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND;
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
|
||||
Assert.That(registry.SetFromYarn("reverb", 0.4f), Is.EqualTo(RESULT.OK));
|
||||
Assert.That(registry.SetFromYarn("REVERB", 0.7f), Is.EqualTo(RESULT.OK));
|
||||
Assert.That(
|
||||
registry.SetFromYarn("missing", 1f),
|
||||
Is.EqualTo(RESULT.ERR_EVENT_NOTFOUND));
|
||||
|
||||
Assert.That(registry.TrackedNames, Has.Count.EqualTo(1));
|
||||
Assert.That(registry.TrackedNames.Single(), Is.EqualTo("reverb"));
|
||||
Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.False);
|
||||
Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.False);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void CaptureFinalValues_ReadsFmodFinalValueAndRetainsFailuresForRetry()
|
||||
{
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.Reads["stage"] = new ParameterRead(9f, 2.37f, RESULT.OK);
|
||||
api.Reads["missing"] = new ParameterRead(0f, 0f, RESULT.ERR_EVENT_NOTFOUND);
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
registry.SetFromYarn("stage", 9f);
|
||||
registry.SetFromYarn("missing", 1f);
|
||||
var warnings = new List<string>();
|
||||
|
||||
var snapshot = registry.CaptureFinalValues(warnings.Add);
|
||||
|
||||
Assert.That(snapshot, Has.Count.EqualTo(1));
|
||||
Assert.That(snapshot["stage"], Is.EqualTo(2.37f));
|
||||
Assert.That(snapshot["stage"], Is.Not.EqualTo(9f));
|
||||
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage", "missing" }));
|
||||
Assert.That(warnings, Has.Count.EqualTo(1));
|
||||
StringAssert.Contains("missing", warnings[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void ResetToDefaults_RemovesOnlyParametersThatFmodResetSuccessfully()
|
||||
{
|
||||
var goodId = new PARAMETER_ID { data1 = 1, data2 = 2 };
|
||||
var failedId = new PARAMETER_ID { data1 = 3, data2 = 4 };
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.Descriptions["good"] = new ParameterDescriptionRead(
|
||||
new PARAMETER_DESCRIPTION { id = goodId, defaultvalue = 0.25f },
|
||||
RESULT.OK);
|
||||
api.Descriptions["failed"] = new ParameterDescriptionRead(
|
||||
new PARAMETER_DESCRIPTION { id = failedId, defaultvalue = 0.75f },
|
||||
RESULT.OK);
|
||||
api.SetByIdResults[failedId] = RESULT.ERR_INVALID_PARAM;
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
registry.SetFromYarn("good", 1f);
|
||||
registry.SetFromYarn("failed", 1f);
|
||||
var warnings = new List<string>();
|
||||
|
||||
registry.ResetToDefaults(warnings.Add);
|
||||
|
||||
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "failed" }));
|
||||
Assert.That(api.SetByIdCalls, Has.Count.EqualTo(2));
|
||||
var goodReset = api.SetByIdCalls.Single(call => call.Id.Equals(goodId));
|
||||
Assert.That(goodReset.Value, Is.EqualTo(0.25f));
|
||||
Assert.That(goodReset.IgnoreSeekSpeed, Is.True);
|
||||
Assert.That(warnings, Has.Count.EqualTo(1));
|
||||
StringAssert.Contains("failed", warnings[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void RestoreFinalValues_JumpsImmediatelyThenNextYarnTargetUsesSeekSpeed()
|
||||
{
|
||||
var api = new FakeFmodGlobalParameterApi();
|
||||
api.SetByNameResults["missing"] = RESULT.ERR_EVENT_NOTFOUND;
|
||||
var registry = new YarnGlobalParameterRegistry(api);
|
||||
var warnings = new List<string>();
|
||||
|
||||
registry.RestoreFinalValues(
|
||||
new Dictionary<string, float>
|
||||
{
|
||||
["stage"] = 2.37f,
|
||||
["missing"] = 1f
|
||||
},
|
||||
warnings.Add);
|
||||
registry.SetFromYarn("stage", 3f);
|
||||
|
||||
Assert.That(api.SetByNameCalls, Has.Count.EqualTo(3));
|
||||
Assert.That(api.SetByNameCalls[0].Value, Is.EqualTo(2.37f));
|
||||
Assert.That(api.SetByNameCalls[0].IgnoreSeekSpeed, Is.True);
|
||||
Assert.That(api.SetByNameCalls[1].Name, Is.EqualTo("missing"));
|
||||
Assert.That(api.SetByNameCalls[1].IgnoreSeekSpeed, Is.True);
|
||||
Assert.That(api.SetByNameCalls[2].Value, Is.EqualTo(3f));
|
||||
Assert.That(api.SetByNameCalls[2].IgnoreSeekSpeed, Is.False);
|
||||
Assert.That(registry.TrackedNames, Is.EquivalentTo(new[] { "stage" }));
|
||||
Assert.That(warnings, Has.Count.EqualTo(1));
|
||||
StringAssert.Contains("missing", warnings[0]);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void AudioSnapshotDto_RoundTripsFinalValuesWithoutChangingSchema()
|
||||
{
|
||||
var source = new SaveSnapshot();
|
||||
source.sections[SnapshotProviderIds.Audio] = new AudioSnapshotDto
|
||||
{
|
||||
ambState = "Dream",
|
||||
yarnGlobalParameters = new Dictionary<string, float>
|
||||
{
|
||||
["reverb"] = 0.42f,
|
||||
["hs1LogStage"] = 2.37f
|
||||
}
|
||||
};
|
||||
|
||||
var json = JsonConvert.SerializeObject(source);
|
||||
var restored = JsonConvert.DeserializeObject<SaveSnapshot>(json);
|
||||
var audio = ((JObject)restored.sections[SnapshotProviderIds.Audio])
|
||||
.ToObject<AudioSnapshotDto>();
|
||||
|
||||
Assert.That(restored.schemaVersion, Is.EqualTo(2));
|
||||
Assert.That(SaveSnapshotSchema.CurrentVersion, Is.EqualTo(2));
|
||||
Assert.That(audio.yarnGlobalParameters["reverb"], Is.EqualTo(0.42f));
|
||||
Assert.That(audio.yarnGlobalParameters["hs1LogStage"], Is.EqualTo(2.37f));
|
||||
}
|
||||
|
||||
private readonly struct SetByNameCall
|
||||
{
|
||||
internal SetByNameCall(string name, float value, bool ignoreSeekSpeed)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
IgnoreSeekSpeed = ignoreSeekSpeed;
|
||||
}
|
||||
|
||||
internal string Name { get; }
|
||||
internal float Value { get; }
|
||||
internal bool IgnoreSeekSpeed { get; }
|
||||
}
|
||||
|
||||
private readonly struct SetByIdCall
|
||||
{
|
||||
internal SetByIdCall(PARAMETER_ID id, float value, bool ignoreSeekSpeed)
|
||||
{
|
||||
Id = id;
|
||||
Value = value;
|
||||
IgnoreSeekSpeed = ignoreSeekSpeed;
|
||||
}
|
||||
|
||||
internal PARAMETER_ID Id { get; }
|
||||
internal float Value { get; }
|
||||
internal bool IgnoreSeekSpeed { get; }
|
||||
}
|
||||
|
||||
private readonly struct ParameterRead
|
||||
{
|
||||
internal ParameterRead(float value, float finalValue, RESULT result)
|
||||
{
|
||||
Value = value;
|
||||
FinalValue = finalValue;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
internal float Value { get; }
|
||||
internal float FinalValue { get; }
|
||||
internal RESULT Result { get; }
|
||||
}
|
||||
|
||||
private readonly struct ParameterDescriptionRead
|
||||
{
|
||||
internal ParameterDescriptionRead(
|
||||
PARAMETER_DESCRIPTION description,
|
||||
RESULT result)
|
||||
{
|
||||
Description = description;
|
||||
Result = result;
|
||||
}
|
||||
|
||||
internal PARAMETER_DESCRIPTION Description { get; }
|
||||
internal RESULT Result { get; }
|
||||
}
|
||||
|
||||
private sealed class FakeFmodGlobalParameterApi : IFmodGlobalParameterApi
|
||||
{
|
||||
internal readonly Dictionary<string, RESULT> SetByNameResults =
|
||||
new(System.StringComparer.OrdinalIgnoreCase);
|
||||
internal readonly Dictionary<string, ParameterRead> Reads =
|
||||
new(System.StringComparer.OrdinalIgnoreCase);
|
||||
internal readonly Dictionary<string, ParameterDescriptionRead> Descriptions =
|
||||
new(System.StringComparer.OrdinalIgnoreCase);
|
||||
internal readonly Dictionary<PARAMETER_ID, RESULT> SetByIdResults = new();
|
||||
internal readonly List<SetByNameCall> SetByNameCalls = new();
|
||||
internal readonly List<SetByIdCall> SetByIdCalls = new();
|
||||
|
||||
public RESULT SetParameterByName(
|
||||
string name,
|
||||
float value,
|
||||
bool ignoreSeekSpeed)
|
||||
{
|
||||
SetByNameCalls.Add(new SetByNameCall(name, value, ignoreSeekSpeed));
|
||||
return SetByNameResults.TryGetValue(name, out var result)
|
||||
? result
|
||||
: RESULT.OK;
|
||||
}
|
||||
|
||||
public RESULT GetParameterByName(
|
||||
string name,
|
||||
out float value,
|
||||
out float finalValue)
|
||||
{
|
||||
if (Reads.TryGetValue(name, out var read))
|
||||
{
|
||||
value = read.Value;
|
||||
finalValue = read.FinalValue;
|
||||
return read.Result;
|
||||
}
|
||||
|
||||
value = 0f;
|
||||
finalValue = 0f;
|
||||
return RESULT.OK;
|
||||
}
|
||||
|
||||
public RESULT GetParameterDescriptionByName(
|
||||
string name,
|
||||
out PARAMETER_DESCRIPTION description)
|
||||
{
|
||||
if (Descriptions.TryGetValue(name, out var read))
|
||||
{
|
||||
description = read.Description;
|
||||
return read.Result;
|
||||
}
|
||||
|
||||
description = default;
|
||||
return RESULT.ERR_EVENT_NOTFOUND;
|
||||
}
|
||||
|
||||
public RESULT SetParameterById(
|
||||
PARAMETER_ID id,
|
||||
float value,
|
||||
bool ignoreSeekSpeed)
|
||||
{
|
||||
SetByIdCalls.Add(new SetByIdCall(id, value, ignoreSeekSpeed));
|
||||
return SetByIdResults.TryGetValue(id, out var result)
|
||||
? result
|
||||
: RESULT.OK;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e120d9cfce5a4723a6e640b3cc3ce29b
|
||||
@@ -42,6 +42,7 @@ namespace AibisDream.Kit
|
||||
private EventInstance _voiceInstance;
|
||||
private readonly Dictionary<string, EventInstance> _musicDict = new();
|
||||
private readonly Dictionary<string, EventInstance> _sfxDict = new();
|
||||
private YarnGlobalParameterRegistry _yarnGlobalParameters;
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -52,6 +53,9 @@ namespace AibisDream.Kit
|
||||
private AmbState? _ambStateBeforePauseMenu;
|
||||
|
||||
public AmbState CurrentAmbState => _currentAmbState;
|
||||
private YarnGlobalParameterRegistry YarnGlobalParameters =>
|
||||
_yarnGlobalParameters ??= new YarnGlobalParameterRegistry(
|
||||
new RuntimeFmodGlobalParameterApi());
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -124,7 +128,8 @@ namespace AibisDream.Kit
|
||||
|
||||
var dto = new AudioSnapshotDto
|
||||
{
|
||||
ambState = ambState.ToString()
|
||||
ambState = ambState.ToString(),
|
||||
yarnGlobalParameters = YarnGlobalParameters.CaptureFinalValues(Debug.LogWarning)
|
||||
};
|
||||
|
||||
foreach (var pair in _musicDict)
|
||||
@@ -142,21 +147,34 @@ namespace AibisDream.Kit
|
||||
return dto;
|
||||
}
|
||||
|
||||
public void RestoreSnapshot(AudioSnapshotDto data)
|
||||
public void RestoreSnapshot(AudioSnapshotDto data, Action<string> warn = null)
|
||||
{
|
||||
warn ??= Debug.LogWarning;
|
||||
YarnGlobalParameters.ResetToDefaults(warn);
|
||||
|
||||
foreach (var musicEvent in _musicDict.Values)
|
||||
{
|
||||
musicEvent.stop(STOP_MODE.IMMEDIATE);
|
||||
musicEvent.release();
|
||||
}
|
||||
|
||||
foreach (var sfxEvent in _sfxDict.Values)
|
||||
{
|
||||
sfxEvent.stop(STOP_MODE.IMMEDIATE);
|
||||
sfxEvent.release();
|
||||
}
|
||||
|
||||
_musicDict.Clear();
|
||||
_sfxDict.Clear();
|
||||
|
||||
if (!string.IsNullOrEmpty(data.ambState)
|
||||
&& Enum.TryParse<AmbState>(data.ambState, out var ambState))
|
||||
{
|
||||
ChangeAmb(ambState);
|
||||
}
|
||||
|
||||
YarnGlobalParameters.RestoreFinalValues(data.yarnGlobalParameters, warn);
|
||||
|
||||
if (data.musicPaths != null)
|
||||
{
|
||||
foreach (var music in data.musicPaths)
|
||||
@@ -176,13 +194,6 @@ namespace AibisDream.Kit
|
||||
_sfxDict[sfx.Key] = instance;
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(data.ambState)
|
||||
&& Enum.TryParse<AmbState>(data.ambState, out var ambState))
|
||||
{
|
||||
ChangeAmb(ambState);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public void PauseAll()
|
||||
@@ -250,6 +261,16 @@ namespace AibisDream.Kit
|
||||
RuntimeManager.StudioSystem.setParameterByName(paramName, value);
|
||||
}
|
||||
|
||||
public void SetYarnGlobalParam(string paramName, float value)
|
||||
{
|
||||
var result = YarnGlobalParameters.SetFromYarn(paramName, value);
|
||||
if (result != RESULT.OK)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
$"[AudioManager] Yarn 全局参数 {paramName ?? "(null)"} 设置失败:{result}。");
|
||||
}
|
||||
}
|
||||
|
||||
public void SetMainMenuStage(int stage)
|
||||
{
|
||||
SetGlobalParam(MainMenuStageParamName, stage);
|
||||
@@ -267,6 +288,7 @@ namespace AibisDream.Kit
|
||||
|
||||
private void OnGameStart()
|
||||
{
|
||||
YarnGlobalParameters.ResetToDefaults(Debug.LogWarning);
|
||||
SetMainMenuStage(3);
|
||||
}
|
||||
|
||||
@@ -300,6 +322,7 @@ namespace AibisDream.Kit
|
||||
|
||||
_musicDict.Clear();
|
||||
_sfxDict.Clear();
|
||||
YarnGlobalParameters.ResetToDefaults(Debug.LogWarning);
|
||||
ChangeAmb(menuAmbState);
|
||||
}
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@ namespace AibisDream.Kit
|
||||
[YarnCommand("set_global_param")]
|
||||
public static void SetGlobalParam(string paramName, float value)
|
||||
{
|
||||
AudioManager.Instance.SetGlobalParam(paramName, value);
|
||||
AudioManager.Instance.SetYarnGlobalParam(paramName, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using FMOD;
|
||||
using FMOD.Studio;
|
||||
using FMODUnity;
|
||||
|
||||
namespace AibisDream.Kit
|
||||
{
|
||||
internal interface IFmodGlobalParameterApi
|
||||
{
|
||||
RESULT SetParameterByName(string name, float value, bool ignoreSeekSpeed);
|
||||
RESULT GetParameterByName(string name, out float value, out float finalValue);
|
||||
RESULT GetParameterDescriptionByName(string name, out PARAMETER_DESCRIPTION description);
|
||||
RESULT SetParameterById(PARAMETER_ID id, float value, bool ignoreSeekSpeed);
|
||||
}
|
||||
|
||||
internal sealed class RuntimeFmodGlobalParameterApi : IFmodGlobalParameterApi
|
||||
{
|
||||
public RESULT SetParameterByName(string name, float value, bool ignoreSeekSpeed)
|
||||
{
|
||||
return RuntimeManager.StudioSystem.setParameterByName(name, value, ignoreSeekSpeed);
|
||||
}
|
||||
|
||||
public RESULT GetParameterByName(string name, out float value, out float finalValue)
|
||||
{
|
||||
return RuntimeManager.StudioSystem.getParameterByName(name, out value, out finalValue);
|
||||
}
|
||||
|
||||
public RESULT GetParameterDescriptionByName(
|
||||
string name,
|
||||
out PARAMETER_DESCRIPTION description)
|
||||
{
|
||||
return RuntimeManager.StudioSystem.getParameterDescriptionByName(name, out description);
|
||||
}
|
||||
|
||||
public RESULT SetParameterById(PARAMETER_ID id, float value, bool ignoreSeekSpeed)
|
||||
{
|
||||
return RuntimeManager.StudioSystem.setParameterByID(id, value, ignoreSeekSpeed);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 只记录曾被 Yarn set_global_param 成功修改的参数名。
|
||||
/// 参数值与默认值始终以 FMOD Studio System 为唯一来源。
|
||||
/// </summary>
|
||||
internal sealed class YarnGlobalParameterRegistry
|
||||
{
|
||||
private readonly IFmodGlobalParameterApi _api;
|
||||
private readonly HashSet<string> _trackedNames =
|
||||
new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
internal YarnGlobalParameterRegistry(IFmodGlobalParameterApi api)
|
||||
{
|
||||
_api = api ?? throw new ArgumentNullException(nameof(api));
|
||||
}
|
||||
|
||||
internal IReadOnlyCollection<string> TrackedNames => _trackedNames;
|
||||
|
||||
internal RESULT SetFromYarn(string name, float value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(name))
|
||||
{
|
||||
return RESULT.ERR_INVALID_PARAM;
|
||||
}
|
||||
|
||||
var result = _api.SetParameterByName(name, value, ignoreSeekSpeed: false);
|
||||
if (result == RESULT.OK)
|
||||
{
|
||||
_trackedNames.Add(name);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
internal Dictionary<string, float> CaptureFinalValues(Action<string> warn)
|
||||
{
|
||||
var values = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var name in _trackedNames)
|
||||
{
|
||||
var result = _api.GetParameterByName(name, out _, out var finalValue);
|
||||
if (result == RESULT.OK)
|
||||
{
|
||||
values[name] = finalValue;
|
||||
continue;
|
||||
}
|
||||
|
||||
warn?.Invoke(
|
||||
$"[AudioManager] 捕获 Yarn 全局参数 {name} 失败:{result}。本次存档跳过该参数。");
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
|
||||
internal void ResetToDefaults(Action<string> warn)
|
||||
{
|
||||
var names = new List<string>(_trackedNames);
|
||||
foreach (var name in names)
|
||||
{
|
||||
var descriptionResult =
|
||||
_api.GetParameterDescriptionByName(name, out var description);
|
||||
if (descriptionResult != RESULT.OK)
|
||||
{
|
||||
warn?.Invoke(
|
||||
$"[AudioManager] 查询 Yarn 全局参数 {name} 默认值失败:{descriptionResult}。保留追踪以便稍后重试。");
|
||||
continue;
|
||||
}
|
||||
|
||||
var resetResult = _api.SetParameterById(
|
||||
description.id,
|
||||
description.defaultvalue,
|
||||
ignoreSeekSpeed: true);
|
||||
if (resetResult != RESULT.OK)
|
||||
{
|
||||
warn?.Invoke(
|
||||
$"[AudioManager] 重置 Yarn 全局参数 {name} 失败:{resetResult}。保留追踪以便稍后重试。");
|
||||
continue;
|
||||
}
|
||||
|
||||
_trackedNames.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
internal void RestoreFinalValues(
|
||||
IReadOnlyDictionary<string, float> values,
|
||||
Action<string> warn)
|
||||
{
|
||||
if (values == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var pair in values)
|
||||
{
|
||||
var result = _api.SetParameterByName(
|
||||
pair.Key,
|
||||
pair.Value,
|
||||
ignoreSeekSpeed: true);
|
||||
if (result == RESULT.OK)
|
||||
{
|
||||
_trackedNames.Add(pair.Key);
|
||||
continue;
|
||||
}
|
||||
|
||||
warn?.Invoke(
|
||||
$"[AudioManager] 恢复 Yarn 全局参数 {pair.Key} 失败:{result}。跳过该参数。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fe8472981b434b1d94e494cca8e3801d
|
||||
@@ -37,7 +37,7 @@ namespace AibisDream.SaveSystem
|
||||
return;
|
||||
}
|
||||
|
||||
AudioManager.Instance.RestoreSnapshot(audio);
|
||||
AudioManager.Instance.RestoreSnapshot(audio, context.Warn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,10 @@ namespace AibisDream.SaveSystem
|
||||
public bool isTalking;
|
||||
}
|
||||
|
||||
/// <summary>sections["audio"]:FMOD 音乐/SFX 终态与 AMB 状态(不记播放进度)。</summary>
|
||||
/// <summary>
|
||||
/// sections["audio"]:FMOD 音乐/SFX 终态、AMB 状态与 Yarn 全局参数 finalValue
|
||||
/// (不记 Event 播放进度)。
|
||||
/// </summary>
|
||||
[Serializable]
|
||||
public class AudioSnapshotDto
|
||||
{
|
||||
@@ -134,6 +137,9 @@ namespace AibisDream.SaveSystem
|
||||
|
||||
/// <summary><see cref="AmbState"/> 枚举名。</summary>
|
||||
public string ambState;
|
||||
|
||||
/// <summary>由 Yarn set_global_param 修改过的参数名 → 捕获时的 FMOD finalValue。</summary>
|
||||
public Dictionary<string, float> yarnGlobalParameters = new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user