mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 08:33:41 +01:00
Wire format for MsgMhfEnterTournamentQuest (0x00D2) derived from mhfo-hd.dll binary analysis (FUN_114f4280). Five new tables back the full lifecycle: schedule, cups, sub-events, player registrations, and run submissions. All six tournament handlers are now DB-driven: - EnumerateRanking: returns active tournament schedule with cups and sub-events; computes phase state byte from timestamps - EnumerateOrder: returns per-event leaderboard ranked by submission time, with SJIS-encoded character and guild names - InfoTournament: exposes tournament detail and player registration state across all three query types - EntryTournament: registers player and returns entry handle used by the client in the subsequent EnterTournamentQuest packet - EnterTournamentQuest: parses the previously-unimplemented packet and records the run in tournament_results - AcquireTournament: stubs rewards (item IDs not yet reversed) Seed data (TournamentDefaults.sql) reproduces tournament #150 cups and sub-events so a fresh install has a working tournament immediately.
317 lines
8.7 KiB
Go
317 lines
8.7 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"erupe-ce/common/byteframe"
|
|
ps "erupe-ce/common/pascalstring"
|
|
cfg "erupe-ce/config"
|
|
"erupe-ce/network/mhfpacket"
|
|
"time"
|
|
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// TournamentInfo0 represents tournament information (type 0).
|
|
type TournamentInfo0 struct {
|
|
ID uint32
|
|
MaxPlayers uint32
|
|
CurrentPlayers uint32
|
|
Unk1 uint16
|
|
TextColor uint16
|
|
Unk2 uint32
|
|
Time1 time.Time
|
|
Time2 time.Time
|
|
Time3 time.Time
|
|
Time4 time.Time
|
|
Time5 time.Time
|
|
Time6 time.Time
|
|
Unk3 uint8
|
|
Unk4 uint8
|
|
MinHR uint32
|
|
MaxHR uint32
|
|
Unk5 string
|
|
Unk6 string
|
|
}
|
|
|
|
// TournamentInfo21 represents tournament information (type 21).
|
|
type TournamentInfo21 struct {
|
|
Unk0 uint32
|
|
Unk1 uint32
|
|
Unk2 uint32
|
|
Unk3 uint8
|
|
}
|
|
|
|
// TournamentInfo22 represents tournament information (type 22).
|
|
type TournamentInfo22 struct {
|
|
Unk0 uint32
|
|
Unk1 uint32
|
|
Unk2 uint32
|
|
Unk3 uint8
|
|
Unk4 string
|
|
}
|
|
|
|
// TournamentReward represents a tournament reward entry.
|
|
type TournamentReward struct {
|
|
Unk0 uint16
|
|
Unk1 uint16
|
|
Unk2 uint16
|
|
}
|
|
|
|
// tournamentState returns the state byte for the EnumerateRanking response.
|
|
// 0 = no tournament / before start, 1 = registration open, 2 = hunting active,
|
|
// 3 = ranking/reward period.
|
|
func tournamentState(now int64, t *Tournament) uint8 {
|
|
if t == nil || now < t.StartTime {
|
|
return 0
|
|
}
|
|
if now <= t.EntryEnd {
|
|
return 1
|
|
}
|
|
if now <= t.RankingEnd {
|
|
return 2
|
|
}
|
|
return 3
|
|
}
|
|
|
|
func handleMsgMhfEnumerateRanking(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfEnumerateRanking)
|
|
bf := byteframe.NewByteFrame()
|
|
|
|
now := TimeAdjusted().Unix()
|
|
tournament, err := s.server.tournamentRepo.GetActive(now)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get active tournament for EnumerateRanking", zap.Error(err))
|
|
}
|
|
|
|
if tournament == nil {
|
|
// No active tournament: write zeroed timestamps, current time, state 0, empty data.
|
|
bf.WriteBytes(make([]byte, 16))
|
|
bf.WriteUint32(uint32(now))
|
|
bf.WriteUint8(0)
|
|
ps.Uint8(bf, "", false)
|
|
bf.WriteUint16(0) // numEvents
|
|
bf.WriteUint8(0) // numCups
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
return
|
|
}
|
|
|
|
state := tournamentState(now, tournament)
|
|
|
|
bf.WriteUint32(uint32(tournament.StartTime))
|
|
bf.WriteUint32(uint32(tournament.EntryEnd))
|
|
bf.WriteUint32(uint32(tournament.RankingEnd))
|
|
bf.WriteUint32(uint32(tournament.RewardEnd))
|
|
bf.WriteUint32(uint32(now))
|
|
bf.WriteUint8(state)
|
|
ps.Uint8(bf, tournament.Name, true)
|
|
|
|
subEvents, err := s.server.tournamentRepo.GetSubEvents()
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tournament sub-events", zap.Error(err))
|
|
subEvents = nil
|
|
}
|
|
bf.WriteUint16(uint16(len(subEvents)))
|
|
for _, se := range subEvents {
|
|
bf.WriteUint32(se.ID)
|
|
bf.WriteUint16(uint16(se.CupGroup))
|
|
bf.WriteInt16(se.EventSubType)
|
|
bf.WriteUint32(se.QuestFileID)
|
|
ps.Uint8(bf, se.Name, true)
|
|
}
|
|
|
|
cups, err := s.server.tournamentRepo.GetCups(tournament.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tournament cups", zap.Error(err))
|
|
cups = nil
|
|
}
|
|
bf.WriteUint8(uint8(len(cups)))
|
|
for _, cup := range cups {
|
|
bf.WriteUint32(cup.ID)
|
|
bf.WriteUint16(uint16(cup.CupGroup))
|
|
bf.WriteUint16(uint16(cup.CupType))
|
|
bf.WriteUint16(uint16(cup.Unk))
|
|
ps.Uint8(bf, cup.Name, true)
|
|
ps.Uint16(bf, cup.Description, true)
|
|
}
|
|
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfEnumerateOrder(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfEnumerateOrder)
|
|
bf := byteframe.NewByteFrame()
|
|
|
|
now := uint32(TimeAdjusted().Unix())
|
|
bf.WriteUint32(pkt.EventID)
|
|
bf.WriteUint32(now)
|
|
|
|
entries, err := s.server.tournamentRepo.GetLeaderboard(pkt.EventID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tournament leaderboard", zap.Error(err), zap.Uint32("eventID", pkt.EventID))
|
|
entries = nil
|
|
}
|
|
|
|
bf.WriteUint16(uint16(len(entries)))
|
|
bf.WriteUint16(0) // unk
|
|
|
|
for _, e := range entries {
|
|
bf.WriteUint32(e.CharID)
|
|
bf.WriteUint32(e.Rank)
|
|
bf.WriteUint16(e.Grade)
|
|
bf.WriteUint16(0) // pad
|
|
bf.WriteUint16(e.HR)
|
|
if s.server.erupeConfig.RealClientMode >= cfg.G10 {
|
|
bf.WriteUint16(e.GR)
|
|
}
|
|
bf.WriteUint16(0) // pad
|
|
charNameBytes := []byte(e.CharName)
|
|
guildNameBytes := []byte(e.GuildName)
|
|
bf.WriteUint8(uint8(len(charNameBytes) + 1))
|
|
bf.WriteUint8(uint8(len(guildNameBytes) + 1))
|
|
bf.WriteNullTerminatedBytes(charNameBytes)
|
|
bf.WriteNullTerminatedBytes(guildNameBytes)
|
|
}
|
|
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfInfoTournament(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfInfoTournament)
|
|
bf := byteframe.NewByteFrame()
|
|
|
|
now := TimeAdjusted().Unix()
|
|
|
|
switch pkt.QueryType {
|
|
case 0:
|
|
tournament, err := s.server.tournamentRepo.GetActive(now)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get active tournament for InfoTournament type 0", zap.Error(err))
|
|
}
|
|
bf.WriteUint32(0) // unk header
|
|
if tournament == nil {
|
|
bf.WriteUint32(0) // count = 0
|
|
break
|
|
}
|
|
bf.WriteUint32(1) // count
|
|
bf.WriteUint32(tournament.ID)
|
|
bf.WriteUint32(0) // MaxPlayers
|
|
bf.WriteUint32(0) // CurrentPlayers
|
|
bf.WriteUint16(0) // Unk1
|
|
bf.WriteUint16(0) // TextColor
|
|
bf.WriteUint32(0) // Unk2
|
|
bf.WriteUint32(uint32(tournament.StartTime))
|
|
bf.WriteUint32(uint32(tournament.EntryEnd))
|
|
bf.WriteUint32(uint32(tournament.RankingEnd))
|
|
bf.WriteUint32(uint32(tournament.RewardEnd))
|
|
bf.WriteUint32(uint32(tournament.RewardEnd))
|
|
bf.WriteUint32(uint32(tournament.RewardEnd))
|
|
bf.WriteUint8(0) // Unk3
|
|
bf.WriteUint8(0) // Unk4
|
|
bf.WriteUint32(0) // MinHR
|
|
bf.WriteUint32(0) // MaxHR
|
|
ps.Uint8(bf, tournament.Name, true)
|
|
ps.Uint16(bf, "", false)
|
|
case 1:
|
|
// Return player registration status.
|
|
bf.WriteUint32(uint32(now))
|
|
tournament, err := s.server.tournamentRepo.GetActive(now)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get active tournament for InfoTournament type 1", zap.Error(err))
|
|
}
|
|
if tournament == nil {
|
|
bf.WriteUint32(0) // tournamentID
|
|
bf.WriteUint32(0) // entryID
|
|
bf.WriteUint32(0)
|
|
bf.WriteUint8(0) // not registered
|
|
bf.WriteUint32(0)
|
|
ps.Uint8(bf, "", true)
|
|
break
|
|
}
|
|
entry, err := s.server.tournamentRepo.GetEntry(s.charID, tournament.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get tournament entry for InfoTournament type 1", zap.Error(err))
|
|
}
|
|
bf.WriteUint32(tournament.ID)
|
|
if entry != nil {
|
|
bf.WriteUint32(entry.ID)
|
|
bf.WriteUint32(0)
|
|
bf.WriteUint8(1) // registered
|
|
} else {
|
|
bf.WriteUint32(0)
|
|
bf.WriteUint32(0)
|
|
bf.WriteUint8(0) // not registered
|
|
}
|
|
bf.WriteUint32(0)
|
|
ps.Uint8(bf, tournament.Name, true)
|
|
case 2:
|
|
// Return empty lists (reward structures unknown).
|
|
bf.WriteUint32(0)
|
|
bf.WriteUint32(0) // count type 21
|
|
bf.WriteUint32(0) // count type 22
|
|
}
|
|
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfEntryTournament(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfEntryTournament)
|
|
now := TimeAdjusted().Unix()
|
|
|
|
tournament, err := s.server.tournamentRepo.GetActive(now)
|
|
if err != nil {
|
|
s.logger.Error("Failed to get active tournament for EntryTournament", zap.Error(err))
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
if tournament == nil {
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
|
|
entryID, err := s.server.tournamentRepo.Register(s.charID, tournament.ID)
|
|
if err != nil {
|
|
s.logger.Error("Failed to register for tournament", zap.Error(err),
|
|
zap.Uint32("charID", s.charID), zap.Uint32("tournamentID", tournament.ID))
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
return
|
|
}
|
|
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint32(entryID)
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|
|
|
|
func handleMsgMhfEnterTournamentQuest(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfEnterTournamentQuest)
|
|
s.logger.Debug("EnterTournamentQuest",
|
|
zap.Uint32("tournamentID", pkt.TournamentID),
|
|
zap.Uint32("entryHandle", pkt.EntryHandle),
|
|
zap.Uint32("unk2", pkt.Unk2),
|
|
zap.Uint32("questSlot", pkt.QuestSlot),
|
|
zap.Uint32("stageHandle", pkt.StageHandle),
|
|
)
|
|
if err := s.server.tournamentRepo.SubmitResult(
|
|
s.charID,
|
|
pkt.TournamentID,
|
|
pkt.Unk2,
|
|
pkt.QuestSlot,
|
|
pkt.StageHandle,
|
|
); err != nil {
|
|
s.logger.Error("Failed to submit tournament result", zap.Error(err))
|
|
}
|
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
|
}
|
|
|
|
func handleMsgMhfAcquireTournament(s *Session, p mhfpacket.MHFPacket) {
|
|
pkt := p.(*mhfpacket.MsgMhfAcquireTournament)
|
|
// Reward item IDs are unknown. Return an empty reward list.
|
|
rewards := []TournamentReward{}
|
|
bf := byteframe.NewByteFrame()
|
|
bf.WriteUint8(uint8(len(rewards)))
|
|
for _, reward := range rewards {
|
|
bf.WriteUint16(reward.Unk0)
|
|
bf.WriteUint16(reward.Unk1)
|
|
bf.WriteUint16(reward.Unk2)
|
|
}
|
|
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
|
}
|