Skip to content

Bugfix/property deletion orphan fix#1974

Closed
zunath wants to merge 20 commits intomasterfrom
bugfix/property-deletion-orphan-fix
Closed

Bugfix/property deletion orphan fix#1974
zunath wants to merge 20 commits intomasterfrom
bugfix/property-deletion-orphan-fix

Conversation

@zunath
Copy link
Copy Markdown
Owner

@zunath zunath commented Apr 24, 2026

Summary by CodeRabbit

  • Bug Fixes

    • Added defensive validation to prevent spawn/access failures when interior, building, or city records are missing; errors are logged and spawning/access is skipped.
    • Improved property deletion to iterate and handle missing children, relocating docked starships safely and avoiding failures.
    • Wrapped world spawn in error handling so failures are logged and processing continues.
    • Fixed field-scoped search query construction for accurate results.
    • Prevented reference-aliasing by cloning location data when copying positions.
  • Refactor

    • Centralized ship relocation logic and removed duplicated per-location handling.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Warning

Rate limit exceeded

@zunath has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 49 minutes and 39 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 6586edb3-92d6-4b7c-a642-b20e7917e4fc

📥 Commits

Reviewing files that changed from the base of the PR and between 68d7b9b and 705b3e3.

📒 Files selected for processing (4)
  • SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs
  • SWLOR.Game.Server/Service/DB.cs
  • SWLOR.Game.Server/Service/DBService/DBQuery.cs
  • SWLOR.Game.Server/Service/Property.cs
📝 Walkthrough

Walkthrough

Added defensive null checks in property layout spawn/access logic, tightened RediSearch field-scoping for OR lists, added PropertyLocation.Clone(), centralized and hardened starship relocation during property deletion, and wrapped property spawn operations in try/catch to log and continue on errors.

Changes

