Skip to content

feat(quest): add QuestProgressS6PlugIn for Season 6 client compatibility#793

Closed
noelfelipe wants to merge 2 commits into
MUnique:masterfrom
noelfelipe:feature/quest-progress-s6-plugin
Closed

feat(quest): add QuestProgressS6PlugIn for Season 6 client compatibility#793
noelfelipe wants to merge 2 commits into
MUnique:masterfrom
noelfelipe:feature/quest-progress-s6-plugin

Conversation

@noelfelipe
Copy link
Copy Markdown

Problem

The default QuestProgressPlugIn sends a 251-byte C1 F6 0C packet in the Season 9 format (QuestProgress struct). When a Season 6 GMO client receives this packet, it interprets it as PMSG_NPC_QUESTEXP_INFO and attempts to parse the trailing data as NPC_QUESTEXP_REQUEST_INFO[5] and NPC_QUESTEXP_REWARD_INFO[5] arrays. Because the packet layout does not match what the S6 client expects, it reads garbage byte counts, loops out of bounds over an internal array, and disconnects.

Root Cause

OpenMU uses a single QuestProgressPlugIn with no client-version guard. The packet it produces matches the Season 9+ protocol (C1 header, QuestProgress / QuestState struct layout). The Season 6 client speaks a different, older protocol for the same opcode (F6 0C):

Client S6 expected layout (C2 packet, 272 bytes):
\
PWMSG_HEADER (4 bytes) : C2 sizeH sizeL 0xF6
SubCode (1 byte) : 0x0C
RequestCount (1 byte)
RewardCount (1 byte)
RandCount (1 byte)
QuestIndex (4 bytes) : (Group << 16) | Number, little-endian

5 x NPC_QUESTEXP_REQUEST_INFO (28 bytes each, MSVC default alignment):
QUEST_REQUEST_TYPE [+0] BYTE (1) + pad (1)
m_wIndex [+2] WORD (2)
m_dwValue [+4] DWORD (4) required count / level
m_wCurValue [+8] DWORD (4) current progress
m_byItemInfo[15] [+12] BYTE (15)
struct pad [+27] BYTE (1)

5 x NPC_QUESTEXP_REWARD_INFO (24 bytes each, MSVC default alignment):
QUEST_REWARD_TYPE [+0] BYTE (1) + pad (1)
m_wIndex [+2] WORD (2)
m_dwValue [+4] DWORD (4) amount
m_byItemInfo[15] [+8] BYTE (15)
struct pad [+23] BYTE (1)
\\

Solution

Add QuestProgressS6PlugIn decorated with [MinimumClient(6, 0)] so that the ViewPlugInContainer selects it over the undecorated default plugin for any Season 6 client. For Season 9+ clients the existing QuestProgressExtendedPlugIn (MinimumClient 106.3) takes precedence.

The plugin builds the 272-byte packet manually using BinaryPrimitives for endian-safe writes and maps OpenMU domain objects to S6 wire types:

OpenMU Wire type Notes
QuestMonsterKillRequirement QUEST_REQUEST_MONSTER (1) Monster.Number → m_wIndex, MinimumNumber → m_dwValue, KillCount → m_wCurValue
QuestItemRequirement QUEST_REQUEST_ITEM (3) item type → m_wIndex, inventory count → m_wCurValue, serialized → m_byItemInfo
QuestReward.Experience QUEST_REWARD_EXP (1) Value → m_dwValue
QuestReward.Money QUEST_REWARD_ZEN (2) Value → m_dwValue
QuestReward.Item QUEST_REWARD_ITEM (4) item type → m_wIndex, serialized → m_byItemInfo

Affected files

  • src/GameServer/RemoteView/Quest/QuestProgressS6PlugIn.cs (new)

No existing files were modified. The original QuestProgressPlugIn can be disabled via the PlugInConfiguration table in the database (IsActive = false for GUID 5D2B7F90-FEFA-4889-B339-D64512471613) on servers that only serve Season 6 clients.

## Problem

The default QuestProgressPlugIn sends a 251-byte C1 F6 0C packet in the
Season 9 format (QuestProgress struct). When a Season 6 GMO client
receives this packet, it interprets it as PMSG_NPC_QUESTEXP_INFO and
attempts to parse the trailing data as NPC_QUESTEXP_REQUEST_INFO[5] and
NPC_QUESTEXP_REWARD_INFO[5] arrays. Because the packet layout does not
match what the S6 client expects, it reads garbage byte counts, loops
out of bounds over an internal array, and disconnects.

## Root Cause

OpenMU uses a single QuestProgressPlugIn with no client-version guard.
The packet it produces matches the Season 9+ protocol (C1 header,
QuestProgress / QuestState struct layout). The Season 6 client speaks a
different, older protocol for the same opcode (F6 0C):

  Client S6 expected layout (C2 packet, 272 bytes):
    PWMSG_HEADER  (4 bytes) : C2 sizeH sizeL 0xF6
    SubCode       (1 byte)  : 0x0C
    RequestCount  (1 byte)
    RewardCount   (1 byte)
    RandCount     (1 byte)
    QuestIndex    (4 bytes) : (Group << 16) | Number, little-endian

    5 x NPC_QUESTEXP_REQUEST_INFO (28 bytes each, MSVC default alignment):
      QUEST_REQUEST_TYPE  [+0]  BYTE  (1) + pad (1)
      m_wIndex            [+2]  WORD  (2)
      m_dwValue           [+4]  DWORD (4)   required count / level
      m_wCurValue         [+8]  DWORD (4)   current progress
      m_byItemInfo[15]    [+12] BYTE  (15)
      struct pad          [+27] BYTE  (1)

    5 x NPC_QUESTEXP_REWARD_INFO (24 bytes each, MSVC default alignment):
      QUEST_REWARD_TYPE   [+0]  BYTE  (1) + pad (1)
      m_wIndex            [+2]  WORD  (2)
      m_dwValue           [+4]  DWORD (4)   amount
      m_byItemInfo[15]    [+8]  BYTE  (15)
      struct pad          [+23] BYTE  (1)

