how to add quest actions?

Discuss modding questions and implementation details.
Post Reply
User avatar
John Doom
Posts: 126
Joined: Wed Dec 01, 2021 5:59 pm
Location: Italy
Contact:

how to add quest actions?

Post by John Doom »

Hi, I'm trying to make a new action command. The problem is registering the new action, because every attempt I've made so far happened after OnRegisterCustomActions is called. Help?

Code: Select all

namespace ActionsMod
{
    public class Actions : MonoBehaviour
    {
        private static Mod mod;

        [Invoke(StateManager.StateTypes.Start, 0)]
        public static void Init(InitParams initParams)
        {
            mod = initParams.Mod;

            var go = new GameObject(mod.Title);
            go.AddComponent<Actions>();

            //QuestMachine.OnRegisterCustomActions += OnRegisterCustomActions;

            Debug.LogError(0);
            mod.IsReady = true;
        }

        private void Start()
        {
            Debug.LogError(1);
            QuestMachine.OnRegisterCustomActions += OnRegisterCustomActions;
        }

        private static void OnRegisterCustomActions()
        {
            Debug.LogError(2);
            QuestMachine.Instance.RegisterAction(new CreateFollower(null));
        }
    }
}

User avatar
BadLuckBurt
Posts: 948
Joined: Sun Nov 05, 2017 8:30 pm

Re: how to add quest actions?

Post by BadLuckBurt »

This line probably isn't executed because it has been commented out, see if it works when you remove the //

Code: Select all

//QuestMachine.OnRegisterCustomActions += OnRegisterCustomActions;
DFU on UESP: https://en.uesp.net/w/index.php?title=T ... fall_Unity
DFU Nexus Mods: https://www.nexusmods.com/daggerfallunity
My github repositories with mostly DFU related stuff: https://github.com/BadLuckBurt

.

User avatar
John Doom
Posts: 126
Joined: Wed Dec 01, 2021 5:59 pm
Location: Italy
Contact:

Re: how to add quest actions?

Post by John Doom »

:lol: nope, i already tried using that line and didn't solve the issue, i commented it out after on purpose

UPDATE: I bypassed "RaiseOnRegisterCustomerActionsEvent()" directly using "QuestMachine.Instance.RegisterAction(new CreateFollower(null));". I don't think it's orthodox, but works.

UPDATE2: nah, it just crashes the game :lol:

User avatar
BadLuckBurt
Posts: 948
Joined: Sun Nov 05, 2017 8:30 pm

Re: how to add quest actions?

Post by BadLuckBurt »

John Doom wrote: Thu Dec 02, 2021 5:24 am :lol: nope, i already tried using that line and didn't solve the issue, i commented it out after on purpose

UPDATE: I bypassed "RaiseOnRegisterCustomerActionsEvent()" directly using "QuestMachine.Instance.RegisterAction(new CreateFollower(null));". I don't think it's orthodox, but works.

UPDATE2: nah, it just crashes the game :lol:
Just making sure that wasn't the issue :D

Do you have a repository somewhere so we can look at the entire code? I've been gone for a few months so I'm still rusty but I remembered that I experimented with custom quest actions before when I was working on custom buttons for message boxes.

You can find my experiment here: https://github.com/BadLuckBurt/dfu-custom-quest-prompt.

It won't compile or anything because it's missing the custom MessageBox class but you can take a look at the BLB_Custom_Prompt_Demo class to see how I add the new quest action in the InitAtStartState method. I will be setting up my dev environment for DFU again this weekend so if this doesn't let you figure it out yet, I can take a look at it later.

Once you get this one working, the Followers one you posted about probably needs the same.
DFU on UESP: https://en.uesp.net/w/index.php?title=T ... fall_Unity
DFU Nexus Mods: https://www.nexusmods.com/daggerfallunity
My github repositories with mostly DFU related stuff: https://github.com/BadLuckBurt

.

User avatar
John Doom
Posts: 126
Joined: Wed Dec 01, 2021 5:59 pm
Location: Italy
Contact:

Re: how to add quest actions?

Post by John Doom »

Nyet, it works in the editor but not in-game. btw I'm using 0.13.3 both in unity and in-game. These are the main offenders:

Actions.cs

Code: Select all

using UnityEngine;
using DaggerfallWorkshop.Game;
using DaggerfallWorkshop.Game.Utility.ModSupport;
using DaggerfallWorkshop.Game.Questing;

