Skip to content

feat: send the map's discriminator (Devil Square stage) to the extended client - #1346

Open
Mosch0512 wants to merge 6 commits into
MUnique:masterfrom
Mosch0512:feat/map-discriminator
Open

Mosch0512 wants to merge 6 commits into
MUnique:masterfrom
Mosch0512:feat/map-discriminator

Conversation

@Mosch0512

@Mosch0512 Mosch0512 commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

MuMain needs to know which stage of Devil Square a player is in: its monster data can make a monster attack differently on one stage (map variants, MuMain's monsters design D67), and the map number alone does not tell the stages apart. OpenMU already does: the seven Devil Square stages are map definitions on number 9 (stages 1–4) and 32 (stages 5–7) with Discriminator 1 to 7. This sends that discriminator to the extended client.

The original MapChanged packet stays as it was (coding rules, rule 2): the extended client gets new or longer …Extended packets.

Commits

  1. feat(packets): MapChangedExtended carries the map's discriminator — MapChangedExtended (C3 1C 0F, 16 bytes) is MapChanged with MapDiscriminator appended at index 15, after the 15 bytes of the original. A client can tell by the length whether a server sends it: an older server sends the 15-byte MapChanged, whose bytes 10 to 14 are not initialized.
  2. feat(gameserver): map change and teleport send the discriminator to the extended client — MapChangeExtendedPlugIn and TeleportExtendedPlugIn ([MinimumClient(106, 3)]) send MapChangedExtended with the discriminator of the player's map; other clients keep MapChangePlugIn / TeleportPlugIn and the original packet. A discriminator which doesn't fit into a byte is sent as 0, not cut to the one of another map. Plugin names and descriptions in en, de and zh-CN.
  3. feat: the extended client gets the discriminator when it enters the world — CharacterInformationExtended (C3 F3 03) gets MapDiscriminator at index 92 and is 93 bytes long. A character which loses its connection in Devil Square can log in again and rejoin its stage without a map change; the mini game puts it on the stage's map before the character information is sent, so this is how the client learns the stage then.
  4. test(packets): ReadPackets moves through the packets in a while loop — the test's packet loop no longer moves a for counter in its body (Sonar S127, reported by Codacy here).

The structs, connection extensions and packet tests come from the project's XSLT transformations. The markdown documentation comes from GenerateMarkdownDocs.xslt, run with Saxon (the build runs it with npx xslt3); on master's XML it reproduces all 336 committed packet documents byte for byte.

Testing

  • MapDiscriminatorRemoteViewTests: a remote player logs in through PlayerTestHelper and warps to a Devil Square stage. The extended client gets MapChangedExtended with the stage for a map change, a teleport and a failed map change (also for discriminators 257 and -1, which it gets as 0), and CharacterInformationExtended with the stage when it enters the world on a stage. A Season 6 client gets the 15-byte MapChanged.
  • Each test fails when the three plugins send 0 instead of the discriminator.
  • MUnique.OpenMU.Network.Packets.Tests: 650/650.
  • MUnique.OpenMU.Tests: 2507 passed, 2 skipped, 3 failed. The 3 are Discord text tests (GuildShowsMasterAndOnlineMembersAsync, WhoShowsCharacterAsync, SetupReportListsCreatedAdoptedAndFailed), which expect \n and get \r\n on Windows; they don't touch this change.
  • Each of the first three commits builds on its own; after the fourth, MapDiscriminatorRemoteViewTests passes again (5/5).

Clients

The fork PR is Mosch0512#7.

GameMapDefinition.Discriminator tells maps with the same number apart:
the stages of Devil Square are seven definitions on maps 9 (1 to 4) and
32 (5 to 7). The extended client needs it to know the stage it is in.

MapChangedExtended (C3 1C 0F) is MapChanged with the discriminator
appended at index 15, after the 15 bytes of the original, which stays
as it was. A client can tell by the length whether a server sends it:
an older server sends the 15-byte MapChanged, whose bytes 10 to 14 are
not initialized.

The structs, the connection extension and the packet tests come from
the XSLT transformations of the project, the markdown documentation
from GenerateMarkdownDocs.xslt run with Saxon (the build runs it with
npx xslt3).
…he extended client

MuMain (the extended client, 106.3) needs to know which stage of Devil
Square a player is in: its monster data can make a monster attack
differently on one stage, and the map number alone does not tell the
stages of a map apart.

MapChangeExtendedPlugIn and TeleportExtendedPlugIn send MapChangedExtended
with the discriminator of the player's map. Other clients keep
MapChangePlugIn and TeleportPlugIn, and the original MapChanged. A
discriminator which doesn't fit into a byte is sent as 0, instead of
being cut to the one of another map.

Tests: the extended client gets the discriminator with a map change, a
teleport and a failed map change, a Season 6 client the original packet.
PlayerTestHelper can now log in any player, so the remote player of the
tests enters the world the usual way instead of getting its character
set by reflection.
…inator

A character which loses its connection during Devil Square can log in
again and rejoin the game on its stage. That needs no map change: the
mini game puts the character on the map of its stage before the
character information is sent, so MapChangedExtended never tells the
client the stage then.

CharacterInformationExtended (C3 F3 03) gets the discriminator at
index 92, which makes it 93 bytes long, and UpdateCharacterStatsExtendedPlugIn
sends the one of the character's map. As with MapChangedExtended, a
client can tell by the length whether a server sends it.

The structs, the connection extension and the packet tests come from
the XSLT transformations of the project, the markdown documentation
from GenerateMarkdownDocs.xslt run with Saxon.

Test: the extended client gets the discriminator when it enters the
world on a stage of Devil Square.
@codacy-production

codacy-production Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 31 complexity · 6 duplication

Metric Results
Complexity 31
Duplication 6

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

The for loop moved its counter in the body (Sonar S127, reported by Codacy
on MUnique#1346). Same packets, same failure without a header.

@sven-n sven-n left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Changes requested: src/Network/Packets/ServerToClient/ServerToClientPackets.xml defines MapChangedExtended with five reserved bytes (offsets 10–14), while the generated sender in src/Network/Packets/ServerToClient/ConnectionExtensions.cs:1412-1423 does not initialize them. GetSpan() need not return zero-filled memory, so a reused output buffer can leak stale bytes. Please explicitly initialize reserved bytes and test with a prefilled nonzero buffer. Please also verify compatibility with released MuMain and the companion extended client for map changes, teleports, and direct login to Devil Square.

The generated senders write only the fields of a packet, so its unused
bytes kept what an earlier packet left in the reused output buffer:
bytes 10 to 14 of MapChangedExtended and byte 89 of
CharacterInformationExtended (review of MUnique#1346).

The map change, the teleport and the character information of the
extended client write into a span which is cleared first, as
OpenNpcWindowPlugIn does. The remote view tests write into memory which
is filled with 0xFF, and a test checks the unused bytes.
@Mosch0512

Copy link
Copy Markdown
Contributor Author

Thanks for the review. Both points are addressed.

Unused bytes (d75043b): the extended client's map change, teleport and character information are now written into a span that is cleared first, as OpenNpcWindowPlugIn already does. So bytes 10–14 of MapChangedExtended are 0, and so is byte 89 of CharacterInformationExtended (between InventoryExtensions and Resets; that gap was already there in the 92-byte version). The map change and the teleport share one sender, SendClearedMapChangedExtendedAsync.

The remote view tests now write into a PipeWriter whose memory is pre-filled with 0xFF. The new test ExtendedPacketsWriteTheirUnusedBytesAsZeroAsync checks the gaps after a map change, a teleport, a failed map change and the character information. Without the Clear() calls it fails with 255, 255, 255, 255, 255 and 255. The other MUnique.OpenMU.Tests pass (3 Discord tests fail on Windows because of \r\n, also on master).

The original MapChanged and the other generated senders leave their gaps as they are on master, because the generator writes only the fields. If you'd like that fixed for all packets, clearing the span in the generated Ref constructors could be a separate PR.

Compatibility, tested live against a server built from d75043b with a fresh database. Both clients report version 106.3, so both get the extended packets:

Released MuMain v1.22.1 (built from 7de4c59, the release commit) MuMain with sven-n/MuMain#886 (2a2f5f9)
Map change into Devil Square 7 (Charon) MapChangedExtended, 16 bytes, map 32, discriminator 7: entered same packet: entered
Teleport (Dark Wizard's Teleport skill) MapChangedExtended, 16 bytes, IsMapChange: False: teleported same: teleported
Direct login into Devil Square 7 (connection lost inside, logged in again during the event) CharacterInformationExtended, 93 bytes, map 32, discriminator 7: back in Devil Square same: back in Devil Square

The packets are from the admin panel's network analyzer, and for both logins the server logged "Devil Square 7 for Game Server 0": Player ... rejoined the game after losing its connection. The released client reads the map change only up to byte 9 (ReceiveTeleport casts the buffer) and checks the character information only for a minimum length (safe_cast to its 92-byte struct), so the longer packets change nothing for it.

Unrelated to this PR, seen with both clients: going from Devil Square to character selection through the menu warps the character out, and the client's F3 12 arrives after the character was deselected. The server then logs Error while processing the message: C1 04 F3 12 ... SelectedCharacter ... is not initialized yet (PlayerMapTransitions.ClientReadyAfterMapChangeAsync). I can open an issue for it.

CancelPendingFlush and Complete had empty bodies (Sonar S1186) and FlushAsync
ignored its cancellation token (Sonar S1172), reported by Codacy on
MUnique#1346. The empty methods now say why nothing happens, and a
flush with a cancelled token throws, as it does on a real pipe.

This branch has not been deployed

No deployments
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