Skip to content

feat: imbuement system#1616

Open
andreoam wants to merge 10 commits intoopentibiabr:mainfrom
andreoam:feat-Imbue-15+/Fix-imbue-1412
Open

feat: imbuement system#1616
andreoam wants to merge 10 commits intoopentibiabr:mainfrom
andreoam:feat-Imbue-15+/Fix-imbue-1412

Conversation

@andreoam
Copy link
Contributor

@andreoam andreoam commented Jan 26, 2026

Description

Nessa pr foi adicionado sistema de imbui para o protocolo 15+. Imbue scroll funcionando 100%
Eu tambem deixei o imbue do cliente 14 com a aparência semelhante ao cliente oficial

Behavior

Actual

Imagens do antigo cliente 14

imbui 14 before imbui 14 before_2

Expected

Modificação para versão 14

imbue.14.after.mp4

Modificação para versão 15

imbue.15.mp4

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How Has This Been Tested

o arquivo protocolgameparse eu fiz bem "porco" apenas para o sistema da versao 15 funcionar, então, para os programadores de plantão que poderem ajudar a melhora-lo, fiquem a vontade

Test Configuration:

  • Server Version: Canary server para versão 14 e Crystal server para versao 15
  • Operating System: Windows

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

    • Imbuement Shrine UI: item/scroll selection, imbuement/clear flows, confirmation dialogs, timers, and cost display
    • New imbuement modules: selection, item/scroll handlers, and imbuement management
    • New progress bar widgets and vertical/inverted variants for status and timers
    • Exposed client APIs and protocol support for imbuement interactions
  • UX Improvements

    • Resource requirement tooltips, formatted balance display, and conditional enablement of imbue actions

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 26, 2026

📝 Walkthrough

Walkthrough

Adds a complete imbuement UI subsystem, new SD/inverted progress bar widgets and styles, client-game bindings and protocol support for imbuement window actions, and multiple Lua controllers to drive item/scroll selection, imbuement application and clearing.

Changes

Cohort / File(s) Summary
Progress bar styles
data/styles/10-progressbars.otui
Added ThickProgressBar, HealthBar, ManaBar, multiple SD/inverted/vertical ProgressBar variants and adjusted image-border values and colors.
Progress bar widgets
modules/corelib/ui/uiprogressbarsd.lua, modules/corelib/uiprogressbarsdinverted.lua
Added UIProgressBarSD and UIProgressBarSDInverted classes with min/max/value handling, percent APIs, border parsing from styles, lifecycle hooks, and background rendering logic.
Imbuement UI module
modules/game_imbui/t_imbui.lua, modules/game_imbui/t_imbui.otmod, modules/game_imbui/t_imbui.otui
New Imbuement module: lifecycle, helpers (balance, formatting, item lookups), event wiring, and comprehensive OTUI layout for select/scroll/imbue/clear flows.
Imbuement controllers
modules/game_imbui/classes/imbuementselection.lua, modules/game_imbui/classes/imbuementitem.lua, modules/game_imbui/classes/imbuementscroll.lua
New controllers handling item/scroll picking, slot selection, UI population, requirement validation, confirmation flows, and shutdown/cleanup logic.
Game imbuing UI tweaks
modules/game_imbuing/imbuing.lua, modules/game_imbuing/imbuing.otui
Enhanced existing imbuing UI: formatted balances, enablement based on item/gold availability, protection cost handling, tooltip wiring, slot icon updates, progress-timer bar and info tooltip panel.
Client game bindings
src/client/game.cpp, src/client/game.h, src/client/luafunctions.cpp
Added g_game methods selectImbuementItem(itemId,pos,stackpos) and selectImbuementScroll() and exposed them to Lua; include canPerformGameAction() checks.
Protocol changes
src/client/protocolcodes.h, src/client/protocolgame.h, src/client/protocolgameparse.cpp, src/client/protocolgamesend.cpp
Added GameServerInventoryImbuements opcode (96); added ProtocolGame::sendImbuementWindowAction and parsing for imbuement window types (CHOICE/SELECT_ITEM/SCROLL) with Lua callbacks; send/parse conditional payload for SELECT_ITEM.
Module integration
modules/game_interface/interface.otmod
Registered game_imbui module to load-later list.

Sequence Diagram

