feat(diva): implement Diva Defense (UD) system

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)
This commit is contained in:
Houmgaor
2026-03-20 17:52:01 +01:00
parent 7ff033e36e
commit 2bd92c9ae7
13 changed files with 708 additions and 69 deletions

View File

@@ -1,6 +1,9 @@
package channelserver
import (
"encoding/json"
"time"
"github.com/jmoiron/sqlx"
)
@@ -75,3 +78,149 @@ func (r *DivaRepository) GetTotalPoints(eventID uint32) (int64, int64, error) {
}
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
}