State of Motovun Forest

This is current state of my new mod called “Motovun Forest”.

I’m working for a while on my new mod called “Motovun Forest”. Lot of stuff are implemented including sounds, some NPC’s, ambient behavior, passages, statues, map system.. Still I’m missing the story..

Well, here are 2 new videos of me wandering around Motovun Forest area in it’s current state..

Feel free to drop me some idea(s) for this mod, if you have one.. I do have some general setting, but still it ‘s full of “plot holes”.. 😉

Area walkthrough – part 1

Area walkthrough – part 2

Performance and memory leaks patch for DAO

Yesterday I have noticed a new mod on Nexus –Performance and Memory Leak Fix – DXVK by kikokoalas . Since I’m modding the game from time to time, I have noticed these issues and memory problems. Game usually starts to stutter and lose some textures over time, specifically on newer versions of Windows (10/11). For Kivito’s YADAM is recommended to save often and restart the game from time to time..

So what is all about? Well – some guy (team) have his own project and he has made a fix for DX9 used in DAO.. This is not specifically for DAO, other older games are supported too, you have config file where you need to setup “how to behave” with the specific game..

Luckily mod author has done all needed stuff, so I have tried it and I can tell that it is working. Before adding the “dll” to the game’s “bin_ship” folder, I’ve tried some of my areas in game and saw sporadic memory increase (Task manager).

After adding mentioned “dll” from mod, I have tested same again and got lower memory consumption and seems that sporadic issues from before were not happening at all, memory consumption was stable.

I have also tried to run the toolset to see if this “patch” will somehow impact modding with the toolset and didn’t have any issues at all, but also I can’t confirm that this “dll” helps working with the toolset and toolset’s memory issues (large levels have returning “out of memory” error in both cases).

I guess you (I) will need to experiment more with it.. If you encounter some issues, I guess the fix would be removing added files from game’s “bin_ship” folder.

Cheers!

Lightmapping Tips for Windows 10/11 and Dragon Age Toolset

I can confirm that the latest available “LightMapper.py” script and lightmapping process work on Windows 11 with the setup described in previous posts. I can also confirm that whole process is much shorter on modern PC’s with multiple cores. Here are some additional tips, which are mostly documented in the main wiki but presented here in a concise version:

  1. Compatibility:
    • The setup works with Windows 10/11 and ActiveState Python 2.5.5.x.
  2. Handling Large Levels:
    • If you encounter an “out of memory” error with larger levels on Windows 10/11, consider the following:
      • Use a patched version of “DragonAgeToolset.exe” that is Large Address Aware (LAA). This allows the toolset to utilize more memory than the maximum available for a 32-bit application.
      • Note that some operations may not function correctly with the LAA version. In such cases, keep both exe files and use the LAA version specifically for lightmapping large levels (and possibly exporting).
      • For other tasks, stick to the normal exe version. Experimentation is key here.
      • You can download the LAA exe I use for the DAO toolset from Nexus mods. Place it in toolset’s folder next to original exe and create shortcut on your desktop.
  3. Cleaning Up Temp Files:
    • Before creating light maps, delete all temporary lightmap files for the level from the user’s temp and toolset’s temp folders. These folders are typically located at:
      • C:\Users\UserName\AppData\Local\Temp\DaLightmap
      • ..\Documents\BioWare\Dragon Age\Toolset\..
    • Usually I search for the folder based on the “Layout name” defined in the area properties for the specific level.
    • After deletion, consider restarting the toolset, as it may cache some data.
  4. Water Surfaces:
    • To include water surfaces in your level (later the area), export them (“Do all local posts”) from the “Single Player” module.
    • I recommend doing all level-related work within the “Single Player” module – but be careful!

I hope you find these tips helpful! I guess I will update this post if I recall or encounter additional information.

Cheers! 🌟

Some hints about lights in levels

Let’s continue discussing the lighting setup in Dragon Age: Origins (DAO) levels.

Ages were passed since I last created a level (around 2017). The most intricate one I worked on was “Motovun Forest,” a segment of an entire “region” for my upcoming mod. Initially, it was part of Kivito’s YADAM, but unfortunately, it didn’t make the final cut. As I share some advice, keep in mind that my insights are mostly based on memory, serving as a helpful reminder for myself as well.

The “EclipseRay” light mapper bundled with the toolset differs from the one BioWare used for their original levels (rumor has it they employed “Mental Ray” or something similar). Consequently, achieving a high-quality light map for a custom level can be quite challenging. You’ll encounter artifacts, spots, and triangles depending on your light configuration. Given the time-consuming nature of this process, fixing or “hiding” errors can easily consume days.

To set up the toolset’s light mapper with the latest script, follow the instructions outlined in this post.

When dealing with errors, your best bet is to experiment with tessellation and light configuration in the affected areas. Adjusting tessellation may alter the terrain’s appearance (including textures), while tweaking light settings—such as type, intensity, and radius—can make a difference. If all else fails, consider “hiding” the error by strategically placing vegetation or props like “rubble.”