Cohort / File(s) Summary
Property Layout Defensive Checks
SWLOR.Game.Server/Feature/PropertyLayoutDefinition/BankLayoutDefinition.cs, SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs
Spawn and access logic now validate DB lookups for property, parent building, and city; on missing records the handlers log detailed errors/messages and return early to skip spawn or deny access.
Database Query Scoping
SWLOR.Game.Server/Service/DBService/DBQuery.cs
AddFieldSearch overloads treat empty enumerables as no-ops and wrap pipe-joined OR lists in parentheses (e.g., `@field:(a
Property Deletion & Ship Relocation
SWLOR.Game.Server/Service/Property.cs
Major cleanup: use TryGetValue for DockPosition, prefer/clone LastNPCDockPosition when relocating ships, fallback to resolved CZ-220 waypoint, iterate child IDs via ToList() and DB.Get (skip missing), add RelocateDockedShips helper to move/clear starport registrations, and wrap SpawnIntoWorld with try/catch to log and continue on exceptions.
Structure Change Cleanup
SWLOR.Game.Server/Service/PropertyService/StructureChangedAction.cs
Removed per-starship relocation from RetrieveStarport; only dock-point unregistration remains (relocation centralized elsewhere).
PropertyLocation Clone
SWLOR.Game.Server/Service/PropertyService/PropertyLocation.cs
Added public PropertyLocation Clone() to produce a shallow copy and avoid reference aliasing when duplicating location objects.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Deleter as "Property.DeleteProperty"
  participant DB as "DB Service"
  participant Ship as "Starship Entity"
  participant Space as "Space Service"
  participant Logger as "Log"

  Deleter->>DB: Load target property, snapshot ChildPropertyIds
  loop for each childId
    Deleter->>DB: Get(childId)
    alt child found
      Deleter->>DB: Load child interior & docked ships
      loop per ship
        Deleter->>Ship: Clear RegisteredStarport
        Deleter->>Ship: Set DockPosition / LastNPCDockPosition (use Clone or CZ-220 fallback)
        Deleter->>DB: Persist ship changes
      end
      Deleter->>Space: Unregister dock points
    else missing child
      Deleter->>Logger: Write error and continue
    end
  end
  Deleter->>Logger: Log completion
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through records, checked each door,
Cloned a spot so refs don't bore,
Scoped the queries, caught the null,
Sent ships ashore and logged the lull—
A tidy burrow, safe once more.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 57.14% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Bugfix/property deletion orphan fix' is directly related to the main changes in the pull request, which focus on defensive handling of missing database dependencies during property deletion and preventing orphaned docked starships.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bugfix/property-deletion-orphan-fix

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

❤️ Share

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 976-1007: In RelocateDockedShips, after clearing
RegisteredStarport for a ship but before skipping when LastNPCDockPosition is
missing, add a warning log that the ship was left without a dock fallback
(include ship.Id, ship.CustomName and the parent
property.Id/property.CustomName) so stranded ships are visible; reference the
RelocateDockedShips method, the RegisteredStarport key mutation, and the
Positions keys (PropertyLocationType.LastNPCDockPosition and
PropertyLocationType.DockPosition) and ensure you still avoid mutating
DockPosition when LastNPCDockPosition is absent.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: af108ad8-079f-4549-8e35-d5dc0e412b03

📥 Commits

Reviewing files that changed from the base of the PR and between 90f3721 and 2ff78dc.

📒 Files selected for processing (5)
  • SWLOR.Game.Server/Feature/PropertyLayoutDefinition/BankLayoutDefinition.cs
  • SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs
  • SWLOR.Game.Server/Service/DBService/DBQuery.cs
  • SWLOR.Game.Server/Service/Property.cs
  • SWLOR.Game.Server/Service/PropertyService/StructureChangedAction.cs

Comment thread SWLOR.Game.Server/Service/Property.cs
@zunath
Copy link
Copy Markdown
Owner Author

zunath commented Apr 24, 2026

@coderabbitai - review now that the rate limit is over

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

@zunath Sure! Kicking off the review now.


🧠 Learnings used
Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-08T02:53:03.088Z
Learning: In the SWLOR_NWN repository (zunath/SWLOR_NWN), maintainer zunath wants CodeRabbit to always check for merge conflicts when reviewing PRs and explicitly flag them to contributors.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1957
File: Module/ifo/module.ifo.json:2899-2899
Timestamp: 2026-03-29T13:23:17.045Z
Learning: In the SWLOR_NWN repository, `Module/ifo/module.ifo.json` has been historically corrupted by PRs that introduced truncated or missing lines (e.g., incomplete entries in `Mod_Area_list`). On every PR touching this file, always: (1) verify the file is valid, well-formed JSON; (2) check every entry in `Mod_Area_list` has a complete `__struct_id` and `Area_Name` block with both `type` and `value` fields; (3) flag any truncated, incomplete, or empty entries; (4) flag any removals from `Mod_Area_list` unless explicitly confirmed as intentional by the PR author.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1929
File: Module/utc/frogboss.utc.json:55-58
Timestamp: 2025-09-12T22:58:09.040Z
Learning: DecayTime in UTC files for this SWLOR NWN project doesn't need adjustment because the solution has custom code that overrides the default NWN behavior and keeps NPC bodies around longer than the specified DecayTime value.
✅ Actions performed

Review triggered.

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

@zunath
Copy link
Copy Markdown
Owner Author

zunath commented Apr 25, 2026

@coderabbitai - Try to review the code again. The rate limit should be over

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 25, 2026

@zunath Sure! Triggering the review now.


🧠 Learnings used
Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-08T02:53:03.088Z
Learning: In the SWLOR_NWN repository (zunath/SWLOR_NWN), maintainer zunath wants CodeRabbit to always check for merge conflicts when reviewing PRs and explicitly flag them to contributors.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1957
File: Module/ifo/module.ifo.json:2899-2899
Timestamp: 2026-03-29T13:23:17.045Z
Learning: In the SWLOR_NWN repository, `Module/ifo/module.ifo.json` has been historically corrupted by PRs that introduced truncated or missing lines (e.g., incomplete entries in `Mod_Area_list`). On every PR touching this file, always: (1) verify the file is valid, well-formed JSON; (2) check every entry in `Mod_Area_list` has a complete `__struct_id` and `Area_Name` block with both `type` and `value` fields; (3) flag any truncated, incomplete, or empty entries; (4) flag any removals from `Mod_Area_list` unless explicitly confirmed as intentional by the PR author.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1929
File: Module/utc/frogboss.utc.json:55-58
Timestamp: 2025-09-12T22:58:09.040Z
Learning: DecayTime in UTC files for this SWLOR NWN project doesn't need adjustment because the solution has custom code that overrides the default NWN behavior and keeps NPC bodies around longer than the specified DecayTime value.
✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
SWLOR.Game.Server/Service/Property.cs (1)

1026-1068: Optional: cz220Cache is used only as a "resolved successfully" flag.

The PropertyLocation returned at line 1056 is never stored on a ship — it's only read for the null check at line 1060, and lines 1067-1068 rebuild it from scratch (deliberately, to avoid cross-ship aliasing). A boolean would convey the intent more clearly and avoid the wasted instance:

♻️ Optional simplification
-            // Lazily resolved + cached across the loop so we only hit GetWaypointByTag once,
-            // and only when at least one stranded ship actually needs it.
-            PropertyLocation cz220Cache = null;
-            var cz220Resolved = false;
+            // Lazily probed across the loop so we only hit GetWaypointByTag once,
+            // and only when at least one stranded ship actually needs it.
+            bool? cz220Available = null;
@@
-                // No personal fallback - try the guaranteed CZ-220 NPC starport.
-                if (!cz220Resolved)
-                {
-                    cz220Cache = BuildGuaranteedFallbackDockPosition();
-                    cz220Resolved = true;
-                }
-
-                if (cz220Cache != null)
+                // No personal fallback - try the guaranteed CZ-220 NPC starport.
+                cz220Available ??= BuildGuaranteedFallbackDockPosition() != null;
+
+                if (cz220Available == true)

Functionally equivalent; purely a readability nit.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SWLOR.Game.Server/Service/Property.cs` around lines 1026 - 1068, cz220Cache
is only used as a null-check marker while cz220Resolved indicates resolution;
replace cz220Cache with a simple bool to avoid creating unused PropertyLocation
instances: remove cz220Cache, keep cz220Resolved (or rename to
cz220Resolved/hasGuaranteedFallback), call BuildGuaranteedFallbackDockPosition()
once when cz220Resolved is false to set the flag, and later always call
BuildGuaranteedFallbackDockPosition() when assigning
ship.Positions[PropertyLocationType.DockPosition] and
ship.Positions[PropertyLocationType.LastNPCDockPosition]; update references in
the loop accordingly (symbols: cz220Cache, cz220Resolved,
BuildGuaranteedFallbackDockPosition, PropertyLocationType.DockPosition,
PropertyLocationType.LastNPCDockPosition).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 1043-1051: The code assigns
ship.Positions[PropertyLocationType.DockPosition] to the same reference as
ship.Positions[PropertyLocationType.LastNPCDockPosition], causing aliasing;
update the LastNPCDockPosition path in the relocation logic (the block using
hasOwnFallback) to produce a distinct copy instead of assigning the same
object—either by calling BuildGuaranteedFallbackDockPosition() twice (as the
CZ-220 path does) or by cloning the PropertyLocation instance before assigning
to DockPosition; also apply the same clone/fix to the identical aliasing at
CleanUpData (line ~433) and first confirm whether PropertyLocation in
SWLOR.Game.Server.Entity is a class (reference) or struct (value) to choose
cloning vs. direct assignment.

---

Nitpick comments:
In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 1026-1068: cz220Cache is only used as a null-check marker while
cz220Resolved indicates resolution; replace cz220Cache with a simple bool to
avoid creating unused PropertyLocation instances: remove cz220Cache, keep
cz220Resolved (or rename to cz220Resolved/hasGuaranteedFallback), call
BuildGuaranteedFallbackDockPosition() once when cz220Resolved is false to set
the flag, and later always call BuildGuaranteedFallbackDockPosition() when
assigning ship.Positions[PropertyLocationType.DockPosition] and
ship.Positions[PropertyLocationType.LastNPCDockPosition]; update references in
the loop accordingly (symbols: cz220Cache, cz220Resolved,
BuildGuaranteedFallbackDockPosition, PropertyLocationType.DockPosition,
PropertyLocationType.LastNPCDockPosition).
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 253eb7a3-c38d-4618-8724-8f927fadb50a

📥 Commits

Reviewing files that changed from the base of the PR and between 2ff78dc and 3e49eb7.

📒 Files selected for processing (1)
  • SWLOR.Game.Server/Service/Property.cs

Comment thread SWLOR.Game.Server/Service/Property.cs
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
SWLOR.Game.Server/Service/Property.cs (1)

422-441: ⚠️ Potential issue | 🟠 Major

Unguarded Positions[LastNPCDockPosition] access can crash CleanUpData.

RelocateDockedShips guards its LastNPCDockPosition path with ContainsKey (line 1047), but this block doesn't. If any legacy starship record was persisted without a LastNPCDockPosition entry and its DockPosition.InstancePropertyId points at a deleted starport, line 437 will throw KeyNotFoundException and abort the rest of the starship cleanup loop — potentially leaving other ships stranded on the same boot.

Given this was the exact class of orphaning that motivated the PR, please align with RelocateDockedShips and handle the missing-fallback case (CZ-220 backfill, or at minimum a logged warning + skip).

🛡️ Suggested guard
                 var dbStarport = DB.Get<WorldProperty>(dockPosition.InstancePropertyId);
                 if (dbStarport == null)
                 {
                     // The PC starport no longer exists (probably destroyed by the previous cleanup)
                     // Luckily we track the last NPC starport they visited so we can simply replace
                     // their docked position with it.
                     //
                     // Clone rather than assigning the same reference to both keys. PropertyLocation
                     // is a reference type, and later in-place mutations of DockPosition (e.g. in
                     // migrations) would otherwise silently corrupt LastNPCDockPosition too.
-                    property.Positions[PropertyLocationType.DockPosition] = property.Positions[PropertyLocationType.LastNPCDockPosition].Clone();
-
-                    Log.Write(LogGroup.Property, $"Starship '{property.CustomName}' ({property.Id}) was docked at a non-existent player starport. It has been relocated to the last NPC dock position at '{property.Positions[PropertyLocationType.LastNPCDockPosition].AreaResref}'.");
+                    if (property.Positions.TryGetValue(PropertyLocationType.LastNPCDockPosition, out var lastNpc))
+                    {
+                        property.Positions[PropertyLocationType.DockPosition] = lastNpc.Clone();
+                        Log.Write(LogGroup.Property, $"Starship '{property.CustomName}' ({property.Id}) was docked at a non-existent player starport. It has been relocated to the last NPC dock position at '{lastNpc.AreaResref}'.");
+                    }
+                    else
+                    {
+                        Log.Write(LogGroup.Error, $"Starship '{property.CustomName}' ({property.Id}) was docked at a non-existent player starport and has no LastNPCDockPosition. DockPosition left untouched; manual intervention may be required.", true);
+                    }
                 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SWLOR.Game.Server/Service/Property.cs` around lines 422 - 441, The code in
Property.cs unguards access to
property.Positions[PropertyLocationType.LastNPCDockPosition] when replacing a
missing player dock, which can throw KeyNotFoundException and abort CleanUpData;
update the block in the starship cleanup logic (same area as the DockPosition
handling) to first check
property.Positions.ContainsKey(PropertyLocationType.LastNPCDockPosition)
(matching RelocateDockedShips) and if missing either log a warning via Log.Write
and skip relocation or use a safe fallback before calling Clone(), ensuring you
reference Property.Positions, PropertyLocationType.DockPosition,
PropertyLocationType.LastNPCDockPosition, and preserve the existing log message
when relocation occurs.
🧹 Nitpick comments (1)
SWLOR.Game.Server/Service/Property.cs (1)

1060-1080: Reuse cz220Cache instead of re-resolving the waypoint per assignment.

Lines 1028-1031 introduce cz220Cache specifically to hash the waypoint lookup once per call, but lines 1074-1075 ignore the cache and re-invoke BuildGuaranteedFallbackDockPosition() twice for every stranded ship (and once more each for any additional ships). Beyond the redundant GetWaypointByTag/GetArea/GetPosition calls, it also means the two assignments could theoretically diverge (e.g. DockPosition set, second call returns null on a transient failure, LastNPCDockPosition clobbered to null).

Now that PropertyLocation.Clone() exists, clone the cached instance instead — same no-aliasing guarantee, one waypoint resolution per deletion.

♻️ Suggested cleanup
                 if (cz220Cache != null)
                 {
                     // Use independent PropertyLocation instances per Positions key. Storing the
                     // same reference under both keys would alias them, so a later in-place
                     // mutation of one (see _11_RefundMobilityAndUpdateVelesStarport for an
                     // example of in-place mutation of these structs) would silently corrupt
                     // the other.
-                    ship.Positions[PropertyLocationType.DockPosition] = BuildGuaranteedFallbackDockPosition();
-                    ship.Positions[PropertyLocationType.LastNPCDockPosition] = BuildGuaranteedFallbackDockPosition();
+                    ship.Positions[PropertyLocationType.DockPosition] = cz220Cache.Clone();
+                    ship.Positions[PropertyLocationType.LastNPCDockPosition] = cz220Cache.Clone();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SWLOR.Game.Server/Service/Property.cs` around lines 1060 - 1080, The code
re-computes the CZ-220 fallback waypoint twice per ship by calling
BuildGuaranteedFallbackDockPosition() for both
ship.Positions[PropertyLocationType.DockPosition] and
PropertyLocationType.LastNPCDockPosition instead of reusing cz220Cache; replace
those two calls with cloned copies of the already-resolved cz220Cache (via
PropertyLocation.Clone()) so each position gets an independent instance while
avoiding redundant GetWaypointByTag/GetArea/GetPosition calls and possible
transient divergence.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 1082-1086: The warning about an unrecoverable dock position loss
is being logged to LogGroup.Property; change the Log.Write call so it uses
LogGroup.Error instead of LogGroup.Property to ensure operators see it as an
error; do the same for the companion DB.Get<WorldProperty> miss noted earlier
(replace its Log.Write LogGroup.Property usage with LogGroup.Error). Ensure the
Log.Write signatures and message text remain unchanged except for the LogGroup
enum value.

---

Outside diff comments:
In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 422-441: The code in Property.cs unguards access to
property.Positions[PropertyLocationType.LastNPCDockPosition] when replacing a
missing player dock, which can throw KeyNotFoundException and abort CleanUpData;
update the block in the starship cleanup logic (same area as the DockPosition
handling) to first check
property.Positions.ContainsKey(PropertyLocationType.LastNPCDockPosition)
(matching RelocateDockedShips) and if missing either log a warning via Log.Write
and skip relocation or use a safe fallback before calling Clone(), ensuring you
reference Property.Positions, PropertyLocationType.DockPosition,
PropertyLocationType.LastNPCDockPosition, and preserve the existing log message
when relocation occurs.

---

Nitpick comments:
In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 1060-1080: The code re-computes the CZ-220 fallback waypoint twice
per ship by calling BuildGuaranteedFallbackDockPosition() for both
ship.Positions[PropertyLocationType.DockPosition] and
PropertyLocationType.LastNPCDockPosition instead of reusing cz220Cache; replace
those two calls with cloned copies of the already-resolved cz220Cache (via
PropertyLocation.Clone()) so each position gets an independent instance while
avoiding redundant GetWaypointByTag/GetArea/GetPosition calls and possible
transient divergence.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8020b6b3-1187-4ca1-a8c8-c7417a7d06b8

📥 Commits

Reviewing files that changed from the base of the PR and between 3e49eb7 and 9919b76.

📒 Files selected for processing (2)
  • SWLOR.Game.Server/Service/Property.cs
  • SWLOR.Game.Server/Service/PropertyService/PropertyLocation.cs

Comment thread SWLOR.Game.Server/Service/Property.cs Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
SWLOR.Game.Server/Service/Property.cs (2)

446-456: Mirror the RelocateDockedShips cache-and-Clone() pattern here.

This branch calls BuildGuaranteedFallbackDockPosition() twice per stranded ship — once into cz220Dock and again on Line 453 — to avoid aliasing. The new RelocateDockedShips helper (Lines 1083-1099) handles the same problem more cleanly: resolve the waypoint once, then Clone() for each Positions key. That pattern is also faster across the loop because GetWaypointByTag/GetArea/GetPosition only run once total instead of twice per stranded ship, and it keeps the two cleanup paths consistent so future edits to one don't drift from the other.

♻️ Suggested alignment with RelocateDockedShips
-                            var cz220Dock = BuildGuaranteedFallbackDockPosition();
-                            if (cz220Dock != null)
-                            {
-                                property.Positions[PropertyLocationType.DockPosition] = cz220Dock;
-                                property.Positions[PropertyLocationType.LastNPCDockPosition] = BuildGuaranteedFallbackDockPosition();
+                            var cz220Dock = BuildGuaranteedFallbackDockPosition();
+                            if (cz220Dock != null)
+                            {
+                                property.Positions[PropertyLocationType.DockPosition] = cz220Dock.Clone();
+                                property.Positions[PropertyLocationType.LastNPCDockPosition] = cz220Dock.Clone();

Optionally hoist BuildGuaranteedFallbackDockPosition() out of the foreach (var property in starshipProperties) loop and lazy-resolve it (the same pattern as cz220Cache / cz220Resolved on Lines 1052-1053) so the waypoint is looked up at most once across the whole CleanUpData pass.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SWLOR.Game.Server/Service/Property.cs` around lines 446 - 456, This code
calls BuildGuaranteedFallbackDockPosition() twice which re-resolves the waypoint
and causes aliasing; mirror the RelocateDockedShips pattern by resolving the
fallback once into a local cache (like cz220Cache/cz220Resolved), then assign
property.Positions[PropertyLocationType.DockPosition] = cz220Cache.Clone() and
property.Positions[PropertyLocationType.LastNPCDockPosition] =
cz220Cache.Clone(); alternatively hoist the cached resolve outside the starship
loop so BuildGuaranteedFallbackDockPosition() runs at most once per CleanUpData
pass, and ensure you use Clone() to avoid shared references between the two
Positions entries.

1057-1067: Surface the missing-ship case in logs.

When DB.Get<WorldProperty>(shipId) returns null we silently continue, which is inconsistent with the equivalent missing-child path in DeleteProperty (Line 900) that logs at LogGroup.Error. A starport tracking a ship id that no longer resolves is the same class of orphaned reference this PR is trying to make visible — it should be logged the same way so operators don't have to guess why a stranded ship never showed up at CZ-220.

🪵 Suggested log on the missing-ship branch
                 var ship = DB.Get<WorldProperty>(shipId);
                 if (ship == null)
+                {
+                    Log.Write(LogGroup.Error, $"Starport '{property.CustomName}' ({property.Id}) referenced docked ship '{shipId}' which no longer exists. Skipping relocation.", true);
                     continue;
+                }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SWLOR.Game.Server/Service/Property.cs` around lines 1057 - 1067, The loop
over dockedShipIds silently skips when DB.Get<WorldProperty>(shipId) returns
null; add an error log (using the same LogGroup.Error pattern used in
DeleteProperty) that reports the missing shipId and the enclosing property
context (e.g., the starport/property.Id or property.Name) so orphaned ship
references are visible to operators; make this change in the same foreach that
uses dockedShipIds and DB.Get<WorldProperty>(shipId) (referencing the ship
variable and the current property variable) and ensure the new log message
mirrors the style and severity used at the DeleteProperty missing-child branch.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 446-456: This code calls BuildGuaranteedFallbackDockPosition()
twice which re-resolves the waypoint and causes aliasing; mirror the
RelocateDockedShips pattern by resolving the fallback once into a local cache
(like cz220Cache/cz220Resolved), then assign
property.Positions[PropertyLocationType.DockPosition] = cz220Cache.Clone() and
property.Positions[PropertyLocationType.LastNPCDockPosition] =
cz220Cache.Clone(); alternatively hoist the cached resolve outside the starship
loop so BuildGuaranteedFallbackDockPosition() runs at most once per CleanUpData
pass, and ensure you use Clone() to avoid shared references between the two
Positions entries.
- Around line 1057-1067: The loop over dockedShipIds silently skips when
DB.Get<WorldProperty>(shipId) returns null; add an error log (using the same
LogGroup.Error pattern used in DeleteProperty) that reports the missing shipId
and the enclosing property context (e.g., the starport/property.Id or
property.Name) so orphaned ship references are visible to operators; make this
change in the same foreach that uses dockedShipIds and
DB.Get<WorldProperty>(shipId) (referencing the ship variable and the current
property variable) and ensure the new log message mirrors the style and severity
used at the DeleteProperty missing-child branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 79f1f54c-fbcc-4e50-b48a-afa93427b751

📥 Commits

Reviewing files that changed from the base of the PR and between 9919b76 and 68d7b9b.

📒 Files selected for processing (1)
  • SWLOR.Game.Server/Service/Property.cs

@zunath
Copy link
Copy Markdown
Owner Author

zunath commented Apr 27, 2026

@coderabbitai - Any other issues you can find that haven't been resolved already?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Let me do a deeper look at the changed files to find any remaining issues.


🏁 Script executed:

#!/bin/bash
# Read the key changed files for analysis
echo "=== Property.cs ==="
fd -e cs -g "Property.cs" --full-path | grep -v Test | grep "Service/Property.cs"

echo "=== DBQuery.cs ==="
fd -e cs -g "DBQuery.cs"

echo "=== PropertyLocation.cs ==="
fd -e cs -g "PropertyLocation.cs"

echo "=== StructureChangedAction.cs ==="
fd -e cs -g "StructureChangedAction.cs"

echo "=== BankLayoutDefinition.cs ==="
fd -e cs -g "BankLayoutDefinition.cs"

echo "=== StarportLayoutDefinition.cs ==="
fd -e cs -g "StarportLayoutDefinition.cs"

Length of output: 500


🏁 Script executed:

#!/bin/bash
cat SWLOR.Game.Server/Service/Property.cs

Length of output: 120487


🏁 Script executed:

#!/bin/bash
cat SWLOR.Game.Server/Service/DBService/DBQuery.cs

Length of output: 7941


🏁 Script executed:

#!/bin/bash
cat SWLOR.Game.Server/Service/PropertyService/PropertyLocation.cs
cat SWLOR.Game.Server/Service/PropertyService/StructureChangedAction.cs
cat SWLOR.Game.Server/Feature/PropertyLayoutDefinition/BankLayoutDefinition.cs
cat SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs

Length of output: 29373


@zunath Here are the remaining issues I found that don't appear to have been addressed yet:


1. 🐛 CleanUpData — Unguarded indexer on DockPosition (Property.cs)

var dockPosition = property.Positions[PropertyLocationType.DockPosition];

If any legacy starship record is missing the DockPosition key entirely (edge-case corruption, migration gap, etc.), this throws a KeyNotFoundException that aborts the entire starship cleanup loop — exactly the kind of abort that the SpawnIntoWorld try/catch was added to prevent. Suggest using TryGetValue:

if (!property.Positions.TryGetValue(PropertyLocationType.DockPosition, out var dockPosition))
{
    Log.Write(LogGroup.Error, $"Starship '{property.CustomName}' ({property.Id}) is missing a DockPosition entry. Skipping relocation check.", true);
    DB.Set(property);
    continue;
}

2. 🐛 CanAccess — No null guards, unlike the new OnSpawn handler (StarportLayoutDefinition.cs)

The PR added null checks to the OnSpawn callback but the runtime path—called every time a player uses a starport terminal—has no equivalent protection:

private static bool CanAccess(uint player)
{
    ...
    var dbProperty = DB.Get<WorldProperty>(propertyId);        // could be null
    var dbBuilding = DB.Get<WorldProperty>(dbProperty.ParentPropertyId); // NullReferenceException
    var cityId = dbBuilding.ParentPropertyId;                  // NullReferenceException
    ...
}

If a player is inside a starport whose parent record was already deleted (or is orphaned), this throws at runtime instead of failing gracefully. Suggest mirroring the OnSpawn null guards:

if (dbProperty == null)
{
    SendMessageToPC(player, "This terminal is unavailable. Please notify an administrator.");
    return false;
}
var dbBuilding = DB.Get<WorldProperty>(dbProperty.ParentPropertyId);
if (dbBuilding == null)
{
    SendMessageToPC(player, "This terminal is unavailable. Please notify an administrator.");
    return false;
}

3. ⚠️ AddFieldSearch — Empty list produces invalid RediSearch syntax (DBQuery.cs)

Both new overloads wrap the pipe-joined values in parentheses, but if an empty IEnumerable is passed, the result is @field:() — an invalid RediSearch expression that will throw at query time. A simple guard would prevent this:

public DBQuery<T> AddFieldSearch(string fieldName, IEnumerable<int> search)
{
    var list = search.ToList();
    if (list.Count == 0) return this; // or throw ArgumentException

    var searchText = $"({string.Join("|", list)})";
    ...
}

Same applies to the IEnumerable<string> overload.


4. 🐛 Pre-existing: Inverted tie-breaking condition in ProcessCityElections (Property.cs)

This is not introduced by this PR, but it is worth flagging. The condition comment says "if the top two are the same (tied), incumbent wins" but the actual check is the opposite:

// Comment says: "If top two are the same, incumbent mayor wins."
else if (orderedVotes.Count >= 2 && orderedVotes.ElementAt(0).Value != orderedVotes.ElementAt(1).Value)
//                                                                    ^^^ should be ==
{
    Log.Write(LogGroup.Property, $"Top 2 candidates were tied. Incumbent mayor '{incumbentMayorId}' wins the election.");
}

With !=, this branch fires when votes are different (a clear winner exists), but logs "tied" and leaves the incumbent in place — and the actual winner never gets the city. The condition should be ==.


5. 🐛 Pre-existing: RefreshPermissions removes from wrong dictionary (Property.cs)

Also not introduced by this PR, but related to permissions correctness:

for (var index = dbPermission.GrantPermissions.Count - 1; index >= 0; index--)
{
    var grantPermission = dbPermission.GrantPermissions.ElementAt(index).Key;
    if (!masterList.Contains(grantPermission))
    {
        dbPermission.Permissions.Remove(grantPermission); // ← should be GrantPermissions.Remove
        ...
    }
}

Grant permissions are never actually pruned; instead, entries are incorrectly removed from the regular Permissions dictionary.


Items 1–3 are directly related to this PR's changes and could cause runtime failures. Items 4–5 are pre-existing bugs — would you like me to open separate issues for those?


🧠 Learnings used
Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-08T02:53:03.088Z
Learning: In the SWLOR_NWN repository (zunath/SWLOR_NWN), maintainer zunath wants CodeRabbit to always check for merge conflicts when reviewing PRs and explicitly flag them to contributors.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1957
File: Module/ifo/module.ifo.json:2899-2899
Timestamp: 2026-03-29T13:23:17.045Z
Learning: In the SWLOR_NWN repository, `Module/ifo/module.ifo.json` has been historically corrupted by PRs that introduced truncated or missing lines (e.g., incomplete entries in `Mod_Area_list`). On every PR touching this file, always: (1) verify the file is valid, well-formed JSON; (2) check every entry in `Mod_Area_list` has a complete `__struct_id` and `Area_Name` block with both `type` and `value` fields; (3) flag any truncated, incomplete, or empty entries; (4) flag any removals from `Mod_Area_list` unless explicitly confirmed as intentional by the PR author.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1929
File: Module/utc/frogboss.utc.json:55-58
Timestamp: 2025-09-12T22:58:09.040Z
Learning: DecayTime in UTC files for this SWLOR NWN project doesn't need adjustment because the solution has custom code that overrides the default NWN behavior and keeps NPC bodies around longer than the specified DecayTime value.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1906
File: Module/are/viscaradeepmount.are.json:44194-44196
Timestamp: 2025-08-03T15:13:29.324Z
Learning: In the SWLOR_NWN repository, the "Version" field in area files (e.g., Module/are/*.are.json) is incremented automatically by the external toolset with each change and does not affect functionality. It should not be flagged as an issue during code review.

@zunath
Copy link
Copy Markdown
Owner Author

zunath commented Apr 27, 2026

@coderabbitai - Open tickets for the extra issues you found and provide AI agent prompts for each of the 5 issues.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

Opening all 5 issues now.


🏁 Script executed:

#!/bin/bash
# Issue 1: Unguarded indexer on DockPosition in CleanUpData
gh issue create \
  --repo zunath/SWLOR_NWN \
  --title "Bug: Unguarded DockPosition indexer in CleanUpData can abort starship cleanup loop (Property.cs)" \
  --assignee zunath \
  --body "## Summary
In \`SWLOR.Game.Server/Service/Property.cs\`, the \`CleanUpData\` method accesses \`property.Positions[PropertyLocationType.DockPosition]\` using a direct dictionary indexer. If a legacy or corrupted starship record is missing the \`DockPosition\` key, this throws a \`KeyNotFoundException\` that aborts the entire starship cleanup loop.

This is especially concerning since the PR (`#1974`) added \`try/catch\` to \`SpawnIntoWorld\` to prevent exactly this kind of abort — the same defensive pattern should be applied here.

## Steps to Reproduce
1. Have a starship record in the database with a missing \`DockPosition\` entry (e.g., due to corruption or a migration gap).
2. Trigger property deletion/cleanup that calls \`CleanUpData\`.
3. The loop throws a \`KeyNotFoundException\` and aborts all subsequent starship processing.

## Suggested Fix
Replace the direct indexer with \`TryGetValue\`:
\`\`\`csharp
if (!property.Positions.TryGetValue(PropertyLocationType.DockPosition, out var dockPosition))
{
    Log.Write(LogGroup.Error, \$\"Starship '{property.CustomName}' ({property.Id}) is missing a DockPosition entry. Skipping relocation check.\", true);
    DB.Set(property);
    continue;
}
\`\`\`

## References
- PR: https://github.com/zunath/SWLOR_NWN/pull/1974
- Reported by: `@zunath`
- File: \`SWLOR.Game.Server/Service/Property.cs\`"

echo "---ISSUE1DONE---"

# Issue 2: CanAccess no null guards in StarportLayoutDefinition.cs
gh issue create \
  --repo zunath/SWLOR_NWN \
  --title "Bug: CanAccess in StarportLayoutDefinition lacks null guards, can throw NullReferenceException at runtime" \
  --assignee zunath \
  --body "## Summary
In \`SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs\`, PR `#1974` added null checks to the \`OnSpawn\` callback, but the \`CanAccess\` method (called every time a player interacts with a starport terminal) has no equivalent protection. If a player is inside a starport whose parent record has been deleted or orphaned, \`DB.Get\` returns null and the subsequent property chain access throws a \`NullReferenceException\`.

## Affected Code
\`\`\`csharp
var dbProperty = DB.Get<WorldProperty>(propertyId);        // could be null
var dbBuilding = DB.Get<WorldProperty>(dbProperty.ParentPropertyId); // NullReferenceException if dbProperty is null
var cityId = dbBuilding.ParentPropertyId;                  // NullReferenceException if dbBuilding is null
\`\`\`

## Suggested Fix
Mirror the null guards added to \`OnSpawn\`:
\`\`\`csharp
if (dbProperty == null)
{
    SendMessageToPC(player, \"This terminal is unavailable. Please notify an administrator.\");
    return false;
}
var dbBuilding = DB.Get<WorldProperty>(dbProperty.ParentPropertyId);
if (dbBuilding == null)
{
    SendMessageToPC(player, \"This terminal is unavailable. Please notify an administrator.\");
    return false;
}
\`\`\`

## References
- PR: https://github.com/zunath/SWLOR_NWN/pull/1974
- Reported by: `@zunath`
- File: \`SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs\`"

echo "---ISSUE2DONE---"

# Issue 3: AddFieldSearch empty list in DBQuery.cs
gh issue create \
  --repo zunath/SWLOR_NWN \
  --title "Bug: AddFieldSearch with empty IEnumerable produces invalid RediSearch syntax '@field:()' (DBQuery.cs)" \
  --assignee zunath \
  --body "## Summary
In \`SWLOR.Game.Server/Service/DBService/DBQuery.cs\`, PR `#1974` updated the \`AddFieldSearch\` overloads for \`IEnumerable<int>\` and \`IEnumerable<string>\` to wrap pipe-joined values in parentheses (e.g., \`@field:(a|b|c)\`). However, if an empty \`IEnumerable\` is passed, the resulting query string becomes \`@field:()\`, which is invalid RediSearch syntax and will cause a query-time exception.

## Steps to Reproduce
1. Call \`AddFieldSearch\` with an empty list/enumerable.
2. Execute the query.
3. RediSearch throws an exception due to the invalid \`@field:()\` expression.

## Suggested Fix
Add an early return (or throw \`ArgumentException\`) if the list is empty:
\`\`\`csharp
public DBQuery<T> AddFieldSearch(string fieldName, IEnumerable<int> search)
{
    var list = search.ToList();
    if (list.Count == 0) return this; // or throw new ArgumentException(\"Search list cannot be empty.\", nameof(search));

    var searchText = \$\"({string.Join(\"|\", list)})\";
    // ... rest of method
}
\`\`\`
Apply the same guard to the \`IEnumerable<string>\` overload.

## References
- PR: https://github.com/zunath/SWLOR_NWN/pull/1974
- Reported by: `@zunath`
- File: \`SWLOR.Game.Server/Service/DBService/DBQuery.cs\`"

echo "---ISSUE3DONE---"

# Issue 4: Inverted tie-breaking in ProcessCityElections
gh issue create \
  --repo zunath/SWLOR_NWN \
  --title "Bug: Inverted tie-breaking condition in ProcessCityElections causes incumbent to win when they shouldn't (Property.cs)" \
  --assignee zunath \
  --body "## Summary
In \`SWLOR.Game.Server/Service/Property.cs\`, the \`ProcessCityElections\` method has an inverted condition for detecting tied votes. The comment says the incumbent mayor should win when the top two candidates are tied, but the condition uses \`!=\` instead of \`==\`, causing the incumbent to be kept in place whenever the top two candidates have **different** vote counts — i.e., when there is a clear winner. The actual winner never receives the city in this scenario.

## Affected Code
\`\`\`csharp
// Comment: \"If top two are the same, incumbent mayor wins.\"
else if (orderedVotes.Count >= 2 && orderedVotes.ElementAt(0).Value != orderedVotes.ElementAt(1).Value)
//                                                                    ^^^ should be ==
{
    Log.Write(LogGroup.Property, \$\"Top 2 candidates were tied. Incumbent mayor '{incumbentMayorId}' wins the election.\");
}
\`\`\`

## Suggested Fix
Change the condition from \`!=\` to \`==\`:
\`\`\`csharp
else if (orderedVotes.Count >= 2 && orderedVotes.ElementAt(0).Value == orderedVotes.ElementAt(1).Value)
{
    Log.Write(LogGroup.Property, \$\"Top 2 candidates were tied. Incumbent mayor '{incumbentMayorId}' wins the election.\");
}
\`\`\`

## References
- Discovered during review of PR: https://github.com/zunath/SWLOR_NWN/pull/1974 (pre-existing bug)
- Reported by: `@zunath`
- File: \`SWLOR.Game.Server/Service/Property.cs\`"

echo "---ISSUE4DONE---"

# Issue 5: RefreshPermissions removes from wrong dictionary
gh issue create \
  --repo zunath/SWLOR_NWN \
  --title "Bug: RefreshPermissions prunes from Permissions instead of GrantPermissions, leaving stale grant entries (Property.cs)" \
  --assignee zunath \
  --body "## Summary
In \`SWLOR.Game.Server/Service/Property.cs\`, the \`RefreshPermissions\` method iterates \`dbPermission.GrantPermissions\` to prune stale entries, but incorrectly calls \`dbPermission.Permissions.Remove(grantPermission)\` instead of \`dbPermission.GrantPermissions.Remove(grantPermission)\`. As a result, grant permissions are never actually pruned — instead, entries are mistakenly removed from the regular \`Permissions\` dictionary, corrupting regular permissions while leaving stale grant permissions intact.

## Affected Code
\`\`\`csharp
for (var index = dbPermission.GrantPermissions.Count - 1; index >= 0; index--)
{
    var grantPermission = dbPermission.GrantPermissions.ElementAt(index).Key;
    if (!masterList.Contains(grantPermission))
    {
        dbPermission.Permissions.Remove(grantPermission); // ← BUG: should be GrantPermissions.Remove
        // ...
    }
}
\`\`\`

## Suggested Fix
\`\`\`csharp
dbPermission.GrantPermissions.Remove(grantPermission);
\`\`\`

## References
- Discovered during review of PR: https://github.com/zunath/SWLOR_NWN/pull/1974 (pre-existing bug)
- Reported by: `@zunath`
- File: \`SWLOR.Game.Server/Service/Property.cs\`"

echo "---ISSUE5DONE---"

Length of output: 325


@zunath All 5 issues have been opened. Here are the links and AI agent prompts for each:


Issue 1 — #1975: Unguarded DockPosition indexer in CleanUpData

AI Agent Prompt:

In SWLOR.Game.Server/Service/Property.cs, locate the CleanUpData method. Find the line that accesses property.Positions[PropertyLocationType.DockPosition] using a direct dictionary indexer. Replace it with a TryGetValue call so that if the key is missing, the code logs an error via Log.Write(LogGroup.Error, ..., true), calls DB.Set(property), and calls continue to skip to the next iteration rather than throwing a KeyNotFoundException. Do not change any other behavior in the method.


Issue 2 — #1976: CanAccess missing null guards in StarportLayoutDefinition

AI Agent Prompt:

In SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs, locate the CanAccess method. After the line var dbProperty = DB.Get<WorldProperty>(propertyId);, add a null check: if dbProperty is null, send the player a message saying the terminal is unavailable and return false. After var dbBuilding = DB.Get<WorldProperty>(dbProperty.ParentPropertyId);, add a similar null check for dbBuilding. Mirror the same pattern used in the OnSpawn null checks added in PR #1974. Do not change any other logic in CanAccess.


Issue 3 — #1977: AddFieldSearch with empty IEnumerable produces invalid RediSearch syntax

AI Agent Prompt:

In SWLOR.Game.Server/Service/DBService/DBQuery.cs, locate both AddFieldSearch overloads that accept IEnumerable<int> and IEnumerable<string>. In each, after materializing the enumerable to a list, add a guard: if the list is empty, return this immediately (or throw an ArgumentException with a clear message). This prevents the construction of the invalid RediSearch expression @field:(). Apply the guard to both overloads identically.


Issue 4 — #1978: Inverted tie-breaking condition in ProcessCityElections

AI Agent Prompt:

In SWLOR.Game.Server/Service/Property.cs, locate the ProcessCityElections method. Find the else if branch that checks whether the top two candidates are tied. The condition currently uses != but the intent (as stated in the adjacent comment) is to detect a tie, so it should use ==. Change orderedVotes.ElementAt(0).Value != orderedVotes.ElementAt(1).Value to orderedVotes.ElementAt(0).Value == orderedVotes.ElementAt(1).Value. Do not change anything else in the method.


Issue 5 — #1979: RefreshPermissions removes from wrong dictionary

AI Agent Prompt:

In SWLOR.Game.Server/Service/Property.cs, locate the RefreshPermissions method. Find the loop that iterates dbPermission.GrantPermissions backwards and prunes stale entries. Inside the loop, the call dbPermission.Permissions.Remove(grantPermission) is incorrect — it should be dbPermission.GrantPermissions.Remove(grantPermission). Make this single-word fix. Do not change any other logic in the method.


🧠 Learnings used
Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-08T02:53:03.088Z
Learning: In the SWLOR_NWN repository (zunath/SWLOR_NWN), maintainer zunath wants CodeRabbit to always check for merge conflicts when reviewing PRs and explicitly flag them to contributors.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1957
File: Module/ifo/module.ifo.json:2899-2899
Timestamp: 2026-03-29T13:23:17.045Z
Learning: In the SWLOR_NWN repository, `Module/ifo/module.ifo.json` has been historically corrupted by PRs that introduced truncated or missing lines (e.g., incomplete entries in `Mod_Area_list`). On every PR touching this file, always: (1) verify the file is valid, well-formed JSON; (2) check every entry in `Mod_Area_list` has a complete `__struct_id` and `Area_Name` block with both `type` and `value` fields; (3) flag any truncated, incomplete, or empty entries; (4) flag any removals from `Mod_Area_list` unless explicitly confirmed as intentional by the PR author.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1929
File: Module/utc/frogboss.utc.json:55-58
Timestamp: 2025-09-12T22:58:09.040Z
Learning: DecayTime in UTC files for this SWLOR NWN project doesn't need adjustment because the solution has custom code that overrides the default NWN behavior and keeps NPC bodies around longer than the specified DecayTime value.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1906
File: Module/are/viscaradeepmount.are.json:44194-44196
Timestamp: 2025-08-03T15:13:29.324Z
Learning: In the SWLOR_NWN repository, the "Version" field in area files (e.g., Module/are/*.are.json) is incremented automatically by the external toolset with each change and does not affect functionality. It should not be flagged as an issue during code review.

@zunath
Copy link
Copy Markdown
Owner Author

zunath commented Apr 27, 2026

@coderabbitai - Review now that the rate limit is over

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 27, 2026

@zunath Sure! Triggering the review now.


🧠 Learnings used
Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1957
File: Module/ifo/module.ifo.json:2899-2899
Timestamp: 2026-03-29T13:23:17.045Z
Learning: In the SWLOR_NWN repository, `Module/ifo/module.ifo.json` has been historically corrupted by PRs that introduced truncated or missing lines (e.g., incomplete entries in `Mod_Area_list`). On every PR touching this file, always: (1) verify the file is valid, well-formed JSON; (2) check every entry in `Mod_Area_list` has a complete `__struct_id` and `Area_Name` block with both `type` and `value` fields; (3) flag any truncated, incomplete, or empty entries; (4) flag any removals from `Mod_Area_list` unless explicitly confirmed as intentional by the PR author.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-08T02:53:03.088Z
Learning: In the SWLOR_NWN repository (zunath/SWLOR_NWN), maintainer zunath wants CodeRabbit to always check for merge conflicts when reviewing PRs and explicitly flag them to contributors.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1906
File: Module/are/viscaradeepmount.are.json:44194-44196
Timestamp: 2025-08-03T15:13:29.324Z
Learning: In the SWLOR_NWN repository, the "Version" field in area files (e.g., Module/are/*.are.json) is incremented automatically by the external toolset with each change and does not affect functionality. It should not be flagged as an issue during code review.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1929
File: Module/utc/frogboss.utc.json:55-58
Timestamp: 2025-09-12T22:58:09.040Z
Learning: DecayTime in UTC files for this SWLOR NWN project doesn't need adjustment because the solution has custom code that overrides the default NWN behavior and keeps NPC bodies around longer than the specified DecayTime value.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1906
File: Module/dlg/repbase_sgtqon.dlg.json:2-2
Timestamp: 2025-08-03T15:13:58.831Z
Learning: In SWLOR NWN module dialog files (DLG), the trailing space in __data_type fields (e.g., "DLG " instead of "DLG") is added by official Neverwinter Nights tooling and is the correct format. This should not be flagged as an issue during code review.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-07T21:18:45.269Z
Learning: In SWLOR_NWN, the directory `SWLOR.Game.Server/Docker/influxdb/` is used for a local Docker InfluxDB instance. The runtime data subdirectories (`data/`, `wal/`, `meta/`) are NOT gitignored and can be accidentally committed. On every PR, always flag any files under `SWLOR.Game.Server/Docker/influxdb/data/`, `SWLOR.Game.Server/Docker/influxdb/wal/`, or `SWLOR.Game.Server/Docker/influxdb/meta/` as accidental inclusions that must be removed before merge.
✅ Actions performed

Review triggered.

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

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@SWLOR.Game.Server/Service/DBService/DBQuery.cs`:
- Around line 59-60: The current early return "if (list.Count == 0) return this"
in DBQuery<T> turns an empty filter into "no filter" which widens results;
change both overloads (the blocks at the shown diff and at lines ~94-95) to
instead mark the query as match-none. Implement this by setting the DBQuery<T>
internal state to a match-none sentinel (e.g., a private _matchNone flag or
equivalent in DBQuery<T>) or by appending a false condition so subsequent
processing returns zero rows; update methods that build/execute the query to
check that match-none flag and short-circuit to an empty result. Ensure the
change is applied to both overloads that currently return this on empty lists.

In `@SWLOR.Game.Server/Service/Property.cs`:
- Around line 436-467: When handling the dbStarport == null repair path inside
the CleanUpData branch that relocates DockPosition (the code that checks
property.Positions and/or builds cz220Dock), also clear the stale
RegisteredStarport relation so the ship is no longer marked as docked to a
deleted starport: remove or clear
property.ChildPropertyIds[PropertyChildType.RegisteredStarport] (or call the
existing helper that unregisters a child) in both the LastNPCDockPosition and
cz220 fallback branches after setting DockPosition/LastNPCDockPosition, and log
the change similarly; do not leave the RegisteredStarport entry populated when
the starport record is missing.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 660e169a-bf30-476f-bf81-044f4f38bae5

📥 Commits

Reviewing files that changed from the base of the PR and between 68d7b9b and 9d6568b.

📒 Files selected for processing (3)
  • SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs
  • SWLOR.Game.Server/Service/DBService/DBQuery.cs
  • SWLOR.Game.Server/Service/Property.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs

Comment on lines +59 to +60
if (list.Count == 0)
return this;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fail closed on empty enumerable filters.

Returning this here turns “match none” into “drop the filter entirely”. That broadens callers unexpectedly — for example, Property.GetCategoryPermissions would return all of a player's permissions when categoryIds is empty instead of none.

💡 Safer direction
-            if (list.Count == 0)
-                return this;
+            if (list.Count == 0)
+                throw new ArgumentException("Search list cannot be empty.", nameof(search));

Apply the same change to both overloads, or introduce an explicit internal match-none state in DBQuery<T> if empty lists should mean zero results.

Also applies to: 94-95

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@SWLOR.Game.Server/Service/DBService/DBQuery.cs` around lines 59 - 60, The
current early return "if (list.Count == 0) return this" in DBQuery<T> turns an
empty filter into "no filter" which widens results; change both overloads (the
blocks at the shown diff and at lines ~94-95) to instead mark the query as
match-none. Implement this by setting the DBQuery<T> internal state to a
match-none sentinel (e.g., a private _matchNone flag or equivalent in
DBQuery<T>) or by appending a false condition so subsequent processing returns
zero rows; update methods that build/execute the query to check that match-none
flag and short-circuit to an empty result. Ensure the change is applied to both
overloads that currently return this on empty lists.

Comment thread SWLOR.Game.Server/Service/Property.cs
@zunath
Copy link
Copy Markdown
Owner Author

zunath commented Apr 28, 2026

@coderabbitai - Please update with the latest changes made

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Property.cs (relevant sections) ==="
cat SWLOR.Game.Server/Service/Property.cs

echo "=== DBQuery.cs ==="
cat SWLOR.Game.Server/Service/DBService/DBQuery.cs

echo "=== StarportLayoutDefinition.cs ==="
cat SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs

echo "=== PropertyLocation.cs ==="
cat SWLOR.Game.Server/Service/PropertyService/PropertyLocation.cs

echo "=== StructureChangedAction.cs ==="
cat SWLOR.Game.Server/Service/PropertyService/StructureChangedAction.cs

echo "=== BankLayoutDefinition.cs ==="
cat SWLOR.Game.Server/Feature/PropertyLayoutDefinition/BankLayoutDefinition.cs

Length of output: 159887


@zunath Here's a full status update on all 5 previously flagged issues — all of them have been resolved in the latest changes:


✅ Issue #1975TryGetValue for DockPosition in CleanUpData (Property.cs)

Fixed. The direct dictionary indexer has been replaced with TryGetValue. If DockPosition is missing, the code now logs a warning, saves the property, and continues rather than throwing KeyNotFoundException and aborting the entire loop.


✅ Issue #1976 — Null guards in CanAccess (StarportLayoutDefinition.cs)

Fixed. Both dbProperty and dbBuilding now have null checks after DB.Get. If either returns null, the player receives "This terminal is unavailable." and the method returns false, mirroring the OnSpawn guard pattern.


✅ Issue #1977 — Empty IEnumerable guard in AddFieldSearch (DBQuery.cs)

Fixed. Both the IEnumerable<int> and IEnumerable<string> overloads now short-circuit on an empty list by setting MatchNone = true and returning this, preventing the invalid @field:() RediSearch expression from ever being constructed. The MatchNone flag approach is more thorough than a plain early return, as it signals to callers that the query cannot match any document.


✅ Issue #1978 — Inverted tie-breaking condition in ProcessCityElections (Property.cs)

Fixed. The condition has been corrected from != to ==:

else if (orderedVotes.Count >= 2 && orderedVotes.ElementAt(0).Value == orderedVotes.ElementAt(1).Value)

The incumbent mayor now correctly wins only when there is a genuine tie.


✅ Issue #1979RefreshPermissions removes from wrong dictionary (Property.cs)

Fixed. The GrantPermissions pruning loop now correctly calls dbPermission.GrantPermissions.Remove(grantPermission) instead of incorrectly removing from dbPermission.Permissions.


All 5 issues are resolved. The PR looks good to merge from a correctness standpoint. 🎉


🧠 Learnings used
Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1957
File: Module/ifo/module.ifo.json:2899-2899
Timestamp: 2026-03-29T13:23:17.045Z
Learning: In the SWLOR_NWN repository, `Module/ifo/module.ifo.json` has been historically corrupted by PRs that introduced truncated or missing lines (e.g., incomplete entries in `Mod_Area_list`). On every PR touching this file, always: (1) verify the file is valid, well-formed JSON; (2) check every entry in `Mod_Area_list` has a complete `__struct_id` and `Area_Name` block with both `type` and `value` fields; (3) flag any truncated, incomplete, or empty entries; (4) flag any removals from `Mod_Area_list` unless explicitly confirmed as intentional by the PR author.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-08T02:53:03.088Z
Learning: In the SWLOR_NWN repository (zunath/SWLOR_NWN), maintainer zunath wants CodeRabbit to always check for merge conflicts when reviewing PRs and explicitly flag them to contributors.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1906
File: Module/are/viscaradeepmount.are.json:44194-44196
Timestamp: 2025-08-03T15:13:29.324Z
Learning: In the SWLOR_NWN repository, the "Version" field in area files (e.g., Module/are/*.are.json) is incremented automatically by the external toolset with each change and does not affect functionality. It should not be flagged as an issue during code review.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-07T21:18:45.269Z
Learning: In SWLOR_NWN, the directory `SWLOR.Game.Server/Docker/influxdb/` is used for a local Docker InfluxDB instance. The runtime data subdirectories (`data/`, `wal/`, `meta/`) are NOT gitignored and can be accidentally committed. On every PR, always flag any files under `SWLOR.Game.Server/Docker/influxdb/data/`, `SWLOR.Game.Server/Docker/influxdb/wal/`, or `SWLOR.Game.Server/Docker/influxdb/meta/` as accidental inclusions that must be removed before merge.

If you found this review helpful, would you consider giving us a shout-out on X?

Thank you for using CodeRabbit!

@zunath
Copy link
Copy Markdown
Owner Author

zunath commented Apr 28, 2026

@coderabbitai - Anything left to fix?

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 28, 2026

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
echo "=== Property.cs ==="
cat SWLOR.Game.Server/Service/Property.cs

echo "=== DBQuery.cs ==="
cat SWLOR.Game.Server/Service/DBService/DBQuery.cs

echo "=== StarportLayoutDefinition.cs ==="
cat SWLOR.Game.Server/Feature/PropertyLayoutDefinition/StarportLayoutDefinition.cs

echo "=== BankLayoutDefinition.cs ==="
cat SWLOR.Game.Server/Feature/PropertyLayoutDefinition/BankLayoutDefinition.cs

echo "=== PropertyLocation.cs ==="
cat SWLOR.Game.Server/Service/PropertyService/PropertyLocation.cs

echo "=== StructureChangedAction.cs ==="
cat SWLOR.Game.Server/Service/PropertyService/StructureChangedAction.cs

echo "=== Check for merge conflict markers ==="
rg -n "<<<<<<|=======|>>>>>>>" --type cs

Length of output: 159908


@zunath All 5 previously flagged issues are confirmed fixed in the current code ✅ and there are no merge conflict markers.

However, I spotted one remaining gap in StructureChangedAction.cs that wasn't part of the original 5 issues — the RetrieveStarport() action (triggered when a player manually picks up their starport structure):

// StructureChangedAction.cs — RetrieveStarport()
var dbCity = DB.Get<WorldProperty>(property.ParentPropertyId);
var cityArea = Area.GetAreaByResref(dbCity.ParentPropertyId);  // ← NRE if dbCity is null
var instance = Property.GetRegisteredInstance(interiorId);    // ← KeyNotFoundException if not in cache

Two concerns here:

  1. dbCity is unguarded — if the parent city is already gone (e.g. deleted just before the player retrieves the structure), dbCity.ParentPropertyId throws a NullReferenceException. The equivalent OnSpawn path in StarportLayoutDefinition.cs was given null guards in this PR; the same pattern should apply here.

  2. GetRegisteredInstance(interiorId) uses a direct dictionary indexer — if the starport interior's OnSpawn was previously skipped (e.g. due to an orphaned record that the new null-guard causes to early-return), the interior was never registered and this throws a KeyNotFoundException.

Suggested fix:

var dbCity = DB.Get<WorldProperty>(property.ParentPropertyId);
if (dbCity == null)
{
    Log.Write(LogGroup.Error, $"RetrieveStarport: parent city '{property.ParentPropertyId}' not found for property '{property.CustomName}' ({property.Id}). Skipping dock point removal.", true);
    return;
}

if (!_propertyInstances.ContainsKey(interiorId))
{
    Log.Write(LogGroup.Error, $"RetrieveStarport: interior '{interiorId}' was not registered as an instance. Skipping dock point removal.", true);
    return;
}

var cityArea = Area.GetAreaByResref(dbCity.ParentPropertyId);
var instance = Property.GetRegisteredInstance(interiorId);
var dockPoint = GetLandingWaypoint(instance.Area);
Space.RemoveLandingPoint(dockPoint, cityArea);

Note: _propertyInstances is private to Property.cs, so you'd either need to expose a TryGetRegisteredInstance helper or use an equivalent check via Property.GetPlaceableByPropertyId. Would you like me to open a new issue for this?


🧠 Learnings used
Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 1957
File: Module/ifo/module.ifo.json:2899-2899
Timestamp: 2026-03-29T13:23:17.045Z
Learning: In the SWLOR_NWN repository, `Module/ifo/module.ifo.json` has been historically corrupted by PRs that introduced truncated or missing lines (e.g., incomplete entries in `Mod_Area_list`). On every PR touching this file, always: (1) verify the file is valid, well-formed JSON; (2) check every entry in `Mod_Area_list` has a complete `__struct_id` and `Area_Name` block with both `type` and `value` fields; (3) flag any truncated, incomplete, or empty entries; (4) flag any removals from `Mod_Area_list` unless explicitly confirmed as intentional by the PR author.

Learnt from: zunath
Repo: zunath/SWLOR_NWN PR: 0
File: :0-0
Timestamp: 2026-04-08T02:53:03.088Z
Learning: In the SWLOR_NWN repository (zunath/SWLOR_NWN), maintainer zunath wants CodeRabbit to always check for merge conflicts when reviewing PRs and explicitly flag them to contributors.

@zunath zunath mentioned this pull request May 5, 2026
@zunath
Copy link
Copy Markdown
Owner Author

zunath commented May 5, 2026

@codex review this PR

@chatgpt-codex-connector
Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 👍

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@zunath zunath closed this May 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant