mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-26 01:23:13 +01:00
refactor(channelserver): split repo_guild.go into domain-focused files
The 1004-line monolith covered 11 game subsystems across 62 methods. Split into 7 files by domain (RP, posts, alliances, adventures, hunts, cooking) while keeping core CRUD/membership/scouts in the original. Same package, receiver, and interface — no behavior changes.
This commit is contained in:
@@ -1,13 +1,9 @@
|
|||||||
package channelserver
|
package channelserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
|
||||||
|
|
||||||
"erupe-ce/common/stringsupport"
|
|
||||||
|
|
||||||
"github.com/jmoiron/sqlx"
|
"github.com/jmoiron/sqlx"
|
||||||
)
|
)
|
||||||
@@ -440,477 +436,6 @@ func (r *GuildRepository) SetRecruiter(charID uint32, allowed bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddMemberDailyRP adds RP to a member's daily total.
|
|
||||||
func (r *GuildRepository) AddMemberDailyRP(charID uint32, amount uint16) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_characters SET rp_today=rp_today+$1 WHERE character_id=$2`, amount, charID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExchangeEventRP subtracts RP from a guild's event pool and returns the new balance.
|
|
||||||
func (r *GuildRepository) ExchangeEventRP(guildID uint32, amount uint16) (uint32, error) {
|
|
||||||
var balance uint32
|
|
||||||
err := r.db.QueryRow(`UPDATE guilds SET event_rp=event_rp-$1 WHERE id=$2 RETURNING event_rp`, amount, guildID).Scan(&balance)
|
|
||||||
return balance, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRankRP adds RP to a guild's rank total.
|
|
||||||
func (r *GuildRepository) AddRankRP(guildID uint32, amount uint16) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guilds SET rank_rp = rank_rp + $1 WHERE id = $2`, amount, guildID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddEventRP adds RP to a guild's event total.
|
|
||||||
func (r *GuildRepository) AddEventRP(guildID uint32, amount uint16) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guilds SET event_rp = event_rp + $1 WHERE id = $2`, amount, guildID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRoomRP returns the current room RP for a guild.
|
|
||||||
func (r *GuildRepository) GetRoomRP(guildID uint32) (uint16, error) {
|
|
||||||
var rp uint16
|
|
||||||
err := r.db.QueryRow(`SELECT room_rp FROM guilds WHERE id = $1`, guildID).Scan(&rp)
|
|
||||||
return rp, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRoomRP sets the room RP for a guild.
|
|
||||||
func (r *GuildRepository) SetRoomRP(guildID uint32, rp uint16) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guilds SET room_rp = $1 WHERE id = $2`, rp, guildID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddRoomRP atomically adds RP to a guild's room total.
|
|
||||||
func (r *GuildRepository) AddRoomRP(guildID uint32, amount uint16) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guilds SET room_rp = room_rp + $1 WHERE id = $2`, amount, guildID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetRoomExpiry sets the room expiry time for a guild.
|
|
||||||
func (r *GuildRepository) SetRoomExpiry(guildID uint32, expiry time.Time) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guilds SET room_expiry = $1 WHERE id = $2`, expiry, guildID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Guild Posts ---
|
|
||||||
|
|
||||||
// ListPosts returns active guild posts of the given type, ordered by newest first.
|
|
||||||
func (r *GuildRepository) ListPosts(guildID uint32, postType int) ([]*MessageBoardPost, error) {
|
|
||||||
rows, err := r.db.Queryx(
|
|
||||||
`SELECT id, stamp_id, title, body, author_id, created_at, liked_by
|
|
||||||
FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
|
|
||||||
ORDER BY created_at DESC`, guildID, postType)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
var posts []*MessageBoardPost
|
|
||||||
for rows.Next() {
|
|
||||||
post := &MessageBoardPost{}
|
|
||||||
if err := rows.StructScan(post); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
posts = append(posts, post)
|
|
||||||
}
|
|
||||||
return posts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreatePost inserts a new guild post and soft-deletes excess posts beyond maxPosts.
|
|
||||||
func (r *GuildRepository) CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error {
|
|
||||||
tx, err := r.db.BeginTxx(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback() }()
|
|
||||||
|
|
||||||
if _, err := tx.Exec(
|
|
||||||
`INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
||||||
guildID, authorID, stampID, postType, title, body); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(`UPDATE guild_posts SET deleted = true WHERE id IN (
|
|
||||||
SELECT id FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
|
|
||||||
ORDER BY created_at DESC OFFSET $3
|
|
||||||
)`, guildID, postType, maxPosts); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeletePost soft-deletes a guild post by ID.
|
|
||||||
func (r *GuildRepository) DeletePost(postID uint32) error {
|
|
||||||
_, err := r.db.Exec("UPDATE guild_posts SET deleted = true WHERE id = $1", postID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePost updates the title and body of a guild post.
|
|
||||||
func (r *GuildRepository) UpdatePost(postID uint32, title, body string) error {
|
|
||||||
_, err := r.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE id = $3", title, body, postID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdatePostStamp updates the stamp of a guild post.
|
|
||||||
func (r *GuildRepository) UpdatePostStamp(postID, stampID uint32) error {
|
|
||||||
_, err := r.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE id = $2", stampID, postID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetPostLikedBy returns the liked_by CSV string for a guild post.
|
|
||||||
func (r *GuildRepository) GetPostLikedBy(postID uint32) (string, error) {
|
|
||||||
var likedBy string
|
|
||||||
err := r.db.QueryRow("SELECT liked_by FROM guild_posts WHERE id = $1", postID).Scan(&likedBy)
|
|
||||||
return likedBy, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetPostLikedBy updates the liked_by CSV string for a guild post.
|
|
||||||
func (r *GuildRepository) SetPostLikedBy(postID uint32, likedBy string) error {
|
|
||||||
_, err := r.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, postID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountNewPosts returns the count of non-deleted posts created after the given time.
|
|
||||||
func (r *GuildRepository) CountNewPosts(guildID uint32, since time.Time) (int, error) {
|
|
||||||
var count int
|
|
||||||
err := r.db.QueryRow(
|
|
||||||
`SELECT COUNT(*) FROM guild_posts WHERE guild_id = $1 AND deleted = false AND (EXTRACT(epoch FROM created_at)::int) > $2`,
|
|
||||||
guildID, since.Unix()).Scan(&count)
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Guild Alliances ---
|
|
||||||
|
|
||||||
const allianceInfoSelectSQL = `
|
|
||||||
SELECT
|
|
||||||
ga.id,
|
|
||||||
ga.name,
|
|
||||||
created_at,
|
|
||||||
parent_id,
|
|
||||||
CASE
|
|
||||||
WHEN sub1_id IS NULL THEN 0
|
|
||||||
ELSE sub1_id
|
|
||||||
END,
|
|
||||||
CASE
|
|
||||||
WHEN sub2_id IS NULL THEN 0
|
|
||||||
ELSE sub2_id
|
|
||||||
END
|
|
||||||
FROM guild_alliances ga
|
|
||||||
`
|
|
||||||
|
|
||||||
// GetAllianceByID loads alliance data including parent and sub guilds.
|
|
||||||
func (r *GuildRepository) GetAllianceByID(allianceID uint32) (*GuildAlliance, error) {
|
|
||||||
rows, err := r.db.Queryx(fmt.Sprintf(`%s WHERE ga.id = $1`, allianceInfoSelectSQL), allianceID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
if !rows.Next() {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
return r.scanAllianceWithGuilds(rows)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListAlliances returns all alliances with their guild data populated.
|
|
||||||
func (r *GuildRepository) ListAlliances() ([]*GuildAlliance, error) {
|
|
||||||
rows, err := r.db.Queryx(allianceInfoSelectSQL)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
var alliances []*GuildAlliance
|
|
||||||
for rows.Next() {
|
|
||||||
alliance, err := r.scanAllianceWithGuilds(rows)
|
|
||||||
if err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
alliances = append(alliances, alliance)
|
|
||||||
}
|
|
||||||
return alliances, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAlliance creates a new guild alliance with the given parent guild.
|
|
||||||
func (r *GuildRepository) CreateAlliance(name string, parentGuildID uint32) error {
|
|
||||||
_, err := r.db.Exec("INSERT INTO guild_alliances (name, parent_id) VALUES ($1, $2)", name, parentGuildID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteAlliance removes an alliance by ID.
|
|
||||||
func (r *GuildRepository) DeleteAlliance(allianceID uint32) error {
|
|
||||||
_, err := r.db.Exec("DELETE FROM guild_alliances WHERE id=$1", allianceID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveGuildFromAlliance removes a guild from its alliance, shifting sub2 into sub1's slot if needed.
|
|
||||||
func (r *GuildRepository) RemoveGuildFromAlliance(allianceID, guildID, subGuild1ID, subGuild2ID uint32) error {
|
|
||||||
if guildID == subGuild1ID && subGuild2ID > 0 {
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = sub2_id, sub2_id = NULL WHERE id = $1`, allianceID)
|
|
||||||
return err
|
|
||||||
} else if guildID == subGuild1ID {
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = NULL WHERE id = $1`, allianceID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_alliances SET sub2_id = NULL WHERE id = $1`, allianceID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// scanAllianceWithGuilds scans an alliance row and populates its guild data.
|
|
||||||
func (r *GuildRepository) scanAllianceWithGuilds(rows *sqlx.Rows) (*GuildAlliance, error) {
|
|
||||||
alliance := &GuildAlliance{}
|
|
||||||
if err := rows.StructScan(alliance); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
parentGuild, err := r.GetByID(alliance.ParentGuildID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
alliance.ParentGuild = *parentGuild
|
|
||||||
alliance.TotalMembers += parentGuild.MemberCount
|
|
||||||
|
|
||||||
if alliance.SubGuild1ID > 0 {
|
|
||||||
subGuild1, err := r.GetByID(alliance.SubGuild1ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
alliance.SubGuild1 = *subGuild1
|
|
||||||
alliance.TotalMembers += subGuild1.MemberCount
|
|
||||||
}
|
|
||||||
|
|
||||||
if alliance.SubGuild2ID > 0 {
|
|
||||||
subGuild2, err := r.GetByID(alliance.SubGuild2ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
alliance.SubGuild2 = *subGuild2
|
|
||||||
alliance.TotalMembers += subGuild2.MemberCount
|
|
||||||
}
|
|
||||||
|
|
||||||
return alliance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Guild Adventures ---
|
|
||||||
|
|
||||||
// ListAdventures returns all adventures for a guild.
|
|
||||||
func (r *GuildRepository) ListAdventures(guildID uint32) ([]*GuildAdventure, error) {
|
|
||||||
rows, err := r.db.Queryx(
|
|
||||||
"SELECT id, destination, charge, depart, return, collected_by FROM guild_adventures WHERE guild_id = $1", guildID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
var adventures []*GuildAdventure
|
|
||||||
for rows.Next() {
|
|
||||||
adv := &GuildAdventure{}
|
|
||||||
if err := rows.StructScan(adv); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
adventures = append(adventures, adv)
|
|
||||||
}
|
|
||||||
return adventures, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAdventure inserts a new guild adventure.
|
|
||||||
func (r *GuildRepository) CreateAdventure(guildID, destination uint32, depart, returnTime int64) error {
|
|
||||||
_, err := r.db.Exec(
|
|
||||||
"INSERT INTO guild_adventures (guild_id, destination, depart, return) VALUES ($1, $2, $3, $4)",
|
|
||||||
guildID, destination, depart, returnTime)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateAdventureWithCharge inserts a new guild adventure with an initial charge (Diva variant).
|
|
||||||
func (r *GuildRepository) CreateAdventureWithCharge(guildID, destination, charge uint32, depart, returnTime int64) error {
|
|
||||||
_, err := r.db.Exec(
|
|
||||||
"INSERT INTO guild_adventures (guild_id, destination, charge, depart, return) VALUES ($1, $2, $3, $4, $5)",
|
|
||||||
guildID, destination, charge, depart, returnTime)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectAdventure marks an adventure as collected by the given character (CSV append).
|
|
||||||
// Uses SELECT FOR UPDATE to prevent concurrent double-collect.
|
|
||||||
func (r *GuildRepository) CollectAdventure(adventureID uint32, charID uint32) error {
|
|
||||||
tx, err := r.db.BeginTxx(context.Background(), nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = tx.Rollback() }()
|
|
||||||
|
|
||||||
var collectedBy string
|
|
||||||
err = tx.QueryRow("SELECT collected_by FROM guild_adventures WHERE id = $1 FOR UPDATE", adventureID).Scan(&collectedBy)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
collectedBy = stringsupport.CSVAdd(collectedBy, int(charID))
|
|
||||||
if _, err = tx.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ChargeAdventure adds charge to a guild adventure.
|
|
||||||
func (r *GuildRepository) ChargeAdventure(adventureID uint32, amount uint32) error {
|
|
||||||
_, err := r.db.Exec("UPDATE guild_adventures SET charge = charge + $1 WHERE id = $2", amount, adventureID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Guild Treasure Hunts ---
|
|
||||||
|
|
||||||
// GetPendingHunt returns the pending (unacquired) hunt for a character, or nil if none.
|
|
||||||
func (r *GuildRepository) GetPendingHunt(charID uint32) (*TreasureHunt, error) {
|
|
||||||
hunt := &TreasureHunt{}
|
|
||||||
err := r.db.QueryRowx(
|
|
||||||
`SELECT id, host_id, destination, level, start, hunt_data FROM guild_hunts WHERE host_id=$1 AND acquired=FALSE`,
|
|
||||||
charID).StructScan(hunt)
|
|
||||||
if errors.Is(err, sql.ErrNoRows) {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return hunt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListGuildHunts returns acquired level-2 hunts for a guild, with hunter counts and claim status.
|
|
||||||
func (r *GuildRepository) ListGuildHunts(guildID, charID uint32) ([]*TreasureHunt, error) {
|
|
||||||
rows, err := r.db.Queryx(`SELECT gh.id, gh.host_id, gh.destination, gh.level, gh.start, gh.collected, gh.hunt_data,
|
|
||||||
(SELECT COUNT(*) FROM guild_characters gc WHERE gc.treasure_hunt = gh.id AND gc.character_id <> $1) AS hunters,
|
|
||||||
CASE
|
|
||||||
WHEN ghc.character_id IS NOT NULL THEN true
|
|
||||||
ELSE false
|
|
||||||
END AS claimed
|
|
||||||
FROM guild_hunts gh
|
|
||||||
LEFT JOIN guild_hunts_claimed ghc ON gh.id = ghc.hunt_id AND ghc.character_id = $1
|
|
||||||
WHERE gh.guild_id=$2 AND gh.level=2 AND gh.acquired=TRUE
|
|
||||||
`, charID, guildID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
var hunts []*TreasureHunt
|
|
||||||
for rows.Next() {
|
|
||||||
hunt := &TreasureHunt{}
|
|
||||||
if err := rows.StructScan(hunt); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
hunts = append(hunts, hunt)
|
|
||||||
}
|
|
||||||
return hunts, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateHunt inserts a new guild treasure hunt.
|
|
||||||
func (r *GuildRepository) CreateHunt(guildID, hostID, destination, level uint32, huntData []byte, catsUsed string) error {
|
|
||||||
_, err := r.db.Exec(
|
|
||||||
`INSERT INTO guild_hunts (guild_id, host_id, destination, level, hunt_data, cats_used) VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
||||||
guildID, hostID, destination, level, huntData, catsUsed)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// AcquireHunt marks a treasure hunt as acquired.
|
|
||||||
func (r *GuildRepository) AcquireHunt(huntID uint32) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_hunts SET acquired=true WHERE id=$1`, huntID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RegisterHuntReport sets a character's active treasure hunt.
|
|
||||||
func (r *GuildRepository) RegisterHuntReport(huntID, charID uint32) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=$1 WHERE character_id=$2`, huntID, charID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// CollectHunt marks a hunt as collected and clears all characters' treasure_hunt references.
|
|
||||||
func (r *GuildRepository) CollectHunt(huntID uint32) error {
|
|
||||||
if _, err := r.db.Exec(`UPDATE guild_hunts SET collected=true WHERE id=$1`, huntID); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE treasure_hunt=$1`, huntID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClaimHuntReward records that a character has claimed a treasure hunt reward.
|
|
||||||
func (r *GuildRepository) ClaimHuntReward(huntID, charID uint32) error {
|
|
||||||
_, err := r.db.Exec(`INSERT INTO guild_hunts_claimed VALUES ($1, $2)`, huntID, charID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Guild Cooking/Meals ---
|
|
||||||
|
|
||||||
// ListMeals returns all meals for a guild.
|
|
||||||
func (r *GuildRepository) ListMeals(guildID uint32) ([]*GuildMeal, error) {
|
|
||||||
rows, err := r.db.Queryx("SELECT id, meal_id, level, created_at FROM guild_meals WHERE guild_id = $1", guildID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
var meals []*GuildMeal
|
|
||||||
for rows.Next() {
|
|
||||||
meal := &GuildMeal{}
|
|
||||||
if err := rows.StructScan(meal); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
meals = append(meals, meal)
|
|
||||||
}
|
|
||||||
return meals, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CreateMeal inserts a new guild meal and returns the new ID.
|
|
||||||
func (r *GuildRepository) CreateMeal(guildID, mealID, level uint32, createdAt time.Time) (uint32, error) {
|
|
||||||
var id uint32
|
|
||||||
err := r.db.QueryRow(
|
|
||||||
"INSERT INTO guild_meals (guild_id, meal_id, level, created_at) VALUES ($1, $2, $3, $4) RETURNING id",
|
|
||||||
guildID, mealID, level, createdAt).Scan(&id)
|
|
||||||
return id, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateMeal updates an existing guild meal's fields.
|
|
||||||
func (r *GuildRepository) UpdateMeal(mealID, newMealID, level uint32, createdAt time.Time) error {
|
|
||||||
_, err := r.db.Exec("UPDATE guild_meals SET meal_id = $1, level = $2, created_at = $3 WHERE id = $4",
|
|
||||||
newMealID, level, createdAt, mealID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClaimHuntBox updates the box_claimed timestamp for a guild character.
|
|
||||||
func (r *GuildRepository) ClaimHuntBox(charID uint32, claimedAt time.Time) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_characters SET box_claimed=$1 WHERE character_id=$2`, claimedAt, charID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// GuildKill represents a kill log entry for guild hunt data.
|
|
||||||
type GuildKill struct {
|
|
||||||
ID uint32 `db:"id"`
|
|
||||||
Monster uint32 `db:"monster"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListGuildKills returns kill log entries for guild members since the character's last box claim.
|
|
||||||
func (r *GuildRepository) ListGuildKills(guildID, charID uint32) ([]*GuildKill, error) {
|
|
||||||
rows, err := r.db.Queryx(`SELECT kl.id, kl.monster FROM kill_logs kl
|
|
||||||
INNER JOIN guild_characters gc ON kl.character_id = gc.character_id
|
|
||||||
WHERE gc.guild_id=$1
|
|
||||||
AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2)
|
|
||||||
`, guildID, charID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = rows.Close() }()
|
|
||||||
var kills []*GuildKill
|
|
||||||
for rows.Next() {
|
|
||||||
kill := &GuildKill{}
|
|
||||||
if err := rows.StructScan(kill); err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
kills = append(kills, kill)
|
|
||||||
}
|
|
||||||
return kills, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// CountGuildKills returns the count of kill log entries for guild members since the character's last box claim.
|
|
||||||
func (r *GuildRepository) CountGuildKills(guildID, charID uint32) (int, error) {
|
|
||||||
var count int
|
|
||||||
err := r.db.QueryRow(`SELECT COUNT(*) FROM kill_logs kl
|
|
||||||
INNER JOIN guild_characters gc ON kl.character_id = gc.character_id
|
|
||||||
WHERE gc.guild_id=$1
|
|
||||||
AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2)
|
|
||||||
`, guildID, charID).Scan(&count)
|
|
||||||
return count, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Guild Scouts ---
|
|
||||||
|
|
||||||
// ScoutedCharacter represents an invited character in the scout list.
|
// ScoutedCharacter represents an invited character in the scout list.
|
||||||
type ScoutedCharacter struct {
|
type ScoutedCharacter struct {
|
||||||
CharID uint32 `db:"id"`
|
CharID uint32 `db:"id"`
|
||||||
@@ -920,18 +445,6 @@ type ScoutedCharacter struct {
|
|||||||
ActorID uint32 `db:"actor_id"`
|
ActorID uint32 `db:"actor_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClearTreasureHunt clears the treasure_hunt field for a character on logout.
|
|
||||||
func (r *GuildRepository) ClearTreasureHunt(charID uint32) error {
|
|
||||||
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE character_id=$1`, charID)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertKillLog records a monster kill log entry for a character.
|
|
||||||
func (r *GuildRepository) InsertKillLog(charID uint32, monster int, quantity uint8, timestamp time.Time) error {
|
|
||||||
_, err := r.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, charID, monster, quantity, timestamp)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListInvitedCharacters returns all characters with pending guild invitations.
|
// ListInvitedCharacters returns all characters with pending guild invitations.
|
||||||
func (r *GuildRepository) ListInvitedCharacters(guildID uint32) ([]*ScoutedCharacter, error) {
|
func (r *GuildRepository) ListInvitedCharacters(guildID uint32) ([]*ScoutedCharacter, error) {
|
||||||
rows, err := r.db.Queryx(`
|
rows, err := r.db.Queryx(`
|
||||||
@@ -954,51 +467,3 @@ func (r *GuildRepository) ListInvitedCharacters(guildID uint32) ([]*ScoutedChara
|
|||||||
}
|
}
|
||||||
return chars, nil
|
return chars, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RolloverDailyRP moves rp_today into rp_yesterday for all members of a guild,
|
|
||||||
// then updates the guild's rp_reset_at timestamp.
|
|
||||||
// Uses SELECT FOR UPDATE to prevent concurrent rollovers from racing.
|
|
||||||
func (r *GuildRepository) RolloverDailyRP(guildID uint32, noon time.Time) error {
|
|
||||||
tx, err := r.db.Begin()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Lock the guild row and re-check whether rollover is still needed.
|
|
||||||
var rpResetAt time.Time
|
|
||||||
if err := tx.QueryRow(
|
|
||||||
`SELECT COALESCE(rp_reset_at, '2000-01-01'::timestamptz) FROM guilds WHERE id = $1 FOR UPDATE`,
|
|
||||||
guildID,
|
|
||||||
).Scan(&rpResetAt); err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !rpResetAt.Before(noon) {
|
|
||||||
// Another goroutine already rolled over; nothing to do.
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(
|
|
||||||
`UPDATE guild_characters SET rp_yesterday = rp_today, rp_today = 0 WHERE guild_id = $1`,
|
|
||||||
guildID,
|
|
||||||
); err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if _, err := tx.Exec(
|
|
||||||
`UPDATE guilds SET rp_reset_at = $1 WHERE id = $2`,
|
|
||||||
noon, guildID,
|
|
||||||
); err != nil {
|
|
||||||
_ = tx.Rollback()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return tx.Commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddWeeklyBonusUsers atomically adds numUsers to the guild's weekly bonus exceptional user count.
|
|
||||||
func (r *GuildRepository) AddWeeklyBonusUsers(guildID uint32, numUsers uint8) error {
|
|
||||||
_, err := r.db.Exec(
|
|
||||||
"UPDATE guilds SET weekly_bonus_users = weekly_bonus_users + $1 WHERE id = $2",
|
|
||||||
numUsers, guildID,
|
|
||||||
)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|||||||
69
server/channelserver/repo_guild_adventure.go
Normal file
69
server/channelserver/repo_guild_adventure.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"erupe-ce/common/stringsupport"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListAdventures returns all adventures for a guild.
|
||||||
|
func (r *GuildRepository) ListAdventures(guildID uint32) ([]*GuildAdventure, error) {
|
||||||
|
rows, err := r.db.Queryx(
|
||||||
|
"SELECT id, destination, charge, depart, return, collected_by FROM guild_adventures WHERE guild_id = $1", guildID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
var adventures []*GuildAdventure
|
||||||
|
for rows.Next() {
|
||||||
|
adv := &GuildAdventure{}
|
||||||
|
if err := rows.StructScan(adv); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
adventures = append(adventures, adv)
|
||||||
|
}
|
||||||
|
return adventures, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAdventure inserts a new guild adventure.
|
||||||
|
func (r *GuildRepository) CreateAdventure(guildID, destination uint32, depart, returnTime int64) error {
|
||||||
|
_, err := r.db.Exec(
|
||||||
|
"INSERT INTO guild_adventures (guild_id, destination, depart, return) VALUES ($1, $2, $3, $4)",
|
||||||
|
guildID, destination, depart, returnTime)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAdventureWithCharge inserts a new guild adventure with an initial charge (Diva variant).
|
||||||
|
func (r *GuildRepository) CreateAdventureWithCharge(guildID, destination, charge uint32, depart, returnTime int64) error {
|
||||||
|
_, err := r.db.Exec(
|
||||||
|
"INSERT INTO guild_adventures (guild_id, destination, charge, depart, return) VALUES ($1, $2, $3, $4, $5)",
|
||||||
|
guildID, destination, charge, depart, returnTime)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectAdventure marks an adventure as collected by the given character (CSV append).
|
||||||
|
// Uses SELECT FOR UPDATE to prevent concurrent double-collect.
|
||||||
|
func (r *GuildRepository) CollectAdventure(adventureID uint32, charID uint32) error {
|
||||||
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
var collectedBy string
|
||||||
|
err = tx.QueryRow("SELECT collected_by FROM guild_adventures WHERE id = $1 FOR UPDATE", adventureID).Scan(&collectedBy)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
collectedBy = stringsupport.CSVAdd(collectedBy, int(charID))
|
||||||
|
if _, err = tx.Exec("UPDATE guild_adventures SET collected_by = $1 WHERE id = $2", collectedBy, adventureID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ChargeAdventure adds charge to a guild adventure.
|
||||||
|
func (r *GuildRepository) ChargeAdventure(adventureID uint32, amount uint32) error {
|
||||||
|
_, err := r.db.Exec("UPDATE guild_adventures SET charge = charge + $1 WHERE id = $2", amount, adventureID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
115
server/channelserver/repo_guild_alliance.go
Normal file
115
server/channelserver/repo_guild_alliance.go
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
)
|
||||||
|
|
||||||
|
const allianceInfoSelectSQL = `
|
||||||
|
SELECT
|
||||||
|
ga.id,
|
||||||
|
ga.name,
|
||||||
|
created_at,
|
||||||
|
parent_id,
|
||||||
|
CASE
|
||||||
|
WHEN sub1_id IS NULL THEN 0
|
||||||
|
ELSE sub1_id
|
||||||
|
END,
|
||||||
|
CASE
|
||||||
|
WHEN sub2_id IS NULL THEN 0
|
||||||
|
ELSE sub2_id
|
||||||
|
END
|
||||||
|
FROM guild_alliances ga
|
||||||
|
`
|
||||||
|
|
||||||
|
// GetAllianceByID loads alliance data including parent and sub guilds.
|
||||||
|
func (r *GuildRepository) GetAllianceByID(allianceID uint32) (*GuildAlliance, error) {
|
||||||
|
rows, err := r.db.Queryx(fmt.Sprintf(`%s WHERE ga.id = $1`, allianceInfoSelectSQL), allianceID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
if !rows.Next() {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return r.scanAllianceWithGuilds(rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAlliances returns all alliances with their guild data populated.
|
||||||
|
func (r *GuildRepository) ListAlliances() ([]*GuildAlliance, error) {
|
||||||
|
rows, err := r.db.Queryx(allianceInfoSelectSQL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
var alliances []*GuildAlliance
|
||||||
|
for rows.Next() {
|
||||||
|
alliance, err := r.scanAllianceWithGuilds(rows)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
alliances = append(alliances, alliance)
|
||||||
|
}
|
||||||
|
return alliances, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateAlliance creates a new guild alliance with the given parent guild.
|
||||||
|
func (r *GuildRepository) CreateAlliance(name string, parentGuildID uint32) error {
|
||||||
|
_, err := r.db.Exec("INSERT INTO guild_alliances (name, parent_id) VALUES ($1, $2)", name, parentGuildID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteAlliance removes an alliance by ID.
|
||||||
|
func (r *GuildRepository) DeleteAlliance(allianceID uint32) error {
|
||||||
|
_, err := r.db.Exec("DELETE FROM guild_alliances WHERE id=$1", allianceID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveGuildFromAlliance removes a guild from its alliance, shifting sub2 into sub1's slot if needed.
|
||||||
|
func (r *GuildRepository) RemoveGuildFromAlliance(allianceID, guildID, subGuild1ID, subGuild2ID uint32) error {
|
||||||
|
if guildID == subGuild1ID && subGuild2ID > 0 {
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = sub2_id, sub2_id = NULL WHERE id = $1`, allianceID)
|
||||||
|
return err
|
||||||
|
} else if guildID == subGuild1ID {
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_alliances SET sub1_id = NULL WHERE id = $1`, allianceID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_alliances SET sub2_id = NULL WHERE id = $1`, allianceID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// scanAllianceWithGuilds scans an alliance row and populates its guild data.
|
||||||
|
func (r *GuildRepository) scanAllianceWithGuilds(rows *sqlx.Rows) (*GuildAlliance, error) {
|
||||||
|
alliance := &GuildAlliance{}
|
||||||
|
if err := rows.StructScan(alliance); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
parentGuild, err := r.GetByID(alliance.ParentGuildID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alliance.ParentGuild = *parentGuild
|
||||||
|
alliance.TotalMembers += parentGuild.MemberCount
|
||||||
|
|
||||||
|
if alliance.SubGuild1ID > 0 {
|
||||||
|
subGuild1, err := r.GetByID(alliance.SubGuild1ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alliance.SubGuild1 = *subGuild1
|
||||||
|
alliance.TotalMembers += subGuild1.MemberCount
|
||||||
|
}
|
||||||
|
|
||||||
|
if alliance.SubGuild2ID > 0 {
|
||||||
|
subGuild2, err := r.GetByID(alliance.SubGuild2ID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alliance.SubGuild2 = *subGuild2
|
||||||
|
alliance.TotalMembers += subGuild2.MemberCount
|
||||||
|
}
|
||||||
|
|
||||||
|
return alliance, nil
|
||||||
|
}
|
||||||
43
server/channelserver/repo_guild_cooking.go
Normal file
43
server/channelserver/repo_guild_cooking.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ListMeals returns all meals for a guild.
|
||||||
|
func (r *GuildRepository) ListMeals(guildID uint32) ([]*GuildMeal, error) {
|
||||||
|
rows, err := r.db.Queryx("SELECT id, meal_id, level, created_at FROM guild_meals WHERE guild_id = $1", guildID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
var meals []*GuildMeal
|
||||||
|
for rows.Next() {
|
||||||
|
meal := &GuildMeal{}
|
||||||
|
if err := rows.StructScan(meal); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
meals = append(meals, meal)
|
||||||
|
}
|
||||||
|
return meals, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateMeal inserts a new guild meal and returns the new ID.
|
||||||
|
func (r *GuildRepository) CreateMeal(guildID, mealID, level uint32, createdAt time.Time) (uint32, error) {
|
||||||
|
var id uint32
|
||||||
|
err := r.db.QueryRow(
|
||||||
|
"INSERT INTO guild_meals (guild_id, meal_id, level, created_at) VALUES ($1, $2, $3, $4) RETURNING id",
|
||||||
|
guildID, mealID, level, createdAt).Scan(&id)
|
||||||
|
return id, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateMeal updates an existing guild meal's fields.
|
||||||
|
func (r *GuildRepository) UpdateMeal(mealID, newMealID, level uint32, createdAt time.Time) error {
|
||||||
|
_, err := r.db.Exec("UPDATE guild_meals SET meal_id = $1, level = $2, created_at = $3 WHERE id = $4",
|
||||||
|
newMealID, level, createdAt, mealID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimHuntBox updates the box_claimed timestamp for a guild character.
|
||||||
|
func (r *GuildRepository) ClaimHuntBox(charID uint32, claimedAt time.Time) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_characters SET box_claimed=$1 WHERE character_id=$2`, claimedAt, charID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
135
server/channelserver/repo_guild_hunt.go
Normal file
135
server/channelserver/repo_guild_hunt.go
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"errors"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GuildKill represents a kill log entry for guild hunt data.
|
||||||
|
type GuildKill struct {
|
||||||
|
ID uint32 `db:"id"`
|
||||||
|
Monster uint32 `db:"monster"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPendingHunt returns the pending (unacquired) hunt for a character, or nil if none.
|
||||||
|
func (r *GuildRepository) GetPendingHunt(charID uint32) (*TreasureHunt, error) {
|
||||||
|
hunt := &TreasureHunt{}
|
||||||
|
err := r.db.QueryRowx(
|
||||||
|
`SELECT id, host_id, destination, level, start, hunt_data FROM guild_hunts WHERE host_id=$1 AND acquired=FALSE`,
|
||||||
|
charID).StructScan(hunt)
|
||||||
|
if errors.Is(err, sql.ErrNoRows) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return hunt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListGuildHunts returns acquired level-2 hunts for a guild, with hunter counts and claim status.
|
||||||
|
func (r *GuildRepository) ListGuildHunts(guildID, charID uint32) ([]*TreasureHunt, error) {
|
||||||
|
rows, err := r.db.Queryx(`SELECT gh.id, gh.host_id, gh.destination, gh.level, gh.start, gh.collected, gh.hunt_data,
|
||||||
|
(SELECT COUNT(*) FROM guild_characters gc WHERE gc.treasure_hunt = gh.id AND gc.character_id <> $1) AS hunters,
|
||||||
|
CASE
|
||||||
|
WHEN ghc.character_id IS NOT NULL THEN true
|
||||||
|
ELSE false
|
||||||
|
END AS claimed
|
||||||
|
FROM guild_hunts gh
|
||||||
|
LEFT JOIN guild_hunts_claimed ghc ON gh.id = ghc.hunt_id AND ghc.character_id = $1
|
||||||
|
WHERE gh.guild_id=$2 AND gh.level=2 AND gh.acquired=TRUE
|
||||||
|
`, charID, guildID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
var hunts []*TreasureHunt
|
||||||
|
for rows.Next() {
|
||||||
|
hunt := &TreasureHunt{}
|
||||||
|
if err := rows.StructScan(hunt); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hunts = append(hunts, hunt)
|
||||||
|
}
|
||||||
|
return hunts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateHunt inserts a new guild treasure hunt.
|
||||||
|
func (r *GuildRepository) CreateHunt(guildID, hostID, destination, level uint32, huntData []byte, catsUsed string) error {
|
||||||
|
_, err := r.db.Exec(
|
||||||
|
`INSERT INTO guild_hunts (guild_id, host_id, destination, level, hunt_data, cats_used) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||||
|
guildID, hostID, destination, level, huntData, catsUsed)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AcquireHunt marks a treasure hunt as acquired.
|
||||||
|
func (r *GuildRepository) AcquireHunt(huntID uint32) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_hunts SET acquired=true WHERE id=$1`, huntID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterHuntReport sets a character's active treasure hunt.
|
||||||
|
func (r *GuildRepository) RegisterHuntReport(huntID, charID uint32) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=$1 WHERE character_id=$2`, huntID, charID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CollectHunt marks a hunt as collected and clears all characters' treasure_hunt references.
|
||||||
|
func (r *GuildRepository) CollectHunt(huntID uint32) error {
|
||||||
|
if _, err := r.db.Exec(`UPDATE guild_hunts SET collected=true WHERE id=$1`, huntID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE treasure_hunt=$1`, huntID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClaimHuntReward records that a character has claimed a treasure hunt reward.
|
||||||
|
func (r *GuildRepository) ClaimHuntReward(huntID, charID uint32) error {
|
||||||
|
_, err := r.db.Exec(`INSERT INTO guild_hunts_claimed VALUES ($1, $2)`, huntID, charID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListGuildKills returns kill log entries for guild members since the character's last box claim.
|
||||||
|
func (r *GuildRepository) ListGuildKills(guildID, charID uint32) ([]*GuildKill, error) {
|
||||||
|
rows, err := r.db.Queryx(`SELECT kl.id, kl.monster FROM kill_logs kl
|
||||||
|
INNER JOIN guild_characters gc ON kl.character_id = gc.character_id
|
||||||
|
WHERE gc.guild_id=$1
|
||||||
|
AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2)
|
||||||
|
`, guildID, charID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
var kills []*GuildKill
|
||||||
|
for rows.Next() {
|
||||||
|
kill := &GuildKill{}
|
||||||
|
if err := rows.StructScan(kill); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
kills = append(kills, kill)
|
||||||
|
}
|
||||||
|
return kills, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountGuildKills returns the count of kill log entries for guild members since the character's last box claim.
|
||||||
|
func (r *GuildRepository) CountGuildKills(guildID, charID uint32) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := r.db.QueryRow(`SELECT COUNT(*) FROM kill_logs kl
|
||||||
|
INNER JOIN guild_characters gc ON kl.character_id = gc.character_id
|
||||||
|
WHERE gc.guild_id=$1
|
||||||
|
AND kl.timestamp >= (SELECT box_claimed FROM guild_characters WHERE character_id=$2)
|
||||||
|
`, guildID, charID).Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearTreasureHunt clears the treasure_hunt field for a character on logout.
|
||||||
|
func (r *GuildRepository) ClearTreasureHunt(charID uint32) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE character_id=$1`, charID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertKillLog records a monster kill log entry for a character.
|
||||||
|
func (r *GuildRepository) InsertKillLog(charID uint32, monster int, quantity uint8, timestamp time.Time) error {
|
||||||
|
_, err := r.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, charID, monster, quantity, timestamp)
|
||||||
|
return err
|
||||||
|
}
|
||||||
89
server/channelserver/repo_guild_posts.go
Normal file
89
server/channelserver/repo_guild_posts.go
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListPosts returns active guild posts of the given type, ordered by newest first.
|
||||||
|
func (r *GuildRepository) ListPosts(guildID uint32, postType int) ([]*MessageBoardPost, error) {
|
||||||
|
rows, err := r.db.Queryx(
|
||||||
|
`SELECT id, stamp_id, title, body, author_id, created_at, liked_by
|
||||||
|
FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
|
||||||
|
ORDER BY created_at DESC`, guildID, postType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = rows.Close() }()
|
||||||
|
var posts []*MessageBoardPost
|
||||||
|
for rows.Next() {
|
||||||
|
post := &MessageBoardPost{}
|
||||||
|
if err := rows.StructScan(post); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
posts = append(posts, post)
|
||||||
|
}
|
||||||
|
return posts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatePost inserts a new guild post and soft-deletes excess posts beyond maxPosts.
|
||||||
|
func (r *GuildRepository) CreatePost(guildID, authorID, stampID uint32, postType int, title, body string, maxPosts int) error {
|
||||||
|
tx, err := r.db.BeginTxx(context.Background(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() { _ = tx.Rollback() }()
|
||||||
|
|
||||||
|
if _, err := tx.Exec(
|
||||||
|
`INSERT INTO guild_posts (guild_id, author_id, stamp_id, post_type, title, body) VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||||
|
guildID, authorID, stampID, postType, title, body); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(`UPDATE guild_posts SET deleted = true WHERE id IN (
|
||||||
|
SELECT id FROM guild_posts WHERE guild_id = $1 AND post_type = $2 AND deleted = false
|
||||||
|
ORDER BY created_at DESC OFFSET $3
|
||||||
|
)`, guildID, postType, maxPosts); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeletePost soft-deletes a guild post by ID.
|
||||||
|
func (r *GuildRepository) DeletePost(postID uint32) error {
|
||||||
|
_, err := r.db.Exec("UPDATE guild_posts SET deleted = true WHERE id = $1", postID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePost updates the title and body of a guild post.
|
||||||
|
func (r *GuildRepository) UpdatePost(postID uint32, title, body string) error {
|
||||||
|
_, err := r.db.Exec("UPDATE guild_posts SET title = $1, body = $2 WHERE id = $3", title, body, postID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdatePostStamp updates the stamp of a guild post.
|
||||||
|
func (r *GuildRepository) UpdatePostStamp(postID, stampID uint32) error {
|
||||||
|
_, err := r.db.Exec("UPDATE guild_posts SET stamp_id = $1 WHERE id = $2", stampID, postID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetPostLikedBy returns the liked_by CSV string for a guild post.
|
||||||
|
func (r *GuildRepository) GetPostLikedBy(postID uint32) (string, error) {
|
||||||
|
var likedBy string
|
||||||
|
err := r.db.QueryRow("SELECT liked_by FROM guild_posts WHERE id = $1", postID).Scan(&likedBy)
|
||||||
|
return likedBy, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetPostLikedBy updates the liked_by CSV string for a guild post.
|
||||||
|
func (r *GuildRepository) SetPostLikedBy(postID uint32, likedBy string) error {
|
||||||
|
_, err := r.db.Exec("UPDATE guild_posts SET liked_by = $1 WHERE id = $2", likedBy, postID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// CountNewPosts returns the count of non-deleted posts created after the given time.
|
||||||
|
func (r *GuildRepository) CountNewPosts(guildID uint32, since time.Time) (int, error) {
|
||||||
|
var count int
|
||||||
|
err := r.db.QueryRow(
|
||||||
|
`SELECT COUNT(*) FROM guild_posts WHERE guild_id = $1 AND deleted = false AND (EXTRACT(epoch FROM created_at)::int) > $2`,
|
||||||
|
guildID, since.Unix()).Scan(&count)
|
||||||
|
return count, err
|
||||||
|
}
|
||||||
101
server/channelserver/repo_guild_rp.go
Normal file
101
server/channelserver/repo_guild_rp.go
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
package channelserver
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// AddMemberDailyRP adds RP to a member's daily total.
|
||||||
|
func (r *GuildRepository) AddMemberDailyRP(charID uint32, amount uint16) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guild_characters SET rp_today=rp_today+$1 WHERE character_id=$2`, amount, charID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExchangeEventRP subtracts RP from a guild's event pool and returns the new balance.
|
||||||
|
func (r *GuildRepository) ExchangeEventRP(guildID uint32, amount uint16) (uint32, error) {
|
||||||
|
var balance uint32
|
||||||
|
err := r.db.QueryRow(`UPDATE guilds SET event_rp=event_rp-$1 WHERE id=$2 RETURNING event_rp`, amount, guildID).Scan(&balance)
|
||||||
|
return balance, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRankRP adds RP to a guild's rank total.
|
||||||
|
func (r *GuildRepository) AddRankRP(guildID uint32, amount uint16) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guilds SET rank_rp = rank_rp + $1 WHERE id = $2`, amount, guildID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddEventRP adds RP to a guild's event total.
|
||||||
|
func (r *GuildRepository) AddEventRP(guildID uint32, amount uint16) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guilds SET event_rp = event_rp + $1 WHERE id = $2`, amount, guildID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRoomRP returns the current room RP for a guild.
|
||||||
|
func (r *GuildRepository) GetRoomRP(guildID uint32) (uint16, error) {
|
||||||
|
var rp uint16
|
||||||
|
err := r.db.QueryRow(`SELECT room_rp FROM guilds WHERE id = $1`, guildID).Scan(&rp)
|
||||||
|
return rp, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRoomRP sets the room RP for a guild.
|
||||||
|
func (r *GuildRepository) SetRoomRP(guildID uint32, rp uint16) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guilds SET room_rp = $1 WHERE id = $2`, rp, guildID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddRoomRP atomically adds RP to a guild's room total.
|
||||||
|
func (r *GuildRepository) AddRoomRP(guildID uint32, amount uint16) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guilds SET room_rp = room_rp + $1 WHERE id = $2`, amount, guildID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRoomExpiry sets the room expiry time for a guild.
|
||||||
|
func (r *GuildRepository) SetRoomExpiry(guildID uint32, expiry time.Time) error {
|
||||||
|
_, err := r.db.Exec(`UPDATE guilds SET room_expiry = $1 WHERE id = $2`, expiry, guildID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// RolloverDailyRP moves rp_today into rp_yesterday for all members of a guild,
|
||||||
|
// then updates the guild's rp_reset_at timestamp.
|
||||||
|
// Uses SELECT FOR UPDATE to prevent concurrent rollovers from racing.
|
||||||
|
func (r *GuildRepository) RolloverDailyRP(guildID uint32, noon time.Time) error {
|
||||||
|
tx, err := r.db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Lock the guild row and re-check whether rollover is still needed.
|
||||||
|
var rpResetAt time.Time
|
||||||
|
if err := tx.QueryRow(
|
||||||
|
`SELECT COALESCE(rp_reset_at, '2000-01-01'::timestamptz) FROM guilds WHERE id = $1 FOR UPDATE`,
|
||||||
|
guildID,
|
||||||
|
).Scan(&rpResetAt); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !rpResetAt.Before(noon) {
|
||||||
|
// Another goroutine already rolled over; nothing to do.
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(
|
||||||
|
`UPDATE guild_characters SET rp_yesterday = rp_today, rp_today = 0 WHERE guild_id = $1`,
|
||||||
|
guildID,
|
||||||
|
); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, err := tx.Exec(
|
||||||
|
`UPDATE guilds SET rp_reset_at = $1 WHERE id = $2`,
|
||||||
|
noon, guildID,
|
||||||
|
); err != nil {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddWeeklyBonusUsers atomically adds numUsers to the guild's weekly bonus exceptional user count.
|
||||||
|
func (r *GuildRepository) AddWeeklyBonusUsers(guildID uint32, numUsers uint8) error {
|
||||||
|
_, err := r.db.Exec(
|
||||||
|
"UPDATE guilds SET weekly_bonus_users = weekly_bonus_users + $1 WHERE id = $2",
|
||||||
|
numUsers, guildID,
|
||||||
|
)
|
||||||
|
return err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user