In the DAO toolset level editor, you’ll encounter various types of lights: ambient, point animated, point static, baked, and more. For additional information, consult the main wiki dedicated to the DAO toolset.

I vaguely recall some limitations regarding light placement within a level. It seems there could be only one point (dynamic or static) light per level chunk, positioned carefully to avoid crossing chunk boundaries. However, this behavior isn’t always consistent—I’ve encountered instances where this rule didn’t apply at all.

When it comes to character lighting, a common approach is to use a three-point setup arranged in a triangle. The primary light source is a “Point – Static” light type with a very large radius affecting only character. Additionally, ambient light is used (which I believe imparts color to shadows) for external levels. Depending on the light source, the emitted color varies based on the main light direction (such as the sun) or, in the case of indoor environments, variations in room colors. To gain a deeper understanding, I recommend examining original BioWare levels to see how they utilized character lights.

For torches and bonfires, my typical workflow involves creating a “prop” group as a template for torches. Within this group, I set initial values for lights and props. Then, I duplicate this group and make specific modifications as needed after testing. The “–resMult” parameter in the latest “LightMapper.py” script is particularly useful during this process.

Specifically for torches, I use the “fxe_fire_trch_ost_p” model for the fire effect and “prp_torch_01_0” for the torch itself. I incorporate two types of lights: “Point – Baked” with a larger radius for overall level coloring, and “Point – Animated” with a smaller radius that affects both characters and (sometimes) the environment.

If you encounter issues with lighting artifacts (near chunk boundaries), consider adjusting the point light settings to avoid interference with other, stronger nearby static lights. Sometimes you can combine the point static light to affect both the level and characters. However, if having issues, try to ensure that the light radius remains small enough to avoid touching the terrain.

Torch template in action
Baked light setup
Animated point light setup

The same setup applies to bonfires, but with different models (“fxe_fire_larg_p” and “plc_bonfirever01_0”) and adjustments to the light (radius/intensity/affection) settings.

Afterward, it becomes a cycle of “testing – modifying – testing” until you achieve the desired result.

Note: When testing your level, combine it with your specific “areadata_xxx.gda” file. Assign the AREA_ID from the “areadata_xxx.gda” (or just “areadata.gda” if not using your own gda in your mod) to your test area for consistent results (like atmosphere and color hue).

Here is “no audio” walkthrough of “Motovun Forest” region made using this kind of setup. Originally “Village By The Sea” and “Village” community levels were made without torches and in a daytime version, but I have modified them for “atmosphere” to be consistent for whole region.

That’s all for this post! Cheers!

Insights into the DAO Light Mapper and the LightMapper.py Script

Recently, I was asked if I have the most recent Python script for the toolset light mapper. It took me some time to locate it, but in this post, I’ll describe how to set up the DAO toolset to use the script.

Light mapping has been an ongoing issue with the DAO toolset since its inception. The initial version (1.0) suffered from serious bugs, while version 1.2, included in the latest toolset release, remains usable but falls short of achieving the results seen in BioWare’s original levels. The light mapping process itself is time-consuming, often requiring 6-7+ hours on older PCs (depending on level size) just to reveal that the light map is flawed—almost like Murphy’s law in action.

When dealing with light configuration in a level, you’ll likely encounter issues such as black dots, spots, and triangles in the lightmap. To improve results, consider adjusting the terrain’s tessellation, modifying the light type near affected areas, adjusting intensity, and exploring other options. If all else fails, consider masking the issues with vegetation or props.

Unfortunately, this persistent problem was never fully resolved. However, for testing convenience, an updated script (whose origin I can’t quite recall) offers options to speed up the light mapping process. At least this way, you’ll spend less time waiting to determine if everything is “working” or “looking as it should.”

The DAO Toolset originally ships with the EclipseRay 1.2 light mapper engine. The interface between the DAO toolset and EclipseRay is the LightMapper.py script. When the toolset calls EclipseRay.exe, it includes specific parameters, including this script.

Initially, there were only two options alongside the script’s name: -cpus=2 and --terse. However, I’m uncertain whether -cpus was functional at all. In this updated version, you can specify the number of cores to use and adjust the quality of the light map.

So here is “how to setup and use it”:

To use this updated script, ensure that you have installed ‘ActiveStatePython 2.5.5.7’ or a compatible version. I’ve included some Python installations from my system in the zip file, although I’m uncertain which one I was using—most likely ActivePython for Windows.

Follow these steps:

  • Navigate to your Game\toolset installation folder.
  • Inside the “lightmapper” folder, create a backup of the old “LightMapper.py” script.
  • Replace the old script with the “LightMapper.py” from the file you have.

Then in DAO Toolset go to “Tools => Options” for “Level Editor => LightMapper Command” and replace the “LightMapper Command”:

  • Old: $(TOOLSET)\Lightmapper\EclipseRay.exe LightMapper.py –terse -cpus=2
  • New: $(TOOLSET)\Lightmapper\EclipseRay.exe LightMapper.py –resMult=0.5 –progress –terse –cpus=4

