Skip to content

Fb global id generation#18

Closed
artemijan wants to merge 3 commits intomasterfrom
fb-global-id
Closed

Fb global id generation#18
artemijan wants to merge 3 commits intomasterfrom
fb-global-id

Conversation

@artemijan
Copy link
Copy Markdown
Owner

@artemijan artemijan commented Nov 25, 2025

Summary by CodeRabbit

  • Refactor
    • Unified object identifiers: player and item entities now use stable object IDs across network packets and game systems, improving consistency of character/item references.
    • Added an ID factory to manage allocation and reuse of object IDs.
  • Tests
    • Updated test data and expectations to align with the new object-id behavior.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Nov 25, 2025

Caution

Review failed

The pull request is closed.

Walkthrough

Introduces a centralized ObjectId/IdFactory system and migrates players, items, party logic, paper doll, and many packet serializers/tests to use object IDs via get_object_id() instead of legacy character model IDs.

Changes

Cohort / File(s) Change Summary
Core ID Management
l2-core/src/id_factory.rs, l2-core/src/lib.rs
New IdFactory singleton and ObjectId wrapper; exported id_factory module. Implements allocation, recycling, conversions, comparisons, and Drop-based release.
Player model
l2-core/src/game_objects/player/_player.rs
Adds pub object_id: ObjectId; replaces get_id() with get_object_id(); initializes object_id from IdFactory in Player::new.
Item model
l2-core/src/game_objects/item/_item.rs
Adds object_id: ObjectId to ItemObject; assigns factory-generated object IDs and uses them as map keys.
Player utilities
l2-core/src/game_objects/player/party.rs, l2-core/src/game_objects/player/quest.rs
party::index_of() parameter renamed to player_object_id and comparisons use get_object_id(); quest::has_state() parameter renamed and simplified.
PaperDoll API
l2-core/src/game_objects/player/paper_doll.rs
restore_visible_inventory() signature changed to accept HashMap<i32, ItemObject>; accesses nested item_model fields and object_id.
Formatting-only
l2-core/src/data/char_template.rs
Local variable bindings consolidated to single-line forms; no behavioral change.
Outbound packets (many)
game/src/packets/to_client/char_info.rs, .../char_move_to_location.rs, .../char_selected.rs, .../move_to.rs, .../user_info.rs, .../relation_changed.rs, .../skill_list.rs, .../quest_list.rs
Replace uses of char_model.id / get_id() with get_object_id() and update tests to assign ObjectIds and adjust expected serialized bytes.
Extended packet handlers
game/src/packets/to_client/extended/equipped_items.rs, .../inventory_weight.rs, .../premium_state.rs, .../rotation.rs
Same migration to get_object_id(); tests updated to set player.object_id and assert new serialized payloads.
From-client packet
game/src/packets/from_client/restart.rs
Uses player.get_object_id() when constructing object-deletion requests.
Login tests / imports
login/src/enums.rs, login/src/packet/handleable/player_logout.rs
Added tracing imports; replaced a test slice creation &[acc.clone()] with std::slice::from_ref(&acc) to avoid cloning.
Workspace deps
Cargo.toml, l2-core/Cargo.toml
Added serial_test = "3.2.0" to workspace deps and enabled it for dev-dependencies in l2-core.

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant IdFactory as IdFactory (singleton)
    participant Player
    participant Item as ItemObject
    participant Packet as PacketHandler

    Note right of IdFactory: Allocation flow
    Packet->>Player: request get_object_id()
    Player->>IdFactory: IdFactory::instance().get_next_id() (on construct)
    IdFactory-->>Player: ObjectId(Arc<i32>)
    Player->>Packet: get_object_id() -> i32

    Packet->>Packet: serialize object id into buffer
    Note over Packet,Player: Tests now set Player.object_id explicitly

    Item->>IdFactory: get_next_id()
    IdFactory-->>Item: ObjectId(Arc<i32>)
    Item->>Item: store object_id and expose via item_model
    Note over IdFactory: On last ObjectId Arc drop -> release_id()
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Files to inspect closely:
    • l2-core/src/id_factory.rs — concurrency, atomic/collection correctness, Drop behavior.
    • l2-core/src/game_objects/player/_player.rs — constructor and getter changes.
    • l2-core/src/game_objects/player/paper_doll.rs — signature change and item access paths.
    • Packet modules with updated tests — verify serialized bytes match ObjectId values.

Possibly related PRs

Poem

