mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
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.
238
Contributing.md
Normal file
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.
|
||||
3
Home.md
3
Home.md
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user