Skip to content

fix: a null sub-packet loses its separating space - #493

Merged
erwan-joly merged 3 commits into
NosCoreIO:masterfrom
denislauri1999:fix/null-subpacket-separator
Aug 23, 2026
Merged

fix: a null sub-packet loses its separating space#493
erwan-joly merged 3 commits into
NosCoreIO:masterfrom
denislauri1999:fix/null-subpacket-separator

Conversation

@denislauri1999

@denislauri1999 denislauri1999 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

A sub-packet serialized from null returns "-1" without the leading separator that the non-null path gets from its discriminator. The -1 ends up glued to the field before it, and the client cannot split the packet into fields at all.

sc_n 1 319 26719 50 1000 1536-1-1-1 -1 0 ...      <- today
sc_n 1 319 26719 50 1000 1536 -1 -1 -1 0 ...      <- expected

It shows up anywhere a sub-packet property can be null. Today that is sc_n's four equipment slots and gidx's family identifier — I hit it building both (NosCoreIO/NosCore#2281, NosCoreIO/NosCore#2283).

One-line fix in Serializer.PacketSerializer, plus a regression test on sc_n.

Two test failures on master are pre-existing and unrelated — DocumentationTest refuses CRLF checkouts on Windows; they fail identically with this change reverted.


While I was here: StringSerializer already replaces the separator with ^ for every non-final string field, so callers do not need to do it by hand. I had been doing exactly that in NosCore and you were right to flag it — I am removing it there, no change needed here.

One thing I would like your opinion on rather than change unilaterally: GidxPacket.FamilyIdentifier is modelled as a serverId.familyId compound sub-packet, but a packet capture writes that field as a single family id in all 670 gidx lines — not one contains a dot — and -1 when the character has no family. As modelled it can produce neither. Would you like a follow-up flattening it to a nullable id, or is the compound deliberate for something cross-server?

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Tests
    • Added coverage confirming that missing required sub-packets preserve the expected separator and -1 placeholder during serialization.
    • Verified that optional missing sub-packets continue to serialize as null.

A sub-packet serialized from null returns "-1" without the leading separator the non-null path
gets from its discriminator, so the -1 ends up glued to the field before it and the client
cannot split the packet into fields at all:

    sc_n 1 319 26719 50 1000 1536-1-1-1 -1 0 ...

instead of

    sc_n 1 319 26719 50 1000 1536 -1 -1 -1 0 ...

It shows anywhere a sub-packet property can be null, which today means sc_n's four equipment
slots and gidx's family identifier.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@denislauri1999
denislauri1999 force-pushed the fix/null-subpacket-separator branch from 8351771 to fba5074 Compare August 23, 2026 02:22
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 641cb3e4-234c-4400-898f-8b31ad2db013

📥 Commits

Reviewing files that changed from the base of the PR and between 8ea1796 and 2bc808f.

📒 Files selected for processing (1)
  • src/NosCore.Packets/Serializer.cs

Walkthrough

The test fixture registers ScnPacket and verifies separated -1 output for null sub-packets. The serializer change removes explanatory comments and does not alter runtime behavior.

Changes

Serialization test coverage

Layer / File(s) Summary
Verify null sub-packet separators
src/NosCore.Packets/Serializer.cs, test/NosCore.Packets.Tests/SerializerTest.cs
The shared test serializer registers ScnPacket. A regression test verifies that null sub-packets preserve the separator before the -1 placeholder. Explanatory serializer comments were removed without changing executable behavior.

Estimated code review effort: 1 (Trivial) | ~5 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 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 describes the main fix: preserving the separator for null sub-packets during serialization.
Docstring Coverage ✅ Passed Docstring check was indeterminate for this PR — some files could not be analyzed in time. Not blocking.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

return Expression.Condition(
Expression.Equal(specificTypeExpression, Expression.Constant(null, typeof(object))),
Expression.Constant(indexAttr.IsOptional ? null : "-1", typeof(object)),
Expression.Constant(indexAttr.IsOptional ? null : $"{discriminator}-1", typeof(object)),

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.

I don't think it's supposed to be there we have attribute to handle those edge case

Comment thread src/NosCore.Packets/Serializer.cs Outdated
incrementExpr = Expression.Constant(!isFromList || !isOptionalSerie);
}

// A null sub-packet is written as -1, and it has to carry the same leading separator

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.

don't need this comment

The new case built an ScnPacket but the serializer in this fixture only knows
the types it is handed, so Serialize threw KeyNotFoundException before it could
assert anything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@denislauri1999

Copy link
Copy Markdown
Contributor Author

Comment removed.

On the attribute — I would rather you point me at the one you mean than guess, because I could not find it. Here is what I traced, in case it changes the answer.

The leading space before a sub-packet does not come from the property's own splitter. For a property with SpecialSeparator, the outer splitter is deliberately emptied:

index.SpecialSeparator != null
    ? Expression.Constant(indexAttr.SpecialSeparator ?? string.Empty, typeof(object))
    : (Expression)Expression.Convert(propertySplitter, typeof(object));

and the space is put back further down by handing the sub-packet a discriminator of " ":

if (header == null && indexAttr.SpecialSeparator != null)
{
    header = " ";
}

The null branch returns before that discriminator is ever used, which is why the space disappears only when the value is null. So as far as I can see the current mechanism is SpecialSeparator plus that discriminator, and my change just makes the null path agree with the non-null one. If there is an attribute that expresses it more directly I have missed it and will happily redo it that way.

Separately, and independent of this: ScnPacket index 9 (BootsInstanceDetails) has no SpecialSeparator while 6, 7 and 8 do, so that slot serialises as three space-separated fields where the other three are dotted. Want that in this PR or a separate one?

And a data point that may bear on both: a capture writes all four of those slots as a bare -1 when empty, never -1.0.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@erwan-joly
erwan-joly merged commit 338e963 into NosCoreIO:master Aug 23, 2026
2 checks passed
@denislauri1999

Copy link
Copy Markdown
Contributor Author

Thanks for merging. One consequence worth knowing: gidx for a character with no family now serialises correctly — the capture writes it as gidx 1 741328 -1 - 0 and a null sub-packet finally produces that.

A character with a family still does not, because GidxFamilySubPacket renders serverId.familyId where the capture has a single id in all 670 lines. That is the question from the description, still open whenever you have a view: flatten it to a nullable id, or is the compound deliberate?

erwan-joly added a commit that referenced this pull request Aug 23, 2026
…r patch (#494)

#493 patched the serializer so a null sub-packet emits its discriminator with
the -1. That was the wrong level: NullableSerializer already carries the
splitter into the null branch for a nullable value, and gidx only needed the
engine change because the packet was modelled wrong.

The server gidx declared FamilyIdentifier as a two-field sub-packet rendering
serverId.familyId. The client gidx next to it already declares a plain
long FamilyId at the same index, and a capture writes a single id. Flattened to
long?, so a null family now serialises as "gidx 1 741328 -1 - 0" through the
ordinary nullable path with no serializer change at all.

ScnPacket index 9 was missing the SpecialSeparator its three siblings carry, so
the boots slot went out as three space-separated fields instead of dotted.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@denislauri1999

Copy link
Copy Markdown
Contributor Author

Thanks for #494 — flattening the family id is right, and I have verified it against the capture on 21.0.0. Both cases reproduce exactly now:

gidx 1 521919 5083 [NDM](Gardien) 3    <- with a family
gidx 1 741328 -1 - 0                    <- without

Bumping NosCore to 21.0.0 in NosCoreIO/NosCore#2286.

One thing to flag rather than reopen, since you reverted the serializer change deliberately: sc_n still cannot be produced correctly, and none of the three ways of declaring it gets there.

null sub-packets        sc_n ... 1536 990.0.0-1-1-1 0 0 ...   <- the -1s are glued on
all three fields set    sc_n ... 1536 990.0.0 -1.0.0 -1.0.0 -1.0.0 0 ...
the capture             sc_n ... 1536 990.0.0 997.0.0 -1 -1 0 ...

The real server writes an empty slot as a bare -1, which only a null sub-packet expresses, and that is the path that loses the space.

It costs nothing today — nothing creates a partner yet, so the branch is unreachable — and I am not going to push on it. But if a partner ever gets equipment, that is where it will surface, and the same shape applies to any future packet with an optional sub-packet. Happy to leave it closed if you would rather solve it another way when it matters.

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.

2 participants