NOTE that parameter require 2 dashes (like “- -” without space between) , I guess because of fonts used in theme it’s showing as one long dash.

Parameters description:

  • --cpus: Experiment with this parameter if you have multiple cores. Adjust the number of cores to potentially improve speed. Keep in mind that this script is older, so using more cores might lead to issues depending on the number of CPUs employed.
  • --resMult: This parameter controls the quality of the lightmap, affecting its size and the time needed for creation. For quick testing, you can set it to 0.5 (half quality). However, be aware that different values of --resMult may introduce issues. For instance, at 0.5, you might encounter artifacts, while at 1.0 (or perhaps just 1), they could be present but less noticeable. Sometimes even lower quality settings yield better results.
  • --progress: Use this parameter to display additional information in the toolset’s log.

And for the end – here is link for the file from my Acronis cloud (since free WordPress does not allow it), or this link from Nexus mods for version without Python installation files.

Btw. TESTED ON WIN11!

That’s all for today, have fun!

Ambient Behavior – Gossip

Today, a quick one: How to incorporate gossip and gossip triggers as the ‘final touch’ for ambient behavior.

In the DA:O main campaign, there are areas where you can ‘hear’ conversations between two NPCs about various themes, the current state of the world, events, comments, etc. (e.g., Denerim, Mountain Pass, Orzammar).

The game fully supports this feature out of the box, but here’s a brief guide on how to use it in your mod(s).

The team behind the game has already created a gossip trigger. To use it, you need:

  1. Create two NPCs who will engage in the ‘gossip’ and assign them tags, placing them in your area.
  2. Establish a conversation between the two NPCs. In the conversation for both the speaker and the listener, utilize the respective NPC tags. The PC does not have any lines, and the PC’s line is always “CONTINUE.”
  3. Set the conversation as ambient for every line. Usually, if you designate a line in the tree as ambient, the rest of the lines (child lines) will inherit this setting, propagating it throughout that part of the conversation.
  4. Locate the “gen00tr_gossip” trigger in the palette window and position it in the area near the NPCs engaged in conversation.
  5. Open the trigger variables and configure the following variables to match your created resources:
    • TRIG_TALK_SPEAKER: Tag of the first gossip-speaking NPC
    • TRIG_TALK_DIALOG_OVERRIDE: Optionally, you can input a different conversation for the NPC, not the default one
    • TRIG_TALK_LISTENER: Tag of the NPC who will be part of the gossip
    • As the gossip trigger comes with some “defaults,” insert variables like NONE for string values you prefer not to use or -1 for plot flag numbers (e.g., TRIG_TALK_SET_PLOT and TRIG_TALK_SET_FLAG).

Some other settings are not mandatory when configuring the gossip trigger but could prove useful. For example, setting plot flags, activating the trigger based on the plot, or naming TRIG_TALK_LISTENER variable since most of these details are defined in the conversation itself.

Now, when the PC or party member enters the trigger, NPCs should initiate a conversation, engaging in gossip between themselves. Depending on the settings, the conversation could be repeated or not, and it might involve setting specific plot flags, among other possibilities.

Cheers for now!

Ambient Behavior in areas, part 3

This time, we’ll build upon the concept from part 2 and introduce an advanced trigger from the original development team, as seen in the “Lothering” area of the game.

In “Lothering,” the developers implemented a custom version of the “gen00tr_ambient” trigger. They opted to design a single trigger that, based on the number suffix, would define a specific “category” of animations and randomly select one animation from that category.

I prefer to (attempt to) organize my work, so I typically create a folder in the toolset’s “Palette window” for my mod. I also prefix that folder with the letter “z” to ensure it appears last among all folders in the “Palette window,” as shown in the following image. Additionally, I like to prefix my elements such as functions with “mfrs” in this case.

Preparation

To begin, we should create a new trigger for our “module” (in my case, “Motovun Forest“). After creating the trigger, we need to develop a script specifically for that trigger. In this case, both are named “mfrs000tr_gen_ambient.”

Contents of script file is something like this:

#include "sys_ambient_h"
#include "mfrs_utility_h"
#include "mfrs_constants_h"

//paste here constants if not having custom constants file like mfrs_constants_h


//paste here functions if not having custom functions file like mfrs_utility_h

const string HERO = "<hero>";
const int   INT_RESET   = 0;
const float FLOAT_RESET = 0.0f;

