Skip to content

feat: proficiency#1593

Open
andreoam wants to merge 17 commits intoopentibiabr:mainfrom
andreoam:feat-proficiency
Open

feat: proficiency#1593
andreoam wants to merge 17 commits intoopentibiabr:mainfrom
andreoam:feat-proficiency

Conversation

@andreoam
Copy link
Contributor

@andreoam andreoam commented Jan 13, 2026

Description

This PR adds the Proficiency Module for Client 15.00+

Behavior

Actual

image

Type of change

  • New feature (non-breaking change which adds functionality)

Test Configuration:

  • Server Version: Canary

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I checked the PR checks reports
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is ​​effective or that my feature works

Summary by CodeRabbit

  • New Features
    • Full Weapon Proficiency system: track weapon XP, levels and perks.
    • Dedicated Proficiency window with item grid, progress, perk selection, filters and search.
    • Top-bar proficiency widget for quick status and one-click access.
    • Real-time updates when equipping items and gaining proficiency XP.
    • Client and scripting support to send proficiency actions and apply perk selections to the server.

@sonarqubecloud
Copy link

@Berzekz
Copy link

Berzekz commented Jan 13, 2026

Where is the credits to the real developers?

{47D4257D-4C56-4B57-B46D-EBF304C6EA1C} {9F41E269-1B31-4D78-9086-908645F1754F} {784705BD-E68C-4EB1-BA83-BB685ED524C2} {60D33572-CB20-49FB-BE67-3BE563AB9CE3}

@libergod
Copy link
Collaborator

Qual server tá usando para validar?

@andreoam
Copy link
Contributor Author

Qual server tá usando para validar?

Canary and crystal

@coderabbitai
Copy link

coderabbitai bot commented Jan 14, 2026

📝 Walkthrough

Walkthrough

Adds a complete Weapon Proficiency feature: new top-bar widget, full proficiency UI and data layers, inventory hooks, client protocol send/parse and Lua bindings, ThingType/protobuf proficiency flags, and C++/Lua plumbing to request/apply proficiencies and update the UI.

Changes

Cohort / File(s) Summary
Top-Bar UI & Statsbar
data/styles/30-statsbar.otui, modules/game_interface/widgets/statsbar.lua
Adds ProficiencyTopBarWidget to multiple top-bar variants (size 113x13, hidden by default); increases stats bar heights; adds StatsBar.initProficiencyTopBar() and invokes it on construct/start to conditionally show/refresh the top-bar.
Proficiency Core Module
modules/game_proficiency/proficiency.lua, modules/game_proficiency/proficiency.otmod, modules/game_proficiency/const.lua
New weapon proficiency module: lifecycle (init/terminate/onGameStart/onGameEnd), server event handlers, caching, request/apply flows, top-bar updates, public APIs, and otmod descriptor (autoloaded).
Proficiency UI & Data
modules/game_proficiency/proficiency.otui, modules/game_proficiency/proficiency_data.lua
Adds full OTUI window and widgets (ItemBox, StarWidget, Bonus panels, MainWindow) and ProficiencyData for JSON-driven lookups, icon/tooltip helpers, level/XP utilities, and weapon-to-proficiency resolution.
Inventory Integration
modules/game_inventory/inventory.lua
On equip (left slot) triggers optional g_game.sendWeaponProficiencyAction and calls modules.game_proficiency.updateTopBarProficiency() when available.
Client Game & Lua Bindings
src/client/game.h, src/client/game.cpp, src/client/luafunctions.cpp
Adds Game methods sendWeaponProficiencyAction, sendWeaponProficiencyApply, sendWeaponProficiencyApplyLua; exposes Lua bindings g_game.sendWeaponProficiencyAction and g_game.sendWeaponProficiencyApplyLua.
Protocol Send & Parse
src/client/protocolgamesend.cpp, src/client/protocolgame.h, src/client/protocolgameparse.cpp, src/client/protocolcodes.h
Introduces ClientWeaponProficiency opcode and protocol send methods for actions and applying perks (list of level/perkPosition pairs). Inlines parsing of proficiency info/experience in parseMessage/parseWeaponProficiencyInfo and invokes Lua callbacks with extracted data.
ThingType / Flags / Protobuf
src/client/const.h, src/client/thingtype.h, src/client/thingtype.cpp, src/protobuf/appearances.proto
Adds GameProficiency feature flag, ThingFlagAttrProficiency bit and ThingType::getProficiencyId(), and protobuf AppearanceFlagProficiency + AppearanceFlags.proficiency field.
Lua Value Casting
src/framework/luaengine/luavaluecasts.h
Adds std::pair push/parsing helpers to serialize/deserialize 2-element Lua tables as C++ pairs (used for perk (level, position) pairs).

Sequence Diagrams

sequenceDiagram
    participant UI as Proficiency UI
    participant Lua as modules.game_proficiency
    participant Game as Game (C++)
    participant Protocol as ProtocolGame
    participant Server as Server

    UI->>Lua: User clicks "Apply" with selected perks
    Lua->>Game: g_game.sendWeaponProficiencyApplyLua(itemId, levels, perkPositions)
    Game->>Protocol: sendWeaponProficiencyApply(itemId, pairedPerks)
    Protocol->>Server: ClientWeaponProficiency (action=3, itemId, perks[])
    Server-->>Protocol: WeaponProficiencyInfo
    Protocol->>Lua: onWeaponProficiency(itemId, experience, perks, marketCategory)
    Lua->>UI: refresh UI and top-bar progress
Loading
sequenceDiagram
    participant Inventory as Inventory Module
    participant Lua as modules.game_proficiency
    participant Game as Game (C++)
    participant Protocol as ProtocolGame
    participant StatsBar as StatsBar Widget

    Inventory->>Inventory: Equip weapon in left slot
    Inventory->>Lua: call updateTopBarProficiency()
    Lua->>Game: g_game.sendWeaponProficiencyAction(actionType, itemId)
    Game->>Protocol: sendWeaponProficiencyAction(actionType, itemId)
    Protocol->>Server: ClientWeaponProficiency (action=0/2)
    Server-->>Protocol: WeaponProficiencyExperience
    Protocol->>Lua: onWeaponProficiencyExperience(itemId, experience, hasUnusedPerk)
    Lua->>StatsBar: updateTopBarProficiency() -> update progress display
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Suggested reviewers

  • libergod
  • majestyotbr
  • InnerCircleTFS

Poem

🐰 I hopped through code with stars and perks,

A top-bar wink, a window that works.
Weapons learn as numbers climb,
Click, apply, and save that time.
Hop on — proficiency's prime!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.15% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: proficiency' is vague and overly generic, using non-descriptive terminology that does not clearly convey what the proficiency feature entails or its scope. Consider expanding the title to be more specific, such as 'feat: add weapon proficiency system' or 'feat: implement proficiency module for client 15.00+' to better communicate the primary change.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🤖 Fix all issues with AI agents
In `@modules/game_proficiency/const.lua`:
- Line 38: Rename the misspelled constant AUGMENT_CHAIN_LENGHT to
AUGMENT_CHAIN_LENGTH in modules/game_proficiency/const.lua and update every
usage/reference in the codebase to the new symbol; ensure the value (5) stays
the same and run tests or search to catch any remaining references to
AUGMENT_CHAIN_LENGHT so there are no undefined constant errors.

In `@modules/game_proficiency/proficiency.lua`:
- Around line 1009-1011: The variable cacheId is declared twice in the same
function (first at the earlier assignment and again around the later block),
causing redundancy and potential confusion; remove the second local declaration
and reuse the existing cacheId variable by replacing the later "local cacheId =
marketItem.originalId or itemId" with a simple assignment "cacheId =
marketItem.originalId or itemId" (or eliminate the later assignment entirely if
not needed), keeping references like self.selectedItemId and any subsequent
proficiency lookups using the original cacheId.

