Skip to content
Leontopodium Nivale edited this page Aug 30, 2024 · 2 revisions

How do I go about starting to make a module?

Recording

You have 2 options that you can do now a days:

-> 1: You can set the replay's to automatically record while you're in a duty image

-> 2: You can manually start recording, and end the recording when you want to image

Both are valid, personally I like to have it auto-record so I can go through after and view replays if I need to with existing modules, or even keep for protentional future projects.

Get to the boss in question, keep dps to a minimal, and you want to see -every- mechanic that the boss has. That way you don't have to go back in later because somehow you missed 1 mechanic and have to sit there for a round 2. (Most basic dungeons don't require sitting for a long time, but things like alliance raids can have multiple mechanics that are missed).

How to view a replay

image

When you open up the replay manager, you can use the button at 1 to open a file browser and locate where you keep your replays. By default they get stored in %appdata% -> XIVLauncher/pluginConfigs/BossMod/replays. This is what you're going to initially see

image

  1. Is your timeline, this will let you scrub through your whole recording and pause/play for parts of the fight
  2. Shows all your active party members and the information about them (targeting, hp, status's they have) ---> The target is important for later, because that will contain the bossID needed for a part in the module to pull specific encounter information
  3. Will show you all of the actors in the fight (including ones that square hides behind the scenes/untargetable mobs that you do see)
  4. is where we'll be grabbing a lot of our module info for later. We won't need this IMMEDIATELY starting this out, but it's nice to know where it is.

Coding/Creating Initial Module

  • If you weren't already, it helps having a coding software to do this (visual studio has a free version that everyone can use, if you like paid software I know some developers use FreeRider)

You have two options, one is a bit more technical than the other but stay with me on this:

Option 1 (Starting from fresh/learning how everything works)

If you want an empty base to start off with, here's what you can paste into the new file:

namespace BossMod.Expansion.ContentType.ContentName.BossName;

public enum OID : uint
{
    Boss = 0x0,
}



public enum AID : uint
{

}

class BossNameStates : StateMachineBuilder
{
    public BossNameStates(BossModule module) : base(module)
    {
        TrivialPhase()
            ;
    }
}

[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "YourNameHere", GroupType = BossModuleInfo.GroupType.CFC, GroupID = x, NameID = y)]
public class BossName(WorldState ws, Actor primary) : BossModule(ws, primary, new(x, z), new ArenaBoundsXXX(20));

There's a lot to break down here, so let's start from the top here:

namespace BossMod.Expansion.ContentType.ContentName.BossName;

The things that you need to edit:

  • Expansion -- What expansion is this in?
    • RealmReborn | Heavensward | Stormblood | Shadowbringers | Endwalker | Dawntrail
  • ContentType -- What content is it? Dungeon? Trial? Extreme? Deep Dungeon?
  • ContentName -- What is this from? Castrum3ElectricBoogalo? EurekaOrthos | -- Usually put the name of the dungeon/content here
  • BossName -- Good formatting for this is usually the following (especially in dungeons).

Do note: this is used in the rest of the script as well, so remember to reference it a lot

  • xxBossName (In the case of eureka orthos for instance: DD10GancanaghStates)
  • xx Number at the front to indicate what boss this is, or even which group of ads your currently dealing with
  • BossName is the name of the boss, all one word.
public enum OID : uint
{
    Boss = 0x0,
}

This is just as important to have, don't be me and leave this empty initially

  • Boss = 0x0 is in reference to what the boss's ID is. When you are viewing a replay, you'll notice in the party tab/All actor's tab the section that say's target. This contains the bossID that needs to go in front of Boss = 0x. In this case, it would be: Boss = 0x3D54,

example of the party tab
image

example of the all actors tab (typically the one with the lowest health is the safest one to assume this is the boss) image

class BossNameStates : StateMachineBuilder
{
    public BossNameStates(BossModule module) : base(module)
    {
        TrivialPhase()
            ;
    }
}
  • BossNameStates -- This is the name of the boss + states (example: DD10GancanaghStates)
[ModuleInfo(BossModuleInfo.Maturity.Contributed, Contributors = "YourNameHere", GroupType = BossModuleInfo.GroupType.CFC, GroupID = x, NameID = y)]
  • Maturity -- Maturity.WIP | Maturity.Contributed | Maturity.Verified
    • Maturity.WIP - Works, but not fully complete/there's still mechanics that either need to be added, tweaked, or just overall makes sure it works
    • Maturity.Contributed - SHOULD be a fully complete module (all the mechanics are in there, AOE sizes are correct). Hasn't been checked over by the developer of the plugin, but should be 99% complete/should have majority of the kinks worked out
    • Maturity.Verified - Veyn has either checked the module himself and seen that it works properly/as it should/the code is clean, or they created it themself (which at that point should also be at the level that is expected for the modules).

