feat(lighting): support for OTClient style light rendering #28
feat(lighting): support for OTClient style light rendering #28karolak6612 wants to merge 6 commits into
Conversation
…ation Introduced a comprehensive lighting system mimicking the server's WorldLight protocol, replacing simple ambient levels with global intensity/color settings, ground-based light blocking, and translucent propagation. Added support for creature-based light sources and integrated refined controls into both the main map editor and ingame preview UI. - `ImguiMapEditor/Application/CallbackMediator.cpp` — added server light intensity/color to mediator state - `ImguiMapEditor/Application/EditorSession.h` — updated session lighting defaults - `ImguiMapEditor/Domain/CreatureType.h` — added light level/color properties to creature types - `ImguiMapEditor/Domain/ItemType.h` — added `lens_help` for translucent light propagation - `ImguiMapEditor/Domain/LightTypes.h` — defined `GlobalLight` struct and updated `LightConfig` to support server-style global lighting - `ImguiMapEditor/Domain/Tile.h` — added `TranslucentLight` flag - `ImguiMapEditor/IO/CreatureXmlReader.cpp` — parsed light properties from creature definitions - `ImguiMapEditor/Presentation/MainWindow.cpp` — updated lighting toolbar with intensity/color sliders - `ImguiMapEditor/Rendering/Frame/RenderState.h` — added config hash for cache invalidation - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — implemented light blocking logic and translucent ground propagation - `ImguiMapEditor/Rendering/Light/LightGatherer.h` — added `GroundBrightness` tracking for occlusion - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — implemented multi-layered ambient light blending and ground-based light occlusion - `ImguiMapEditor/Rendering/Light/LightManager.h` — passed `GroundBrightness` and optional light injection to renderer - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp` — added player light injection and updated lighting configuration - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h` — added config hash tracking - `ImguiMapEditor/Rendering/Passes/LightingPass.cpp` — switched to new server-based lighting configuration and validation - `ImguiMapEditor/Services/ClientDataService.cpp` — exposed `lens_help` to merged item data - `ImguiMapEditor/Services/ViewSettings.cpp` — updated config load/save for new server light parameters - `ImguiMapEditor/Services/ViewSettings.h` — added server light settings fields - `ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp` — updated preview UI with intensity/color controls
…l grid Moved ground-blocking logic from chunk-specific tracking to a viewport-level grid to support consistent light occlusion across multiple floors. Refactored LightGatherer and LightManager to use the new `ViewportGroundBlocking` struct instead of `GroundBrightness`. - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — removed `GroundBrightness` tracking, updated `gatherForChunkMultiFloor` and `gatherLightsFromNeighborChunk` to use `ViewportGroundBlocking` - `ImguiMapEditor/Rendering/Light/LightGatherer.h` — replaced `GroundBrightness` with `ViewportGroundBlocking` struct - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — initialized `viewport_blocking_`, updated `render` and `computeChunkLight` signature and logic to use the new blocking grid - `ImguiMapEditor/Rendering/Light/LightManager.h` — added `viewport_blocking_` member, updated `computeChunkLight` signature
Refactored lighting system to use a central `ViewportLightBuffer` and integrated floor visibility calculations based on user map clicks, mimicking RME behavior for improved accuracy. - `ImguiMapEditor/Rendering/Core/IRenderPass.h` — added `light_visibility_origin` to `RenderContext` - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — implemented `ViewportLightBuffer` collection and RME-style light occlusion/propagation logic - `ImguiMapEditor/Rendering/Light/LightGatherer.h` — replaced `ViewportGroundBlocking` with `ViewportLightBuffer` for unified light/occlusion data - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — integrated `gatherViewportLightBuffer` and `computeViewportLight` into light render loop - `ImguiMapEditor/Rendering/Light/LightManager.h` — added `renderClientVisible` with visibility origin support, removed light cache - `ImguiMapEditor/Rendering/Map/MapRenderer.cpp` — added `light_visibility_origin_` tracking and passed to render context - `ImguiMapEditor/Rendering/Map/MapRenderer.h` — added `light_visibility_origin_` storage - `ImguiMapEditor/Rendering/Passes/LightingPass.cpp` — switched to `renderClientVisible` using context visibility origin - `ImguiMapEditor/Rendering/Passes/LightingPass.h` — removed unused floor range tracking members - `ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp` — refined `tileLimitsFloorsView` and `isLookPossible` using explicit item flags - `ImguiMapEditor/UI/Map/MapPanel.cpp` — pushed visibility origin to renderer - `ImguiMapEditor/UI/Map/MapPanelInput.cpp` — updated visibility origin on mouse clicks and floor changes - `ImguiMapEditor/UI/Map/MapPanelInput.h` — added `light_visibility_origin` tracking
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 a significant overhaul to the lighting system within the map editor to align with OTClient's rendering behavior. By implementing a more robust light gathering and rendering pipeline, the editor now correctly handles floor-based light occlusion and translucency. These changes include updates to domain models for creatures and items, improved UI controls for fine-tuning lighting parameters, and a refactored rendering engine that provides a more accurate representation of the game world's lighting. 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
|
Reviewer's GuideRefactors the lighting system to emulate OTClient/RME-style light rendering by replacing chunk-based caching with a viewport light buffer, integrating server global light and client minimum ambient sliders, adding floor visibility–aware light gathering (including translucent propagation and creature lights), and exposing new lighting controls in the UI and configuration paths. File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
WalkthroughThis PR replaces chunk-cache lighting with a viewport-buffer pipeline. It adds a server/global light model (GlobalLight), extends LightConfig and domain types for creature/item light/lens data, introduces ViewportLightBuffer and a refactored LightGatherer to populate it, updates LightManager to compute viewport lighting (optionally merging injected lights) and invalidate on a LightConfig hash, persists server-light settings through ViewSettings/EditorSession and tab callbacks, adds UI pickers/sliders, and threads an optional light-visibility origin from input into rendering. Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
📋 Review Summary
This PR introduces a significant rework of the lighting system to support OTClient-style light rendering, including server-side global light settings and client-side ambient sliders. It also improves floor visibility logic to better match client behavior.
🔍 General Feedback
- Correctness: Identified a potential regression in
FloorVisibilityCalculator::isLookPossiblewhere null tiles are treated as non-transparent. - Maintainability: There is some code duplication (projected floor offset calculation) and use of magic numbers for default light settings.
- Performance: The removal of the light cache simplifies the system but introduces a full viewport re-render for lighting on every change. This should be monitored for performance impacts.
- Completeness: The PR is comprehensive in its UI and rendering updates, but would benefit from unit tests for the complex visibility and light gathering logic.
Overall, the rework brings the editor much closer to in-game rendering accuracy. Fixing the null-tile visibility check is recommended before merging.
| void LightGatherer::clear() { | ||
| lights_.clear(); | ||
| namespace { | ||
|
|
There was a problem hiding this comment.
🟠 This helper function is duplicated here and in LightManager.cpp. It should be moved to a common header (e.g., Config.h or a utility file) to avoid redundancy.
| @@ -35,39 +49,44 @@ bool LightManager::initialize() { | |||
| } | |||
|
|
|||
| void LightManager::invalidateTile(int32_t x, int32_t y) { | |||
There was a problem hiding this comment.
🟡 With the removal of LightCache, fine-grained invalidation is no longer possible. While this simplifies the implementation, it means any map change (no matter how small) will cause a full re-gather and re-compute of the viewport lighting. This might be a performance bottleneck for very large viewports or slow CPUs. Consider if a simple bounding-box check could be used to only re-render light if the change is within the viewport.
| // === Lighting Settings === | ||
| bool map_lighting_enabled = false; // Enable lighting in main map viewport | ||
| int map_ambient_light = 128; // 0 = dark, 255 = full bright | ||
| int map_ambient_light = 0; // Client minimum ambient (0-255), acts as floor |
There was a problem hiding this comment.
🟡 Magic numbers 215 (default color) and 200 (default intensity) are used here and in other files. It would be better to define them as named constants in Domain::LightConfig or Core::Config.
| if (ground) { | ||
| const Domain::ItemType* ground_type = getItemType(ground); | ||
| if (ground_type && ground_type->blocks_projectile) { | ||
| if (ground_type && |
There was a problem hiding this comment.
🔴 Returning false for !tile (null tiles) seems incorrect. Empty space should be transparent to light and view. The previous implementation correctly returned true. This change might cause visibility issues in areas with empty tiles.
| if (ground_type && | |
| bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { | |
| if (!tile) return true; // Empty tile is transparent |
| uint8_t ambient_color = 215; // DEPRECATED: kept for backward compat, use global_light.color | ||
| uint8_t ambient_level = 255; // DEPRECATED: kept for backward compat, computed from global_light + slider | ||
| }; | ||
|
|
There was a problem hiding this comment.
🟢 The comment says ambient_level is "computed from global_light + slider", but the code only assigns global_light.intensity. The actual combined computation happens in LightManager::computeViewportLight. The comment should be updated to reflect that these are now purely for backward compatibility and might not reflect the final rendered intensity.
There was a problem hiding this comment.
Code Review
This pull request implements RME-style viewport lighting and floor visibility culling by introducing a global viewport light buffer, tracking server-side global light settings alongside a client minimum ambient slider, and propagating light through translucent tiles. However, two critical issues were identified in the visibility culling logic: tileLimitsFloorsView incorrectly ignores secondary items on a tile and early-exits on IgnoreLook flags, and isLookPossible incorrectly treats empty tiles as opaque blockers instead of transparent space.
| bool FloorVisibilityCalculator::tileLimitsFloorsView(const Domain::Tile* tile, bool is_free_view) const { | ||
| if (!tile) return false; | ||
|
|
||
| // Check ground first | ||
| const Domain::Item* ground = tile->getGround(); | ||
| if (!ground) return false; | ||
|
|
||
| const Domain::ItemType* ground_type = getItemType(ground); | ||
| if (!ground_type) return false; | ||
|
|
||
| // Items with isDontHide never block view | ||
| if (ground_type->is_dont_hide) return false; | ||
|
|
||
| // Ground tiles block view | ||
| if (ground_type->is_ground) return true; | ||
|
|
||
| // isOnBottom items (walls) block view | ||
| if (ground_type->is_on_bottom) { | ||
| if (is_free_view) { | ||
| // Free view: any wall blocks | ||
| return true; | ||
| } else { | ||
| // Player view: only walls that block projectiles | ||
| return ground_type->blocks_projectile; | ||
| } | ||
|
|
||
| const Domain::Item* first_thing = tile->getGround(); | ||
| if (!first_thing && !tile->getItems().empty()) { | ||
| first_thing = tile->getItems().front().get(); | ||
| } | ||
|
|
||
| // Check other items on tile for blocking | ||
| for (const auto& item : tile->getItems()) { | ||
| const Domain::ItemType* item_type = getItemType(item.get()); | ||
| if (!item_type) continue; | ||
|
|
||
| if (item_type->is_dont_hide) continue; | ||
|
|
||
| if (item_type->is_ground) return true; | ||
|
|
||
| if (item_type->is_on_bottom) { | ||
| if (is_free_view) { | ||
| return true; | ||
| } else { | ||
| if (item_type->blocks_projectile) return true; | ||
| } | ||
| } | ||
|
|
||
| const Domain::ItemType* item_type = getItemType(first_thing); | ||
| if (!item_type) return false; | ||
|
|
||
| if (item_type->hasFlag(Domain::ItemFlag::IgnoreLook)) { | ||
| return false; | ||
| } | ||
|
|
||
| return false; | ||
|
|
||
| const bool is_ground_tile = item_type->isGround() || item_type->is_ground; | ||
| const bool is_bottom = item_type->always_on_bottom || item_type->is_on_bottom; | ||
| const bool blocks_projectile = | ||
| item_type->blocks_projectile || | ||
| item_type->hasFlag(Domain::ItemFlag::BlockMissiles); | ||
|
|
||
| if (is_free_view) { | ||
| return is_ground_tile || is_bottom; | ||
| } | ||
|
|
||
| return is_ground_tile || (is_bottom && blocks_projectile); | ||
| } |
There was a problem hiding this comment.
The simplified implementation of tileLimitsFloorsView only checks the first_thing (either the ground item or the first item in tile->getItems()). This introduces two correctness issues:
- Ignoring other items: If a tile has a non-blocking ground (or translucent ground) and a blocking wall/roof in
tile->getItems(), the blocking effect of the wall/roof will be completely ignored because only the ground is checked. - Early exit on
IgnoreLook: If the first item has theIgnoreLookflag, the function returnsfalseimmediately, ignoring any other blocking items on the tile.
To fix this, we should check the ground item first, and then iterate through all items in tile->getItems(), skipping only those with IgnoreLook and returning true if any item blocks the view.
bool FloorVisibilityCalculator::tileLimitsFloorsView(const Domain::Tile* tile, bool is_free_view) const {
if (!tile) return false;
auto check_item_blocking = [this, is_free_view](const Domain::Item* item) -> bool {
if (!item) return false;
const Domain::ItemType* item_type = getItemType(item);
if (!item_type) return false;
if (item_type->hasFlag(Domain::ItemFlag::IgnoreLook)) {
return false;
}
const bool is_ground_tile = item_type->isGround() || item_type->is_ground;
const bool is_bottom = item_type->always_on_bottom || item_type->is_on_bottom;
const bool blocks_projectile =
item_type->blocks_projectile ||
item_type->hasFlag(Domain::ItemFlag::BlockMissiles);
if (is_free_view) {
return is_ground_tile || is_bottom;
}
return is_ground_tile || (is_bottom && blocks_projectile);
};
if (check_item_blocking(tile->getGround())) {
return true;
}
for (const auto& item : tile->getItems()) {
if (check_item_blocking(item.get())) {
return true;
}
}
return false;
}| bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { | ||
| if (!tile) return true; // Empty tile is transparent | ||
| if (!tile) return false; |
There was a problem hiding this comment.
An empty/null tile represents empty space, which is transparent and does not block the line of sight. Returning false here incorrectly treats empty tiles as opaque blockers, which will break the floor visibility culling algorithm when empty tiles are present around the camera.
We should restore the original behavior and return true when tile is nullptr.
| bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { | |
| if (!tile) return true; // Empty tile is transparent | |
| if (!tile) return false; | |
| bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { | |
| if (!tile) return true; |
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In FloorVisibilityCalculator::isLookPossible, changing the behavior for a null tile from
truetofalsemakes empty tiles count as opaque; if this isn’t strictly required for OTClient compatibility, consider reverting or documenting this to avoid surprising visibility behavior. - In LightGatherer::gatherLightsForViewportFloor, the special-case branch for
floor == Config::Map::GROUND_LAYER + 1computesabove_offsetbut still usesfloor_offsetwhen projecting translucent-light tiles; this looks like it will misproject those lights and may need to useabove_offsetinstead. - The
projectedFloorOffsetTileshelper is duplicated in both LightGatherer.cpp and LightManager.cpp; consider centralizing this in a shared header or utility to avoid divergence between the two implementations.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In FloorVisibilityCalculator::isLookPossible, changing the behavior for a null tile from `true` to `false` makes empty tiles count as opaque; if this isn’t strictly required for OTClient compatibility, consider reverting or documenting this to avoid surprising visibility behavior.
- In LightGatherer::gatherLightsForViewportFloor, the special-case branch for `floor == Config::Map::GROUND_LAYER + 1` computes `above_offset` but still uses `floor_offset` when projecting translucent-light tiles; this looks like it will misproject those lights and may need to use `above_offset` instead.
- The `projectedFloorOffsetTiles` helper is duplicated in both LightGatherer.cpp and LightManager.cpp; consider centralizing this in a shared header or utility to avoid divergence between the two implementations.
## Individual Comments
### Comment 1
<location path="ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp" line_range="24-33" />
<code_context>
+ const Domain::Item* first_thing = tile->getGround();
</code_context>
<issue_to_address>
**issue (bug_risk):** Considering only the first item when limiting floor visibility can miss blockers
The old `tileLimitsFloorsView` walked all items on the tile; now it only inspects `ground` / the first item from `getItems()`. That can mark a tile as non-blocking when a blocking wall/object is further down the stack.
If you’re optimizing, you could still fast-path on `first_thing` when it clearly blocks, but then fall back to scanning the rest of the items when it doesn’t, to preserve the previous behavior while using the new flag-based logic.
</issue_to_address>
### Comment 2
<location path="ImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cpp" line_range="67-22" />
<code_context>
}
bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const {
- if (!tile) return true; // Empty tile is transparent
+ if (!tile) return false;
// Check all items for projectile blocking
</code_context>
<issue_to_address>
**issue (bug_risk):** Treating missing tiles as opaque in isLookPossible changes previous semantics
Previously, `isLookPossible` returned `true` for `nullptr`, treating empty tiles as transparent. Now `nullptr` returns `false`, so diagonal checks in `calcFirstVisibleFloor` will treat missing tiles as opaque. This can cause floors to be marked as covered when the map simply has no tile (e.g., sparse/partially loaded maps). If non-existent tiles should be considered empty space, this condition should remain `true` or missing tiles should be distinguished from solid ones explicitly.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| const Domain::Item* first_thing = tile->getGround(); | ||
| if (!first_thing && !tile->getItems().empty()) { | ||
| first_thing = tile->getItems().front().get(); | ||
| } | ||
|
|
||
| // Check other items on tile for blocking | ||
| for (const auto& item : tile->getItems()) { | ||
| const Domain::ItemType* item_type = getItemType(item.get()); | ||
| if (!item_type) continue; | ||
|
|
||
| if (item_type->is_dont_hide) continue; | ||
|
|
||
| if (item_type->is_ground) return true; | ||
|
|
||
| if (item_type->is_on_bottom) { | ||
| if (is_free_view) { | ||
| return true; | ||
| } else { | ||
| if (item_type->blocks_projectile) return true; | ||
| } | ||
| } | ||
|
|
||
| const Domain::ItemType* item_type = getItemType(first_thing); | ||
| if (!item_type) return false; | ||
|
|
||
| if (item_type->hasFlag(Domain::ItemFlag::IgnoreLook)) { | ||
| return false; |
There was a problem hiding this comment.
issue (bug_risk): Considering only the first item when limiting floor visibility can miss blockers
The old tileLimitsFloorsView walked all items on the tile; now it only inspects ground / the first item from getItems(). That can mark a tile as non-blocking when a blocking wall/object is further down the stack.
If you’re optimizing, you could still fast-path on first_thing when it clearly blocks, but then fall back to scanning the rest of the items when it doesn’t, to preserve the previous behavior while using the new flag-based logic.
| if (!tile) return false; | ||
|
|
||
| // Check ground first | ||
| const Domain::Item* ground = tile->getGround(); |
There was a problem hiding this comment.
issue (bug_risk): Treating missing tiles as opaque in isLookPossible changes previous semantics
Previously, isLookPossible returned true for nullptr, treating empty tiles as transparent. Now nullptr returns false, so diagonal checks in calcFirstVisibleFloor will treat missing tiles as opaque. This can cause floors to be marked as covered when the map simply has no tile (e.g., sparse/partially loaded maps). If non-existent tiles should be considered empty space, this condition should remain true or missing tiles should be distinguished from solid ones explicitly.
There was a problem hiding this comment.
Pull Request Overview
The PR successfully implements the requested OTClient-style lighting system, including translucent propagation and ground-blocking mechanisms. However, the overall quality is currently not up to standards. The logic in LightGatherer.cpp is identified as high-risk due to high cyclomatic complexity paired with a lack of automated test coverage. Additionally, performance bottlenecks were identified in LightManager.cpp regarding redundant vector allocations during chunk processing. These architectural and quality issues, along with duplicated hashing logic across render passes, should be addressed to ensure long-term maintainability.
About this PR
- The
LightConfigstruct inLightTypes.hretains deprecatedambient_colorandambient_levelfields. These should be removed to clean up the domain model and ensure the code reflects the new global light and client slider logic.
Test suggestions
- Verify light sources are correctly extracted from both creature types and item types during chunk gathering.
- Verify that multiple light sources at the same coordinate/floor are deduplicated and keep the highest intensity.
- Verify translucent propagation: tiles on z=8 should receive light if the tile directly above on z=7 is marked as translucent or has lens_help.
- Verify ground blocking: a light source on floor 8 should not illuminate floor 7 if floor 7 has a solid (non-translucent) ground item.
- Verify ambient light calculation: above-ground (z <= 7) should use the server's global light as a base, while underground (z > 7) should be black by default.
- Unit test coverage for high-complexity logic in LightGatherer.cpp (specifically ground blocking and floor propagation).
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Unit test coverage for high-complexity logic in LightGatherer.cpp (specifically ground blocking and floor propagation).
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
| config.ambient_level = config.global_light.intensity; | ||
|
|
||
| // Auto-invalidate if config changed | ||
| uint32_t config_hash = static_cast<uint32_t>(config.global_light.intensity) | |
There was a problem hiding this comment.
🟡 MEDIUM RISK
The configuration hashing logic used for cache invalidation is duplicated here and in LightingPass.cpp. This should be centralized to ensure that adding new fields to LightConfig correctly triggers invalidation across all render passes.
Try running the following prompt in your IDE agent:
Add a
uint32_t computeHash() constmethod to theLightConfigstruct inImguiMapEditor/Domain/LightTypes.hthat returns the combined hash of intensity, color, and slider. Then, updateIngamePreviewRenderer.cppandLightingPass.cppto use this method for cache invalidation.
| } | ||
| } | ||
|
|
||
| void LightGatherer::gatherLightsFromNeighborChunk( |
There was a problem hiding this comment.
🟡 MEDIUM RISK
This method handles multiple distinct concerns: item-based lighting, creature-based lighting, and z-axis propagation. Given the high complexity and lack of coverage for this file, moving the 'Translucent ground propagation' logic (lines 215-239) into its own method would reduce the function length and facilitate isolated testing.
Try running the following prompt in your IDE agent:
In
ImguiMapEditor/Rendering/Light/LightGatherer.cpp, extract the translucent ground propagation logic (lines 215-239) into a separate private method namedhandleFloorPropagationto reduce the length ofgatherLightsFromNeighborChunk.
| } | ||
| } | ||
|
|
||
| void LightGatherer::registerGroundBlockingForChunk( |
There was a problem hiding this comment.
🟡 MEDIUM RISK
This method's complexity (12) stems from nested tile processing within chunk iteration. As this file is identified as complex and lacks automated coverage, refactoring is highly recommended to improve testability. Extracting the logic that identifies and registers blocking tiles into a private helper method would clarify the implementation.
Try running the following prompt in your IDE agent:
Refactor
MapEditor::Rendering::LightGatherer::registerGroundBlockingForChunkinImguiMapEditor/Rendering/Light/LightGatherer.cppby extracting the tile processing logic inside theforEachTilelambda into a private helper function namedprocessTileBlocking.
| computeChunkLight(grid, gatherer_->getLights(), config, cx, cy); | ||
| // Combine gathered lights with any injected lights | ||
| if (injected_lights && !injected_lights->empty()) { | ||
| std::vector<Domain::LightSource> combined = gatherer_->getLights(); |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Suggestion: Avoid copying the gathered lights vector for every chunk. Try running the following prompt in your coding agent:
Update
computeChunkLightto accept an optional second vector of injected lights and iterate over both in the light processing loop to avoid the vector copy on line 190 ofLightManager.cpp.
| @@ -68,6 +68,7 @@ class IngamePreviewRenderer { | |||
| // Light system (independent from MapRenderer) | |||
| std::unique_ptr<LightManager> light_manager_; | |||
| uint8_t last_ambient_light_ = 255; | |||
There was a problem hiding this comment.
⚪ LOW RISK
Nitpick: This variable is now unused and can be removed.
There was a problem hiding this comment.
Actionable comments posted: 7
🤖 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/Domain/LightTypes.h`:
- Around line 38-39: Update the DEPRECATED comments for the fields ambient_color
and ambient_level in LightTypes.h to make clear they are only stored for
backward compatibility and not computed here; e.g., state that ambient_level is
initialized to 255 and any computation combining global_light and a slider
occurs elsewhere in the rendering pipeline (not in this struct) so developers
won’t expect logic inside the struct definition.
In `@ImguiMapEditor/Domain/Tile.h`:
- Line 30: The comment on the TranslucentLight flag in Tile.h is ambiguous about
"z=8" — update the comment for TranslucentLight to state explicitly whether z=8
is merely an example or a required constraint; if it is an example, change the
text to something like "e.g. z=8" and say the flag marks an underground tile
that is lit by translucent ground above; if z=8 is special, state that
explicitly and briefly explain why (what z coordinate system or rendering rule
makes z==8 special) and where that invariant is enforced (e.g., reference the
rendering/lighting code or function that checks z). Ensure the updated comment
references the TranslucentLight flag so readers know the intended scope.
In `@ImguiMapEditor/IO/CreatureXmlReader.cpp`:
- Around line 138-144: The light attributes are being cast to uint8_t without
range checks causing silent truncation; update the handling for
node.attribute("lightlevel") and node.attribute("lightcolor") in
CreatureXmlReader.cpp to read the value into a wider type (e.g., unsigned int),
validate it is <= 255, and if it exceeds 255 log or warn (include attribute name
and raw value) and clamp to 255 before assigning to creature->light_level and
creature->light_color (use static_cast<uint8_t> on the validated/clamped value).
In `@ImguiMapEditor/Rendering/Light/LightGatherer.cpp`:
- Around line 16-25: The function projectedFloorOffsetTiles is duplicated across
LightGatherer.cpp and LightManager.cpp; extract it into a single shared header
(e.g. a MapLightingUtils or LightHelpers header) as an inline/free function in
the same anonymous or project namespace, export or include that header in both
LightGatherer.cpp and LightManager.cpp, and remove the duplicate definitions
from the cpp files so both units call the single shared
projectedFloorOffsetTiles implementation; ensure any dependent constants like
LIGHT_COLLECTION_MARGIN_TILES or Config::Map::GROUND_LAYER remain visible to the
shared header or are referenced from it.
In `@ImguiMapEditor/Rendering/Light/LightManager.cpp`:
- Around line 16-25: The function projectedFloorOffsetTiles is duplicated;
remove the duplicate in LightManager.cpp and consolidate to a single shared
implementation (e.g., move the function to a common header/source or an existing
utility module) so both LightManager and LightGatherer call that single symbol.
Update includes in the files that used the duplicate so they include the new
header, keep the function in an appropriate namespace, and ensure only one
definition of projectedFloorOffsetTiles remains in the codebase.
In `@ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp`:
- Around line 226-259: The config hash computed in IngamePreviewRenderer
(config_hash) omits config.camera_floor but LightManager::render checks
camera_floor when deciding config_changed; include camera_floor in the hash
computation so changes to floor update last_config_hash_ and trigger
invalidateAll() consistently—update the config_hash expression to incorporate
config.camera_floor (or floor) alongside config.global_light.intensity,
config.global_light.color, and config.client_slider so IngamePreviewRenderer and
LightManager use matching invalidation criteria.
In `@ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h`:
- Around line 70-71: Remove the unused field last_ambient_light_ from the
IngamePreviewRenderer class: locate the declaration "uint8_t last_ambient_light_
= 255;" (next to last_config_hash_) in IngamePreviewRenderer.h and delete it;
ensure no other code refers to last_ambient_light_ (if there are references,
either implement their intended logic or update those sites to use
last_config_hash_ or another appropriate member), then rebuild to confirm no
remaining references.
🪄 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: 316b00cc-0982-40ef-a4a0-2b5246ce5230
📒 Files selected for processing (28)
ImguiMapEditor/Application/CallbackMediator.cppImguiMapEditor/Application/EditorSession.hImguiMapEditor/Domain/CreatureType.hImguiMapEditor/Domain/ItemType.hImguiMapEditor/Domain/LightTypes.hImguiMapEditor/Domain/Tile.hImguiMapEditor/IO/CreatureXmlReader.cppImguiMapEditor/Presentation/MainWindow.cppImguiMapEditor/Rendering/Core/IRenderPass.hImguiMapEditor/Rendering/Frame/RenderState.hImguiMapEditor/Rendering/Light/LightGatherer.cppImguiMapEditor/Rendering/Light/LightGatherer.hImguiMapEditor/Rendering/Light/LightManager.cppImguiMapEditor/Rendering/Light/LightManager.hImguiMapEditor/Rendering/Map/MapRenderer.cppImguiMapEditor/Rendering/Map/MapRenderer.hImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cppImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.hImguiMapEditor/Rendering/Passes/LightingPass.cppImguiMapEditor/Rendering/Passes/LightingPass.hImguiMapEditor/Rendering/Visibility/FloorVisibilityCalculator.cppImguiMapEditor/Services/ClientDataService.cppImguiMapEditor/Services/ViewSettings.cppImguiMapEditor/Services/ViewSettings.hImguiMapEditor/UI/Map/MapPanel.cppImguiMapEditor/UI/Map/MapPanelInput.cppImguiMapEditor/UI/Map/MapPanelInput.hImguiMapEditor/UI/Windows/IngameBoxWindow.cpp
💤 Files with no reviewable changes (1)
- ImguiMapEditor/Rendering/Passes/LightingPass.h
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| BestPractice | 1 medium |
| ErrorProne | 12 medium |
| Complexity | 8 medium |
🟢 Metrics 62 complexity · -2 duplication
Metric Results Complexity 62 Duplication -2
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
…nagement Centralized lighting configuration constants, extracted projection logic into a shared header, and added hashing support to `LightConfig` for efficient cache invalidation. - `ImguiMapEditor/Core/Config.h` — introduced `Lighting` namespace with default configuration constants - `ImguiMapEditor/Domain/LightTypes.h` — updated to use central lighting constants and added `computeHash()` to `LightConfig` - `ImguiMapEditor/Domain/Tile.h` — updated `TranslucentLight` documentation - `ImguiMapEditor/IO/CreatureXmlReader.cpp` — added light attribute clamping for creature parsing - `ImguiMapEditor/Rendering/Frame/RenderState.h` — updated `last_config_hash` documentation - `ImguiMapEditor/Rendering/Light/LightGatherer.cpp` — utilized shared `projectedFloorOffsetTiles` and centralized lighting constants - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — removed local `projectedFloorOffsetTiles`, updated ambient calculation to use central constants - `ImguiMapEditor/Rendering/Light/LightProjection.h` — created new header for shared projection logic - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp` — utilized `config.computeHash()` and centralized constants for player lights - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h` — removed unused `last_ambient_light_` member - `ImguiMapEditor/Rendering/Passes/LightingPass.cpp` — refactored invalidation logic to use `config.computeHash()` - `ImguiMapEditor/Services/ViewSettings.cpp` — updated settings loading to use central lighting constants - `ImguiMapEditor/Services/ViewSettings.h` — updated lighting settings to use central constants
There was a problem hiding this comment.
📋 Review Summary
This PR introduces a significant rework of the lighting system to support OTClient-style light rendering, including global server light intensity/color, floor-based visibility for light propagation, and creature/item light properties.
The architectural changes are solid, especially the move towards a more realistic visibility algorithm and the addition of per-floor light propagation.
🔍 General Feedback
- Correctness: The visibility calculator has been refined to match client-side logic (covered position shift), which is a great improvement. However, some optimizations in
tileLimitsFloorsViewmight miss blockers if they are not the first item on the tile. - Maintainability: The use of
computeHashfor cache invalidation is a clean way to handle lighting updates. - UI/UX: The addition of server light sliders in the preview window is very helpful for testing in-game aesthetics directly within the editor.
- Performance: The removal of
LightCachein favor of a directViewportLightBuffershould be monitored for any regressions on high-resolution displays, although it likely simplifies the pipeline.
Please address the minor concerns regarding tile blocking logic and the potential regression in isLookPossible for null tiles.
| const Domain::ItemType* ground_type = getItemType(ground); | ||
| if (ground_type && ground_type->blocks_projectile) { | ||
| if (ground_type && | ||
| (ground_type->blocks_projectile || |
There was a problem hiding this comment.
🟡 Changing the return value for null tiles from true to false might cause issues in areas where tiles are not yet loaded or are intentionally left empty. Typically, empty space (null tile) in Tibia is considered transparent. Is this change intentional to prevent rendering artifacts in unloaded chunks?
| } | ||
|
|
||
| const Domain::Item* first_thing = tile->getGround(); | ||
| if (!first_thing && !tile->getItems().empty()) { |
There was a problem hiding this comment.
🟠 The new logic only checks the ground or the very first item in the tile's item list. If a tile has multiple items (e.g., a non-blocking decoration followed by a blocking wall), this will fail to identify the tile as a blocker. The previous implementation correctly iterated through all items on the tile.
| if (!first_thing && !tile->getItems().empty()) { | |
| for (const auto& item : tile->getItems()) { | |
| const Domain::ItemType* item_type = getItemType(item.get()); | |
| if (!item_type) continue; | |
| // ... existing logic to check flags ... | |
| } |
| ImGui::SetNextItemWidth(50); | ||
| int color = static_cast<int>(settings.preview_server_light_color); | ||
| if (ImGui::SliderInt("##color", &color, 0, 215)) { | ||
| settings.preview_server_light_color = static_cast<uint8_t>(color); |
There was a problem hiding this comment.
🟡 Why is the color slider limited to 215? While 215 is the standard white light in the Tibia palette, other indices (0-255) might be used for colored world lighting effects in some servers. Consider allowing the full 0-255 range.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ImguiMapEditor/Rendering/Light/LightManager.cpp (1)
117-128:⚠️ Potential issue | 🟠 Major | ⚡ Quick winInclude injected lights in the cache key.
render()now acceptsinjected_lights, but the reuse path ignores them. If a preview/player light moves while bounds andLightConfigstay the same, this returns the previous texture and the injected light appears frozen. At minimum, bypass the fast path when injected lights are present; ideally hash them into the cached state.Suggested minimal fix
+ const bool has_injected_lights = + injected_lights && !injected_lights->empty(); + - if (!bounds_changed && !config_changed && !force_update_) { + if (!bounds_changed && !config_changed && !force_update_ && + !has_injected_lights) { float world_x = start_x * 32.0f; float world_y = start_y * 32.0f; float screen_x = (world_x - camera_x * 32.0f) * zoom + viewport_width / 2.0f; float screen_y = (world_y - camera_y * 32.0f) * zoom + viewport_height / 2.0f; float screen_w = width_tiles * 32.0f * zoom;🤖 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/Light/LightManager.cpp` around lines 117 - 128, The fast-return path in render() (the branch gated by bounds_changed, config_changed, and force_update_) ignores the new injected_lights parameter causing moving preview/player lights to appear frozen; update the logic to either bypass this fast path when injected_lights is non-empty or incorporate injected_lights into the cache key used for texture reuse (e.g., include a hash/unique identifier derived from injected_lights when deciding reuse). Modify the condition that checks bounds_changed/config_changed/force_update_ to also check injected_lights.empty() (for the minimal fix) or add injected_lights hashing into the cached state comparison used before calling overlay_->apply with texture_->getTextureId() so the cached texture is invalidated when injected lights change.ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp (1)
242-251: 🧹 Nitpick | 🔵 Trivial | ⚡ Quick winPlayer light color matches the comment (no mismatch)
Config::Lighting::DEFAULT_SERVER_LIGHT_COLORis defined as215, so the preview player light’scoloraligns with the comment’s OTClient local playercolor=215(white). Optional: adjust the comment wording to avoid implying this is a separately defined “player” color constant.🤖 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/Passes/IngamePreviewRenderer.cpp` around lines 242 - 251, The comment implies a distinct "player" color constant but the code uses Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR (value 215), so update the comment near the player light initialization in IngamePreviewRenderer.cpp to reflect that it reuses the server default color rather than a separate player color; mention Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR and the player light construction (player_lights vector and Domain::LightSource fields) so readers know which symbols are involved.
🤖 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/Light/LightGatherer.cpp`:
- Around line 232-235: The translucent propagation path in LightGatherer.cpp is
hard-coding Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR and
Config::Lighting::TRANSLUCENT_PROPAGATION_INTENSITY in the addLight(...) call;
instead obtain the current active/global server light settings (tint and
propagation intensity) from the runtime LightManager or the session preview
state and pass those values into addLight(projected_x, projected_y, ..., floor)
so propagation matches day/night configuration — either thread the active light
struct through this call site or replace the literal constants with a
LightManager::getActivePropagationTint() / getActivePropagationIntensity()
accessor and use those returned values when calling addLight.
---
Outside diff comments:
In `@ImguiMapEditor/Rendering/Light/LightManager.cpp`:
- Around line 117-128: The fast-return path in render() (the branch gated by
bounds_changed, config_changed, and force_update_) ignores the new
injected_lights parameter causing moving preview/player lights to appear frozen;
update the logic to either bypass this fast path when injected_lights is
non-empty or incorporate injected_lights into the cache key used for texture
reuse (e.g., include a hash/unique identifier derived from injected_lights when
deciding reuse). Modify the condition that checks
bounds_changed/config_changed/force_update_ to also check
injected_lights.empty() (for the minimal fix) or add injected_lights hashing
into the cached state comparison used before calling overlay_->apply with
texture_->getTextureId() so the cached texture is invalidated when injected
lights change.
In `@ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp`:
- Around line 242-251: The comment implies a distinct "player" color constant
but the code uses Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR (value 215), so
update the comment near the player light initialization in
IngamePreviewRenderer.cpp to reflect that it reuses the server default color
rather than a separate player color; mention
Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR and the player light construction
(player_lights vector and Domain::LightSource fields) so readers know which
symbols are involved.
🪄 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: dea9ecd7-c8e4-435c-a947-3182f4cd7dd8
📒 Files selected for processing (13)
ImguiMapEditor/Core/Config.hImguiMapEditor/Domain/LightTypes.hImguiMapEditor/Domain/Tile.hImguiMapEditor/IO/CreatureXmlReader.cppImguiMapEditor/Rendering/Frame/RenderState.hImguiMapEditor/Rendering/Light/LightGatherer.cppImguiMapEditor/Rendering/Light/LightManager.cppImguiMapEditor/Rendering/Light/LightProjection.hImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cppImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.hImguiMapEditor/Rendering/Passes/LightingPass.cppImguiMapEditor/Services/ViewSettings.cppImguiMapEditor/Services/ViewSettings.h
💤 Files with no reviewable changes (1)
- ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.h
| addLight(light_buffer, projected_x, projected_y, | ||
| Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR, | ||
| Config::Lighting::TRANSLUCENT_PROPAGATION_INTENSITY, | ||
| floor); |
There was a problem hiding this comment.
Use the active server/global light for translucent propagation.
This path hard-codes the default server light color and propagation intensity, so changing the new session/preview server light settings will not change the light leaking through translucent ground. Please thread the current global light into this step, or defer the propagation tint/intensity decision to LightManager, so underground propagation stays consistent with the configured day/night state.
🤖 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/Light/LightGatherer.cpp` around lines 232 - 235, The
translucent propagation path in LightGatherer.cpp is hard-coding
Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR and
Config::Lighting::TRANSLUCENT_PROPAGATION_INTENSITY in the addLight(...) call;
instead obtain the current active/global server light settings (tint and
propagation intensity) from the runtime LightManager or the session preview
state and pass those values into addLight(projected_x, projected_y, ..., floor)
so propagation matches day/night configuration — either thread the active light
struct through this call site or replace the literal constants with a
LightManager::getActivePropagationTint() / getActivePropagationIntensity()
accessor and use those returned values when calling addLight.
|
/gemini review |
|
@gemini-cli /review |
|
🤖 Hi @karolak6612, I've received your request, and I'm working on it now! You can track my progress in the logs for more details. |
There was a problem hiding this comment.
📋 Review Summary
This pull request introduces a significant rework of the lighting system, bringing it much closer to OTClient and RME behavior. Key highlights include support for creature and item light properties, global world light (intensity and color), and a more accurate floor visibility algorithm.
The implementation is generally solid, with good use of caching and a hashing mechanism to minimize unnecessary recalculations.
🔍 General Feedback
- Lighting Accuracy: The addition of server-style light intensity and color, along with player light in the preview, greatly enhances the editor's utility for map designers.
- Performance: Using a hash of the lighting configuration for cache invalidation is a great approach.
- UI/UX: The new sliders for server light properties are useful, though the behavior of the light visibility origin following every click might need a quick check to ensure it doesn't become distracting during normal editing.
- Optimization: The floor visibility calculator is a bit loop-heavy with tile lookups; minor optimizations here could benefit larger maps.
Overall, a very high-quality contribution that improves the visual fidelity of the editor.
| [[nodiscard]] uint32_t computeHash() const { | ||
| return static_cast<uint32_t>(global_light.intensity) | | ||
| (static_cast<uint32_t>(global_light.color) << 8) | | ||
| (static_cast<uint32_t>(client_slider) << 16) | |
There was a problem hiding this comment.
🟡 The computeHash function currently excludes the enabled field. While LightingPass checks this field before calling render, it might be safer to include it in the hash to ensure the cache is invalidated if the master toggle is changed, or add a comment explaining why it's omitted.
| .source_floor = static_cast<int16_t>(floor) | ||
| }); | ||
|
|
||
| light_manager_->render(map, viewport_width, viewport_height, camera_x, |
There was a problem hiding this comment.
🟢 The addition of player light in the preview renderer correctly mimics OTClient behavior and is a great improvement for "ingame" feel.
|
|
||
| const unsigned int raw_value = attr.as_uint(); | ||
| if (raw_value > 255U) { | ||
| warnings.push_back(std::string("Creature '") + creature->name + |
There was a problem hiding this comment.
🟠 Using attr.as_uint() and then checking > 255 is safe, but ensure that the warning message (which was truncated in the diff) clearly identifies which creature has the invalid light property to help users fix their creatures.xml.
| if (ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { | ||
| glm::vec2 mouse_pos(io.MousePos.x, io.MousePos.y); | ||
| Domain::Position target_pos = camera.screenToTile(mouse_pos); | ||
| setLightVisibilityOrigin(target_pos); |
There was a problem hiding this comment.
🟢 Updating the light visibility origin on every left-click (paste, selection, etc.) will cause the lighting focus to shift constantly. Is this the intended behavior for the editor? In many cases, users might prefer the lighting to stay focused on a specific area or follow the camera.
|
|
||
| while (upper_z > 0 && covered_z > 0) { | ||
| --upper_z; | ||
| --covered_z; |
There was a problem hiding this comment.
🟡 The loop for calculating floor visibility performs multiple map.getTile calls per iteration. Given that this is called frequently during rendering, consider if there's a more efficient way to access these tiles, perhaps by checking if the chunk has changed before doing a full tile lookup.
There was a problem hiding this comment.
Code Review
This pull request refactors the lighting system to use a single viewport-wide light buffer and introduces global server light settings alongside client ambient sliders. It also updates floor visibility calculations and adds creature light property parsing. Feedback highlights two critical regressions in FloorVisibilityCalculator: returning false for null tiles in isLookPossible incorrectly blocks sight, and checking only the first item in tileLimitsFloorsView fails to account for other blocking items on the tile.
| bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { | ||
| if (!tile) return true; // Empty tile is transparent | ||
| if (!tile) return false; |
There was a problem hiding this comment.
Returning false when tile is nullptr in isLookPossible causes the floor visibility algorithm to treat empty/null tiles as blocking sight. As a result, in calcFirstVisibleFloor, the algorithm will skip checking upper floors for any neighbor positions that have null tiles on the camera floor. This means any roofs or blockers on upper floors at those positions will be completely ignored, leading to incorrect rendering of higher floors that should be occluded.
It should return true for null tiles to indicate that empty space is transparent and allows looking through to upper floors.
| bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { | |
| if (!tile) return true; // Empty tile is transparent | |
| if (!tile) return false; | |
| bool FloorVisibilityCalculator::isLookPossible(const Domain::Tile* tile) const { | |
| if (!tile) return true; // Empty tile is transparent |
| bool FloorVisibilityCalculator::tileLimitsFloorsView(const Domain::Tile* tile, bool is_free_view) const { | ||
| if (!tile) return false; | ||
|
|
||
| // Check ground first | ||
| const Domain::Item* ground = tile->getGround(); | ||
| if (!ground) return false; | ||
|
|
||
| const Domain::ItemType* ground_type = getItemType(ground); | ||
| if (!ground_type) return false; | ||
|
|
||
| // Items with isDontHide never block view | ||
| if (ground_type->is_dont_hide) return false; | ||
|
|
||
| // Ground tiles block view | ||
| if (ground_type->is_ground) return true; | ||
|
|
||
| // isOnBottom items (walls) block view | ||
| if (ground_type->is_on_bottom) { | ||
| if (is_free_view) { | ||
| // Free view: any wall blocks | ||
| return true; | ||
| } else { | ||
| // Player view: only walls that block projectiles | ||
| return ground_type->blocks_projectile; | ||
| } | ||
|
|
||
| const Domain::Item* first_thing = tile->getGround(); | ||
| if (!first_thing && !tile->getItems().empty()) { | ||
| first_thing = tile->getItems().front().get(); | ||
| } | ||
|
|
||
| // Check other items on tile for blocking | ||
| for (const auto& item : tile->getItems()) { | ||
| const Domain::ItemType* item_type = getItemType(item.get()); | ||
| if (!item_type) continue; | ||
|
|
||
| if (item_type->is_dont_hide) continue; | ||
|
|
||
| if (item_type->is_ground) return true; | ||
|
|
||
| if (item_type->is_on_bottom) { | ||
| if (is_free_view) { | ||
| return true; | ||
| } else { | ||
| if (item_type->blocks_projectile) return true; | ||
| } | ||
| } | ||
|
|
||
| const Domain::ItemType* item_type = getItemType(first_thing); | ||
| if (!item_type) return false; | ||
|
|
||
| if (item_type->hasFlag(Domain::ItemFlag::IgnoreLook)) { | ||
| return false; | ||
| } | ||
|
|
||
| return false; | ||
|
|
||
| const bool is_ground_tile = item_type->isGround() || item_type->is_ground; | ||
| const bool is_bottom = item_type->always_on_bottom || item_type->is_on_bottom; | ||
| const bool blocks_projectile = | ||
| item_type->blocks_projectile || | ||
| item_type->hasFlag(Domain::ItemFlag::BlockMissiles); | ||
|
|
||
| if (is_free_view) { | ||
| return is_ground_tile || is_bottom; | ||
| } | ||
|
|
||
| return is_ground_tile || (is_bottom && blocks_projectile); | ||
| } |
There was a problem hiding this comment.
In tileLimitsFloorsView, the new implementation only checks the first_thing (either the ground or the first item in getItems()) to determine if the tile limits the floor view. This is a regression from the original implementation which iterated through all items on the tile.
If a tile has no ground but contains multiple items (e.g., a decorative item and a wall), and the decorative item happens to be the first in the list, the wall will be completely ignored, and the tile will incorrectly be treated as not limiting the view.
To ensure correctness, the function should check all items on the tile, or at least ensure that any blocking items (like walls/ground) are not missed.
bool FloorVisibilityCalculator::tileLimitsFloorsView(const Domain::Tile* tile, bool is_free_view) const {
if (!tile) return false;
const auto check_blocking = [&](const Domain::Item* item) {
if (!item) return false;
const Domain::ItemType* item_type = getItemType(item);
if (!item_type || item_type->hasFlag(Domain::ItemFlag::IgnoreLook)) {
return false;
}
const bool is_ground_tile = item_type->isGround() || item_type->is_ground;
const bool is_bottom = item_type->always_on_bottom || item_type->is_on_bottom;
const bool blocks_projectile =
item_type->blocks_projectile ||
item_type->hasFlag(Domain::ItemFlag::BlockMissiles);
if (is_free_view) {
return is_ground_tile || is_bottom;
}
return is_ground_tile || (is_bottom && blocks_projectile);
};
if (check_blocking(tile->getGround())) {
return true;
}
for (const auto& item : tile->getItems()) {
if (check_blocking(item.get())) {
return true;
}
}
return false;
}Ensure dynamic preview/player lights are correctly recomputed every frame by disabling texture reuse when injected lights are active. Updated preview renderer comments to clarify the source of default player light colors. - `ImguiMapEditor/Rendering/Light/LightManager.cpp` — added `has_injected_lights` check to force recomputation when dynamic lights are present - `ImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cpp` — [minor] updated comment to reference `DEFAULT_SERVER_LIGHT_COLOR`
Replaced numerical sliders for light color selection with a visual palette picker to improve usability and offer a preview of Tibia's 8-bit color palette. - `ImguiMapEditor/CMakeLists.txt` — added `LightColorPalettePicker.cpp` to build sources - `ImguiMapEditor/Core/Config.h` — defined `LIGHT_COLOR_COUNT` and `MAX_SERVER_LIGHT_COLOR` constants - `ImguiMapEditor/Presentation/MainWindow.cpp` — integrated `LightColorPalettePicker` into main editor light settings - `ImguiMapEditor/UI/Widgets/LightColorPalettePicker.cpp` — implemented popup widget with palette grid, swatches, and hover tooltips - `ImguiMapEditor/UI/Widgets/LightColorPalettePicker.h` — defined `LightColorPalettePicker` interface - `ImguiMapEditor/UI/Windows/IngameBoxWindow.cpp` — replaced slider with `LightColorPalettePicker` in ingame box settings
There was a problem hiding this comment.
📋 Review Summary
This PR introduces a significant rework of the lighting system to support OTClient-style light rendering, including server light intensity/color persistence, creature/item light properties, and dynamic light visibility based on floor blocking rules.
🔍 General Feedback
The new lighting model is much more authentic to the Tibia/OTClient experience. The addition of the LightColorPalettePicker is a great UI improvement for configuring world lighting.
I identified a few issues that should be addressed:
- 🔴 Light Projection Bug: High-floor lights and ground occlusion seem to be shifted NW instead of SE due to a sign error in the projection logic.
- 🟠 Cache Invalidation: The light texture reuse optimization might lead to stale dynamic lights staying visible when they should have been removed.
- 🟡 Performance: Some iteration loops over chunks could be optimized, though they are likely acceptable for the editor's typical viewport.
Overall, a solid enhancement to the rendering pipeline!
| LIGHT_COLLECTION_MARGIN_TILES; | ||
| const int32_t max_y = | ||
| light_buffer.origin_y + light_buffer.height - 1 + floor_offset + | ||
| LIGHT_COLLECTION_MARGIN_TILES; |
There was a problem hiding this comment.
🔴 The projection logic for multi-floor lighting appears to be reversed.
In projectedFloorOffsetTiles, floor 6 (above floor 7) returns an offset of 1.
In LightGatherer::gatherLightsForViewportFloor, this is used as projected_x = tile->getX() - floor_offset, resulting in x - 1.
However, in TerrainPass.cpp, higher floors are shifted SE (positive offset), so a tile at world x on floor 6 is drawn at screen x + 1.
This causes light sources and ground occlusion to be misaligned with the terrain they are supposed to affect on floors other than the current one.
| LIGHT_COLLECTION_MARGIN_TILES; | |
| const int32_t projected_x = tile->getX() + floor_offset; | |
| const int32_t projected_y = tile->getY() + floor_offset; |
| int32_t tile_end_x = tile_start_x + Domain::Chunk::SIZE; | ||
| int32_t tile_end_y = tile_start_y + Domain::Chunk::SIZE; | ||
|
|
||
| const int32_t floor_offset = projectedFloorOffsetTiles(current_floor, floor); |
There was a problem hiding this comment.
🔴 The min_x/y and max_x/y calculation for visible chunks is also affected by the reversed floor_offset.
If floor_offset is 1 for floor 6, we want to gather world tiles x such that x + 1 is on screen. So x should be screen_x - 1.
The current code uses light_buffer.origin_x + floor_offset, which is screen_x + 1.
| const int32_t floor_offset = projectedFloorOffsetTiles(current_floor, floor); | |
| const int32_t min_x = | |
| light_buffer.origin_x - floor_offset - LIGHT_COLLECTION_MARGIN_TILES; | |
| const int32_t min_y = | |
| light_buffer.origin_y - floor_offset - LIGHT_COLLECTION_MARGIN_TILES; | |
| const int32_t max_x = | |
| light_buffer.origin_x + light_buffer.width - 1 - floor_offset + | |
| LIGHT_COLLECTION_MARGIN_TILES; | |
| const int32_t max_y = | |
| light_buffer.origin_y + light_buffer.height - 1 - floor_offset + | |
| LIGHT_COLLECTION_MARGIN_TILES; |
|
|
||
| chunk->forEachTile([&](const Domain::Tile* tile) { | ||
| if (!tile || tile->getZ() != floor) return; | ||
|
|
There was a problem hiding this comment.
🟡 Performance concern: forEachTile iterates over all 1024 tiles in each visible chunk for every floor from start_floor to current_floor. While necessary to find ground occlusion, this could be expensive for large viewports or many floors.
Since this logic is primarily to support "OTClient style" visibility where ground blocks light from below, perhaps the occlusion can be gathered more efficiently or cached alongside the terrain mesh.
| if (creature_type && creature_type->light_level > 0) { | ||
| addLight(light_buffer, projected_x, projected_y, | ||
| creature_type->light_color, | ||
| creature_type->light_level, floor); |
There was a problem hiding this comment.
🟢 Matches OTClient/RME translucent light propagation rules for z == GROUND_LAYER + 1.
if (floor == Config::Map::GROUND_LAYER + 1) {
// ...
addLight(light_buffer, projected_x, projected_y,
Config::Lighting::DEFAULT_SERVER_LIGHT_COLOR,
Config::Lighting::TRANSLUCENT_PROPAGATION_INTENSITY,
floor);
}This is a nice addition for underground atmosphere.
|
|
||
| // If bounds and config match, we can reuse the texture | ||
| const bool has_injected_lights = injected_lights && !injected_lights->empty(); | ||
|
|
There was a problem hiding this comment.
🟠 Potential stale cache issue.
The optimization to reuse the light texture doesn't account for the case where injected_lights were present in the previous frame but are absent in the current one. If has_injected_lights becomes false, but bounds_changed and config_changed are also false, the renderer will reuse the texture that still contains the "baked" lights from the previous frame.
Consider tracking last_has_injected_lights_ or forcing an update if the injected_lights list changed its state.
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 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/Light/LightManager.cpp`:
- Around line 114-120: The cache reuse logic incorrectly ignores transitions
where injected_lights were present last frame but are absent now, leaving stale
injected lighting; modify LightManager to track prior injected-light presence
(e.g., add a member like had_injected_lights_ or similar) and update it each
frame, then change the reuse condition (the check using injected_lights /
has_injected_lights and force_update_) so that a change in injected-light state
(had_injected_lights_ != has_injected_lights) forces a recompute (set
force_update_ or bypass reuse) and then update had_injected_lights_ to the
current has_injected_lights for the next frame.
🪄 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: 8ed38426-15c6-488f-88f1-ea3aa34f5fd2
📒 Files selected for processing (8)
ImguiMapEditor/CMakeLists.txtImguiMapEditor/Core/Config.hImguiMapEditor/Presentation/MainWindow.cppImguiMapEditor/Rendering/Light/LightManager.cppImguiMapEditor/Rendering/Passes/IngamePreviewRenderer.cppImguiMapEditor/UI/Widgets/LightColorPalettePicker.cppImguiMapEditor/UI/Widgets/LightColorPalettePicker.hImguiMapEditor/UI/Windows/IngameBoxWindow.cpp
| const bool has_injected_lights = injected_lights && !injected_lights->empty(); | ||
|
|
||
| // If bounds and config match, we can reuse the texture. | ||
| // Injected lights are dynamic preview/player lights, so they always recompute. | ||
| // But we MUST recalculate screen coordinates because sub-pixel camera movement | ||
| // changes where the texture is drawn, even if the integer tile range is same. | ||
| if (!bounds_changed && !config_changed && !force_update_) { | ||
| if (!has_injected_lights && !bounds_changed && !config_changed && !force_update_) { |
There was a problem hiding this comment.
Handle injected-light state transitions in cache reuse.
If injected lights were present in the previous frame but are absent now, this condition can still reuse the old texture and keep stale injected lighting visible. Track prior injected-light presence and force recompute on state changes.
💡 Proposed fix
--- a/ImguiMapEditor/Rendering/Light/LightManager.h
+++ b/ImguiMapEditor/Rendering/Light/LightManager.h
@@
bool force_update_ = true;
+ bool last_had_injected_lights_ = false;--- a/ImguiMapEditor/Rendering/Light/LightManager.cpp
+++ b/ImguiMapEditor/Rendering/Light/LightManager.cpp
@@
- const bool has_injected_lights = injected_lights && !injected_lights->empty();
+ const bool has_injected_lights = injected_lights && !injected_lights->empty();
+ const bool injected_state_changed =
+ has_injected_lights != last_had_injected_lights_;
@@
- if (!has_injected_lights && !bounds_changed && !config_changed && !force_update_) {
+ if (!has_injected_lights && !injected_state_changed &&
+ !bounds_changed && !config_changed && !force_update_) {
@@
last_config_ = config;
+ last_had_injected_lights_ = has_injected_lights;
force_update_ = false;📝 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.
| const bool has_injected_lights = injected_lights && !injected_lights->empty(); | |
| // If bounds and config match, we can reuse the texture. | |
| // Injected lights are dynamic preview/player lights, so they always recompute. | |
| // But we MUST recalculate screen coordinates because sub-pixel camera movement | |
| // changes where the texture is drawn, even if the integer tile range is same. | |
| if (!bounds_changed && !config_changed && !force_update_) { | |
| if (!has_injected_lights && !bounds_changed && !config_changed && !force_update_) { | |
| const bool has_injected_lights = injected_lights && !injected_lights->empty(); | |
| const bool injected_state_changed = | |
| has_injected_lights != last_had_injected_lights_; | |
| // If bounds and config match, we can reuse the texture. | |
| // Injected lights are dynamic preview/player lights, so they always recompute. | |
| // But we MUST recalculate screen coordinates because sub-pixel camera movement | |
| // changes where the texture is drawn, even if the integer tile range is same. | |
| if (!has_injected_lights && !injected_state_changed && | |
| !bounds_changed && !config_changed && !force_update_) { |
🧰 Tools
🪛 Clang (14.0.6)
[warning] 114-114: variable 'has_injected_lights' is not initialized
(cppcoreguidelines-init-variables)
🪛 Cppcheck (2.20.0)
[style] 115-115: The function 'getDataDirectory' is never used.
(unusedFunction)
[style] 116-116: The function 'setDataDirectory' is never used.
(unusedFunction)
[style] 118-118: The function 'setDescription' is never used.
(unusedFunction)
[style] 115-115: The function 'getDuration' is never used.
(unusedFunction)
[style] 116-116: The function 'setDuration' is never used.
(unusedFunction)
[style] 119-119: The function 'getSubtype' is never used.
(unusedFunction)
[style] 120-120: The function 'setSubtype' 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/Rendering/Light/LightManager.cpp` around lines 114 - 120, The
cache reuse logic incorrectly ignores transitions where injected_lights were
present last frame but are absent now, leaving stale injected lighting; modify
LightManager to track prior injected-light presence (e.g., add a member like
had_injected_lights_ or similar) and update it each frame, then change the reuse
condition (the check using injected_lights / has_injected_lights and
force_update_) so that a change in injected-light state (had_injected_lights_ !=
has_injected_lights) forces a recompute (set force_update_ or bypass reuse) and
then update had_injected_lights_ to the current has_injected_lights for the next
frame.
Summary by CodeRabbit
New Features
Improvements