In `@src/client/protocolgameparse.cpp`:
- Around line 170-179: Wrap the Proto::GameServerWeaponProficiencyExperience
handler in a client-version check using g_game.getClientVersion() to avoid older
clients parsing the new payload; specifically, inside the case for
Proto::GameServerWeaponProficiencyExperience add: if (g_game.getClientVersion()
>= <SUMMER_UPDATE_2025_VERSION>) { /* existing reads: msg->getU16(),
msg->getU32(), msg->getU8() and
g_lua.callGlobalField("g_game","onWeaponProficiencyExperience",...) */ } so that
on older clients the message is ignored (no msg reads) and stream parsing stays
in sync—replace <SUMMER_UPDATE_2025_VERSION> with the actual client version
number for the Summer Update 2025 release.

In `@src/framework/luaengine/luavaluecasts.h`:
- Around line 605-623: The rawGeti calls pass arguments in the wrong order and
wrongly adjust negative stack indexes; swap the arguments so the first parameter
is the table stack-position and the second is the element key, and stop
subtracting 1 from negative indexes. Replace the two calls g_lua.rawGeti(index <
0 ? index - 1 : index, 1) and g_lua.rawGeti(index < 0 ? index - 1 : index, 2)
with g_lua.rawGeti(index, 1) and g_lua.rawGeti(index, 2) respectively (leaving
the surrounding luavalue_cast(-1, firstValue)/luavalue_cast(-1, secondValue),
pop(), and assignments to pair.first/pair.second unchanged).
🧹 Nitpick comments (13)
src/client/const.h (2)

471-471: Consider using English for code comments.

The comment // game proficiency apenas para 1510+ is in Portuguese. For consistency with the rest of the codebase and to help international contributors, consider translating to English: // game proficiency for 1510+ only.

Suggested change
-        GameProficiency = 19, // game proficiency apenas para 1510+
+        GameProficiency = 19, // game proficiency for 1510+ only

870-875: Minor indentation inconsistency.

Line 870 has inconsistent leading whitespace (3 spaces instead of 4). The addition of IMBUEMENT_WINDOW_SCROLL = 2 extends the enum correctly.

Suggested fix
-   enum Imbuement_Window_t : uint8_t
+    enum Imbuement_Window_t : uint8_t
modules/game_inventory/inventory.lua (1)

155-164: Consider documenting the magic number 0 in sendWeaponProficiencyAction.

The first parameter 0 passed to sendWeaponProficiencyAction(0, item:getId()) is unclear. Consider using a named constant or adding a comment to clarify its meaning (e.g., action type, request mode).

💡 Suggested improvement
             -- Request proficiency data for new weapon
             if item and g_game.sendWeaponProficiencyAction then
-                g_game.sendWeaponProficiencyAction(0, item:getId())
+                -- Action type 0 = request proficiency data
+                g_game.sendWeaponProficiencyAction(0, item:getId())
             end

Or define a constant in the proficiency module's const.lua file.

modules/game_proficiency/proficiency.otmod (1)

1-13: Website URL points to a different repository.

The website field points to https://github.com/mehah/otclient, but this PR is targeting opentibiabr/otclient. Consider updating the URL to match the target repository for consistency.

💡 Suggested fix
 Module
   name: game_proficiency
   description: Weapon Proficiency System
   author: OTClient Team
-  website: https://github.com/mehah/otclient
+  website: https://github.com/opentibiabr/otclient
   sandboxed: true
src/client/protocolgameparse.cpp (1)

6342-6370: Harden parseWeaponProficiencyInfo: reserve vector, use correct index types, and confirm marketData sentinel logic.

  • perksCount is uint8_t; using int i is minor but avoidable.
  • perks.reserve(perksCount) avoids reallocations.
  • !marketData.name.empty() as the only “market data exists” check could misclassify items if category is set but name is empty (depends on your MarketData invariants).
Proposed tweak
 const uint8_t perksCount = msg->getU8();
     
 std::vector<std::pair<uint8_t, uint8_t>> perks;