////////////////////////////////////////////////////////////////////////////////
/** @brief Gets the creature whose ambient behaviour is/was overridden by a trigger.
**//////////////////////////////////////////////////////////////////////////////
object[] Ambient_GetOverrideTargets(object oTrigger, object oTriggerer)
{
    object[] aTarget;
    //comma-separated lists of creature tags.
    string   sTriggerer = GetLocalString(oTrigger, AMBIENT_TRIGGER_FILTER);
    string   sTarget    = GetLocalString(oTrigger, AMBIENT_TARGET_FILTER);
    //if sTarget is empty, use sTriggerer and vice versa.
    if (IsStringEmpty(sTriggerer))
    {
        if (IsStringEmpty(sTarget))
        {
            PrintToLog("-----> LOG_CHANNEL_AMBIENT_AI, Ambient_GetOverrideTargets(), *** No tag specified for triggerer or target creature.");
            return aTarget;
        }
        else
            sTriggerer = sTarget;
    }
    if (IsStringEmpty(sTarget))
        sTarget = sTriggerer;

    PrintToLog("-----> LOG_CHANNEL_AMBIENT_AI, Ambient_GetOverrideTargets(), trigger: " + sTriggerer + "; target: " + sTarget);

    //determine whether to override ambient behaviour
    if (FindSubString(sTriggerer, GetTag(oTriggerer)) >= 0 || (FindSubString(sTriggerer, HERO) >= 0 && IsControlled(oTriggerer)))
    {
        //get targets whose ambient behaviour to override
        int i = 0, j = 0, k = 0;
        if (FindSubString(sTarget, GetTag(oTriggerer)) >= 0)
            aTarget[i++] = oTriggerer;

        string[] aTargetTag = SplitString(sTarget, ",");
        for (j = 0; j < GetArraySize(aTargetTag); j++)
        {   //changed # of creatures to 1 from 10
            object[] aCreature = GetNearestObjectByTag(oTriggerer, aTargetTag[j], OBJECT_TYPE_CREATURE, 1, 1);
            for (k = 0; k < GetArraySize(aCreature); k++)
            {
                if (IsInTrigger(aCreature[k], oTrigger))
                    aTarget[i++] = aCreature[k];
            }
        }
    }
    return aTarget;
}

void main()
{
    event  ev        = GetCurrentEvent();
    object oCreature = GetEventCreator(ev);
    int i            = 0;
    object oThis = OBJECT_SELF;
    Log_Events("", ev);

    //do this only if trigger is active
    if (GetObjectActive(oThis)){

        switch (GetEventType(ev))
        {
            case EVENT_TYPE_ENTER:
            {
                //determine a random animation to play based on the trigger tag.
                int     nOverride           =   mfrs_DetermineAnimToPlay(OBJECT_SELF);
                //randomize anim freqency
                float   fMaxOverrideFreq    =   (Random(5) * 0.1f) + 1.0f;
                SetLocalInt(OBJECT_SELF, AMBIENT_ANIM_PATTERN_OVERRIDE, nOverride);
                SetLocalFloat(OBJECT_SELF, AMBIENT_ANIM_FREQ_OVERRIDE, fMaxOverrideFreq);

                //override ambient behaviour.
                object[] aTarget = Ambient_GetOverrideTargets(OBJECT_SELF, oCreature);
                int   nAnimPattern   = GetLocalInt(OBJECT_SELF, AMBIENT_ANIM_PATTERN_OVERRIDE);
                float fAnimFreq      = GetLocalFloat(OBJECT_SELF, AMBIENT_ANIM_FREQ_OVERRIDE);
                int   nOverrideCount = GetLocalInt(OBJECT_SELF, AMBIENT_ANIM_OVERRIDE_COUNT);

                for (i = 0; i < GetArraySize(aTarget); i++)
                {
                    if (GetLocalInt(aTarget[i], AMBIENT_ANIM_OVERRIDE_COUNT) == 0)
                    {
                        Ambient_OverrideBehaviour(aTarget[i], nAnimPattern, fAnimFreq, nOverrideCount);
                        PrintToLog("-----> LOG_CHANNEL_AMBIENT_AI, ***** Playing Animation: " + IntToString(nAnimPattern) + ", creature: " + GetTag(aTarget[i]));
                    }
                }
                break;
            }
            case EVENT_TYPE_EXIT:
            {
                //restore ambient behaviour.
                if (GetLocalInt(OBJECT_SELF, AMBIENT_ANIM_OVERRIDE_COUNT) == 0)
                {
                    object[] aTarget = Ambient_GetOverrideTargets(OBJECT_SELF, oCreature);
                    for (i = 0; i < GetArraySize(aTarget); i++)
                    {
                        Ambient_RestoreBehaviour(aTarget[i]);
                        PrintToLog("-----> LOG_CHANNEL_AMBIENT_AI, ***** Restoring ambient: " + GetTag(aTarget[i]));
                    }
                }
                SetLocalInt(OBJECT_SELF, AMBIENT_ANIM_PATTERN_OVERRIDE, INT_RESET);
                SetLocalFloat(OBJECT_SELF, AMBIENT_ANIM_FREQ_OVERRIDE, FLOAT_RESET);
                break;
            }
        }
    }
}