Typically you want to start at WIP, and only put it as contributed once you know it all works as it should.

  • Contributors = "YourNameHere" -- This is where you put whatever penname you would like here
  • GroupID = x -- *In place of x, you want to put the zone location of where this boss takes place at. To find this, either open up the massive spreadsheet through godbert (or if you have snd installed, you can use the built in spreadsheet browser there) and you'll find it under contentfindercondition

Example of it in snd. Godbert functions the same way.
image image

  • NameID = y -- *In place of y, you want to put the id of the boss that the module is for. You can find this info under bnpcname in the spreadsheet.

And the last line:

public class BossName(WorldState ws, Actor primary) : BossModule(ws, primary, new(x, z), new ArenaBoundsXXX(20));
  • BossName is once again the name of the boss that you've used through this whole document

  • new(x, z) | x and z are the center/midpoint of your whole arena. This will help keep the attacks (and you) on the screen. There's a couple of ways to do this:

    • Easy way to get this is if the boss recenters itself you can use the position of the boss to tell where the center of the arena is (best way to do it)
    • Another way if there's an actor in the center of the arena that you can use (good example of one of the secondary mobs being in the DIRECT center image
    • Another suggestion is while you're facing the boss's, try and run edge to edge of the arena, can do some math from there to tell the center of the arena
    • Last suggestion is the plugin Hyperborea, personally use this when it's an arena that is just a pain in the ass to get to at times, or just need to go out of my way to see just the arena.
  • new ArenaBoundsXXX(y) is the shape of the arena. You have 3 pre-defined ones, and 1 custom one

    • new ArenaBoundsCircle(radius) <-- Circle arena
    • new ArenaBoundsSquare(radius) <-- Square arena
    • new ArenaBoundsRect(width, height) <-- Rectangular arena, has more paramaters on top of what's here, but this is the basic
    • new ArenaBoundsCustom() <-- this one I have thankfully not had to mess with, but this is used if you have some really odd shapes (like the 70 alliance raid with thunder god for instance). Will have to write up a guide with this in the future.

Here's what the full start of what one would look like as an example:

namespace BossMod.Endwalker.DeepDungeon.EurekaOrthos.DD10Gancanagh;

public enum OID : uint
{

}

public enum AID : uint
{

}

class DD10GancanaghStates : StateMachineBuilder
{
    public DD10GancanaghStates(BossModule module) : base(module)
    {
        TrivialPhase()
            ;
    }
}

[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "YourNameHere", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 897, NameID = 1224)]
public class DD10Gancanagh(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -300), new ArenaBoundsSquare(20));

Option 1 (The more automated process)

In the analysis tab, it should drop down to the following:
image