namespace ActionsMod
{
    public class Actions : MonoBehaviour
    {
        private static Mod mod;

        [Invoke(StateManager.StateTypes.Start)]
        public static void Init(InitParams initParams)
        {
            mod = initParams.Mod;

            var go = new GameObject(mod.Title);
            go.AddComponent<Actions>();

            GameManager.Instance.QuestMachine.RegisterAction(new CreateFollower(null));

            mod.IsReady = true;
        }
    }
}
CreateFollowers.cs

Code: Select all

using UnityEngine;
using System;
using System.Text.RegularExpressions;
using DaggerfallWorkshop.Utility;
using FullSerializer;
using DaggerfallWorkshop.Game.Utility;

namespace DaggerfallWorkshop.Game.Questing
{
    public class CreateFollower : ActionTemplate
    {
        enum Seq {none, creating, done};

        Symbol foeSymbol;

        Seq seq = Seq.none;
        GameObject foeObject;
        EnemyMotor motor;
        EnemySenses senses;
        EnemyAttack attack;

        public override string Pattern
        {
            get
            {
                return @"create follower (?<symbol>[a-zA-Z0-9_.-]+)";
            }
        }

        public CreateFollower(Quest parentQuest)
            : base(parentQuest)
        {
            PlayerEnterExit.OnTransitionExterior += PlayerEnterExit_OnTransition;
            PlayerEnterExit.OnTransitionInterior += PlayerEnterExit_OnTransition;
            PlayerEnterExit.OnTransitionDungeonExterior += PlayerEnterExit_OnTransition;
            PlayerEnterExit.OnTransitionDungeonInterior += PlayerEnterExit_OnTransition;
            StreamingWorld.OnInitWorld += StreamingWorld_OnInitWorld;
        }

        public override void InitialiseOnSet()
        {
        }

        public override IQuestAction CreateNew(string source, Quest parentQuest)
        {
            // Source must match pattern
            Match match = Test(source);
            if (!match.Success)
                return null;

            // Factory new action
            CreateFollower action = new CreateFollower(parentQuest);
            action.foeSymbol = new Symbol(match.Groups["symbol"].Value);

            return action;
        }

        public override void Update(Task caller)
        {
            // Check for a new spawn event - only one spawn event can be running at a time
            if (seq==Seq.none)
            {
                // Get the Foe resource
                Foe foe = ParentQuest.GetFoe(foeSymbol);
                if (foe == null)
                {
                    SetComplete();
                    throw new Exception(string.Format("create follower could not find Foe with symbol name {0}", Symbol.Name));
                }

                // Start deploying GameObjects
                CreatePendingFoeSpawn(foe);
            }

            // Try to deploy
            if (seq==Seq.creating)
            {
                TryPlacement();
                GameManager.Instance.RaiseOnEncounterEvent();
            }

            //update
            if (seq == Seq.done)
            {
                if (senses.Target == GameManager.Instance.PlayerEntityBehaviour)
                {
                    //follow player without attacking
                    //TODO: for ranged attacks
                    attack.ResetMeleeTimer();
                }

                Vector3 distance =
                    foeObject.transform.position -
                    GameManager.Instance.PlayerObject.transform.position;
                if (distance.magnitude > attack.MeleeDistance*5.0f)
                    Reset();
            }
        }

        #region Private Methods

        void CreatePendingFoeSpawn(Foe foe)
        {
            if (foeObject == null)
            {
                // Get foe GameObject
                MobileReactions reaction = MobileReactions.Hostile;
                MobileGender gender = MobileGender.Male;

                foeObject = GameObjectHelper.CreateEnemy("lolz",
                    foe.FoeType,
                    Vector3.zero,
                    mobileGender: gender,
                    mobileReaction: reaction);
                if (foeObject == null)
                {
                    SetComplete();
                    throw new Exception(string.Format("create foe attempted to create {0}x{1} GameObjects and failed.", foe.SpawnCount, Symbol.Name));
                }
            }

            // Initiate deployment process
            // Usually the foe will spawn immediately but can take longer depending on available placement space
            // This process ensures these foes have all been deployed before starting next cycle
            seq = Seq.creating;
        }