If you examine the includes in the script, you’ll find files named “mfrs_utility_h” and “mfrs_constants_h.” Following BioWare’s syntax and resource organization, I’ve stored certain functions and constants in these files. It’s worth noting that the functions and constants from these files can be readily included in this trigger script, starting with constants and then functions (see in comments of the trigger script).

Here is excerpt from my “mfrs_constants_h” file:

//trigger types
const int           TRIGGER_TYPE_TALK                   = 1;
const int           TRIGGER_TYPE_IDLE                   = 2;
const int           TRIGGER_TYPE_PRAY                   = 3;
const int           TRIGGER_TYPE_PLACEABLE              = 4;
const int           TRIGGER_TYPE_CUSTOMER               = 5;
const int           TRIGGER_TYPE_MERCHANT               = 6;
const int           TRIGGER_TYPE_GUARD                  = 7;
//only for animals
const int           TRIGGER_TYPE_DEER                   = 8;
const int           TRIGGER_TYPE_DOG                    = 9;

And here is excerpt from my “mfrs_utility_h” file:

//taken from lothering scripts and extended to randomize ambient behavior
//returns one of 11 predefined talk animations
int mfrs_RandTalkAnim();

//returns one of 6 predefined idle animations
int mfrs_RandIdleAnim();

//returns one of 2 predefined interaction with placeable animations
int mfrs_RandPlaceableAnim();

//returns one of 2 predefined praying animations
int mfrs_RandPrayAnim();

//returns one of 2 predefined customer animations
int mfrs_RandCustomerAnim();

//returns one of 4 predefined merchant animations
int mfrs_RandMerchantAnim();

//returns one of 8 predefined guard animations
int mfrs_RandGuardAnim();

//returns one of 2 predefined deer animations
int mfrs_RandDeerAnim();

//returns one of 4 predefined dog animations
int mfrs_RandDogAnim();

//determines which animation to play depending on trigger
int mfrs_DetermineAnimToPlay(object oTrigger);

//restores default team ambient behavior
void mfrs_RestoreTeamAmbient(int nTeamID);

//forces creature to override animation, ie. dead loop (7,87,41,42,43)
void mfrs_PlayAnimation(object oTarget, int nAnimation);

//returns random talk anim
int mfrs_RandTalkAnim()
{
    int nRand   = Random(11);
    int nAnim;
    if     (nRand == 0) nAnim = 10;
    else if(nRand == 1) nAnim = 11;
    else if(nRand == 2) nAnim = 12;
    else if(nRand == 3) nAnim = 13;
    else if(nRand == 4) nAnim = 14;
    else if(nRand == 5) nAnim = 76;
    else if(nRand == 6) nAnim = 77;
    else if(nRand == 7) nAnim = 74;
    else if(nRand == 8) nAnim = 75;
    else if(nRand == 9) nAnim = 71;
    else if(nRand == 10) nAnim = 72;
    return nAnim;
}

//returns random idle anim
int mfrs_RandIdleAnim()
{
    int nRand   = Random(6);
    int nAnim;
    if     (nRand == 0) nAnim = 5;
    else if(nRand == 1) nAnim = 6;
    else if(nRand == 2) nAnim = 24;
    else if(nRand == 3) nAnim = 28;
    else if(nRand == 4) nAnim = 29;
    else if(nRand == 5) nAnim = 30;
    return nAnim;
}

//returns rando placeable anim
int mfrs_RandPlaceableAnim()
{
    int nRand   = Random(2);
    int nAnim;
    if     (nRand == 0) nAnim = 16;
    else if(nRand == 1) nAnim = 35;
    return nAnim;
}

//returns random customer anim
int mfrs_RandCustomerAnim()
{
    int nRand   = Random(2);
    int nAnim;
    if     (nRand == 0) nAnim = 15;
    else if(nRand == 1) nAnim = 115;
    return nAnim;
}

//returns random pray anim
int mfrs_RandPrayAnim()
{
    int nRand   = Random(2);
    int nAnim;
    if     (nRand == 0) nAnim = 9;
    else if(nRand == 1) nAnim = 33;
    return nAnim;
}

//returns random merchant anim
int mfrs_RandMerchantAnim()
{
    int nRand   = Random(4);
    int nAnim;
    if     (nRand == 0) nAnim = 20;
    else if(nRand == 1) nAnim = 21;
    else if(nRand == 2) nAnim = 88;
    else if(nRand == 3) nAnim = 114;
    return nAnim;
}

//returns random guard anim
int mfrs_RandGuardAnim()
{
    int nRand   = Random(8);
    int nAnim;
    if     (nRand == 0) nAnim = 5;
    else if(nRand == 1) nAnim = 6;
    else if(nRand == 2) nAnim = 24;
    else if(nRand == 3) nAnim = 25;
    else if(nRand == 4) nAnim = 28;
    else if(nRand == 5) nAnim = 29;
    else if(nRand == 6) nAnim = 90;
    else if(nRand == 7) nAnim = 91;
    return nAnim;
}

