Skip to content

Commit

Permalink
Merge pull request #333 from GothicVRProject/feature/npc-idle-dialogs
Browse files Browse the repository at this point in the history
Feature/npc idle dialogs
  • Loading branch information
JaXt0r authored Apr 3, 2024
2 parents cf9a861 + 954230c commit a5fa85d
Show file tree
Hide file tree
Showing 11 changed files with 260 additions and 25 deletions.
33 changes: 29 additions & 4 deletions Assets/GothicVR/Scripts/Caches/AssetCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ public static class AssetCache
private static readonly Dictionary<string, IMultiResolutionMesh> MrmCache = new();
private static readonly Dictionary<string, IMorphMesh> MmbCache = new();
private static readonly Dictionary<string, ItemInstance> ItemDataCache = new();
private static readonly Dictionary<string, MusicThemeInstance> MusiThemeCache = new();
private static readonly Dictionary<int, SvmInstance> SvmDataCache = new();
private static readonly Dictionary<string, MusicThemeInstance> MusicThemeCache = new();
private static readonly Dictionary<string, SoundEffectInstance> SfxDataCache = new();
private static readonly Dictionary<string, ParticleEffectInstance> PfxDataCache = new();
private static readonly Dictionary<string, SoundData> SoundCache = new();
Expand Down Expand Up @@ -213,7 +214,7 @@ public static IMorphMesh TryGetMmb(string key)
public static MusicThemeInstance TryGetMusic(string key)
{
var preparedKey = GetPreparedKey(key);
if (MusiThemeCache.TryGetValue(preparedKey, out var data))
if (MusicThemeCache.TryGetValue(preparedKey, out var data))
return data;

MusicThemeInstance newData = null;
Expand All @@ -225,7 +226,7 @@ public static MusicThemeInstance TryGetMusic(string key)
{
// ignored
}
MusiThemeCache[preparedKey] = newData;
MusicThemeCache[preparedKey] = newData;

return newData;
}
Expand Down Expand Up @@ -268,6 +269,29 @@ public static ItemInstance TryGetItemData(string key)

return newData;
}

/// <summary>
/// Hint: Instances only need to be initialized once in ZenKit.
/// </summary>
[CanBeNull]
public static SvmInstance TryGetSvmData(int voiceId)
{
if (SvmDataCache.TryGetValue(voiceId, out var data))
return data;

SvmInstance newData = null;
try
{
newData = GameData.GothicVm.InitInstance<SvmInstance>($"SVM_{voiceId}");
}
catch (Exception)
{
// ignored
}
SvmDataCache[voiceId] = newData;

return newData;
}

