Skip to content

Commit

Permalink
Voice Acting system improvements (Albeoris#252)
Browse files Browse the repository at this point in the history
* Add a MemoriaLog function to NCalcUtility
* Multiple audio paths for battle voice effects
* Play the voice line of the first choice automatically
* BattleVoiceEffects.txt hot loading
* Initialize more expression units for the battle voice system
* Keeping track of the last player character to act for BattleInOut voice acting
* Fix WhenFlee not working properly
  • Loading branch information
SamsamTS authored Jul 21, 2023
1 parent fd54653 commit da5826c
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 37 deletions.
5 changes: 5 additions & 0 deletions Assembly-CSharp/Global/SFX/SFX.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1643,6 +1643,7 @@ public static void StartCommon(Boolean mode)
SFX.SFX_InitSystem(SFX.BattleCallback);
}
SFX.isSystemRun = true;
SFX.lastPlayedExeId = 0;
}

public static void EndDebugRoom()
Expand Down Expand Up @@ -1942,6 +1943,8 @@ public static void Play(SpecialEffect effNum)
SFX.isRunning = true;
SFX.frameIndex = 0;
SFX.effectPointFrame = -1;
if (SFX.request.exe.btl_id <= 8)
SFX.lastPlayedExeId = SFX.request.exe.btl_id;
PSXTextureMgr.Reset();
SFXMesh.DetectEyeFrameStart = -1;
Int32 num3 = Marshal.SizeOf(SFX.request);
Expand Down Expand Up @@ -2521,4 +2524,6 @@ public struct PSXMAT
public static Int32 preventStepInOut = -1;

public static Int32 effectPointFrame = -1;

public static UInt16 lastPlayedExeId = 0;
}
2 changes: 1 addition & 1 deletion Assembly-CSharp/Global/battle/battle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,7 @@ private static void BattleTrailingLoop(FF9StateGlobal sys, FF9StateBattleSystem
btlsnd.ff9btlsnd_sndeffect_play(2907, 0, SByte.MaxValue, 128);
btlsnd.ff9btlsnd_sndeffect_play(2908, 0, SByte.MaxValue, 128);
btlsys.btl_escape_fade -= 2;
BattleVoice.TriggerOnBattleInOut("Flee");
}
}
break;
Expand Down Expand Up @@ -455,7 +456,6 @@ private static void BattleTrailingLoop(FF9StateGlobal sys, FF9StateBattleSystem
}
UIManager.Battle.SetBattleFollowMessage(BattleMesages.DroppedGil, gilLost);
}
BattleVoice.TriggerOnBattleInOut("Flee");
break;
}
if (btlsys.btl_phase != 5)
Expand Down
150 changes: 120 additions & 30 deletions Assembly-CSharp/Memoria/VoiceActing/BattleVoice.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,24 +17,33 @@ static BattleVoice()
if (!Configuration.VoiceActing.Enabled)
return;

foreach (AssetManager.AssetFolder folder in AssetManager.FolderLowToHigh)
if (folder.TryFindAssetInModOnDisc(BattleVoicePath, out String fullPath))
ParseEffect(File.ReadAllText(fullPath));
FileSystemWatcher watcher = new FileSystemWatcher("./", $"*{BattleVoicePath}");
watcher.IncludeSubdirectories = true;
watcher.NotifyFilter = NotifyFilters.LastWrite;
watcher.Changed += (sender, e) =>
{
if (e.ChangeType != WatcherChangeTypes.Changed) return;
SoundLib.VALog($"File changed: '{e.FullPath}'");
isDirty = true;
};
watcher.EnableRaisingEvents = true;

LoadEffects();
}

