How to enable more skills in Char Editor

Discussion in 'General Modification' started by anatoliy, Jun 9, 2020.

Remove all ads!
  1. anatoliy

    anatoliy Established Member

    Joined:
    Feb 18, 2017
    Messages:
    236
    Likes Received:
    75
    Hi

    Can anyone point me to which files allow to show more Skills in the Character Creation / Level Up screen?

    BR,
    A
     
  2. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    2,867
    Likes Received:
    148
    IIRC it's hardcoded in the dll in some table that defines whether the skill is enabled.
     
    anatoliy likes this.
  3. anatoliy

    anatoliy Established Member

    Joined:
    Feb 18, 2017
    Messages:
    236
    Likes Received:
    75
    Was it addressed in T+? I mean to be able to override these settings?
     
  4. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    2,867
    Likes Received:
    148
    No. You can see the table address in skill.cpp, and I guess edit it in memory instead of changing the dll.
     
    anatoliy likes this.
  5. Endarire

    Endarire Ronald Rynnwrathi

    Joined:
    Jan 7, 2004
    Messages:
    545
    Likes Received:
    45
  6. Shiningted

    Shiningted I want my goat back Global Moderator

    Joined:
    Oct 23, 2004
    Messages:
    12,108
    Likes Received:
    138
    Activating the Skills can be done with the Activator HERE.

    Getting them to show outside levelup screen? No idea. If you do a .dll hack for it, let me know :)
     
    anatoliy likes this.
  7. anatoliy

    anatoliy Established Member

    Joined:
    Feb 18, 2017
    Messages:
    236
    Likes Received:
    75
    I have created small alteration in T+:
    Code:
    void LegacySkillSystem::LoadSkillsProps(const std::string& path)
    {
        std::string error;
        if (!vfs->FileExists(path)) return;
        json11::Json json = json.parse(vfs->ReadAsString(path), error);
    
        if (json.is_null()) {
            throw TempleException("Unable to parse skills_props.json from {}: {}", path, error);
        }
    
        if (!json.is_array()) {
            throw TempleException("skills_props.json must start with an array at the root");
        }
    
        for (auto& item : json.array_items()) {
            if (!item.is_object()) {
                logger->warn("Skipping skill that is not an object.");
                continue;
            }
            auto idNode = item["id"];
            if (!idNode.is_number()) {
                logger->warn("Skipping skill that is missing 'id' attribute.");
                continue;
            }
            uint32_t id = (uint32_t)item["id"].int_value();
            if (id < 0 || id > SkillEnum::skill_use_rope) {
                logger->warn("Skipping skill that is out of bounds.");
                continue;
            }
    
            if (!item["enabled"].is_number()) {
                logger->warn("Skipping skill that is missing 'enabled' int attribute.");
                continue;
            }
            if (id)
                skillPropsTable[id].classFlags &= ~0x80000000;
            else skillPropsTable[id].classFlags |= 0x80000000;
        }
    }
    
    And the file skills_props.json:
    Code:
    [
    {
    "code": "skill_balance",
    "id": 22,
    "enabled": 1
    },
    {
    "code": "skill_climb",
    "id": 23,
    "enabled": 1
    },
    {
    "code": "skill_escape_artist",
    "id": 27,
    "enabled": 1
    },
    {
    "code": "skill_jump",
    "id": 32,
    "enabled": 1
    },
    {
    "code": "skill_jump",
    "id": 32,
    "enabled": 1
    },
    {
    "code": "skill_knowledge_arcana",
    "id": 33,
    "enabled": 1
    },
    {
    "code": "skill_knowledge_religion",
    "id": 34,
    "enabled": 1
    },
    {
    "code": "skill_knowledge_nature",
    "id": 35,
    "enabled": 1
    },
    {
    "code": "skill_knowledge_all",
    "id": 36,
    "enabled": 1
    },
    {
    "code": "skill_use_rope",
    "id": 41,
    "enabled": 1
    }
    ]
    
    That did displayed and allowed editing Skills in the Char Editor screen. But in Inventory screen it is still not accessible. Will investigate further.
     
  8. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    2,867
    Likes Received:
    148
    The skills UI (char_skills_ui) is a can of worms unfortunately.

    Troika hardcoded it so the first 21 skills are those that are enabled, and the rest come afterwards.

    The skills UI is hardcoded to iterate through the first 21 skills, regardless of the skill props table, and also has a 21 sized buffer for the button widgets (LgcyButton*[21] at 0x10D1A338).
    You'll either have to patch these limits (and perhaps re-order the skills so only the ones active are at the beginning) or rewrite the skills UI code partially or entirely - doable, but a PITA (I guess a day or two of work, plus whatever it takes to learn).

    For more info see UiCharSkillWidgetsInit (0x101BE110).

    BTW your JSON has skill_jump twice.
     
    anatoliy likes this.
  9. anatoliy

    anatoliy Established Member

    Joined:
    Feb 18, 2017
    Messages:
    236
    Likes Received:
    75
    Thank you Sitra!!
     
  10. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    2,867
    Likes Received:
    148
    BTW please replace
    Code:
    id > SkillEnum::skill_use_rope
    
    With
    Code:
    id >= SkillEnum::skill_count
    

    Another BTW: don't do that, at least for the vanilla skills, since the enums are used in many places inside the CPP code. It's better to just alphabetically sort the buttons in the UI.
     
    anatoliy likes this.
  11. anatoliy

    anatoliy Established Member

    Joined:
    Feb 18, 2017
    Messages:
    236
    Likes Received:
    75
    I thought you will notice that)) Thanks!

    As for the enums - yes, off course.
     
  12. anatoliy

    anatoliy Established Member

    Joined:
    Feb 18, 2017
    Messages:
    236
    Likes Received:
    75
    Rewrote original code into new ui\ui_skills.h, ui\ui_skills.cpp:
    Code:
    #include "stdafx.h"
    #include "ui_skills.h"
    #include <util/fixes.h>
    #include <ui/ui.h>
    #include <temple/dll.h>
    
    UiSkills uiSkills;
    
    class UiSkillsHooks : public TempleFix {
    public:
        void apply() override {
            replaceFunction<int(__cdecl)()>(0x101BE110, []() {
                return uiSkills.UiCharSkillWidgetsInit();
            });
        }
    } uiSkillsHooks;
    
    int UiSkills::UiCharSkillWidgetsInit()
    {
        int uiCharSkillsWndX = temple::GetRef<int>(0x10D19EA8);
        int uiCharSkillsWndY = temple::GetRef<int>(0x10D19EAC);
        int uiCharSkillsWndW = temple::GetRef<int>(0x10D19EB0);
        int uiCharSkillsWndH = temple::GetRef<int>(0x10D19EB4);
    
        LgcyWindow widget;
        memcpy(widget.name, "char_skills_ui_main_window", 27);
        widget.x = uiCharSkillsWndX;
        widget.xrelated = uiCharSkillsWndX;
        widget.width = uiCharSkillsWndW;
        widget.y = uiCharSkillsWndY;
        widget.yrelated = uiCharSkillsWndY;
        widget.type = LgcyWidgetType::Window;
        widget.parentId = -1;
        widget.widgetId = -1;
        widget.flags = 1;
        widget.height = uiCharSkillsWndH;
        widget.render = temple::GetRef<void(__cdecl)(int)>(0x101BCD50); //UiCharSkillsWndRender
        widget.handleMessage = temple::GetRef<BOOL(__cdecl)(int, TigMsg*)>(0x101BD0C0); //UiCharSkillsWndMsg;
        widget.renderTooltip = 0;
        widget.childrenCount = 0;
        widget.field_28c = 0;
        //widget.field_288 = 0;
        //widget.windowId = 0;
        int result = uiManager->AddWindow(widget);
        if (result)
        {
            int uiCharSkillsWndScrollbarX = temple::GetRef<int>(0x10D19EB8);
            int uiCharSkillsWndScrollbarYdef = temple::GetRef<int>(0x10D19EBC);
            int uiCharSkillsWndScrollbarH = temple::GetRef<int>(0x10D19EC4);
    
            LgcyWindow* uiCharSkillsWnd = uiManager->GetWindow(result);;
            temple::GetRef<LgcyWindow*>(0x10D1A330) = uiCharSkillsWnd;
    
            LgcyScrollBar scrollbar;
            result = temple::GetRef<int(__cdecl)(LgcyScrollBar* widg, int x, int y, int height)>(0x101FA7B0)(&scrollbar, uiCharSkillsWndScrollbarX, uiCharSkillsWndScrollbarYdef, uiCharSkillsWndScrollbarH); //UiScrollbarInit
            if (!result)
            {
                scrollbar.yMax = 1;
                scrollbar.scrollQuantum = 1;
                scrollbar.field8C = 1;
                result = uiManager->AddScrollBar(scrollbar);
                if (result)
                {
                    result = uiManager->AddChild(uiCharSkillsWnd->widgetId, scrollbar.widgetId);
                    if (result)
                    {
                        LgcyScrollBar* uiCharSkillsScrollbar = uiManager->GetScrollBar(scrollbar.widgetId);
                        temple::GetRef<LgcyScrollBar*>(0x10D1A334) = uiCharSkillsScrollbar;
    
                        LgcyButton **uiCharSkillsBtns = temple::GetRef<LgcyButton*[]>(0x10D1A338);
    
                        int dword_10D19EC8 = temple::GetRef<int>(0x10D19EC8);
                        int dword_10D19ED4 = temple::GetRef<int>(0x10D19ED4);
                        int dword_10D19ECC = temple::GetRef<int>(0x10D19ECC);
                        int dword_10D19ED0 = temple::GetRef<int>(0x10D19ED0);
                        int v2 = 0, v1;
                        LgcyButton button;
                        while (1)
                        {
                            memcpy(button.name, "char_skills_ui_skill_button", 0x1Cu);
                            v1 = v2;
                            button.parentId = widget.widgetId;
                            button.x = dword_10D19EC8 + widget.x;
                            button.xrelated = dword_10D19EC8 + widget.x;
                            button.height = dword_10D19ED4;
                            button.y = widget.y + dword_10D19ECC + v2 * dword_10D19ED4;
                            button.yrelated = widget.y + dword_10D19ECC + v2 * dword_10D19ED4;
                            button.type = LgcyWidgetType::Button;
                            button.widgetId = -1;
                            button.flags = 0;
                            button.width = dword_10D19ED0;
                            button.render = temple::GetRef<void(__cdecl)(int)>(0x101BD850); //UiCharSkillsBtnRender;
                            button.handleMessage = temple::GetRef<BOOL(__cdecl)(int, TigMsg*)>(0x101BE050); //UiCharSkillsBtnMsg;
                            button.renderTooltip = temple::GetRef<void(__cdecl)(int x, int y, int* widgetId)>(0x101BDFC0); //UiCharSkillsBtnTooltip;;
                            button.buttonState = LgcyButtonState::Normal;
                            button.flags = 0;
                            button.field8C = -1;
                            button.field90 = -1;
                            button.field84 = -1;
                            button.field88 = -1;
                            result = uiManager->AddButton(button);
                            if (!result)
                                break;
                            result = uiManager->AddChild(uiCharSkillsWnd->widgetId, button.widgetId);
                            if (!result)
                                break;
                            LgcyButton* b = uiManager->GetButton(button.widgetId);
                            uiCharSkillsBtns[v2++] = b;
                            if (v1 + 1 >= 20)
                                return 0;
                        }
                    }
                }
            }
        }
        return result;
    }
    
    It works correctly, and does not crush on T+ exit))
    Next I will need to inject into uiCharSkillsBtns new instance of the array, which I will hold in the UiSkills class.

    The funny thing is, that both UiCharSkillsBtnRender and UiCharSkillsBtnTooltip identify to which skill button relates to based on such loop in Vanilla:
    Code:
      _idx = 0;
      while ( uiCharSkillsBtns[_idx]->widgetId != widgetId )
      {
        if ( ++_idx >= 20 )
        {
          idx = -1;
          goto LABEL_5;
        }
      }
      idx = _idx;
    
    Meaning that it will not see a button as a skill. So rewrite is needed as well.

    @Sitra Achara, @_doug_ is there a way to inject via asm here (cmp eax, 14h) a new size?
    Code:
    .text:101BD8D0                 loc_101BD8D0: ; CODE XREF: UiCharSkillsBtnRender+94j
    .text:101BD8D0 558 8B 0C 85 38+ mov     ecx, uiCharSkillsBtns[eax*4]
    .text:101BD8D7 558 39 59 0C     cmp     [ecx+0Ch], ebx
    .text:101BD8DA 558 0F 84 AE 02+ jz      loc_101BDB8E
    .text:101BD8E0 558 40           inc     eax
    .text:101BD8E1 558 83 F8 14     cmp     eax, 14h
    .text:101BD8E4 558 7C EA        jl      short loc_101BD8D0
    .text:101BD8E6 558 8B DF        mov     ebx, edi
    
     
  13. _doug_

    _doug_ Established Member

    Joined:
    Jul 9, 2009
    Messages:
    175
    Likes Received:
    48
    I think you may be able to use TempleFix::writeHex() to change that 14h to whatever it should be. I'd have to look into it a bit to figure out exactly what parameters to use.
     
  14. Sitra Achara

    Sitra Achara Senior Member

    Joined:
    Sep 1, 2003
    Messages:
    2,867
    Likes Received:
    148
    That's a little dirty hehe... remember that the decompilation sometimes produces bad flow. Like that while(1) loop, and those nested if's. (those checks aren't really necessary anyway)

    Also, for initing the UI widget objects, most of the time you can rely on the constructor rather than manually settings the fields.
    Check out ui_dialog.cpp UiDialogImpl::WidgetsInit for a more streamlined example.

    Lastly please rename:
    dword_10D19EC8 - uiCharSkillsBtnXoff
    dword_10D19ECC - uiCharSkillsBtnYoff
    dword_10D19ED0 - uiCharSkillsBtnW
    dword_10D19ED4 - uiCharSkillsBtnH

    These values are based on what's specified in data/art/interface/char_ui/char_skills_ui/7_char_skills_ui.mes.
     
    anatoliy likes this.
  15. anatoliy

    anatoliy Established Member

    Joined:
    Feb 18, 2017
    Messages:
    236
    Likes Received:
    75
    I agree, that the code should be polished, redundancies removed, loops improved and overall code readability.

    But this is incremental code refactor process. At least for me. First, copy code as is, with lines be as close to decompiled as they can get. It is easier to spot errors in such way.

    And I'd rather have intermid commit somewhere on the line.

    As for the improvements you mentioned Sitra, I will surely do them))

    Also, the sad truth is that UiCharSkillsBtnRender and UiCharSkillsBtnTooltip would have to be refactored as well :(
     
Our Host!