[GUIDE] Adding custom tactical signs to the Classic client

Hi,

I'm going to show you how to add custom tactical signs into the Classic client. To see end result please check https://lucera2.com/threads/tactical-signs-for-monsters.8572/

@Deazer feel free to move this thread to Guides section if you would like.

1. Prepare your assets for the client.

You can find 4 default tactical signs available in the game at `SysTextures.L2UI_CT1.HeadDisplay`. They're named `HeadDisplay_DF_mark_01`, `HeadDisplay_DF_mark_02` etc.
To extract L2UI_CT1.utx you can use `umodel`, but first decrypt it using for example `mxencdec`.

Icons for Action Window (Alt + C) can be found at `SysTextures.Icon.action_i`. They're named `action070`, `action071` etc.
You can extract them same way.

Once you have all your assets, you can create your own .utx package using L2Editor, or put them into original .utx packages. Don't forget to encrypt them back using `mxencdec`.

2. Modify .dat files.
Ideally would be to modify the `ActionName_Classic-eu.dat` to add your custom actions, but it happens that entries added there, do not add them into the Action Window (Alt + C). So I have skipped modifying that file. You can keep original one.

I'm using user commands (the ones used in game by typing `/tacticalsign1`, `/tacticalsign5` etc). You can instead use voiced commands, in this case you need to skip this step completely.

I have added new commands into `CommandName_Classic-eu.dat`. Important `id` is client id, not relevant to us, `action` is the number that is used by server to implement functionality of that command, keep them noted, because we will need them in a moment.

Code:
cmd_begin    id=2000    action=2000    cmd=[tacticalsign5]    cmd_end
cmd_begin    id=2001    action=2001    cmd=[tacticalsign6]    cmd_end
cmd_begin    id=2002    action=2002    cmd=[tacticalsign7]    cmd_end
cmd_begin    id=2003    action=2003    cmd=[tacticalsign8]    cmd_end
cmd_begin    id=2004    action=2004    cmd=[tacticalsign9]    cmd_end
cmd_begin    id=2005    action=2005    cmd=[tacticalsign10]    cmd_end
cmd_begin    id=2006    action=2006    cmd=[tacticalsign11]    cmd_end
cmd_begin    id=2007    action=2007    cmd=[tacticalsign12]    cmd_end
cmd_begin    id=2008    action=2008    cmd=[tacticalsign13]    cmd_end
cmd_begin    id=2009    action=2009    cmd=[tacticalsign14]    cmd_end
cmd_begin    id=2010    action=2010    cmd=[tacticalsign15]    cmd_end
cmd_begin    id=2011    action=2011    cmd=[tacticalsign16]    cmd_end
cmd_begin    id=2012    action=2012    cmd=[targettacticalsign5]    cmd_end
cmd_begin    id=2013    action=2013    cmd=[targettacticalsign6]    cmd_end
cmd_begin    id=2014    action=2014    cmd=[targettacticalsign7]    cmd_end
cmd_begin    id=2015    action=2015    cmd=[targettacticalsign8]    cmd_end
cmd_begin    id=2016    action=2016    cmd=[targettacticalsign9]    cmd_end
cmd_begin    id=2017    action=2017    cmd=[targettacticalsign10]    cmd_end
cmd_begin    id=2018    action=2018    cmd=[targettacticalsign11]    cmd_end
cmd_begin    id=2019    action=2019    cmd=[targettacticalsign12]    cmd_end
cmd_begin    id=2020    action=2020    cmd=[targettacticalsign13]    cmd_end
cmd_begin    id=2021    action=2021    cmd=[targettacticalsign14]    cmd_end
cmd_begin    id=2022    action=2022    cmd=[targettacticalsign15]    cmd_end
cmd_begin    id=2023    action=2023    cmd=[targettacticalsign16]    cmd_end

Last part is to modify `SysString_Classic-eu.dat` to add new System Messages, that are used to notify users in game that someone used tactical sign.