        void TryPlacement()
        {
            PlayerEnterExit playerEnterExit = GameManager.Instance.PlayerEnterExit;

            // Place in world near player depending on local area
            if (playerEnterExit.IsPlayerInsideBuilding)
            {
                PlaceFoeBuildingInterior(foeObject, playerEnterExit.Interior);
            }
            else if (playerEnterExit.IsPlayerInsideDungeon)
            {
                PlaceFoeDungeonInterior(foeObject, playerEnterExit.Dungeon);
            }
            else if (!playerEnterExit.IsPlayerInside && GameManager.Instance.PlayerGPS.IsPlayerInLocationRect)
            {
                PlaceFoeExteriorLocation(foeObject, GameManager.Instance.StreamingWorld.CurrentPlayerLocationObject);
            }
            else
            {
                PlaceFoeWilderness(foeObject);
            }
        }

        #endregion

        #region Placement Methods

        // Place foe somewhere near player when inside a building
        // Building interiors have spawn nodes for this placement so we can roll out foes all at once
        void PlaceFoeBuildingInterior(GameObject gameObjects, DaggerfallInterior interiorParent)
        {
            // Must have a DaggerfallLocation parent
            if (interiorParent == null)
            {
                SetComplete();
                throw new Exception("PlaceFoeFreely() must have a DaggerfallLocation parent object.");
            }

            // Always place foes around player rather than use spawn points
            // Spawn points work well for "interior hunt" quests but less so for "directly attack the player"
            // Feel just placing freely will yield best results overall
            PlaceFoeFreely(gameObjects, interiorParent.transform);
            return;
        }

        // Place foe somewhere near player when inside a dungeon
        // Dungeons interiors are complex 3D environments with no navgrid/navmesh or known spawn nodes
        void PlaceFoeDungeonInterior(GameObject gameObjects, DaggerfallDungeon dungeonParent)
        {
            PlaceFoeFreely(gameObjects, dungeonParent.transform);
        }

        // Place foe somewhere near player when outside a location navgrid is available
        // Navgrid placement helps foe avoid getting tangled in geometry like buildings
        void PlaceFoeExteriorLocation(GameObject gameObjects, DaggerfallLocation locationParent)
        {
            PlaceFoeFreely(gameObjects, locationParent.transform);
        }

        // Place foe somewhere near player when outside and no navgrid available
        // Wilderness environments are currently open so can be placed on ground anywhere within range
        void PlaceFoeWilderness(GameObject gameObjects)
        {
            // TODO this false will need to be true when start caching enemies
            GameManager.Instance.StreamingWorld.TrackLooseObject(gameObjects, false, -1, -1, true);
            PlaceFoeFreely(gameObjects, null, 8f, 25f);
        }

        // Uses raycasts to find next spawn position just outside of player's field of view
        void PlaceFoeFreely(GameObject gameObjects, Transform parent, float minDistance = 5f, float maxDistance = 20f)
        {
            const float overlapSphereRadius = 0.65f;
            const float separationDistance = 1.25f;
            const float maxFloorDistance = 4f;

            // Must have received a valid array
            if (gameObjects == null)
                return;

            // Set parent - otherwise caller must set a parent
            if (parent)
                gameObjects.transform.parent = parent;

            // Select a left or right direction outside of camera FOV
            Quaternion rotation;
            float directionAngle = GameManager.Instance.MainCamera.fieldOfView;
            directionAngle += UnityEngine.Random.Range(0f, 4f);
            if (UnityEngine.Random.Range(0f, 1f) > 0.5f)
                rotation = Quaternion.Euler(0, -directionAngle, 0);
            else
                rotation = Quaternion.Euler(0, directionAngle, 0);

            // Get direction vector and create a new ray
            Vector3 angle = (rotation * Vector3.forward).normalized;
            Vector3 spawnDirection = GameManager.Instance.PlayerObject.transform.TransformDirection(angle).normalized;
            Ray ray = new Ray(GameManager.Instance.PlayerObject.transform.position, spawnDirection);

            // Check for a hit
            Vector3 currentPoint;
            RaycastHit initialHit;
            if (Physics.Raycast(ray, out initialHit, maxDistance))
            {
                float cos_normal = Vector3.Dot(-spawnDirection, initialHit.normal.normalized);
                if (cos_normal < 1e-6)
                    return;
                float separationForward = separationDistance / cos_normal;

                // Must be greater than minDistance
                float distanceSlack = initialHit.distance - separationForward - minDistance;
                if (distanceSlack < 0f)
                    return;

                // Separate out from hit point
                float extraDistance = UnityEngine.Random.Range(0f, Mathf.Min(2f, distanceSlack));
                currentPoint = initialHit.point - spawnDirection * (separationForward + extraDistance);
            }
            else
            {
                // Player might be in an open area (e.g. outdoors) pick a random point along spawn direction
                currentPoint = GameManager.Instance.PlayerObject.transform.position + spawnDirection * UnityEngine.Random.Range(minDistance, maxDistance);
            }

            // Must be able to find a surface below
            RaycastHit floorHit;
            ray = new Ray(currentPoint, Vector3.down);
            if (!Physics.Raycast(ray, out floorHit, maxFloorDistance))
                return;

            // Ensure this is open space
            Vector3 testPoint = floorHit.point + Vector3.up * separationDistance;
            Collider[] colliders = Physics.OverlapSphere(testPoint, overlapSphereRadius);
            if (colliders.Length > 0)
                return;

            // This looks like a good spawn position
            foeObject.transform.position = testPoint;
            FinalizeFoe(foeObject);
            gameObjects.transform.LookAt(GameManager.Instance.PlayerObject.transform.position);
            motor = foeObject.GetComponent<EnemyMotor>();
            senses = foeObject.GetComponent<EnemySenses>();
            attack = foeObject.GetComponent<EnemyAttack>();
            seq = Seq.done;
        }