//returns random DEER anim - ONLY FOR RAMS, OXES, DEERS, HALLAS AND GOATS 
int mfrs_RandDeerAnim()
{
    int nRand   = Random(2);
    int nAnim;
    if     (nRand == 0) nAnim = 59;
    else if(nRand == 1) nAnim = 60;
    return nAnim;
}

//returns random DOG anim - ONLY FOR WOLF AND MABARY TYPES
int mfrs_RandDogAnim()
{
    int nRand   = Random(4);
    int nAnim;
    if     (nRand == 0) nAnim = 3;
    else if(nRand == 1) nAnim = 4;
    else if(nRand == 2) nAnim = 130;
    else if(nRand == 3) nAnim = 65;
    return nAnim;
}

//determines which anim to play by trigger type
int mfrs_DetermineAnimToPlay(object oTrigger)
{
    int     nAnimOverride;
    //get the tag and length of the Trigger.
    string  sTag            =   GetTag(oTrigger);
    int     nStringLength   =   GetStringLength(sTag);

    PrintToLog("-----> LOG_CHANNEL_AMBIENT_AI, ***** Trigger Tag is: " + sTag);

    //get the substring for the trigger type.
    string  sTriggerType    =   SubString(sTag, nStringLength - 1, 1);
    int     nType           =   StringToInt(sTriggerType);

    //if trigger type is 0 then do something random between 1 and 5
    if (nType == 0) nType = Random(5) + 1;

    PrintToLog("-----> LOG_CHANNEL_AMBIENT_AI, ***** Trigger Type is: " + IntToString(nType));

    switch(nType)
    {
        case TRIGGER_TYPE_TALK:     //1
        {
            nAnimOverride   =   mfrs_RandTalkAnim();

            break;
        }
        case TRIGGER_TYPE_IDLE:     //2
        {
            nAnimOverride   =   mfrs_RandIdleAnim();

            break;
        }
        case TRIGGER_TYPE_PRAY:     //3
        {
            nAnimOverride   =   mfrs_RandPrayAnim();

            break;
        }
        case TRIGGER_TYPE_PLACEABLE://4
        {
            nAnimOverride   =   mfrs_RandPlaceableAnim();

            break;
        }
        case TRIGGER_TYPE_CUSTOMER: //5
        {
            nAnimOverride   =   mfrs_RandCustomerAnim();

            break;
        }
        case TRIGGER_TYPE_MERCHANT: //6
        {
            nAnimOverride   =   mfrs_RandMerchantAnim();

            break;
        }
        case TRIGGER_TYPE_GUARD:    //7
        {
            nAnimOverride   =   mfrs_RandGuardAnim();

            break;
        }
        case TRIGGER_TYPE_DEER:     //8
        {
            nAnimOverride   =   mfrs_RandDeerAnim();

            break;
        }
        case TRIGGER_TYPE_DOG:      //9
        {
            nAnimOverride   =   mfrs_RandDogAnim();

            break;
        }
    }
    return nAnimOverride;
}

//restores ambient animations
void mfrs_RestoreTeamAmbient(int nTeamID)
{
    object  [] arTeam   =   GetTeam(nTeamID);
    object  oCurrent;

    int     nTeamSize   =   GetArraySize(arTeam);
    int     nLoop;

    for(nLoop = 0; nLoop < nTeamSize; nLoop++)
    {
        oCurrent    =   arTeam[nLoop];

        WR_ClearAllCommands(oCurrent, TRUE);

        Ambient_RestoreBehaviour(oCurrent);

        PrintToLog("-----> LOG_CHANNEL_AMBIENT_AI, ***** Restoring ambient behavior for: " + GetTag(oCurrent));
    }
}

//forces creature to override - play animation
void mfrs_PlayAnimation(object oTarget, int nAnimation)
{
    // Override ambient behaviour.
    SetLocalInt(oTarget, AMBIENT_ANIM_PATTERN_OVERRIDE, nAnimation);
    int   nAnimPattern   = GetLocalInt(oTarget, AMBIENT_ANIM_PATTERN_OVERRIDE);
    float fAnimFreq      = GetLocalFloat(oTarget, AMBIENT_ANIM_FREQ_OVERRIDE);
    int   nOverrideCount = GetLocalInt(oTarget, AMBIENT_ANIM_OVERRIDE_COUNT);
    Ambient_OverrideBehaviour(oTarget, nAnimPattern, 0.0f, 0); //fAnimFreq, nOverrideCount);
}

Note that I’m using the “mfrs” prefix for my elements; you should choose a different prefix for your module.

Now, how to use it?

It’s incredibly easy. Simply add the trigger to your area as we did for the generic trigger in part 2.

The only task is to set the tags of creatures for whom you want to trigger ambient animations into trigger variables and name the trigger with a number extension for the category from which you want to randomly play animations. For instance, if we label the ambient trigger tag as “my_customer_trigger_5” and assign the AMBIENT_TRIGGER_FILTER tag for our wandering NPC’s as “my_npc_customer,” when the NPC arrives at the waypoint and enters the trigger, it should play one of the “customer” animations.