🐰 I nibble nets of IDs anew,
Arc-wrapped numbers, tidy and true.
Packets carry tidy names,
Items hop with recycled gains —
A rabbit's cheer for IDs that renew!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.92% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ 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 'Fb global id generation' accurately captures the main change: introducing global ID generation via a new IdFactory module for managing object identifiers across the codebase.

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between da47424 and 5ed40ee.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (4)
  • Cargo.toml (1 hunks)
  • l2-core/Cargo.toml (1 hunks)
  • l2-core/src/game_objects/player/quest.rs (1 hunks)
  • l2-core/src/id_factory.rs (1 hunks)

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

@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

Caution

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

⚠️ Outside diff range comments (1)
l2-core/src/game_objects/player/quest.rs (1)

16-27: Quest::has_state ignores the requested state and always returns true when any state is present

The current implementation shadows the state parameter and compares the value to itself:

  • Inner state binding hides the parameter.
  • state.eq(state) is always true when a value exists.
  • As a result, has_state("started") and has_state("completed") both return true whenever a State value is set.

Because is_started/is_completed rely on has_state, this breaks quest state checks and directly affects logic in QuestList::new (completed quests may be serialized as started and never hit the is_completed branch).

Suggest correcting the implementation to actually compare against the requested state:

-    pub fn has_state(&self, state: &str) -> bool {
-        if let Some(state) = self
-            .model
-            .variables
-            .get(QuestVariables::State.as_key())
-            .and_then(JsonValue::as_str)
-            && state.eq(state)
-        {
-            true
-        } else {
-            false
-        }
-    }
+    pub fn has_state(&self, expected_state: &str) -> bool {
+        self.model
+            .variables
+            .get(QuestVariables::State.as_key())
+            .and_then(JsonValue::as_str)
+            .map_or(false, |state| state.eq(expected_state))
+    }
🧹 Nitpick comments (5)
login/src/enums.rs (1)

3-23: Use tracing instead of println! for state transition errors

You now import tracing::{error, info} but still use println! in the invalid‑transition branch. For consistent, structured logging (and to justify the new import), consider switching that message to error!:

 impl GS {
@@
-            (Self::BfConnected, Self::Authed) => *self = Self::Authed,
-            _ => {
-                println!("Can't transition from connection state {self} to {desired_state}");
-                return Err(GSLoginFailReasons::NotAuthed);
-            }
+            (Self::BfConnected, Self::Authed) => *self = Self::Authed,
+            _ => {
+                error!("Can't transition from connection state {self} to {desired_state}");
+                return Err(GSLoginFailReasons::NotAuthed);
+            }
         }

Optionally, you could also add an info! on successful transitions if that’s useful for debugging state flows.

l2-core/src/game_objects/item/_item.rs (1)

9-9: Unused import: Arc.

The Arc import on line 9 doesn't appear to be used in this code segment. Consider removing it unless it's used elsewhere in the file.

-use std::sync::Arc;
l2-core/src/id_factory.rs (3)

75-88: Approve the deadlock mitigation, but consider further refinement.

The implementation correctly avoids holding a lock during modification by copying the ID value and dropping the iterator before calling remove(). This addresses the deadlock concern mentioned in the comment.

However, for even clearer code, consider using pop() if DashSet provides it (eliminating the two-phase approach):

pub fn get_next_id(&self) -> ObjectId {
    // Try to get a reusable ID atomically
    if let Some(id_ref) = self.reusable_ids.iter().next() {
        let id_val = *id_ref;
        drop(id_ref); // explicit drop for clarity
        if self.reusable_ids.remove(&id_val) {
            return ObjectId::new(id_val);
        }
        // If remove failed, another thread took it, fall through to allocate new
    }
    ObjectId::new(self.next_id.fetch_add(1, Ordering::SeqCst))
}

This handles the race where another thread might remove the same ID between iteration and removal.


10-10: Add overflow protection for the ID counter.

The next_id counter will eventually overflow after ~2 billion allocations (since it starts at 0x1000_0000 and i32::MAX is ~2.1 billion). When this happens, fetch_add will wrap to negative values, causing undefined behavior in the ID system.

Consider adding a check in get_next_id():

pub fn get_next_id(&self) -> ObjectId {
    // ... reusable IDs logic ...
    
    let new_id = self.next_id.fetch_add(1, Ordering::SeqCst);
    if new_id < 0 {
        panic!("ObjectId counter overflow - no more IDs available!");
    }
    ObjectId::new(new_id)
}

