Files
Erupe/server/channelserver/repo_rengoku.go
Houmgaor ecfe58ffb4 feat: add SQLite support, setup wizard enhancements, and live dashboard
Add zero-dependency SQLite mode so users can run Erupe without
PostgreSQL. A transparent db.DB wrapper auto-translates PostgreSQL
SQL ($N placeholders, now(), ::casts, ILIKE, public. prefix,
TRUNCATE) for SQLite at runtime — all 28 repo files use the wrapper
with no per-query changes needed.

Setup wizard gains two new steps: quest file detection with download
link, and gameplay presets (solo/small/community/rebalanced). The API
server gets a /dashboard endpoint with auto-refreshing stats.

CI release workflow now builds and pushes Docker images to GHCR
alongside binary artifacts on tag push.

Key changes:
- common/db: DB/Tx wrapper with 6 SQL translation rules
- server/migrations/sqlite: full SQLite schema (0001-0005)
- config: Database.Driver field ("postgres" or "sqlite")
- main.go: SQLite connection with WAL mode, single writer
- server/setup: quest check + preset selection steps
- server/api: /dashboard with live stats
- .github/workflows: Docker in release, deduplicate docker.yml
2026-03-05 18:00:30 +01:00

81 lines
2.6 KiB
Go

package channelserver
import (
"fmt"
dbutil "erupe-ce/common/db"
)
// RengokuRepository centralizes all database access for the rengoku_score table.
type RengokuRepository struct {
db *dbutil.DB
}
// NewRengokuRepository creates a new RengokuRepository.
func NewRengokuRepository(db *dbutil.DB) *RengokuRepository {
return &RengokuRepository{db: db}
}
// UpsertScore ensures a rengoku_score row exists for the character and updates it.
func (r *RengokuRepository) UpsertScore(charID uint32, maxStagesMp, maxPointsMp, maxStagesSp, maxPointsSp uint32) error {
var t int
err := r.db.QueryRow("SELECT character_id FROM rengoku_score WHERE character_id=$1", charID).Scan(&t)
if err != nil {
if _, err := r.db.Exec("INSERT INTO rengoku_score (character_id) VALUES ($1)", charID); err != nil {
return fmt.Errorf("insert rengoku_score: %w", err)
}
}
if _, err := r.db.Exec(
"UPDATE rengoku_score SET max_stages_mp=$1, max_points_mp=$2, max_stages_sp=$3, max_points_sp=$4 WHERE character_id=$5",
maxStagesMp, maxPointsMp, maxStagesSp, maxPointsSp, charID,
); err != nil {
return fmt.Errorf("update rengoku_score: %w", err)
}
return nil
}
// rengokuScoreQuery is the shared FROM/JOIN clause for ranking queries.
const rengokuScoreQueryRepo = `, c.name FROM rengoku_score rs
LEFT JOIN characters c ON c.id = rs.character_id
LEFT JOIN guild_characters gc ON gc.character_id = rs.character_id `
// rengokuColumnForLeaderboard maps a leaderboard index to the score column name.
func rengokuColumnForLeaderboard(leaderboard uint32) string {
switch leaderboard {
case 0, 2:
return "max_stages_mp"
case 1, 3:
return "max_points_mp"
case 4, 6:
return "max_stages_sp"
case 5, 7:
return "max_points_sp"
default:
return "max_stages_mp"
}
}
// rengokuIsGuildFiltered returns true if the leaderboard index is guild-scoped.
func rengokuIsGuildFiltered(leaderboard uint32) bool {
return leaderboard == 2 || leaderboard == 3 || leaderboard == 6 || leaderboard == 7
}
// GetRanking returns rengoku scores for the given leaderboard.
// For guild-scoped leaderboards (2,3,6,7), guildID filters the results.
func (r *RengokuRepository) GetRanking(leaderboard uint32, guildID uint32) ([]RengokuScore, error) {
col := rengokuColumnForLeaderboard(leaderboard)
var result []RengokuScore
var err error
if rengokuIsGuildFiltered(leaderboard) {
err = r.db.Select(&result,
fmt.Sprintf("SELECT %s AS score %s WHERE guild_id=$1 ORDER BY %s DESC", col, rengokuScoreQueryRepo, col),
guildID,
)
} else {
err = r.db.Select(&result,
fmt.Sprintf("SELECT %s AS score %s ORDER BY %s DESC", col, rengokuScoreQueryRepo, col),
)
}
return result, err
}