Within here, you will see ALL of the actors that gets spawned in your recording. What you want to look for is the enemy/boss that you are currently making the module on. (For the sake of the example, I'm going to be using the floor 10 Boss "Gancanagh").

If there are multiple of the same name, you can confirm which one you're looking for by going to the "All actors" tab. It's usually the one that has the lower HP value, so if you need to scrub through the replay a bit to make sure you get the right one, that's 100% fine. image

Once you've figured out the ID, match it to the one under Participant info, right click it, and it should bring up a little box that gives you two options. You want to press the one that says Generate module stub (trivial states)

image

Afterwards, it'll copy a starting code block for you to use in your module! For Gancanagh it'll look like the following:

public enum OID : uint
{
    Boss = 0x3D54,
    Helper = 0x233C,
}

class GancanaghStates : StateMachineBuilder
{
    public GancanaghStates(BossModule module) : base(module)
    {
        TrivialPhase();
    }
}

[ModuleInfo(BossModuleInfo.Maturity.WIP, GroupType = BossModuleInfo.GroupType.CFC, GroupID = 897, NameID = 12240)]
public class Gancanagh(WorldState ws, Actor primary) : BossModule(ws, primary, new(100, 100), new ArenaBoundsCircle(20));

and if you notice, it has GroupID and NameID already in there for you! The main things you'll need to add is:

  • Contributors to show who's made it
  • Change all the names to have the proper naming scheme (so in this case, D10Gancanagh)
  • include a namespace at the very top as well.
  • fix the ArenaBounds as well, to match the shape of what arena you're currently fighting in.

This is nice for once you get all the knowhow's of what works and doesn't.

Once this is all created and done right, you just have to build the plugin yourself, and either load it up in game or through UIDev.

Alright, the base of the module is made, now what?

How to create attack hints for the minimap.

Now that the above is done and you've build the plugin, you should be able to start grabbing the important information from the replay's. Re-open the replay management ui, and find the replay that you have saved on your pc and re-open it.

If the build/module is set up correctly, you should see this
image

The area between the green dot and the red dot (if you're color blind, the two dots on the right) is your module!
image

When you're timelaps is between those two dots, it should show you the minimap for that boss.

Now that you have that, you're going to want to open up Analysis and you should see a a dropdown for the boss in that replay now
image

Opening that dropdown in itself will give you the following
image

What you're going to want to do is Right Click the Participant Info dropdown, and you should get the following window
image

Typically, you really only want to do Generate missing enum values for boss module because this will only grab the info that you're currently missing in the module and won't re-create un-necessary information again.

Once you select that, go back to your code editor, and page it below public enum OID : uint. It should look similar to the following:

public enum OID : uint
{
    Boss = 0x3D54, // R1.800, x?
    PachypodiumMine = 0x3D55, // R1.500, x9
}

Usually, it will have _Gen_NameofEnemyHere, you want to remove it to -just- have the name of the enemy.

  • For Ability info, typically I just do a Generate enum for boss module since I have no data that would go under public enum AID : uint
  • Status info, Icon info, and Tether info are on a fight by fight basis, and sometimes you'll need them for bosses, other times you won't. They're all self explanitory in itself (status shows you the status's in the fight, icon if there's any special icons/effects that happens on enemies/players [limit cut dots for example, tb circle], and tether info if you have a "drag x amount of distance from the boss`

This is what I got from the floor 10 Orthos boss for example from Ability Info

public enum AID : uint
{
    _AutoAttack_Attack = 6499, // Boss->player, no cast, single-target
    _Weaponskill_AuthoritativeShriek = 31477, // Boss->self, 3.0s cast, single-target
    _Spell_Mandrashock = 31478, // 3D55->self, 5.0s cast, range 10 circle
    _Spell_Mandrastorm = 31479, // Boss->self, 5.0s cast, range 60 circle
    _Spell_Mandrashock = 32700, // 3D55->self, 8.0s cast, range 10 circle
}

You'll notice there's a lot of details here, so breaking it down:

  • You'll want to shorten down all the attacks to just the base name. _Spell_Mandrastorm -> Mandrastorm for example.
  • You'll also notice that each of them tell you from what point they originate, and who it's targeting. This can be useful to tell what kind of attack you need to write, as well as the range of the attack + what shape it is (thank you veyn for making this so detailed).
    • SOMETIMES. There can be missing information that just isn't extracted, or it's a pain to find sometimes. If this ever happens, you can expand the Ability info tab, find the skill/spell in question, and within there is a -whole- bunch of information about the attack (range, drop-off, cast time... ect).
  • You'll also notice that I have 2 spells with the exact same name, but different cast times/ID's. This is because the 1st shock 31478 has only 4-5 of them going off at the same time, while the 2nd one 32700 has them going off in a titan style fashion, while also having a longer cast time. Make sure to name these in a way that you can tell what is what for later, but also make it legible and easy for people to understand -what the fuck it is- in case they need to go back and edit it later.

This is how mine is looking so far for example.

public enum AID : uint
{
    Attack = 6499, // Boss->player, no cast, single-target // direct auto attack
    AuthoritativeShriek = 31477, // Boss->self, 3.0s cast, single-target, Activates the PachypodiumMine on the floor
    MandrashockSingle = 31478, // 3D55->self, 5.0s cast, range 10 circle, Floor drones casting at the same time, usually within a pattern
    MandrashockTitan = 32700, // 3D55->self, 8.0s cast, range 10 circle,  Floor drones casting in a titan-formation
    Mandrastorm = 31479, // Boss->self, 5.0s cast, range 60 circle, Proximity AOE from boss ? distance (18.5f seems the the point where it will hit for flat damage)
}

How to create the actual ui information

Now taking the above information from the public enum AID : uint, you can actually tie the modules to the attacks themselves.

Lets start off with MandrashockSingle. The end result of it would be (in a basic module, more complex attacks have more code attached to it, but that's a seperate thing at the second)

class MandrashockSingle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MandrashockSingle), new AOEShapeCircle(10f));

Breaking everything down

  • class MandrashockSingle(BossModule module) : MandrashockSingle is the name of what you want to call the mechanic that is being displayed. Save everyone the trouble and match it up to the name of the module if it's just a simple attack. everything else is the same before and to the :
  • Components.TypeofAttack <-- After you throw in Components. it will bring up the whole list of attacks that are currently build into the boss modules. There is a lot, but each one will tell you what information you need to fill in as you fill in the information. For the current example. It's going to be Components.SelfTargetedAOEs
  • Components.SelfTargetedAOEs <-- this is the TYPE of aoe that is currently going off. This attack is being cast from the boss, and going on itself (as shown here MandrashockSingle = 31478, // 3D55->self,.
    If you would like to see the dropdown information i'm referencing image
    • BossModule module <-- module
    • ActionID aid <-- This is letting you tell -what- this attack is going to show up and disappear on the map with. Maybe the boss is casting one thing, and there's actually a hidden actor that is actually casting it separately? Maybe the attack is coming from 2 different sources, but you want them to show up and disappear in the same instance. It will usually look like ActionID.MakeSpell(AID.Spelltiedtoattack)
    • AoeShape shape <-- what shape is the aoe that is going off? The same logic that making the shape of the arena is applied here as well.
    • maxCasts how many of these cast do you want being visible at once? Great if you have a series of the same attack going off in a sequence, but want it to only show (for a good example) 6 of the 9 cast that are going off, then have the last 3 show up after the first 3 go off. (the MandrashockTitan is a good example of this)
class MandraShockTitan(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MandrashockTitan), new AOEShapeCircle(10f), 6); // made this 6 at the second to keep the safe spot shown for 3rd hit.

this is a good example of limiting cast. Here the spell MandrashockTitan had 9 cast that normally go off in a sequence.

This is what it looks like when i limit the amount of cast down to 6 to help show the initial safe spot:
image image image

vs if you were to have all the aoe's going off/disappearing whenever the cast disappears
image image image

  • Personally, I'd try and make it to where it's not visually overstimulating/easy to read. No reason to make it to where it's going to be confusing to the player, while also not confusing the Ai TOO much.

Testing/Loading the actions themselves

So up to this point, you should have some of the actions written out, or at least one of them to test out. The way to test to see if it it works properly/if you even attached the right component. Here's where I'm currently sitting at writing up all the modules so far, but you can also just have 1 attack written out and still be in the same place.

namespace BossMod.Endwalker.DeepDungeon.EurekaOrthos.DD10Gancanagh;

public enum OID : uint
{
    Boss = 0x3D54, // R1.800, x?
    PachypodiumMine = 0x3D55, // R1.500, x9
}

public enum AID : uint
{
    Attack = 6499, // Boss->player, no cast, single-target // direct auto attack
    AuthoritativeShriek = 31477, // Boss->self, 3.0s cast, single-target, Activates the PachypodiumMine on the floor
    MandrashockSingle = 31478, // 3D55->self, 5.0s cast, range 10 circle, Floor drones casting at the same time, usually within a pattern
    MandrashockTitan = 32700, // 3D55->self, 8.0s cast, range 10 circle,  Floor drones casting in a titan-formation
    Mandrastorm = 31479, // Boss->self, 5.0s cast, range 60 circle, Proximity AOE from boss ? distance (18.5f seems the the point where it will hit for flat damage)
}

class MandrashockSingle(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MandrashockSingle), new AOEShapeCircle(10f));
class MandraShockTitan(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.MandrashockTitan), new AOEShapeCircle(10f), 9); // made this 6 at the second to keep the safe spot shown for 3rd hit.
class MandraStorm(BossModule module) : Components.SelfTargetedAOEs(module, ActionID.MakeSpell(AID.Mandrastorm), new AOEShapeCircle(18.5f));