Or use u32/u64 instead of i32 if the protocol permits, and document the maximum number of allocations before restart is needed.


102-122: Tests use shared singleton state.

The tests use IdFactory::instance(), which returns the global singleton. This means tests are not isolated and could interfere with each other if run in parallel. While the current tests may work, this is fragile.

Consider refactoring IdFactory to support direct instantiation for testing:

impl IdFactory {
    #[cfg(test)]
    pub fn new_for_test() -> Self {
        Self {
            next_id: AtomicI32::new(FIRST_OID),
            reusable_ids: DashSet::new(),
        }
    }
}

Then update tests to use isolated instances:

#[test]
fn test_allocation() {
    let factory = IdFactory::new_for_test();
    // ... test with isolated instance
}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2976e66 and da47424.

📒 Files selected for processing (23)
  • game/src/packets/from_client/restart.rs (1 hunks)
  • game/src/packets/to_client/char_info.rs (1 hunks)
  • game/src/packets/to_client/char_move_to_location.rs (1 hunks)
  • game/src/packets/to_client/char_selected.rs (3 hunks)
  • game/src/packets/to_client/extended/equipped_items.rs (3 hunks)
  • game/src/packets/to_client/extended/inventory_weight.rs (3 hunks)
  • game/src/packets/to_client/extended/premium_state.rs (3 hunks)
  • game/src/packets/to_client/extended/rotation.rs (3 hunks)
  • game/src/packets/to_client/move_to.rs (1 hunks)
  • game/src/packets/to_client/quest_list.rs (2 hunks)
  • game/src/packets/to_client/relation_changed.rs (1 hunks)
  • game/src/packets/to_client/skill_list.rs (0 hunks)
  • game/src/packets/to_client/user_info.rs (5 hunks)
  • l2-core/src/data/char_template.rs (1 hunks)
  • l2-core/src/game_objects/item/_item.rs (3 hunks)
  • l2-core/src/game_objects/player/_player.rs (8 hunks)
  • l2-core/src/game_objects/player/paper_doll.rs (1 hunks)
  • l2-core/src/game_objects/player/party.rs (1 hunks)
  • l2-core/src/game_objects/player/quest.rs (1 hunks)
  • l2-core/src/id_factory.rs (1 hunks)
  • l2-core/src/lib.rs (1 hunks)
  • login/src/enums.rs (1 hunks)
  • login/src/packet/handleable/player_logout.rs (1 hunks)
💤 Files with no reviewable changes (1)
  • game/src/packets/to_client/skill_list.rs
🧰 Additional context used
🧬 Code graph analysis (12)
game/src/packets/to_client/quest_list.rs (1)
l2-core/src/id_factory.rs (1)
  • new (18-20)
game/src/packets/to_client/relation_changed.rs (1)
entities/src/entities/character.rs (1)
  • Relation (76-95)
game/src/packets/from_client/restart.rs (1)
game/src/packets/to_client/delete_object.rs (1)
  • new (13-21)
game/src/packets/to_client/extended/equipped_items.rs (3)
game/src/packets/to_client/extended/inventory_weight.rs (1)
  • new (14-24)
game/src/packets/to_client/extended/premium_state.rs (1)
  • new (14-21)
game/src/packets/to_client/extended/rotation.rs (1)
  • new (15-24)
game/src/packets/to_client/extended/premium_state.rs (2)
l2-core/src/game_objects/player/_player.rs (1)
  • new (77-116)
l2-core/src/id_factory.rs (1)
  • new (18-20)
login/src/packet/handleable/player_logout.rs (1)
login/src/controller/player_management.rs (1)
  • on_players_in_game (94-112)
game/src/packets/to_client/user_info.rs (2)
game/src/packets/to_client/extended/equipped_items.rs (1)
  • new (18-52)
l2-core/src/game_objects/player/_player.rs (1)
  • new (77-116)
game/src/packets/to_client/char_selected.rs (2)
l2-core/src/id_factory.rs (1)
  • new (18-20)
game/src/packets/to_client/char_selection.rs (1)
  • new (21-137)
game/src/packets/to_client/extended/inventory_weight.rs (5)
game/src/packets/to_client/extended/equipped_items.rs (1)
  • new (18-52)
game/src/packets/to_client/extended/premium_state.rs (1)
  • new (14-21)
game/src/packets/to_client/extended/rotation.rs (1)
  • new (15-24)
l2-core/src/id_factory.rs (1)
  • new (18-20)
game/src/packets/to_client/char_info.rs (1)
  • new (47-198)
