mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-27 01:53:19 +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:
167
server/channelserver/repo_tournament.go
Normal file
167
server/channelserver/repo_tournament.go
Normal file
@@ -0,0 +1,167 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user