class DD10GancanaghStates : StateMachineBuilder
{
    public DD10GancanaghStates(BossModule module) : base(module)
    {
        TrivialPhase()
          ;
    }
}

[ModuleInfo(BossModuleInfo.Maturity.WIP, Contributors = "Ice", GroupType = BossModuleInfo.GroupType.CFC, GroupID = 897, NameID = 12240)]
public class DD10Gancanagh(WorldState ws, Actor primary) : BossModule(ws, primary, new(-300, -300), new ArenaBoundsSquare(20));

In TrivialPhase() this is where you place your attack modules that get activated/telegraphed in the mini-map. And should look something to the folowing:

class DD10GancanaghStates : StateMachineBuilder
{
    public DD10GancanaghStates(BossModule module) : base(module)
    {
        TrivialPhase()
            .ActivateOnEnter<MandrashockSingle>()
            .ActivateOnEnter<MandraShockTitan>()
            .ActivateOnEnter<AttackModuleNameHere>();
    }
}
  • .ActivateOnEnter<AttackModuleNameHere>() is how you have it load up with the module. <AttackModuleNameHere> is the name of the skill/spell itself that you decided to name it (Hopefully similar if not the same name as the skill/spell in question). For instance, class MandrashockSingle(BossModule module), We're pulling MandrashockSingle and putting it in the .ActivateOnEnter<>()

Once you have it put in there, you can build the plugin, load the replay, find the part where the skill goes off, and make sure it works properly!