feat(ui): bugix item compositor#31
Conversation
Implemented multi-tile corpse rendering before creatures and aligned mount drawing logic with OTClient parity. Added support for ping-pong animations and per-instance random phase offsets. - `ImguiMapEditor/Domain/ItemType.h` — added `is_lying_object` flag - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — added ping-pong phase logic, handled `start_frame` and `is_async` offsets - `ImguiMapEditor/Rendering/Animation/ItemAnimation.h` — declared `getPingPongPhase` - `ImguiMapEditor/Rendering/Map/TileRenderer.cpp` — added pre-creature drawing for multi-tile lying objects - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — extracted `drawMount` and `emitPlaceholder` methods, updated drawing order - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.h` — added helper methods for mount and placeholder rendering - `ImguiMapEditor/Rendering/Tile/ItemRenderer.h` — updated common item rendering to reverse order and exclude multi-tile lying corpses - `ImguiMapEditor/Services/ClientDataService.cpp` — mapped `is_lying_object` from DAT data
Standardized item and brush preview rendering into a unified `RenderPreviewCard` utility to enforce consistent styling, selection feedback, and rounded aesthetics. Updated compositor logic to correctly support multi-layer items by iterating through layers before grid tiles. - `ImguiMapEditor/Services/ItemCompositor.cpp` — updated composite logic to iterate layers first, matching map renderer order; removed hardcoded background color - `ImguiMapEditor/UI/Utils/PreviewUtils.cpp` — implemented `RenderPreviewCard` for uniform rounded UI element rendering with state support - `ImguiMapEditor/UI/Utils/PreviewUtils.hpp` — added `RenderPreviewCard` declaration - `ImguiMapEditor/UI/Widgets/Properties/PropertyPanelRenderer.cpp` — migrated slot rendering to `RenderPreviewCard` - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp` — migrated brush grid to `RenderPreviewCard`, cleaned up redundant drawing logic - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.h` — updated `getBrushTexture` return type to `Rendering::Texture*` - `ImguiMapEditor/UI/Widgets/TilesetWidget.cpp` — migrated item grid to `RenderPreviewCard` - `ImguiMapEditor/UI/Windows/BrowseTile/ItemsListRenderer.cpp` — migrated item list row rendering to `RenderPreviewCard`
Reviewer's GuideRefactors and extends rendering and previewing of creatures and items to match OTClient/RME behavior, adds proper mount/addon handling and lying corpse depth ordering, unifies UI preview cards, and enhances item animation phase logic including ping-pong and start-frame support. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 1 issue, and left some high level feedback:
- In
UI::Utils::RenderPreviewCardyou mixImGui::IsMouseHoveringRect(min, max)(manual rect) with callers usingImGui::IsItemHovered()on the trailing dummy; consider basing both hover/tooltip logic on the same ImGui item to avoid subtle mismatches when hovering padded vs non-padded regions. - In
TilesetGridWidget::renderBrushGridyou now have two different card rendering paths (one usingRenderPreviewCard, one reimplementing a rounded card and selection border); it would be cleaner and less error-prone to consolidate these to a single shared helper so card visuals and interaction remain consistent.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In `UI::Utils::RenderPreviewCard` you mix `ImGui::IsMouseHoveringRect(min, max)` (manual rect) with callers using `ImGui::IsItemHovered()` on the trailing dummy; consider basing both hover/tooltip logic on the same ImGui item to avoid subtle mismatches when hovering padded vs non-padded regions.
- In `TilesetGridWidget::renderBrushGrid` you now have two different card rendering paths (one using `RenderPreviewCard`, one reimplementing a rounded card and selection border); it would be cleaner and less error-prone to consolidate these to a single shared helper so card visuals and interaction remain consistent.
## Individual Comments
### Comment 1
<location path="ImguiMapEditor/UI/Utils/PreviewUtils.cpp" line_range="48-57" />
<code_context>
+bool RenderPreviewCard(Rendering::Texture* texture, float size,
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Hover detection is decoupled from the ImGui item, which can be fragile with clipping/scrolling.
`RenderPreviewCard` computes hover using `ImGui::IsMouseHoveringRect(min, max)` while using a separate `ImGui::Dummy` as the clickable item. This can diverge from ImGui’s own hover logic in more complex layouts (clipping, overlapping items). To keep hover and click semantics consistent, derive the rect from the `Dummy` item (`ImGui::GetItemRectMin/Max()` + `ImGui::IsItemHovered()`), or call `Dummy` first and then base both hover and click on that item’s state.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 3 medium |
| ErrorProne | 1 medium |
| Complexity | 3 medium |
🟢 Metrics 51 complexity · -5 duplication
Metric Results Complexity 51 Duplication -5
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
There was a problem hiding this comment.
Pull Request Overview
This PR is currently not up to standards according to Codacy, and the review identifies several high-severity issues that prevent merging. While intended as a 'bugix', the changes introduce a significant rework of the item rendering compositor and new features like creature mounts and addons without a supporting PR description.
Key issues include a depth-sorting regression in ItemRenderer.h that will cause items to render in the wrong vertical order, and a logic error in ItemAnimation.cpp where frame offsets are applied at the wrong stage of the ping-pong calculation, leading to visual jumps. There is also a risk of double-rendering multi-tile objects in TileRenderer.cpp. Until the core rendering passes and animation math are corrected and validated with tests, this PR should not be merged.
About this PR
- The PR description is empty despite containing significant architectural changes to the rendering pipeline and new features like mounts and addons. This makes validation and future maintenance difficult.
- The title 'bugix' is misleading. The PR introduces substantial new features (mounts, addons, ping-pong animations) alongside bug fixes. Please update the title and provide a description of the new capabilities.
Test suggestions
- Missing: Verify ItemAnimation::getPingPongPhase returns correct sequence (e.g., 0, 1, 2, 1 for 3 frames)
- Missing: Verify creature mount rendering correctly applies colorization from lookMountHead/Body/Legs/Feet
- Missing: Verify creature addon visibility correctly respects the lookAddons bitmask
- Missing: Verify multi-tile items marked as 'is_lying_object' are skipped in the standard item pass and rendered in the pre-creature pass
- Missing: Verify async animation logic correctly calculates tile-based offsets to prevent all instances from animating in unison
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Missing: Verify ItemAnimation::getPingPongPhase returns correct sequence (e.g., 0, 1, 2, 1 for 3 frames)
2. Missing: Verify creature mount rendering correctly applies colorization from lookMountHead/Body/Legs/Feet
3. Missing: Verify creature addon visibility correctly respects the lookAddons bitmask
4. Missing: Verify multi-tile items marked as 'is_lying_object' are skipped in the standard item pass and rendered in the pre-creature pass
5. Missing: Verify async animation logic correctly calculates tile-based offsets to prevent all instances from animating in unison
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
| int ItemAnimation::getPingPongPhase(int raw_phase, int frames) { | ||
| if (frames <= 1) return 0; | ||
| int cycle = frames * 2 - 2; | ||
| if (cycle <= 0) return 0; |
There was a problem hiding this comment.
🔴 HIGH RISK
The condition 'cycle <= 0' is redundant. Since 'frames' is guaranteed to be at least 2 by the previous check, 'cycle' (2 * frames - 2) will always be greater than zero.
| for (int phase = 0; phase < static_cast<int>(item.frame_durations.size()); ++phase) { | ||
| int dur = getPhaseDuration(item, phase); | ||
| if (elapsed < dur) | ||
| return (phase + start_phase) % frames; |
There was a problem hiding this comment.
🔴 HIGH RISK
Applying start_phase to the final index in a ping-pong animation breaks visual continuity. The offset should be applied to the timing or raw phase before the reflection logic, similar to the fixed-duration implementation at line 58. Additionally, the method has exceeded complexity thresholds and should be refactored to extract ping-pong and forward loop logic into helper methods.
| for (auto it = items.rbegin(); it != items.rend(); ++it) { | ||
| const auto &entry = *it; |
There was a problem hiding this comment.
🔴 HIGH RISK
Changing Pass 2 to reverse iteration creates a depth-sorting inconsistency. If the item stack is stored Bottom-to-Top (as in RME/OTClient), rbegin will draw the topmost items first, causing them to be covered by lower items. Furthermore, ensure that items with a null 'type' (no metadata) still render correctly in the proper pass now that the explicit fallback handling has been removed.
| } | ||
| } | ||
|
|
||
| void CreatureRenderer::drawMount(const Domain::Outfit &outfit, int dir, |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Method drawMount exceeds complexity and parameter limits (CCN 20, 10 parameters). Consider extracting frame selection logic and grouping rendering properties into a RenderContext struct.
|
|
||
| // CREATURE RENDERING - Per-tile immediate (OTClient parity) | ||
| // Corpse correction: draw multi-tile lying corpses before creatures | ||
| for (const auto &[item, type] : render_cache_) { |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Manually re-queuing lying objects after queueAll breaks the item stack's depth order on the same tile and can lead to double-rendering if the item is also marked is_on_bottom. These items should be handled as a distinct pass within the ItemRenderer to preserve their relative elevation.
There was a problem hiding this comment.
📋 Review Summary
This PR introduces significant improvements to item and creature rendering, standardizes the preview UI across several widgets, and adds support for advanced animation modes (ping-pong, start frames).
The most critical finding is a regression in item rendering order within ItemRenderer::queueAll. Changing the common items pass to reverse iteration while the input items vector is sorted bottom-to-top will cause rendering artifacts (specifically, the ground being drawn on top of other items).
🔍 General Feedback
- Rendering Parity: Great progress on matching OTClient/RME rendering logic, especially regarding multi-tile corpses (lying objects) and mount rendering.
- UI Consistency: Standardizing item/creature previews into a reusable
RenderPreviewCardis a very good architectural move. - Correctness: The animation logic for ping-pong and start frames looks solid.
🔴 Critical Issues
- Rendering Order: Pass 2 in
ItemRenderer::queueAllmust be forward-iterating to maintain the painter's algorithm for bottom-to-top sorted item stacks.
🟡 Minor Suggestions
- Consistency: Use
SpriteUtils::getSpriteIndexinItemCompositorfor more robust indexing. - Cleanup: Some unused legacy code in
ItemCompositorwas correctly removed, but ensure that the removal of the gray background shade in previews is an intended design choice.
| // PASS 2: Common items — reverse (OTClient parity) | ||
| // Multi-tile lying corpses excluded (drawn pre-creature in TileRenderer) | ||
| for (auto it = items.rbegin(); it != items.rend(); ++it) { | ||
| const auto &entry = *it; |
There was a problem hiding this comment.
🔴 Critical Regression in Rendering Order
Changing Pass 2 to reverse iteration (rbegin to rend) will cause items to be drawn in top-to-bottom order. Since render_cache_ (passed from TileRenderer) contains items in bottom-to-top order (Ground at index 0, followed by stack items), this reverse loop will draw the Ground last, causing it to appear on top of all other items.
In Tibia, items should be drawn from bottom to top (painter's algorithm). If "OTClient parity" is desired, please ensure the input items span is already sorted top-to-bottom, otherwise, this should remain a forward loop.
| for (int cx = 0; cx < w; ++cx) { | ||
| size_t sprite_index = | ||
| (static_cast<size_t>(layer) * h + cy) * w + cx; | ||
| if (sprite_index >= type->sprite_ids.size()) |
There was a problem hiding this comment.
🟡 Use SpriteUtils::getSpriteIndex for Consistency
While this simplified indexing works for frame 0 with no patterns, it's safer and more maintainable to use Utils::SpriteUtils::getSpriteIndex(type, cx, cy, layer, 0, 0, 0, 0). This ensures that if the internal sprite storage order ever changes (or if we want to composite specific frames/patterns), the compositor remains correct.
Also, type->sprite_ids contains flattened IDs, so ensuring the loop order matches getSpriteIndex is critical. Your current loop order (layer -> height -> width) correctly matches getSpriteIndex.
| return GetCreaturePreviewImpl(clientData, spriteManager, outfit); | ||
| } | ||
|
|
||
| bool RenderPreviewCard(Rendering::Texture* texture, float size, |
There was a problem hiding this comment.
🟢 Standardized Preview UI
Excellent addition of RenderPreviewCard. Standardizing the preview rendering across the Properties panel, Tileset grid, and Browse Tile window significantly improves UI consistency. Using ImGuiCol_Header for selection and adding the gold border for selected items is a nice touch.
| return tick % frames; | ||
| } | ||
|
|
||
| int ItemAnimation::getPingPongPhase(int raw_phase, int frames) { |
There was a problem hiding this comment.
🟢 Improved Animation Support
The addition of ping-pong animation support and start_frame (random and fixed) brings the editor closer to OTClient's rendering parity. The mathematical implementation of getPingPongPhase is correct.
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces significant enhancements to the map editor's rendering and UI systems. It improves the animation engine with new ping-pong capabilities, refines the rendering order for multi-tile corpses to ensure correct isometric depth, and adds support for rendering creature mounts. Furthermore, it standardizes the visual representation of items and creatures across the UI by introducing a unified preview card component, ensuring a consistent look and feel throughout the application. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces several rendering and UI improvements, including support for multi-tile lying corpses, refactored creature and mount rendering, multi-layer item compositing, and a unified RenderPreviewCard utility for UI previews. However, the code review identifies several critical issues: reverse iteration of common items in ItemRenderer.h breaks visual stacking and elevation; rendering multi-tile corpses after queueAll in TileRenderer.cpp causes elevation and overlap bugs; a boundary condition in ItemAnimation.cpp causes visual flickering in ping-pong animations; and using ImGui::IsMouseHoveringRect in PreviewUtils.cpp leads to robust input handling issues when overlapping windows are present.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| for (auto it = items.rbegin(); it != items.rend(); ++it) { | ||
| const auto &entry = *it; | ||
| if (entry.type && entry.type->is_on_bottom) continue; | ||
| if (entry.type && entry.type->is_on_top) continue; | ||
| if (entry.type && entry.type->is_lying_object && | ||
| (entry.type->width > 1 || entry.type->height > 1)) | ||
| continue; | ||
| renderItem(entry); | ||
| } |
There was a problem hiding this comment.
Iterating through the common items in reverse order (rbegin to rend) completely breaks the visual stacking order and the accumulated_elevation calculation. In this codebase, items are stored in bottom-to-top order (where the end of the vector is the top of the stack). Rendering them in reverse order means the topmost item is drawn first at 0 elevation, and the bottom-most item is drawn last at the highest elevation, causing items to render upside down and float in the air.
Please revert this to forward iteration as in the original implementation.
for (const auto &entry : items) {
if (entry.type && entry.type->is_on_bottom) continue;
if (entry.type && entry.type->is_on_top) continue;
if (entry.type && entry.type->is_lying_object &&
(entry.type->width > 1 || entry.type->height > 1))
continue;
renderItem(entry);
}| if (elapsed >= total) | ||
| elapsed = cycle_total - elapsed; |
There was a problem hiding this comment.
In the ping-pong animation path with custom durations, when elapsed is exactly equal to total, the mirroring logic sets elapsed to total. Since total is the sum of all frame durations, the subsequent loop fails to find any phase where elapsed < dur (as elapsed will reduce to exactly dur in the last iteration, and dur < dur is false). This causes the function to fall through and return start_phase (the first frame) for a single tick/frame, causing a noticeable visual flicker at the peak of the animation.
Using elapsed = cycle_total - 1 - elapsed maps the reverse range [total, cycle_total - 1] perfectly to [0, total - 1], preventing this boundary bug.
| if (elapsed >= total) | |
| elapsed = cycle_total - elapsed; | |
| if (elapsed >= total) | |
| elapsed = cycle_total - 1 - elapsed; |
| // Corpse correction: draw multi-tile lying corpses before creatures | ||
| for (const auto &[item, type] : render_cache_) { | ||
| if (type && type->is_lying_object && (type->width > 1 || type->height > 1)) { | ||
| item_renderer_.queueWithColor( | ||
| type, screen_x, screen_y, size, tile_x, tile_y, tile_z, anim_ticks, | ||
| missing_sprites, ground_color.r, ground_color.g, ground_color.b, | ||
| item_alpha, &accumulated_elevation, item, 0, | ||
| tile_has_hook_south, tile_has_hook_east); | ||
| } | ||
| } |
There was a problem hiding this comment.
Excluding multi-tile lying corpses from queueAll and rendering them afterwards in TileRenderer::queueTile introduces a severe rendering and elevation bug:
- Any items stacked on top of the corpse (which appear after the corpse in the
itemsvector) will be rendered duringqueueAllfirst, without the corpse's elevation contribution, causing them to render too low. - The corpse is rendered after
queueAll, meaning it will visually overlap and render on top of items that are supposed to be on top of it. - The corpse is rendered with the fully accumulated elevation of the entire stack (including items above it), causing it to float.
Furthermore, this does not solve the isometric multi-tile overlap issue across adjacent tiles, as those tiles are processed in diagonal order. It is highly recommended to keep corpses in the normal queueAll pass.
| bool RenderPreviewCard(Rendering::Texture* texture, float size, | ||
| bool is_selected, float rounding, float padding) { | ||
| ImDrawList* dl = ImGui::GetWindowDrawList(); | ||
| ImVec2 min = ImGui::GetCursorScreenPos(); | ||
| ImVec2 max(min.x + size, min.y + size); | ||
|
|
||
| ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_FrameBg); | ||
| ImU32 border_col = ImGui::GetColorU32(ImGuiCol_Border); | ||
|
|
||
| if (is_selected) { | ||
| bg_col = ImGui::GetColorU32(ImGuiCol_Header); | ||
| border_col = IM_COL32(255, 200, 0, 255); // Gold | ||
| } else if (ImGui::IsMouseHoveringRect(min, max)) { | ||
| bg_col = ImGui::GetColorU32(ImGuiCol_HeaderHovered); | ||
| } | ||
|
|
||
| dl->AddRectFilled(min, max, bg_col, rounding); | ||
| dl->AddRect(min, max, border_col, rounding); | ||
|
|
||
| if (texture) { | ||
| ImVec2 img_min(min.x + padding, min.y + padding); | ||
| ImVec2 img_max(max.x - padding, max.y - padding); | ||
| dl->AddImageRounded( | ||
| (void*)(intptr_t)texture->id(), img_min, img_max, | ||
| ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, rounding); | ||
| } | ||
|
|
||
| ImGui::Dummy(ImVec2(size, size)); | ||
| return ImGui::IsItemClicked(); | ||
| } |
There was a problem hiding this comment.
Using ImGui::IsMouseHoveringRect to detect hover states can cause visual bugs because it returns true even if the mouse is hovering over an overlapping window, popup, or modal menu that is currently on top of the preview card.
It is highly recommended to call ImGui::Dummy first, and then use ImGui::IsItemHovered() and ImGui::IsItemClicked() to ensure correct and robust input/hover state handling.
bool RenderPreviewCard(Rendering::Texture* texture, float size,
bool is_selected, float rounding, float padding) {
ImVec2 min = ImGui::GetCursorScreenPos();
ImGui::Dummy(ImVec2(size, size));
bool is_hovered = ImGui::IsItemHovered();
bool is_clicked = ImGui::IsItemClicked();
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 max(min.x + size, min.y + size);
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_FrameBg);
ImU32 border_col = ImGui::GetColorU32(ImGuiCol_Border);
if (is_selected) {
bg_col = ImGui::GetColorU32(ImGuiCol_Header);
border_col = IM_COL32(255, 200, 0, 255); // Gold
} else if (is_hovered) {
bg_col = ImGui::GetColorU32(ImGuiCol_HeaderHovered);
}
dl->AddRectFilled(min, max, bg_col, rounding);
dl->AddRect(min, max, border_col, rounding);
if (texture) {
ImVec2 img_min(min.x + padding, min.y + padding);
ImVec2 img_max(max.x - padding, max.y - padding);
dl->AddImageRounded(
(void*)(intptr_t)texture->id(), img_min, img_max,
ImVec2(0, 0), ImVec2(1, 1), IM_COL32_WHITE, rounding);
}
return is_clicked;
}|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (2)
WalkthroughThis PR adds an ItemType flag Possibly related PRs
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
♻️ Duplicate comments (1)
ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp (1)
66-72:⚠️ Potential issue | 🟠 Major | ⚡ Quick win
start_phaseapplied incorrectly in duration-based ping-pong path.In the duration-based ping-pong branch,
start_phaseis applied as a rotation of the final phase index ((phase + start_phase) % frames). This breaks the symmetric nature of ping-pong animations.Compare with line 58 where
start_phaseis correctly added to the raw phase before ping-pong reflection. The same approach should be used here: offset should be incorporated into the elapsed time or raw phase calculation before selecting which phase it falls into.Example issue: with
frames=3andstart_phase=1, ping-pong goes0→1→2→1→0. After adding 1 and modulo, it becomes1→2→0→2→1, which is no longer symmetric.🐛 Suggested fix: apply start_phase as timing offset
- int elapsed = static_cast<int>((global_ms + static_cast<int64_t>(tile_offset) * 500) % cycle_total); + // Apply start_phase as a timing offset (fraction of total duration) + int phase_offset_ms = (static_cast<int64_t>(start_phase) * total) / frames; + int elapsed = static_cast<int>((global_ms + static_cast<int64_t>(tile_offset) * 500 + phase_offset_ms) % cycle_total); if (elapsed >= total) elapsed = cycle_total - elapsed; for (int phase = 0; phase < static_cast<int>(item.frame_durations.size()); ++phase) { int dur = getPhaseDuration(item, phase); if (elapsed < dur) - return (phase + start_phase) % frames; + return phase; elapsed -= dur; } - return start_phase; + return 0;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` around lines 66 - 72, The ping-pong duration branch incorrectly applies start_phase as a modular shift to the final phase index ((phase + start_phase) % frames); instead incorporate start_phase as a timing/phase offset before deciding which phase the elapsed time falls into (mirroring the approach used at line 58). Adjust the computation so you convert start_phase into an initial time/phase offset (or add it to the raw phase index prior to ping-pong reflection) and subtract that from elapsed (or add to the raw phase) before iterating getPhaseDuration(item, phase) and returning phase; use the same treatment of start_phase used elsewhere to preserve symmetric ping-pong order for item.frame_durations, getPhaseDuration, frames and start_phase.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ImguiMapEditor/Rendering/Map/TileRenderer.cpp`:
- Around line 199-208: The corpse-correction loop may double-render items that
are both multi-tile lying corpses and marked is_on_bottom; update the loop that
iterates render_cache_ (the block that checks type->is_lying_object and
type->width/height) to skip any item whose type->is_on_bottom is true so they
aren't re-queued here; locate the corpse correction pass around the call to
item_renderer_.queueWithColor and add a guard (check type->is_on_bottom) before
calling queueWithColor to avoid double-rendering items also handled in
queueAll's on-bottom pass.
In `@ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp`:
- Around line 47-48: The fallback for computing dir uses a hardcoded "2 %
pattern_x" which incorrectly wraps directions; replace that logic to wrap using
the actual direction value like drawMount does: compute pattern_x as now (const
int pattern_x = std::max<int>(1, outfit_data->pattern_x)) and set dir by taking
direction % pattern_x (or direction < pattern_x ? direction : direction %
pattern_x) so the wrap uses the direction variable and not the literal 2; update
the code around variables pattern_x, dir and reference outfit_data to match
drawMount's wrapping behavior that uses mount_data->pattern_x.
In `@ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp`:
- Around line 289-290: The code hardcodes isSelected=false before calling
Utils::RenderPreviewCard, so clicked tiles never render as selected; replace the
hardcoded flag by computing whether the current tile/texture (tex) equals the
widget's current selection (e.g., compare to the widget's selected texture/index
member) and pass that boolean to RenderPreviewCard, and ensure the click
handling (the clicked variable) updates that selected member so subsequent calls
to RenderPreviewCard will render selected styling; look for references to
RenderPreviewCard, getIconSize(), the local isSelected/clicked variables and the
TilesetGridWidget's selected texture/index state to implement this change.
---
Duplicate comments:
In `@ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp`:
- Around line 66-72: The ping-pong duration branch incorrectly applies
start_phase as a modular shift to the final phase index ((phase + start_phase) %
frames); instead incorporate start_phase as a timing/phase offset before
deciding which phase the elapsed time falls into (mirroring the approach used at
line 58). Adjust the computation so you convert start_phase into an initial
time/phase offset (or add it to the raw phase index prior to ping-pong
reflection) and subtract that from elapsed (or add to the raw phase) before
iterating getPhaseDuration(item, phase) and returning phase; use the same
treatment of start_phase used elsewhere to preserve symmetric ping-pong order
for item.frame_durations, getPhaseDuration, frames and start_phase.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: 31cf9e9b-7470-4bba-964a-f9aae4a32b08
📒 Files selected for processing (16)
ImguiMapEditor/Domain/ItemType.hImguiMapEditor/Rendering/Animation/ItemAnimation.cppImguiMapEditor/Rendering/Animation/ItemAnimation.hImguiMapEditor/Rendering/Map/TileRenderer.cppImguiMapEditor/Rendering/Tile/CreatureRenderer.cppImguiMapEditor/Rendering/Tile/CreatureRenderer.hImguiMapEditor/Rendering/Tile/ItemRenderer.hImguiMapEditor/Services/ClientDataService.cppImguiMapEditor/Services/ItemCompositor.cppImguiMapEditor/UI/Utils/PreviewUtils.cppImguiMapEditor/UI/Utils/PreviewUtils.hppImguiMapEditor/UI/Widgets/Properties/PropertyPanelRenderer.cppImguiMapEditor/UI/Widgets/TilesetGridWidget.cppImguiMapEditor/UI/Widgets/TilesetGridWidget.hImguiMapEditor/UI/Widgets/TilesetWidget.cppImguiMapEditor/UI/Windows/BrowseTile/ItemsListRenderer.cpp
Refactored animation and rendering logic to unify sprite index calculation and fix rendering order issues. Consolidated UI brush card rendering and streamlined preview card interactions. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — fixed ping-pong animation start phase offset and reflection - `ImguiMapEditor/Rendering/Map/TileRenderer.cpp` — removed redundant multi-tile corpse rendering pass - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — extracted mount frame selection into helper - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.h` — added `selectMountFrame` declaration - `ImguiMapEditor/Rendering/Tile/ItemRenderer.h` — updated pass 2 rendering to forward iteration - `ImguiMapEditor/Services/ItemCompositor.cpp` — migrated to `SpriteUtils::getSpriteIndex` - `ImguiMapEditor/UI/Utils/PreviewUtils.cpp` — simplified `RenderPreviewCard` interaction logic - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp` — consolidated card rendering into `renderBrushCard` - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.h` — added `renderBrushCard` declaration - `ImguiMapEditor/Utils/SpriteUtils.cpp` — added `Domain::ItemType` overload for sprite index calculation - `ImguiMapEditor/Utils/SpriteUtils.h` — added `Domain::ItemType` overload declaration and missing header
There was a problem hiding this comment.
📋 Review Summary
This PR introduces significant improvements to item rendering and UI consistency. Key highlights include:
- Mount Rendering Support: Well-implemented support for mounts with proper coloring and animation selection.
- Improved Item Compositing: Multi-layer and multi-tile items are now composited correctly for UI previews.
- Rendering Order Fix: Switching from REVERSE to FORWARD iteration in
queueAllfixes long-standing item stacking issues. - UI Consistency: The new
RenderPreviewCardutility unifies the look and feel of item/creature previews across the application.
🔍 General Feedback
While the core changes are solid, I've noted a few areas for refinement:
- Animation Logic: There's a slight discrepancy between index-based and duration-based ping-pong animations. The current duration-based approach doubles the time spent on "peak" frames.
- Lying Objects: The
is_lying_objectflag was added but isn't explicitly used to overrideis_on_topbehavior in the rendering passes. - Safety: Added a suggestion for a null/empty check in
ItemCompositorto prevent potential crashes with malformed item data.
Overall, a very positive set of changes that enhances the editor's visual fidelity.
| int total = static_cast<int>(item.total_duration); | ||
| if (total <= 0) return start_phase; | ||
| int cycle_total = total * 2; | ||
| int raw_elapsed = static_cast<int>((global_ms + static_cast<int64_t>(tile_offset) * 500) % cycle_total); |
There was a problem hiding this comment.
🟡 Consistency Issue: There is a slight inconsistency between the index-based ping-pong logic (getPingPongPhase uses 2*frames - 2) and the duration-based logic here (uses 2*total).
The duration-based version effectively doubles the duration of the "peak" frames (first and last), resulting in a sequence like (0, 1, 2, 2, 1, 0), whereas the index-based version results in (0, 1, 2, 1).
While often acceptable in an editor, for strict OTClient parity, you might want to use a cycle of 2*total - duration(first) - duration(last).
| int raw_elapsed = static_cast<int>((global_ms + static_cast<int64_t>(tile_offset) * 500) % cycle_total); | ||
| // Apply start_phase offset before reflection | ||
| int elapsed = (raw_elapsed + start_phase * (total / std::max(1, frames))) % cycle_total; | ||
| // Reflect: map [total, cycle_total) back to [0, total) |
There was a problem hiding this comment.
🟡 Redundant Calculation: raw_elapsed already includes tile_offset * 500 ms, which desynchronizes animations per tile.
int raw_elapsed = static_cast<int>((global_ms + static_cast<int64_t>(tile_offset) * 500) % cycle_total);Adding start_phase * (total / frames) to it might result in "double-shifting" the animation start point if start_frame == 255. If this is intended for parity with a specific client behavior, please disregard.
| } | ||
| if (entry.type && entry.type->is_on_bottom) continue; | ||
| if (entry.type && entry.type->is_on_top) continue; | ||
| renderItem(entry); |
There was a problem hiding this comment.
🟡 Implementation Gap: The is_lying_object flag was added to ItemType, but it doesn't appear to be used in the rendering passes yet.
To fulfill the PR's claim of "Corrected rendering order for multi-tile lying objects (now render before creatures)", you might need to handle cases where a corpse is marked as is_on_top in the DAT (which would otherwise defer it to the post-creature pass).
Consider explicitly including lying objects in queueAll even if they have the is_on_top flag:
if (entry.type && entry.type->is_on_top && !entry.type->is_lying_object) continue;| if (it != cache_.end()) { | ||
| return it->second.get(); | ||
| } | ||
|
|
There was a problem hiding this comment.
🔴 Safety Issue: If type->sprite_ids is empty, this will crash. While isValidForRendering() usually catches this, it's safer to check explicitly here before accessing [0].
| if (type->width == 1 && type->height == 1 && type->layers <= 1 && !type->sprite_ids.empty()) { |
| const int w = std::max<int>(1, type->width); | ||
| const int h = std::max<int>(1, type->height); | ||
| const int layers = std::max<int>(1, type->layers); | ||
| const int composite_size = std::max(w, h) * 32; |
There was a problem hiding this comment.
🟢 Refinement: Using std::max<int>(1, ...) for loop bounds and denominators is good practice here to prevent division by zero or infinite loops if data is malformed.
| Services::SpriteManager& spriteManager, | ||
| const Domain::Outfit& outfit) { | ||
| // Basic validity check for outfit could go here, but helper handles it. | ||
| return GetCreaturePreviewImpl(clientData, spriteManager, outfit); |
There was a problem hiding this comment.
🟢 Excellent Addition: This unified card renderer significantly improves UI consistency across the editor. The use of standard ImGui theme colors instead of hardcoded values is also a great improvement.
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp`:
- Around line 57-72: The current ping-pong uses a uniform 2*item.total_duration
cycle and shifts start_phase by total/frames which breaks uneven
frame_durations; instead build an explicit timeline using per-phase durations:
create a vector of phase durations from item.frame_durations via
getPhaseDuration(item, phase), append the reverse sequence (omit duplicate
end-phase) to form the ping-pong cycle, compute the cycle length as the sum of
those durations, compute the start offset by summing durations of the first
start_phase phases, add (global_ms + tile_offset*500) mod cycle_length, then
walk the per-phase durations to find and return the active phase (mod frames
where appropriate). Use symbols item.frame_durations, getPhaseDuration,
item.total_duration, start_phase, global_ms, tile_offset and frames to locate
and replace the current logic.
In `@ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp`:
- Around line 153-159: selectMountFrame() may return an index into
mount_data->idle_frames but drawMount() only swaps mount_sprites to
mount_data->walk_sprite_ids; update drawMount() so it picks the correct sprite
array based on the frame group (idle vs walk) before indexing: inspect
selectMountFrame's return or mount_data flags and set mount_sprites to
mount_data->idle_sprite_ids when idle_frames are used (and to
mount_data->walk_sprite_ids when walking), falling back to
mount_data->sprite_ids only if specific idle/walk sprite vectors are empty;
apply the same fix for the other occurrence around the 208-217 block so idle
mounted creatures use idle_sprite_ids instead of sprite_ids.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: ae1e7c86-8717-403e-aa94-b90ed3bf907b
📒 Files selected for processing (11)
ImguiMapEditor/Rendering/Animation/ItemAnimation.cppImguiMapEditor/Rendering/Map/TileRenderer.cppImguiMapEditor/Rendering/Tile/CreatureRenderer.cppImguiMapEditor/Rendering/Tile/CreatureRenderer.hImguiMapEditor/Rendering/Tile/ItemRenderer.hImguiMapEditor/Services/ItemCompositor.cppImguiMapEditor/UI/Utils/PreviewUtils.cppImguiMapEditor/UI/Widgets/TilesetGridWidget.cppImguiMapEditor/UI/Widgets/TilesetGridWidget.hImguiMapEditor/Utils/SpriteUtils.cppImguiMapEditor/Utils/SpriteUtils.h
💤 Files with no reviewable changes (1)
- ImguiMapEditor/Rendering/Map/TileRenderer.cpp
Improved ping-pong animation calculation, refined mount sprite selection for idle/walk states, added safety checks for item rendering, and highlighted the selected brush in the tileset grid. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — refactored ping-pong animation to use explicit timeline based on per-phase durations - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — updated mount sprite selection to use idle groups when available - `ImguiMapEditor/Services/ItemCompositor.cpp` — added safety check for empty `sprite_ids` in single-tile item rendering - `ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp` — added highlighting for selected brush in tileset grid
There was a problem hiding this comment.
📋 Review Summary
This PR introduces significant improvements to item and creature rendering, including mount support, ping-pong animations, and corrected depth sorting for common items.
🔍 General Feedback
- Correctness: Found a logic error in creature direction mapping for outfits with fewer than 4 directions.
- Performance: The refactored
ItemAnimation::getPhasemethod performs multiplestd::vectorallocations in a hot path, which could degrade frame rates on large maps. - Completeness: The
is_lying_objectproperty is introduced but not explicitly used in the rendering pipeline, although the general stacking order fix for Pass 2 partially addresses the described issue. - UI: The new
RenderPreviewCardutility is a great addition for UI consistency.
Please address the performance and direction mapping issues before merging.
| // Apply selection dimming | ||
| if (creature->isSelected()) { | ||
| creature_color.r *= 0.5f; | ||
| creature_color.g *= 0.5f; |
There was a problem hiding this comment.
🔴 Incorrect direction mapping for creatures with < 4 directions.
The current logic:
int dir = direction < pattern_x ? direction : 2 % pattern_x;will map South (2) and West (3) to index 0 (North) if pattern_x is 2. This is likely incorrect for creatures that only have South and East directions. Using a modulo or a more specific mapping table would be safer.
| creature_color.g *= 0.5f; | |
| int dir = direction % pattern_x; |
| std::vector<int> timeline; | ||
| timeline.reserve(num_phases * 2); | ||
| for (int i = 0; i < num_phases; ++i) | ||
| timeline.push_back(i); |
There was a problem hiding this comment.
🟠 Performance: Memory allocations in hot path.
std::vector<int> timeline and phase_durs are allocated on every call to getPhase. Since this method is called for every animated item on screen every frame, this will lead to significant allocator churn.
Consider using a fixed-size stack array or caching these timelines, as animation data for a specific ItemType is typically static.
| int frames = std::max<int>(item.frames, 1); | ||
| if (frames <= 1) | ||
| return 0; | ||
|
|
There was a problem hiding this comment.
🟢 Restricted async animation check.
bool is_async = !item.frame_durations.empty() && item.animation_mode == 0;This check requires duration data for an item to be considered asynchronous. However, some items without explicit duration data (defaulting to 500ms) might still be intended to run asynchronously if animation_mode == 0. Consider checking only the animation_mode.
| // ============================================================ | ||
|
|
||
| // PASS 1: OnBottom items (priority 2) - FORWARD | ||
| // PASS 1: OnBottom items — forward |
There was a problem hiding this comment.
🟠 Unused property: is_lying_object.
The property is_lying_object is correctly parsed from the data files but is never used in the rendering passes. If these items should be rendered with higher priority (e.g., in Pass 1 with walls/ground decorations) to ensure they are always beneath creatures even if they are high in the tile stack, they should be checked here.
// PASS 1: OnBottom items — forward
for (const auto &entry : items) {
if (entry.type && (entry.type->is_on_bottom || entry.type->is_lying_object)) {
renderItem(entry);
}
}There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp (1)
47-48:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winInconsistent direction fallback logic persists.
The fallback
2 % pattern_xuses a hardcoded literal2instead of wrapping the actualdirection. Compare withdrawMountat line 151 which correctly usesdir % std::max<int>(1, mount_data->pattern_x).For example, if
pattern_x = 2anddirection = 3, the result is2 % 2 = 0, not3 % 2 = 1.Proposed fix
const int pattern_x = std::max<int>(1, outfit_data->pattern_x); - int dir = direction < pattern_x ? direction : 2 % pattern_x; + int dir = direction % pattern_x;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` around lines 47 - 48, The fallback for computing dir is wrong: replace the hardcoded `2` with the actual `direction` so the wrap uses `direction % pattern_x`; specifically update the computation that uses `pattern_x` (from `outfit_data->pattern_x`) and `direction` to mirror the correct logic used in `drawMount`/`mount_data->pattern_x` (i.e., compute `pattern_x = std::max<int>(1, outfit_data->pattern_x)` then set `dir` to `direction` if less than `pattern_x`, otherwise use `direction % pattern_x`).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp`:
- Around line 332-334: The cross-selected call to renderBrushCard currently
omits the isPulsing and pulseElapsed arguments (relying on defaults) so the
preview never pulses; update the call that computes isCrossSelected (where
brush->getName() == selectedBrushName_) to pass the same pulse state used
elsewhere: compute or reuse the local isPulsing and pulseElapsed variables
(matching other renderBrushCard call sites) and call renderBrushCard(cursorPos,
tileSize, tex, isCrossSelected, ImGui::IsItemHovered(), isPulsing, pulseElapsed)
so the cross-tileset preview animates consistently.
---
Duplicate comments:
In `@ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp`:
- Around line 47-48: The fallback for computing dir is wrong: replace the
hardcoded `2` with the actual `direction` so the wrap uses `direction %
pattern_x`; specifically update the computation that uses `pattern_x` (from
`outfit_data->pattern_x`) and `direction` to mirror the correct logic used in
`drawMount`/`mount_data->pattern_x` (i.e., compute `pattern_x = std::max<int>(1,
outfit_data->pattern_x)` then set `dir` to `direction` if less than `pattern_x`,
otherwise use `direction % pattern_x`).
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: ASSERTIVE
Plan: Pro
Run ID: e6fea9d0-af3d-45a0-ae55-81190b1d49cd
📒 Files selected for processing (4)
ImguiMapEditor/Rendering/Animation/ItemAnimation.cppImguiMapEditor/Rendering/Tile/CreatureRenderer.cppImguiMapEditor/Services/ItemCompositor.cppImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp
| bool isCrossSelected = brush->getName() == selectedBrushName_; | ||
| renderBrushCard(cursorPos, tileSize, tex, isCrossSelected, | ||
| ImGui::IsItemHovered()); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check the function signature in the header to verify if default parameters exist
rg -A5 'renderBrushCard' ImguiMapEditor/UI/Widgets/TilesetGridWidget.hRepository: Open-Tibia-Tools/tibia-imgui-map-editor
Length of output: 366
Cross-tileset path disables pulse animation by relying on renderBrushCard defaults
renderBrushCard declares bool isPulsing = false and float pulseElapsed = 0.0f, so the 5-argument call compiles—but it also means the cross-tileset preview never enables pulse animation, unlike other call sites that pass isPulsing/pulseElapsed. Pass the computed pulse values in the cross-selected path to keep behavior consistent.
🧰 Tools
🪛 Cppcheck (2.20.0)
[style] 332-332: The function 'isReadable' is never used.
(unusedFunction)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@ImguiMapEditor/UI/Widgets/TilesetGridWidget.cpp` around lines 332 - 334, The
cross-selected call to renderBrushCard currently omits the isPulsing and
pulseElapsed arguments (relying on defaults) so the preview never pulses; update
the call that computes isCrossSelected (where brush->getName() ==
selectedBrushName_) to pass the same pulse state used elsewhere: compute or
reuse the local isPulsing and pulseElapsed variables (matching other
renderBrushCard call sites) and call renderBrushCard(cursorPos, tileSize, tex,
isCrossSelected, ImGui::IsItemHovered(), isPulsing, pulseElapsed) so the
cross-tileset preview animates consistently.
Replaced dynamic heap-allocated std::vectors with fixed-size stack arrays to eliminate allocation overhead in animation phase calculation, and simplified creature direction mapping. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — migrated timeline and phase duration buffers to stack arrays with constant size limits, removed unnecessary vector headers - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — simplified direction mapping logic to a direct modulo operation
There was a problem hiding this comment.
📋 Review Summary
Overall, this PR introduces significant improvements to the rendering pipeline, especially regarding animation timing (ping-pong support) and creature mounts. The introduction of interactive preview cards also enhances the UI consistency.
However, there are a few critical and high-priority issues that should be addressed:
- 🔴 Buffer Overflow in
ItemAnimation.cpp: The ping-pong timeline construction for animations with custom durations can overflow the stack-allocatedtimelinebuffer and accessphase_dursout-of-bounds ifnum_phasesexceedsMAX_PHASES(32). - 🟠 Incorrect Mount Rendering:
CreatureRenderersetsz_pattern = 1for all mounted creatures without verifying if the outfit's DAT data actually supports multiple Z patterns. This will lead to incorrect sprite lookups for older client versions or simple outfits. - 🟡 Unused
is_lying_object: Theis_lying_objectproperty was added toItemTypeand merged from DAT, but it is not yet utilized inItemRendererto control rendering order, which was one of the stated goals in the PR summary. - 🟡 Code Duplication: Identical logic for
getSpriteIndexis implemented for bothClientItemandItemTypeinSpriteUtils.cpp.
🔍 General Feedback
- Performance: The move to forward-order rendering in
ItemRenderer(painter's algorithm) is a great fix for depth issues. - Maintainability: The consolidation of preview logic into
RenderPreviewCardis a good architectural move. - Testing: Ensure that ping-pong animations and multi-tile mount offsets are verified with different client versions (especially 7.6 vs 10.x+).
|
|
||
| if (base_sprite_idx >= sprites->size()) { | ||
| continue; | ||
| // Draw mount before rider |
There was a problem hiding this comment.
🟠 Missing Pattern Z Check: Setting z_pattern = 1 assumes the outfit data actually has at least 2 Z patterns (one for rider, one for rider+mount). If outfit_data->pattern_z is 1, passing z_pattern = 1 to getSpriteIndex will result in an incorrect (likely out-of-bounds) index.
| // Draw mount before rider | |
| int z_pattern = 0; | |
| if (outfit.lookMount > 0 && outfit_data->pattern_z > 1) { | |
| z_pattern = 1; |
| } | ||
|
|
||
| int ItemAnimation::getPhaseDuration(const Domain::ItemType &item, int phase) { | ||
| if (phase < 0 || phase >= static_cast<int>(item.frame_durations.size())) |
There was a problem hiding this comment.
🟢 Type Safety: The return value of getPhaseDuration is cast to int, but the calculation (d.first + d.second) / 2 could potentially overflow if d.first and d.second are large. While unlikely for frame durations, using int64_t for the sum or static casting the operands to int64_t before addition would be safer.
| int cycle_length = 0; | ||
| for (int i = 0; i < num_phases && i < MAX_PHASES; ++i) { | ||
| phase_durs[i] = getPhaseDuration(item, i); | ||
| cycle_length += phase_durs[i]; |
There was a problem hiding this comment.
🔴 Potential Buffer Overflow: If num_phases (from item.frame_durations.size()) exceeds MAX_PHASES, the second loop will use i values starting from num_phases - 2, which could be much larger than MAX_PHASES. These values are used as indices in phase_durs and added to timeline, potentially overflowing both or causing out-of-bounds access.
Additionally, the second loop does not check if timeline_len exceeds MAX_TIMELINE.
| cycle_length += phase_durs[i]; | |
| const int effective_phases = std::min<int>(num_phases, MAX_PHASES); | |
| for (int i = 0; i < effective_phases; ++i) | |
| timeline[timeline_len++] = i; | |
| for (int i = effective_phases - 2; i >= 1; --i) | |
| timeline[timeline_len++] = i; |
| const int height = std::max<int>(1, item->height); | ||
| const int layers = std::max<int>(1, item->layers); | ||
| const int pattern_x_count = std::max<int>(1, item->pattern_x); | ||
| const int pattern_y_count = std::max<int>(1, item->pattern_y); |
There was a problem hiding this comment.
🟡 Code Duplication: The implementation of getSpriteIndex for ItemType is identical to the one for ClientItem. Consider using a template or a private helper function to avoid logic duplication.
Clamped animation phase counts to prevent potential overflows and added a pattern depth check when rendering mounts. - `ImguiMapEditor/Rendering/Animation/ItemAnimation.cpp` — clamped loop iterations to `MAX_PHASES` to prevent potential out-of-bounds access - `ImguiMapEditor/Rendering/Tile/CreatureRenderer.cpp` — added `outfit_data->pattern_z > 1` check when deciding to render a mount
There was a problem hiding this comment.
📋 Review Summary
This PR significantly improves the rendering quality and correctness for items and creatures. Key highlights include:
- Mount Rendering: Correct implementation of mount visuals with proper state handling (idle/walk).
- Item Composition: Fixed UI icon rendering for multi-layer and multi-tile items using correct sprite indexing.
- Animation Logic: Added support for Ping-pong animations and start-frame offsets.
- UI Improvements: Introduced interactive preview cards for a more consistent look across the editor.
🔍 General Feedback
The architectural changes in the rendering pipeline are solid and improve maintainability. I've noted a few minor points regarding an unused property and animation synchronization edge cases, but overall this is a high-quality contribution.
One specific concern is the is_lying_object property which appears to be currently unused despite being mentioned in the PR summary as a fix for rendering depth. If this was intended to force corpses into a specific rendering pass, that logic seems to be missing.
| bool is_locked = false; // Door key lock status (for highlight_locked_doors) | ||
|
|
||
| // Lying object (multi-tile corpses) — drawn before creatures | ||
| bool is_lying_object = false; |
There was a problem hiding this comment.
🟡 Unused property: The is_lying_object property is merged into ItemType from DAT, but it is not utilized in the ItemRenderer or any other part of the rendering pipeline.
The PR summary mentions: "Multi-tile “lying” items now render beneath creatures for correct depth.", but without logic checking this flag in ItemRenderer::queueAll (e.g., to move these items to Pass 1), they will continue to render in Pass 2 (Common items), which is already beneath creatures.
If the intention was to specifically handle multi-tile corpses that might overlap creatures on adjacent tiles due to isometric rendering order, further logic in ItemRenderer would likely be needed.
|
|
||
| int start_phase = 0; | ||
| if (item.start_frame == 255 && is_async) { | ||
| start_phase = (tile_x * 17 + tile_y * 31 + tile_z * 7) % frames; |
There was a problem hiding this comment.
🟡 Animation Synchronization: The is_async flag is only set if frame_durations is not empty. This means that items using a static 500ms frame duration (common for many Tibia items) will always have tile_offset = 0, causing their animations to be perfectly synchronized across all tiles even if animation_mode == 0 (Async) was requested.
Consider allowing tile_offset for items without explicit durations if animation_mode == 0.
| Utils::SpriteUtils::loadDecodedSprite(spr_reader_, sprite_id); | ||
| if (sprite_data.size() < 32 * 32 * 4) | ||
| continue; | ||
|
|
There was a problem hiding this comment.
🟢 Excellent Fix: Moving to Utils::SpriteUtils::getSpriteIndex and adding the layer loop correctly handles multi-layer items (like those with templates/colorable parts) in the UI icons. The previous implementation was only correct for simple single-layer items.
| continue; | ||
|
|
||
| uint32_t template_id = 0; | ||
| if (layers >= 2) { |
There was a problem hiding this comment.
🟢 Robust Implementation: The mount rendering logic correctly handles the interaction between rider animation state (idle vs walk) and mount sprite selection. The addition of z_pattern logic for outfits with mounts is also correct for OTClient/Tibia parity.
| // PASS 1: OnBottom items (priority 2) - FORWARD | ||
| // PASS 1: OnBottom items — forward | ||
| for (const auto &entry : items) { | ||
| if (entry.type && entry.type->is_on_bottom) { |
There was a problem hiding this comment.
🟢 Improved Readability: The refactored multi-pass rendering logic is much cleaner and easier to reason about while maintaining the correct OTClient painter's algorithm order.
Summary by CodeRabbit
New Features
Bug Fixes
Improvements