If we append zero at the end of the trigger tag, the NPC will randomly select one of the first 5 categories and then play a random ambient animation from that category.

Simple, right? 😉

Kivito’s YADAM – a small history

Well, here is a brief development history of Kivito’s YADAM for those wanting to know more. Originally, this was a response to a question on Kivito’s YADAM mod site posts, but I decided to move it here to avoid cluttering the useful space on the mod site.

The development of Kivito’s YADAM began around 2012. In the very beginning, it was a birthday gift for my friend named “Pukijev / Puki’s after (party).” It was much simpler than today’s version, with fewer areas and simpler quests where he had to save his girlfriend from bandits. Many of our common friends were added to this mod as NPCs and quest givers, and some of their names have carried over into today’s version.

Since we played it at his party and received positive feedback, he suggested creating a full mod out of it. So it started to grow as I was learning the toolset.

As someone has already noticed, Witcher 2/3 served as inspiration for many elements in Kivito’s YADAM. Originally, the mod was intended to be twice as large, featuring Kivito as a party member with a ‘story inside a story’ questline. The player would be ‘replaced’ by a Witcher-like character named ‘Garelt of Trivia,’ complete with his voice, party members, and spells—a funny playful crossover between two worlds. Most elements like spells and areas were already created and functional.

But, alas—the curse of being a ‘Jack of all trades.’ As a non-native English speaker, my English is primarily technical, limiting my ability to express myself more creatively. Writing the ‘story’ and debugging plots became a nightmare, relying on Google Translate for what I hoped were proper translations.

I sought help on forums for the story and conversations, even attempting to connect teams via a VPN server, akin to BioWare’s approach, but received no responses.

I usually work in phases but tend to push until I snap. This was the case with this mod. I’d lose time and energy on trivial things, experimenting here and there, trying to implement new things I’d learned—ultimately losing focus on the original vision. This could be evident throughout the mod, especially towards the end when I was completely burned out, unable to look at the toolset anymore. There were like 3 phases of creation between the years, with every phase finishing with some kind of me burning out.

So in 2018, I decided to release the beta to salvage the time invested, with a strong belief that I wouldn’t embark on something like this again. To be honest – The beta was a comedy of errors and a grammar chaos. 😀

However, the toolset is like a demonic mistress, and with the rise of AI tools in 2023, I finally found the ‘missing part’—conversation grammar and story styles, along with AI voiceovers. And then, with the help of Bing and ChatGPT, I fixed most of the text, stripped away most of my jokes and fourth-wall breaks, but, as you’ve seen, some bits remain (“You can’t help me! No one can!!”).

As for the feeling of ‘unfinished’ parts of Kivito’s YADAM, ‘The Designer’ was initially meant to be ‘The Architect,’ an attempt to connect Awakening and Origins, introducing ‘Red lyrium.’ Unfortunately, I lost too much time in trying to create it and eventually abandoned the idea (‘It was just a legend..’).

I do have other mods, and I’m currently working on a new one—’Motovun Forest‘, utilizing elements created for this mod. The story won’t be connected; too much time has passed. I contemplated finishing it, despite the lack of a grand reward at the end. I’d welcome help, especially from someone skilled in creating stories, conversations, and such—I’m more of a technical person.

Meanwhile, I have created “Gamblers of Ferelden” – an ambient/quest mod inspired by Witcher 3 gamblers in taverns, “Bully” – a cosmetic addon with abilities to harass other NPCs in exploration mode, “DAO Convo companion” – to make life easier for people using AI voiceovers from play.ht site and to use it with the toolset. All of these mods have roots in Kivito’s YADAM, and most of that stuff was developed for Kivito’s YADAM.

I guess the end of modding for DA:O will come eventually, but concerning the state of modern games, the DA:O toolset is, in my opinion, one of the best software published—ever.

As in case anyone still wondering what “YADAM” means? Well, it means – “Yet Another Dragon Age Mod“. 😉

Ambient Behavior in areas, part 2

In first part we have done the basics, now it’s time to make things little complicated.

In the first part, we covered animating NPCs and moving them around. Now, in this part, we’ll delve into triggers to enhance the versatility of NPC animations.

Observing the construction of the main campaign reveals the extensive use of triggers for ambient behavior, altering NPC behavior upon approaching specific waypoints. A specific waypoint is typically placed inside a trigger, with the trigger itself being of type “gentr_ambient,” signifying a ‘generic ambient trigger.’

While the same trigger can be employed for multiple NPCs, there’s a drawback to the implementation of “gentr_ambient”—you can only specify one animation to be overridden, essentially mirroring the creation of an NPC with a fixed animation. Luckily, ambient AI system is initially set up to play both animations, from creature vars and from the trigger, by some chance.

