Modding new enemies in DFU

Discuss coding questions, pull requests, and implementation details.
User avatar
Kab the Bird Ranger
Posts: 123
Joined: Tue Feb 23, 2021 12:30 am

Modding new enemies in DFU

Post by Kab the Bird Ranger »

For the past few weeks, I've been investigating our options for modding new enemies into DFU. It's impossible to do by any measure currently, but I was wondering how far DFU was from supporting some forms of new enemies. It is my opinion that being able to mod new enemies is one of the core features people expect when modding a role-playing game.

To help this discussion, I've made a reference implementation for what changes would have to be made in DFU, and what a mod using these changes would look like
The DFU changes are here: https://github.com/KABoissonneault/dagg ... 6ca1507f8d
The reference mod is here: https://github.com/KABoissonneault/DFU- ... Sprites.cs

In this implementation, I tried creating a new type of "class" enemy called "Druid", which is like a Mage, except with brown robes. Its enemy id is 147.

In DFU, I found that there's really only two components we need to create to define our new enemy: a MobileEnemy, and a DFCareer.

MobileEnemy

Mods can currently add new MobileEnemy instances to EnemyBasics.Enemies, due to it being public and mutable. Adding values to an array is not too convenient, but a temporary conversion to and from a "List<MobileEnemy>" does the trick. The new value will appear just fine in "GameObjectHelper.EnemyDict", and "SetupDemoEnemy.ApplyEnemySettings" will find the modded MobileEnemy just fine.

There's two systems in DFU that access EnemyBasics.Enemies as an array directly: "EnemyMotor.MakeEnemyHostileToAttacker" and "SoulBound.GetEnchantmentSettings". I'm pretty sure those systems only work for the 42 first enemies, since the enemy ID would match the array index. So mods should only add values at the end, and shouldn't expect those two systems to work on custom enemies.

Now, when creating an enemy, SetupDemoEnemy will try to set the DFCareer of the EnemyEntity, using an EntityType of either EnemyMonster or EnemyClass (ie: humans with a class). If the ID is between 0 and 42, it's an EnemyMonster, if the ID is between 128 and 146 it's an EnemyClass, otherwise it's an error. But then, how should it know whether our Druid (ID=147) is a monster or a class?

For the reference implementation, I decided that for every 256 IDs, the first 128 would be monsters, and the last 128 would be classes. So, 0-127 are monsters, 128-255 are classes, 256-383 are monsters again, and 384-511 are classes again, etc. This doesn't change classic IDs, and doesn't really limit how many IDs mods can use.

Next step in the SetupDemoEnemy process: setting the enemy career on the EnemyEntity.

DFCareer
EnemyEntity.SetEnemyCareer currently has two ways to get the DFCareer template. For monsters, it loads MONSTER.BSA, then loads the career for our enemy id. For classes, it loads the "CLASS<id>.CFG" for the class id, which is the enemy id minus 128. Of course, this won't work for our arbitrary enemy IDs.

In the reference implementation, I added a public static Dictionary in DaggerfallEntity, "CustomCareerTemplates". The key is the enemy id, and the value is the DFCareer template for that enemy type. Any system can easily detect whether an enemy id corresponds to a custom enemy by using this collection.

Quests
I'm not sure if there's an official way to add to Quests-Foes.txt without overriding the entire file, but in my case, I just wrote this

Code: Select all

QuestMachine.Instance.FoesTable.AddIntoTable(new string[] { "147, Druid" });
Then, quests can refer to "Druid" to spawn my new enemy.

Text Manager
This is minor, but the enemy's name is sometimes user-facing (ex: "You see a Druid."), and as a result, goes through the TextManager, in order to support localization. The approach taken by the manager only works for the classic enemies, and will break when given another enemy ID. For my reference implementation, I use the DaggerfallEntity.CustomCareerTemplates dictionary to detect a custom enemy, and use the career name in that case.

Mobile Sprites
This is the part that I blocked on. Personally, I don't find it worth adding new enemy types if players can't distinguish them from their appearance. I've investigated two approaches: new Textures, and palette swaps.

For new textures, I tried adding new textures named 512_0-0.png, 512_0-1.png, ... through my mod, and set 512 as the MobileEnemy's MaleTexture and FemaleTexture. This failed pretty hard, and from what I've gathered so far, this would take a lot of work, as the system would need to know if the sprites have Ranged, Idle, and Casting animations. I abandoned this approach f or now.

For my Druid, and for many other "new enemies" I had in mind, simply being able to change some colors on the existing sprites is enough. My Druid sprite just takes TEXTURE.486 and changes the robe color (18 colors total). I saw that while TextureFile loads the ART_PAL.COL palette by default, its palette can be replaced before its pixels are accessed. I thought of adding a new "palette override" field to MobileEnemy, and have DaggerfallMobileUnit use this palette to generate its material in "AssignMeshAndMaterial". Again, I didn't test this approach, as I didn't find a "clean" way to do it.

Conclusion
As can be seen in the reference implementation, there's not much DFU has to add to support new enemies. Sure, not everything would work properly, such as soul traps for custom monsters, but I don't think these limitations would stop the feature from being useful. However, I wouldn't consider this feature "version 0.1 ready" until I figure some way to let players customize the appearance of the enemy.

I'm curious what existing discussions people might have had on this topic, and any comments people would have on the current implementation. I went for the "minimal change" approach, given the state of the project, which means not everything is as clean as I would have wanted it to be. The #1 priority should be not breaking anything, of course.

User avatar
Kab the Bird Ranger
Posts: 123
Joined: Tue Feb 23, 2021 12:30 am