        // Fine tunes foe position slightly based on mobility and enables GameObject
        void FinalizeFoe(GameObject go)
        {
            var mobileUnit = go.GetComponentInChildren<MobileUnit>();
            if (mobileUnit)
            {
                // Align ground creatures on surface, raise flying creatures slightly into air
                if (mobileUnit.Enemy.Behaviour != MobileBehaviour.Flying)
                    GameObjectHelper.AlignControllerToGround(go.GetComponent<CharacterController>());
                else
                    go.transform.localPosition += Vector3.up * 1.5f;
            }
            else
            {
                // Just align to ground
                GameObjectHelper.AlignControllerToGround(go.GetComponent<CharacterController>());
            }

            go.SetActive(true);
        }

        private void Reset()
        {
            seq = Seq.none;
        }

        #endregion

        #region Event Handlers

        private void PlayerEnterExit_OnTransition(PlayerEnterExit.TransitionEventArgs args)
        {
            Reset();
        }

        private void StreamingWorld_OnInitWorld()
        {
            Reset();
        }

        #endregion

        #region Serialization

        [fsObject("v1")]
        public struct SaveData_v1
        {
            public Symbol foeSymbol;
        }

        public override object GetSaveData()
        {
            SaveData_v1 data = new SaveData_v1();
            data.foeSymbol = foeSymbol;

            return data;
        }

        public override void RestoreSaveData(object dataIn)
        {
            if (dataIn == null)
                return;

            SaveData_v1 data = (SaveData_v1)dataIn;
            foeSymbol = data.foeSymbol;
        }

        #endregion
    }
}

User avatar
BadLuckBurt
Posts: 948
Joined: Sun Nov 05, 2017 8:30 pm

Re: how to add quest actions?

Post by BadLuckBurt »

The enum might be causing the issue if you're not pre-compiling the mod. I haven't used that myself yet but I know the standard way mods are built doesn't support enums and you need use the precompile option for that to work. I know other modders have faced the same issue so hopefully one of them can chip in here
DFU on UESP: https://en.uesp.net/w/index.php?title=T ... fall_Unity
DFU Nexus Mods: https://www.nexusmods.com/daggerfallunity
My github repositories with mostly DFU related stuff: https://github.com/BadLuckBurt

.

User avatar
John Doom
Posts: 126
Joined: Wed Dec 01, 2021 5:59 pm
Location: Italy
Contact:

Re: how to add quest actions?

Post by John Doom »

That did it, thanks :)

User avatar
BadLuckBurt
Posts: 948
Joined: Sun Nov 05, 2017 8:30 pm

Re: how to add quest actions?

Post by BadLuckBurt »

John Doom wrote: Thu Dec 02, 2021 2:11 pm That did it, thanks :)
Awesome, glad to hear that :)

I wonder if it would be possible to detect an enum when building a mod to warn people about this limitation but that's a separate question.
DFU on UESP: https://en.uesp.net/w/index.php?title=T ... fall_Unity
DFU Nexus Mods: https://www.nexusmods.com/daggerfallunity
My github repositories with mostly DFU related stuff: https://github.com/BadLuckBurt

.

Post Reply