From 64cc285fd8389afedfa4e19acce95454a53670fb Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Mon, 24 Nov 2025 18:41:37 +0100 Subject: [PATCH] doc: inline code documentation. --- network/mhfpacket/mhfpacket.go | 85 ++++++- network/mhfpacket/msg_mhf_enumerate_quest.go | 34 ++- network/mhfpacket/msg_sys_enter_stage.go | 26 +- server/channelserver/handlers_data.go | 27 ++- server/channelserver/handlers_stage.go | 48 +++- server/channelserver/handlers_table.go | 39 +++ server/channelserver/sys_channel_server.go | 241 ++++++++++++++----- server/channelserver/sys_semaphore.go | 80 ++++-- server/channelserver/sys_session.go | 164 +++++++++---- server/channelserver/sys_stage.go | 132 +++++++--- server/signserver/dbutils.go | 2 +- 11 files changed, 697 insertions(+), 181 deletions(-) diff --git a/network/mhfpacket/mhfpacket.go b/network/mhfpacket/mhfpacket.go index 7bf8ad2c4..21fcb5e57 100644 --- a/network/mhfpacket/mhfpacket.go +++ b/network/mhfpacket/mhfpacket.go @@ -1,3 +1,50 @@ +// Package mhfpacket provides Monster Hunter Frontier packet definitions and interfaces. +// +// This package contains: +// - MHFPacket interface: The common interface all packets implement +// - 400+ packet type definitions in msg_*.go files +// - Packet parsing (client -> server) and building (server -> client) logic +// - Opcode-to-packet-type mapping via FromOpcode() +// +// Packet Structure: +// +// MHF packets follow this wire format: +// [2 bytes: Opcode][N bytes: Packet-specific data][2 bytes: Footer 0x00 0x10] +// +// Each packet type defines its own structure matching the binary format expected +// by the Monster Hunter Frontier client. +// +// Implementing a New Packet: +// +// 1. Create msg_mhf_your_packet.go with packet struct +// 2. Implement Parse() to read data from ByteFrame +// 3. Implement Build() to write data to ByteFrame +// 4. Implement Opcode() to return the packet's ID +// 5. Register in opcodeToPacketMap in opcode_mapping.go +// 6. Add handler in server/channelserver/handlers_table.go +// +// Example: +// +// type MsgMhfYourPacket struct { +// AckHandle uint32 // Common field for request/response matching +// 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 { +// bf.WriteUint32(m.AckHandle) +// bf.WriteUint16(m.SomeField) +// return nil +// } package mhfpacket import ( @@ -6,22 +53,52 @@ import ( "erupe-ce/network/clientctx" ) -// Parser is the interface that wraps the Parse method. +// Parser is the interface for deserializing packets from wire format. +// +// The Parse method reads packet data from a ByteFrame (binary stream) and +// populates the packet struct's fields. It's called when a packet arrives +// from the client. +// +// Parameters: +// - bf: ByteFrame positioned after the opcode (contains packet payload) +// - ctx: Client context (version info, capabilities) for version-specific parsing +// +// Returns an error if the packet data is malformed or incomplete. type Parser interface { Parse(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error } -// Builder is the interface that wraps the Build method. +// Builder is the interface for serializing packets to wire format. +// +// The Build method writes the packet struct's fields to a ByteFrame (binary stream) +// in the format expected by the client. It's called when sending a packet to the client. +// +// Parameters: +// - bf: ByteFrame to write packet data to (opcode already written by caller) +// - ctx: Client context (version info, capabilities) for version-specific building +// +// Returns an error if serialization fails. type Builder interface { Build(bf *byteframe.ByteFrame, ctx *clientctx.ClientContext) error } -// Opcoder is the interface that wraps the Opcode method. +// Opcoder is the interface for identifying a packet's opcode. +// +// The Opcode method returns the unique packet identifier used for routing +// packets to their handlers and for packet logging/debugging. type Opcoder interface { Opcode() network.PacketID } -// MHFPacket is the interface that groups the Parse, Build, and Opcode methods. +// MHFPacket is the unified interface that all Monster Hunter Frontier packets implement. +// +// Every packet type must be able to: +// - Parse itself from binary data (Parser) +// - Build itself into binary data (Builder) +// - Identify its opcode (Opcoder) +// +// This interface allows the packet handling system to work generically across +// all packet types while maintaining type safety through type assertions in handlers. type MHFPacket interface { Parser Builder diff --git a/network/mhfpacket/msg_mhf_enumerate_quest.go b/network/mhfpacket/msg_mhf_enumerate_quest.go index 7bf1e1355..58ffb1a34 100644 --- a/network/mhfpacket/msg_mhf_enumerate_quest.go +++ b/network/mhfpacket/msg_mhf_enumerate_quest.go @@ -8,14 +8,34 @@ import ( "erupe-ce/network/clientctx" ) -// MsgMhfEnumerateQuest represents the MSG_MHF_ENUMERATE_QUEST +// MsgMhfEnumerateQuest is sent by the client to request a paginated list of available quests. +// +// This packet is used when: +// - Accessing the quest counter/board in town +// - Scrolling through quest lists +// - Switching between quest categories/worlds +// +// The server responds with quest metadata and binary file paths. The client then +// loads quest details from binary files on disk or via MSG_SYS_GET_FILE. +// +// Pagination: +// Quest lists can be very large (hundreds of quests). The client requests quests +// in batches using the Offset field: +// - Offset 0: First batch (quests 0-N) +// - Offset N: Next batch (quests N-2N) +// - Continues until server returns no more quests +// +// World Types: +// - 0: Newbie World (beginner quests) +// - 1: Normal World (standard quests) +// - 2+: Other world categories (events, special quests) type MsgMhfEnumerateQuest struct { - AckHandle uint32 - Unk0 uint8 // Hardcoded 0 in the binary - World uint8 - Counter uint16 - Offset uint16 // Increments to request following batches of quests - Unk4 uint8 // Hardcoded 0 in the binary + AckHandle uint32 // Response handle for matching server response to request + Unk0 uint8 // Hardcoded 0 in the binary (purpose unknown) + World uint8 // World ID/category to enumerate quests for + Counter uint16 // Client counter for tracking sequential requests + Offset uint16 // Pagination offset - increments by batch size for next page + Unk4 uint8 // Hardcoded 0 in the binary (purpose unknown) } // Opcode returns the ID associated with this packet type. diff --git a/network/mhfpacket/msg_sys_enter_stage.go b/network/mhfpacket/msg_sys_enter_stage.go index 35244bd62..9e305108e 100644 --- a/network/mhfpacket/msg_sys_enter_stage.go +++ b/network/mhfpacket/msg_sys_enter_stage.go @@ -9,11 +9,29 @@ import ( "erupe-ce/network/clientctx" ) -// MsgSysEnterStage represents the MSG_SYS_ENTER_STAGE +// MsgSysEnterStage is sent by the client to enter an existing stage. +// +// This packet is used when: +// - Moving from one town area to another (e.g., Mezeporta -> Pallone) +// - Joining another player's room or quest +// - Entering a persistent stage that already exists +// +// The stage must already exist on the server. For creating new stages (quests, rooms), +// use MSG_SYS_CREATE_STAGE followed by MSG_SYS_ENTER_STAGE. +// +// Stage ID Format: +// Stage IDs are encoded strings like "sl1Ns200p0a0u0" that identify specific +// game areas: +// - sl1Ns200p0a0u0: Mezeporta (main town) +// - sl1Ns211p0a0u0: Rasta bar +// - Quest stages: Dynamic IDs created when quests start +// +// After entering, the session's stage pointer is updated and the player receives +// broadcasts from other players in that stage. type MsgSysEnterStage struct { - AckHandle uint32 - UnkBool uint8 - StageID string + AckHandle uint32 // Response handle for acknowledgment + UnkBool uint8 // Boolean flag (purpose unknown, possibly force-enter) + StageID string // ID of the stage to enter (length-prefixed string) } // Opcode returns the ID associated with this packet type. diff --git a/server/channelserver/handlers_data.go b/server/channelserver/handlers_data.go index 64b14072c..8632487f2 100644 --- a/server/channelserver/handlers_data.go +++ b/server/channelserver/handlers_data.go @@ -315,31 +315,32 @@ func handleMsgMhfGetPaperData(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetPaperData) var data []byte var err error - if pkt.Unk2 == 4 { + switch pkt.Unk2 { + case 4: data, err = hex.DecodeString("0A218EAD000000000000000000000000") - } else if pkt.Unk2 == 5 { + case 5: data, err = hex.DecodeString("0A218EAD00000000000000000000003403E900010000000000000000000003E900020000000000000000000003EB00010064006400C80064000003EB00020096006400F00064000003EC000A270F002800000000000003ED000A01F4000000000000000003EF00010000000000000000000003F000C801900BB801900BB8000003F200010FA0000000000000000003F200020FA0000000000000000003F3000117703A984E2061A8753003F3000217703A984E2061A8753003F400011F40445C57E46B6C791803F400021F40445C57E46B6C791803F700010010001000100000000003F7000200100010001000000000044D000107E001F4000000000000044D000207E001F4000000000000044F0001000000000BB800000BB8044F0002000000000BB800000BB804500001000A270F00280000000004500002000A270F00280000000004510001000A01F400000000000004510002000A01F400000000000007D100010011003A0000000602BC07D100010014003A0000000300C807D100010016003A0000000700FA07D10001001B003A00000001006407D100010035003A0000000803E807D100010043003A0000000901F407D100010044003A00000002009607D10001004A003A0000000400C807D10001004B003A0000000501F407D10001004C003A0000000A032007D100010050003A0000000B038407D100010059003A0000000C025807D100020011003C0000000602BC07D100020014003C0000000300C807D100020016003C00000007015E07D10002001B003C00000001006407D100020027003C0000000D00C807D100020028003C0000000F025807D100020035003C0000000803E807D100020043003C0000000201F407D100020044003C00000009009607D10002004A003C0000000400C807D10002004B003C0000000501F407D10002004C003C0000000A032007D100020050003C0000000B038407D100020051003C0000000E038407D100020059003C0000000C025807D10002005E003C0000001003E8") - } else if pkt.Unk2 == 6 { + case 6: data, err = hex.DecodeString("0A218EAD0000000000000000000001A503EA00640000000000000000000003EE00012710271000000000000003EE000227104E2000000000000003F100140000000000000000000003F5000100010001006400C8012C03F5000100010002006400C8012C03F5000100020001012C006400C803F5000100020002012C006400C803F500010003000100C8012C006403F500010003000200C8012C006403F5000200010001012C006400C803F5000200010002012C006400C803F500020002000100C8012C006403F500020002000200C8012C006403F5000200030001006400C8012C03F5000200030002006400C8012C03F500030001000100C8012C006403F500030001000200C8012C006403F5000300020001006400C8012C03F5000300020002006400C8012C03F5000300030001012C006400C803F5000300030002012C006400C803F800010001005000000000000003F800010002005000000000000003F800010003005000000000000003F800020001005000000000000003F800020002005000000000000003F800020003005000000000000004B10001003C003200000000000004B10002003C003200000000000004B200010000000500320000000004B2000100060014003C0000000004B200010015002800460000000004B200010029007800500000000004B20001007900A0005A0000000004B2000100A100FA00640000000004B2000100FB01F400640000000004B2000101F5270F00640000000004B200020000006400640000000004B20002006500C800640000000004B2000200C901F400960000000004B2000201F5270F00960000000004B3000100000005000A0000000004B300010006000A00140000000004B30001000B001E001E0000000004B30001001F003C00280000000004B30001003D007800320000000004B3000100790082003C0000000004B300010083008C00460000000004B30001008D009600500000000004B30001009700A000550000000004B3000100A100C800640000000004B3000100C901F400640000000004B3000101F5270F00640000000004B300020000007800460000000004B30002007901F400780000000004B3000201F5270F00780000000004B4000100000005000F0000000004B400010006000A00140000000004B40001000B000F00190000000004B4000100100014001B0000000004B4000100150019001E0000000004B40001001A001E00200000000004B40001001F002800230000000004B400010029003200250000000004B400010033003C00280000000004B40001003D0046002B0000000004B4000100470050002D0000000004B400010051005A002F0000000004B40001005B006400320000000004B400010065006E003C0000000004B40001006F007800460000000004B4000100790082004B0000000004B400010083008C00520000000004B40001008D00A000550000000004B4000100A100C800640000000004B4000100C901F400640000000004B4000101F5270F00640000000004B400020000007800460000000004B40002007901F400780000000004B4000201F5270F0078000000000FA10001000000000000000000000FA10002000029AB0005000000010FA10002000029AB0005000000010FA10002000029AB0005000000010FA10002000029AB0005000000010FA10002000029AC0002000000010FA10002000029AC0002000000010FA10002000029AC0002000000010FA10002000029AC0002000000010FA10002000029AD0001000000010FA10002000029AD0001000000010FA10002000029AD0001000000010FA10002000029AD0001000000010FA10002000029AF0003000000010FA10002000029AF0003000000010FA10002000029AF0003000000010FA10002000029AF0003000000010FA10002000028900001000000010FA10002000028900001000000010FA10002000029AE0002000000010FA10002000029AE0002000000010FA10002000029BA0002000000010FA10002000029BB0002000000010FA10002000029B60001000000010FA10002000029B60001000000010FA5000100002B970001138800010FA5000100002B9800010D1600010FA5000100002B99000105DC00010FA5000100002B9A0001006400010FA5000100002B9B0001003200010FA5000200002B970002070800010FA5000200002B98000204B000010FA5000200002B99000201F400010FA5000200002B9A0001003200010FA5000200002B1D0001009600010FA5000200002B1E0001009600010FA5000200002B240001009600010FA5000200002B310001009600010FA5000200002B330001009600010FA5000200002B470001009600010FA5000200002B5A0001009600010FA5000200002B600001009600010FA5000200002B6D0001009600010FA5000200002B780001009600010FA5000200002B7D0001009600010FA5000200002B810001009600010FA5000200002B870001009600010FA5000200002B7C0001009600010FA5000200002B1F0001009600010FA5000200002B200001009600010FA5000200002B290001009600010FA5000200002B350001009600010FA5000200002B370001009600010FA5000200002B450001009600010FA5000200002B5B0001009600010FA5000200002B610001009600010FA5000200002B790001009600010FA5000200002B7A0001009600010FA5000200002B7B0001009600010FA5000200002B830001009600010FA5000200002B890001009600010FA5000200002B580001009600010FA5000200002B210001009600010FA5000200002B270001009600010FA5000200002B2E0001009600010FA5000200002B390001009600010FA5000200002B3C0001009600010FA5000200002B430001009600010FA5000200002B5C0001009600010FA5000200002B620001009600010FA5000200002B6F0001009600010FA5000200002B7F0001009600010FA5000200002B800001009600010FA5000200002B820001009600010FA5000200002B500001009600010FA50002000028820001009600010FA50002000028800001009600010FA6000100002B970001138800010FA6000100002B9800010D1600010FA6000100002B99000105DC00010FA6000100002B9A0001006400010FA6000100002B9B0001003200010FA6000200002B970002070800010FA6000200002B98000204B000010FA6000200002B99000201F400010FA6000200002B9A0001003200010FA6000200002B1D0001009600010FA6000200002B1E0001009600010FA6000200002B240001009600010FA6000200002B310001009600010FA6000200002B330001009600010FA6000200002B470001009600010FA6000200002B5A0001009600010FA6000200002B600001009600010FA6000200002B6D0001009600010FA6000200002B780001009600010FA6000200002B7D0001009600010FA6000200002B810001009600010FA6000200002B870001009600010FA6000200002B7C0001009600010FA6000200002B1F0001009600010FA6000200002B200001009600010FA6000200002B290001009600010FA6000200002B350001009600010FA6000200002B370001009600010FA6000200002B450001009600010FA6000200002B5B0001009600010FA6000200002B610001009600010FA6000200002B790001009600010FA6000200002B7A0001009600010FA6000200002B7B0001009600010FA6000200002B830001009600010FA6000200002B890001009600010FA6000200002B580001009600010FA6000200002B210001009600010FA6000200002B270001009600010FA6000200002B2E0001009600010FA6000200002B390001009600010FA6000200002B3C0001009600010FA6000200002B430001009600010FA6000200002B5C0001009600010FA6000200002B620001009600010FA6000200002B6F0001009600010FA6000200002B7F0001009600010FA6000200002B800001009600010FA6000200002B820001009600010FA6000200002B500001009600010FA60002000028820001009600010FA60002000028800001009600010FA7000100002B320001004600010FA7000100002B340001004600010FA7000100002B360001004600010FA7000100002B380001004600010FA7000100002B3A0001004600010FA7000100002B6E0001004600010FA7000100002B700001004600010FA7000100002B660001004600010FA7000100002B680001004600010FA7000100002B6A0001004600010FA7000100002B220001004600010FA7000100002B230001004600010FA7000100002B420001004600010FA7000100002B840001004600010FA7000100002B3B0001004600010FA7000100002B280001004600010FA7000100002B260001004600010FA7000100002B5F0001004600010FA7000100002B630001004600010FA7000100002B640001004600010FA7000100002B710001004600010FA7000100002B7E0001004600010FA7000100002B4C0001004600010FA7000100002B4D0001004600010FA7000100002B4E0001004600010FA7000100002B4F0001004600010FA7000100002B560001004600010FA7000100002B570001004600010FA70001000028860001004600010FA70001000028870001004600010FA70001000028880001004600010FA70001000028890001004600010FA700010000288A0001004600010FA7000100002B3D0001002D00010FA7000100002B3F0001002D00010FA7000100002B410001002D00010FA7000100002B440001002D00010FA7000100002B460001002D00010FA7000100002B6C0001002D00010FA7000100002B730001002D00010FA7000100002B770001002D00010FA7000100002B860001002D00010FA7000100002B300001002D00010FA7000100002B520001002D00010FA7000100002B590001002D00010FA700010000287F0001002D00010FA70001000028830001002D00010FA70001000028850001002D00010FA7000100002B480001000F00010FA7000100002B490001000F00010FA7000100002B4B0001000F00010FA7000100002B750001000F00010FA7000100002B550001000E00010FA7000100002B2D0001000A00010FA7000100002B8B0001000A00010FA70001000028840001000500010FA70001000028810001000100010FA7000100002B9B0001009600010FA7000100002CC90001003200010FA7000100002CCA0001001900010FA7000100002CCB000100C800010FA7000100002CCC0001019000010FA7000100002CCD0001009600010FA7000100002B1D0001005C00010FA7000100002B1E0001005C00010FA7000100002B240001005C00010FA7000100002B310001005C00010FA7000100002B330001005C00010FA7000100002B470001005C00010FA7000100002B5A0001005C00010FA7000100002B600001005C00010FA7000100002B6D0001005C00010FA7000100002B7D0001005C00010FA7000100002B810001005C00010FA7000100002B870001005C00010FA7000100002B7C0001005C00010FA7000100002B1F0001005C00010FA7000100002B200001005C00010FA7000100002B290001005C00010FA7000100002B350001005C00010FA7000100002B370001005C00010FA7000100002B450001005C00010FA7000100002B5B0001005C00010FA7000100002B610001005C00010FA7000100002B790001005C00010FA7000100002B7A0001005C00010FA7000100002B7B0001005C00010FA7000100002B830001005C00010FA7000100002B890001005B00010FA7000100002B580001005B00010FA7000100002B210001005B00010FA7000100002B270001005B00010FA7000100002B2E0001005B00010FA7000100002B390001005B00010FA7000100002B3C0001005B00010FA7000100002B430001005B00010FA7000100002B5C0001005B00010FA7000100002B620001005B00010FA7000100002B6F0001005B00010FA7000100002B7F0001005B00010FA7000100002B800001005B00010FA7000100002B820001005B00010FA7000100002B500001005B00010FA70001000028820001005B00010FA70001000028800001005B00010FA7000100002B250001005B00010FA7000100002B3E0001005B00010FA7000100002B5D0001005B00010FA7000100002B650001005B00010FA7000100002B720001005B00010FA7000100002B850001005B00010FA7000100002B2B0001005B00010FA7000100002B5E0001005B00010FA7000100002B740001005B00010FA7000100002B400001005B00010FA7000100002B4A0001005B00010FA7000100002B6B0001005B00010FA7000100002B880001005B00010FA7000100002B510001005B00010FA7000100002B530001005B00010FA7000100002B540001005B00010FA7000100002B2A0001005B00010FA7000100002B670001005B00010FA7000100002B690001005B00010FA7000100002B760001005B00010FA7000100002B2F0001005B00010FA7000100002B2C0001005B00010FA7000100002B8A0001005B00010FA7000200002B320001005A00010FA7000200002B340001005A00010FA7000200002B360001005A00010FA7000200002B380001005A00010FA7000200002B3A0001005A00010FA7000200002B6E0001005A00010FA7000200002B700001005A00010FA7000200002B660001005A00010FA7000200002B680001005A00010FA7000200002B6A0001005A00010FA7000200002B220001005A00010FA7000200002B230001005A00010FA7000200002B420001005A00010FA7000200002B840001005A00010FA7000200002B3B0001005A00010FA7000200002B280001005A00010FA7000200002B260001005A00010FA7000200002B5F0001005A00010FA7000200002B630001005A00010FA7000200002B640001005A00010FA7000200002B710001005A00010FA7000200002B7E0001005A00010FA7000200002B4C0001005A00010FA7000200002B4D0001005A00010FA7000200002B4E0001005A00010FA7000200002B4F0001005A00010FA7000200002B560001005A00010FA7000200002B570001005A00010FA70002000028860001005A00010FA70002000028870001005A00010FA70002000028880001005A00010FA70002000028890001005A00010FA700020000288A0001005A00010FA7000200002B3D0001005000010FA7000200002B3F0001005000010FA7000200002B410001005000010FA7000200002B440001005000010FA7000200002B460001005000010FA7000200002B6C0001005000010FA7000200002B730001005000010FA7000200002B770001005000010FA7000200002B860001005000010FA7000200002B300001005000010FA7000200002B520001005000010FA7000200002B590001005000010FA700020000287F0001005000010FA70002000028830001005000010FA70002000028850001005000010FA7000200002B480001001600010FA7000200002B490001001600010FA7000200002B4B0001001600010FA7000200002B750001001600010FA7000200002B550001001600010FA7000200002B2D0001000F00010FA7000200002B8B0001000F00010FA70002000028840001000800010FA70002000028810001000200010FA7000200002B97000304C400010FA7000200002B980003028A00010FA7000200002B99000300A000010FA7000200002D8D0001032000010FA7000200002D8E0001032000010FA7000200002B9B000101F400010FA7000200002B9A0001022600010FA7000200002CC90001003200010FA7000200002CCA0001001900010FA7000200002CCB000100FA00010FA7000200002CCC000101F400010FA7000200002CCD000100AF0001106A000100002B9B000117700001106A000100002CC9000100C80001106A000100002CCA000100640001106A000100002CCB000103E80001106A000100002CCC000107D00001106A000100002CCD000102BC0001106A000200002D8D000103200001106A000200002D8E000103200001106A000200002B9B000101900001106A000200002CC9000101900001106A000200002CCA000100C80001106A000200002CCB000107D00001106A000200002CCC00010FA00001106A000200002CCD000105780001") - } else if pkt.Unk2 == 6001 { + case 6001: data, err = hex.DecodeString("0A218EAD0000000000000000000000052B97010113882B9801010D162B99010105DC2B9A010100642B9B01010032") - } else if pkt.Unk2 == 6002 { + case 6002: data, err = hex.DecodeString("0A218EAD00000000000000000000002F2B97020107082B98020104B02B99020101F42B9A010100322B1D010100962B1E010100962B24010100962B31010100962B33010100962B47010100962B5A010100962B60010100962B6D010100962B78010100962B7D010100962B81010100962B87010100962B7C010100962B1F010100962B20010100962B29010100962B35010100962B37010100962B45010100962B5B010100962B61010100962B79010100962B7A010100962B7B010100962B83010100962B89010100962B58010100962B21010100962B27010100962B2E010100962B39010100962B3C010100962B43010100962B5C010100962B62010100962B6F010100962B7F010100962B80010100962B82010100962B5001010096288201010096288001010096") - } else if pkt.Unk2 == 6010 { + case 6010: data, err = hex.DecodeString("0A218EAD00000000000000000000000B2B9701010E742B9801010B542B99010105142CBD010100FA2CBE010100FA2F17010100FA2F21010100FA2F1A010100FA2F24010100FA2DFE010100C82DFD01010190") - } else if pkt.Unk2 == 6011 { + case 6011: data, err = hex.DecodeString("0A218EAD00000000000000000000000B2B9701010E742B9801010B542B99010105142CBD010100FA2CBE010100FA2F17010100FA2F21010100FA2F1A010100FA2F24010100FA2DFE010100C82DFD01010190") - } else if pkt.Unk2 == 6012 { + case 6012: data, err = hex.DecodeString("0A218EAD00000000000000000000000D2B9702010DAC2B9802010B542B990201051430DC010101902CBD010100C82CBE010100C82F17010100C82F21010100C82F1A010100C82F24010100C82DFF010101902E00010100C82E0101010064") - } else if pkt.Unk2 == 7001 { + case 7001: data, err = hex.DecodeString("0A218EAD00000000000000000000009D2B1D010101222B1E0101010E2B240101010E2B31010101222B33010101222B47010101222B5A010101182B600101012C2B6D010101182B78010101222B7D010101222B810101012C2B87010101222B7C0101010E2B220101002F2B250101002F2B380101002F2B360101002F2B3E010100302B5D0101002F2B640101002F2B650101002F2B700101002F2B720101002F2B7E0101002F2B850101002F2B4C0101002F2B4F0101002F2B560101002F28860101002F28870101002F2B2B010100112B3F010100102B44010100102B5E010100112B74010100112B52010100112B97010104B02B970201028A2B98010103202B980201012C2B99010100642B99020100322B9C010100642B9A010100642B9B010100642B960101012C2CC70101012C2C5C0101012C2CC80101012C2C5D010101F42B1F0102012C2B200102010E2B290102012C2B35010201222B37010201222B45010201222B5B010201182B610102012C2B79010200FA2B7A0102012C2B7B010201182B83010201222B89010201042B580102012C2B260102002F2B3A0102002F2B3B0102002F2B400102002F2B4A0102002F2B5F0102002F2B660102002F2B680102002F2B6A0102002F2B6B0102002F2B710102002F2B88010200302B4D0102002F2B510102002F2B530102002F28880102002F28890102002F2B77010200112B3D010200112B86010200112B46010200112B30010200102B54010200102B97010204B02B970202028A2B98010203202B980202012C2B99010200642B99020200322B9C010200642B9A010200642B9B010200642B960102012C2CC70102012C2C5C0102012C2CC80102012C2C5D010201F42B210103010A2B270103010A2B2E0103010A2B390103010A2B3C0103010A2B430103010A2B5C0103010A2B620103010A2B6F0103010A2B7F0103010C2B800103010C2B820103010C2B500103010C28820103010A28800103010C2B23010300322B28010300322B2A010300322B32010300322B34010300322B42010300322B63010300322B67010300322B69010300322B6E010300322B76010300322B84010300322B4E010300322B57010300322B2F01030032288A010300322B2C0103000F2B410103000F2B8A0103000F2B6C0103000F2B730103000F2B590103000F287F0103000F28830103000F28850103000F2A1A010301772BC9010301772A3D010301772C7D010301772B97010303E82B97020300FA2B98010302BC2B98020300AF2B990103012C2B990203004B2CC9010300352CCA0103001B2CCB0103010A2CCC010302152CCD010300BA") - } else if pkt.Unk2 == 7002 { + case 7002: data, err = hex.DecodeString("0A218EAD0000000000000000000000B92B1D010100642B1E010100642B24010100642B31010100642B33010100642B47010100642B5A010100642B60010100642B6D010100642B78010100642B7D010100642B81010100642B87010100642B7C010100642B220101003C2B250101003C2B380101003C2B360101003C2B3E0101003C2B5D0101003C2B640101003C2B650101003C2B700101003C2B720101003C2B7E0101003C2B850101003C2B4C0101003C2B4F0101003C2B560101003C28860101003C28870101003C2B2B010100142B3F010100142B44010100142B5E010100142B74010100142B52010100142B9C010101902B9A010100C82B9B010100C82CC7010100642CC80101009628730101009630DA010100C830DB0101012C30DC01010384353D0101015E353C010100C82C5C010100642C5D010100962EEE010100FA2EF0010101902EEF0101019A2B97020101F42B97040101F42B97060101F42B98020101902B98040101902B98060101902B99020100642B99040100642B99060100642B1F010200642B20010200642B29010200642B35010200642B37010200642B45010200642B5B010200642B61010200642B79010200642B7A010200642B7B010200642B83010200642B89010200642B58010200642B260102003C2B3A0102003C2B3B0102003C2B400102003C2B4A0102003C2B5F0102003C2B660102003C2B680102003C2B6A0102003C2B6B0102003C2B710102003C2B880102003C2B4D0102003C2B510102003C2B530102003C28880102003C28890102003C2B77010200142B3D010200142B86010200142B46010200142B30010200142B54010200142B9C010201902B9A010200C82B9B010200C82CC7010200FA2CC80102015E30DA0102009630DB010200C830DC0102015E353D010200FA353C010200C82873010201902B96010200642C5C010200642C5D010200642EEE0102012C2EF0010201C22EEF010201CC2B97020201F42B97040201F42B97060201F42B98020201902B98040201902B98060201902B99020200642B99040200642B99060200642B21010300782B27010300782B2E010300782B39010300782B3C010300782B43010300782B5C010300782B62010300782B6F010300782B7F010300782B80010300782B82010300782B50010300782882010300782880010300782B23010300412B28010300412B2A010300412B32010300412B34010300412B42010300412B63010300412B67010300412B69010300412B6E010300412B76010300412B84010300412B4E010300412B57010300412B2F01030041288A010300412B2C0103000F2B410103000F2B8A0103000F2B6C0103000F2B730103000F2B590103000F287F0103000F28830103000F28850103000F2A1A030301EA2BC9030301EA2A3D030301EA2C7D030301EA2F0E030301F430D7030301F42B97020301F42B97040301F42B97060301F42B98020301902B98040301902B98060301902B99020300642B99040300642B99060300642CC9010300352CCA0103001B2CCB0103010A2CCC010302152CCD010300BA") - } else if pkt.Unk2 == 7011 { + case 7011: data, err = hex.DecodeString("0A218EAD00000000000000000000009D2B1D010101222B1E0101010E2B240101010E2B31010101222B33010101222B47010101222B5A010101182B600101012C2B6D010101182B78010101222B7D010101222B810101012C2B87010101222B7C0101010E2B220101002F2B250101002F2B380101002F2B360101002F2B3E010100302B5D0101002F2B640101002F2B650101002F2B700101002F2B720101002F2B7E0101002F2B850101002F2B4C0101002F2B4F0101002F2B560101002F28860101002F28870101002F2B2B010100112B3F010100102B44010100102B5E010100112B74010100112B52010100112B97010104B02B970201028A2B98010103202B980201012C2B99010100642B99020100322B9C010100642B9A010100642B9B010100642B960101012C2CC70101012C2C5C0101012C2CC80101012C2C5D010101F42B1F0102012C2B200102010E2B290102012C2B35010201222B37010201222B45010201222B5B010201182B610102012C2B79010200FA2B7A0102012C2B7B010201182B83010201222B89010201042B580102012C2B260102002F2B3A0102002F2B3B0102002F2B400102002F2B4A0102002F2B5F0102002F2B660102002F2B680102002F2B6A0102002F2B6B0102002F2B710102002F2B88010200302B4D0102002F2B510102002F2B530102002F28880102002F28890102002F2B77010200112B3D010200112B86010200112B46010200112B30010200102B54010200102B97010204B02B970202028A2B98010203202B980202012C2B99010200642B99020200322B9C010200642B9A010200642B9B010200642B960102012C2CC70102012C2C5C0102012C2CC80102012C2C5D010201F42B210103010A2B270103010A2B2E0103010A2B390103010A2B3C0103010A2B430103010A2B5C0103010A2B620103010A2B6F0103010A2B7F0103010C2B800103010C2B820103010C2B500103010C28820103010A28800103010C2B23010300322B28010300322B2A010300322B32010300322B34010300322B42010300322B63010300322B67010300322B69010300322B6E010300322B76010300322B84010300322B4E010300322B57010300322B2F01030032288A010300322B2C0103000F2B410103000F2B8A0103000F2B6C0103000F2B730103000F2B590103000F287F0103000F28830103000F28850103000F2A1A010301772BC9010301772A3D010301772C7D010301772B97010303E82B97020300FA2B98010302BC2B98020300AF2B990103012C2B990203004B2CC9010300352CCA0103001B2CCB0103010A2CCC010302152CCD010300BA") - } else if pkt.Unk2 == 7012 { + case 7012: data, err = hex.DecodeString("0A218EAD00000000000000000000009D2B1D010101222B1E0101010E2B240101010E2B31010101222B33010101222B47010101222B5A010101182B600101012C2B6D010101182B78010101222B7D010101222B810101012C2B87010101222B7C0101010E2B220101002F2B250101002F2B380101002F2B360101002F2B3E010100302B5D0101002F2B640101002F2B650101002F2B700101002F2B720101002F2B7E0101002F2B850101002F2B4C0101002F2B4F0101002F2B560101002F28860101002F28870101002F2B2B010100112B3F010100102B44010100102B5E010100112B74010100112B52010100112B97010104B02B970201028A2B98010103202B980201012C2B99010100642B99020100322B9C010100642B9A010100642B9B010100642B960101012C2CC70101012C2C5C0101012C2CC80101012C2C5D010101F42B1F0102012C2B200102010E2B290102012C2B35010201222B37010201222B45010201222B5B010201182B610102012C2B79010200FA2B7A0102012C2B7B010201182B83010201222B89010201042B580102012C2B260102002F2B3A0102002F2B3B0102002F2B400102002F2B4A0102002F2B5F0102002F2B660102002F2B680102002F2B6A0102002F2B6B0102002F2B710102002F2B88010200302B4D0102002F2B510102002F2B530102002F28880102002F28890102002F2B77010200112B3D010200112B86010200112B46010200112B30010200102B54010200102B97010204B02B970202028A2B98010203202B980202012C2B99010200642B99020200322B9C010200642B9A010200642B9B010200642B960102012C2CC70102012C2C5C0102012C2CC80102012C2C5D010201F42B210103010A2B270103010A2B2E0103010A2B390103010A2B3C0103010A2B430103010A2B5C0103010A2B620103010A2B6F0103010A2B7F0103010C2B800103010C2B820103010C2B500103010C28820103010A28800103010C2B23010300322B28010300322B2A010300322B32010300322B34010300322B42010300322B63010300322B67010300322B69010300322B6E010300322B76010300322B84010300322B4E010300322B57010300322B2F01030032288A010300322B2C0103000F2B410103000F2B8A0103000F2B6C0103000F2B730103000F2B590103000F287F0103000F28830103000F28850103000F2A1A010301772BC9010301772A3D010301772C7D010301772B97010303E82B97020300FA2B98010302BC2B98020300AF2B990103012C2B990203004B2CC9010300352CCA0103001B2CCB0103010A2CCC010302152CCD010300BA") - } else { + default: data = []byte{0x00, 0x00, 0x00, 0x00} s.logger.Info("GET_PAPER request for unknown type") } diff --git a/server/channelserver/handlers_stage.go b/server/channelserver/handlers_stage.go index e411365d3..8ee77478c 100644 --- a/server/channelserver/handlers_stage.go +++ b/server/channelserver/handlers_stage.go @@ -11,11 +11,28 @@ import ( "go.uber.org/zap" ) +// handleMsgSysCreateStage creates a new stage (room/quest instance). +// +// This is called when a player: +// - Posts a quest +// - Creates a private room +// - Initiates any activity requiring a new stage instance +// +// The handler: +// 1. Checks if stage already exists (return failure if it does) +// 2. Creates new stage with the requesting session as host +// 3. Sets max player count from packet +// 4. Adds stage to server's stage map +// 5. Responds with success/failure +// +// Note: This only creates the stage; the player must call MSG_SYS_ENTER_STAGE +// to actually enter it after creation. func handleMsgSysCreateStage(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysCreateStage) s.server.Lock() defer s.server.Unlock() if _, exists := s.server.stages[pkt.StageID]; exists { + // Stage already exists, cannot create duplicate doAckSimpleFail(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) } else { stage := NewStage(pkt.StageID) @@ -28,6 +45,27 @@ func handleMsgSysCreateStage(s *Session, p mhfpacket.MHFPacket) { func handleMsgSysStageDestruct(s *Session, p mhfpacket.MHFPacket) {} +// doStageTransfer handles the common logic for entering/moving to a stage. +// +// This is a helper function called by handleMsgSysEnterStage and handleMsgSysMoveStage. +// It performs the full stage entry process: +// +// 1. Find or create the target stage +// 2. Add session to the stage's client map +// 3. Remove session from previous stage (if any) +// 4. Update session's stage pointers +// 5. Send cleanup command to client (clear old stage objects) +// 6. Send acknowledgment +// 7. Synchronize existing stage objects to the new player +// 8. Notify other players in the stage about new player +// +// If the stage doesn't exist, it creates it automatically (for persistent town stages). +// For quest stages, MSG_SYS_CREATE_STAGE should be called first. +// +// Parameters: +// - s: The session entering the stage +// - ackHandle: The ack handle to respond to +// - stageID: The stage ID to enter func doStageTransfer(s *Session, ackHandle uint32, stageID string) { s.server.Lock() stage, exists := s.server.stages[stageID] @@ -37,7 +75,7 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) { stage.Lock() stage.clients[s] = s.charID stage.Unlock() - } else { // Create new stage object + } else { // Create new stage object (for persistent stages like towns) s.server.Lock() s.server.stages[stageID] = NewStage(stageID) stage = s.server.stages[stageID] @@ -48,21 +86,21 @@ func doStageTransfer(s *Session, ackHandle uint32, stageID string) { stage.Unlock() } - // Ensure this session no longer belongs to reservations. + // Ensure this session no longer belongs to their previous stage if s.stage != nil { removeSessionFromStage(s) } - // Save our new stage ID and pointer to the new stage itself. + // Save our new stage ID and pointer to the new stage itself s.Lock() s.stageID = stageID s.stage = s.server.stages[stageID] s.Unlock() - // Tell the client to cleanup its current stage objects. + // Tell the client to cleanup its current stage objects s.QueueSendMHF(&mhfpacket.MsgSysCleanupObject{}) - // Confirm the stage entry. + // Confirm the stage entry doAckSimpleSucceed(s, ackHandle, []byte{0x00, 0x00, 0x00, 0x00}) var temp mhfpacket.MHFPacket diff --git a/server/channelserver/handlers_table.go b/server/channelserver/handlers_table.go index db46ad689..952798077 100644 --- a/server/channelserver/handlers_table.go +++ b/server/channelserver/handlers_table.go @@ -5,10 +5,49 @@ import ( "erupe-ce/network/mhfpacket" ) +// handlerFunc is the signature for all packet handler functions. +// +// Handler functions are called when a packet with a matching opcode is received. +// They process the packet and typically respond using the session's Queue methods. +// +// Parameters: +// - s: The session that received the packet (contains player state, connection) +// - p: The parsed packet (must be type-asserted to the specific packet type) +// +// Handler functions should: +// 1. Type-assert the packet to its specific type +// 2. Validate the packet data and session state +// 3. Perform the requested operation (database query, state change, etc.) +// 4. Send a response using doAckBufSucceed/Fail or s.QueueSendMHF +// 5. Handle errors gracefully (log and send error response to client) type handlerFunc func(s *Session, p mhfpacket.MHFPacket) +// handlerTable maps packet opcodes to their handler functions. +// +// This is the central routing table for all incoming packets. When a packet +// arrives, the session's handlePacketGroup() function: +// 1. Reads the opcode from the packet header +// 2. Looks up the handler in this table +// 3. Calls the handler with the session and parsed packet +// +// The table is initialized in init() and contains ~400+ packet handlers covering: +// - System packets (MSG_SYS_*): Connection, stages, objects, semaphores +// - MHF packets (MSG_MHF_*): Game features (quests, guilds, items, events) +// - CA packets (MSG_CA_*): Caravan system +// +// If a packet has no registered handler, it's ignored (logged in dev mode). var handlerTable map[network.PacketID]handlerFunc +// init registers all packet handlers in the handlerTable. +// +// Handlers are organized by feature: +// - handlers_*.go files implement related handler functions +// - This init function registers them all in the central table +// +// Adding a new handler: +// 1. Implement handleMsgYourPacket() in appropriate handlers_*.go file +// 2. Add registration here: handlerTable[network.MSG_YOUR_PACKET] = handleMsgYourPacket +// 3. Define the packet structure in network/mhfpacket/msg_*.go func init() { handlerTable = make(map[network.PacketID]handlerFunc) handlerTable[network.MSG_HEAD] = handleMsgHead diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 309ed1af8..efbf9684e 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -1,3 +1,29 @@ +// Package channelserver implements the Monster Hunter Frontier channel server. +// +// The channel server is the core gameplay component that handles actual game sessions, +// quests, player interactions, and all in-game activities. It uses a stage-based +// architecture where players move between stages (game areas/rooms) and interact +// with other players in real-time. +// +// Architecture Overview: +// +// The channel server manages three primary concepts: +// - Sessions: Individual player connections with their state and packet queues +// - Stages: Game rooms/areas where players interact (towns, quests, lobbies) +// - Semaphores: Resource locks for coordinating multiplayer activities (quests, events) +// +// Multiple channel servers can run simultaneously on different ports, allowing +// horizontal scaling and separation of different world types (Newbie, Normal, etc). +// +// Thread Safety: +// +// This package extensively uses goroutines and shared state. All shared resources +// are protected by mutexes. When modifying code, always consider thread safety: +// - Server-level: s.Lock() / s.Unlock() for session map +// - Stage-level: s.stagesLock.RLock() / s.stagesLock.Lock() for stage map +// - Session-level: session.Lock() / session.Unlock() for session state +// +// Use 'go test -race ./...' to detect race conditions during development. package channelserver import ( @@ -16,91 +42,120 @@ import ( "go.uber.org/zap" ) -// Config struct allows configuring the server. +// Config holds configuration parameters for creating a new channel server. type Config struct { - ID uint16 - Logger *zap.Logger - DB *sqlx.DB - DiscordBot *discordbot.DiscordBot - ErupeConfig *config.Config - Name string - Enable bool + ID uint16 // Channel server ID (unique identifier) + Logger *zap.Logger // Logger instance for this channel server + DB *sqlx.DB // Database connection pool + DiscordBot *discordbot.DiscordBot // Optional Discord bot for chat integration + ErupeConfig *config.Config // Global Erupe configuration + Name string // Display name for the server (shown in broadcasts) + Enable bool // Whether this server is enabled } -// Map key type for a user binary part. +// userBinaryPartID is a composite key for identifying a specific part of a user's binary data. +// User binary data is split into multiple indexed parts and stored separately. type userBinaryPartID struct { - charID uint32 - index uint8 + charID uint32 // Character ID who owns this binary data + index uint8 // Part index (binary data is chunked into multiple parts) } -// Server is a MHF channel server. +// Server represents a Monster Hunter Frontier channel server instance. +// +// The Server manages all active player sessions, game stages, and shared resources. +// It runs two main goroutines: one for accepting connections and one for managing +// the session lifecycle. +// +// Thread Safety: +// Server embeds sync.Mutex for protecting the sessions map. Use Lock()/Unlock() +// when reading or modifying s.sessions. The stages map uses a separate RWMutex +// (stagesLock) to allow concurrent reads during normal gameplay. type Server struct { - sync.Mutex - Channels []*Server - ID uint16 - GlobalID string - IP string - Port uint16 - logger *zap.Logger - db *sqlx.DB - erupeConfig *config.Config - acceptConns chan net.Conn - deleteConns chan net.Conn - sessions map[net.Conn]*Session - listener net.Listener // Listener that is created when Server.Start is called. - isShuttingDown bool + sync.Mutex // Protects sessions map and isShuttingDown flag - stagesLock sync.RWMutex - stages map[string]*Stage + // Server identity and network configuration + Channels []*Server // Reference to all channel servers (for world broadcasts) + ID uint16 // This server's ID + GlobalID string // Global identifier string + IP string // Server IP address + Port uint16 // Server listening port - // Used to map different languages - dict map[string]string + // Core dependencies + logger *zap.Logger // Logger instance + db *sqlx.DB // Database connection pool + erupeConfig *config.Config // Global configuration - // UserBinary - userBinaryPartsLock sync.RWMutex - userBinaryParts map[userBinaryPartID][]byte + // Connection management + acceptConns chan net.Conn // Channel for new accepted connections + deleteConns chan net.Conn // Channel for connections to be cleaned up + sessions map[net.Conn]*Session // Active sessions keyed by connection + listener net.Listener // TCP listener (created when Server.Start is called) + isShuttingDown bool // Shutdown flag to stop goroutines gracefully - // Semaphore - semaphoreLock sync.RWMutex - semaphore map[string]*Semaphore - semaphoreIndex uint32 + // Stage (game room) management + stagesLock sync.RWMutex // Protects stages map (RWMutex for concurrent reads) + stages map[string]*Stage // Active stages keyed by stage ID string - // Discord chat integration - discordBot *discordbot.DiscordBot + // Localization + dict map[string]string // Language string mappings for server messages - name string + // User binary data storage + // Binary data is player-specific custom data that the client stores on the server + userBinaryPartsLock sync.RWMutex // Protects userBinaryParts map + userBinaryParts map[userBinaryPartID][]byte // Chunked binary data by character - raviente *Raviente + // Semaphore (multiplayer coordination) management + semaphoreLock sync.RWMutex // Protects semaphore map and semaphoreIndex + semaphore map[string]*Semaphore // Active semaphores keyed by semaphore ID + semaphoreIndex uint32 // Auto-incrementing ID for new semaphores (starts at 7) + + // Optional integrations + discordBot *discordbot.DiscordBot // Discord bot for chat relay (nil if disabled) + name string // Server display name (used in chat messages) + + // Special event system: Raviente (large-scale multiplayer raid) + raviente *Raviente // Raviente event state and coordination } +// Raviente manages the Raviente raid event, a large-scale multiplayer encounter. +// +// Raviente is a special monster that requires coordination between many players +// across multiple phases. This struct tracks registration, event state, and +// support/assistance data for the active Raviente encounter. type Raviente struct { - sync.Mutex + sync.Mutex // Protects all Raviente data during concurrent access - register *RavienteRegister - state *RavienteState - support *RavienteSupport + register *RavienteRegister // Player registration and event timing + state *RavienteState // Current state of the Raviente encounter + support *RavienteSupport // Support/assistance tracking data } +// RavienteRegister tracks player registration and timing for Raviente events. type RavienteRegister struct { - nextTime uint32 - startTime uint32 - postTime uint32 - killedTime uint32 - ravienteType uint32 - maxPlayers uint32 - carveQuest uint32 - register []uint32 + nextTime uint32 // Timestamp for next Raviente event + startTime uint32 // Event start timestamp + postTime uint32 // Event post-completion timestamp + killedTime uint32 // Timestamp when Raviente was defeated + ravienteType uint32 // Raviente variant (2=Berserk, 3=Extreme, 4=Extreme Limited, 5=Berserk Small) + maxPlayers uint32 // Maximum players allowed (determines scaling) + carveQuest uint32 // Quest ID for carving phase after defeat + register []uint32 // List of registered player IDs (up to 5 slots) } +// RavienteState holds the dynamic state data for an active Raviente encounter. +// The state array contains 29 uint32 values tracking encounter progress. type RavienteState struct { - stateData []uint32 + stateData []uint32 // Raviente encounter state (29 values) } +// RavienteSupport tracks support and assistance data for Raviente encounters. +// The support array contains 25 uint32 values for coordination features. type RavienteSupport struct { - supportData []uint32 + supportData []uint32 // Support/assistance data (25 values) } -// Set up the Raviente variables for the server +// NewRaviente creates and initializes a new Raviente event manager with default values. +// All state and support arrays are initialized to zero, ready for a new event. func NewRaviente() *Raviente { ravienteRegister := &RavienteRegister{ nextTime: 0, @@ -125,6 +180,15 @@ func NewRaviente() *Raviente { return raviente } +// GetRaviMultiplier calculates the difficulty multiplier for Raviente based on player count. +// +// Raviente scales its difficulty based on the number of active participants. If there +// are fewer players than the minimum threshold, the encounter becomes easier by returning +// a multiplier < 1. Returns 1.0 for full groups, or 0 if the semaphore doesn't exist. +// +// Minimum player thresholds: +// - Large Raviente (maxPlayers > 8): 24 players minimum +// - Small Raviente (maxPlayers <= 8): 4 players minimum func (r *Raviente) GetRaviMultiplier(s *Server) float64 { raviSema := getRaviSemaphore(s) if raviSema != nil { @@ -142,7 +206,19 @@ func (r *Raviente) GetRaviMultiplier(s *Server) float64 { return 0 } -// NewServer creates a new Server type. +// NewServer creates and initializes a new channel server with the given configuration. +// +// The server is initialized with default persistent stages (town areas that always exist): +// - sl1Ns200p0a0u0: Mezeporta (main town) +// - sl1Ns211p0a0u0: Rasta bar +// - sl1Ns260p0a0u0: Pallone Caravan +// - sl1Ns262p0a0u0: Pallone Guest House 1st Floor +// - sl1Ns263p0a0u0: Pallone Guest House 2nd Floor +// - sl2Ns379p0a0u0: Diva fountain / prayer fountain +// - sl1Ns462p0a0u0: MezFes (festival area) +// +// Additional dynamic stages are created by players when they create quests or rooms. +// The semaphore index starts at 7 to avoid reserved IDs 0-6. func NewServer(config *Config) *Server { s := &Server{ ID: config.ID, @@ -187,7 +263,16 @@ func NewServer(config *Config) *Server { return s } -// Start starts the server in a new goroutine. +// Start begins listening for connections and starts the server's main goroutines. +// +// This method: +// 1. Creates a TCP listener on the configured port +// 2. Launches acceptClients() goroutine to accept new connections +// 3. Launches manageSessions() goroutine to handle session lifecycle +// 4. Optionally starts Discord chat integration +// +// Returns an error if the listener cannot be created (e.g., port in use). +// The server runs asynchronously after Start() returns successfully. func (s *Server) Start() error { l, err := net.Listen("tcp", fmt.Sprintf(":%d", s.Port)) if err != nil { @@ -206,7 +291,15 @@ func (s *Server) Start() error { return nil } -// Shutdown tries to shut down the server gracefully. +// Shutdown gracefully stops the server and all its goroutines. +// +// This method: +// 1. Sets the shutdown flag to stop accepting new connections +// 2. Closes the TCP listener (causes acceptClients to exit) +// 3. Closes the acceptConns channel (signals manageSessions to exit) +// +// Existing sessions are not forcibly disconnected but will eventually timeout +// or disconnect naturally. For a complete shutdown, wait for all sessions to close. func (s *Server) Shutdown() { s.Lock() s.isShuttingDown = true @@ -267,7 +360,17 @@ func (s *Server) manageSessions() { } } -// BroadcastMHF queues a MHFPacket to be sent to all sessions. +// BroadcastMHF sends a packet to all active sessions on this channel server. +// +// The packet is built individually for each session to handle per-session state +// (like client version differences). Packets are queued in a non-blocking manner, +// so if a session's queue is full, the packet is dropped for that session only. +// +// Parameters: +// - pkt: The MHFPacket to broadcast to all sessions +// - ignoredSession: Optional session to exclude from the broadcast (typically the sender) +// +// Thread Safety: This method locks the server's session map during iteration. func (s *Server) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) { // Broadcast the data. s.Lock() @@ -289,6 +392,16 @@ func (s *Server) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) } } +// WorldcastMHF broadcasts a packet to all channel servers (world-wide broadcast). +// +// This is used for server-wide announcements like Raviente events that should be +// visible to all players across all channels. The packet is sent to every channel +// server except the one specified in ignoredChannel. +// +// Parameters: +// - pkt: The MHFPacket to broadcast across all channels +// - ignoredSession: Optional session to exclude from broadcasts +// - ignoredChannel: Optional channel server to skip (typically the originating channel) func (s *Server) WorldcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session, ignoredChannel *Server) { for _, c := range s.Channels { if c == ignoredChannel { @@ -298,7 +411,13 @@ func (s *Server) WorldcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session, } } -// BroadcastChatMessage broadcasts a simple chat message to all the sessions. +// BroadcastChatMessage sends a simple chat message to all sessions on this server. +// +// The message appears as a system message with the server's configured name as the sender. +// This is typically used for server announcements, maintenance notifications, or events. +// +// Parameters: +// - message: The text message to broadcast to all players func (s *Server) BroadcastChatMessage(message string) { bf := byteframe.NewByteFrame() bf.SetLE() diff --git a/server/channelserver/sys_semaphore.go b/server/channelserver/sys_semaphore.go index 369e481b6..0650e384a 100644 --- a/server/channelserver/sys_semaphore.go +++ b/server/channelserver/sys_semaphore.go @@ -7,27 +7,62 @@ import ( "sync" ) -// Stage holds stage-specific information +// Semaphore is a multiplayer coordination mechanism for quests and events. +// +// Despite the name, Semaphore is NOT an OS synchronization primitive (like sync.Semaphore). +// Instead, it's a game-specific resource lock that coordinates multiplayer activities where: +// - Players must acquire a semaphore before participating +// - A limited number of participants are allowed (maxPlayers) +// - The semaphore tracks both active and reserved participants +// +// Use Cases: +// - Quest coordination: Ensures quest party size limits are enforced +// - Event coordination: Raviente, VS Tournament, Diva Defense +// - Global resources: Prevents multiple groups from starting conflicting events +// +// Semaphore vs Stage: +// - Stages are spatial (game rooms, areas). Players in a stage can see each other. +// - Semaphores are logical (coordination locks). Players in a semaphore are +// participating in the same activity but may be in different stages. +// +// Example: Raviente Event +// - Players acquire the Raviente semaphore to register for the event +// - Multiple quest stages exist (preparation, phase 1, phase 2, carving) +// - All participants share the same semaphore across different stages +// - The semaphore enforces the 32-player limit across all stages +// +// Thread Safety: +// Semaphore embeds sync.RWMutex. Use RLock for reads and Lock for writes. type Semaphore struct { - sync.RWMutex + sync.RWMutex // Protects semaphore state during concurrent access - // Stage ID string - id_semaphore string + // Semaphore identity + id_semaphore string // Semaphore ID string (identifies the resource/activity) + id uint32 // Numeric ID for client communication (auto-generated, starts at 7) - id uint32 + // Active participants + clients map[*Session]uint32 // Sessions actively using this semaphore -> character ID - // Map of session -> charID. - // These are clients that are CURRENTLY in the stage - clients map[*Session]uint32 + // Reserved slots + // Players who have acquired the semaphore but may not be actively in the stage yet. + // The value is always nil; only the key (charID) matters. This is a set implementation. + reservedClientSlots map[uint32]interface{} // Character ID -> nil (set of reserved IDs) - // Map of charID -> interface{}, only the key is used, value is always nil. - reservedClientSlots map[uint32]interface{} - - // Max Players for Semaphore - maxPlayers uint16 + // Capacity + maxPlayers uint16 // Maximum concurrent participants (e.g., 4 for quests, 32 for Raviente) } -// NewStage creates a new stage with intialized values. +// NewSemaphore creates and initializes a new Semaphore for coordinating an activity. +// +// The semaphore is assigned an auto-incrementing ID from the server's semaphoreIndex. +// IDs 0-6 are reserved, so the first semaphore gets ID 7. +// +// Parameters: +// - s: The server (used to generate unique semaphore ID) +// - ID: Semaphore ID string (identifies the activity/resource) +// - MaxPlayers: Maximum participants allowed +// +// Returns a new Semaphore ready for client acquisition. func NewSemaphore(s *Server, ID string, MaxPlayers uint16) *Semaphore { sema := &Semaphore{ id_semaphore: ID, @@ -55,7 +90,22 @@ func (s *Semaphore) BroadcastRavi(pkt mhfpacket.MHFPacket) { } } -// BroadcastMHF queues a MHFPacket to be sent to all sessions in the stage. +// BroadcastMHF sends a packet to all active participants in the semaphore. +// +// This is used for event-wide announcements that all participants need to see, +// regardless of which stage they're currently in. Examples: +// - Raviente phase changes +// - Tournament updates +// - Event completion notifications +// +// Only active clients (in the clients map) receive broadcasts. Reserved clients +// who haven't fully joined yet are excluded. +// +// Parameters: +// - pkt: The MHFPacket to broadcast to all participants +// - ignoredSession: Optional session to exclude from broadcast +// +// Thread Safety: Caller should hold semaphore lock when iterating clients. func (s *Semaphore) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) { // Broadcast the data. for session := range s.clients { diff --git a/server/channelserver/sys_session.go b/server/channelserver/sys_session.go index 40d54fdb3..358192e1b 100644 --- a/server/channelserver/sys_session.go +++ b/server/channelserver/sys_session.go @@ -18,54 +18,85 @@ import ( "go.uber.org/zap" ) +// packet is an internal wrapper for queued outbound packets. type packet struct { - data []byte - nonBlocking bool + data []byte // Raw packet bytes to send + nonBlocking bool // If true, drop packet if queue is full instead of blocking } -// Session holds state for the channel server connection. +// Session represents an active player connection to the channel server. +// +// Each Session manages a single player's connection lifecycle, including: +// - Packet send/receive loops running in separate goroutines +// - Current stage (game area) and stage movement history +// - Character state (ID, courses, guild, etc.) +// - Mail system state +// - Quest/semaphore participation +// +// Lifecycle: +// 1. Created by NewSession() when a player connects +// 2. Started with Start() which launches send/recv goroutines +// 3. Processes packets through handlePacketGroup() -> handler functions +// 4. Cleaned up when connection closes or times out (30 second inactivity) +// +// Thread Safety: +// Session embeds sync.Mutex to protect mutable state. Most handler functions +// acquire the session lock when modifying session fields. The packet queue +// (sendPackets channel) is safe for concurrent access. type Session struct { - sync.Mutex - logger *zap.Logger - server *Server - rawConn net.Conn - cryptConn *network.CryptConn - sendPackets chan packet - clientContext *clientctx.ClientContext - lastPacket time.Time + sync.Mutex // Protects session state during concurrent handler execution - userEnteredStage bool // If the user has entered a stage before - stageID string - stage *Stage - reservationStage *Stage // Required for the stateful MsgSysUnreserveStage packet. - stagePass string // Temporary storage - prevGuildID uint32 // Stores the last GuildID used in InfoGuild - charID uint32 - logKey []byte - sessionStart int64 - courses []mhfcourse.Course - token string - kqf []byte - kqfOverride bool + // Core connection and logging + logger *zap.Logger // Logger with connection address + server *Server // Parent server reference + rawConn net.Conn // Underlying TCP connection + cryptConn *network.CryptConn // Encrypted connection wrapper + sendPackets chan packet // Outbound packet queue (buffered, size 20) + clientContext *clientctx.ClientContext // Client version and capabilities + lastPacket time.Time // Timestamp of last received packet (for timeout detection) - semaphore *Semaphore // Required for the stateful MsgSysUnreserveStage packet. + // Stage (game area) state + userEnteredStage bool // Whether player has entered any stage during this session + stageID string // Current stage ID string (e.g., "sl1Ns200p0a0u0") + stage *Stage // Pointer to current stage object + reservationStage *Stage // Stage reserved for quest (used by unreserve packet) + stagePass string // Temporary password storage for password-protected stages + stageMoveStack *stringstack.StringStack // Navigation history for "back" functionality - // A stack containing the stage movement history (push on enter/move, pop on back) - stageMoveStack *stringstack.StringStack + // Player identity and state + charID uint32 // Character ID for this session + Name string // Character name (for debugging/logging) + prevGuildID uint32 // Last guild ID queried (cached for InfoGuild) + token string // Authentication token from sign server + logKey []byte // Logging encryption key + sessionStart int64 // Session start timestamp (Unix time) + courses []mhfcourse.Course // Active Monster Hunter courses (buffs/subscriptions) + kqf []byte // Key Quest Flags (quest progress tracking) + kqfOverride bool // Whether KQF is being overridden - // Accumulated index used for identifying mail for a client - // I'm not certain why this is used, but since the client is sending it - // I want to rely on it for now as it might be important later. - mailAccIndex uint8 - // Contains the mail list that maps accumulated indexes to mail IDs - mailList []int + // Quest/event coordination + semaphore *Semaphore // Semaphore for quest/event participation (if in a coordinated activity) - // For Debuging - Name string - closed bool + // Mail system state + // The mail system uses an accumulated index system where the client tracks + // mail by incrementing indices rather than direct mail IDs + mailAccIndex uint8 // Current accumulated mail index for this session + mailList []int // Maps accumulated indices to actual mail IDs + + // Connection state + closed bool // Whether connection has been closed (prevents double-cleanup) } -// NewSession creates a new Session type. +// NewSession creates and initializes a new Session for an incoming connection. +// +// The session is created with: +// - A logger tagged with the connection's remote address +// - An encrypted connection wrapper +// - A buffered packet send queue (size 20) +// - Initialized stage movement stack for navigation +// - Session start time set to current time +// +// After creation, call Start() to begin processing packets. func NewSession(server *Server, conn net.Conn) *Session { s := &Session{ logger: server.logger.Named(conn.RemoteAddr().String()), @@ -81,7 +112,17 @@ func NewSession(server *Server, conn net.Conn) *Session { return s } -// Start starts the session packet send and recv loop(s). +// Start begins the session's packet processing by launching send and receive goroutines. +// +// This method spawns two long-running goroutines: +// 1. sendLoop(): Continuously sends queued packets to the client +// 2. recvLoop(): Continuously receives and processes packets from the client +// +// The receive loop handles packet parsing, routing to handlers, and recursive +// packet group processing (when multiple packets arrive in one read). +// +// Both loops run until the connection closes or times out. Unlike the sign and +// entrance servers, the channel server does NOT expect an 8-byte NULL initialization. func (s *Session) Start() { go func() { s.logger.Debug("New connection", zap.String("RemoteAddr", s.rawConn.RemoteAddr().String())) @@ -92,7 +133,19 @@ func (s *Session) Start() { }() } -// QueueSend queues a packet (raw []byte) to be sent. +// QueueSend queues a packet for transmission to the client (blocking). +// +// This method: +// 1. Logs the outbound packet (if dev mode is enabled) +// 2. Attempts to enqueue the packet to the send channel +// 3. If the queue is full, flushes non-blocking packets and retries +// +// Blocking vs Non-blocking: +// This is a blocking send - if the queue fills, it will flush non-blocking +// packets (broadcasts, non-critical messages) to make room for this packet. +// Use QueueSendNonBlocking() for packets that can be safely dropped. +// +// Thread Safety: Safe for concurrent calls from multiple goroutines. func (s *Session) QueueSend(data []byte) { s.logMessage(binary.BigEndian.Uint16(data[0:2]), data, "Server", s.Name) select { @@ -114,7 +167,18 @@ func (s *Session) QueueSend(data []byte) { } } -// QueueSendNonBlocking queues a packet (raw []byte) to be sent, dropping the packet entirely if the queue is full. +// QueueSendNonBlocking queues a packet for transmission (non-blocking, lossy). +// +// Unlike QueueSend(), this method drops the packet immediately if the send queue +// is full. This is used for broadcast messages, stage updates, and other packets +// where occasional packet loss is acceptable (client will re-sync or request again). +// +// Use cases: +// - Stage broadcasts (player movement, chat) +// - Server-wide announcements +// - Non-critical status updates +// +// Thread Safety: Safe for concurrent calls from multiple goroutines. func (s *Session) QueueSendNonBlocking(data []byte) { select { case s.sendPackets <- packet{data, true}: @@ -124,7 +188,15 @@ func (s *Session) QueueSendNonBlocking(data []byte) { } } -// QueueSendMHF queues a MHFPacket to be sent. +// QueueSendMHF queues a structured MHFPacket for transmission to the client. +// +// This is a convenience method that: +// 1. Creates a byteframe and writes the packet opcode +// 2. Calls the packet's Build() method to serialize its data +// 3. Queues the resulting bytes using QueueSend() +// +// The packet is built with the session's clientContext, allowing version-specific +// packet formatting when needed. func (s *Session) QueueSendMHF(pkt mhfpacket.MHFPacket) { // Make the header bf := byteframe.NewByteFrame() @@ -137,7 +209,15 @@ func (s *Session) QueueSendMHF(pkt mhfpacket.MHFPacket) { s.QueueSend(bf.Data()) } -// QueueAck is a helper function to queue an MSG_SYS_ACK with the given ack handle and data. +// QueueAck sends an acknowledgment packet with optional response data. +// +// Many client packets include an "ack handle" field - a unique identifier the client +// uses to match responses to requests. This method constructs and queues a MSG_SYS_ACK +// packet containing the ack handle and response data. +// +// Parameters: +// - ackHandle: The ack handle from the original client packet +// - data: Response payload bytes (can be empty for simple acks) func (s *Session) QueueAck(ackHandle uint32, data []byte) { bf := byteframe.NewByteFrame() bf.WriteUint16(uint16(network.MSG_SYS_ACK)) diff --git a/server/channelserver/sys_stage.go b/server/channelserver/sys_stage.go index eac3127bd..60e05e1af 100644 --- a/server/channelserver/sys_stage.go +++ b/server/channelserver/sys_stage.go @@ -7,49 +7,94 @@ import ( "erupe-ce/network/mhfpacket" ) -// Object holds infomation about a specific object. +// Object represents a placeable object in a stage (e.g., ballista, bombs, traps). +// +// Objects are spawned by players during quests and can be interacted with by +// other players in the same stage. Each object has an owner, position, and +// unique ID for client-server synchronization. type Object struct { - sync.RWMutex - id uint32 - ownerCharID uint32 - x, y, z float32 + sync.RWMutex // Protects object state during updates + id uint32 // Unique object ID (see NextObjectID for ID generation) + ownerCharID uint32 // Character ID of the player who placed this object + x, y, z float32 // 3D position coordinates } -// stageBinaryKey is a struct used as a map key for identifying a stage binary part. +// stageBinaryKey is a composite key for identifying a specific piece of stage binary data. +// +// Stage binary data is custom game state that the stage host (quest leader) sets +// and the server echoes to other clients. It's used for quest state, monster HP, +// environmental conditions, etc. The data is keyed by two ID bytes. type stageBinaryKey struct { - id0 uint8 - id1 uint8 + id0 uint8 // First binary data identifier + id1 uint8 // Second binary data identifier } -// Stage holds stage-specific information +// Stage represents a game room/area where players interact. +// +// Stages are the core spatial concept in Monster Hunter Frontier. They represent: +// - Town areas (Mezeporta, Pallone, etc.) - persistent, always exist +// - Quest instances - created dynamically when a player starts a quest +// - Private rooms - password-protected player gathering areas +// +// Stage Lifecycle: +// 1. Created via NewStage() or MSG_SYS_CREATE_STAGE packet +// 2. Players enter via MSG_SYS_ENTER_STAGE or MSG_SYS_MOVE_STAGE +// 3. Stage host manages state via binary data packets +// 4. Destroyed via MSG_SYS_STAGE_DESTRUCT when empty or quest completes +// +// Client Participation: +// There are two types of client participation: +// - Active clients (in clients map): Currently in the stage, receive broadcasts +// - Reserved slots (in reservedClientSlots): Quest participants who haven't +// entered yet (e.g., loading screen, preparing). They hold a slot but don't +// receive stage broadcasts until they fully enter. +// +// Thread Safety: +// Stage embeds sync.RWMutex. Use RLock for reads (broadcasts, queries) and +// Lock for writes (entering, leaving, state changes). type Stage struct { - sync.RWMutex + sync.RWMutex // Protects all stage state during concurrent access - // Stage ID string - id string + // Stage identity + id string // Stage ID string (e.g., "sl1Ns200p0a0u0" for Mezeporta) - // Objects - objects map[uint32]*Object - objectIndex uint8 + // Objects in the stage (ballistas, bombs, traps, etc.) + objects map[uint32]*Object // Active objects keyed by object ID + objectIndex uint8 // Auto-incrementing index for object ID generation - // Map of session -> charID. - // These are clients that are CURRENTLY in the stage - clients map[*Session]uint32 + // Active participants + clients map[*Session]uint32 // Sessions currently in stage -> their character ID - // Map of charID -> bool, key represents whether they are ready - // These are clients that aren't in the stage, but have reserved a slot (for quests, etc). - reservedClientSlots map[uint32]bool + // Reserved slots for quest participants + // Map of charID -> ready status. These players have reserved a slot but + // haven't fully entered yet (e.g., still loading, in preparation screen) + reservedClientSlots map[uint32]bool // Character ID -> ready flag - // These are raw binary blobs that the stage owner sets, - // other clients expect the server to echo them back in the exact same format. - rawBinaryData map[stageBinaryKey][]byte + // Stage binary data + // Raw binary blobs set by the stage host (quest leader) that track quest state. + // The server stores and echoes this data to clients verbatim. Used for: + // - Monster HP and status + // - Environmental state (time remaining, weather) + // - Quest objectives and progress + rawBinaryData map[stageBinaryKey][]byte // Binary state keyed by (id0, id1) - host *Session - maxPlayers uint16 - password string + // Stage settings + host *Session // Stage host (quest leader, room creator) + maxPlayers uint16 // Maximum players allowed (default 4) + password string // Password for private stages (empty if public) } -// NewStage creates a new stage with intialized values. +// NewStage creates and initializes a new Stage with the given ID. +// +// The stage is created with: +// - Empty client and reserved slot maps +// - Empty object map with objectIndex starting at 0 +// - Empty binary data map +// - Default max players set to 4 (standard quest party size) +// - No password (public stage) +// +// For persistent town stages, this is called during server initialization. +// For dynamic quest stages, this is called when a player creates a quest. func NewStage(ID string) *Stage { s := &Stage{ id: ID, @@ -63,7 +108,24 @@ func NewStage(ID string) *Stage { return s } -// BroadcastMHF queues a MHFPacket to be sent to all sessions in the stage. +// BroadcastMHF sends a packet to all players currently in the stage. +// +// This method is used for stage-local events like: +// - Player chat messages within the stage +// - Monster state updates +// - Object placement/removal notifications +// - Quest events visible only to stage participants +// +// The packet is built individually for each client to support version-specific +// formatting. Packets are sent non-blocking (dropped if queue full). +// +// Reserved clients (those who haven't fully entered) do NOT receive broadcasts. +// +// Parameters: +// - pkt: The MHFPacket to broadcast to stage participants +// - ignoredSession: Optional session to exclude (typically the sender) +// +// Thread Safety: This method holds the stage lock during iteration. func (s *Stage) BroadcastMHF(pkt mhfpacket.MHFPacket, ignoredSession *Session) { s.Lock() defer s.Unlock() @@ -96,6 +158,18 @@ func (s *Stage) isQuest() bool { return len(s.reservedClientSlots) > 0 } +// NextObjectID generates the next available object ID for this stage. +// +// Object IDs have special constraints due to client limitations: +// - Index 0 does not update position correctly (avoided) +// - Index 127 does not update position correctly (avoided) +// - Indices > 127 do not replicate correctly across clients (avoided) +// +// The ID is generated by packing bytes into a uint32 in a specific format +// expected by the client. The objectIndex cycles from 1-126 to stay within +// valid bounds. +// +// Thread Safety: Caller must hold stage lock when calling this method. func (s *Stage) NextObjectID() uint32 { s.objectIndex = s.objectIndex + 1 // Objects beyond 127 do not duplicate correctly diff --git a/server/signserver/dbutils.go b/server/signserver/dbutils.go index ee4dcc493..2fb0f138a 100644 --- a/server/signserver/dbutils.go +++ b/server/signserver/dbutils.go @@ -177,7 +177,7 @@ func (s *Server) getGuildmatesForCharacters(chars []character) []members { if err != nil { continue } - for i, _ := range charGuildmates { + for i := range charGuildmates { charGuildmates[i].CID = char.ID } guildmates = append(guildmates, charGuildmates...)