Skip to content

feat: normalize house ID handling and clientId mapping#3877

Open
dudantas wants to merge 3 commits intomainfrom
dudantas/feat-id-houses-to-otc
Open

feat: normalize house ID handling and clientId mapping#3877
dudantas wants to merge 3 commits intomainfrom
dudantas/feat-id-houses-to-otc

Conversation

@dudantas
Copy link
Member

@dudantas dudantas commented Feb 28, 2026

This pull request refactors how houses are identified and referenced throughout the codebase, standardizing on using the internal house ID instead of the client ID for most operations. It also adds robust handling for duplicate or invalid client IDs when loading houses from XML, and introduces helper functions to resolve and translate house IDs for network communication, improving consistency and reliability in house-related features.

House ID Handling and Refactoring:

  • Replaced usage of getHouseByClientId with getHouse (which uses the internal house ID) in all relevant methods in player.cpp and game.cpp, ensuring consistent referencing of houses by their internal ID. [1] [2] [3] [4] [5] [6] [7] [8] [9] [10]

  • Updated house maps and emplacements in game.cpp to use the internal house ID as the key instead of the client ID, aligning all logic to the new standard. [1] [2]

Robustness in House XML Loading:

  • Enhanced the XML loader in house.cpp to detect and handle duplicate or zero client IDs by assigning a unique fallback client ID, logging a warning when this occurs, and ensuring houseMapClientId is cleared before loading. [1] [2] [3]

Network Protocol Improvements:

  • Introduced helper functions getCyclopediaHouseNetworkId and resolveCyclopediaHouseInternalId in protocolgame.cpp to abstract ID translation between internal and network representations, accommodating both standard and OTCR clients.

  • Refactored all protocol parsing and message sending functions to use these helpers, ensuring the correct house ID is used for both incoming and outgoing network messages (including house auctions, house lists, and house info). [1] [2] [3] [4] [5] [6] [7]

Works with map editor PR: opentibiabr/remeres-map-editor#160

Summary by CodeRabbit

  • Refactor
    • Enhanced house data validation to improve reliability of house management
    • Optimized house identification and lookup mechanisms for more consistent data handling

Replace direct getHouseByClientId/getClientId usage with internal getHouse/getId and add translation helpers to map between network (client) IDs and internal IDs. ProtocolGame now resolves incoming network house IDs (handling OTCR vs legacy clientId) and emits the appropriate network ID when sending house lists and messages. Houses::loadHousesXML was hardened: it validates clientid, warns and falls back on duplicates or zero values, ensures unique clientIds, and clears/repopulates the clientId map. Overall this stabilizes house ID handling across server, network protocol, and data loading.
Ignore data-otservbr-global/world/staticdatahouse.dat to prevent accidental commits of the generated/static data file. Appends the path to the existing .gitignore.
@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

📝 Walkthrough

Walkthrough

This PR migrates the house lookup system from client-based IDs to internal house IDs across multiple components. It introduces a new getHouse(uint32_t) API replacing getHouseByClientId(uint32_t), adds validation logic with fallback ID generation during XML loading, and implements network ID mapping helpers in the protocol layer.

Changes

Cohort / File(s) Summary
House ID API Migration
src/creatures/players/player.cpp, src/game/game.cpp
Replaced calls to getHouseByClientId(houseId) with getHouse(houseId) and updated cyclopedia house list population to use getId() instead of getClientId() for internal ID-based lookups.
House Data Loading & Validation
src/map/house/house.cpp
Added unordered_set-based duplicate detection for house client IDs during XML loading, with fallback ID generation by incrementing from houseId when encountering 0 or duplicate clientid values; includes corresponding warning logs.
Network Protocol Mapping
src/server/network/protocol/protocolgame.cpp
Introduced helper functions getCyclopediaHouseNetworkId() and resolveCyclopediaHouseInternalId() to map between network and internal house IDs for OTCR vs non-OTCR contexts; refactored cyclopedia house list and auction message serialization to use resolved network IDs.
Configuration
.gitignore
Added ignore entry for data-otservbr-global/world/staticdatahouse.dat.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 House IDs shifting, clients fade away,
Internal maps guide the homes to stay,
Network translators keep protocol tight,
Duplicates caught and fallbacks done right!
A tidy refactor, both clean and precise! 🏠✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: normalizing house ID handling and refactoring the clientId mapping system across the codebase.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dudantas/feat-id-houses-to-otc

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.