Re: Modding new enemies in DFU

Post by Kab the Bird Ranger »

Forgot to mention, the one issue I've had with this implementation is that only Mage and Healer are allowed to have a ranged attack that's not a bow attack - that is, a ranged spell. That is because EnemyMotor does this

Code: Select all

hasBowAttack = mobile.Enemy.HasRangedAttack1 && mobile.Enemy.ID > 129 && mobile.Enemy.ID != 132;
Seems like a poor way to handle this, even without modding in mind. There should probably just be another field in MobileEnemy that tells whether the mobile has ranged spells.

User avatar
King of Worms
Posts: 4752
Joined: Mon Oct 17, 2016 11:18 pm
Location: Scourg Barrow (CZ)
Contact:

Re: Modding new enemies in DFU

Post by King of Worms »

No idea on how to implement it, but I (we) ask for this for years. Apparently there are some obstacles.

I want to create new MOB variants. Different colors, completely new mobs (I have one or two ready from daggerfall demo), different sizes (Big Boss vs tiny but in swarms like rats, bats...)

I hope you can break thru this barrier. I dont see any serious efforts to do so for ages, just the acknowledgement its not easy. Well yea, creating DFU was not easy as well yet is done.

So lets go :ugeek:

User avatar
Hazelnut
Posts: 3015
Joined: Sat Aug 26, 2017 2:46 pm
Contact:

Re: Modding new enemies in DFU

Post by Hazelnut »

Am I correct in saying the only way the new enemy would be spawned in the game would be from a quest or C# script? Though modified/new RDBs could spawn them using fixed markers but only if the ID is less than 256.

Also, you could use ArrayUtility.Add() to add elements to an array. It's a useful utility class provided by unity.
See my mod code for examples of how to change various aspects of DFU: https://github.com/ajrb/dfunity-mods

User avatar
Kab the Bird Ranger
Posts: 123
Joined: Tue Feb 23, 2021 12:30 am

Re: Modding new enemies in DFU

Post by Kab the Bird Ranger »

>Am I correct in saying the only way the new enemy would be spawned in the game would be from a quest or C# script?

Yeah, and it's true that while this was enough for my needs, I should probably look into what's required to let mod add them to the spawn tables. I wouldn't want them to be added by default.

>Though modified/new RDBs could spawn them using fixed markers but only if the ID is less than 256.

That's another thing we'll have to look into. If the RDB format only uses 8 bits, then we can make an extension to allow bigger values. For example, I doubt any classic RDB uses the enemy id 255, so we could make DFU interpret 255 as "there's a real 32 bits ID after that". But that's extra changes.

At this point, I'd settle for something that only works in scripts and quests.

User avatar
Hazelnut
Posts: 3015
Joined: Sat Aug 26, 2017 2:46 pm
Contact:

Re: Modding new enemies in DFU

Post by Hazelnut »

There's more than 8 bits available, but the upper 8 are masked off to get the byte value classic needs. The other bits are unused in DFU (for mobs, used for flat npcs) but they are not always zero in classic data hence the masking. Being able to fix spawn new mobs in modded dungeons will make them quite a bit more useful so I'll give it some thought.
See my mod code for examples of how to change various aspects of DFU: https://github.com/ajrb/dfunity-mods

User avatar
Kab the Bird Ranger
Posts: 123
Joined: Tue Feb 23, 2021 12:30 am

Re: Modding new enemies in DFU

Post by Kab the Bird Ranger »

As for the encounter tables, after looking at the code, I can see the hesitation. While it's of course pretty simple for a mod to change or replace any of the encounter tables to add their custom mobile types, I have to recognize that this approach would have issues in the long term. The structure is pretty rigid, with each dungeon type having 20 entries that logically correspond to the expected player level. Mods would end up fighting for these slots pretty quickly. The "alternate spawn" setting does seem more robust to adding more than 20 entries to the table, but we can't rely on players using that, of course.

However, I'm not sure we need to tackle this issue for a first version of new modded enemies. We can leave the current spawn tables as is, and mods can choose to place their new enemy IDs in the table if they want. We can find a more scalable solution later.

I think just being able to spawn your own enemies from quests and custom locations would already be pretty useful.

User avatar
Kab the Bird Ranger
Posts: 123
Joined: Tue Feb 23, 2021 12:30 am

Re: Modding new enemies in DFU

Post by Kab the Bird Ranger »

Also, letting mods palette swap classic textures is probably never going to be a good solution because of the possibility of texture replacements. Better start looking into letting DFU tolerate new textures.

User avatar
King of Worms
Posts: 4752
Joined: Mon Oct 17, 2016 11:18 pm
Location: Scourg Barrow (CZ)
Contact:

Re: Modding new enemies in DFU

Post by King of Worms »

Just for info

Alternate spawn has its own set of issues which were analized by Pango. If I remember right, at some point it reduces the variety of mobs u can encounter. After reading this analisis, Ive decided to disable the option. And I kind of think it should not be in the game options - maybe just in the ini.

User avatar
pango
Posts: 3347
Joined: Wed Jul 18, 2018 6:14 pm
Location: France
Contact:

Re: Modding new enemies in DFU

Post by pango »

King of Worms wrote: Mon Jun 28, 2021 7:37 am Alternate spawn has its own set of issues which were analized by Pango. If I remember right, at some point it reduces the variety of mobs u can encounter.
Yup, the analysis is here: http://forums.dfworkshop.net/viewtopic.php?p=34974
Mastodon: @pango@fosstodon.org
When a measure becomes a target, it ceases to be a good measure.
-- Charles Goodhart

Post Reply