l2-core/src/game_objects/player/_player.rs (4)
l2-core/src/game_objects/item/_item.rs (1)
  • from_items (51-65)
l2-core/src/game_objects/player/inventory.rs (1)
  • from_items (14-18)
l2-core/src/game_objects/player/paper_doll.rs (1)
  • restore_visible_inventory (97-119)
l2-core/src/id_factory.rs (1)
  • instance (63-73)
l2-core/src/id_factory.rs (1)
l2-core/src/game_objects/player/_player.rs (3)
  • new (77-116)
  • from (37-43)
  • from (46-48)
game/src/packets/to_client/extended/rotation.rs (7)
game/src/packets/to_client/char_selected.rs (1)
  • new (16-56)
game/src/packets/to_client/extended/equipped_items.rs (1)
  • new (18-52)
game/src/packets/to_client/extended/inventory_weight.rs (1)
  • new (14-24)
game/src/packets/to_client/extended/premium_state.rs (1)
  • new (14-21)
game/src/packets/to_client/user_info.rs (1)
  • new (28-141)
l2-core/src/id_factory.rs (1)
  • new (18-20)
game/src/packets/to_client/move_to.rs (1)
  • new (14-26)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: test-only
🔇 Additional comments (31)
l2-core/src/data/char_template.rs (1)

142-144: LGTM!

The formatting consolidation improves code readability by placing each variable declaration on a single line, following standard Rust conventions.

login/src/packet/handleable/player_logout.rs (3)

6-18: LGTM!

The handler implementation correctly delegates to on_player_logout and is properly instrumented.


31-42: LGTM!

The test correctly verifies the handler's behavior when the game server is not set.


43-58: Nice optimization - avoids unnecessary clone in test.

The change from &[acc.clone()] to std::slice::from_ref(&acc) eliminates an unnecessary string clone while maintaining the same functionality. This is a good practice for test code efficiency.

game/src/packets/to_client/quest_list.rs (1)

51-51: QuestList test wiring to ObjectId looks correct

Importing ObjectId and assigning a fixed player.object_id keeps the test deterministic and aligns it with the new object‑ID system without affecting production behavior.

Also applies to: 72-72

l2-core/src/lib.rs (1)

25-25: Publicly exporting id_factory is appropriate

Making id_factory a public module is necessary for consumers like the game crate to access ObjectId/IdFactory; no issues with this addition.

game/src/packets/to_client/char_info.rs (1)

58-58: Using get_object_id() in CharInfo is consistent with the new ID model

Switching this field to p.get_object_id() ensures the packet uses the runtime object ID rather than the persistent character ID, matching the rest of the packet refactor.

game/src/packets/from_client/restart.rs (1)

37-37: Restart handler now deletes by object ID

Using player.get_object_id() for DeleteObject ensures the restart broadcast targets the correct in‑world entity instead of a persistent character ID.

game/src/packets/to_client/char_move_to_location.rs (1)

21-21: Movement packet correctly keyed by get_object_id()

Serializing p.get_object_id() as the mover’s identifier matches the new object‑ID system and keeps this packet consistent with other movement‑related packets.

game/src/packets/to_client/relation_changed.rs (1)

48-48: Relations now use runtime object IDs

Setting obj_id from player.get_object_id() correctly ties relation updates to the in‑world object identifier instead of the character’s persistent ID, in line with the rest of the refactor.

game/src/packets/to_client/move_to.rs (1)

17-17: MoveTo packet now keyed by get_object_id()

Writing p.get_object_id() here aligns the movement packet with the new object‑ID system and ensures clients track movement by runtime object ID rather than a persistent character ID.

game/src/packets/to_client/char_selected.rs (2)

23-23: LGTM! Object ID sourcing centralized.

The change from player.char_model.id to player.get_object_id() aligns with the PR's introduction of centralized object ID management via IdFactory and ObjectId.


64-64: Test correctly updated for ObjectId usage.

The test imports ObjectId and assigns a concrete object_id to the player instance, properly reflecting the new ID system.

Also applies to: 89-90

game/src/packets/to_client/extended/premium_state.rs (1)

18-18: LGTM! Consistent with centralized object ID system.

The migration from p.char_model.id to p.get_object_id() is consistent with the broader refactoring across packet handlers.

game/src/packets/to_client/user_info.rs (2)

55-55: LGTM! Object ID sourcing centralized in packet construction.

The change to player.get_object_id() aligns with the broader refactoring to use centralized object ID management.