To use “gentr_ambient”, you need to:

  • Place your NPC in the area and assign a TAG to it (e.g., “my_npc”).
  • Set up waypoints for the NPC to walk to, and assign names and tags to them (e.g., “ap_my_npc”).
  • Position relatively small “gentr_ambient” triggers around the waypoints and assign them meaningful tags as well.
  • Open the trigger’s “variables.”
  • Locate the AMBIENT_TARGET_FILTER and set it to the tag of your NPC. You can insert multiple tags, separated by commas.
  • Find the AMBIENT_ANIM_PATTERN_OVERRIDE variable and insert an integer representing the ambient animation you want to use. Usually it should be different from the one placed into “creature variables”.
  • Look for the AMBIENT_ANIM_OVERRIDE_COUNT variable and enter the number of times to repeat that specific animation. Default is 1 (once) if not specified.

Now that the trigger is configured to wait for an NPC with a specific tag, and assuming you’ve followed the setup from the first part, your NPC should (randomly) move to the waypoint. Upon entering the trigger, it should execute the animation specified in the trigger variables.

Note that there is also a chance not to execute specified animation from trigger, but original one from “creature variables”, and also there is a possibility that it executes both animations in some order.

For the waypoint where we did not put the trigger around it, NPC should execute animation set up in “creature variables”.

In the next part, we will design a trigger to randomize multiple animations of the same type and we will use same trigger to setup multiple types of the trigger (e.g. “praying”, “idle”, “talk”, etc.).

Ambient Behavior in areas, part 1

Incorporating movement and animations for NPCs in your area can significantly enhance the life and immersion of your mod. Surprisingly, it’s much easier than it may seem at first glance.

Let’s start with some basics.

Not every animation can be played as an ambient animation. In essence, animations are controlled by animation trees in the animation composer. You can add more animations, including custom ones, by editing the animation trees in the animation composer. Basically, you need to load the original game animation composer files into the animation composer, add your animations, create some “.gda” files, and export all with the toolset. More information on the subject can be found here.

The list of ambient animations can be found in the form of an Excel file in your toolset directory (usually “..\Dragon Age Origins\tools\Source\2DA\ambient_ai.xls”). There is also a list of all animations available in the game in the form of Excel file(s) on Nexus mods.

To make a creature use ambient animations, you need to:

  • Place your NPC to your area, then in creature properties search for “Variables”
  • Click the button in textbox to open modal dialog with variables for your NPC.
  • Search for AMBIENT_SYSTEM_STATE and set it to “1”.
  • You will also need to set AMBIENT_ANIMATION_PATTERN and set it to some integer value representing the animation (list of some popular animations follows below).

Some of the animations available:

  • Bartender: 102
  • Praying: 9, 33
  • Idle: 5, 6, 24, 28, 29, 30
  • Placeable interaction: 16, 35
  • Talk: 10, 11, 12, 13, 14, 76
  • Customer: 15
  • Merchant: 20, 21

Moving creatures around

I guess it is possible to make NPCs wander around randomly, but in the case of Dragon Age Origins’ main campaign, waypoints are used. In some cases, they are also combined with triggers to randomize specific NPC behaviors.

To make an NPC move around randomly or follow a specific path, you need to:

  • Give your NPC a tag (optional).
  • Create a couple of waypoints; the easiest way is to name them as “ap_<YOUR_NPC_TAG>.” For example, if your NPC has the tag “tavern_bartender,” you should create waypoints with the tag “ap_tavern_bartender” for random movement or “ap_tavern_bartender_01,” “ap_tavern_bartender_02,” etc., for following a path.
  • Place your NPC in your area, then in creature properties under “Variables.”
  • Click it to open a modal with variables for your NPC.
  • Search for AMBIENT_SYSTEM_STATE and set it to “1.”
  • Search for AMBIENT_MOVE_PATTERN and set it to “1” (there are other options, more information can be found here).
  • Optionally, set AMBIENT_ANIMATION_PATTERN to some integer, e.g., “15” (customer in the shop).

With this setup, your NPC will randomly go to some of the waypoints or follow a path (in case you added numbers to the end of the waypoint names). Upon reaching a waypoint, the NPC should execute animation “15” (customer) and then move on to the next waypoint, repeating the animation, and so forth.

If you prefer using custom waypoint names, you can easily do so by selecting your NPC within the designated area, navigating to its variables section, and choosing AMBIENT_MOVE_PREFIX. Here, you can override the waypoint name(s). For instance, if your NPC has the tag “my_npc,” to utilize waypoint movement ‘as is,’ you should name the waypoint(s) as “ap_my_npc(_NN),” where “_NN” is optional and represents the waypoint number. Alternatively, if your waypoints are named “wp_my_custom_waypoint,” enter “wp_my_custom_waypoint” into the AMBIENT_MOVE_PREFIX variable, and the NPC will use those waypoints for movement.

For more information about ambient animation, there is also wiki site here. I encourage you to check it out, as it contains crucial information about ambient AI system used in game.

This should be enough for the first part.

Design a site like this with WordPress.com
Get started