Code:
string_begin    stringID=8000    string=[Token 5 (Circle)]    string_end
string_begin    stringID=8001    string=[Token 6 (Square)]    string_end
string_begin    stringID=8002    string=[Token 7 (Lightning)]    string_end
string_begin    stringID=8003    string=[Token 8 (Triangle)]    string_end
string_begin    stringID=8004    string=[Token 9 (Number 1)]    string_end
string_begin    stringID=8005    string=[Token 10 (Number 2)]    string_end
string_begin    stringID=8006    string=[Token 11 (Number 3)]    string_end
string_begin    stringID=8007    string=[Token 12 (Number 4)]    string_end
string_begin    stringID=8008    string=[Token 13 (Number 5)]    string_end
string_begin    stringID=8009    string=[Token 14 (Number 6)]    string_end
string_begin    stringID=8010    string=[Token 15 (Number 7)]    string_end
string_begin    stringID=8011    string=[Token 16 (Number 8)]    string_end

3. Modify `Interface.xdat` file. Here we have to add our new tactical signs into game. Open that file using xdat editor for fafurion (p166). Open `headDisplay` tab, navigate to `headDisplayElements` -> `8` -> `elements`. By default you'll see only 4 `Element` entries, you need to copy and paste them and modify the `reserved` number, this is your tactical sign id, used by the server and `texName` which is path to your head display icon. If you used original .utx files you might just need to change the last number, depending on how you named your new textures.

1764674221886.webp

4. Now you need to modify `Interface.u` to add the new actions. Open up the `ActionWnd.uc` and add `AddCustomAction` function somewhere in the class.

Code:
function AddCustomAction(int id, string WndName, string actionName, string iconName, string iconNameEx1, string description, string command)
{
    local ItemInfo    infItem;

    infItem.ID.classID = id;
    infItem.Name = actionName;
    infItem.IconName = iconName;
    infItem.IconNameEx1 = iconNameEx1;
    infItem.Description = description;
    infItem.ShortcutType = int(EShortCutItemType.SCIT_ACTION);
    infItem.MacroCommand = command;

    class'UIAPI_ITEMWINDOW'.Static.AddItem("ActionWnd."$WndName, infItem);
}

Then find `HandleActionList` function, and head into end of this function. Add the following code:

