Add contributing guide for developers

Covers the packet handler workflow (4-file walkthrough with real examples),
ack response types, session/server context available to handlers, debug
logging and proxy setup, testing patterns, and CI pipeline.
houmgaor
2026-02-16 17:30:07 +01:00
parent 4082d82b0d
commit 8c8d5b4a2d
2 changed files with 241 additions and 0 deletions

238
Contributing.md Normal file

@@ -0,0 +1,238 @@
# Contributing
This guide covers the development workflows for working on the Erupe codebase.
## Adding a Packet Handler
This is the most common type of contribution. A new packet handler touches 4 files and optionally a 5th for code generation.
### 1. Define the packet struct
Create `network/mhfpacket/msg_mhf_your_packet.go`. Every packet implements the `MHFPacket` interface (Parse, Build, Opcode):
```go
package mhfpacket
import (
"errors"
"erupe-ce/common/byteframe"
"erupe-ce/network"
"erupe-ce/network/clientctx"
)
type MsgMhfYourPacket struct {
AckHandle uint32
SomeField uint16
}
func (m *MsgMhfYourPacket) Opcode() network.PacketID {
return network.MSG_MHF_YOUR_PACKET
}
func (m *MsgMhfYourPacket) Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
m.AckHandle = bf.ReadUint32()
m.SomeField = bf.ReadUint16()
return nil
}
func (m *MsgMhfYourPacket) Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error {
return errors.New("NOT IMPLEMENTED")
}
```
`Parse` reads fields from the binary frame in the exact order the client sends them. `Build` is the inverse — only implement it if the server needs to construct this packet type (most server→client packets reuse `MsgSysAck`).
The `AckHandle` field is present on most packets. The client sends it and expects the same value back in the ack response.
### 2. Register the packet in the opcode switch
Add a case to `network/mhfpacket/opcode_to_packet.go`:
```go
case network.MSG_MHF_YOUR_PACKET:
return &MsgMhfYourPacket{}
```
### 3. Register and implement the handler
Add to `server/channelserver/handlers_table.go`:
```go
handlerTable[network.MSG_MHF_YOUR_PACKET] = handleMsgMhfYourPacket
```
Then implement the handler in the appropriate `handlers_*.go` file. The handler signature is always `func(s *Session, p mhfpacket.MHFPacket)`:
```go
func handleMsgMhfYourPacket(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfYourPacket)
// Do work (DB queries, state changes, etc.)
result, err := s.server.db.Query("SELECT ...")
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
bf := byteframe.NewByteFrame()
bf.WriteUint32(someValue)
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
}
```
### 4. Add the packet ID constant
If this is a genuinely new packet (not an existing `reserve` slot), add it to `network/packetid.go` and regenerate the stringer:
```bash
cd Erupe/network && go generate
```
This regenerates `packetid_string.go` so debug logs show the packet name instead of a number.
### ACK Response Types
Every handler that receives an `AckHandle` must respond with one of these (defined in `handlers.go`):
| Function | When to use |
|----------|-------------|
| `doAckSimpleSucceed(s, ackHandle, data)` | Single operation succeeded |
| `doAckSimpleFail(s, ackHandle, data)` | Single operation failed |
| `doAckBufSucceed(s, ackHandle, data)` | Returning a list/buffer of results |
| `doAckBufFail(s, ackHandle, data)` | List/buffer query failed |
| `doAckEarthSucceed(s, ackHandle, frames)` | Multi-world responses (rare) |
| `stubEnumerateNoResults(s, ackHandle)` | Placeholder for unimplemented enumerate packets |
The difference between Simple and Buf is the `IsBufferResponse` flag in the ack — the client uses it to determine how to parse the response data.
### Real Example
`msg_mhf_create_guild.go``handlers_guild.go:618` is a clean end-to-end example:
1. Packet struct has `AckHandle` and `Name` fields
2. Parse reads them from the binary frame (Shift-JIS → UTF-8 conversion)
3. Handler calls `CreateGuild(s, pkt.Name)` for DB work
4. On error: `doAckSimpleFail` with an error code
5. On success: `doAckSimpleSucceed` with the new guild ID
## What Handlers Have Access To
### Session (`s *Session`)
The session represents one connected player. Key fields available in handlers:
| Field | Type | Purpose |
|-------|------|---------|
| `s.charID` | `uint32` | Character ID |
| `s.stage` | `*Stage` | Current multiplayer stage/room |
| `s.server` | `*Server` | Channel server instance |
| `s.logger` | `*zap.Logger` | Session-scoped logger |
| `s.courses` | `[]mhfcourse.Course` | Active subscription courses |
| `s.semaphore` | `*Semaphore` | Current semaphore lock ownership |
| `s.Name` | `string` | Character name |
| `s.token` | `string` | Login token |
| `s.mailList` | `[]int` | Mail ID mapping |
Key methods: `s.QueueSendMHF(packet)`, `s.isOp()`.
### Server (`s.server`)
The server holds shared state across all sessions on the channel:
| Field | Type | Purpose |
|-------|------|---------|
| `s.server.db` | `*sqlx.DB` | PostgreSQL connection |
| `s.server.erupeConfig` | `*Config` | Server configuration |
| `s.server.stages` | `map[string]*Stage` | All active stages |
| `s.server.Channels` | `[]*Server` | All channel server instances |
| `s.server.raviente` | `*Raviente` | Raviente raid state |
Key methods: `s.server.BroadcastMHF(packet, ignored)`, `s.server.FindSessionByCharID(id)`.
### ByteFrame (`byteframe.NewByteFrame()`)
Binary data serialization. Big-endian by default.
```go
// Writing a response
bf := byteframe.NewByteFrame()
bf.WriteUint32(value)
bf.WriteUint16(count)
bf.WriteBytes(rawData)
data := bf.Data()
// Reading from a packet
bf.ReadUint32()
bf.ReadNullTerminatedBytes()
bf.Seek(10, io.SeekStart)
```
## Debugging
### Packet Logging
Enable in `config.json` under `DebugOptions`:
```json
{
"LogInboundMessages": true,
"LogOutboundMessages": true,
"LogMessageData": true,
"MaxHexdumpLength": 512
}
```
This prints every packet with its opcode name, direction, timing, and hex dump. High-frequency packets (PING, TIME, NOP, POSITION_OBJECT, EXTEND_THRESHOLD, END) are filtered out automatically.
Example output:
```
[PlayerName] -> [Server]
Opcode: (Dec: 24832 Hex: 0x6100 Name: MSG_MHF_CREATE_GUILD)
Data [24 bytes]:
00000000 61 00 00 00 00 01 00 12 54 65 73 74 00 00 00 00 |a.......Test....|
```
### Proxy
Set `ProxyPort` in debug options to redirect all channel server traffic through a proxy on that port, useful for external packet inspection tools.
### Quest Debugging
Set `QuestTools: true` to enable extra quest-related logging (file loading, backporting, caching).
## Testing
```bash
go test -v ./... # All tests
go test -race ./... -timeout=10m # Race detection (mandatory)
go test -v ./server/channelserver/... # One package
go test -run TestGuildRank ./server/channelserver/... # One test
go test -coverprofile=coverage.out ./... # Coverage
```
Race detection is critical — Erupe uses goroutines extensively with shared mutable state (stages, semaphores, Raviente). The CI pipeline runs `-race` on every push.
### Integration Tests
Tests prefixed with `IntegrationTest_` or using `testing.Short()` are skipped in short mode. A separate test database is available via `docker/docker-compose.test.yml` (PostgreSQL on port 5433, tmpfs-backed for speed):
```bash
docker compose -f docker/docker-compose.test.yml up -d
```
### Test Patterns
- **Unit tests**: Table-driven with subtests (`t.Run`), test business logic in isolation
- **Handler tests**: Create mock sessions, call handler directly, verify ack response
- **Integration tests**: Full session lifecycle with simulated connections
## CI
GitHub Actions runs on push to `main`, `develop`, `fix-*`, `feature-*` branches and on PRs:
1. `go test -v ./...` (10 minute timeout)
2. `go test -race ./...` (10 minute timeout)
3. Coverage report → Codecov
4. Build Linux and Windows binaries
Linting (`golangci-lint`) is currently disabled.

@@ -73,6 +73,9 @@ docker compose up server # Start Erupe
- **[Erupe Configuration](Erupe-Configuration)** — full config.json reference (general, debug, gameplay, worlds, courses)
- **[Commands](Commands)** — in-game chat commands (general, admin, Raviente)
### Development
- **[Contributing](Contributing)** — adding packet handlers, debugging, testing, CI
### Reference
- **[Enumerations](Enumerations)** — distribution types, item types, course types, quest types and flags
- **[Client Versions](Client-Versions)** — all MHF versions from Season 1.0 to Z3.1 with dates, platforms, and ClientMode IDs