/// <summary>
/// Hint: Instances only need to be initialized once in ZenKit and don't need to be deleted during runtime.
Expand Down Expand Up @@ -362,7 +386,8 @@ public static void Dispose()
MrmCache.Clear();
MmbCache.Clear();
ItemDataCache.Clear();
MusiThemeCache.Clear();
SvmDataCache.Clear();
MusicThemeCache.Clear();
SfxDataCache.Clear();
PfxDataCache.Clear();
SoundCache.Clear();
Expand Down
1 change: 1 addition & 0 deletions Assets/GothicVR/Scripts/Caches/LookupCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public static class LookupCache
{
/// <summary>
/// [symbolIndex] = Properties-Component
/// Hint: Includes NPCs and Hero (Easier for lookups like "what is nearest enemy in range".)
/// </summary>
public static readonly Dictionary<int, NpcProperties> NpcCache = new();

Expand Down
149 changes: 149 additions & 0 deletions Assets/GothicVR/Scripts/Extensions/ZenKitExtension.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using UnityEngine;
using ZenKit;
using ZenKit.Daedalus;
using ZenKit.Util;
using TextureFormat = UnityEngine.TextureFormat;

Expand Down Expand Up @@ -110,5 +112,152 @@ public static BoneWeight ToBoneWeight(this List<SoftSkinWeightEntry> weights, Li

return data;
}

/// <summary>
/// Leveraging switch statements with string => member mapping as it's faster than reflection.
/// https://www.jacksondunstan.com/articles/2972
/// </summary>
[SuppressMessage("ReSharper", "StringLiteralTypo")]
public static string GetAudioName(this SvmInstance svm, string svmEntry)
{
var fileName = svmEntry.ToLower() switch
{
"$stopmagic" => svm.StopMagic,
"$isaidstopmagic" => svm.ISaidStopMagic,
"$weapondown" => svm.WeaponDown,
"$isaidweapondown" => svm.ISaidWeaponDown,
"$watchyouraim" => svm.WatchYourAim,
"$watchyouraimangry" => svm.WatchYourAimAngry,
"$whatareyoudoing" => svm.WhatAreYouDoing,
"$letsforgetourlittlefight" => svm.LetsForgetOurLittleFight,
"$strange" => svm.Strange,
"$diemonster" => svm.DieMonster,
"$diemortalenemy" => svm.DieMortalEnemy,
"$nowwait" => svm.NowWait,
"$youstillnothaveenough" => svm.YouStillNotHaveEnough,
"$youaskedforit" => svm.YouAskedForIt,
"$nowwaitintruder" => svm.NowWaitIntruder,
"$iwillteachyourespectforforei" => svm.IWillTeachYouRespectForForeignProperty,
"$dirtythief" => svm.DirtyThief,
"$youattackedmycharge" => svm.YouAttackedMyCharge,
"$youkilledoneofus" => svm.YouKilledOneOfUs,
"$dead" => svm.Dead,
"$aargh_1" => svm.Aargh1,
"$aargh_2" => svm.Aargh2,
"$aargh_3" => svm.Aargh3,
"$berzerk" => svm.Berzerk,
"$youllbesorryforthis" => svm.YoullBeSorryForThis,
"$yesyes" => svm.YesYes,
"$shitwhatamonster" => svm.ShitWhatAMonster,
"$help" => svm.Help,
"$wewillmeetagain" => svm.WeWillMeetAgain,
"$nevertrythatagain" => svm.NeverTryThatAgain,
"$itakeyourweapon" => svm.ITakeYourWeapon,
"$itookyourore" => svm.ITookYourOre,
"$shitnoore" => svm.ShitNoOre,
"$handsoff" => svm.HandsOff,
"$getoutofhere" => svm.GetOutOfHere,
"$youviolatedforbiddenterritor" => svm.YouViolatedForbiddenTerritory,
"$youwannafoolme" => svm.YouWannaFoolMe,
"$whatsthissupposedtobe" => svm.WhatsThisSupposedToBe,
"$whyareyouinhere" => svm.WhyAreYouInHere,
"$whatdidyouinthere" => svm.WhatDidYouInThere,
"$wisemove" => svm.WiseMove,
"$alarm" => svm.Alarm,
"$intruderalert" => svm.IntruderAlert,
"$behindyou" => svm.BehindYou,
"$theresafight" => svm.TheresAFight,
"$heyheyhey" => svm.HeyHeyHey,
"$cheerfight" => svm.CheerFight,
"$cheerfriend" => svm.CheerFriend,
"$ooh" => svm.Ooh,
"$yeahwelldone" => svm.YeahWellDone,
"$runcoward" => svm.RunCoward,
"$hedefeatedhim" => svm.HeDefeatedhim,
"$hedeservedit" => svm.HeDeservEdit,
"$hekilledhim" => svm.HeKilledHim,
"$itwasagoodfight" => svm.ItWasAGoodFight,
"$awake" => svm.Awake,
"$friendlygreetings" => svm.FriendlyGreetings,
"$algreetings" => svm.AlGreetings,
"$magegreetings" => svm.MageGreetings,
"$sectgreetings" => svm.SectGreetings,
"$thereheis" => svm.ThereHeIs,
"$nolearnnopoints" => svm.NoLearnNoPoints,
"$nolearnovermax" => svm.NoLearnOverMax,
"$nolearnyoualreadyknow" => svm.NoLearnYouAlreadyKnow,
"$nolearnyourebetter" => svm.NoLearnYouAlreadyKnow,
"$heyyou" => svm.HeyYou,
"$notnow" => svm.NotNow,
"$whatdoyouwant" => svm.WhatDoYouWant,
"$isaidwhatdoyouwant" => svm.ISaidWhatDoYouWant,
"$makeway" => svm.MakeWay,
"$outofmyway" => svm.OutOfMyWay,
"$youdeaforwhat" => svm.YouDeafOrWhat,
"$lookingfortroubleagain" => svm.LookingForTroubleAgain,
"$lookaway" => svm.LookAway,
"$okaykeepit" => svm.OkayKeepIt,
"$whatsthat" => svm.WhatsThat,
"$thatsmyweapon" => svm.ThatsMyWeapon,
"$giveittome" => svm.GiveItTome,
"$youcankeepthecrap" => svm.YouCanKeepTheCrap,
"$theykilledmyfriend" => svm.TheyKilledMyFriend,
"$youdisturbedmyslumber" => svm.YouDisturbedMySlumber,
"$suckergotsome" => svm.SuckerGotSome,
"$suckerdefeatedebr" => svm.SuckerDefeatedEbr,
"$suckerdefeatedgur" => svm.SuckerDefeatedGur,
"$suckerdefeatedmage" => svm.SuckerDefeatedMage,
"$suckerdefeatednov_guard" => svm.SuckerDefeatedNovGuard,
"$suckerdefeatedvlk_guard" => svm.SuckerDefeatedVlkGuard,
"$youdefeatedmycomrade" => svm.YouDefeatedMyComrade,
"$youdefeatednov_guard" => svm.YouDefeatedNovGuard,
"$youdefeatedvlk_guard" => svm.YouDefeatedVlkGuard,
"$youstolefromme" => svm.YouStoleFromMe,
"$youstolefromus" => svm.YouStoleFromUs,
"$youstolefromebr" => svm.YouStoleFromEbr,
"$youstolefromgur" => svm.YouStoleFromGur,
"$stolefrommage" => svm.StoleFromMage,
"$youkilledmyfriend" => svm.YouKilledmyfriend,
"$youkilledebr" => svm.YouKilledEbr,
"$youkilledgur" => svm.YouKilledGur,
"$youkilledmage" => svm.YouKilledMage,
"$youkilledocfolk" => svm.YouKilledOcFolk,
"$youkilledncfolk" => svm.YouKilledNcFolk,
"$youkilledpsifolk" => svm.YouKilledPsiFolk,
"$getthingsright" => svm.GetThingsRight,
"$youdefeatedmewell" => svm.YouDefeatedMeWell,
"$smalltalk01" => svm.Smalltalk01,
"$smalltalk02" => svm.Smalltalk02,
"$smalltalk03" => svm.Smalltalk03,
"$smalltalk04" => svm.Smalltalk04,
"$smalltalk05" => svm.Smalltalk05,
"$smalltalk06" => svm.Smalltalk06,
"$smalltalk07" => svm.Smalltalk07,
"$smalltalk08" => svm.Smalltalk08,
"$smalltalk09" => svm.Smalltalk09,
"$smalltalk10" => svm.Smalltalk10,
"$smalltalk11" => svm.Smalltalk11,
"$smalltalk12" => svm.Smalltalk12,
"$smalltalk13" => svm.Smalltalk13,
"$smalltalk14" => svm.Smalltalk14,
"$smalltalk15" => svm.Smalltalk15,
"$smalltalk16" => svm.Smalltalk16,
"$smalltalk17" => svm.Smalltalk17,
"$smalltalk18" => svm.Smalltalk18,
"$smalltalk19" => svm.Smalltalk19,
"$smalltalk20" => svm.Smalltalk20,
"$smalltalk21" => svm.Smalltalk21,
"$smalltalk22" => svm.Smalltalk22,
"$smalltalk23" => svm.Smalltalk23,
"$smalltalk24" => svm.Smalltalk24,
"$om" => svm.Om,
_ => null
};

if (fileName == null)
Debug.LogError($"key {svmEntry} not (yet) implemented.");

return fileName;
}
}
}
15 changes: 15 additions & 0 deletions Assets/GothicVR/Scripts/Manager/DialogHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,21 @@ public static void ExtAiOutput(NpcInstance self, NpcInstance target, string outp
new(AnimationAction.Type.AIOutput, int0: speakerId, string0: outputName),
npcProps.go));
}