Code:
if( infItem.ID.classID == 81 )
    {
        AddCustomAction(2000, WndName, "Use of Token 5", "icon.action2000", "", "Applies Token 5 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign5)", "/tacticalsign5");
        AddCustomAction(2001, WndName, "Use of Token 6", "icon.action2001", "", "Applies Token 6 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign6)", "/tacticalsign6");
        AddCustomAction(2002, WndName, "Use of Token 7", "icon.action2002", "", "Applies Token 7 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign7)", "/tacticalsign7");
        AddCustomAction(2003, WndName, "Use of Token 8", "icon.action2003", "", "Applies Token 8 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign8)", "/tacticalsign8");
    }

    if( infItem.ID.classID == 85 )
    {
        AddCustomAction(2004, WndName, "Target by Token 5", "icon.action2004", "", "Automatically focuses on the target marked with Token 5. Can be only used in a group.\n\n(Command: /targettacticalsign5)", "/targettacticalsign5");
        AddCustomAction(2005, WndName, "Target by Token 6", "icon.action2005", "", "Automatically focuses on the target marked with Token 6. Can be only used in a group.\n\n(Command: /targettacticalsign6)", "/targettacticalsign6");
        AddCustomAction(2006, WndName, "Target by Token 7", "icon.action2006", "", "Automatically focuses on the target marked with Token 7. Can be only used in a group.\n\n(Command: /targettacticalsign7)", "/targettacticalsign7");
        AddCustomAction(2007, WndName, "Target by Token 8", "icon.action2007", "", "Automatically focuses on the target marked with Token 8. Can be only used in a group.\n\n(Command: /targettacticalsign8)", "/targettacticalsign8");
    
        AddCustomAction(2008, WndName, "Use of Token 9", "icon.action2008", "", "Applies Token 9 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign9)", "/tacticalsign9");
        AddCustomAction(2009, WndName, "Use of Token 10", "icon.action2009", "", "Applies Token 10 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign10)", "/tacticalsign10");
        AddCustomAction(2010, WndName, "Use of Token 11", "icon.action2010", "", "Applies Token 11 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign11)", "/tacticalsign11");
        AddCustomAction(2011, WndName, "Use of Token 12", "icon.action2011", "", "Applies Token 12 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign12)", "/tacticalsign12");
        AddCustomAction(2012, WndName, "Use of Token 13", "icon.action2012", "", "Applies Token 13 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign13)", "/tacticalsign13");
        AddCustomAction(2013, WndName, "Use of Token 14", "icon.action2013", "", "Applies Token 14 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign14)", "/tacticalsign14");
        AddCustomAction(2014, WndName, "Use of Token 15", "icon.action2014", "", "Applies Token 15 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign15)", "/tacticalsign15");
        AddCustomAction(2015, WndName, "Use of Token 16", "icon.action2015", "", "Applies Token 16 to the target. Can be only used in a group.\\n\\n(Command: /tacticalsign16)", "/tacticalsign16");
    
        AddCustomAction(2016, WndName, "Target by Token 9", "icon.action2016", "", "Automatically focuses on the target marked with Token 9. Can be only used in a group.\n\n(Command: /targettacticalsign9)", "/targettacticalsign9");
        AddCustomAction(2017, WndName, "Target by Token 10", "icon.action2017", "", "Automatically focuses on the target marked with Token 10. Can be only used in a group.\n\n(Command: /targettacticalsign10)", "/targettacticalsign10");
        AddCustomAction(2018, WndName, "Target by Token 11", "icon.action2018", "", "Automatically focuses on the target marked with Token 11. Can be only used in a group.\n\n(Command: /targettacticalsign11)", "/targettacticalsign11");
        AddCustomAction(2019, WndName, "Target by Token 12", "icon.action2019", "", "Automatically focuses on the target marked with Token 12. Can be only used in a group.\n\n(Command: /targettacticalsign12)", "/targettacticalsign12");
        AddCustomAction(2020, WndName, "Target by Token 13", "icon.action2020", "", "Automatically focuses on the target marked with Token 13. Can be only used in a group.\n\n(Command: /targettacticalsign13)", "/targettacticalsign13");
        AddCustomAction(2021, WndName, "Target by Token 14", "icon.action2021", "", "Automatically focuses on the target marked with Token 14. Can be only used in a group.\n\n(Command: /targettacticalsign14)", "/targettacticalsign14");
        AddCustomAction(2022, WndName, "Target by Token 15", "icon.action2022", "", "Automatically focuses on the target marked with Token 15. Can be only used in a group.\n\n(Command: /targettacticalsign15)", "/targettacticalsign15");
        AddCustomAction(2023, WndName, "Target by Token 16", "icon.action2023", "", "Automatically focuses on the target marked with Token 16. Can be only used in a group.\n\n(Command: /targettacticalsign16)", "/targettacticalsign16");
    }

Right after:

Code:
class'UIAPI_ITEMWINDOW'.Static.AddItem("ActionWnd."$WndName, infItem);

If you see original Action Window in game, you'll see the 4 default tactical signs, and then 4 actions used to target creature, to make it look good, I'm applying 4 new tactical signs right after the 4-armed arrowed icon, so first 8 actions are used to apply tactical sign on creature, and second line 8 elements are used for targeting. I have added 12 new tactical signs and 12 actions for targeting, so in total 24 new actions. That required to increase the size of that `ItemWindow`, and decreasing `Basic` and `Party` sizes and moving them around to match the window size. It all can be done modifying the `ActionWnd` in `Interface.xdat`.

1764674708377.webp

Last part is to modify the `OnClickItem` function inside `ActionWnd.uc`. Find that function, and right before `DoAction(...)` function add:

