mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-27 18:12:50 +01:00
feat(tournament): implement hunting tournament system end-to-end
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.
This commit is contained in:
@@ -3,8 +3,11 @@ 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).
|
||||
@@ -46,73 +49,6 @@ type TournamentInfo22 struct {
|
||||
Unk4 string
|
||||
}
|
||||
|
||||
func handleMsgMhfInfoTournament(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfInfoTournament)
|
||||
bf := byteframe.NewByteFrame()
|
||||
|
||||
tournamentInfo0 := []TournamentInfo0{}
|
||||
tournamentInfo21 := []TournamentInfo21{}
|
||||
tournamentInfo22 := []TournamentInfo22{}
|
||||
|
||||
switch pkt.QueryType {
|
||||
case 0:
|
||||
bf.WriteUint32(0)
|
||||
bf.WriteUint32(uint32(len(tournamentInfo0)))
|
||||
for _, tinfo := range tournamentInfo0 {
|
||||
bf.WriteUint32(tinfo.ID)
|
||||
bf.WriteUint32(tinfo.MaxPlayers)
|
||||
bf.WriteUint32(tinfo.CurrentPlayers)
|
||||
bf.WriteUint16(tinfo.Unk1)
|
||||
bf.WriteUint16(tinfo.TextColor)
|
||||
bf.WriteUint32(tinfo.Unk2)
|
||||
bf.WriteUint32(uint32(tinfo.Time1.Unix()))
|
||||
bf.WriteUint32(uint32(tinfo.Time2.Unix()))
|
||||
bf.WriteUint32(uint32(tinfo.Time3.Unix()))
|
||||
bf.WriteUint32(uint32(tinfo.Time4.Unix()))
|
||||
bf.WriteUint32(uint32(tinfo.Time5.Unix()))
|
||||
bf.WriteUint32(uint32(tinfo.Time6.Unix()))
|
||||
bf.WriteUint8(tinfo.Unk3)
|
||||
bf.WriteUint8(tinfo.Unk4)
|
||||
bf.WriteUint32(tinfo.MinHR)
|
||||
bf.WriteUint32(tinfo.MaxHR)
|
||||
ps.Uint8(bf, tinfo.Unk5, true)
|
||||
ps.Uint16(bf, tinfo.Unk6, true)
|
||||
}
|
||||
case 1:
|
||||
bf.WriteUint32(uint32(TimeAdjusted().Unix()))
|
||||
bf.WriteUint32(0) // Registered ID
|
||||
bf.WriteUint32(0)
|
||||
bf.WriteUint32(0)
|
||||
bf.WriteUint8(0)
|
||||
bf.WriteUint32(0)
|
||||
ps.Uint8(bf, "", true)
|
||||
case 2:
|
||||
bf.WriteUint32(0)
|
||||
bf.WriteUint32(uint32(len(tournamentInfo21)))
|
||||
for _, info := range tournamentInfo21 {
|
||||
bf.WriteUint32(info.Unk0)
|
||||
bf.WriteUint32(info.Unk1)
|
||||
bf.WriteUint32(info.Unk2)
|
||||
bf.WriteUint8(info.Unk3)
|
||||
}
|
||||
bf.WriteUint32(uint32(len(tournamentInfo22)))
|
||||
for _, info := range tournamentInfo22 {
|
||||
bf.WriteUint32(info.Unk0)
|
||||
bf.WriteUint32(info.Unk1)
|
||||
bf.WriteUint32(info.Unk2)
|
||||
bf.WriteUint8(info.Unk3)
|
||||
ps.Uint8(bf, info.Unk4, true)
|
||||
}
|
||||
}
|
||||
|
||||
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
|
||||
}
|
||||
|
||||
func handleMsgMhfEntryTournament(s *Session, p mhfpacket.MHFPacket) {
|
||||
pkt := p.(*mhfpacket.MsgMhfEntryTournament)
|
||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||
}
|
||||
|
||||
// TournamentReward represents a tournament reward entry.
|
||||
type TournamentReward struct {
|
||||
Unk0 uint16
|
||||
@@ -120,8 +56,254 @@ type TournamentReward struct {
|
||||
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)))
|
||||
|
||||
Reference in New Issue
Block a user