## Solution

Add QuestProgressS6PlugIn decorated with [MinimumClient(6, 0)] so that
the ViewPlugInContainer selects it over the undecorated default plugin
for any Season 6 client.  For Season 9+ clients the existing
QuestProgressExtendedPlugIn (MinimumClient 106.3) takes precedence.

The plugin builds the 272-byte packet manually using BinaryPrimitives
for endian-safe writes and maps OpenMU domain objects to S6 wire types:

  OpenMU QuestMonsterKillRequirement -> QUEST_REQUEST_MONSTER (1)
    - Monster.Number        -> m_wIndex
    - MinimumNumber         -> m_dwValue
    - KillCount (state)     -> m_wCurValue

  OpenMU QuestItemRequirement        -> QUEST_REQUEST_ITEM (3)
    - Item type (group<<9|num)-> m_wIndex
    - MinimumNumber         -> m_dwValue
    - inventory count       -> m_wCurValue
    - serialized item data  -> m_byItemInfo[15]

  OpenMU QuestReward (Experience)    -> QUEST_REWARD_EXP (1)
  OpenMU QuestReward (Money)         -> QUEST_REWARD_ZEN (2)
  OpenMU QuestReward (Item)          -> QUEST_REWARD_ITEM (4)
    - item type             -> m_wIndex
    - Value                 -> m_dwValue
    - serialized item data  -> m_byItemInfo[15]

## Affected files

  src/GameServer/RemoteView/Quest/QuestProgressS6PlugIn.cs  (new)

No existing files were modified. The original QuestProgressPlugIn
can be disabled via the PlugInConfiguration table in the database
(set IsActive = false for GUID 5D2B7F90-FEFA-4889-B339-D64512471613)
to prevent it from competing with this plugin on servers that only
serve Season 6 clients.
@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a new plugin, QuestProgressS6PlugIn, to resolve compatibility issues between the OpenMU server and Season 6 clients. Previously, the server sent quest progress packets in a format expected by newer clients, causing Season 6 clients to misinterpret the data and disconnect. The new plugin ensures that the server sends the correct, legacy-compatible packet structure when a Season 6 client is detected.

Highlights

  • New Plugin Implementation: Added QuestProgressS6PlugIn to handle quest progress packets specifically for Season 6 clients.
  • Client Compatibility: Decorated the new plugin with [MinimumClient(6, 0)] to ensure it is selected for Season 6 clients, preventing disconnects caused by protocol mismatches.
  • Packet Serialization: Implemented manual packet construction using BinaryPrimitives to match the specific 272-byte layout required by the Season 6 client.
New Features

🧠 You can now enable Memory (public preview) to help Gemini Code Assist learn from your team's feedback. This makes future code reviews more consistent and personalized to your project's style. Click here to enable Memory in your admin console.

Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize the Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counterproductive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces QuestProgressS6PlugIn, a Season 6-compatible implementation of IQuestProgressPlugIn that formats and sends quest progress data to the client. The feedback highlights several potential NullReferenceException risks, specifically when accessing kill.Monster, itemReq.Item, and virtual collections like RequiredMonsterKills, RequiredItems, and Rewards. Additionally, it is recommended to replace the placeholder GUID with a unique, randomly generated one to prevent registration conflicts.

Comment thread src/GameServer/RemoteView/Quest/QuestProgressS6PlugIn.cs Outdated
Comment thread src/GameServer/RemoteView/Quest/QuestProgressS6PlugIn.cs
Comment thread src/GameServer/RemoteView/Quest/QuestProgressS6PlugIn.cs Outdated
Comment thread src/GameServer/RemoteView/Quest/QuestProgressS6PlugIn.cs Outdated
- Replace placeholder GUID with randomly generated one (B10A7CBD-...)
- Use null-safe collection access: RequiredMonsterKills?.ToList() ?? new()
- Use null-safe monster dereference: kill.Monster?.Number ?? 0
- Guard against null Item in WriteItemReqSlot with early return pattern
@sven-n
Copy link
Copy Markdown
Member

sven-n commented Jun 1, 2026

Sorry, but this doesn't look right yet:

  1. We don't support Season 9, therefore we don't need a new plugin.
  2. Packet changes must only (with a few exceptions) be done within the ServerToClientPackets.xml and vice-versa. The packet code is auto-generated, and the resulting packet structs must be used.
  3. The quest packets may not be 100 %, see Elf soldier quest system is not working #779 (comment)
  4. This may already be fixed, see Fix QuestProgressExtended packet header overflow (C1 to C2) #789 and Fix quest dialog: accept, progress display, and cancel sven-n/MuMain#425

@sven-n sven-n closed this Jun 1, 2026
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