/// <summary>
/// SVM (Standard Voice Module) dialogs are only for NPCs between each other. Not related to Hero dialogs.
/// </summary>
public static void ExtAiOutputSvm(NpcInstance npc, NpcInstance target, string svmName)
{
var props = GetProperties(npc);

if (target != null)
Debug.LogError($"Ai_OutputSvm() - Handling with target not yet implemented!");

props.AnimationQueue.Enqueue(new OutputSvm(
new(AnimationAction.Type.AITurnToNpc, int0: props.npcInstance.Id, string0: svmName),
props.go));
}

/// <summary>
/// We update the Unity cached/created elements only.
Expand Down
3 changes: 2 additions & 1 deletion Assets/GothicVR/Scripts/Manager/GvrSceneManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -178,9 +178,10 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
break;
case Constants.SceneGeneral:
SceneManager.MoveGameObjectToScene(interactionManager, generalScene);
TeleportPlayerToSpot();

GvrEvents.GeneralSceneLoaded.Invoke();

TeleportPlayerToSpot();
break;
case Constants.SceneMainMenu:
var sphere = scene.GetRootGameObjects().FirstOrDefault(go => go.name == "LoadingSphere");
Expand Down
36 changes: 20 additions & 16 deletions Assets/GothicVR/Scripts/Manager/NpcHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@
using GVR.Vm;
using JetBrains.Annotations;
using UnityEngine;
using UnityEngine.SceneManagement;
using ZenKit.Daedalus;

