Skip to content

feat: IP-bind NosMall endpoint to active game session#2091

Open
erwan-joly wants to merge 14 commits intomasterfrom
feature-handler-fills
Open

feat: IP-bind NosMall endpoint to active game session#2091
erwan-joly wants to merge 14 commits intomasterfrom
feature-handler-fills

Conversation

@erwan-joly
Copy link
Copy Markdown
Collaborator

@erwan-joly erwan-joly commented Apr 24, 2026

Summary

Adds a second verification gate on top of the existing sas = MD5(sid+pid+user_id+m_szName+salt) check on the Mall entrypoint: the caller's X-Forwarded-For must match the IP of the user's live game session, so a leaked sas token can't be redeemed from another machine.

What changed

  • AuthHub / AuthCodeService — new RegisterSessionIp / UnregisterSessionIp / GetSessionIp RPCs keyed by account name, living on MasterServer.
  • ClientSession — on authenticate, capture Channel.RemoteAddress and push it into AuthHub; on disconnect, unregister it. Takes IAuthHub as a new constructor dep.
  • MallController — after the MD5 gate, resolve the session IP via authHub.GetSessionIpAsync(user_id) and reject on mismatch (or missing/malformed X-Forwarded-For).

Test plan

  • dotnet build — green.
  • dotnet test — 924/924 passing.
  • Smoke: log in on one IP, hit the Mall URL from the same client → accepted.
  • Smoke: log in on one IP, replay the same Mall URL from a different IP → rejected.

Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Session IP management: register, unregister, and query account session IPs via client/server endpoints.
    • New admin/mod commands: ChangeGender, SetHairColor, SetHairStyle, Kill, SetSpPoint, ShoutHere, SetBankGold, SetHeroXp, SetJobLevelXp, SetSpAdditionPoint.
  • Improvements

    • Enforced IP check on web entrypoints.
    • Centralized, tick-driven monster respawns and clock-driven skill cooldown resets.
    • Removed artificial per-packet delay for snappier handling.

Adds a second verification gate on top of the existing
sas=MD5(sid+pid+user_id+m_szName+salt) check: the caller's
X-Forwarded-For must match the IP of the user's live game session,
so a leaked sas can't be redeemed from another machine.

- AuthHub / AuthCodeService: new RegisterSessionIp / UnregisterSessionIp
  / GetSessionIp RPCs keyed by account name.
- ClientSession: capture Channel.RemoteAddress on authenticate and
  push it into AuthHub; takes IAuthHub as a new ctor dep.
- MallController: after the MD5 gate, resolve the session IP via
  authHub.GetSessionIpAsync and reject on mismatch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Adds per-account session IP management via SignalR hub and AuthCodeService; integrates IP registration/unregistration into ClientSession and WebAPI validation; centralizes monster respawn scheduling into MapInstance and moves skill-cooldown resets to a clock-driven, per-map tick scheduler; adds multiple GM/mod command packets and handlers; updates tests and constructors for new dependencies.

Changes