428-428: LGTM! Party leadership comparison now uses object IDs.

The update to compare pt.get_leader_id() with p.get_object_id() is consistent with the changes in l2-core/src/game_objects/player/party.rs, where get_leader_id() now returns the leader's object ID rather than the character model ID.

game/src/packets/to_client/extended/inventory_weight.rs (1)

20-20: LGTM! Consistent with centralized object ID system.

The change to p.get_object_id() follows the same pattern as other packet handlers in this PR.

l2-core/src/game_objects/player/party.rs (2)

45-45: LGTM! Leader ID now sourced from object ID.

The change to return self.get_leader().get_object_id() is consistent with the centralized object ID system. This aligns with the usage in user_info.rs line 428 where party leadership is compared using object IDs.


53-57: All callers of Party::index_of() have been properly updated.

Verification confirms there is exactly one call site at l2-core/src/game_objects/player/_player.rs:890, and it correctly passes self.get_object_id() as the parameter. The parameter rename from player_id to player_object_id aligns with the implementation and caller expectations.

game/src/packets/to_client/extended/equipped_items.rs (1)

24-24: LGTM! Consistent with centralized object ID system.

The migration to p.get_object_id() follows the established pattern across packet handlers.

game/src/packets/to_client/extended/rotation.rs (1)

21-21: LGTM! Consistent with centralized object ID system.

The change to player.get_object_id() is consistent with the broader refactoring across packet handlers.

l2-core/src/game_objects/item/_item.rs (2)

45-45: New object_id field added to ItemObject.

The addition of object_id: ObjectId to ItemObject aligns with the broader refactoring to centralize object ID management across players, items, and other game objects.


54-64: No incompatibilities found—HashMap key change is already consistently implemented throughout the codebase.

All identified item HashMap access patterns are already compatible with object_id-based keys:

  • get_item() accepts item_obj_id: i32 parameter (line 449-451 in _player.rs)
  • PaperDoll::restore_visible_inventory() iterates .values() and uses item.object_id.value() (paper_doll.rs:99-102)
  • ItemList packet iterates .values() without depending on keys (item_list.rs:25)
  • Warehouse and Inventory both use ItemObject::from_items() consistently

No code attempts to access items by item.item_model.id (database ID), confirming the refactoring was completed throughout the codebase.

l2-core/src/game_objects/player/paper_doll.rs (1)

97-119: LGTM! Refactoring aligns with the new ObjectId system.

The changes correctly adapt the function to work with the new HashMap<i32, ItemObject> structure and properly access the object_id field through the ObjectId wrapper. The logic remains sound and the nested field accesses are appropriate.

l2-core/src/game_objects/player/_player.rs (4)

82-87: LGTM! Proper integration of ObjectId system.

The inventory creation and ObjectId assignment are correctly implemented. The factory-based ID generation ensures uniqueness and proper lifecycle management.


152-154: LGTM! Accessor method correctly retrieves the underlying ID value.

The get_object_id() method properly delegates to ObjectId::value() to return the primitive i32 value, maintaining the existing API contract while using the new ObjectId system internally.


890-890: LGTM! Party relation logic updated correctly.

The party index lookup now correctly uses get_object_id() to retrieve the player's unique identifier, consistent with the ObjectId refactor.


806-808: Additional impl block is fine.

Splitting Player methods into multiple impl blocks can aid organization. No concerns here.

l2-core/src/id_factory.rs (3)

6-6: Verify the starting ID constant value.

FIRST_OID = 0x1000_0000 (268,435,456 in decimal) seems like an unusual starting point. Ensure this aligns with the game's expected object ID range and doesn't conflict with other ID systems or serialization formats.

If you're unsure about the rationale, consider documenting why this specific value was chosen (e.g., to reserve lower IDs for static/system objects).


62-73: LGTM! Proper singleton implementation.

The use of OnceLock ensures thread-safe lazy initialization of the singleton instance. The implementation is correct and follows Rust best practices.


16-25: LGTM! Clean ObjectId wrapper design.

The ObjectId wrapper around Arc<i32> provides a clean abstraction with appropriate constructor and accessor methods. The use of Arc enables cheap cloning and automatic memory management.