@sonarqubecloud
Copy link

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a24bb8e7d9

ℹ️ 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".

}

const auto house = g_game().map.houses.getHouseByClientId(networkHouseId);
return house ? house->getId() : networkHouseId;

Choose a reason for hiding this comment

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

P1 Badge Reject unknown house IDs in Cyclopedia request parsing

For non-OTCR clients, resolveCyclopediaHouseInternalId currently falls back to returning the raw network ID when getHouseByClientId misses, and downstream handlers now interpret that value as an internal house ID (Game::playerCyclopediaHouse* uses getHouse). This means an unknown/invalid client house ID can be remapped to a different house whenever it matches an internal ID, causing actions (bid/transfer/move out) to target the wrong house instead of being rejected as before.

Useful? React with 👍 / 👎.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/game/game.cpp (1)

11479-11654: Consider extracting repeated house-lookup/validation boilerplate into a helper.

MoveOut, CancelMoveOut, Transfer, CancelTransfer, AcceptTransfer, and RejectTransfer repeat nearly identical retrieval and state checks. A shared helper would reduce drift risk in future rule changes.

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

In `@src/game/game.cpp` around lines 11479 - 11654, Several functions
(playerCyclopediaHouseMoveOut, playerCyclopediaHouseCancelMoveOut,
playerCyclopediaHouseTransfer, playerCyclopediaHouseCancelTransfer,
playerCyclopediaHouseAcceptTransfer, playerCyclopediaHouseRejectTransfer)
duplicate house/player lookup and state/ownership checks; extract a helper in
Game (e.g., validateCyclopediaHouseAction) that accepts playerId, houseId,
expected CyclopediaHouseState (or allow any), and HouseAuctionType and returns a
struct/result containing shared_ptr<Player>, house pointer, and a
Transfer/Accept error code; have the helper perform
getPlayerByID/getPlayerByName as needed, log missing player, check house
existence/state/ownership, send the appropriate sendHouseAuctionMessage on
failure, and then replace the repeated blocks in each function to call this
helper and early-return on error to preserve current behavior and logging.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/map/house/house.cpp`:
- Around line 902-905: Don't mutate the shared houseMapClientId before XML
parsing succeeds: instead of clearing houseMapClientId up-front, create a local
temporary unordered_set (e.g., tempSeenClientIds) and reserve using
houseMap.size(), populate tempSeenClientIds during parse/validation, and only
swap or assign tempSeenClientIds into houseMapClientId after the XML load and
validation complete successfully; ensure any early returns leave the original
houseMapClientId untouched.
- Around line 937-952: The current fallback logic can steal a later house's
valid declared clientid because only previously-seen IDs are excluded; change to
a two-pass approach in Houses::loadHousesXML: first iterate houseNode entries
and collect all declared non-zero clientids into a reservedDeclaredIds set (and
note duplicates), then in the second pass assign final clientIds per house by
using the declared id if it is non-zero and not already assigned, otherwise
compute fallbackClientId by incrementing until it is not in reservedDeclaredIds
nor in assignedClientIds; preserve the existing warning messages (use
house->getName(), houseId, original clientId) when falling back and insert the
chosen id into assignedClientIds (seenClientIds) afterwards.

In `@src/server/network/protocol/protocolgame.cpp`:
- Around line 82-89: resolveCyclopediaHouseInternalId currently falls back to
returning the networkHouseId when getHouseByClientId fails, which mixes network
vs internal ID domains; change resolveCyclopediaHouseInternalId so that if
getHouseByClientId(...) returns null it returns an invalid sentinel (e.g., 0)
instead of networkHouseId, and update any callers that use
resolveCyclopediaHouseInternalId to treat 0 as an error and reject the request
rather than attempting to use the returned value; refer to
resolveCyclopediaHouseInternalId, g_game().map.houses.getHouseByClientId and
house->getId to locate the code paths to adjust.

---

Nitpick comments:
In `@src/game/game.cpp`:
- Around line 11479-11654: Several functions (playerCyclopediaHouseMoveOut,
playerCyclopediaHouseCancelMoveOut, playerCyclopediaHouseTransfer,
playerCyclopediaHouseCancelTransfer, playerCyclopediaHouseAcceptTransfer,
playerCyclopediaHouseRejectTransfer) duplicate house/player lookup and
state/ownership checks; extract a helper in Game (e.g.,
validateCyclopediaHouseAction) that accepts playerId, houseId, expected
CyclopediaHouseState (or allow any), and HouseAuctionType and returns a
struct/result containing shared_ptr<Player>, house pointer, and a
Transfer/Accept error code; have the helper perform
getPlayerByID/getPlayerByName as needed, log missing player, check house
existence/state/ownership, send the appropriate sendHouseAuctionMessage on
failure, and then replace the repeated blocks in each function to call this
helper and early-return on error to preserve current behavior and logging.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 19a2d31 and bd70c2e.

📒 Files selected for processing (5)
  • .gitignore
  • src/creatures/players/player.cpp
  • src/game/game.cpp
  • src/map/house/house.cpp
  • src/server/network/protocol/protocolgame.cpp

Comment on lines +902 to +905
houseMapClientId.clear();
std::unordered_set<uint32_t> seenClientIds;
seenClientIds.reserve(houseMap.size());

Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid mutating houseMapClientId before XML load succeeds.

Clearing shared state up-front means any later parse/validation failure exits with a partially rebuilt (or empty) clientId map.

Suggested hardening (build temp map, swap on success)
- houseMapClientId.clear();
- std::unordered_set<uint32_t> seenClientIds;
- seenClientIds.reserve(houseMap.size());
+ std::unordered_map<uint32_t, std::shared_ptr<House>> rebuiltClientIdMap;
+ rebuiltClientIdMap.reserve(houseMap.size());
+ std::unordered_set<uint32_t> seenClientIds;
+ seenClientIds.reserve(houseMap.size());

  for (const auto &houseNode : doc.child("houses").children()) {
    ...
-   addHouseClientId(clientId, house);
+   rebuiltClientIdMap.emplace(clientId, house);
  }
+ houseMapClientId.swap(rebuiltClientIdMap);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/map/house/house.cpp` around lines 902 - 905, Don't mutate the shared
houseMapClientId before XML parsing succeeds: instead of clearing
houseMapClientId up-front, create a local temporary unordered_set (e.g.,
tempSeenClientIds) and reserve using houseMap.size(), populate tempSeenClientIds
during parse/validation, and only swap or assign tempSeenClientIds into
houseMapClientId after the XML load and validation complete successfully; ensure
any early returns leave the original houseMapClientId untouched.