Cohort / File(s) Summary
Auth Hub & Client
src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/IAuthHub.cs, src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHub.cs, src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHubClient.cs
Added hub methods to register/unregister/get per-account session IP; client mirrors calls; hub delegates to AuthCodeService.
Auth Code Service
src/NosCore.GameObject/Services/AuthService/IAuthCodeService.cs, src/NosCore.GameObject/Services/AuthService/AuthCodeService.cs
Added concurrent dictionary and methods to register, unregister, and retrieve stored session IPs by account.
Session & Web API Integration
src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs, src/NosCore.WebApi/Controllers/NosmallController.cs
ClientSession now accepts IAuthHub, registers remote IP on InitializeAccount and attempts unregister on disconnect; Web API controller injected with IAuthHub validates X-Forwarded-For header against stored session IP in an async action.
Map / Respawn / Monster
src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstance.cs, src/NosCore.GameObject/Services/MapInstanceGenerationService/MapInstanceGenerationService.cs, src/NosCore.GameObject/Messaging/Handlers/Battle/MonsterRespawnHandler.cs, test/.../MonsterRespawnHandlerTests.cs
Centralized respawn scheduling: MapInstance.ScheduleRespawn + per-tick sweep; MonsterRespawnHandler schedules via IClock instead of awaiting delays; generator threads IBattleService into MapInstance.
Battle cooldowns
src/NosCore.GameObject/Services/BattleService/BattleService.cs, src/NosCore.GameObject/Services/BattleService/IBattleService.cs, test/.../BattleServiceTests.cs
Replaced per-skill Task.Delay resets with clock-driven scheduler storing ready timestamps; added TickCooldownResetsAsync(MapInstance) invoked each map tick; constructor gains IClock.
Packet handling timing
src/NosCore.GameObject/Networking/ClientSession/WorldPacketHandlingStrategy.cs
Removed fixed concurrent 200ms delay; packet handlers are awaited directly.
Command packets & handlers
src/NosCore.Data/CommandPackets/*.cs, src/NosCore.PacketHandlers/Command/*
Added multiple GM/mod command packets (ChangeGender, SetHairColor, SetHairStyle, Kill, SetSpPoint, ShoutHere, SetBankGold, SetHeroXp, SetJobLevelXp, SetSpAdditionPoint) and corresponding handlers that modify character state and broadcast updates.
Tests / Helpers
test/NosCore.Tests.Shared/TestHelpers.cs, test/.../BattleServiceTests.cs, test/.../MonsterRespawnHandlerTests.cs
Updated test helpers and tests to supply new constructor dependencies (IBattleService, IAuthHub, IClock).

Sequence Diagram

sequenceDiagram
    participant Client
    participant ClientSession
    participant AuthHub
    participant AuthCodeService
    participant NosmallController

    Client->>ClientSession: Connect (RemoteAddress: IP)
    activate ClientSession
    ClientSession->>AuthHub: RegisterSessionIpAsync(accountName, IP)
    activate AuthHub
    AuthHub->>AuthCodeService: RegisterSessionIp(accountName, IP)
    activate AuthCodeService
    AuthCodeService-->>AuthHub: ack
    deactivate AuthCodeService
    deactivate AuthHub
    deactivate ClientSession

    Client->>NosmallController: Request (X-Forwarded-For: IP)
    activate NosmallController
    NosmallController->>AuthHub: GetSessionIpAsync(accountName)
    activate AuthHub
    AuthHub->>AuthCodeService: GetSessionIp(accountName)
    activate AuthCodeService
    AuthCodeService-->>AuthHub: stored IP
    deactivate AuthCodeService
    AuthHub-->>NosmallController: stored IP
    deactivate AuthHub
    NosmallController->>NosmallController: compare header IP vs stored IP
    alt match
        NosmallController-->>Client: proceed
    else mismatch
        NosmallController-->>Client: reject (ArgumentException)
    end
    deactivate NosmallController

    Client->>ClientSession: Disconnect
    activate ClientSession
    ClientSession->>AuthHub: UnregisterSessionIpAsync(accountName)
    activate AuthHub
    AuthHub->>AuthCodeService: UnregisterSessionIp(accountName)
    activate AuthCodeService
    AuthCodeService-->>AuthHub: ack
    deactivate AuthCodeService
    deactivate AuthHub
    deactivate ClientSession
Loading
sequenceDiagram
    participant MonsterRespawnHandler
    participant Clock
    participant MapInstance
    participant MapTickLoop
    participant BattleService
    participant SessionRegistry

    MonsterRespawnHandler->>Clock: compute respawnAt = now + respawnMs
    activate Clock
    Clock-->>MonsterRespawnHandler: respawnAt
    deactivate Clock
    MonsterRespawnHandler->>MapInstance: ScheduleRespawn(monster, respawnAt)
    activate MapInstance
    MapInstance-->>MapInstance: enqueue respawn
    deactivate MapInstance

    MapTickLoop->>MapInstance: life-loop tick
    activate MapInstance
    MapInstance->>BattleService: TickCooldownResetsAsync(this)
    activate BattleService
    BattleService->>SessionRegistry: enumerate sessions on map
    SessionRegistry-->>BattleService: sessions
    BattleService-->>BattleService: send SkillResetPacket for due entries
    deactivate BattleService

    MapInstance->>MapInstance: SweepPendingRespawnsAsync()
    activate MapInstance
    MapInstance->>MapInstance: reset monster HP/MP/alive/hitlist/pos
    MapInstance->>SessionRegistry: broadcast Monster.GenerateIn()
    deactivate MapInstance
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Feature handler fills #2086: Modifies AuthHub/IAuthHub/AuthHubClient surface and WebAPI auth wiring—strong overlap with added session IP hub methods and client calls.
  • fix/hunt-quest-completion #2085: Alters ClientSession disconnect/cleanup behavior—overlaps with added IAuthHub calls in InitializeAccount/OnDisconnectedAsync.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.76% 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 'feat: IP-bind NosMall endpoint to active game session' directly and clearly describes the main objective: adding IP-binding verification to the NosMall endpoint. This is the primary feature across all changes in the PR.
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 feature-handler-fills

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: 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 `@src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs`:
- Around line 121-125: The code is registering Channel.RemoteAddress without
normalizing so stored values like "127.0.0.1:8080" will fail IPAddress.TryParse
downstream; in ClientSession.cs before calling
authHub.RegisterSessionIpAsync(accountDto.Name, remoteAddress) extract or
normalize the IP-only component (e.g. parse Channel.RemoteAddress as an
EndPoint/IPEndPoint or split off the port and use the Address portion or use
Uri/IPAddress parsing) and pass that plain IP string to RegisterSessionIpAsync;
keep the call fire-and-forget but ensure the stored value is IP-only (mirroring
the way UnregisterSessionIpAsync is handled) so NosmallController.cs
IPAddress.TryParse(expected, ...) succeeds.

In `@src/NosCore.WebApi/Controllers/NosmallController.cs`:
- Around line 30-38: The code reads Request.Headers["X-Forwarded-For"] directly
and throws ArgumentException on mismatch; instead configure ASP.NET's forwarded
headers middleware in WebApiBootstrap by calling UseForwardedHeaders with a
properly configured ForwardedHeadersOptions and trusted proxies/networks, then
in NosmallController.cs rely on the framework-populated connection info (e.g.,
Request.HttpContext.Connection.RemoteIpAddress) rather than parsing
X-Forwarded-For manually; also replace the throw of
ArgumentException("X-Forwarded-For") with an appropriate 4xx response (e.g.,
return Unauthorized/Forbid or BadRequest) in the method that calls
authHub.GetSessionIpAsync so authentication failures produce correct client
error codes.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: d3ead61b-d066-4acb-84d2-8da3b038392a

📥 Commits

Reviewing files that changed from the base of the PR and between e455f1d and e330084.

📒 Files selected for processing (8)
  • src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHub.cs
  • src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/AuthHubClient.cs
  • src/NosCore.GameObject/InterChannelCommunication/Hubs/AuthHub/IAuthHub.cs
  • src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs
  • src/NosCore.GameObject/Services/AuthService/AuthCodeService.cs
  • src/NosCore.GameObject/Services/AuthService/IAuthCodeService.cs
  • src/NosCore.WebApi/Controllers/NosmallController.cs
  • test/NosCore.Tests.Shared/TestHelpers.cs

Comment on lines +121 to +125
var remoteAddress = Channel.RemoteAddress;
if (remoteAddress != null)
{
_ = authHub.RegisterSessionIpAsync(accountDto.Name, remoteAddress);
}
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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Verify RemoteAddress contract and concrete formatting in channel implementations.
rg -n --type=cs -C3 '\bRemoteAddress\b'

# Verify authentication flow call sites for InitializeAccount.
rg -n --type=cs -C3 '\bInitializeAccount\s*\('

Repository: NosCoreIO/NosCore

Length of output: 3955


🏁 Script executed:

# Find IChannel interface to understand RemoteAddress contract
rg -n --type=cs -B5 -A10 'interface IChannel'

Repository: NosCoreIO/NosCore

Length of output: 4778


🏁 Script executed:

# Find IAuthHub interface to understand RegisterSessionIpAsync contract
rg -n --type=cs -B2 -A5 'RegisterSessionIpAsync'

Repository: NosCoreIO/NosCore

Length of output: 3896


🏁 Script executed:

# Check OnDisconnectedAsync implementation for IP unregistration
rg -n --type=cs -B5 -A15 'OnDisconnectedAsync'

Repository: NosCoreIO/NosCore

Length of output: 7798


🏁 Script executed:

# Check if RemoteAddress is normalized or parsed anywhere
rg -n --type=cs 'RemoteAddress' -A5

Repository: NosCoreIO/NosCore

Length of output: 715


🏁 Script executed:

# Get full OnDisconnectedAsync method from ClientSession.cs
sed -n '164,250p' src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs

Repository: NosCoreIO/NosCore

Length of output: 2606


🏁 Script executed:

# Find IChannel interface definition
rg -n --type=cs 'interface IChannel' -A 30

Repository: NosCoreIO/NosCore

Length of output: 3434


🏁 Script executed:

# Search for UnregisterSessionIpAsync calls
rg -n --type=cs 'UnregisterSessionIpAsync' -B 2 -A 3

Repository: NosCoreIO/NosCore

Length of output: 3264


🏁 Script executed:

# Find authCodeService.RegisterSessionIp implementation to understand the sync operation
rg -n --type=cs 'RegisterSessionIp\(' -B 2 -A 8

Repository: NosCoreIO/NosCore

Length of output: 3026


🏁 Script executed:

# Search for IChannel interface more broadly
rg -n --type=cs 'interface IChannel[^A-Za-z]' -A 20

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Search for Channel class and RemoteAddress property definition
rg -n --type=cs 'RemoteAddress' -B 5 -A 2 | head -80

Repository: NosCoreIO/NosCore

Length of output: 889


🏁 Script executed:

# Check DotNetty or other channel implementations for RemoteAddress
fd -e cs -x rg -l 'RemoteAddress' {} \; | head -20

Repository: NosCoreIO/NosCore

Length of output: 128


🏁 Script executed:

# Find the Channel property declaration in ClientSession
rg -n --type=cs 'public.*Channel\s*{|private.*Channel\s*{' src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs -A 3

Repository: NosCoreIO/NosCore

Length of output: 203


🏁 Script executed:

# Look at the full ClientSession class header and constructor to understand Channel type
head -120 src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs | tail -60

Repository: NosCoreIO/NosCore

Length of output: 2556


🏁 Script executed:

# Search for DotNetty IChannel usage
rg -n --type=cs 'DotNetty' | head -20

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Search for what's imported/used for Channel in the file
head -50 src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs

Repository: NosCoreIO/NosCore

Length of output: 1933


🏁 Script executed:

# Find NetworkClient class definition
rg -n --type=cs 'class NetworkClient' -A 30

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Look for Channel property in NetworkClient or related files
rg -n --type=cs 'IChannel|class.*Channel' src/NosCore.Networking/ -A 5 | head -80

Repository: NosCoreIO/NosCore

Length of output: 125


🏁 Script executed:

# Search for NetworkClient class in entire codebase
rg -n --type=cs 'class NetworkClient'

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Find all references to NosCore.Networking namespace
rg -n 'namespace NosCore.Networking'

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Look for Channel property definition
rg -n --type=cs 'public.*Channel\s+\{' -A 2

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Check if Channel is an auto-property or field in ClientSession
rg -n --type=cs '\bChannel\b' src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs

Repository: NosCoreIO/NosCore

Length of output: 505


🏁 Script executed:

# Check for base class definitions
rg -n --type=cs ': NetworkClient'

Repository: NosCoreIO/NosCore

Length of output: 208


🏁 Script executed:

# Look for DotNetty or other networking libraries in project files
find . -name "*.csproj" -o -name "*.sln" | head -5

Repository: NosCoreIO/NosCore

Length of output: 251


🏁 Script executed:

# Check dependencies
rg -n 'DotNetty|IChannel' | head -20

Repository: NosCoreIO/NosCore

Length of output: 7045


🏁 Script executed:

# Find NetworkClient in NosCore.Core
rg -n --type=cs 'class NetworkClient' src/NosCore.Core/

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Check for NosCore.Core imports and structure
find src/NosCore.Core -name "*.cs" | grep -i network | head -10

Repository: NosCoreIO/NosCore

Length of output: 43


🏁 Script executed:

# Look for uses of the IP address from RegisterSessionIp
rg -n --type=cs 'GetSessionIp|_sessionIps' -B 2 -A 5

Repository: NosCoreIO/NosCore

Length of output: 6784


🏁 Script executed:

# Check if any downstream code parses or validates the IP address
rg -n --type=cs 'IPAddress|Parse.*Ip|Ip.*Parse' -B 2 -A 3 | head -50

Repository: NosCoreIO/NosCore

Length of output: 954


Address potential IP parsing failure in downstream Nosmal validation.

Line 124 registers Channel.RemoteAddress as a fire-and-forget call. More critically, NosmallController.cs (line 33–34) calls IPAddress.TryParse(expected, ...) on the stored IP. If Channel.RemoteAddress includes endpoint formatting (e.g., "127.0.0.1:8080" from DotNetty), the parse will fail silently, causing legitimate Mall requests to be rejected even though the session is authenticated.

Ensure remoteAddress is normalized to a plain IP string (without port) before passing to RegisterSessionIpAsync, or extract the address component when storing. The disconnect path correctly uses error handling when calling UnregisterSessionIpAsync.

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

In `@src/NosCore.GameObject/Networking/ClientSession/ClientSession.cs` around
lines 121 - 125, The code is registering Channel.RemoteAddress without
normalizing so stored values like "127.0.0.1:8080" will fail IPAddress.TryParse
downstream; in ClientSession.cs before calling
authHub.RegisterSessionIpAsync(accountDto.Name, remoteAddress) extract or
normalize the IP-only component (e.g. parse Channel.RemoteAddress as an
EndPoint/IPEndPoint or split off the port and use the Address portion or use
Uri/IPAddress parsing) and pass that plain IP string to RegisterSessionIpAsync;
keep the call fire-and-forget but ensure the stored value is IP-only (mirroring
the way UnregisterSessionIpAsync is handled) so NosmallController.cs
IPAddress.TryParse(expected, ...) succeeds.

Comment thread src/NosCore.WebApi/Controllers/NosmallController.cs
erwan-joly and others added 2 commits April 24, 2026 21:49
Three MMORPG-hot paths were holding one sleeping Task per event for
seconds (or longer), which multiplies per-player and per-monster. All
three can be driven from the map's existing 400ms life tick instead.

- WorldPacketHandlingStrategy: drop the
  `Task.WhenAll(handler, Task.Delay(200))` wrapper. The per-session
  packet lock already serializes a single client's packets; the 200ms
  floor was a NosCore-only artifact (vanosilla/OpenNos have nothing
  like it) that only served to hold a timer task per packet.
- MonsterRespawnHandler: swap the fire-and-forget `Task.Delay(RespawnTime)`
  for `MapInstance.ScheduleRespawn(monster, respawnAt)`. The map life
  loop sweeps the pending-respawn table on each 400ms tick and revives
  whichever monsters have hit their ReadyAt.
- BattleService.ScheduleCooldownReset: same idea for skill-cooldown
  SkillResetPacket emission. BattleService now owns a
  `(CharacterVisualId -> CastId -> ReadyAt)` registry. MapInstance
  calls `IBattleService.TickCooldownResetsAsync(this)` on each tick
  to drain ready entries and send the packet through the character's
  normal outbound path.

No behavior change from the player's perspective — cooldown and
respawn timers still fire with ≤400ms of latency, which is under the
game's client-tick granularity.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three missing cosmetic GM commands that parallel the existing item-use
handlers (ChangeGenderHandler, HairDieHandler) — same refresh sequence
(GenerateEq + GenerateIn + GenerateCMode/GenerateEff for gender),
same enum validation (Enum.IsDefined guard), no bypass of sanity checks
beyond what a GM command is entitled to.

- $ChangeGender: flips the character's gender and plays the standard
  visual-refresh + effect 196 swap that the in-game gender card uses.
- $SetHairStyle Style: sets HairStyleType; ignores out-of-range values.
- $SetHairColor Color: sets HairColorType; same guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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: 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 `@src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs`:
- Around line 23-30: The handler currently only checks Enum.IsDefined for
HairStyleType, but player-supported styles are limited up to
HairStyleType.HairStyleB; update SetHairStylePacketHandler to reject styles
greater than HairStyleType.HairStyleB (in addition to the Enum.IsDefined check)
before assigning character.HairStyle so it matches the player validation logic
in PlayerBundleExtensions; specifically, add a guard that returns if
(HairStyleType)packet.Style > HairStyleType.HairStyleB (or packet.Style >
(int)HairStyleType.HairStyleB).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dcbebb7d-7e8a-4946-85c8-29a00c756031

📥 Commits

Reviewing files that changed from the base of the PR and between 21345d4 and 7f0f656.

📒 Files selected for processing (6)
  • src/NosCore.Data/CommandPackets/ChangeGenderPacket.cs
  • src/NosCore.Data/CommandPackets/SetHairColorPacket.cs
  • src/NosCore.Data/CommandPackets/SetHairStylePacket.cs
  • src/NosCore.PacketHandlers/Command/ChangeGenderPacketHandler.cs
  • src/NosCore.PacketHandlers/Command/SetHairColorPacketHandler.cs
  • src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs

Comment thread src/NosCore.PacketHandlers/Command/SetHairStylePacketHandler.cs Outdated
Three more GM commands that reuse existing building blocks:

- $ShoutHere Message — Moderator — broadcasts a yellow chat line to
  the caller's current map only. Complements the server-wide $Shout.
- $Kill — GameMaster — sets the caller's HP to 0 and publishes an
  EntityDiedEvent so PlayerRevivalHandler runs its normal death +
  respawn flow. Self-target only for now; cross-target kill would
  need the cross-channel StatData pattern like $SetLevel / $SetGold.
- $SetSpPoint Value — GameMaster — clamps to WorldConfiguration
  bounds, writes character.SpPoint, and refreshes the client via
  GenerateSpPoint.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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: 2

🧹 Nitpick comments (1)
src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs (1)

23-27: Harden clamp against invalid negative MaxSpPoints config.

Current logic assumes worldConfiguration.Value.MaxSpPoints >= 0. If config is negative, a negative SP value can still be assigned.

🔧 Proposed fix
         public override Task ExecuteAsync(SetSpPointPacket packet, ClientSession session)
         {
-            var clamped = packet.SpPoint < 0 ? 0
-                : packet.SpPoint > worldConfiguration.Value.MaxSpPoints ? worldConfiguration.Value.MaxSpPoints
+            var maxSpPoints = worldConfiguration.Value.MaxSpPoints < 0 ? 0 : worldConfiguration.Value.MaxSpPoints;
+            var clamped = packet.SpPoint < 0 ? 0
+                : packet.SpPoint > maxSpPoints ? maxSpPoints
                 : packet.SpPoint;
             session.Character.SpPoint = clamped;
             return session.SendPacketAsync(session.Character.GenerateSpPoint(worldConfiguration));
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs` around lines
23 - 27, The clamp currently assumes worldConfiguration.Value.MaxSpPoints is
non‑negative so a negative MaxSpPoints could allow negative SP; in
SetSpPointPacketHandler (look for packet.SpPoint,
worldConfiguration.Value.MaxSpPoints, session.Character.SpPoint,
session.SendPacketAsync and GenerateSpPoint) compute an effectiveMax =
Math.Max(0, worldConfiguration.Value.MaxSpPoints) and then clamp packet.SpPoint
into [0, effectiveMax] before assigning to session.Character.SpPoint and calling
session.SendPacketAsync(session.Character.GenerateSpPoint(...)).
🤖 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/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs`:
- Around line 18-22: The ExecuteAsync in KillCommandPacketHandler
unconditionally sets session.Character.Hp to 0 and publishes EntityDiedEvent;
change it to first check whether the character is already dead (e.g.,
session.Character.Hp > 0 or session.Character.IsDead == false) and only set Hp
to 0 and call await messageBus.PublishAsync(new
EntityDiedEvent(...)).ConfigureAwait(false) when the character was alive,
thereby preventing duplicate EntityDiedEvent publications for already-dead
characters.

In `@src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs`:
- Around line 21-27: The handler currently uses
string.IsNullOrEmpty(packet.Message) which treats whitespace-only input as
valid; change the check to string.IsNullOrWhiteSpace(packet.Message) so messages
that are null, empty, or only whitespace are routed to
session.SendPacketAsync(session.Character.GenerateSay(packet.Help(),
SayColorType.Yellow)) instead of being broadcast via
session.Character.MapInstance.SendPacketAsync(session.Character.GenerateSay(packet.Message,
SayColorType.Yellow)); update the condition in the ShoutHerePacketHandler to use
IsNullOrWhiteSpace and leave the rest of the logic intact.

---

Nitpick comments:
In `@src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs`:
- Around line 23-27: The clamp currently assumes
worldConfiguration.Value.MaxSpPoints is non‑negative so a negative MaxSpPoints
could allow negative SP; in SetSpPointPacketHandler (look for packet.SpPoint,
worldConfiguration.Value.MaxSpPoints, session.Character.SpPoint,
session.SendPacketAsync and GenerateSpPoint) compute an effectiveMax =
Math.Max(0, worldConfiguration.Value.MaxSpPoints) and then clamp packet.SpPoint
into [0, effectiveMax] before assigning to session.Character.SpPoint and calling
session.SendPacketAsync(session.Character.GenerateSpPoint(...)).
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3c2f82ef-4480-489d-8b81-63ff0e6f9bf6

📥 Commits

Reviewing files that changed from the base of the PR and between 7f0f656 and 7e5ef41.

📒 Files selected for processing (6)
  • src/NosCore.Data/CommandPackets/KillPacket.cs
  • src/NosCore.Data/CommandPackets/SetSpPointPacket.cs
  • src/NosCore.Data/CommandPackets/ShoutHerePacket.cs
  • src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs
  • src/NosCore.PacketHandlers/Command/SetSpPointPacketHandler.cs
  • src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs
✅ Files skipped from review due to trivial changes (1)
  • src/NosCore.Data/CommandPackets/KillPacket.cs

Comment thread src/NosCore.PacketHandlers/Command/KillCommandPacketHandler.cs
Comment thread src/NosCore.PacketHandlers/Command/ShoutHerePacketHandler.cs Outdated
…eroXp

Four more single-field GM commands that only wire existing ICharacterEntity
writable properties to a command packet. No new infrastructure.

- $SetBankGold Amount — writes Character.BankGold. Bank UI refreshes on
  next open; no separate refresh packet needed.
- $SetSpAdditionPoint Value — clamps to WorldConfiguration.
  MaxAdditionalSpPoints and refreshes via GenerateSpPoint, same shape
  as the just-added $SetSpPoint.
- $SetJobLevelXp Amount — writes Character.JobLevelXp and emits
  GenerateLev so the XP bars sync immediately.
- $SetHeroXp Amount — writes Character.HeroXp; same GenerateLev
  refresh.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@erwan-joly erwan-joly force-pushed the feature-handler-fills branch 2 times, most recently from d83b19e to 164cd20 Compare April 24, 2026 14:03
Capture is a skill-side mechanic, not an item effect. Matches the
vanosilla + OpenNos implementation:
  - Skill carries a `BCardType.Capture` bcard with subtype
    `AdditionalTypes.Capture.CaptureAnimal` (same enum codes as both
    reference servers).
  - BattleService.Hit detects the bcard before damage resolution and
    delegates to ICaptureService, skipping the normal damage pipeline
    (no SuPacket, no stat broadcast, no EntityDamagedEvent) — matches
    vanosilla BattleSystem's `if (!skill.BCards.Any(... Capture ...))`
    guards around the SuPacket sends.
  - Cooldown still fires so a failed roll consumes the gate.

CaptureService rules (cross-referenced with both servers):
  - Caster must be a player, target must be a live monster.
  - monster.Level <= player.Level.
  - monster.HP% < 50 (matches vanosilla BCardCaptureHandler gate).
  - Rate: 90 % if player < level 20, else 50 % (vanosilla constants).
  - Mate level = max(monster.Level - 15, 1) (vanosilla offset).
  - On success: insert MateDto via the auto-generated IDao<MateDto,long>,
    mark monster !IsAlive + Hp = 0, broadcast `out` to despawn the sprite.

Followups (not in this PR): dignity + MaxMateCount + RaidInstance
guards, SuCapturePacket (vanosilla's dedicated capture-su), in-game
mate list refresh post-entry (sc_n / p_clear / pinit).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@erwan-joly erwan-joly force-pushed the feature-handler-fills branch from 164cd20 to 4e514cb Compare April 24, 2026 14:16
erwan-joly and others added 8 commits April 25, 2026 02:28
…HSA-mr8r-92fq-pj8p)

Two moderate-severity advisories surfaced by NuGet audit against the
1.15.x line we shipped in #2085:

  - OpenTelemetry.Api 1.15.2 -> GHSA-g94r-2vxg-569j
  - OpenTelemetry.Exporter.OpenTelemetryProtocol 1.15.2 -> GHSA-mr8r-92fq-pj8p

Bumps:
  - OpenTelemetry.Exporter.OpenTelemetryProtocol   1.15.2 -> 1.15.3
  - OpenTelemetry.Extensions.Hosting               1.15.2 -> 1.15.3
  - OpenTelemetry.Instrumentation.AspNetCore       1.15.1 -> 1.15.2
  - OpenTelemetry.Instrumentation.EntityFrameworkCore 1.15.0-beta.1 -> 1.15.1-beta.1
  - OpenTelemetry.Instrumentation.Http             1.15.0 -> 1.15.1
  - OpenTelemetry.Instrumentation.Runtime          1.15.0 -> 1.15.1

Build clean with 0 vulnerability warnings. Full test suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the Postgres enum with ItemEffectType.MateCapture introduced
during the pet-capture work. Current capture path is skill-based
(BCardType.Capture / CaptureAnimal) but the enum value is kept so the
label is available if we later wire item-based capture cards the
way vanilla NosTale does.

Runs a single AlterDatabase with the new enum label appended; no
column changes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Picks up the CMapPacket.MapType → IsEntering rename; updates the one
call site in MapInstance.GenerateCMap accordingly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Return early when the character is already dead so the command doesn't
emit a second EntityDiedEvent and fire death side-effects twice.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Using IsNullOrWhiteSpace routes "   " to the help message instead of
broadcasting a blank shout to the whole map.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s vanosilla

Unset, the string property serializes to "-", producing "NsTeST - 0 …"
instead of the expected "NsTeST  0 …" (literal double space) that
vanosilla's LoginPacketsExtensions emits. Clients that tolerate the
deserialized form still misparse the serialized-from-NosCore form.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
LeadingBlank now defaults to empty string in NsTestPacket (Packets
20.0.1); drop the redundant explicit assignment. Also picks up the
AscrPacket leading-blank addition (20.0.2).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
NosCore was seeding character skills purely from
WorldConfiguration.BasicSkills[class][origin]. Every character must
own the universal Adventurer set — primary (200), secondary (201) and
Pet Catcher (209) — but nothing enforced that: a character created
from a config where those were missing ended up with no CharacterSkill
row for CastId 16, so `u_s 16 3 <monster>` fell through
SkillResolver.Resolve → null and BattleService replied with
`cancel 2 <target> 0`, which is exactly what we hit live.

Match vanosilla (Plugins.PacketHandling/Customization/BaseSkill.cs
hard-codes the same three) by always inserting 200 / 201 / 209 in
CharNewPacketHandler, de-duped against whatever the config-driven
BasicSkills list contributes. The config-side entries that used to
carry these three are dropped from world.yml since they're now
guaranteed by the handler.

Class-specific capture variants (235 Swordsman, 237 Mage) are still
picked up by SkillService.LearnClassSkillsAsync on JobLevelUp since
they're class-bound in the skill DB, not universal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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