mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Add full Diva Defense / United Defense system: schema, repo layer, i18n bead names, and RE-verified packet handler implementations. Schema (0011_diva.sql): diva_beads, diva_beads_assignment, diva_beads_points, diva_prizes tables; interception_maps/points columns on guilds and guild_characters. Seed (DivaDefaults.sql): 26 prize milestones for personal and guild reward tracks (item_type=26 diva coins). Repo (DivaRepo): 11 new methods covering bead assignment, point accumulation, interception point tracking, prize queries, and cleanup. Mocks wired in test_helpers_test.go. i18n: Bead struct with EN/JP names for all 18 bead types (IDs 1–25). Session tracks currentBeadIndex (-1 = none assigned). Packet handlers corrected against mhfo-hd.dll RE findings: - GetKijuInfo: u8 count, 512-byte desc, color_id+bead_type per entry - SetKiju: 1-byte ACK; persists bead assignment to DB - GetUdMyPoint: 8×18-byte entries, no count prefix - GetUdTotalPointInfo: u8 error + u64[64] + u8[64] + u64 (~585 B) - GetUdSelectedColorInfo: u8 error + u8[8] = 9 bytes - GetUdDailyPresentList: correct u16 count format (was wrong hex) - GetUdNormaPresentList: correct u16 count format (was wrong hex) - GetUdRankingRewardList: correct u16 count with u32 item_id/qty - GetRewardSong: 22-byte layout with 0xFFFFFFFF prayer_end sentinel - AddRewardSongCount: parse implemented (was NOT IMPLEMENTED stub)
227 lines
7.4 KiB
Go
227 lines
7.4 KiB
Go
package channelserver
|
|
|
|
import (
|
|
"encoding/json"
|
|
"time"
|
|
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
// DivaRepository centralizes all database access for diva defense events.
|
|
type DivaRepository struct {
|
|
db *sqlx.DB
|
|
}
|
|
|
|
// NewDivaRepository creates a new DivaRepository.
|
|
func NewDivaRepository(db *sqlx.DB) *DivaRepository {
|
|
return &DivaRepository{db: db}
|
|
}
|
|
|
|
// DeleteEvents removes all diva events.
|
|
func (r *DivaRepository) DeleteEvents() error {
|
|
_, err := r.db.Exec("DELETE FROM events WHERE event_type='diva'")
|
|
return err
|
|
}
|
|
|
|
// InsertEvent creates a new diva event with the given start epoch.
|
|
func (r *DivaRepository) InsertEvent(startEpoch uint32) error {
|
|
_, err := r.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('diva', to_timestamp($1)::timestamp without time zone)", startEpoch)
|
|
return err
|
|
}
|
|
|
|
// DivaEvent represents a diva event row with ID and start_time epoch.
|
|
type DivaEvent struct {
|
|
ID uint32 `db:"id"`
|
|
StartTime uint32 `db:"start_time"`
|
|
}
|
|
|
|
// GetEvents returns all diva events with their ID and start_time epoch.
|
|
func (r *DivaRepository) GetEvents() ([]DivaEvent, error) {
|
|
var result []DivaEvent
|
|
err := r.db.Select(&result, "SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'")
|
|
return result, err
|
|
}
|
|
|
|
// AddPoints atomically adds quest and bonus points for a character in a diva event.
|
|
func (r *DivaRepository) AddPoints(charID, eventID, questPoints, bonusPoints uint32) error {
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO diva_points (char_id, event_id, quest_points, bonus_points, updated_at)
|
|
VALUES ($1, $2, $3, $4, now())
|
|
ON CONFLICT (char_id, event_id) DO UPDATE
|
|
SET quest_points = diva_points.quest_points + EXCLUDED.quest_points,
|
|
bonus_points = diva_points.bonus_points + EXCLUDED.bonus_points,
|
|
updated_at = now()`,
|
|
charID, eventID, questPoints, bonusPoints)
|
|
return err
|
|
}
|
|
|
|
// GetPoints returns the accumulated quest and bonus points for a character in an event.
|
|
func (r *DivaRepository) GetPoints(charID, eventID uint32) (int64, int64, error) {
|
|
var qp, bp int64
|
|
err := r.db.QueryRow(
|
|
"SELECT quest_points, bonus_points FROM diva_points WHERE char_id=$1 AND event_id=$2",
|
|
charID, eventID).Scan(&qp, &bp)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return qp, bp, nil
|
|
}
|
|
|
|
// GetTotalPoints returns the sum of all players' quest and bonus points for an event.
|
|
func (r *DivaRepository) GetTotalPoints(eventID uint32) (int64, int64, error) {
|
|
var qp, bp int64
|
|
err := r.db.QueryRow(
|
|
"SELECT COALESCE(SUM(quest_points),0), COALESCE(SUM(bonus_points),0) FROM diva_points WHERE event_id=$1",
|
|
eventID).Scan(&qp, &bp)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
return qp, bp, nil
|
|
}
|
|
|
|
// GetBeads returns all active bead types from the diva_beads table.
|
|
func (r *DivaRepository) GetBeads() ([]int, error) {
|
|
var types []int
|
|
err := r.db.Select(&types, "SELECT type FROM diva_beads ORDER BY id")
|
|
return types, err
|
|
}
|
|
|
|
// AssignBead inserts a bead assignment for a character, replacing any existing one for that bead slot.
|
|
func (r *DivaRepository) AssignBead(characterID uint32, beadIndex int, expiry time.Time) error {
|
|
_, err := r.db.Exec(`
|
|
INSERT INTO diva_beads_assignment (character_id, bead_index, expiry)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT DO NOTHING`,
|
|
characterID, beadIndex, expiry)
|
|
return err
|
|
}
|
|
|
|
// AddBeadPoints records a bead point contribution for a character.
|
|
func (r *DivaRepository) AddBeadPoints(characterID uint32, beadIndex int, points int) error {
|
|
_, err := r.db.Exec(
|
|
"INSERT INTO diva_beads_points (character_id, bead_index, points) VALUES ($1, $2, $3)",
|
|
characterID, beadIndex, points)
|
|
return err
|
|
}
|
|
|
|
// GetCharacterBeadPoints returns the summed points per bead_index for a character.
|
|
func (r *DivaRepository) GetCharacterBeadPoints(characterID uint32) (map[int]int, error) {
|
|
rows, err := r.db.Query(
|
|
"SELECT bead_index, COALESCE(SUM(points),0) FROM diva_beads_points WHERE character_id=$1 GROUP BY bead_index",
|
|
characterID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
result := make(map[int]int)
|
|
for rows.Next() {
|
|
var idx, pts int
|
|
if err := rows.Scan(&idx, &pts); err != nil {
|
|
return nil, err
|
|
}
|
|
result[idx] = pts
|
|
}
|
|
return result, rows.Err()
|
|
}
|
|
|
|
// GetTotalBeadPoints returns the sum of all points across all characters and bead slots.
|
|
func (r *DivaRepository) GetTotalBeadPoints() (int64, error) {
|
|
var total int64
|
|
err := r.db.QueryRow("SELECT COALESCE(SUM(points),0) FROM diva_beads_points").Scan(&total)
|
|
return total, err
|
|
}
|
|
|
|
// GetTopBeadPerDay returns the bead_index with the most points contributed on day offset `day`
|
|
// (0 = today, 1 = yesterday, etc.). Returns 0 if no data exists for that day.
|
|
func (r *DivaRepository) GetTopBeadPerDay(day int) (int, error) {
|
|
var beadIndex int
|
|
err := r.db.QueryRow(`
|
|
SELECT bead_index
|
|
FROM diva_beads_points
|
|
WHERE timestamp >= (NOW() - ($1 + 1) * INTERVAL '1 day')
|
|
AND timestamp < (NOW() - $1 * INTERVAL '1 day')
|
|
GROUP BY bead_index
|
|
ORDER BY SUM(points) DESC
|
|
LIMIT 1`,
|
|
day).Scan(&beadIndex)
|
|
if err != nil {
|
|
return 0, nil // no data for this day is not an error
|
|
}
|
|
return beadIndex, nil
|
|
}
|
|
|
|
// CleanupBeads deletes all rows from diva_beads, diva_beads_assignment, and diva_beads_points.
|
|
func (r *DivaRepository) CleanupBeads() error {
|
|
if _, err := r.db.Exec("DELETE FROM diva_beads_points"); err != nil {
|
|
return err
|
|
}
|
|
if _, err := r.db.Exec("DELETE FROM diva_beads_assignment"); err != nil {
|
|
return err
|
|
}
|
|
_, err := r.db.Exec("DELETE FROM diva_beads")
|
|
return err
|
|
}
|
|
|
|
// GetPersonalPrizes returns all prize rows with type='personal', ordered by points_req.
|
|
func (r *DivaRepository) GetPersonalPrizes() ([]DivaPrize, error) {
|
|
return r.getPrizesByType("personal")
|
|
}
|
|
|
|
// GetGuildPrizes returns all prize rows with type='guild', ordered by points_req.
|
|
func (r *DivaRepository) GetGuildPrizes() ([]DivaPrize, error) {
|
|
return r.getPrizesByType("guild")
|
|
}
|
|
|
|
func (r *DivaRepository) getPrizesByType(prizeType string) ([]DivaPrize, error) {
|
|
rows, err := r.db.Query(`
|
|
SELECT id, type, points_req, item_type, item_id, quantity, gr, repeatable
|
|
FROM diva_prizes
|
|
WHERE type=$1
|
|
ORDER BY points_req`,
|
|
prizeType)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() { _ = rows.Close() }()
|
|
var prizes []DivaPrize
|
|
for rows.Next() {
|
|
var p DivaPrize
|
|
if err := rows.Scan(&p.ID, &p.Type, &p.PointsReq, &p.ItemType, &p.ItemID, &p.Quantity, &p.GR, &p.Repeatable); err != nil {
|
|
return nil, err
|
|
}
|
|
prizes = append(prizes, p)
|
|
}
|
|
return prizes, rows.Err()
|
|
}
|
|
|
|
// GetCharacterInterceptionPoints returns the interception_points JSON map from guild_characters.
|
|
func (r *DivaRepository) GetCharacterInterceptionPoints(characterID uint32) (map[string]int, error) {
|
|
var raw []byte
|
|
err := r.db.QueryRow(
|
|
"SELECT interception_points FROM guild_characters WHERE char_id=$1",
|
|
characterID).Scan(&raw)
|
|
if err != nil {
|
|
return map[string]int{}, nil
|
|
}
|
|
result := make(map[string]int)
|
|
if len(raw) > 0 {
|
|
if err := json.Unmarshal(raw, &result); err != nil {
|
|
return map[string]int{}, nil
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// AddInterceptionPoints increments the interception points for a quest file ID in guild_characters.
|
|
func (r *DivaRepository) AddInterceptionPoints(characterID uint32, questFileID int, points int) error {
|
|
_, err := r.db.Exec(`
|
|
UPDATE guild_characters
|
|
SET interception_points = interception_points || jsonb_build_object(
|
|
$2::text,
|
|
COALESCE((interception_points->>$2::text)::int, 0) + $3
|
|
)
|
|
WHERE char_id=$1`,
|
|
characterID, questFileID, points)
|
|
return err
|
|
}
|