- for (int i = 0; i < perksCount; ++i) {
+ perks.reserve(perksCount);
+ for (size_t i = 0; i < perksCount; ++i) {
     const uint8_t level = msg->getU8();
     const uint8_t perkPosition = msg->getU8();
     perks.emplace_back(level, perkPosition);
 }
@@
     if (itemType) {
         const auto& marketData = itemType->getMarketData();
-        if (!marketData.name.empty()) {
+        // Verify the correct "market data is present" condition for your data model:
+        if (!marketData.name.empty()) {
             marketCategory = marketData.category;
         }
     }
modules/game_proficiency/proficiency.otui (2)

356-372: Consider using tr() for translatable dropdown options.

The ComboBox options are added with hardcoded English strings. For i18n consistency with other UI elements in this file (e.g., line 226 uses tr('Weapon Proficiency')), consider wrapping these options:

self:addOption(tr("Weapons: Axes"), nil, true)
-- etc.

470-481: Hardcoded warning text should use tr() for localization.

Line 479 contains a user-facing warning message that should be translatable:

-        text: Perks are inactive because you don't meet the requirements to use this weapon.
+        !text: tr("Perks are inactive because you don't meet the requirements to use this weapon.")
modules/game_proficiency/proficiency_data.lua (4)

54-89: Consider adding error logging for failed JSON parsing.

The pcall correctly catches parsing errors, but the error is silently discarded. Adding a log message would help debugging:

 if not status then
+    g_logger.error("Failed to parse proficiencies.json: " .. tostring(result))
     return false
 end

165-202: Partial match search may have performance implications with large proficiency datasets.

This loop iterates through all entries in nameIndex for each item lookup, checking two suffix patterns. For a large item catalog, this could become slow.

Consider:

  1. Building a reverse index during loadProficiencyJson that maps item name suffixes to proficiency IDs
  2. Caching resolved proficiency IDs per item after first lookup

However, if the proficiency count is small (< 100 entries), this is likely acceptable.


323-327: Redundant ternary pattern can be simplified.

 function ProficiencyData:getContentById(id)
     local content = self.content[id]
-    return content and content or nil
+    return content
 end

The expression content and content or nil is equivalent to just content.


486-493: Last resort fallback returns arbitrary proficiency ID.

The iteration for id, _ in pairs(self.content) returns an arbitrary entry since table iteration order in Lua is undefined. This could lead to inconsistent behavior.

Consider using a deterministic fallback (e.g., always return 6 or the lowest ID):

 -- Last resort: return first available proficiency ID
-for id, _ in pairs(self.content) do
-    return id
-end
+-- Always return consistent fallback
 
 return 6
modules/game_proficiency/proficiency.lua (2)

626-644: Indentation inconsistency suggests code formatting issue.

Lines 627-630 and 636-643 have inconsistent indentation that breaks the visual structure:

searchText.onTextChange = function(widget, text)
    WeaponProficiency.searchFilter = text
    WeaponProficiency:refreshItemList()
end  -- This should be indented properly
    end

This appears to work but makes the code harder to maintain. Consider reformatting for consistency.


120-154: Retry logic with scheduleEvent could lead to infinite recursion if StatsBar never loads.

The initTopBarProficiency function schedules itself recursively if conditions aren't met. While there's no explicit retry limit, the function will eventually succeed when the UI loads or fail silently if the game ends.

Consider adding a retry counter similar to autoSelectItem (line 494) to prevent potential issues:

local initRetries = 0
local maxRetries = 10

function initTopBarProficiency()
    if initRetries >= maxRetries then
        g_logger.warning("Failed to initialize proficiency top bar after max retries")
        return
    end
    initRetries = initRetries + 1
    -- ... rest of logic
end
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e8008af and dc3068b.

⛔ Files ignored due to path filters (113)
  • data/images/game/proficiency/augment-icons.png is excluded by !**/*.png
  • data/images/game/proficiency/backdrop_weaponmastery_highlight.png is excluded by !**/*.png
  • data/images/game/proficiency/big-icon-lock-grey.png is excluded by !**/*.png
  • data/images/game/proficiency/bonus-select-bg-progress.png is excluded by !**/*.png
  • data/images/game/proficiency/bonus-select-bg.png is excluded by !**/*.png
  • data/images/game/proficiency/border-weaponmasterytreeicons-active.png is excluded by !**/*.png
  • data/images/game/proficiency/border-weaponmasterytreeicons-inactive.png is excluded by !**/*.png
  • data/images/game/proficiency/full-bonus-select-bg.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-lock-grey.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-0.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-1-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-1-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-1.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-2-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-2-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-2.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-3-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-3-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-3.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-4-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-4-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-4.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-5-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-5-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-5.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-6-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-6-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-6.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-7-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-7-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-masterylevel-7.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-star-dark.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-star-tiny-gold.png is excluded by !**/*.png
  • data/images/game/proficiency/icon-star-tiny-silver.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-0.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-1.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-2.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-3.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-4.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-5.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-6.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-7.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-8.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-9-off.png is excluded by !**/*.png
  • data/images/game/proficiency/icons-9.png is excluded by !**/*.png
  • data/images/game/proficiency/proficiency-progress.png is excluded by !**/*.png
  • data/images/game/proficiency/progress-bg.png is excluded by !**/*.png
  • data/images/game/proficiency/star-progress-bg.png is excluded by !**/*.png
  • data/images/game/proficiency/star-progress.png is excluded by !**/*.png
  • data/images/game/topbar/backgroundTop.png is excluded by !**/*.png
  • data/images/game/topbar/boost-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/boost.png is excluded by !**/*.png
  • data/images/game/topbar/bottom-topbar.png is excluded by !**/*.png
  • data/images/game/topbar/compact/compact-health-cond-mana.png is excluded by !**/*.png
  • data/images/game/topbar/compact/large-container-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/compact/large-container.png is excluded by !**/*.png
  • data/images/game/topbar/compact/small-container-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/compact/small-container.png is excluded by !**/*.png
  • data/images/game/topbar/condition-topbar.png is excluded by !**/*.png
  • data/images/game/topbar/healthMana-topbar.png is excluded by !**/*.png
  • data/images/game/topbar/highlight-proficiency-button.png is excluded by !**/*.png
  • data/images/game/topbar/highlight-proficiency-large-button.png is excluded by !**/*.png
  • data/images/game/topbar/icon-combopoint-empty.png is excluded by !**/*.png
  • data/images/game/topbar/icon-combopoint-filled.png is excluded by !**/*.png
  • data/images/game/topbar/icon-proficiencytree-off.png is excluded by !**/*.png
  • data/images/game/topbar/icon-proficiencytree-on.png is excluded by !**/*.png
  • data/images/game/topbar/icon-serene-off.png is excluded by !**/*.png
  • data/images/game/topbar/icon-serene-on.png is excluded by !**/*.png
  • data/images/game/topbar/icons.png is excluded by !**/*.png
  • data/images/game/topbar/large/large-container-mana-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/large/large-container-mana.png is excluded by !**/*.png
  • data/images/game/topbar/large/large-container-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/large/large-container.png is excluded by !**/*.png
  • data/images/game/topbar/large/small-container-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/large/small-container.png is excluded by !**/*.png
  • data/images/game/topbar/left-bg.png is excluded by !**/*.png
  • data/images/game/topbar/marker_left.png is excluded by !**/*.png
  • data/images/game/topbar/marker_top.png is excluded by !**/*.png
  • data/images/game/topbar/parallel/parallel-health-mana-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/parallel/parallel-health-mana.png is excluded by !**/*.png
  • data/images/game/topbar/proficiency-bg.png is excluded by !**/*.png
  • data/images/game/topbar/proficiency-button.png is excluded by !**/*.png
  • data/images/game/topbar/proficiency-large-button.png is excluded by !**/*.png
  • data/images/game/topbar/proficiency-progress.png is excluded by !**/*.png
  • data/images/game/topbar/progress/mana-progressbar-large-100-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/mana-progressbar-large-100.png is excluded by !**/*.png
  • data/images/game/topbar/progress/manashield-progressbar-large-100-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/manashield-progressbar-large-100.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-10-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-10.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-100-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-100.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-30-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-30.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-4-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-4.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-60-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-60.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-95-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/progressbar-large-95.png is excluded by !**/*.png
  • data/images/game/topbar/progress/shieldmana-progressbar-large-100-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/shieldmana-progressbar-large-100.png is excluded by !**/*.png
  • data/images/game/topbar/progress/wide-mana-progressbar-large-100-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/wide-progressbar-large-10-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/wide-progressbar-large-100-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/wide-progressbar-large-30-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/wide-progressbar-large-4-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/wide-progressbar-large-60-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progress/wide-progressbar-large-95-vertical.png is excluded by !**/*.png
  • data/images/game/topbar/progressbar-large-100.png is excluded by !**/*.png
  • data/images/game/topbar/right-bg.png is excluded by !**/*.png
  • data/images/game/topbar/topBarBG.png is excluded by !**/*.png
  • data/images/options/button_proficiency.png is excluded by !**/*.png
📒 Files selected for processing (22)
  • data/json/SkillwheelStringsJsonLibrary.json
  • data/json/proficiencies.json
  • data/styles/30-statsbar.otui
  • modules/game_interface/widgets/statsbar.lua
  • modules/game_inventory/inventory.lua
  • modules/game_proficiency/const.lua
  • modules/game_proficiency/proficiency.lua
  • modules/game_proficiency/proficiency.otmod
  • modules/game_proficiency/proficiency.otui
  • modules/game_proficiency/proficiency_data.lua
  • src/client/const.h
  • src/client/game.cpp
  • src/client/game.h
  • src/client/luafunctions.cpp
  • src/client/protocolcodes.h
  • src/client/protocolgame.h
  • src/client/protocolgameparse.cpp
  • src/client/protocolgamesend.cpp
  • src/client/thingtype.cpp
  • src/client/thingtype.h
  • src/framework/luaengine/luavaluecasts.h
  • src/protobuf/appearances.proto
🧰 Additional context used
🧬 Code graph analysis (9)
src/framework/luaengine/luavaluecasts.h (2)
src/framework/luaengine/luavaluecasts.cpp (16)
  • push_luavalue (27-31)
  • push_luavalue (27-27)
  • push_luavalue (40-44)
  • push_luavalue (40-40)
  • push_luavalue (55-59)
  • push_luavalue (55-55)
  • push_luavalue (70-74)
  • push_luavalue (70-70)
  • push_luavalue (83-87)
  • push_luavalue (83-83)
  • push_luavalue (90-102)
  • push_luavalue (90-90)
  • push_luavalue (128-140)
  • push_luavalue (128-128)
  • push_luavalue (166-174)
  • push_luavalue (166-166)
src/framework/luaengine/luainterface.cpp (4)
  • isTable (1292-1296)
  • isTable (1292-1292)
  • rawGeti (1091-1095)
  • rawGeti (1091-1091)
src/client/game.h (2)
src/client/game.cpp (6)
  • sendWeaponProficiencyAction (1871-1877)
  • sendWeaponProficiencyAction (1871-1871)
  • sendWeaponProficiencyApply (1879-1885)
  • sendWeaponProficiencyApply (1879-1879)
  • sendWeaponProficiencyApplyLua (1887-1900)
  • sendWeaponProficiencyApplyLua (1887-1887)
src/client/protocolgamesend.cpp (4)
  • sendWeaponProficiencyAction (1592-1603)
  • sendWeaponProficiencyAction (1592-1592)
  • sendWeaponProficiencyApply (1605-1621)
  • sendWeaponProficiencyApply (1605-1605)
modules/game_interface/widgets/statsbar.lua (1)
meta.lua (1)
  • g_game.getClientVersion (1049-1049)
src/client/luafunctions.cpp (2)
src/client/game.cpp (4)
  • sendWeaponProficiencyAction (1871-1877)
  • sendWeaponProficiencyAction (1871-1871)
  • sendWeaponProficiencyApplyLua (1887-1900)
  • sendWeaponProficiencyApplyLua (1887-1887)
src/client/protocolgamesend.cpp (2)
  • sendWeaponProficiencyAction (1592-1603)
  • sendWeaponProficiencyAction (1592-1592)
src/client/game.cpp (2)
src/client/protocolgamesend.cpp (4)
  • sendWeaponProficiencyAction (1592-1603)
  • sendWeaponProficiencyAction (1592-1592)
  • sendWeaponProficiencyApply (1605-1621)
  • sendWeaponProficiencyApply (1605-1605)
src/client/game.h (1)
  • canPerformGameAction (337-340)
modules/game_proficiency/proficiency_data.lua (3)
meta.lua (2)
  • g_resources.fileExists (3711-3711)
  • g_resources.readFileContents (3752-3752)
modules/game_proficiency/proficiency.lua (1)
  • WeaponProficiency:createItemCache (659-728)
modules/corelib/table.lua (1)
  • table.size (162-169)
src/client/protocolgame.h (2)
src/client/game.cpp (4)
  • sendWeaponProficiencyAction (1871-1877)
  • sendWeaponProficiencyAction (1871-1871)
  • sendWeaponProficiencyApply (1879-1885)
  • sendWeaponProficiencyApply (1879-1879)
src/client/protocolgamesend.cpp (4)
  • sendWeaponProficiencyAction (1592-1603)
  • sendWeaponProficiencyAction (1592-1592)
  • sendWeaponProficiencyApply (1605-1621)
  • sendWeaponProficiencyApply (1605-1605)
src/client/thingtype.cpp (1)
src/client/tile.h (1)
  • m_flags (200-200)
modules/game_proficiency/proficiency.lua (1)
modules/game_proficiency/proficiency_data.lua (11)
  • ProficiencyData:getCurrentLevelByExp (713-732)
  • ProficiencyData:getLevelPercent (680-696)
  • ProficiencyData:getMaxExperienceByLevel (707-710)
  • ProficiencyData:getProficiencyIdForItem (374-493)
  • ProficiencyData:getPerkLaneCount (496-502)
  • ProficiencyData:getMaxExperience (673-677)
  • ProficiencyData:getTotalPercent (699-704)
  • ProficiencyData:getContentById (324-327)
  • ProficiencyData:getImageSourceAndClip (537-576)
  • ProficiencyData:getAugmentIconClip (637-643)
  • ProficiencyData:getBonusNameAndTooltip (579-634)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: ubuntu-24.04-linux-debug
  • GitHub Check: windows-2022-windows-release
  • GitHub Check: docker-build
🔇 Additional comments (26)
src/client/const.h (1)

1068-1068: LGTM!

The new ThingFlagAttrProficiency flag correctly uses static_cast<uint64_t> for the bit shift and follows the established pattern for high-bit flags.

src/protobuf/appearances.proto (1)

186-195: LGTM!

The protobuf changes correctly:

  • Remove tag 61 from the reserved set and assign it to the new proficiency field
  • Define AppearanceFlagProficiency message following the same pattern as similar flags (e.g., AppearanceFlagLenshelp)
  • Use optional for backward compatibility
src/framework/luaengine/luavaluecasts.h (1)

590-599: LGTM!

The push_luavalue implementation for std::pair correctly creates a 2-element array table with first at index 1 and second at index 2, following the same pattern as other container types.

modules/game_proficiency/const.lua (2)

1-56: LGTM!

The constants file is well-organized with clear categorization of perk types, augments, and experience tables. The ExperienceTable structure with different progression rates for regular, knight, and crossbow roles is a clean design.


324-333: Good defensive pattern for optional globals.

The UnknownCategories table correctly uses the or fallback pattern (WEAPON_AXE or 2) to handle cases where the weapon type constants might not be defined, ensuring the module remains functional even if those globals are missing.

src/client/protocolcodes.h (1)

325-325: LGTM! No conflicts found.

The new ClientWeaponProficiency = 179 opcode is correctly added to the ClientOpcodes enum. Opcode 179 is already referenced in protocolgamesend.cpp for weapon proficiency actions, and the client/server opcode namespaces are separate, so no conflict exists with GameServerCloseChannel = 179 in the GameServerOpcodes enum.

src/client/thingtype.h (1)

189-190: LGTM!

The new getProficiencyId() accessor and m_proficiencyId member follow the existing patterns in the class (e.g., getSkillWheelGemQualityId). The getter is const-correct, and the member is properly zero-initialized.

Also applies to: 263-263

modules/game_interface/widgets/statsbar.lua (3)

20-23: LGTM!

Height increase from 35 to 52 pixels for Large and Default dimensions accommodates the new proficiency widget in the stats bar.


512-513: LGTM!

Properly initializes the proficiency top bar after constructing the stats bar.


688-709: LGTM!

The initProficiencyTopBar() function is well-structured with appropriate defensive checks:

  • Null check for stats bar
  • Widget existence check before visibility toggle
  • Version gating (>= 1500) for the proficiency feature
  • Module and function existence check before calling updateTopBarProficiency()

The function is called from both constructStatsBar() and OnGameStart() to ensure proper initialization in all scenarios.

src/client/thingtype.cpp (1)

369-375: LGTM!

The proficiency flag handling correctly:

  1. Checks for the presence of the proficiency data with has_proficiency()
  2. Guards the feature with g_game.getFeature(Otc::GameProficiency)
  3. Sets the member and flag appropriately

Both ThingFlagAttrProficiency (defined in const.h:1068) and Otc::GameProficiency (defined in const.h:471) are properly defined. This follows the pattern used for other appearance flags in this function.

src/client/protocolgame.h (2)

152-153: LGTM!

The new method declarations are well-designed:

  • sendWeaponProficiencyAction appropriately uses a default parameter for itemId since it's only needed for specific action types (0 and 2).
  • sendWeaponProficiencyApply uses a clean pair structure for perk data.

Both signatures correctly match their implementations in protocolgamesend.cpp.


370-371: LGTM!

The parser method is correctly placed under the 15.x protocol section and follows the established pattern for message parsers.

src/client/game.h (1)

394-398: LGTM!

The weapon proficiency API follows the established pattern used by similar features (e.g., imbuing). The sendWeaponProficiencyApplyLua wrapper correctly adapts the pair-based API for Lua's table semantics.

One observation: the implementation uses std::min(levels.size(), perkPositions.size()) which silently truncates mismatched arrays. This is acceptable behavior, but consider whether a debug log would help diagnose Lua-side issues if callers accidentally pass arrays of different lengths.

src/client/luafunctions.cpp (1)

388-389: LGTM!

The Lua bindings are correctly set up:

  • sendWeaponProficiencyAction maps directly to the C++ method.
  • sendWeaponProficiencyApply correctly maps to sendWeaponProficiencyApplyLua, which provides a Lua-friendly interface using parallel arrays instead of std::pair. This appropriately hides the internal implementation from Lua callers.
src/client/protocolgamesend.cpp (1)

1592-1621: LGTM! Clean implementation of weapon proficiency protocol methods.

The new protocol methods follow the established patterns in this file. The conditional itemId serialization for specific action types and the perk array serialization are implemented correctly.

Minor observation: The comment on lines 1615-1616 documents the 0-indexed expectation clearly, which is helpful for maintenance.

data/styles/30-statsbar.otui (2)

1300-1372: Well-structured proficiency top bar widget.

The widget definition is clean with:

  • Proper anchor relationships between child elements
  • Defensive onClick handler that checks module existence
  • Clear visual hierarchy with button, background, progress bar, label, and icon
  • Default hidden state (visible: false) appropriate for version-gated features

509-515: The anchor ordering is not an issue. The OTUI parser supports forward references to elements defined later in the file. This pattern is used extensively throughout data/styles/30-statsbar.otui (at lines 406, 418, 512, 528, 588, 604, 625, 632, 659, 1066, 1081, 1095, 1255, 1268, 1281) without causing layout problems, confirming that anchors are resolved at runtime rather than parse time.

modules/game_proficiency/proficiency.otui (3)

1-6: LGTM! Clean base widget definitions.

The MiniStar and widget hierarchy are well-defined with appropriate sizing and image sources.


7-44: ItemBox widget properly configured for interactive item display.

Good use of:

  • change-cursor-image: true and cursor: pointer for UX feedback
  • tooltip-delayed: true to prevent tooltip spam
  • focusable: true with $focus state for keyboard navigation
  • phantom: true on child widgets to allow click-through

413-416: Dynamic ItemBox creation is appropriate for this use case.

Creating 45 ItemBox widgets at setup time is reasonable for the fixed-size grid. The naming convention widget_ + index allows easy lookup.

modules/game_proficiency/proficiency_data.lua (1)

750-774: Robust vocation restriction handling with multiple format support.

Good defensive coding:

  • Handles both table and number formats for restrictVocation
  • Provides fallback for environments without bit32
  • Clear comments explaining the bitmask logic
modules/game_proficiency/proficiency.lua (4)

260-296: Perk conversion logic handles edge cases well.

The 0-indexed to 1-indexed conversion (lines 276-277) and the filtering of invalid values (lines 274-275) are correctly implemented. The cache preservation logic (lines 284-296) for empty server responses is a good defensive measure.


1762-1778: Vocation filter bitmask calculation is correct but could use a helper.

The logic duplicates what's in proficiency_data.lua (lines 752-773). Consider extracting to a shared utility function to maintain DRY principle.

However, given this is filter logic vs. profession type determination, the duplication may be acceptable for clarity.


1897-1904: Perk array conversion correctly handles 1-indexed to 0-indexed transformation.

The conversion from Lua 1-indexed format to server 0-indexed format is correctly implemented:

  • levelIndex - 1 and perkIndex - 1 for the protocol
  • Sorting by level ensures consistent ordering

1852-1855: No action required; empty arrays are properly handled by the protocol implementation.

The Lua call g_game.sendWeaponProficiencyApply(self.selectedItemId, {}, {}) correctly passes two empty arrays to sendWeaponProficiencyApplyLua in src/client/game.cpp. The C++ implementation converts the parallel arrays to a vector of pairs using std::min(levels.size(), perkPositions.size()), which results in an empty perks vector when both input arrays are empty. The protocol message correctly encodes this as perksCount = 0, which is a valid message format that the server should handle.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +605 to +623
// Get first element (index 1 in Lua)
g_lua.rawGeti(index < 0 ? index - 1 : index, 1);
K firstValue;
if (!luavalue_cast(-1, firstValue)) {
g_lua.pop();
} else {
return false;
}
if (g_lua.next(index < 0 ? index - 1 : index)) {
V value;
if (!luavalue_cast(-1, value))
pair.second = value;
pair.first = firstValue;
g_lua.pop();

// Get second element (index 2 in Lua)
g_lua.rawGeti(index < 0 ? index - 1 : index, 2);
V secondValue;
if (!luavalue_cast(-1, secondValue)) {
g_lua.pop();
} else {
return false;
}
pair.second = secondValue;
g_lua.pop();
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Arguments to rawGeti are swapped.

The rawGeti function signature is rawGeti(int n, int index) where n is the table key and index is the stack position. The current code passes these in the wrong order and also applies an incorrect stack adjustment.

Current behavior with index = -1:

  • rawGeti(index < 0 ? index - 1 : index, 1) becomes rawGeti(-2, 1) — this tries to get element -2 from table at stack position 1, which is incorrect.
Proposed fix
     if (g_lua.isTable(index)) {
         // Get first element (index 1 in Lua)
-        g_lua.rawGeti(index < 0 ? index - 1 : index, 1);
+        g_lua.rawGeti(1, index);
         K firstValue;
         if (!luavalue_cast(-1, firstValue)) {
             g_lua.pop();
             return false;
         }
         pair.first = firstValue;
         g_lua.pop();
         
         // Get second element (index 2 in Lua)
-        g_lua.rawGeti(index < 0 ? index - 1 : index, 2);
+        g_lua.rawGeti(2, index);
         V secondValue;
         if (!luavalue_cast(-1, secondValue)) {
             g_lua.pop();
             return false;
         }
         pair.second = secondValue;
         g_lua.pop();
🤖 Prompt for AI Agents
In `@src/framework/luaengine/luavaluecasts.h` around lines 605 - 623, The rawGeti
calls pass arguments in the wrong order and wrongly adjust negative stack
indexes; swap the arguments so the first parameter is the table stack-position
and the second is the element key, and stop subtracting 1 from negative indexes.
Replace the two calls g_lua.rawGeti(index < 0 ? index - 1 : index, 1) and
g_lua.rawGeti(index < 0 ? index - 1 : index, 2) with g_lua.rawGeti(index, 1) and
g_lua.rawGeti(index, 2) respectively (leaving the surrounding luavalue_cast(-1,
firstValue)/luavalue_cast(-1, secondValue), pop(), and assignments to
pair.first/pair.second unchanged).

@dudantas dudantas changed the title Feat: proficiency feat: proficiency Jan 14, 2026
@matzinhozz
Copy link

working fine for me.

bool luavalue_cast(int index, std::unordered_map<K, V, H>& map);

// pair
template<class K, class V>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary?

int push_luavalue(const std::pair<K, V>& pair)
{
g_lua.createTable(2, 0);
push_internal_luavalue(pair.first);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary?

K value;
if (!luavalue_cast(-1, value))
pair.first = value;
// Get first element (index 1 in Lua)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary?

matzinhozz added a commit to matzinhozz/otclient that referenced this pull request Feb 1, 2026
andreoam and others added 3 commits February 11, 2026 00:01
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🤖 Fix all issues with AI agents
In `@modules/game_proficiency/proficiency.lua`:
- Around line 1817-1824: The pcall around self:applyPendingSelections() swallows
errors (variable err) without logging; modify the error path in the block (and
similarly in onOkClick) to log the error before clearing pendingSelections and
calling updateApplyButtonState — include the err and a stack traceback (or
tostring(err)) for context and use the module's existing logging mechanism (or
print/warn if none) so failures in applyPendingSelections are visible while
still clearing pendingSelections and updating the button state.
- Around line 955-958: In refreshItemList the cache key uses "local cacheId =
marketItem.marketData.showAs or displayId" which treats showAs==0 as false and
yields 0; change the logic to explicitly test for nil (e.g. if
marketItem.marketData and marketItem.marketData.showAs ~= nil then cacheId =
marketItem.marketData.showAs else cacheId = displayId end) so that a 0 showAs
uses displayId correctly; update the code around cacheId, displayId and
marketItem.marketData.showAs (the same block that calls itemWidget:setItemId) to
use the nil-check instead of Lua's falsy or pattern.
- Around line 735-741: The sort key construction inside the table.sort
comparator uses "a.marketData.showAs or a.displayId or a.originalId" which
treats showAs==0 as truthy and causes cache lookup mismatches; update the
idA/idB selection in the comparator (the table.sort anonymous function) to
explicitly treat showAs==0 as invalid (e.g. check for nil and nonzero) and fall
back to displayId or originalId so WeaponProficiency.cacheList is looked up
using the actual originalId keys populated by onWeaponProficiency/selectItem.
- Around line 1756-1771: Replace the fragile math.pow usage and handle
playerVocation==0: ensure playerVocation is >=1 (skip or continue if not), then
compute an integer bit mask instead of a float for vocBit; when bit32 is
available use bit32.lshift(1, playerVocation - 1) to produce an integer mask,
otherwise build the mask with integer multiplication (local vocBit = 1; for i=1,
playerVocation-1 do vocBit = vocBit * 2 end) and use that vocBit in the
bit32.band or modulo/shift checks on restrictVocation (variables:
restrictVocation, playerVocation, vocBit, filteredItems, bit32).
- Around line 627-644: The nested closures and if-blocks around
searchText.onTextChange and clearButton.onClick have inconsistent indentation
that obscures which "end" corresponds to which block; re-indent the block
containing searchText.onTextChange (the function assigned to
searchText.onTextChange), its enclosing if that checks for searchText, and the
clearButton.onClick function and its enclosing if so that each function body and
its corresponding "end" align visually with their opening statement (e.g., align
the "end" for searchText.onTextChange directly under the "function(widget,
text)" line, the "end" for the if that found searchText under that if, and
similarly align clearButton.onClick's function end and the if that found
clearButton); ensure WeaponProficiency:refreshItemList(),
WeaponProficiency.searchFilter assignments, and searchWidget:setText('') remain
inside the correct closures (searchText.onTextChange and clearButton.onClick
respectively) and close the blocks in the order: function end, enclosing if end,
then outer scopes.
- Around line 120-154: initTopBarProficiency currently reschedules itself
forever via scheduleEvent when modules.game_interface.StatsBar or statsBar is
nil; add a bounded retry counter (e.g., maxRetries = 15) to the function so
repeated calls increment an attempts parameter and stop rescheduling after the
cap to avoid an infinite loop. Modify initTopBarProficiency to accept an
optional attempts argument (default 0), increment it on each reschedule, and
only call scheduleEvent(initTopBarProficiency, 500, attempts+1) (or equivalent)
when attempts < maxRetries; apply this check in both places that currently call
scheduleEvent (the StatsBarModule nil branch and the statsBar nil branch) and
ensure normal behavior continues when the module becomes available.

In `@src/client/protocolgameparse.cpp`:
- Around line 6340-6369: parseWeaponProficiencyInfo currently parses an opcode
unguarded which can desync older clients; add the same version guard used for
GameServerWeaponProficiencyExperience by checking getClientVersion() >= 1510
before parsing or calling parseWeaponProficiencyInfo (or bail/ignore the
opcode), and apply that same check at the opcode dispatch site that invokes
parseWeaponProficiencyInfo; also replace the magic default 32 with a named
constant (e.g., constexpr uint16_t MarketCategoryWeaponsAll = 32) and use that
constant when initializing marketCategory to improve clarity; keep references to
parseWeaponProficiencyInfo, GameServerWeaponProficiencyExperience,
getClientVersion(), and g_things when making the changes.
🧹 Nitpick comments (5)
modules/game_proficiency/proficiency.lua (5)

4-6: __index assignment is unused — no setmetatable call exists.

WeaponProficiency.__index = WeaponProficiency on line 6 has no effect because WeaponProficiency is never used as a metatable via setmetatable. All methods are called directly on the table (e.g., WeaponProficiency:selectItem(...) or self:refreshItemList()), so the __index field is dead code.


1779-1813: applyOneHandedFilter and applyTwoHandedFilter are dead code.

These two functions are never called. The 1H/2H filtering is handled inline inside refreshItemList (lines 884–915), which also covers the "both active" case. Consider removing these unused functions to avoid maintenance confusion.


1417-1418: Redundant re-declaration of iconPerks and iconPerksGrey.

These locals were already declared on lines 1385–1386 with identical getChildById calls. The second local declarations shadow the first and waste a widget lookup. Remove lines 1417–1418 and reuse the existing variables.

Proposed fix
             -- Handle augment overlay icons visibility for spell augments
-            local iconPerks = bonusIcon:getChildById('iconPerks')
-            local iconPerksGrey = bonusIcon:getChildById('iconPerks-grey')
             if perkData.Type == PERK_SPELL_AUGMENT then

1116-1119: Server request fires on every item selection — consider debouncing.

sendWeaponProficiencyAction is called every time an item is clicked (line 1118). Rapid clicks through the item list could burst many requests. A simple debounce (e.g., cancel a previously scheduled event and re-schedule with a short delay) would reduce unnecessary traffic.


333-337: Re-sorting all categories on every experience tick may be expensive during active combat.

Each call to onWeaponProficiencyExperience triggers table.sort on all 7 category lists. If the server sends frequent XP updates, consider deferring the re-sort (e.g., mark categories dirty and sort lazily when the list is next displayed).

Comment on lines +120 to +154
function initTopBarProficiency()
-- Delay initialization to ensure StatsBar is fully loaded
scheduleEvent(function()
-- Access StatsBar through modules.game_interface
local StatsBarModule = modules.game_interface and modules.game_interface.StatsBar
if not StatsBarModule then
scheduleEvent(initTopBarProficiency, 500)
return
end

local statsBar = StatsBarModule.getCurrentStatsBarWithPosition and StatsBarModule.getCurrentStatsBarWithPosition()
if statsBar then
local profWidget = statsBar:recursiveGetChildById('proficiencyTopBar')
if profWidget then
local shouldShow = g_game.getClientVersion() >= 1500
profWidget:setVisible(shouldShow)

if shouldShow then
-- Request proficiency data for equipped weapon
local player = g_game.getLocalPlayer()
if player then
local leftSlotItem = player:getInventoryItem(InventorySlotLeft)
if leftSlotItem and g_game.sendWeaponProficiencyAction then
local itemId = leftSlotItem:getId()
g_game.sendWeaponProficiencyAction(0, itemId)
end
end
updateTopBarProficiency()
end
end
else
scheduleEvent(initTopBarProficiency, 500)
end
end, 500) -- 500ms delay
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Unbounded retry loop in initTopBarProficiency can schedule events indefinitely.

If StatsBarModule is nil (line 126) or statsBar is nil (line 151), the function re-schedules itself every 500 ms with no retry limit. If the StatsBar module fails to load or is disabled, this runs forever.

Add a retry counter with a reasonable cap (e.g., 10–20 attempts).

Proposed fix
-function initTopBarProficiency()
-    -- Delay initialization to ensure StatsBar is fully loaded
-    scheduleEvent(function()
+function initTopBarProficiency(retryCount)
+    retryCount = retryCount or 0
+    if retryCount >= 15 then return end
+    scheduleEvent(function()
         -- Access StatsBar through modules.game_interface
         local StatsBarModule = modules.game_interface and modules.game_interface.StatsBar
         if not StatsBarModule then 
-            scheduleEvent(initTopBarProficiency, 500)
+            initTopBarProficiency(retryCount + 1)
             return 
         end
         
         local statsBar = StatsBarModule.getCurrentStatsBarWithPosition and StatsBarModule.getCurrentStatsBarWithPosition()
         if statsBar then
             -- ... existing logic ...
         else
-            scheduleEvent(initTopBarProficiency, 500)
+            initTopBarProficiency(retryCount + 1)
         end
     end, 500)
 end
🤖 Prompt for AI Agents
In `@modules/game_proficiency/proficiency.lua` around lines 120 - 154,
initTopBarProficiency currently reschedules itself forever via scheduleEvent
when modules.game_interface.StatsBar or statsBar is nil; add a bounded retry
counter (e.g., maxRetries = 15) to the function so repeated calls increment an
attempts parameter and stop rescheduling after the cap to avoid an infinite
loop. Modify initTopBarProficiency to accept an optional attempts argument
(default 0), increment it on each reschedule, and only call
scheduleEvent(initTopBarProficiency, 500, attempts+1) (or equivalent) when
attempts < maxRetries; apply this check in both places that currently call
scheduleEvent (the StatsBarModule nil branch and the statsBar nil branch) and
ensure normal behavior continues when the module becomes available.

Comment on lines +627 to +644
searchText.onTextChange = function(widget, text)
WeaponProficiency.searchFilter = text
WeaponProficiency:refreshItemList()
end
end

-- Setup clear search button
local clearButton = WeaponProficiency.window:recursiveGetChildById('clearSearchButton')
if clearButton then
clearButton.onClick = function()
local searchWidget = WeaponProficiency.window:recursiveGetChildById('searchText')
if searchWidget then
searchWidget:setText('')
WeaponProficiency.searchFilter = nil
WeaponProficiency:refreshItemList()
end
end
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Misleading indentation in nested closures obscures block structure.

The end keywords for the onTextChange closure (lines 630), its enclosing if (line 631), the onClick closure (line 643), and the outer if blocks (lines 642, 644) are indented inconsistently, making it hard to see which end closes which block.

Proposed fix
     if searchText then
         searchText.onTextChange = function(widget, text)
             WeaponProficiency.searchFilter = text
-    WeaponProficiency:refreshItemList()
-end
+            WeaponProficiency:refreshItemList()
+        end
     end
     
     -- Setup clear search button
     local clearButton = WeaponProficiency.window:recursiveGetChildById('clearSearchButton')
     if clearButton then
         clearButton.onClick = function()
             local searchWidget = WeaponProficiency.window:recursiveGetChildById('searchText')
             if searchWidget then
                 searchWidget:setText('')
                 WeaponProficiency.searchFilter = nil
                 WeaponProficiency:refreshItemList()
-        end
-    end
-end
+            end
+        end
+    end
🤖 Prompt for AI Agents
In `@modules/game_proficiency/proficiency.lua` around lines 627 - 644, The nested
closures and if-blocks around searchText.onTextChange and clearButton.onClick
have inconsistent indentation that obscures which "end" corresponds to which
block; re-indent the block containing searchText.onTextChange (the function
assigned to searchText.onTextChange), its enclosing if that checks for
searchText, and the clearButton.onClick function and its enclosing if so that
each function body and its corresponding "end" align visually with their opening
statement (e.g., align the "end" for searchText.onTextChange directly under the
"function(widget, text)" line, the "end" for the if that found searchText under
that if, and similarly align clearButton.onClick's function end and the if that
found clearButton); ensure WeaponProficiency:refreshItemList(),
WeaponProficiency.searchFilter assignments, and searchWidget:setText('') remain
inside the correct closures (searchText.onTextChange and clearButton.onClick
respectively) and close the blocks in the order: function end, enclosing if end,
then outer scopes.

Comment on lines +735 to +741
table.sort(itemList, function(a, b)
-- Use showAs (marketData.showAs) for cache lookup - this is what the server uses
local idA = a.marketData.showAs or a.displayId or a.originalId
local idB = b.marketData.showAs or b.displayId or b.originalId

local expA = WeaponProficiency.cacheList[idA] and WeaponProficiency.cacheList[idA].exp or 0
local expB = WeaponProficiency.cacheList[idB] and WeaponProficiency.cacheList[idB].exp or 0
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Cache key mismatch in sort: marketData.showAs can be 0 (truthy in Lua), producing a wrong lookup.

In Lua, 0 is truthy, so a.marketData.showAs or a.displayId or a.originalId evaluates to 0 when showAs is 0 — not to a.displayId as likely intended. The proficiency cache is keyed by originalId (set in onWeaponProficiency and selectItem), so items with showAs == 0 won't find their cache entry, and their experience will appear as 0 during sorting.

Proposed fix — guard against zero showAs
-        local idA = a.marketData.showAs or a.displayId or a.originalId
-        local idB = b.marketData.showAs or b.displayId or b.originalId
+        local showAsA = a.marketData.showAs
+        local idA = (showAsA and showAsA ~= 0) and showAsA or a.displayId or a.originalId
+        local showAsB = b.marketData.showAs
+        local idB = (showAsB and showAsB ~= 0) and showAsB or b.displayId or b.originalId
🤖 Prompt for AI Agents
In `@modules/game_proficiency/proficiency.lua` around lines 735 - 741, The sort
key construction inside the table.sort comparator uses "a.marketData.showAs or
a.displayId or a.originalId" which treats showAs==0 as truthy and causes cache
lookup mismatches; update the idA/idB selection in the comparator (the
table.sort anonymous function) to explicitly treat showAs==0 as invalid (e.g.
check for nil and nonzero) and fall back to displayId or originalId so
WeaponProficiency.cacheList is looked up using the actual originalId keys
populated by onWeaponProficiency/selectItem.

Comment on lines +955 to +958
local displayId = marketItem.displayId or marketItem.originalId
-- Use showAs (displayId) as cache key - this is what the server uses
local cacheId = marketItem.marketData.showAs or displayId
itemWidget:setItemId(displayId)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Same showAs == 0 truthy issue for cache key in refreshItemList.

Line 957 has the same pattern: local cacheId = marketItem.marketData.showAs or displayId. When showAs is 0, cacheId becomes 0 instead of displayId, causing a cache miss and showing 0 stars for the item.

Proposed fix
-                local cacheId = marketItem.marketData.showAs or displayId
+                local showAs = marketItem.marketData.showAs
+                local cacheId = (showAs and showAs ~= 0) and showAs or displayId
🤖 Prompt for AI Agents
In `@modules/game_proficiency/proficiency.lua` around lines 955 - 958, In
refreshItemList the cache key uses "local cacheId = marketItem.marketData.showAs
or displayId" which treats showAs==0 as false and yields 0; change the logic to
explicitly test for nil (e.g. if marketItem.marketData and
marketItem.marketData.showAs ~= nil then cacheId = marketItem.marketData.showAs
else cacheId = displayId end) so that a 0 showAs uses displayId correctly;
update the code around cacheId, displayId and marketItem.marketData.showAs (the
same block that calls itemWidget:setItemId) to use the nil-check instead of
Lua's falsy or pattern.

Comment on lines +1756 to +1771
-- restrictVocation is a bitmask: bit N is set if vocation N can use the item
-- playerVocation is 1-based (1=Knight, 2=Paladin, etc.)
-- The bitmask uses 2^(vocation-1) for each vocation
local vocBit = math.pow(2, playerVocation - 1)
if bit32 then
-- Use bit32 library if available
if bit32.band(restrictVocation, vocBit) ~= 0 then
table.insert(filteredItems, item)
end
else
-- Fallback: use modulo arithmetic for bitwise AND
local shifted = math.floor(restrictVocation / vocBit)
if shifted % 2 == 1 then
table.insert(filteredItems, item)
end
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

math.pow returns a float, risking incorrect bitwise comparisons; edge case when playerVocation == 0.

math.pow(2, playerVocation - 1) produces a double. When playerVocation is 0, vocBit becomes 0.5, and the subsequent bit32.band / modulo check silently produces wrong results. Even for valid vocations, relying on float-to-int coercion inside bit32.band is fragile.

Proposed fix
+            if playerVocation <= 0 then
+                -- No vocation - can't filter, include item
+                table.insert(filteredItems, item)
+            else
-            local vocBit = math.pow(2, playerVocation - 1)
-            if bit32 then
-                -- Use bit32 library if available
-                if bit32.band(restrictVocation, vocBit) ~= 0 then
+            local vocBit = bit32 and bit32.lshift(1, playerVocation - 1) or (2 ^ (playerVocation - 1))
+            if bit32 then
+                if bit32.band(restrictVocation, vocBit) ~= 0 then
                     table.insert(filteredItems, item)
                 end
-            else
-                -- Fallback: use modulo arithmetic for bitwise AND
-                local shifted = math.floor(restrictVocation / vocBit)
+            else
+                local shifted = math.floor(restrictVocation / vocBit)
                 if shifted % 2 == 1 then
                     table.insert(filteredItems, item)
                 end
             end
+            end
🤖 Prompt for AI Agents
In `@modules/game_proficiency/proficiency.lua` around lines 1756 - 1771, Replace
the fragile math.pow usage and handle playerVocation==0: ensure playerVocation
is >=1 (skip or continue if not), then compute an integer bit mask instead of a
float for vocBit; when bit32 is available use bit32.lshift(1, playerVocation -
1) to produce an integer mask, otherwise build the mask with integer
multiplication (local vocBit = 1; for i=1, playerVocation-1 do vocBit = vocBit *
2 end) and use that vocBit in the bit32.band or modulo/shift checks on
restrictVocation (variables: restrictVocation, playerVocation, vocBit,
filteredItems, bit32).

Comment on lines +1817 to +1824
local success, err = pcall(function()
self:applyPendingSelections()
end)
if not success then
-- Clear pending selections on error to allow closing
self.pendingSelections = {}
self:updateApplyButtonState()
end
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Errors from applyPendingSelections are silently swallowed.

The err value returned by pcall is captured but never logged. If applyPendingSelections fails, the error is lost, making debugging difficult. The same pattern exists in onOkClick (line 1831).

Proposed fix
     local success, err = pcall(function()
         self:applyPendingSelections()
     end)
     if not success then
+        g_logger.error("Failed to apply proficiency selections: " .. tostring(err))
         -- Clear pending selections on error to allow closing
         self.pendingSelections = {}
         self:updateApplyButtonState()
     end
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
local success, err = pcall(function()
self:applyPendingSelections()
end)
if not success then
-- Clear pending selections on error to allow closing
self.pendingSelections = {}
self:updateApplyButtonState()
end
local success, err = pcall(function()
self:applyPendingSelections()
end)
if not success then
g_logger.error("Failed to apply proficiency selections: " .. tostring(err))
-- Clear pending selections on error to allow closing
self.pendingSelections = {}
self:updateApplyButtonState()
end
🤖 Prompt for AI Agents
In `@modules/game_proficiency/proficiency.lua` around lines 1817 - 1824, The pcall
around self:applyPendingSelections() swallows errors (variable err) without
logging; modify the error path in the block (and similarly in onOkClick) to log
the error before clearing pendingSelections and calling updateApplyButtonState —
include the err and a stack traceback (or tostring(err)) for context and use the
module's existing logging mechanism (or print/warn if none) so failures in
applyPendingSelections are visible while still clearing pendingSelections and
updating the button state.

Comment on lines 6340 to +6369
void ProtocolGame::parseWeaponProficiencyInfo(const InputMessagePtr& msg)
{
msg->getU16(); // itemId
msg->getU32(); // experience

const uint8_t size = msg->getU8();
for (auto j = 0; j < size; ++j) {
msg->getU8(); // proficiencyLevel
msg->getU8(); // perkPosition
// Opcode 0xC4 (196) - Weapon Proficiency Info
// Sent by server in response to sendWeaponProficiencyAction
// Structure: uint16 itemId, uint32 experience, uint8 perksCount, [perksCount * {uint8 level, uint8 perkPosition}]

const uint16_t itemId = msg->getU16();
const uint32_t experience = msg->getU32();
const uint8_t perksCount = msg->getU8();

std::vector<std::pair<uint8_t, uint8_t>> perks;
for (int i = 0; i < perksCount; ++i) {
const uint8_t level = msg->getU8();
const uint8_t perkPosition = msg->getU8();
perks.emplace_back(level, perkPosition);
}

// Get market category for the item (for sorting in UI)
uint16_t marketCategory = 32; // Default: WeaponsAll
if (g_things.isValidDatId(itemId, ThingCategoryItem)) {
const auto& itemType = g_things.getThingType(itemId, ThingCategoryItem);
if (itemType) {
const auto& marketData = itemType->getMarketData();
if (!marketData.name.empty()) {
marketCategory = marketData.category;
}
}
}

g_lua.callGlobalField("g_game", "onWeaponProficiency", itemId, experience, perks, marketCategory);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Missing version gating in parseWeaponProficiencyInfo, inconsistent with the experience handler.

GameServerWeaponProficiencyExperience (Line 173) is gated behind getClientVersion() >= 1510, but parseWeaponProficiencyInfo and its dispatch site (Line 482) have no version check. If an older client somehow receives this opcode, the differing payload structure will cause a stream desync. For consistency and defensive correctness, consider adding the same guard here.

Also, the magic number 32 (Line 6358) for the default market category should ideally be a named constant for clarity.

Suggested version gate and named constant
 void ProtocolGame::parseWeaponProficiencyInfo(const InputMessagePtr& msg)
 {
+    if (g_game.getClientVersion() < 1510) {
+        return;
+    }
+
     // Opcode 0xC4 (196) - Weapon Proficiency Info
-    uint16_t marketCategory = 32; // Default: WeaponsAll
+    static constexpr uint16_t kDefaultMarketCategoryWeaponsAll = 32;
+    uint16_t marketCategory = kDefaultMarketCategoryWeaponsAll;
🤖 Prompt for AI Agents
In `@src/client/protocolgameparse.cpp` around lines 6340 - 6369,
parseWeaponProficiencyInfo currently parses an opcode unguarded which can desync
older clients; add the same version guard used for
GameServerWeaponProficiencyExperience by checking getClientVersion() >= 1510
before parsing or calling parseWeaponProficiencyInfo (or bail/ignore the
opcode), and apply that same check at the opcode dispatch site that invokes
parseWeaponProficiencyInfo; also replace the magic default 32 with a named
constant (e.g., constexpr uint16_t MarketCategoryWeaponsAll = 32) and use that
constant when initializing marketCategory to improve clarity; keep references to
parseWeaponProficiencyInfo, GameServerWeaponProficiencyExperience,
getClientVersion(), and g_things when making the changes.

@libergod
Copy link
Collaborator

Adding this to my TODO list this week. Sorry for the delay in testing. Expect modifications.

@libergod libergod self-assigned this Feb 17, 2026
@libergod libergod requested review from libergod and removed request for InnerCircleTFS February 17, 2026 19:00
@andreoam
Copy link
Contributor Author

@coderabbitai review

@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@kokekanon

This comment was marked as outdated.

@andreoam
Copy link
Contributor Author

ERROR: Unable to load texture '/images/ui/erase-button': unable to open file '/images/ui/erase-button.png': not found
ERROR: failed to load UI from 'weapon_proficiency': 'CloseButton' is not a defined style
ERROR: Lua exception: /game_proficiency/proficiency.lua:55: attempt to index field 'window' (a nil value)
stack traceback:
    [C]: in function '__index'
    /game_proficiency/proficiency.lua:55: in function 'init'
    /game_proficiency/weapon_proficiency.otmod:5:[@onLoad]:1: in main chunk
    [C]: in function 'autoLoadModules'
    /init.lua:160: in function 'loadModules'
    /init.lua:179: in main chunk
ERROR: Unable to load module 'game_proficiency': LUA ERROR:
/game_proficiency/proficiency.lua:55: attempt to index field 'window' (a nil value)
stack traceback:
    [C]: in function '__index'
    /game_proficiency/proficiency.lua:55: in function 'init'
    /game_proficiency/weapon_proficiency.otmod:5:[@onLoad]:1: in main chunk
    [C]: in function 'autoLoadModules'
    /init.lua:160: in function 'loadModules'
    /init.lua:179: in main chunk

Acho que voce esta utilizando outro modulo. Nao existe nenhum arquivo chamado weapon_proficiency.lua nesse PR

@kokekanon
Copy link
Contributor

You’re right, I had an old module in the mods folder. My apologies.

matzinhozz added a commit to matzinhozz/otclient that referenced this pull request Mar 16, 2026
nalejmizupy added a commit to nalejmizupy/otclient that referenced this pull request Mar 21, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants