mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-24 16:43:37 +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.
168 lines
4.9 KiB
Go
168 lines
4.9 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"database/sql"
|
|
"fmt"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// TournamentRepository centralizes all database access for tournament tables.
|
|
type TournamentRepository struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// NewTournamentRepository creates a new TournamentRepository.
|
|
func NewTournamentRepository(db *sqlx.DB) *TournamentRepository {
|
|
return &TournamentRepository{db: db}
|
|
}
|
|
|
|
// GetActive returns the most recently started tournament that is still within its
|
|
// reward window (reward_end >= now), or nil if no active tournament exists.
|
|
func (r *TournamentRepository) GetActive(now int64) (*Tournament, error) {
|
|
var t Tournament
|
|
err := r.db.QueryRowx(
|
|
`SELECT id, name, start_time, entry_end, ranking_end, reward_end
|
|
FROM tournaments
|
|
WHERE start_time <= $1 AND reward_end >= $1
|
|
ORDER BY start_time DESC
|
|
LIMIT 1`,
|
|
now,
|
|
).StructScan(&t)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get active tournament: %w", err)
|
|
}
|
|
return &t, nil
|
|
}
|
|
|
|
// GetCups returns all cups belonging to the given tournament, ordered by ID.
|
|
func (r *TournamentRepository) GetCups(tournamentID uint32) ([]TournamentCup, error) {
|
|
var cups []TournamentCup
|
|
err := r.db.Select(&cups,
|
|
`SELECT id, cup_group, cup_type, unk, name, description
|
|
FROM tournament_cups
|
|
WHERE tournament_id = $1
|
|
ORDER BY id`,
|
|
tournamentID,
|
|
)
|
|
return cups, err
|
|
}
|
|
|
|
// GetSubEvents returns all sub-events ordered by cup group and event sub type.
|
|
func (r *TournamentRepository) GetSubEvents() ([]TournamentSubEvent, error) {
|
|
var events []TournamentSubEvent
|
|
err := r.db.Select(&events,
|
|
`SELECT id, cup_group, event_sub_type, quest_file_id, name
|
|
FROM tournament_sub_events
|
|
ORDER BY cup_group, event_sub_type`,
|
|
)
|
|
return events, err
|
|
}
|
|
|
|
// Register registers a character for a tournament. If the character is already
|
|
// registered the existing entry ID is returned (ON CONFLICT DO NOTHING, then re-SELECT).
|
|
func (r *TournamentRepository) Register(charID, tournamentID uint32) (uint32, error) {
|
|
_, err := r.db.Exec(
|
|
`INSERT INTO tournament_entries (char_id, tournament_id)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT (char_id, tournament_id) DO NOTHING`,
|
|
charID, tournamentID,
|
|
)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("insert tournament entry: %w", err)
|
|
}
|
|
var id uint32
|
|
err = r.db.QueryRow(
|
|
`SELECT id FROM tournament_entries WHERE char_id = $1 AND tournament_id = $2`,
|
|
charID, tournamentID,
|
|
).Scan(&id)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("fetch tournament entry id: %w", err)
|
|
}
|
|
return id, nil
|
|
}
|
|
|
|
// GetEntry returns the registration record for a character/tournament pair, or nil if not found.
|
|
func (r *TournamentRepository) GetEntry(charID, tournamentID uint32) (*TournamentEntry, error) {
|
|
var e TournamentEntry
|
|
err := r.db.QueryRowx(
|
|
`SELECT id, char_id, tournament_id
|
|
FROM tournament_entries
|
|
WHERE char_id = $1 AND tournament_id = $2`,
|
|
charID, tournamentID,
|
|
).StructScan(&e)
|
|
if err == sql.ErrNoRows {
|
|
return nil, nil
|
|
}
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get tournament entry: %w", err)
|
|
}
|
|
return &e, nil
|
|
}
|
|
|
|
// SubmitResult records a completed tournament run for a character.
|
|
func (r *TournamentRepository) SubmitResult(charID, tournamentID, eventID, questSlot, stageHandle uint32) error {
|
|
_, err := r.db.Exec(
|
|
`INSERT INTO tournament_results (char_id, tournament_id, event_id, quest_slot, stage_handle)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
charID, tournamentID, eventID, questSlot, stageHandle,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("insert tournament result: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetLeaderboard returns the ranked leaderboard for an event ID.
|
|
// Rank is assigned by submission order (first submitted = rank 1).
|
|
// Returns at most 100 entries.
|
|
func (r *TournamentRepository) GetLeaderboard(eventID uint32) ([]TournamentRankEntry, error) {
|
|
type row struct {
|
|
CharID uint32 `db:"char_id"`
|
|
Rank int64 `db:"rank"`
|
|
Grade int `db:"grade"`
|
|
HR int `db:"hr"`
|
|
GR int `db:"gr"`
|
|
CharName string `db:"char_name"`
|
|
GuildName string `db:"guild_name"`
|
|
}
|
|
var rows []row
|
|
err := r.db.Select(&rows, `
|
|
SELECT
|
|
r.char_id,
|
|
ROW_NUMBER() OVER (ORDER BY r.submitted_at ASC)::int AS rank,
|
|
c.gr::int AS grade,
|
|
c.hr::int AS hr,
|
|
c.gr::int AS gr,
|
|
c.name AS char_name,
|
|
COALESCE(g.name, '') AS guild_name
|
|
FROM tournament_results r
|
|
JOIN characters c ON c.id = r.char_id
|
|
LEFT JOIN guild_characters gc ON gc.character_id = r.char_id
|
|
LEFT JOIN guilds g ON g.id = gc.guild_id
|
|
WHERE r.event_id = $1
|
|
ORDER BY r.submitted_at ASC
|
|
LIMIT 100`,
|
|
eventID,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get tournament leaderboard: %w", err)
|
|
}
|
|
entries := make([]TournamentRankEntry, len(rows))
|
|
for i, row := range rows {
|
|
entries[i] = TournamentRankEntry{
|
|
CharID: row.CharID,
|
|
Rank: uint32(row.Rank),
|
|
Grade: uint16(row.Grade),
|
|
HR: uint16(row.HR),
|
|
GR: uint16(row.GR),
|
|
CharName: row.CharName,
|
|
GuildName: row.GuildName,
|
|
}
|
|
}
|
|
return entries, nil
|
|
}
|