Code:
if( strID == "ActionMarkItem" )
{
    if( Left( infItem.MacroCommand, 13 ) == "/tacticalsign" )
    {
        ExecuteCommand( infItem.MacroCommand );
        return;
    }
    else if(Left( infItem.MacroCommand, 19 ) == "/targettacticalsign")
    {
        ExecuteCommand( infItem.MacroCommand );
        return;
    }
}

This piece of code will make sure that new tactical signs actions from `Action Window` will trigger usage of user command `/tacticalsign5`, `/targettacticalsign7` etc.

5. Now the last part is to create custom server extension. Main extension file:

Java:
@Override
public void onLoad() {
    System.out.println("TacticalSignExt: onLoad()");
        
    Party.registerTacticalSign(5, 8000);
    Party.registerTacticalSign(6, 8001);
    Party.registerTacticalSign(7, 8002);
    Party.registerTacticalSign(8, 8003);
    Party.registerTacticalSign(9, 8004);
    Party.registerTacticalSign(10, 8005);
    Party.registerTacticalSign(11, 8006);
    Party.registerTacticalSign(12, 8007);
    Party.registerTacticalSign(13, 8008);
    Party.registerTacticalSign(14, 8009);
    Party.registerTacticalSign(15, 8010);
    Party.registerTacticalSign(16, 8011);
        
    UserCommandHandler.getInstance().registerUserCommandHandler(new UserTacticalSign());   
}

See the provided thread at the beginning of this guide, to see what is `Party.registerTacticalSign` function.

And the `UserTacticalSign.java` file:

Java:
public class UserTacticalSign implements IUserCommandHandler {
    private static final int[] USER_COMMANDS = { 2000, 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2020, 2021, 2022, 2023 };

    @Override
    public int[] getUserCommandList() {
        return USER_COMMANDS;
    }

    @Override
    public boolean useUserCommand(int id, Player player) {
        if (!player.isInParty()) {
            return false;
        }
        
        int tacticalSignId = 0;
        
        switch(id) {
        case 2000:
        case 2012:
            tacticalSignId = 5;
            break;
            
        case 2001:
        case 2013:
            tacticalSignId = 6;
            break;
            
        case 2002:
        case 2014:
            tacticalSignId = 7;
            break;
            
        case 2003:
        case 2015:
            tacticalSignId = 8;
            break;
            
        case 2004:
        case 2016:
            tacticalSignId = 9;
            break;
            
        case 2005:
        case 2017:
            tacticalSignId = 10;
            break;
            
        case 2006:
        case 2018:
            tacticalSignId = 11;
            break;
            
        case 2007:
        case 2019:
            tacticalSignId = 12;
            break;
            
        case 2008:
        case 2020:
            tacticalSignId = 13;
            break;
            
        case 2009:
        case 2021:
            tacticalSignId = 14;
            break;
            
        case 2010:
        case 2022:
            tacticalSignId = 15;
            break;
            
        case 2011:
        case 2023:
            tacticalSignId = 16;
            break;
        }
        
        if(tacticalSignId == 0) {
            return false;
        }
        
        if(id == 2000 || id == 2001 || id == 2002 || id == 2003 || id == 2004 || id == 2005 || id == 2006 || id == 2007|| id == 2008 || id == 2009 || id == 2010 || id == 2011) {
            if (player.getTarget() == null || !player.getTarget().isCreature()) {
                return false;
            }
            
            if(player.getTarget() instanceof Creature creatureTarget) {
                player.getParty().addTacticalSign(player, tacticalSignId, creatureTarget);
            } else {
                return false;
            }
            
            return true;
        } else if(id == 2012 || id == 2013 || id == 2014 || id == 2015 || id == 2016 || id == 2017 || id == 2018 || id == 2019 || id == 2020 || id == 2021 || id == 2022 || id == 2023) {
            player.getParty().setTargetBasedOnTacticalSignId(player, tacticalSignId);
            
            return true;
        }
        
        return false;
    }
}

Congratulations! You have successfully implemented custom tactical signs into your client.
 
Back
Top