#[derive(Debug, Clone)]
pub struct Player {
pub object_id: ObjectId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consider making the object_id field private.

The object_id field is currently public, which exposes implementation details and could allow external code to replace the ObjectId directly, bypassing the factory's ID management and lifecycle tracking. This undermines the integrity of the ID system.

Consider this change:

-    pub object_id: ObjectId,
+    object_id: ObjectId,

Then provide a public getter if external read access is needed (though get_object_id() already serves this purpose).

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub object_id: ObjectId,
object_id: ObjectId,
🤖 Prompt for AI Agents
In l2-core/src/game_objects/player/_player.rs around line 53, the object_id
field is declared pub which exposes internal ID management; change its
visibility from pub to private (remove pub) so external code cannot replace it,
rely on the existing get_object_id() for read access (or add a simple public
getter if missing), then update any external call sites that currently write or
access object_id directly to use constructor/factory or the getter; ensure
internal module code still accesses the private field directly or use pub(crate)
only if intra-crate write access is required.

Comment on lines +48 to +54
impl Drop for ObjectId {
fn drop(&mut self) {
// Only release if this is the last Arc
if Arc::strong_count(&self.0) == 1 {
IdFactory::instance().release_id(*self.0);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Race condition in Drop implementation.

The check Arc::strong_count(&self.0) == 1 and the subsequent call to release_id() are not atomic. Between checking the count and calling release_id(), another thread could:

  1. Clone the Arc (incrementing the count to 2+)
  2. The original thread proceeds to release the ID
  3. Result: ID is released while still in use

This can lead to ID collisions where the factory reissues an ID that's still referenced elsewhere.

Unfortunately, there's no perfect solution with the current Drop trait. Consider one of these approaches:

Solution 1: Remove automatic recycling from Drop
Require explicit ID release when objects are no longer needed:

-impl Drop for ObjectId {
-    fn drop(&mut self) {
-        // Only release if this is the last Arc
-        if Arc::strong_count(&self.0) == 1 {
-            IdFactory::instance().release_id(*self.0);
-        }
-    }
-}

Then update owner structs (like Player) to explicitly release IDs in their Drop implementations.

Solution 2: Accept the limitation and document it
Add clear documentation that ObjectId clones must not outlive the original in multi-threaded contexts. This is fragile but may be acceptable if your architecture guarantees single-ownership semantics.

Solution 3: Use weak references or other lifecycle management
Redesign to track object lifecycles differently, possibly with explicit lifecycle management at a higher level.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
l2-core/src/id_factory.rs around lines 48-54: the Drop impl for ObjectId uses
Arc::strong_count(...) == 1 then calls IdFactory::instance().release_id(...),
which is racy because the Arc can be cloned between the check and the release;
to fix, remove automatic recycling from ObjectId::drop (make it a no-op) and
instead require explicit release: add/ensure a public release_id API (on
IdFactory or an explicit ObjectId::release/self.release method) and update all
owning types (e.g., Player) to call that release in their Drop implementations
(or where ownership ends), make the explicit release operation thread-safe in
IdFactory, and add a comment documenting the change.

Comment on lines +90 to +95
pub fn release_id(&self, id: impl Into<i32>) {
let val = id.into();
if !self.reusable_ids.insert(val) {
warn!("Trying to release already released id: {val}");
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add validation to prevent releasing invalid IDs.

The release_id method accepts any i32 value without validation. This allows:

  1. Releasing IDs below FIRST_OID that were never allocated
  2. Releasing negative IDs
  3. Releasing IDs beyond the current next_id counter

This could pollute the reusable_ids pool with invalid IDs that break the allocation invariants.

Apply this diff to add validation:

 pub fn release_id(&self, id: impl Into<i32>) {
     let val = id.into();
+    if val < FIRST_OID {
+        warn!("Attempted to release invalid id {val} (below FIRST_OID)");
+        return;
+    }
+    if val >= self.next_id.load(Ordering::SeqCst) {
+        warn!("Attempted to release unallocated id {val}");
+        return;
+    }
     if !self.reusable_ids.insert(val) {
         warn!("Trying to release already released id: {val}");
     }
 }
🤖 Prompt for AI Agents
In l2-core/src/id_factory.rs around lines 90 to 95, validate the incoming id
before inserting into reusable_ids: after converting id.into(), check that it is
>= FIRST_OID, non-negative, and strictly less than the current allocation cursor
(read self.next_id with appropriate atomic ordering) — if any check fails, log a
warning and return without inserting; otherwise proceed to insert and keep the
existing duplicate-release warning path. Ensure you read next_id atomically to
avoid races and perform the validations before calling reusable_ids.insert.

…n issue with accessing static global variable
@artemijan artemijan closed this Nov 25, 2025
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