sequenceDiagram
    actor User
    participant UI as ImbuementUI
    participant Controller as ImbuementItem/Scroll
    participant Game as GameClient
    participant Protocol as ProtocolGame
    participant Server

    User->>UI: open imbuement window
    UI->>Controller: populate panels (select/scroll/imbue)
    User->>Controller: choose slot / select imbuement
    Controller->>Controller: validate requirements (items, gold)
    User->>Controller: confirm apply
    Controller->>Game: call selectImbuementItem(itemId,pos,stackpos) / selectImbuementScroll()
    Game->>Protocol: sendImbuementWindowAction(type, ... )
    Protocol->>Server: transmit opcode 0xB2 payload
    Server->>Protocol: respond GameServerInventoryImbuements
    Protocol->>Game: parseImbuementWindow -> invoke Lua callbacks
    Game->>UI: update UI state (onImbuementItem/onImbuementScroll)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Suggested reviewers

  • libergod
  • majestyotbr

Poem

🐇 Hoppity hop, the shrine's alive,

Bars that fill and tooltips jive,
Slots and scrolls in careful hand,
Gold and items meet demand,
A rabbit cheers — UI dreams arrive!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: imbuement system' clearly and concisely describes the main addition in this changeset - a complete imbuement system with UI, Lua classes, protocol handling, and styling.

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

✨ Finishing touches
🧪 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: 9

🤖 Fix all issues with AI agents
In `@data/styles/10-progressbars.otui`:
- Around line 42-68: The file defines ProgressBarSD twice with different bases,
causing the first definition to be overridden; rename one of the ProgressBarSD
classes to a distinct name (e.g., ProgressBarSD_UIProgressBar or
ProgressBarSD_SD) and update all usages/inheritance to reference the new name so
each variant keeps its intended base (references to UIProgressBarSD vs
UIProgressBar), ensure derived classes like HealthBarSD inherit from the correct
renamed variant, and keep other classes (ProgressBarSDInv, InvManaBar,
InvVerticalBar, VerticalBar) unchanged unless they need to point to the renamed
symbol.

In `@modules/corelib/ui/uiprogressbarsd.lua`:
- Around line 4-15: The constructor UIProgressBarSD.create currently initializes
progressbar.min/progressbar.max causing self.minimum/self.maximum to remain nil
and break setValue clamps; change the initialization to set progressbar.minimum
and progressbar.maximum (and keep progressbar.value) so setValue and any
style-driven percent calculations have valid bounds; update any places relying
on min/max to use minimum/maximum or set both aliases if needed to preserve
compatibility.

In `@modules/corelib/uiprogressbarsd.lua`:
- Around line 4-16: In UIProgressBarSD.create(), the progress bar initializes
progressbar.min/progressbar.max which leaves self.minimum/self.maximum nil (used
by setValue); change the initialization to set progressbar.minimum = 0 and
progressbar.maximum = 100 (keep progressbar.value = 0 and the other setup calls
as-is) so clamps in setValue work correctly.

In `@modules/corelib/uiprogressbarsdinverted.lua`:
- Around line 4-15: The create function sets progressbar.min/progressbar.max
which leaves progressbar.minimum/progressbar.maximum nil and causes setValue
(which clamps against self.minimum/self.maximum) to fail; change
UIProgressBarSDInverted.create to initialize progressbar.minimum and
progressbar.maximum (and set progressbar.value after them) instead of
progressbar.min/progressbar.max so setValue/clamping works correctly, leaving
other initializations (bgBorder*, setFocusable, setOn, insertLuaCall("onSetup"))
unchanged.

In `@modules/game_imbui/classes/imbuementitem.lua`:
- Around line 19-29: The loop in ImbuementItem.setup uses `#activeSlots` which
fails if activeSlots is 0-based; change the iteration to use the provided slots
count instead (iterate from 0 to slots-1) when populating
self.activeSlots["slot"..i] = activeSlots[i] or {}; update the loop in
ImbuementItem.setup to use the function parameter slots (or self.slots) as the
upper bound so slot1/slot2 aren't dropped when indices start at 0.

In `@modules/game_imbui/classes/imbuementselection.lua`:
- Around line 52-92: In ImbuementSelection.onChooseItemMouseRelease the branch
for scroll selection incorrectly calls g_game.selectImbuementItem; change it so
when self.isSelectingScroll is true you call g_game.selectImbuementScroll() (no
arguments) and when false call g_game.selectImbuementItem(itemId, pos,
stackPos); keep the rest of the cleanup (self.pickItem:ungrabMouse(),
g_mouse.popCursor('target')) as-is.

In `@modules/game_imbui/t_imbui.lua`:
- Around line 185-209: The toggleMenu function uses a Lua 5.2-only goto (label
::continue::) which breaks under LuaJIT; refactor Imbuement:toggleMenu to remove
the goto by inverting the condition and using nested conditionals: iterate
pairs(self), skip non-userdata or key == 'window' with an if check (e.g., if
type(value) == 'userdata' and key ~= 'window' then ... end), then inside handle
the key == menu branch to show and adjust self.window:setHeight(...) for the
four menu cases and an else branch to hide the value; ensure the loop simply
continues naturally without any goto or labels.