namespace GVR.Manager
Expand All @@ -22,6 +21,24 @@ public static class NpcHelper
{
private const float fpLookupDistance = 20f; // meter

static NpcHelper()
{
GvrEvents.GeneralSceneLoaded.AddListener(GeneralSceneLoaded);
}

private static void GeneralSceneLoaded()
{
var heroIndex = GameData.GothicVm.GlobalHero!.Index;
var playerGo = GameObject.FindWithTag(Constants.PlayerTag);
var playerProperties = playerGo.GetComponent<NpcProperties>();

// Set data for NPC.
playerProperties.npcInstance = (NpcInstance)GameData.GothicVm.GlobalHero;

// Cache hero for future lookups.
LookupCache.NpcCache[heroIndex] = playerProperties;
}

public static bool ExtIsMobAvailable(NpcInstance npcInstance, string vobName)
{
var npc = GetNpc(npcInstance);
Expand Down Expand Up @@ -161,19 +178,12 @@ public static bool ExtWldDetectNpcEx(NpcInstance npcInstance, int specificNpcInd
var foundNpc = LookupCache.NpcCache.Values
.Where(i => i.go != null) // ignore empty (safe check)
.Where(i => i.npcInstance.Index != npcInstance.Index) // ignore self
.Where(i => detectPlayer || i.npcInstance.Index != GameData.GothicVm.GlobalHero!.Index) // if we don't detect player, then skip it
.Where(i => specificNpcIndex < 0 || specificNpcIndex == i.npcInstance.Index) // Specific NPC is found right now?
.Where(i => aiState < 0 || npc.state == i.state)
.OrderBy(i => Vector3.Distance(i.transform.position, npcPos)) // get nearest
.FirstOrDefault();

// FIXME - Add Hero check
if (detectPlayer)
{
// FIXME - Currently our hero has no aistate. Is it needed or ignore him when checked here?
// var npcDistance = Vector3.Distance(npcPos, foundNpc.transform.position);
// var heroDistance = Vector3.Distance(npcPos, Camera.main!.transform.position);
}

// We need to set it, as there are calls where we immediately need _other_. e.g.:
// if (Wld_DetectNpc(self, ...) && (Npc_GetDistToNpc(self, other)<HAI_DIST_SMALLTALK)
if (foundNpc != null)
Expand Down Expand Up @@ -544,13 +554,7 @@ public static GameObject GetHeroGameObject()
{
var heroIndex = GameData.GothicVm.GlobalHero!.Index;

if (!LookupCache.NpcCache.TryGetValue(heroIndex, out var heroProperties))
{
LookupCache.NpcCache[heroIndex] = GameObject.FindWithTag(Constants.PlayerTag).GetComponent<NpcProperties>();
heroProperties = LookupCache.NpcCache[heroIndex];
}

return heroProperties.go;
return LookupCache.NpcCache[heroIndex].go;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ public class Output: AbstractAnimationAction
{
private float audioPlaySeconds;

private int speakerId => Action.Int0;
protected virtual string outputName => Action.String0;

public Output(AnimationAction action, GameObject npcGo) : base(action, npcGo)
{ }

public override void Start()
{
var soundData = AssetCache.TryGetSound(Action.String0);
var soundData = AssetCache.TryGetSound(outputName);
var audioClip = SoundCreator.ToAudioClip(soundData);
audioPlaySeconds = audioClip.length;

// Hero
if (Action.Int0 == 0)
if (speakerId == 0)
{
// If NPC talked before, we stop it immediately (As some audio samples are shorter than the actual animation)
AnimationCreator.StopAnimation(NpcGo);
Expand All @@ -38,7 +41,6 @@ public override void Start()
var gestureCount = GetDialogGestureCount();
var randomId = Random.Range(1, gestureCount+1);

// FIXME - We need to handle both mds and mdh options! (base vs overlay)
AnimationCreator.PlayAnimation(Props.mdsNames, $"T_DIALOGGESTURE_{randomId:00}", NpcGo);
AnimationCreator.PlayHeadMorphAnimation(Props, HeadMorph.HeadMorphType.Viseme);

Expand Down
Loading

0 comments on commit a5fa85d

Please sign in to comment.