public static void InitBattle()
{
{
_currentVoicePlay.Clear();
}

private class BattleSpeaker
{
{
public CharacterId playerId = CharacterId.NONE;
public String enemyModel = null;
public Int32 enemyBattleId = -1;

public Boolean CheckIsCharacter(BTL_DATA btl)
{
{
if (btl.bi.player != 0)
return playerId != CharacterId.NONE && (CharacterId)btl.bi.slot_no == playerId;
if (playerId != CharacterId.NONE)
Expand All @@ -44,7 +53,7 @@ public Boolean CheckIsCharacter(BTL_DATA btl)
}

public BTL_DATA FindBtlUnlimited()
{
{
FF9StateBattleSystem ff9Battle = FF9StateSystem.Battle.FF9Battle;
for (Int32 i = 0; i < 8; i++)
if (CheckIsCharacter(ff9Battle.btl_data[i]))
Expand All @@ -62,15 +71,16 @@ public static Boolean CheckCanSpeak(BTL_DATA btl, Int32 voicePriority, BattleSta
if (_currentVoicePlay.TryGetValue(btl, out playingVoice))
return voicePriority > playingVoice.Key;
return true;
}
}
}

private class GenericVoiceEffect
{
public List<BattleSpeaker> Speakers = new List<BattleSpeaker>();
public String Condition = "";
public String AudioPath = "";
public String[] AudioPaths;
public Int32 Priority = 0;
public Int32 lastPlayed = -1;

public Boolean CheckSpeakerAll(BTL_DATA statusExceptionBtl = null, BattleStatus statusException = 0)
{
Expand Down Expand Up @@ -129,18 +139,62 @@ private class BattleStatusChange : GenericVoiceEffect

private static Dictionary<BTL_DATA, KeyValuePair<Int32, SoundProfile>> _currentVoicePlay = new Dictionary<BTL_DATA, KeyValuePair<Int32, SoundProfile>>();

private static Boolean isDirty = true;
private static FileSystemWatcher _watcher = null;

private static CharacterId VictoryFocusIndex => SFX.lastPlayedExeId != 0 && SFX.lastPlayedExeId < 16 ? btl_scrp.FindBattleUnit(SFX.lastPlayedExeId)?.PlayerIndex ?? CharacterId.NONE : CharacterId.NONE;

private static void LoadEffects()
{
isDirty = false;

InOutEffect.Clear();
ActEffect.Clear();
HittedEffect.Clear();
StatusChangeEffect.Clear();

foreach (AssetManager.AssetFolder folder in AssetManager.FolderLowToHigh)
{
if (folder.TryFindAssetInModOnDisc(BattleVoicePath, out String fullPath))
{
SoundLib.VALog($"Parsing: '{fullPath}'");
ParseEffect(File.ReadAllText(fullPath));
}
}
}

private static void PlayVoiceEffect(GenericVoiceEffect voiceEffect)
{
{
List<BTL_DATA> speakerBtlList = new List<BTL_DATA>();
foreach (BattleSpeaker speaker in voiceEffect.Speakers)
{
{
BTL_DATA btl = speaker.FindBtlUnlimited();
if (btl != null)
speakerBtlList.Add(btl);
}
String soundPath = $"Voices/{Localization.GetSymbol()}/{voiceEffect.AudioPath}";

Int32 audioIndex = 0;
if (voiceEffect.AudioPaths.Length > 1)
{
// Pick a random audio excluding the last one that was played in that situation
if (voiceEffect.lastPlayed >= 0 && voiceEffect.lastPlayed < voiceEffect.AudioPaths.Length)
{
audioIndex = UnityEngine.Random.Range(0, voiceEffect.AudioPaths.Length - 1);
if (audioIndex >= voiceEffect.lastPlayed)
audioIndex++;
}
else
{
audioIndex = UnityEngine.Random.Range(0, voiceEffect.AudioPaths.Length);
}
}
voiceEffect.lastPlayed = audioIndex;

String soundPath = $"Voices/{Localization.GetSymbol()}/{voiceEffect.AudioPaths[audioIndex]}";
Boolean soundExists = AssetManager.HasAssetOnDisc("Sounds/" + soundPath + ".akb", true, true) || AssetManager.HasAssetOnDisc("Sounds/" + soundPath + ".ogg", true, false);
SoundLib.VALog($"battlevoice:{voiceEffect.GetType()} character:{(speakerBtlList.Count > 0 ? new BattleUnit(speakerBtlList[0]).Name : "no speaker")} path:{soundPath}" + (soundExists ? "" : " (not found)"));
if (!soundExists) return;

SoundProfile audioProfile = VoicePlayer.CreateLoadThenPlayVoice(soundPath.GetHashCode(), soundPath,
() =>
{
Expand All @@ -149,7 +203,7 @@ private static void PlayVoiceEffect(GenericVoiceEffect voiceEffect)
});
KeyValuePair<Int32, SoundProfile> playingVoice;
foreach (BTL_DATA btl in speakerBtlList)
{
{
if (_currentVoicePlay.TryGetValue(btl, out playingVoice))
SoundLib.voicePlayer.StopSound(playingVoice.Value);
_currentVoicePlay[btl] = new KeyValuePair<Int32, SoundProfile>(voiceEffect.Priority, audioProfile);
Expand All @@ -161,6 +215,8 @@ public static void TriggerOnBattleInOut(String when)
if (!Configuration.VoiceActing.Enabled)
return;

if (isDirty) LoadEffects();

try
{
List<BattleInOut> retainedEffects = new List<BattleInOut>();
Expand All @@ -174,6 +230,7 @@ public static void TriggerOnBattleInOut(String when)
Expression c = new Expression(effect.Condition);
BattleUnit unit = new BattleUnit(effect.Speakers[0].FindBtlUnlimited());
NCalcUtility.InitializeExpressionUnit(ref c, unit);
c.Parameters["VictoryFocusIndex"] = (UInt32)VictoryFocusIndex;
c.EvaluateFunction += NCalcUtility.commonNCalcFunctions;
c.EvaluateParameter += NCalcUtility.commonNCalcParameters;
if (!NCalcUtility.EvaluateNCalcCondition(c.Evaluate()))
Expand Down Expand Up @@ -201,6 +258,8 @@ public static void TriggerOnBattleAct(BTL_DATA actingChar, String when, CMD_DATA
if (!Configuration.VoiceActing.Enabled)
return;

if (isDirty) LoadEffects();

try
{
List<BattleAct> retainedEffects = new List<BattleAct>();
Expand All @@ -215,6 +274,16 @@ public static void TriggerOnBattleAct(BTL_DATA actingChar, String when, CMD_DATA
BattleUnit unit = new BattleUnit(actingChar);
BattleCommand cmd = new BattleCommand(cmdUsed);
NCalcUtility.InitializeExpressionUnit(ref c, unit);
List<BTL_DATA> t = FF9.btl_util.findAllBtlData(cmdUsed.tar_id);
if (t.Count > 0)
{
BattleUnit target = new BattleUnit(t[0]);
NCalcUtility.InitializeExpressionUnit(ref c, target, "Target");
}
else
{
NCalcUtility.InitializeExpressionNullableUnit(ref c, null, "Target");
}
NCalcUtility.InitializeExpressionCommand(ref c, cmd);
if (calc != null) // Should be the case only when "HitEffect"
NCalcUtility.InitializeExpressionAbilityContext(ref c, calc);
Expand All @@ -235,16 +304,18 @@ public static void TriggerOnBattleAct(BTL_DATA actingChar, String when, CMD_DATA
PlayVoiceEffect(retainedEffects[UnityEngine.Random.Range(0, retainedEffects.Count)]);
}
catch (Exception err)
{
{
Log.Error(err);
}
}
}

public static void TriggerOnHitted(BTL_DATA hittedChar, BattleCalculator calc)
{
if (!Configuration.VoiceActing.Enabled)
return;

if (isDirty) LoadEffects();

try
{
List<BattleHitted> retainedEffects = new List<BattleHitted>();
Expand All @@ -258,6 +329,7 @@ public static void TriggerOnHitted(BTL_DATA hittedChar, BattleCalculator calc)
Expression c = new Expression(effect.Condition);
BattleUnit unit = new BattleUnit(hittedChar);
NCalcUtility.InitializeExpressionUnit(ref c, unit);
NCalcUtility.InitializeExpressionUnit(ref c, calc.Caster, "Caster");
NCalcUtility.InitializeExpressionCommand(ref c, calc.Command);
NCalcUtility.InitializeExpressionAbilityContext(ref c, calc);
c.EvaluateFunction += NCalcUtility.commonNCalcFunctions;
Expand Down Expand Up @@ -287,15 +359,17 @@ public static void TriggerOnStatusChange(BTL_DATA statusedChar, String when, Bat
if (!Configuration.VoiceActing.Enabled)
return;

if (isDirty) LoadEffects();

try
{
List<BattleStatusChange> retainedEffects = new List<BattleStatusChange>();
Int32 retainedPriority = Int32.MinValue;
Boolean discardStatusChecks = String.Compare(when, "Removed") != 0;
foreach (BattleStatusChange effect in StatusChangeEffect)
{
if (String.Compare(effect.When, when) != 0 || (whichStatus & effect.Status) == 0 || effect.Priority < retainedPriority || !effect.CheckSpeakerAll(statusedChar, effect.Status))
continue;
Boolean discardStatusChecks = String.Compare(when, "Removed") != 0;
if (discardStatusChecks && !effect.CheckIsFirstSpeaker(statusedChar, effect.Status))
continue;
if (!discardStatusChecks && !effect.CheckIsFirstSpeaker(statusedChar))
Expand Down Expand Up @@ -351,7 +425,7 @@ private static void ParseEffect(String effectCode)
continue;
String[] charArgToken = charArg.Trim().Split(':');
if (charArgToken.Length == 1)
{
{
// The speaker is a player character identified by its CharacterId
try
{
Expand All @@ -360,12 +434,12 @@ private static void ParseEffect(String effectCode)
newSpeakers.Add(speak);
}
catch (Exception err)
{
{
Log.Warning($"[{nameof(BattleVoice)}] Unrecognized player character {charArgToken[0]}");
}
}
}
else if (charArgToken.Length == 2)
{
{
// The speaker is an enemy identified by its battle ID and/or its model name
BattleSpeaker speak = new BattleSpeaker();
if (charArgToken[0].Length > 0)
Expand All @@ -380,15 +454,31 @@ private static void ParseEffect(String effectCode)
Log.Warning($"[{nameof(BattleVoice)}] Expected a speaker for the effect {bvCode}");
continue;
}
String path = null;
Match pathMatch = new Regex(@"\bVoicePath:(\S+)").Match(bvArgs);
if (pathMatch.Success)
path = pathMatch.Groups[1].Value;
if (String.IsNullOrEmpty(path))
String[] paths = null;
Match pathsMatch = new Regex(@"\bVoicePath:(.*)").Match(bvArgs);
if (pathsMatch.Success)
{
Log.Warning($"[{nameof(BattleVoice)}] Expected a voice audio path for the effect {bvCode}");
String pathsValue = pathsMatch.Groups[1].Value.Trim();
if (pathsValue.IndexOf(',') > 0)
{
Int32 p = pathsValue.LastIndexOf('/');
String folder = (p < 0) ? "" : folder = pathsValue.Substring(0, p + 1);
String[] files = (p < 0) ? pathsValue.Split(',') : pathsValue.Substring(p + 1).Split(',');
paths = new String[files.Length];
for (Int32 j = 0; j < files.Length; j++)
paths[j] = folder + files[j].Trim();
}
else if (!String.IsNullOrEmpty(pathsValue))
{
paths = new String[] { pathsValue };
}
}
if (paths == null)
{
Log.Warning($"[{nameof(BattleVoice)}] Expected voice audio path(s) for the effect {bvCode}");
continue;
}

String condition = null;
Match conditionMatch = new Regex(@"\[Condition\](.*?)\[/Condition\]").Match(bvArgs);
if (conditionMatch.Success)
Expand All @@ -401,7 +491,7 @@ private static void ParseEffect(String effectCode)
{
BattleInOut newEffect = new BattleInOut();
newEffect.Speakers = newSpeakers;
newEffect.AudioPath = path;
newEffect.AudioPaths = paths;
newEffect.Condition = condition;
newEffect.Priority = priority;
Match whenMatch = new Regex(@"\bWhen(\w+)\b").Match(bvArgs);
Expand All @@ -413,7 +503,7 @@ private static void ParseEffect(String effectCode)
{
BattleAct newEffect = new BattleAct();
newEffect.Speakers = newSpeakers;
newEffect.AudioPath = path;
newEffect.AudioPaths = paths;
newEffect.Condition = condition;
newEffect.Priority = priority;
Match whenMatch = new Regex(@"\bWhen(\w+)\b").Match(bvArgs);
Expand All @@ -425,7 +515,7 @@ private static void ParseEffect(String effectCode)
{
BattleHitted newEffect = new BattleHitted();
newEffect.Speakers = newSpeakers;
newEffect.AudioPath = path;
newEffect.AudioPaths = paths;
newEffect.Condition = condition;
newEffect.Priority = priority;
HittedEffect.Add(newEffect);
Expand All @@ -434,7 +524,7 @@ private static void ParseEffect(String effectCode)
{
BattleStatusChange newEffect = new BattleStatusChange();
newEffect.Speakers = newSpeakers;
newEffect.AudioPath = path;
newEffect.AudioPaths = paths;
newEffect.Condition = condition;
newEffect.Priority = priority;
Match whenMatch = new Regex(@"\bWhen(\w+)\b").Match(bvArgs);
Expand Down
Loading

0 comments on commit da5826c

Please sign in to comment.