Comment on lines +937 to +952
uint32_t clientId = pugi::cast<uint32_t>(houseNode.attribute("clientid").value());
if (clientId == 0 || seenClientIds.contains(clientId)) {
uint32_t fallbackClientId = static_cast<uint32_t>(houseId);
while (fallbackClientId == 0 || seenClientIds.contains(fallbackClientId)) {
++fallbackClientId;
}

if (clientId == 0) {
g_logger().warn("[Houses::loadHousesXML] - House '{}' (id: {}) has invalid clientid=0. Falling back to clientId={}.", house->getName(), houseId, fallbackClientId);
} else {
g_logger().warn("[Houses::loadHousesXML] - Duplicate clientid={} detected for house '{}' (id: {}). Falling back to clientId={}.", clientId, house->getName(), houseId, fallbackClientId);
}

clientId = fallbackClientId;
}
seenClientIds.insert(clientId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Fallback clientId resolution is order-dependent and can remap otherwise valid IDs.

The fallback loop only excludes already-seen IDs. A fallback chosen early can collide with a later house’s valid declared clientid, forcing unnecessary remapping.

Suggested hardening (two-pass reservation of declared IDs)
+ std::unordered_map<uint32_t, uint32_t> declaredClientIdCount;
+ for (const auto &houseNode : doc.child("houses").children()) {
+   const uint32_t parsedId = pugi::cast<uint32_t>(houseNode.attribute("clientid").value());
+   if (parsedId != 0) {
+     ++declaredClientIdCount[parsedId];
+   }
+ }

  uint32_t clientId = pugi::cast<uint32_t>(houseNode.attribute("clientid").value());
- if (clientId == 0 || seenClientIds.contains(clientId)) {
+ const bool duplicatedInSource = clientId != 0 && declaredClientIdCount[clientId] > 1;
+ if (clientId == 0 || duplicatedInSource || seenClientIds.contains(clientId)) {
    uint32_t fallbackClientId = static_cast<uint32_t>(houseId);
-   while (fallbackClientId == 0 || seenClientIds.contains(fallbackClientId)) {
+   while (
+     fallbackClientId == 0 ||
+     seenClientIds.contains(fallbackClientId) ||
+     (declaredClientIdCount.contains(fallbackClientId) && declaredClientIdCount[fallbackClientId] == 1)
+   ) {
      ++fallbackClientId;
    }
    clientId = fallbackClientId;
  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/map/house/house.cpp` around lines 937 - 952, The current fallback logic
can steal a later house's valid declared clientid because only previously-seen
IDs are excluded; change to a two-pass approach in Houses::loadHousesXML: first
iterate houseNode entries and collect all declared non-zero clientids into a
reservedDeclaredIds set (and note duplicates), then in the second pass assign
final clientIds per house by using the declared id if it is non-zero and not
already assigned, otherwise compute fallbackClientId by incrementing until it is
not in reservedDeclaredIds nor in assignedClientIds; preserve the existing
warning messages (use house->getName(), houseId, original clientId) when falling
back and insert the chosen id into assignedClientIds (seenClientIds) afterwards.

Comment on lines +82 to +89
uint32_t resolveCyclopediaHouseInternalId(uint32_t networkHouseId, bool isOTCR) {
if (isOTCR) {
return networkHouseId;
}

const auto house = g_game().map.houses.getHouseByClientId(networkHouseId);
return house ? house->getId() : networkHouseId;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Avoid fallback that mixes network and internal house ID domains.

When clientId lookup fails (non-OTCR), returning networkHouseId directly can target the wrong internal house if IDs overlap. Prefer returning an invalid sentinel (e.g., 0) and rejecting the request at call sites.

🔧 Proposed fix
 uint32_t resolveCyclopediaHouseInternalId(uint32_t networkHouseId, bool isOTCR) {
 	if (isOTCR) {
 		return networkHouseId;
 	}
 
 	const auto house = g_game().map.houses.getHouseByClientId(networkHouseId);
-	return house ? house->getId() : networkHouseId;
+	return house ? house->getId() : 0;
 }
 case 1: {
 	const uint32_t houseId = resolveCyclopediaHouseInternalId(msg.get<uint32_t>(), isOTCR);
+	if (houseId == 0) {
+		break;
+	}
 	const uint64_t bidValue = msg.get<uint64_t>();
 	g_game().playerCyclopediaHouseBid(player->getID(), houseId, bidValue);
 	break;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/server/network/protocol/protocolgame.cpp` around lines 82 - 89,
resolveCyclopediaHouseInternalId currently falls back to returning the
networkHouseId when getHouseByClientId fails, which mixes network vs internal ID
domains; change resolveCyclopediaHouseInternalId so that if
getHouseByClientId(...) returns null it returns an invalid sentinel (e.g., 0)
instead of networkHouseId, and update any callers that use
resolveCyclopediaHouseInternalId to treat 0 as an error and reject the request
rather than attempting to use the returned value; refer to
resolveCyclopediaHouseInternalId, g_game().map.houses.getHouseByClientId and
house->getId to locate the code paths to adjust.

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