In `@modules/game_imbuing/imbuing.lua`:
- Around line 161-175: The setProtection function accesses selectedImbue['cost']
and ['protectionCost'] without ensuring selectedImbue is defined; guard against
nil by checking selectedImbue at the top of the protection-true branch (or
early-return if protection cannot be enabled) before using its fields, and only
update emptyImbue.cost, emptyImbue.successRate, emptyImbue.cost:setColor and
protectionBtn:setImageClip when selectedImbue is non-nil; reference the
setProtection function, selectedImbue, emptyImbue and protectionBtn to locate
and apply the guard and handle the insufficient-gold check (bankGold +
inventoryGold) safely.

In `@src/client/protocolgameparse.cpp`:
- Around line 5746-5832: The code in ProtocolGame::parseImbuementWindow reads a
windowType for client versions >= 1510 but only handles values 0,1,2 and will
fall through into the legacy parser on any other value; add a default guard
immediately after reading const uint8_t windowType = msg->getU8() that detects
when windowType is not 0,1,2, logs a warning (using the project's logger)
including the unexpected windowType and returns early to avoid corrupting the
message stream and desync with the legacy parsing path.
🧹 Nitpick comments (4)
modules/game_imbuing/imbuing.lua (1)

64-140: Consider extracting the “hasAllItems/hasEnoughGold + UI state” logic into a helper.
This logic is repeated in onOptionChange, setProtection, and onResourcesBalanceChange, which makes future tweaks easy to miss.

src/client/protocolcodes.h (1)

224-224: Keep the custom opcode in the 51–99 block for readability.
Line 224 introduces a value in the custom range but places it after higher-valued opcodes; consider moving it into the “NOTE: add any custom opcodes in this range” section to keep ordering/scanning consistent.

src/client/protocolgamesend.cpp (1)

1469-1482: Avoid magic opcode/type values and validate action types.
Line 1472 hard-codes 0xB2 and Line 1473 uses raw type codes. Please confirm this opcode is valid for all supported protocol versions; if it’s 15+ only, guard older versions. Also consider defining a named opcode in Proto::ClientOpcodes and validating the action type to avoid malformed packets coming from Lua.

♻️ Suggested hardening
 void ProtocolGame::sendImbuementWindowAction(const uint8_t type, const uint16_t itemId, const Position& pos, const uint8_t stackpos)
 {
+    // If this is protocol 15+ only, guard older clients.
+    if (g_game.getClientVersion() < 1510) {
+        g_logger.warn("Imbuement window action not supported for client version {}", g_game.getClientVersion());
+        return;
+    }
+
+    if (type != 1 && type != 2) {
+        g_logger.warn("Invalid imbuement window action type: {}", type);
+        return;
+    }
     const auto& msg = std::make_shared<OutputMessage>();
-    msg->addU8(0xB2);  // same opcode as parseImbuementWindow on server
-    msg->addU8(type);  // 1 = SELECT_ITEM, 2 = SCROLL
+    msg->addU8(Proto::ClientImbuementWindowAction);
+    msg->addU8(type);
modules/game_imbui/t_imbui.lua (1)

17-74: Prefer ResourceTypes constants in getPlayerBalance.

Line 31–32 uses numeric resource IDs; aligning with ResourceTypes.BANK_BALANCE / ResourceTypes.GOLD_EQUIPPED (already used at Line 216–217) avoids drift if enums change.

♻️ Suggested change
-  local bankGold = player:getResourceBalance(1) or 0  -- BANK_BALANCE
-  local inventoryGold = player:getResourceBalance(0) or 0  -- GOLD_EQUIPPED
+  local bankGold = player:getResourceBalance(ResourceTypes.BANK_BALANCE) or 0
+  local inventoryGold = player:getResourceBalance(ResourceTypes.GOLD_EQUIPPED) or 0

Comment on lines +42 to +68
ProgressBarSD < UIProgressBarSD
height: 22
tooltip-delayed: true

ProgressBarSD < UIProgressBar
height: 22
tooltip-delayed: true

HealthBarSD < ProgressBarSD

ProgressBarSDInv < UIProgressBarSDInverted

InvManaBar < UIProgressBarSDInverted
size: 10 30
image-border: 1

InvVerticalBar < UIVerticalProgressBarSDInverted
size: 10 30
image-border: 1

VerticalBar < UIVerticalProgressBarSD
size: 10 30
image-border: 1

ProgressBarSD < UIProgressBarSD
height: 22
tooltip-delayed: true No newline at end of file
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

Duplicate ProgressBarSD definitions override each other.
The class is declared multiple times with different bases, so only the last definition survives. If you need SD styling for both UIProgressBarSD and UIProgressBar, use distinct class names (and update references).

🔧 One possible consolidation
-ProgressBarSD < UIProgressBar
+ProgressBarSDPlain < UIProgressBar
   height: 22
   tooltip-delayed: true
@@
-ProgressBarSD < UIProgressBarSD
-  height: 22
-  tooltip-delayed: true
🤖 Prompt for AI Agents
In `@data/styles/10-progressbars.otui` around lines 42 - 68, The file defines
ProgressBarSD twice with different bases, causing the first definition to be
overridden; rename one of the ProgressBarSD classes to a distinct name (e.g.,
ProgressBarSD_UIProgressBar or ProgressBarSD_SD) and update all
usages/inheritance to reference the new name so each variant keeps its intended
base (references to UIProgressBarSD vs UIProgressBar), ensure derived classes
like HealthBarSD inherit from the correct renamed variant, and keep other
classes (ProgressBarSDInv, InvManaBar, InvVerticalBar, VerticalBar) unchanged
unless they need to point to the renamed symbol.

Comment on lines +4 to +15
function UIProgressBarSD.create()
local progressbar = UIProgressBarSD.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.min = 0
progressbar.max = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
return progressbar
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

Initialize minimum/maximum instead of min/max to avoid nil clamps.
setValue clamps using self.minimum/self.maximum; with current init those are nil until setters run, so a style-driven percent can throw.

🐛 Proposed fix
-  progressbar.min = 0
-  progressbar.max = 100
+  progressbar.minimum = 0
+  progressbar.maximum = 100
📝 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
function UIProgressBarSD.create()
local progressbar = UIProgressBarSD.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.min = 0
progressbar.max = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
return progressbar
function UIProgressBarSD.create()
local progressbar = UIProgressBarSD.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.minimum = 0
progressbar.maximum = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
return progressbar
🤖 Prompt for AI Agents
In `@modules/corelib/ui/uiprogressbarsd.lua` around lines 4 - 15, The constructor
UIProgressBarSD.create currently initializes progressbar.min/progressbar.max
causing self.minimum/self.maximum to remain nil and break setValue clamps;
change the initialization to set progressbar.minimum and progressbar.maximum
(and keep progressbar.value) so setValue and any style-driven percent
calculations have valid bounds; update any places relying on min/max to use
minimum/maximum or set both aliases if needed to preserve compatibility.

Comment on lines +4 to +16
function UIProgressBarSD.create()
local progressbar = UIProgressBarSD.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.min = 0
progressbar.max = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
progressbar:insertLuaCall("onSetup")
progressbar:insertLuaCall("onGeometryChange")
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

Initialize minimum/maximum instead of min/max to avoid nil clamps.
setValue clamps via self.minimum/self.maximum; with current init those are nil until setters run.

🐛 Proposed fix
-  progressbar.min = 0
-  progressbar.max = 100
+  progressbar.minimum = 0
+  progressbar.maximum = 100
📝 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
function UIProgressBarSD.create()
local progressbar = UIProgressBarSD.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.min = 0
progressbar.max = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
progressbar:insertLuaCall("onSetup")
progressbar:insertLuaCall("onGeometryChange")
function UIProgressBarSD.create()
local progressbar = UIProgressBarSD.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.minimum = 0
progressbar.maximum = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
progressbar:insertLuaCall("onSetup")
progressbar:insertLuaCall("onGeometryChange")
🤖 Prompt for AI Agents
In `@modules/corelib/uiprogressbarsd.lua` around lines 4 - 16, In
UIProgressBarSD.create(), the progress bar initializes
progressbar.min/progressbar.max which leaves self.minimum/self.maximum nil (used
by setValue); change the initialization to set progressbar.minimum = 0 and
progressbar.maximum = 100 (keep progressbar.value = 0 and the other setup calls
as-is) so clamps in setValue work correctly.

Comment on lines +4 to +15
function UIProgressBarSDInverted.create()
local progressbar = UIProgressBarSDInverted.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.min = 0
progressbar.max = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
progressbar:insertLuaCall("onSetup")
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

Initialize minimum/maximum instead of min/max to avoid nil clamps.
setValue clamps via self.minimum/self.maximum; with current init those are nil until setters run.

🐛 Proposed fix
-  progressbar.min = 0
-  progressbar.max = 100
+  progressbar.minimum = 0
+  progressbar.maximum = 100
📝 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
function UIProgressBarSDInverted.create()
local progressbar = UIProgressBarSDInverted.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.min = 0
progressbar.max = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
progressbar:insertLuaCall("onSetup")
function UIProgressBarSDInverted.create()
local progressbar = UIProgressBarSDInverted.internalCreate()
progressbar:setFocusable(false)
progressbar:setOn(true)
progressbar.minimum = 0
progressbar.maximum = 100
progressbar.value = 0
progressbar.bgBorderLeft = 0
progressbar.bgBorderRight = 0
progressbar.bgBorderTop = 0
progressbar.bgBorderBottom = 0
progressbar:insertLuaCall("onSetup")
🤖 Prompt for AI Agents
In `@modules/corelib/uiprogressbarsdinverted.lua` around lines 4 - 15, The create
function sets progressbar.min/progressbar.max which leaves
progressbar.minimum/progressbar.maximum nil and causes setValue (which clamps
against self.minimum/self.maximum) to fail; change
UIProgressBarSDInverted.create to initialize progressbar.minimum and
progressbar.maximum (and set progressbar.value after them) instead of
progressbar.min/progressbar.max so setValue/clamping works correctly, leaving
other initializations (bgBorder*, setFocusable, setOn, insertLuaCall("onSetup"))
unchanged.

Comment on lines +19 to +29
function ImbuementItem.setup(itemId, tier, slots, activeSlots, availableImbuements, needItems)
self.itemId = itemId
self.tier = tier
self.slots = slots

self.activeSlots = {}
for i = 0, #activeSlots do
self.activeSlots["slot"..i] = activeSlots[i] or {}
end
self.availableImbuements = availableImbuements or {}
self.needItems = needItems or {}
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

#activeSlots can drop slots when indices start at 0.

Line 25 uses the length operator; if activeSlots is 0-based (slot0–slot2), # is undefined and may return 0, leaving slot1/slot2 empty. Iterate by slots count instead.

🐛 Suggested change
-    self.activeSlots = {}
-    for i = 0, `#activeSlots` do
-        self.activeSlots["slot"..i] = activeSlots[i] or {}
-    end
+    self.activeSlots = {}
+    activeSlots = activeSlots or {}
+    for i = 0, (slots or 0) - 1 do
+        self.activeSlots["slot"..i] = activeSlots[i] or {}
+    end
🤖 Prompt for AI Agents
In `@modules/game_imbui/classes/imbuementitem.lua` around lines 19 - 29, The loop
in ImbuementItem.setup uses `#activeSlots` which fails if activeSlots is 0-based;
change the iteration to use the provided slots count instead (iterate from 0 to
slots-1) when populating self.activeSlots["slot"..i] = activeSlots[i] or {};
update the loop in ImbuementItem.setup to use the function parameter slots (or
self.slots) as the upper bound so slot1/slot2 aren't dropped when indices start
at 0.

Comment on lines +52 to +92
function ImbuementSelection.onChooseItemMouseRelease(widget, mousePosition, mouseButton)
local item = nil
if mouseButton == MouseLeftButton then
local clickedWidget = modules.game_interface.getRootPanel():recursiveGetChildByPos(mousePosition, false)
if clickedWidget then
if clickedWidget:getClassName() == 'UIGameMap' then
local tile = clickedWidget:getTile(mousePosition)
if tile then
local thing = tile:getTopMoveThing()
if thing and thing:isItem() then
item = thing
end
end
elseif clickedWidget:getClassName() == 'UIItem' and not clickedWidget:isVirtual() then
item = clickedWidget:getItem()
end
end
end

if item and item:isPickupable() then
local pos = item:getPosition()
local itemId = item:getId()
local stackPos = item:getStackPos()

if self.isSelectingScroll then
g_game.selectImbuementItem(itemId, pos, stackPos)
else
g_game.selectImbuementItem(itemId, pos, stackPos)
end

self.pickItem:ungrabMouse()
g_mouse.popCursor('target')

return true
else
modules.game_textmessage.displayFailureMessage(tr('Sorry, not possible.'))
end

self.pickItem:ungrabMouse()
g_mouse.popCursor('target')
return true
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether scroll-specific and item selection APIs exist
rg -n "selectImbuementScroll" .
echo "---"
rg -n "selectImbuementItem" .

Repository: opentibiabr/otclient

Length of output: 993


🏁 Script executed:

#!/bin/bash
# Read the specific file and lines mentioned in the review
cat -n modules/game_imbui/classes/imbuementselection.lua | sed -n '52,92p'

Repository: opentibiabr/otclient

Length of output: 1641


Scroll selection uses wrong API.
Both if self.isSelectingScroll branches call g_game.selectImbuementItem() identically (lines 76–79). The scroll-specific API g_game.selectImbuementScroll() exists but is never invoked, causing scroll selections to be treated as item selections and failing the intended flow.

Fix
    if self.isSelectingScroll then
-      g_game.selectImbuementItem(itemId, pos, stackPos)
+      g_game.selectImbuementScroll()
    else
      g_game.selectImbuementItem(itemId, pos, stackPos)
    end

Note: selectImbuementScroll() takes no parameters, unlike selectImbuementItem().

🤖 Prompt for AI Agents
In `@modules/game_imbui/classes/imbuementselection.lua` around lines 52 - 92, In
ImbuementSelection.onChooseItemMouseRelease the branch for scroll selection
incorrectly calls g_game.selectImbuementItem; change it so when
self.isSelectingScroll is true you call g_game.selectImbuementScroll() (no
arguments) and when false call g_game.selectImbuementItem(itemId, pos,
stackPos); keep the rest of the cleanup (self.pickItem:ungrabMouse(),
g_mouse.popCursor('target')) as-is.

Comment on lines +185 to +209
function Imbuement:toggleMenu(menu)
for key, value in pairs(self) do
if type(value) ~= 'userdata' or key == 'window' then
goto continue
end

if key == menu then
value:show()
-- Ajustar tamanho da janela baseado no menu
if menu == 'selectItemOrScroll' then
self.window:setHeight(388)
elseif menu == 'scrollImbue' then
self.window:setHeight(655)
elseif menu == 'selectImbue' then
self.window:setHeight(528)
elseif menu == 'clearImbue' then
self.window:setHeight(502)
end
else
value:hide()
end

::continue::
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 | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# Search for Lua version references in the codebase
rg -n "LuaJIT|LUA_VERSION|lua5|lua-5|lua51|lua52|lua53|lua54" -i

Repository: opentibiabr/otclient

Length of output: 3139


🏁 Script executed:

# Also check for common version indicators in build/config files
fd -t f "(package|config|setup|version|CMakeLists|Makefile|build)" | head -20

Repository: opentibiabr/otclient

Length of output: 637


🏁 Script executed:

# Verify the actual file and code at lines 185-209
cat -n modules/game_imbui/t_imbui.lua | sed -n '185,209p'

Repository: opentibiabr/otclient

Length of output: 865


🏁 Script executed:

# Search for all uses of goto in the codebase to see if this is a pattern
rg "goto|::.*::" --multiline-dotall

Repository: opentibiabr/otclient

Length of output: 50377


Remove goto — incompatible with Lua 5.1/LuaJIT.

Line 188 uses goto which is a syntax error in Lua 5.1/LuaJIT (goto was introduced in Lua 5.2). The client uses LuaJIT, so this module will fail to load. Refactor using nested conditionals instead.

🔧 Suggested change (no `goto`)
 function Imbuement:toggleMenu(menu)
   for key, value in pairs(self) do
-    if type(value) ~= 'userdata' or key == 'window' then
-      goto continue
-    end
-
-    if key == menu then
-      value:show()
-      -- Ajustar tamanho da janela baseado no menu
-      if menu == 'selectItemOrScroll' then
-        self.window:setHeight(388)
-      elseif menu == 'scrollImbue' then
-        self.window:setHeight(655)
-      elseif menu == 'selectImbue' then
-        self.window:setHeight(528)
-      elseif menu == 'clearImbue' then
-        self.window:setHeight(502)
-      end
-    else
-      value:hide()
-    end
-
-    ::continue::
+    if type(value) == 'userdata' and key ~= 'window' then
+      if key == menu then
+        value:show()
+        -- Ajustar tamanho da janela baseado no menu
+        if menu == 'selectItemOrScroll' then
+          self.window:setHeight(388)
+        elseif menu == 'scrollImbue' then
+          self.window:setHeight(655)
+        elseif menu == 'selectImbue' then
+          self.window:setHeight(528)
+        elseif menu == 'clearImbue' then
+          self.window:setHeight(502)
+        end
+      else
+        value:hide()
+      end
+    end
   end
 end
🤖 Prompt for AI Agents
In `@modules/game_imbui/t_imbui.lua` around lines 185 - 209, The toggleMenu
function uses a Lua 5.2-only goto (label ::continue::) which breaks under
LuaJIT; refactor Imbuement:toggleMenu to remove the goto by inverting the
condition and using nested conditionals: iterate pairs(self), skip non-userdata
or key == 'window' with an if check (e.g., if type(value) == 'userdata' and key
~= 'window' then ... end), then inside handle the key == menu branch to show and
adjust self.window:setHeight(...) for the four menu cases and an else branch to
hide the value; ensure the loop simply continues naturally without any goto or
labels.

Comment on lines 161 to 175
function setProtection(value)
protection = value
if protection then
emptyImbue.cost:setText(selectedImbue['cost'] + selectedImbue['protectionCost'])
emptyImbue.cost:setText(comma_value(selectedImbue['cost'] + selectedImbue['protectionCost']))
emptyImbue.successRate:setText('100%')
emptyImbue.successRate:setColor('green')
protectionBtn:setImageClip(torect('66 0 66 66'))

-- Verificar se há gold suficiente para o custo total com proteção
if (bankGold + inventoryGold) < (selectedImbue['cost'] + selectedImbue['protectionCost']) then
emptyImbue.cost:setColor('red')
else
emptyImbue.cost:setColor('white')
end
else
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

Guard selectedImbue when enabling protection.
If the protection toggle is used before an imbuement is selected, selectedImbue[...] will error.

🐛 Proposed fix
 function setProtection(value)
     protection = value
     if protection then
+        if not selectedImbue then return end
         emptyImbue.cost:setText(comma_value(selectedImbue['cost'] + selectedImbue['protectionCost']))
         emptyImbue.successRate:setText('100%')
📝 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
function setProtection(value)
protection = value
if protection then
emptyImbue.cost:setText(selectedImbue['cost'] + selectedImbue['protectionCost'])
emptyImbue.cost:setText(comma_value(selectedImbue['cost'] + selectedImbue['protectionCost']))
emptyImbue.successRate:setText('100%')
emptyImbue.successRate:setColor('green')
protectionBtn:setImageClip(torect('66 0 66 66'))
-- Verificar se há gold suficiente para o custo total com proteção
if (bankGold + inventoryGold) < (selectedImbue['cost'] + selectedImbue['protectionCost']) then
emptyImbue.cost:setColor('red')
else
emptyImbue.cost:setColor('white')
end
else
function setProtection(value)
protection = value
if protection then
if not selectedImbue then return end
emptyImbue.cost:setText(comma_value(selectedImbue['cost'] + selectedImbue['protectionCost']))
emptyImbue.successRate:setText('100%')
emptyImbue.successRate:setColor('green')
protectionBtn:setImageClip(torect('66 0 66 66'))
-- Verificar se há gold suficiente para o custo total com proteção
if (bankGold + inventoryGold) < (selectedImbue['cost'] + selectedImbue['protectionCost']) then
emptyImbue.cost:setColor('red')
else
emptyImbue.cost:setColor('white')
end
else
🤖 Prompt for AI Agents
In `@modules/game_imbuing/imbuing.lua` around lines 161 - 175, The setProtection
function accesses selectedImbue['cost'] and ['protectionCost'] without ensuring
selectedImbue is defined; guard against nil by checking selectedImbue at the top
of the protection-true branch (or early-return if protection cannot be enabled)
before using its fields, and only update emptyImbue.cost,
emptyImbue.successRate, emptyImbue.cost:setColor and protectionBtn:setImageClip
when selectedImbue is non-nil; reference the setProtection function,
selectedImbue, emptyImbue and protectionBtn to locate and apply the guard and
handle the insufficient-gold check (bankGold + inventoryGold) safely.

Comment on lines +5746 to +5832
if (g_game.getClientVersion() >= 1510) {
const uint8_t windowType = msg->getU8();

// 0 = CHOICE (select item or scroll)
if (windowType == 0) {
msg->getU8(); // unknown
msg->getU16(); // padding
msg->getU32(); // padding
g_lua.callGlobalField("g_game", "onOpenImbuementWindow");
return;
}

// 1 = SELECT_ITEM
if (windowType == 1) {
msg->getU8(); // unknown

const uint16_t itemId = msg->getU16();
const auto& item = Item::create(itemId);
if (!item || item->getId() == 0) {
throw Exception("ProtocolGame::parseImbuementWindow: unable to create item with invalid id {}", itemId);
}

uint8_t tier = 0;
if (item->getClassification() > 0) {
tier = msg->getU8();
}

const uint8_t slots = msg->getU8();
std::unordered_map<int, std::tuple<Imbuement, uint32_t, uint32_t>> activeSlots;
for (auto i = 0; i < slots; i++) {
const uint8_t firstByte = msg->getU8();
if (firstByte == 0x01) {
Imbuement imbuement = getImbuementInfo(msg);
const uint32_t duration = msg->getU32();
const uint32_t removalCost = msg->getU32();
activeSlots[i] = std::make_tuple(imbuement, duration, removalCost);
}
}

const uint16_t imbuementsSize = msg->getU16();
std::vector<Imbuement> imbuements;
imbuements.reserve(imbuementsSize);
for (auto i = 0; i < imbuementsSize; ++i) {
imbuements.push_back(getImbuementInfo(msg));
}

const uint32_t neededItemsListCount = msg->getU32();
std::vector<ItemPtr> neededItemsList;
neededItemsList.reserve(neededItemsListCount);
for (uint32_t i = 0; i < neededItemsListCount; ++i) {
const uint16_t needItemId = msg->getU16();
const uint16_t count = msg->getU16();
const auto& needItem = Item::create(needItemId);
needItem->setCount(count);
neededItemsList.push_back(needItem);
}
g_lua.callGlobalField("g_game", "onImbuementItem", itemId, tier, slots, activeSlots, imbuements, neededItemsList);
return;
}

// 2 = SCROLL
if (windowType == 2) {
msg->getU8(); // unknown
msg->getU8(); // unknown
msg->getU8(); // unknown

const uint16_t imbuementsSize = msg->getU16();
std::vector<Imbuement> imbuements;
imbuements.reserve(imbuementsSize);
for (auto i = 0; i < imbuementsSize; ++i) {
imbuements.push_back(getImbuementInfo(msg));
}

const uint32_t neededItemsListCount = msg->getU32();
std::vector<ItemPtr> neededItemsList;
neededItemsList.reserve(neededItemsListCount);
for (uint32_t i = 0; i < neededItemsListCount; ++i) {
const uint16_t needItemId = msg->getU16();
const uint16_t count = msg->getU16();
const auto& needItem = Item::create(needItemId);
needItem->setCount(count);
neededItemsList.push_back(needItem);
}
g_lua.callGlobalField("g_game", "onImbuementScroll", imbuements, neededItemsList);
return;
}
}
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

Guard unknown imbuement window types to avoid parser desync.
For 1510+, an unexpected windowType will fall through to the legacy parser after consuming one byte, which can corrupt the stream and disconnect. Add a default branch that logs and returns (or skips) before the legacy path executes.

🔧 Suggested fix (add a default guard)
         // 2 = SCROLL
         if (windowType == 2) {
             msg->getU8(); // unknown
             msg->getU8(); // unknown
             msg->getU8(); // unknown
@@
             g_lua.callGlobalField("g_game", "onImbuementScroll", imbuements, neededItemsList);
             return;
         }
+        g_logger.warning("ProtocolGame::parseImbuementWindow: unknown window type {}", windowType);
+        return;
     }
🤖 Prompt for AI Agents
In `@src/client/protocolgameparse.cpp` around lines 5746 - 5832, The code in
ProtocolGame::parseImbuementWindow reads a windowType for client versions >=
1510 but only handles values 0,1,2 and will fall through into the legacy parser
on any other value; add a default guard immediately after reading const uint8_t
windowType = msg->getU8() that detects when windowType is not 0,1,2, logs a
warning (using the project's logger) including the unexpected windowType and
returns early to avoid corrupting the message stream and desync with the legacy
parsing path.

@majestyotbr majestyotbr changed the title Feat: imbue Client 15+ / fix: aparence imbue 14 feat: imbuement system Jan 26, 2026
@paczielny
Copy link

At least rename the Lua files

matzinhozz added a commit to matzinhozz/otclient that referenced this pull request Jan 27, 2026
- Integra sistema de imbuement para protocolo 15+
- Adiciona novos componentes de progress bar (UIProgressBarSD, UIProgressBarSDInverted)
- Adiciona módulo game_imbui com classes para seleção de item/scroll
- Adiciona métodos selectImbuementItem e selectImbuementScroll em Game
- Resolve conflitos mantendo funcionalidades existentes (Wheel of Destiny, Weapon Proficiency)
- Mantém lógica de updateBackground do HEAD para esconder imagem quando width == 1
matzinhozz added a commit to matzinhozz/otclient that referenced this pull request Feb 1, 2026
@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 added the 15.x label Feb 17, 2026
@libergod libergod self-requested a review February 17, 2026 18:31
matzinhozz added a commit to matzinhozz/otclient that referenced this pull request Mar 16, 2026
@github-actions
Copy link

This PR is stale because it has been open 45 days with no activity.

@github-actions github-actions bot added the Stale label Mar 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants