refactor(channelserver): extract GuildRepository for guild table access

Per anti-patterns.md item #9, guild-related SQL was scattered across
~15 handler files with no repository abstraction. Following the same
pattern established by CharacterRepository, this centralizes all
guilds, guild_characters, and guild_applications table access into a
single GuildRepository (~30 methods).

guild_model.go and handlers_guild_member.go are trimmed to types and
pure business logic only. All handler files (guild_*, festa, mail,
house, mercenary, rengoku) now call s.server.guildRepo methods
instead of direct DB queries or methods on domain objects.
This commit is contained in:
Houmgaor
2026-02-20 22:06:55 +01:00
parent d642cbef24
commit 96d07f1c04
21 changed files with 1244 additions and 791 deletions

View File

@@ -1,18 +1,10 @@
package channelserver
import (
"database/sql"
"database/sql/driver"
"encoding/json"
"errors"
"erupe-ce/common/mhfitem"
_config "erupe-ce/config"
"fmt"
"time"
"erupe-ce/common/byteframe"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
// FestivalColor is a festival color identifier string.
@@ -150,486 +142,3 @@ func (g *Guild) Rank(mode _config.Mode) uint16 {
}
return 17
}
const guildInfoSelectQuery = `
SELECT
g.id,
g.name,
rank_rp,
event_rp,
room_rp,
COALESCE(room_expiry, '1970-01-01') AS room_expiry,
main_motto,
sub_motto,
created_at,
leader_id,
c.name AS leader_name,
comment,
COALESCE(pugi_name_1, '') AS pugi_name_1,
COALESCE(pugi_name_2, '') AS pugi_name_2,
COALESCE(pugi_name_3, '') AS pugi_name_3,
pugi_outfit_1,
pugi_outfit_2,
pugi_outfit_3,
pugi_outfits,
recruiting,
COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_color,
COALESCE((SELECT SUM(fs.souls) FROM festa_submissions fs WHERE fs.guild_id=g.id), 0) AS souls,
COALESCE((
SELECT id FROM guild_alliances ga WHERE
ga.parent_id = g.id OR
ga.sub1_id = g.id OR
ga.sub2_id = g.id
), 0) AS alliance_id,
icon,
(SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count
FROM guilds g
JOIN guild_characters gc ON gc.character_id = leader_id
JOIN characters c on leader_id = c.id
`
func (guild *Guild) Save(s *Session) error {
_, err := s.server.db.Exec(`
UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7,
pugi_outfit_1=$8, pugi_outfit_2=$9, pugi_outfit_3=$10, pugi_outfits=$11, icon=$12, leader_id=$13 WHERE id=$1
`, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3,
guild.PugiOutfit1, guild.PugiOutfit2, guild.PugiOutfit3, guild.PugiOutfits, guild.Icon, guild.LeaderCharID)
if err != nil {
s.logger.Error("failed to update guild data", zap.Error(err), zap.Uint32("guildID", guild.ID))
return err
}
return nil
}
func (guild *Guild) CreateApplication(s *Session, charID uint32, applicationType GuildApplicationType, transaction *sql.Tx) error {
query := `
INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type)
VALUES ($1, $2, $3, $4)
`
var err error
if transaction == nil {
_, err = s.server.db.Exec(query, guild.ID, charID, s.charID, applicationType)
} else {
_, err = transaction.Exec(query, guild.ID, charID, s.charID, applicationType)
}
if err != nil {
s.logger.Error(
"failed to add guild application",
zap.Error(err),
zap.Uint32("guildID", guild.ID),
zap.Uint32("charID", charID),
)
return err
}
return nil
}
func (guild *Guild) Disband(s *Session) error {
transaction, err := s.server.db.Begin()
if err != nil {
s.logger.Error("failed to begin transaction", zap.Error(err))
return err
}
_, err = transaction.Exec("DELETE FROM guild_characters WHERE guild_id = $1", guild.ID)
if err != nil {
s.logger.Error("failed to remove guild characters", zap.Error(err), zap.Uint32("guildId", guild.ID))
rollbackTransaction(s, transaction)
return err
}
_, err = transaction.Exec("DELETE FROM guilds WHERE id = $1", guild.ID)
if err != nil {
s.logger.Error("failed to remove guild", zap.Error(err), zap.Uint32("guildID", guild.ID))
rollbackTransaction(s, transaction)
return err
}
_, err = transaction.Exec("DELETE FROM guild_alliances WHERE parent_id=$1", guild.ID)
if err != nil {
s.logger.Error("failed to remove guild alliance", zap.Error(err), zap.Uint32("guildID", guild.ID))
rollbackTransaction(s, transaction)
return err
}
_, err = transaction.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guild.ID)
if err != nil {
s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID))
rollbackTransaction(s, transaction)
return err
}
_, err = transaction.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guild.ID)
if err != nil {
s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID))
rollbackTransaction(s, transaction)
return err
}
err = transaction.Commit()
if err != nil {
s.logger.Error("failed to commit transaction", zap.Error(err))
return err
}
s.logger.Info("Character disbanded guild", zap.Uint32("charID", s.charID), zap.Uint32("guildID", guild.ID))
return nil
}
func (guild *Guild) RemoveCharacter(s *Session, charID uint32) error {
_, err := s.server.db.Exec("DELETE FROM guild_characters WHERE character_id=$1", charID)
if err != nil {
s.logger.Error(
"failed to remove character from guild",
zap.Error(err),
zap.Uint32("charID", charID),
zap.Uint32("guildID", guild.ID),
)
return err
}
return nil
}
func (guild *Guild) AcceptApplication(s *Session, charID uint32) error {
transaction, err := s.server.db.Begin()
if err != nil {
s.logger.Error("failed to start db transaction", zap.Error(err))
return err
}
_, err = transaction.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID)
if err != nil {
s.logger.Error("failed to accept character's guild application", zap.Error(err))
rollbackTransaction(s, transaction)
return err
}
_, err = transaction.Exec(`
INSERT INTO guild_characters (guild_id, character_id, order_index)
VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1))
`, guild.ID, charID)
if err != nil {
s.logger.Error(
"failed to add applicant to guild",
zap.Error(err),
zap.Uint32("guildID", guild.ID),
zap.Uint32("charID", charID),
)
rollbackTransaction(s, transaction)
return err
}
err = transaction.Commit()
if err != nil {
s.logger.Error("failed to commit db transaction", zap.Error(err))
rollbackTransaction(s, transaction)
return err
}
return nil
}
// This is relying on the fact that invitation ID is also character ID right now
// if invitation ID changes, this will break.
func (guild *Guild) CancelInvitation(s *Session, charID uint32) error {
_, err := s.server.db.Exec(
`DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`,
charID, guild.ID,
)
if err != nil {
s.logger.Error(
"failed to cancel guild invitation",
zap.Error(err),
zap.Uint32("guildID", guild.ID),
zap.Uint32("charID", charID),
)
return err
}
return nil
}
func (guild *Guild) RejectApplication(s *Session, charID uint32) error {
_, err := s.server.db.Exec(
`DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'applied'`,
charID, guild.ID,
)
if err != nil {
s.logger.Error(
"failed to reject guild application",
zap.Error(err),
zap.Uint32("guildID", guild.ID),
zap.Uint32("charID", charID),
)
return err
}
return nil
}
func (guild *Guild) ArrangeCharacters(s *Session, charIDs []uint32) error {
transaction, err := s.server.db.Begin()
if err != nil {
s.logger.Error("failed to start db transaction", zap.Error(err))
return err
}
for i, id := range charIDs {
_, err := transaction.Exec("UPDATE guild_characters SET order_index = $1 WHERE character_id = $2", 2+i, id)
if err != nil {
err = transaction.Rollback()
if err != nil {
s.logger.Error("failed to rollback db transaction", zap.Error(err))
}
return err
}
}
err = transaction.Commit()
if err != nil {
s.logger.Error("failed to commit db transaction", zap.Error(err))
return err
}
return nil
}
func (guild *Guild) GetApplicationForCharID(s *Session, charID uint32, applicationType GuildApplicationType) (*GuildApplication, error) {
row := s.server.db.QueryRowx(`
SELECT * from guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = $3
`, charID, guild.ID, applicationType)
application := &GuildApplication{}
err := row.StructScan(application)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
s.logger.Error(
"failed to retrieve guild application for character",
zap.Error(err),
zap.Uint32("charID", charID),
zap.Uint32("guildID", guild.ID),
)
return nil, err
}
return application, nil
}
func (guild *Guild) HasApplicationForCharID(s *Session, charID uint32) (bool, error) {
row := s.server.db.QueryRowx(`
SELECT 1 from guild_applications WHERE character_id = $1 AND guild_id = $2
`, charID, guild.ID)
num := 0
err := row.Scan(&num)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
if err != nil {
s.logger.Error(
"failed to retrieve guild applications for character",
zap.Error(err),
zap.Uint32("charID", charID),
zap.Uint32("guildID", guild.ID),
)
return false, err
}
return true, nil
}
// CreateGuild creates a new guild in the database and adds the session's character as its leader.
func CreateGuild(s *Session, guildName string) (int32, error) {
transaction, err := s.server.db.Begin()
if err != nil {
s.logger.Error("failed to start db transaction", zap.Error(err))
return 0, err
}
guildResult, err := transaction.Query(
"INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id",
guildName, s.charID,
)
if err != nil {
s.logger.Error("failed to create guild", zap.Error(err))
rollbackTransaction(s, transaction)
return 0, err
}
var guildId int32
guildResult.Next()
err = guildResult.Scan(&guildId)
if err != nil {
s.logger.Error("failed to retrieve guild ID", zap.Error(err))
rollbackTransaction(s, transaction)
return 0, err
}
err = guildResult.Close()
if err != nil {
s.logger.Error("failed to finalise query", zap.Error(err))
rollbackTransaction(s, transaction)
return 0, err
}
_, err = transaction.Exec(`
INSERT INTO guild_characters (guild_id, character_id)
VALUES ($1, $2)
`, guildId, s.charID)
if err != nil {
s.logger.Error("failed to add character to guild", zap.Error(err))
rollbackTransaction(s, transaction)
return 0, err
}
err = transaction.Commit()
if err != nil {
s.logger.Error("failed to commit guild creation", zap.Error(err))
return 0, err
}
return guildId, nil
}
func rollbackTransaction(s *Session, transaction *sql.Tx) {
err := transaction.Rollback()
if err != nil {
s.logger.Error("failed to rollback transaction", zap.Error(err))
}
}
// GetGuildInfoByID retrieves guild info by guild ID, returning nil if not found.
func GetGuildInfoByID(s *Session, guildID uint32) (*Guild, error) {
rows, err := s.server.db.Queryx(fmt.Sprintf(`
%s
WHERE g.id = $1
LIMIT 1
`, guildInfoSelectQuery), guildID)
if err != nil {
s.logger.Error("failed to retrieve guild", zap.Error(err), zap.Uint32("guildID", guildID))
return nil, err
}
defer func() { _ = rows.Close() }()
hasRow := rows.Next()
if !hasRow {
return nil, nil
}
return buildGuildObjectFromDbResult(rows, err, s)
}
// GetGuildInfoByCharacterId retrieves guild info for a character, including applied guilds.
func GetGuildInfoByCharacterId(s *Session, charID uint32) (*Guild, error) {
rows, err := s.server.db.Queryx(fmt.Sprintf(`
%s
WHERE EXISTS(
SELECT 1
FROM guild_characters gc1
WHERE gc1.character_id = $1
AND gc1.guild_id = g.id
)
OR EXISTS(
SELECT 1
FROM guild_applications ga
WHERE ga.character_id = $1
AND ga.guild_id = g.id
AND ga.application_type = 'applied'
)
LIMIT 1
`, guildInfoSelectQuery), charID)
if err != nil {
s.logger.Error("failed to retrieve guild for character", zap.Error(err), zap.Uint32("charID", charID))
return nil, err
}
defer func() { _ = rows.Close() }()
hasRow := rows.Next()
if !hasRow {
return nil, nil
}
return buildGuildObjectFromDbResult(rows, err, s)
}
func buildGuildObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*Guild, error) {
guild := &Guild{}
err := result.StructScan(guild)
if err != nil {
s.logger.Error("failed to retrieve guild data from database", zap.Error(err))
return nil, err
}
return guild, nil
}
func guildGetItems(s *Session, guildID uint32) []mhfitem.MHFItemStack {
var data []byte
var items []mhfitem.MHFItemStack
if err := s.server.db.QueryRow(`SELECT item_box FROM guilds WHERE id=$1`, guildID).Scan(&data); err != nil && !errors.Is(err, sql.ErrNoRows) {
s.logger.Error("Failed to get guild item box", zap.Error(err))
}
if len(data) > 0 {
box := byteframe.NewByteFrameFromBytes(data)
numStacks := box.ReadUint16()
box.ReadUint16() // Unused
for i := 0; i < int(numStacks); i++ {
items = append(items, mhfitem.ReadWarehouseItem(box))
}
}
return items
}

View File

@@ -389,10 +389,10 @@ func handleMsgMhfInfoFesta(s *Session, p mhfpacket.MHFPacket) {
// state festa (U)ser
func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfStateFestaU)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
applicant := false
if guild != nil {
applicant, _ = guild.HasApplicationForCharID(s, s.charID)
applicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID)
}
if err != nil || guild == nil || applicant {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
@@ -418,10 +418,10 @@ func handleMsgMhfStateFestaU(s *Session, p mhfpacket.MHFPacket) {
// state festa (G)uild
func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfStateFestaG)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
applicant := false
if guild != nil {
applicant, _ = guild.HasApplicationForCharID(s, s.charID)
applicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID)
}
resp := byteframe.NewByteFrame()
if err != nil || guild == nil || applicant {
@@ -443,12 +443,12 @@ func handleMsgMhfStateFestaG(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfEnumerateFestaMember(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateFestaMember)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
if err != nil || guild == nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
members, err := GetGuildMembers(s, guild.ID, false)
members, err := s.server.guildRepo.GetMembers(guild.ID, false)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
@@ -487,7 +487,7 @@ func handleMsgMhfVoteFesta(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfEntryFesta(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEntryFesta)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
if err != nil || guild == nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return

View File

@@ -14,7 +14,7 @@ import (
func handleMsgMhfCreateGuild(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfCreateGuild)
guildId, err := CreateGuild(s, pkt.Name)
guildId, err := s.server.guildRepo.Create(s.charID, pkt.Name)
if err != nil {
bf := byteframe.NewByteFrame()
@@ -37,7 +37,7 @@ func handleMsgMhfCreateGuild(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfArrangeGuildMember(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfArrangeGuildMember)
guild, err := GetGuildInfoByID(s, pkt.GuildID)
guild, err := s.server.guildRepo.GetByID(pkt.GuildID)
if err != nil {
s.logger.Error(
@@ -57,7 +57,7 @@ func handleMsgMhfArrangeGuildMember(s *Session, p mhfpacket.MHFPacket) {
return
}
err = guild.ArrangeCharacters(s, pkt.CharIDs)
err = s.server.guildRepo.ArrangeCharacters(pkt.CharIDs)
if err != nil {
s.logger.Error(
@@ -79,13 +79,13 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) {
var err error
if pkt.GuildID > 0 {
guild, err = GetGuildInfoByID(s, pkt.GuildID)
guild, err = s.server.guildRepo.GetByID(pkt.GuildID)
} else {
guild, err = GetGuildInfoByCharacterId(s, s.charID)
guild, err = s.server.guildRepo.GetByCharID(s.charID)
}
if guild != nil {
isApplicant, _ := guild.HasApplicationForCharID(s, s.charID)
isApplicant, _ := s.server.guildRepo.HasApplication(guild.ID, s.charID)
if isApplicant {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 2))
return
@@ -93,7 +93,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) {
}
if guild == nil && s.prevGuildID > 0 {
guild, err = GetGuildInfoByID(s, s.prevGuildID)
guild, err = s.server.guildRepo.GetByID(s.prevGuildID)
}
if err != nil {
@@ -105,7 +105,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) {
return
}
guildMembers, err := GetGuildMembers(s, guild.ID, false)
guildMembers, err := s.server.guildRepo.GetMembers(guild.ID, false)
if err != nil {
s.logger.Error("failed to retrieve guild")
@@ -157,7 +157,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) {
if guild.AllianceID > 0 {
bf.WriteUint16(alliance.TotalMembers - uint16(len(guildMembers)))
if guild.ID != alliance.ParentGuildID {
mems, err := GetGuildMembers(s, alliance.ParentGuildID, false)
mems, err := s.server.guildRepo.GetMembers(alliance.ParentGuildID, false)
if err != nil {
s.logger.Error("Failed to get parent guild members for alliance", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
@@ -168,7 +168,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) {
}
}
if guild.ID != alliance.SubGuild1ID {
mems, err := GetGuildMembers(s, alliance.SubGuild1ID, false)
mems, err := s.server.guildRepo.GetMembers(alliance.SubGuild1ID, false)
if err != nil {
s.logger.Error("Failed to get sub guild 1 members for alliance", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
@@ -179,7 +179,7 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) {
}
}
if guild.ID != alliance.SubGuild2ID {
mems, err := GetGuildMembers(s, alliance.SubGuild2ID, false)
mems, err := s.server.guildRepo.GetMembers(alliance.SubGuild2ID, false)
if err != nil {
s.logger.Error("Failed to get sub guild 2 members for alliance", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
@@ -204,9 +204,9 @@ func handleMsgMhfEnumerateGuildMember(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetGuildManageRight(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetGuildManageRight)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
if guild == nil || s.prevGuildID != 0 {
guild, err := GetGuildInfoByID(s, s.prevGuildID)
guild, err := s.server.guildRepo.GetByID(s.prevGuildID)
s.prevGuildID = 0
if guild == nil || err != nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
@@ -216,7 +216,7 @@ func handleMsgMhfGetGuildManageRight(s *Session, p mhfpacket.MHFPacket) {
bf := byteframe.NewByteFrame()
bf.WriteUint32(uint32(guild.MemberCount))
members, _ := GetGuildMembers(s, guild.ID, false)
members, _ := s.server.guildRepo.GetMembers(guild.ID, false)
for _, member := range members {
bf.WriteUint32(member.CharID)
bf.WriteBool(member.Recruiter)
@@ -237,9 +237,9 @@ func handleMsgMhfGetGuildTargetMemberNum(s *Session, p mhfpacket.MHFPacket) {
var err error
if pkt.GuildID == 0x0 {
guild, err = GetGuildInfoByCharacterId(s, s.charID)
guild, err = s.server.guildRepo.GetByCharID(s.charID)
} else {
guild, err = GetGuildInfoByID(s, pkt.GuildID)
guild, err = s.server.guildRepo.GetByID(pkt.GuildID)
}
if err != nil || guild == nil {
@@ -266,7 +266,7 @@ func handleMsgMhfEnumerateGuildItem(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfUpdateGuildItem)
newStacks := mhfitem.DiffItemStacks(guildGetItems(s, pkt.GuildID), pkt.UpdatedItems)
if _, err := s.server.db.Exec(`UPDATE guilds SET item_box=$1 WHERE id=$2`, mhfitem.SerializeWarehouseItems(newStacks), pkt.GuildID); err != nil {
if err := s.server.guildRepo.SaveItemBox(pkt.GuildID, mhfitem.SerializeWarehouseItems(newStacks)); err != nil {
s.logger.Error("Failed to update guild item box", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
@@ -275,7 +275,7 @@ func handleMsgMhfUpdateGuildItem(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfUpdateGuildIcon(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfUpdateGuildIcon)
guild, err := GetGuildInfoByID(s, pkt.GuildID)
guild, err := s.server.guildRepo.GetByID(pkt.GuildID)
if err != nil {
s.logger.Error("Failed to get guild info for icon update", zap.Error(err))
@@ -283,7 +283,7 @@ func handleMsgMhfUpdateGuildIcon(s *Session, p mhfpacket.MHFPacket) {
return
}
characterInfo, err := GetCharacterGuildData(s, s.charID)
characterInfo, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil {
s.logger.Error("Failed to get character guild data for icon update", zap.Error(err))
@@ -322,7 +322,7 @@ func handleMsgMhfUpdateGuildIcon(s *Session, p mhfpacket.MHFPacket) {
guild.Icon = icon
err = guild.Save(s)
err = s.server.guildRepo.Save(guild)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
@@ -364,7 +364,7 @@ func handleMsgMhfUpdateGuild(s *Session, p mhfpacket.MHFPacket) {}
func handleMsgMhfSetGuildManageRight(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfSetGuildManageRight)
if _, err := s.server.db.Exec("UPDATE guild_characters SET recruiter=$1 WHERE character_id=$2", pkt.Allowed, pkt.CharID); err != nil {
if err := s.server.guildRepo.SetRecruiter(pkt.CharID, pkt.Allowed); err != nil {
s.logger.Error("Failed to update guild manage right", zap.Error(err))
}
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
@@ -393,3 +393,22 @@ func handleMsgMhfOperationInvGuild(s *Session, p mhfpacket.MHFPacket) {
}
func handleMsgMhfUpdateGuildcard(s *Session, p mhfpacket.MHFPacket) {}
// guildGetItems reads and parses the guild item box.
func guildGetItems(s *Session, guildID uint32) []mhfitem.MHFItemStack {
data, err := s.server.guildRepo.GetItemBox(guildID)
if err != nil {
s.logger.Error("Failed to get guild item box", zap.Error(err))
return nil
}
var items []mhfitem.MHFItemStack
if len(data) > 0 {
box := byteframe.NewByteFrameFromBytes(data)
numStacks := box.ReadUint16()
box.ReadUint16() // Unused
for i := 0; i < int(numStacks); i++ {
items = append(items, mhfitem.ReadWarehouseItem(box))
}
}
return items
}

View File

@@ -21,7 +21,7 @@ type GuildAdventure struct {
func handleMsgMhfLoadGuildAdventure(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadGuildAdventure)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
data, err := s.server.db.Queryx("SELECT id, destination, charge, depart, return, collected_by FROM guild_adventures WHERE guild_id = $1", guild.ID)
if err != nil {
s.logger.Error("Failed to get guild adventures from db", zap.Error(err))
@@ -52,7 +52,7 @@ func handleMsgMhfLoadGuildAdventure(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfRegistGuildAdventure(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfRegistGuildAdventure)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
_, err := s.server.db.Exec("INSERT INTO guild_adventures (guild_id, destination, depart, return) VALUES ($1, $2, $3, $4)", guild.ID, pkt.Destination, TimeAdjusted().Unix(), TimeAdjusted().Add(6*time.Hour).Unix())
if err != nil {
s.logger.Error("Failed to register guild adventure", zap.Error(err))
@@ -87,7 +87,7 @@ func handleMsgMhfChargeGuildAdventure(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfRegistGuildAdventureDiva(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfRegistGuildAdventureDiva)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
_, err := s.server.db.Exec("INSERT INTO guild_adventures (guild_id, destination, charge, depart, return) VALUES ($1, $2, $3, $4, $5)", guild.ID, pkt.Destination, pkt.Charge, TimeAdjusted().Unix(), TimeAdjusted().Add(1*time.Hour).Unix())
if err != nil {
s.logger.Error("Failed to register guild adventure", zap.Error(err))

View File

@@ -73,7 +73,7 @@ func buildAllianceObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*G
return nil, err
}
parentGuild, err := GetGuildInfoByID(s, alliance.ParentGuildID)
parentGuild, err := s.server.guildRepo.GetByID(alliance.ParentGuildID)
if err != nil {
s.logger.Error("Failed to get parent guild info", zap.Error(err))
return nil, err
@@ -83,7 +83,7 @@ func buildAllianceObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*G
}
if alliance.SubGuild1ID > 0 {
subGuild1, err := GetGuildInfoByID(s, alliance.SubGuild1ID)
subGuild1, err := s.server.guildRepo.GetByID(alliance.SubGuild1ID)
if err != nil {
s.logger.Error("Failed to get sub guild 1 info", zap.Error(err))
return nil, err
@@ -94,7 +94,7 @@ func buildAllianceObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*G
}
if alliance.SubGuild2ID > 0 {
subGuild2, err := GetGuildInfoByID(s, alliance.SubGuild2ID)
subGuild2, err := s.server.guildRepo.GetByID(alliance.SubGuild2ID)
if err != nil {
s.logger.Error("Failed to get sub guild 2 info", zap.Error(err))
return nil, err
@@ -119,7 +119,7 @@ func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateJoint)
guild, err := GetGuildInfoByID(s, pkt.GuildID)
guild, err := s.server.guildRepo.GetByID(pkt.GuildID)
if err != nil {
s.logger.Error("Failed to get guild info", zap.Error(err))
}

View File

@@ -23,7 +23,7 @@ type MessageBoardPost struct {
func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateGuildMessageBoard)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
if pkt.BoardType == 1 {
pkt.MaxPosts = 4
}
@@ -63,10 +63,10 @@ func handleMsgMhfEnumerateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfUpdateGuildMessageBoard(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfUpdateGuildMessageBoard)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
applicant := false
if guild != nil {
applicant, _ = guild.HasApplicationForCharID(s, s.charID)
applicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID)
}
if err != nil || guild == nil || applicant {
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))

View File

@@ -18,7 +18,7 @@ type GuildMeal struct {
func handleMsgMhfLoadGuildCooking(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfLoadGuildCooking)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
data, err := s.server.db.Queryx("SELECT id, meal_id, level, created_at FROM guild_meals WHERE guild_id = $1", guild.ID)
if err != nil {
s.logger.Error("Failed to get guild meals from db", zap.Error(err))
@@ -49,7 +49,7 @@ func handleMsgMhfLoadGuildCooking(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfRegistGuildCooking(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfRegistGuildCooking)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
startTime := TimeAdjusted().Add(time.Duration(s.server.erupeConfig.GameplayOptions.ClanMealDuration-3600) * time.Second)
if pkt.OverwriteID != 0 {
if _, err := s.server.db.Exec("UPDATE guild_meals SET meal_id = $1, level = $2, created_at = $3 WHERE id = $4", pkt.MealID, pkt.Success, startTime, pkt.OverwriteID); err != nil {
@@ -121,7 +121,7 @@ func handleMsgMhfGuildHuntdata(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint8(count)
}
case 2: // Check
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
if err == nil {
var count uint8
err = s.server.db.QueryRow(`SELECT COUNT(*) FROM kill_logs kl

View File

@@ -9,7 +9,6 @@ import (
"erupe-ce/common/stringsupport"
_config "erupe-ce/config"
"erupe-ce/network/mhfpacket"
"github.com/jmoiron/sqlx"
)
// Guild sentinel and cost constants
@@ -25,9 +24,9 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) {
var err error
if pkt.GuildID > 0 {
guild, err = GetGuildInfoByID(s, pkt.GuildID)
guild, err = s.server.guildRepo.GetByID(pkt.GuildID)
} else {
guild, err = GetGuildInfoByCharacterId(s, s.charID)
guild, err = s.server.guildRepo.GetByCharID(s.charID)
}
if err == nil && guild != nil {
@@ -37,7 +36,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) {
guildComment := stringsupport.UTF8ToSJIS(guild.Comment)
guildLeaderName := stringsupport.UTF8ToSJIS(guild.LeaderName)
characterGuildData, err := GetCharacterGuildData(s, s.charID)
characterGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID)
characterJoinedAt := guildNotJoinedSentinel
if characterGuildData != nil && characterGuildData.JoinedAt != nil {
@@ -196,7 +195,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) {
bf.WriteUint32(0) // No alliance
}
applicants, err := GetGuildMembers(s, guild.ID, true)
applicants, err := s.server.guildRepo.GetMembers(guild.ID, true)
if err != nil || (characterGuildData != nil && !characterGuildData.CanRecruit()) {
bf.WriteUint16(0)
} else {
@@ -280,89 +279,81 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) {
var guilds []*Guild
var alliances []*GuildAlliance
var rows *sqlx.Rows
var err error
if pkt.Type <= 8 {
var tempGuilds []*Guild
rows, err = s.server.db.Queryx(guildInfoSelectQuery)
tempGuilds, err = s.server.guildRepo.ListAll()
if err == nil {
for rows.Next() {
guild, err := buildGuildObjectFromDbResult(rows, err, s)
if err != nil {
continue
switch pkt.Type {
case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME:
searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes())
for _, guild := range tempGuilds {
if strings.Contains(guild.Name, searchName) {
guilds = append(guilds, guild)
}
}
tempGuilds = append(tempGuilds, guild)
}
}
switch pkt.Type {
case mhfpacket.ENUMERATE_GUILD_TYPE_GUILD_NAME:
searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes())
for _, guild := range tempGuilds {
if strings.Contains(guild.Name, searchName) {
guilds = append(guilds, guild)
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME:
searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes())
for _, guild := range tempGuilds {
if strings.Contains(guild.LeaderName, searchName) {
guilds = append(guilds, guild)
}
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_NAME:
searchName, _ := stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes())
for _, guild := range tempGuilds {
if strings.Contains(guild.LeaderName, searchName) {
guilds = append(guilds, guild)
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID:
CID := pkt.Data1.ReadUint32()
for _, guild := range tempGuilds {
if guild.LeaderCharID == CID {
guilds = append(guilds, guild)
}
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_LEADER_ID:
CID := pkt.Data1.ReadUint32()
for _, guild := range tempGuilds {
if guild.LeaderCharID == CID {
guilds = append(guilds, guild)
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS:
if pkt.Sorting {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].MemberCount > tempGuilds[j].MemberCount
})
} else {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].MemberCount < tempGuilds[j].MemberCount
})
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_MEMBERS:
if pkt.Sorting {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].MemberCount > tempGuilds[j].MemberCount
})
} else {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].MemberCount < tempGuilds[j].MemberCount
})
}
guilds = tempGuilds
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION:
if pkt.Sorting {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].CreatedAt.Unix() > tempGuilds[j].CreatedAt.Unix()
})
} else {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].CreatedAt.Unix() < tempGuilds[j].CreatedAt.Unix()
})
}
guilds = tempGuilds
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK:
if pkt.Sorting {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].RankRP > tempGuilds[j].RankRP
})
} else {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].RankRP < tempGuilds[j].RankRP
})
}
guilds = tempGuilds
case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO:
mainMotto := uint8(pkt.Data1.ReadUint16())
subMotto := uint8(pkt.Data1.ReadUint16())
for _, guild := range tempGuilds {
if guild.MainMotto == mainMotto && guild.SubMotto == subMotto {
guilds = append(guilds, guild)
guilds = tempGuilds
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_REGISTRATION:
if pkt.Sorting {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].CreatedAt.Unix() > tempGuilds[j].CreatedAt.Unix()
})
} else {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].CreatedAt.Unix() < tempGuilds[j].CreatedAt.Unix()
})
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING:
recruitingMotto := uint8(pkt.Data1.ReadUint16())
for _, guild := range tempGuilds {
if guild.MainMotto == recruitingMotto {
guilds = append(guilds, guild)
guilds = tempGuilds
case mhfpacket.ENUMERATE_GUILD_TYPE_ORDER_RANK:
if pkt.Sorting {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].RankRP > tempGuilds[j].RankRP
})
} else {
sort.Slice(tempGuilds, func(i, j int) bool {
return tempGuilds[i].RankRP < tempGuilds[j].RankRP
})
}
guilds = tempGuilds
case mhfpacket.ENUMERATE_GUILD_TYPE_MOTTO:
mainMotto := uint8(pkt.Data1.ReadUint16())
subMotto := uint8(pkt.Data1.ReadUint16())
for _, guild := range tempGuilds {
if guild.MainMotto == mainMotto && guild.SubMotto == subMotto {
guilds = append(guilds, guild)
}
}
case mhfpacket.ENUMERATE_GUILD_TYPE_RECRUITING:
recruitingMotto := uint8(pkt.Data1.ReadUint16())
for _, guild := range tempGuilds {
if guild.MainMotto == recruitingMotto {
guilds = append(guilds, guild)
}
}
}
}
@@ -370,10 +361,12 @@ func handleMsgMhfEnumerateGuild(s *Session, p mhfpacket.MHFPacket) {
if pkt.Type > 8 {
var tempAlliances []*GuildAlliance
rows, err = s.server.db.Queryx(allianceInfoSelectQuery)
if err == nil {
rows, queryErr := s.server.db.Queryx(allianceInfoSelectQuery)
if queryErr != nil {
err = queryErr
} else {
for rows.Next() {
alliance, _ := buildAllianceObjectFromDbResult(rows, err, s)
alliance, _ := buildAllianceObjectFromDbResult(rows, queryErr, s)
tempAlliances = append(tempAlliances, alliance)
}
}

View File

@@ -1,11 +1,7 @@
package channelserver
import (
"fmt"
"time"
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
// GuildMember represents a guild member with role and stats.
@@ -45,112 +41,3 @@ func (gm *GuildMember) CanRecruit() bool {
func (gm *GuildMember) IsSubLeader() bool {
return gm.OrderIndex <= 3
}
func (gm *GuildMember) Save(s *Session) error {
_, err := s.server.db.Exec("UPDATE guild_characters SET avoid_leadership=$1, order_index=$2 WHERE character_id=$3", gm.AvoidLeadership, gm.OrderIndex, gm.CharID)
if err != nil {
s.logger.Error(
"failed to update guild member data",
zap.Error(err),
zap.Uint32("charID", gm.CharID),
zap.Uint32("guildID", gm.GuildID),
)
return err
}
return nil
}
const guildMembersSelectSQL = `
SELECT
COALESCE(g.id, 0) AS guild_id,
joined_at,
COALESCE((SELECT SUM(souls) FROM festa_submissions fs WHERE fs.character_id=c.id), 0) AS souls,
COALESCE(rp_today, 0) AS rp_today,
COALESCE(rp_yesterday, 0) AS rp_yesterday,
c.name,
c.id AS character_id,
COALESCE(order_index, 0) AS order_index,
c.last_login,
COALESCE(recruiter, false) AS recruiter,
COALESCE(avoid_leadership, false) AS avoid_leadership,
c.hr,
c.gr,
c.weapon_id,
c.weapon_type,
CASE WHEN g.leader_id = c.id THEN true ELSE false END AS is_leader,
character.is_applicant
FROM (
SELECT character_id, true as is_applicant, guild_id
FROM guild_applications ga
WHERE ga.application_type = 'applied'
UNION
SELECT character_id, false as is_applicant, guild_id
FROM guild_characters gc
) character
JOIN characters c on character.character_id = c.id
LEFT JOIN guild_characters gc ON gc.character_id = character.character_id
LEFT JOIN guilds g ON g.id = gc.guild_id
`
// GetGuildMembers loads all members of a guild.
func GetGuildMembers(s *Session, guildID uint32, applicants bool) ([]*GuildMember, error) {
rows, err := s.server.db.Queryx(fmt.Sprintf(`
%s
WHERE character.guild_id = $1 AND is_applicant = $2
`, guildMembersSelectSQL), guildID, applicants)
if err != nil {
s.logger.Error("failed to retrieve membership data for guild", zap.Error(err), zap.Uint32("guildID", guildID))
return nil, err
}
defer func() { _ = rows.Close() }()
members := make([]*GuildMember, 0)
for rows.Next() {
member, err := buildGuildMemberObjectFromDBResult(rows, err, s)
if err != nil {
return nil, err
}
members = append(members, member)
}
return members, nil
}
// GetCharacterGuildData loads a character's guild membership.
func GetCharacterGuildData(s *Session, charID uint32) (*GuildMember, error) {
rows, err := s.server.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID)
if err != nil {
s.logger.Error(fmt.Sprintf("failed to retrieve membership data for character '%d'", charID))
return nil, err
}
defer func() { _ = rows.Close() }()
hasRow := rows.Next()
if !hasRow {
return nil, nil
}
return buildGuildMemberObjectFromDBResult(rows, err, s)
}
func buildGuildMemberObjectFromDBResult(rows *sqlx.Rows, _ error, s *Session) (*GuildMember, error) {
memberData := &GuildMember{}
err := rows.StructScan(&memberData)
if err != nil {
s.logger.Error("failed to retrieve guild data from database", zap.Error(err))
return nil, err
}
return memberData, nil
}

View File

@@ -14,12 +14,12 @@ import (
func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateGuild)
guild, err := GetGuildInfoByID(s, pkt.GuildID)
guild, err := s.server.guildRepo.GetByID(pkt.GuildID)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
characterGuildInfo, err := GetCharacterGuildData(s, s.charID)
characterGuildInfo, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
@@ -34,14 +34,14 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
s.logger.Warn(fmt.Sprintf("character '%d' is attempting to manage guild '%d' without permission", s.charID, guild.ID))
response = 0
} else {
err = guild.Disband(s)
err = s.server.guildRepo.Disband(guild.ID)
if err != nil {
response = 0
}
}
bf.WriteUint32(uint32(response))
case mhfpacket.OperateGuildResign:
guildMembers, err := GetGuildMembers(s, guild.ID, false)
guildMembers, err := s.server.guildRepo.GetMembers(guild.ID, false)
if err == nil {
sort.Slice(guildMembers[:], func(i, j int) bool {
return guildMembers[i].OrderIndex < guildMembers[j].OrderIndex
@@ -51,16 +51,16 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
guild.LeaderCharID = guildMembers[i].CharID
guildMembers[0].OrderIndex = guildMembers[i].OrderIndex
guildMembers[i].OrderIndex = 1
_ = guildMembers[0].Save(s)
_ = guildMembers[i].Save(s)
_ = s.server.guildRepo.SaveMember(guildMembers[0])
_ = s.server.guildRepo.SaveMember(guildMembers[i])
bf.WriteUint32(guildMembers[i].CharID)
break
}
}
_ = guild.Save(s)
_ = s.server.guildRepo.Save(guild)
}
case mhfpacket.OperateGuildApply:
err = guild.CreateApplication(s, s.charID, GuildApplicationTypeApplied, nil)
err = s.server.guildRepo.CreateApplication(guild.ID, s.charID, s.charID, GuildApplicationTypeApplied, nil)
if err == nil {
bf.WriteUint32(guild.LeaderCharID)
} else {
@@ -68,9 +68,9 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
}
case mhfpacket.OperateGuildLeave:
if characterGuildInfo.IsApplicant {
err = guild.RejectApplication(s, s.charID)
err = s.server.guildRepo.RejectApplication(guild.ID, s.charID)
} else {
err = guild.RemoveCharacter(s, s.charID)
err = s.server.guildRepo.RemoveCharacter(s.charID)
}
response := 1
if err != nil {
@@ -88,11 +88,11 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
case mhfpacket.OperateGuildDonateRank:
bf.WriteBytes(handleDonateRP(s, uint16(pkt.Data1.ReadUint32()), guild, 0))
case mhfpacket.OperateGuildSetApplicationDeny:
if _, err := s.server.db.Exec("UPDATE guilds SET recruiting=false WHERE id=$1", guild.ID); err != nil {
if err := s.server.guildRepo.SetRecruiting(guild.ID, false); err != nil {
s.logger.Error("Failed to deny guild applications", zap.Error(err))
}
case mhfpacket.OperateGuildSetApplicationAllow:
if _, err := s.server.db.Exec("UPDATE guilds SET recruiting=true WHERE id=$1", guild.ID); err != nil {
if err := s.server.guildRepo.SetRecruiting(guild.ID, true); err != nil {
s.logger.Error("Failed to allow guild applications", zap.Error(err))
}
case mhfpacket.OperateGuildSetAvoidLeadershipTrue:
@@ -105,7 +105,7 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
return
}
guild.Comment, _ = stringsupport.SJISToUTF8(pkt.Data2.ReadNullTerminatedBytes())
_ = guild.Save(s)
_ = s.server.guildRepo.Save(guild)
case mhfpacket.OperateGuildUpdateMotto:
if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
@@ -114,7 +114,7 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
_ = pkt.Data1.ReadUint16()
guild.SubMotto = pkt.Data1.ReadUint8()
guild.MainMotto = pkt.Data1.ReadUint8()
_ = guild.Save(s)
_ = s.server.guildRepo.Save(guild)
case mhfpacket.OperateGuildRenamePugi1:
handleRenamePugi(s, pkt.Data2, guild, 1)
case mhfpacket.OperateGuildRenamePugi2:
@@ -128,7 +128,7 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
case mhfpacket.OperateGuildChangePugi3:
handleChangePugi(s, uint8(pkt.Data1.ReadUint32()), guild, 3)
case mhfpacket.OperateGuildUnlockOutfit:
if _, err := s.server.db.Exec(`UPDATE guilds SET pugi_outfits=$1 WHERE id=$2`, pkt.Data1.ReadUint32(), guild.ID); err != nil {
if err := s.server.guildRepo.SetPugiOutfits(guild.ID, pkt.Data1.ReadUint32()); err != nil {
s.logger.Error("Failed to unlock guild pugi outfit", zap.Error(err))
}
case mhfpacket.OperateGuildDonateRoom:
@@ -138,13 +138,13 @@ func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
quantity := uint16(pkt.Data1.ReadUint32())
bf.WriteBytes(handleDonateRP(s, quantity, guild, 1))
// TODO: Move this value onto rp_yesterday and reset to 0... daily?
if _, err := s.server.db.Exec(`UPDATE guild_characters SET rp_today=rp_today+$1 WHERE character_id=$2`, quantity, s.charID); err != nil {
if err := s.server.guildRepo.AddMemberDailyRP(s.charID, quantity); err != nil {
s.logger.Error("Failed to update guild character daily RP", zap.Error(err))
}
case mhfpacket.OperateGuildEventExchange:
rp := uint16(pkt.Data1.ReadUint32())
var balance uint32
if err := s.server.db.QueryRow(`UPDATE guilds SET event_rp=event_rp-$1 WHERE id=$2 RETURNING event_rp`, rp, guild.ID).Scan(&balance); err != nil {
balance, err := s.server.guildRepo.ExchangeEventRP(guild.ID, rp)
if err != nil {
s.logger.Error("Failed to exchange guild event RP", zap.Error(err))
}
bf.WriteUint32(balance)
@@ -169,7 +169,7 @@ func handleRenamePugi(s *Session, bf *byteframe.ByteFrame, guild *Guild, num int
default:
guild.PugiName3 = name
}
_ = guild.Save(s)
_ = s.server.guildRepo.Save(guild)
}
func handleChangePugi(s *Session, outfit uint8, guild *Guild, num int) {
@@ -181,7 +181,7 @@ func handleChangePugi(s *Session, outfit uint8, guild *Guild, num int) {
case 3:
guild.PugiOutfit3 = outfit
}
_ = guild.Save(s)
_ = s.server.guildRepo.Save(guild)
}
func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte {
@@ -193,8 +193,8 @@ func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte {
}
var resetRoom bool
if _type == 2 {
var currentRP uint16
if err := s.server.db.QueryRow(`SELECT room_rp FROM guilds WHERE id = $1`, guild.ID).Scan(&currentRP); err != nil {
currentRP, err := s.server.guildRepo.GetRoomRP(guild.ID)
if err != nil {
s.logger.Error("Failed to get guild room RP", zap.Error(err))
}
if currentRP+amount >= 30 {
@@ -206,23 +206,23 @@ func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte {
saveData.Save(s)
switch _type {
case 0:
if _, err := s.server.db.Exec(`UPDATE guilds SET rank_rp = rank_rp + $1 WHERE id = $2`, amount, guild.ID); err != nil {
if err := s.server.guildRepo.AddRankRP(guild.ID, amount); err != nil {
s.logger.Error("Failed to update guild rank RP", zap.Error(err))
}
case 1:
if _, err := s.server.db.Exec(`UPDATE guilds SET event_rp = event_rp + $1 WHERE id = $2`, amount, guild.ID); err != nil {
if err := s.server.guildRepo.AddEventRP(guild.ID, amount); err != nil {
s.logger.Error("Failed to update guild event RP", zap.Error(err))
}
case 2:
if resetRoom {
if _, err := s.server.db.Exec(`UPDATE guilds SET room_rp = 0 WHERE id = $1`, guild.ID); err != nil {
if err := s.server.guildRepo.SetRoomRP(guild.ID, 0); err != nil {
s.logger.Error("Failed to reset guild room RP", zap.Error(err))
}
if _, err := s.server.db.Exec(`UPDATE guilds SET room_expiry = $1 WHERE id = $2`, TimeAdjusted().Add(time.Hour*24*7), guild.ID); err != nil {
if err := s.server.guildRepo.SetRoomExpiry(guild.ID, TimeAdjusted().Add(time.Hour*24*7)); err != nil {
s.logger.Error("Failed to update guild room expiry", zap.Error(err))
}
} else {
if _, err := s.server.db.Exec(`UPDATE guilds SET room_rp = room_rp + $1 WHERE id = $2`, amount, guild.ID); err != nil {
if err := s.server.guildRepo.AddRoomRP(guild.ID, amount); err != nil {
s.logger.Error("Failed to update guild room RP", zap.Error(err))
}
}
@@ -233,7 +233,7 @@ func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte {
}
func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild, avoidLeadership bool) {
characterGuildData, err := GetCharacterGuildData(s, s.charID)
characterGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
@@ -242,7 +242,7 @@ func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild,
characterGuildData.AvoidLeadership = avoidLeadership
err = characterGuildData.Save(s)
err = s.server.guildRepo.SaveMember(characterGuildData)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
@@ -255,14 +255,14 @@ func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild,
func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateGuildMember)
guild, err := GetGuildInfoByCharacterId(s, pkt.CharID)
guild, err := s.server.guildRepo.GetByCharID(pkt.CharID)
if err != nil || guild == nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
actorCharacter, err := GetCharacterGuildData(s, s.charID)
actorCharacter, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil || (!actorCharacter.IsSubLeader() && guild.LeaderCharID != s.charID) {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
@@ -272,7 +272,7 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) {
var mail Mail
switch pkt.Action {
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT:
err = guild.AcceptApplication(s, pkt.CharID)
err = s.server.guildRepo.AcceptApplication(guild.ID, pkt.CharID)
mail = Mail{
RecipientID: pkt.CharID,
Subject: "Accepted!",
@@ -280,7 +280,7 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) {
IsSystemMessage: true,
}
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT:
err = guild.RejectApplication(s, pkt.CharID)
err = s.server.guildRepo.RejectApplication(guild.ID, pkt.CharID)
mail = Mail{
RecipientID: pkt.CharID,
Subject: "Rejected",
@@ -288,7 +288,7 @@ func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) {
IsSystemMessage: true,
}
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK:
err = guild.RemoveCharacter(s, pkt.CharID)
err = s.server.guildRepo.RemoveCharacter(pkt.CharID)
mail = Mail{
RecipientID: pkt.CharID,
Subject: "Kicked",

View File

@@ -12,7 +12,7 @@ import (
func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfPostGuildScout)
actorCharGuildData, err := GetCharacterGuildData(s, s.charID)
actorCharGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil {
s.logger.Error("Failed to get character guild data for scout", zap.Error(err))
@@ -25,7 +25,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
return
}
guildInfo, err := GetGuildInfoByID(s, actorCharGuildData.GuildID)
guildInfo, err := s.server.guildRepo.GetByID(actorCharGuildData.GuildID)
if err != nil {
s.logger.Error("Failed to get guild info for scout", zap.Error(err))
@@ -33,7 +33,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
return
}
hasApplication, err := guildInfo.HasApplicationForCharID(s, pkt.CharID)
hasApplication, err := s.server.guildRepo.HasApplication(guildInfo.ID, pkt.CharID)
if err != nil {
s.logger.Error("Failed to check application for scout", zap.Error(err))
@@ -54,10 +54,10 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
return
}
err = guildInfo.CreateApplication(s, pkt.CharID, GuildApplicationTypeInvited, transaction)
err = s.server.guildRepo.CreateApplication(guildInfo.ID, pkt.CharID, s.charID, GuildApplicationTypeInvited, transaction)
if err != nil {
rollbackTransaction(s, transaction)
_ = transaction.Rollback()
s.logger.Error("Failed to create guild scout application", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, nil)
return
@@ -77,7 +77,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
err = mail.Send(s, transaction)
if err != nil {
rollbackTransaction(s, transaction)
_ = transaction.Rollback()
doAckBufFail(s, pkt.AckHandle, nil)
return
}
@@ -96,7 +96,7 @@ func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfCancelGuildScout)
guildCharData, err := GetCharacterGuildData(s, s.charID)
guildCharData, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil {
s.logger.Error("Failed to get character guild data for cancel scout", zap.Error(err))
@@ -109,14 +109,14 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) {
return
}
guild, err := GetGuildInfoByID(s, guildCharData.GuildID)
guild, err := s.server.guildRepo.GetByID(guildCharData.GuildID)
if err != nil {
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
err = guild.CancelInvitation(s, pkt.InvitationID)
err = s.server.guildRepo.CancelInvitation(guild.ID, pkt.InvitationID)
if err != nil {
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
@@ -129,7 +129,7 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfAnswerGuildScout)
bf := byteframe.NewByteFrame()
guild, err := GetGuildInfoByCharacterId(s, pkt.LeaderID)
guild, err := s.server.guildRepo.GetByCharID(pkt.LeaderID)
if err != nil {
s.logger.Error("Failed to get guild info for answer scout", zap.Error(err))
@@ -137,7 +137,7 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) {
return
}
app, err := guild.GetApplicationForCharID(s, s.charID, GuildApplicationTypeInvited)
app, err := s.server.guildRepo.GetApplication(guild.ID, s.charID, GuildApplicationTypeInvited)
if app == nil || err != nil {
s.logger.Warn(
@@ -154,7 +154,7 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) {
var mail []Mail
if pkt.Answer {
err = guild.AcceptApplication(s, s.charID)
err = s.server.guildRepo.AcceptApplication(guild.ID, s.charID)
mail = append(mail, Mail{
RecipientID: s.charID,
Subject: s.server.i18n.guild.invite.success.title,
@@ -169,7 +169,7 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) {
IsSystemMessage: true,
})
} else {
err = guild.RejectApplication(s, s.charID)
err = s.server.guildRepo.RejectApplication(guild.ID, s.charID)
mail = append(mail, Mail{
RecipientID: s.charID,
Subject: s.server.i18n.guild.invite.rejected.title,
@@ -201,13 +201,13 @@ func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfGetGuildScoutList)
guildInfo, _ := GetGuildInfoByCharacterId(s, s.charID)
guildInfo, _ := s.server.guildRepo.GetByCharID(s.charID)
if guildInfo == nil && s.prevGuildID == 0 {
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
} else {
guildInfo, err := GetGuildInfoByID(s, s.prevGuildID)
guildInfo, err := s.server.guildRepo.GetByID(s.prevGuildID)
if guildInfo == nil || err != nil {
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
@@ -216,7 +216,7 @@ func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) {
rows, err := s.server.db.Queryx(`
SELECT c.id, c.name, c.hr, c.gr, ga.actor_id
FROM guild_applications ga
FROM guild_applications ga
JOIN characters c ON c.id = ga.character_id
WHERE ga.guild_id = $1 AND ga.application_type = 'invited'
`, guildInfo.ID)

View File

@@ -25,7 +25,7 @@ type TreasureHunt struct {
func handleMsgMhfEnumerateGuildTresure(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateGuildTresure)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
if err != nil || guild == nil {
doAckBufSucceed(s, pkt.AckHandle, make([]byte, 4))
return
@@ -86,7 +86,7 @@ func handleMsgMhfRegistGuildTresure(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfRegistGuildTresure)
bf := byteframe.NewByteFrameFromBytes(pkt.Data)
huntData := byteframe.NewByteFrame()
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
if err != nil || guild == nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return

View File

@@ -82,11 +82,11 @@ func handleMsgMhfEnumerateHouse(s *Session, p mhfpacket.MHFPacket) {
}
}
case 2:
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
if err != nil || guild == nil {
break
}
guildMembers, err := GetGuildMembers(s, guild.ID, false)
guildMembers, err := s.server.guildRepo.GetMembers(guild.ID, false)
if err != nil {
break
}
@@ -190,10 +190,10 @@ func handleMsgMhfLoadHouse(s *Session, p mhfpacket.MHFPacket) {
// Guild verification
if state > 3 {
ownGuild, err := GetGuildInfoByCharacterId(s, s.charID)
isApplicant, _ := ownGuild.HasApplicationForCharID(s, s.charID)
ownGuild, err := s.server.guildRepo.GetByCharID(s.charID)
isApplicant, _ := s.server.guildRepo.HasApplication(ownGuild.ID, s.charID)
if err == nil && ownGuild != nil {
othersGuild, err := GetGuildInfoByCharacterId(s, pkt.CharID)
othersGuild, err := s.server.guildRepo.GetByCharID(pkt.CharID)
if err == nil && othersGuild != nil {
if othersGuild.ID == ownGuild.ID && !isApplicant {
allowed = true

View File

@@ -339,13 +339,13 @@ func handleMsgMhfSendMail(s *Session, p mhfpacket.MHFPacket) {
`
if pkt.RecipientID == 0 { // Guild mail
g, err := GetGuildInfoByCharacterId(s, s.charID)
g, err := s.server.guildRepo.GetByCharID(s.charID)
if err != nil {
s.logger.Error("Failed to get guild info for mail")
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
return
}
gm, err := GetGuildMembers(s, g.ID, false)
gm, err := s.server.guildRepo.GetMembers(g.ID, false)
if err != nil {
s.logger.Error("Failed to get guild members for mail")
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))

View File

@@ -388,7 +388,7 @@ type Airou struct {
func getGuildAirouList(s *Session) []Airou {
var guildCats []Airou
bannedCats := make(map[uint32]int)
guild, err := GetGuildInfoByCharacterId(s, s.charID)
guild, err := s.server.guildRepo.GetByCharID(s.charID)
if err != nil {
return guildCats
}

View File

@@ -213,8 +213,11 @@ type RengokuScore struct {
func handleMsgMhfEnumerateRengokuRanking(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfEnumerateRengokuRanking)
guild, _ := GetGuildInfoByCharacterId(s, s.charID)
isApplicant, _ := guild.HasApplicationForCharID(s, s.charID)
guild, _ := s.server.guildRepo.GetByCharID(s.charID)
var isApplicant bool
if guild != nil {
isApplicant, _ = s.server.guildRepo.HasApplication(guild.ID, s.charID)
}
if isApplicant {
guild = nil
}

View File

@@ -0,0 +1,472 @@
package channelserver
import (
"database/sql"
"errors"
"fmt"
"time"
"github.com/jmoiron/sqlx"
)
// GuildRepository centralizes all database access for guild-related tables
// (guilds, guild_characters, guild_applications).
type GuildRepository struct {
db *sqlx.DB
}
// NewGuildRepository creates a new GuildRepository.
func NewGuildRepository(db *sqlx.DB) *GuildRepository {
return &GuildRepository{db: db}
}
const guildInfoSelectSQL = `
SELECT
g.id,
g.name,
rank_rp,
event_rp,
room_rp,
COALESCE(room_expiry, '1970-01-01') AS room_expiry,
main_motto,
sub_motto,
created_at,
leader_id,
c.name AS leader_name,
comment,
COALESCE(pugi_name_1, '') AS pugi_name_1,
COALESCE(pugi_name_2, '') AS pugi_name_2,
COALESCE(pugi_name_3, '') AS pugi_name_3,
pugi_outfit_1,
pugi_outfit_2,
pugi_outfit_3,
pugi_outfits,
recruiting,
COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_color,
COALESCE((SELECT SUM(fs.souls) FROM festa_submissions fs WHERE fs.guild_id=g.id), 0) AS souls,
COALESCE((
SELECT id FROM guild_alliances ga WHERE
ga.parent_id = g.id OR
ga.sub1_id = g.id OR
ga.sub2_id = g.id
), 0) AS alliance_id,
icon,
(SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count
FROM guilds g
JOIN guild_characters gc ON gc.character_id = leader_id
JOIN characters c on leader_id = c.id
`
const guildMembersSelectSQL = `
SELECT
COALESCE(g.id, 0) AS guild_id,
joined_at,
COALESCE((SELECT SUM(souls) FROM festa_submissions fs WHERE fs.character_id=c.id), 0) AS souls,
COALESCE(rp_today, 0) AS rp_today,
COALESCE(rp_yesterday, 0) AS rp_yesterday,
c.name,
c.id AS character_id,
COALESCE(order_index, 0) AS order_index,
c.last_login,
COALESCE(recruiter, false) AS recruiter,
COALESCE(avoid_leadership, false) AS avoid_leadership,
c.hr,
c.gr,
c.weapon_id,
c.weapon_type,
CASE WHEN g.leader_id = c.id THEN true ELSE false END AS is_leader,
character.is_applicant
FROM (
SELECT character_id, true as is_applicant, guild_id
FROM guild_applications ga
WHERE ga.application_type = 'applied'
UNION
SELECT character_id, false as is_applicant, guild_id
FROM guild_characters gc
) character
JOIN characters c on character.character_id = c.id
LEFT JOIN guild_characters gc ON gc.character_id = character.character_id
LEFT JOIN guilds g ON g.id = gc.guild_id
`
func scanGuild(rows *sqlx.Rows) (*Guild, error) {
guild := &Guild{}
if err := rows.StructScan(guild); err != nil {
return nil, err
}
return guild, nil
}
func scanGuildMember(rows *sqlx.Rows) (*GuildMember, error) {
member := &GuildMember{}
if err := rows.StructScan(member); err != nil {
return nil, err
}
return member, nil
}
// GetByID retrieves guild info by guild ID, returning nil if not found.
func (r *GuildRepository) GetByID(guildID uint32) (*Guild, error) {
rows, err := r.db.Queryx(fmt.Sprintf(`%s WHERE g.id = $1 LIMIT 1`, guildInfoSelectSQL), guildID)
if err != nil {
return nil, err
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
return scanGuild(rows)
}
// GetByCharID retrieves guild info for a character, including applied guilds.
func (r *GuildRepository) GetByCharID(charID uint32) (*Guild, error) {
rows, err := r.db.Queryx(fmt.Sprintf(`
%s
WHERE EXISTS(
SELECT 1
FROM guild_characters gc1
WHERE gc1.character_id = $1
AND gc1.guild_id = g.id
)
OR EXISTS(
SELECT 1
FROM guild_applications ga
WHERE ga.character_id = $1
AND ga.guild_id = g.id
AND ga.application_type = 'applied'
)
LIMIT 1
`, guildInfoSelectSQL), charID)
if err != nil {
return nil, err
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
return scanGuild(rows)
}
// ListAll returns all guilds. Used for guild enumeration/search.
func (r *GuildRepository) ListAll() ([]*Guild, error) {
rows, err := r.db.Queryx(guildInfoSelectSQL)
if err != nil {
return nil, err
}
defer rows.Close()
var guilds []*Guild
for rows.Next() {
guild, err := scanGuild(rows)
if err != nil {
continue
}
guilds = append(guilds, guild)
}
return guilds, nil
}
// Create creates a new guild and adds the leader as its first member.
func (r *GuildRepository) Create(leaderCharID uint32, guildName string) (int32, error) {
tx, err := r.db.Begin()
if err != nil {
return 0, err
}
var guildID int32
err = tx.QueryRow(
"INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id",
guildName, leaderCharID,
).Scan(&guildID)
if err != nil {
_ = tx.Rollback()
return 0, err
}
_, err = tx.Exec(`INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)`, guildID, leaderCharID)
if err != nil {
_ = tx.Rollback()
return 0, err
}
if err := tx.Commit(); err != nil {
return 0, err
}
return guildID, nil
}
// Save persists guild metadata changes.
func (r *GuildRepository) Save(guild *Guild) error {
_, err := r.db.Exec(`
UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7,
pugi_outfit_1=$8, pugi_outfit_2=$9, pugi_outfit_3=$10, pugi_outfits=$11, icon=$12, leader_id=$13 WHERE id=$1
`, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3,
guild.PugiOutfit1, guild.PugiOutfit2, guild.PugiOutfit3, guild.PugiOutfits, guild.Icon, guild.LeaderCharID)
return err
}
// Disband removes a guild, its members, and cleans up alliance references.
func (r *GuildRepository) Disband(guildID uint32) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
stmts := []string{
"DELETE FROM guild_characters WHERE guild_id = $1",
"DELETE FROM guilds WHERE id = $1",
"DELETE FROM guild_alliances WHERE parent_id=$1",
}
for _, stmt := range stmts {
if _, err := tx.Exec(stmt, guildID); err != nil {
_ = tx.Rollback()
return err
}
}
if _, err := tx.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guildID); err != nil {
_ = tx.Rollback()
return err
}
if _, err := tx.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guildID); err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
// RemoveCharacter removes a character from their guild.
func (r *GuildRepository) RemoveCharacter(charID uint32) error {
_, err := r.db.Exec("DELETE FROM guild_characters WHERE character_id=$1", charID)
return err
}
// AcceptApplication deletes the application and adds the character to the guild.
func (r *GuildRepository) AcceptApplication(guildID, charID uint32) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
if _, err := tx.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID); err != nil {
_ = tx.Rollback()
return err
}
if _, err := tx.Exec(`
INSERT INTO guild_characters (guild_id, character_id, order_index)
VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1))
`, guildID, charID); err != nil {
_ = tx.Rollback()
return err
}
return tx.Commit()
}
// CreateApplication inserts a guild application or invitation.
// If tx is non-nil, the operation participates in the given transaction.
func (r *GuildRepository) CreateApplication(guildID, charID, actorID uint32, appType GuildApplicationType, tx *sql.Tx) error {
query := `INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) VALUES ($1, $2, $3, $4)`
if tx != nil {
_, err := tx.Exec(query, guildID, charID, actorID, appType)
return err
}
_, err := r.db.Exec(query, guildID, charID, actorID, appType)
return err
}
// CancelInvitation removes an invitation for a character.
func (r *GuildRepository) CancelInvitation(guildID, charID uint32) error {
_, err := r.db.Exec(
`DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`,
charID, guildID,
)
return err
}
// RejectApplication removes an applied application for a character.
func (r *GuildRepository) RejectApplication(guildID, charID uint32) error {
_, err := r.db.Exec(
`DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'applied'`,
charID, guildID,
)
return err
}
// ArrangeCharacters reorders guild members by updating their order_index values.
func (r *GuildRepository) ArrangeCharacters(charIDs []uint32) error {
tx, err := r.db.Begin()
if err != nil {
return err
}
for i, id := range charIDs {
if _, err := tx.Exec("UPDATE guild_characters SET order_index = $1 WHERE character_id = $2", 2+i, id); err != nil {
_ = tx.Rollback()
return err
}
}
return tx.Commit()
}
// GetApplication retrieves a specific application by character, guild, and type.
// Returns nil, nil if not found.
func (r *GuildRepository) GetApplication(guildID, charID uint32, appType GuildApplicationType) (*GuildApplication, error) {
app := &GuildApplication{}
err := r.db.QueryRowx(`
SELECT * from guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = $3
`, charID, guildID, appType).StructScan(app)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
if err != nil {
return nil, err
}
return app, nil
}
// HasApplication checks whether any application exists for the character in the guild.
func (r *GuildRepository) HasApplication(guildID, charID uint32) (bool, error) {
var n int
err := r.db.QueryRow(`SELECT 1 from guild_applications WHERE character_id = $1 AND guild_id = $2`, charID, guildID).Scan(&n)
if errors.Is(err, sql.ErrNoRows) {
return false, nil
}
if err != nil {
return false, err
}
return true, nil
}
// GetItemBox returns the raw item_box bytes for a guild.
func (r *GuildRepository) GetItemBox(guildID uint32) ([]byte, error) {
var data []byte
err := r.db.QueryRow(`SELECT item_box FROM guilds WHERE id=$1`, guildID).Scan(&data)
if errors.Is(err, sql.ErrNoRows) {
return nil, nil
}
return data, err
}
// SaveItemBox writes the serialized item box data for a guild.
func (r *GuildRepository) SaveItemBox(guildID uint32, data []byte) error {
_, err := r.db.Exec(`UPDATE guilds SET item_box=$1 WHERE id=$2`, data, guildID)
return err
}
// GetMembers loads all members (or applicants) of a guild.
func (r *GuildRepository) GetMembers(guildID uint32, applicants bool) ([]*GuildMember, error) {
rows, err := r.db.Queryx(fmt.Sprintf(`
%s
WHERE character.guild_id = $1 AND is_applicant = $2
`, guildMembersSelectSQL), guildID, applicants)
if err != nil {
return nil, err
}
defer rows.Close()
members := make([]*GuildMember, 0)
for rows.Next() {
member, err := scanGuildMember(rows)
if err != nil {
return nil, err
}
members = append(members, member)
}
return members, nil
}
// GetCharacterMembership loads a character's guild membership data.
// Returns nil, nil if the character is not in any guild.
func (r *GuildRepository) GetCharacterMembership(charID uint32) (*GuildMember, error) {
rows, err := r.db.Queryx(fmt.Sprintf("%s WHERE character.character_id=$1", guildMembersSelectSQL), charID)
if err != nil {
return nil, err
}
defer rows.Close()
if !rows.Next() {
return nil, nil
}
return scanGuildMember(rows)
}
// SaveMember persists guild member changes (avoid_leadership and order_index).
func (r *GuildRepository) SaveMember(member *GuildMember) error {
_, err := r.db.Exec(
"UPDATE guild_characters SET avoid_leadership=$1, order_index=$2 WHERE character_id=$3",
member.AvoidLeadership, member.OrderIndex, member.CharID,
)
return err
}
// SetRecruiting updates whether a guild is accepting applications.
func (r *GuildRepository) SetRecruiting(guildID uint32, recruiting bool) error {
_, err := r.db.Exec("UPDATE guilds SET recruiting=$1 WHERE id=$2", recruiting, guildID)
return err
}
// SetPugiOutfits updates the unlocked pugi outfit bitmask.
func (r *GuildRepository) SetPugiOutfits(guildID uint32, outfits uint32) error {
_, err := r.db.Exec(`UPDATE guilds SET pugi_outfits=$1 WHERE id=$2`, outfits, guildID)
return err
}
// SetRecruiter updates whether a character has recruiter rights.
func (r *GuildRepository) SetRecruiter(charID uint32, allowed bool) error {
_, err := r.db.Exec("UPDATE guild_characters SET recruiter=$1 WHERE character_id=$2", allowed, charID)
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
}

View File

@@ -0,0 +1,531 @@
package channelserver
import (
"testing"
"time"
"github.com/jmoiron/sqlx"
)
func setupGuildRepo(t *testing.T) (*GuildRepository, *sqlx.DB, uint32, uint32) {
t.Helper()
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "guild_test_user")
charID := CreateTestCharacter(t, db, userID, "GuildLeader")
repo := NewGuildRepository(db)
guildID := CreateTestGuild(t, db, charID, "TestGuild")
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, guildID, charID
}
func TestGetByID(t *testing.T) {
repo, _, guildID, charID := setupGuildRepo(t)
guild, err := repo.GetByID(guildID)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
if guild == nil {
t.Fatal("Expected guild, got nil")
}
if guild.ID != guildID {
t.Errorf("Expected guild ID %d, got %d", guildID, guild.ID)
}
if guild.Name != "TestGuild" {
t.Errorf("Expected name 'TestGuild', got %q", guild.Name)
}
if guild.LeaderCharID != charID {
t.Errorf("Expected leader %d, got %d", charID, guild.LeaderCharID)
}
}
func TestGetByIDNotFound(t *testing.T) {
repo, _, _, _ := setupGuildRepo(t)
guild, err := repo.GetByID(999999)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
if guild != nil {
t.Errorf("Expected nil for non-existent guild, got: %+v", guild)
}
}
func TestGetByCharID(t *testing.T) {
repo, _, guildID, charID := setupGuildRepo(t)
guild, err := repo.GetByCharID(charID)
if err != nil {
t.Fatalf("GetByCharID failed: %v", err)
}
if guild == nil {
t.Fatal("Expected guild, got nil")
}
if guild.ID != guildID {
t.Errorf("Expected guild ID %d, got %d", guildID, guild.ID)
}
}
func TestGetByCharIDNotFound(t *testing.T) {
repo, _, _, _ := setupGuildRepo(t)
guild, err := repo.GetByCharID(999999)
if err != nil {
t.Fatalf("GetByCharID failed: %v", err)
}
if guild != nil {
t.Errorf("Expected nil for non-member, got: %+v", guild)
}
}
func TestCreate(t *testing.T) {
db := SetupTestDB(t)
defer TeardownTestDB(t, db)
repo := NewGuildRepository(db)
userID := CreateTestUser(t, db, "create_guild_user")
charID := CreateTestCharacter(t, db, userID, "CreateLeader")
guildID, err := repo.Create(charID, "NewGuild")
if err != nil {
t.Fatalf("Create failed: %v", err)
}
if guildID <= 0 {
t.Errorf("Expected positive guild ID, got %d", guildID)
}
// Verify guild exists
guild, err := repo.GetByID(uint32(guildID))
if err != nil {
t.Fatalf("GetByID after Create failed: %v", err)
}
if guild == nil {
t.Fatal("Created guild not found")
}
if guild.Name != "NewGuild" {
t.Errorf("Expected name 'NewGuild', got %q", guild.Name)
}
// Verify leader is a member
member, err := repo.GetCharacterMembership(charID)
if err != nil {
t.Fatalf("GetCharacterMembership failed: %v", err)
}
if member == nil {
t.Fatal("Leader not found as guild member")
}
}
func TestSaveGuild(t *testing.T) {
repo, _, guildID, _ := setupGuildRepo(t)
guild, err := repo.GetByID(guildID)
if err != nil {
t.Fatalf("GetByID failed: %v", err)
}
guild.Comment = "Updated comment"
guild.MainMotto = 5
guild.SubMotto = 3
if err := repo.Save(guild); err != nil {
t.Fatalf("Save failed: %v", err)
}
updated, err := repo.GetByID(guildID)
if err != nil {
t.Fatalf("GetByID after Save failed: %v", err)
}
if updated.Comment != "Updated comment" {
t.Errorf("Expected comment 'Updated comment', got %q", updated.Comment)
}
if updated.MainMotto != 5 || updated.SubMotto != 3 {
t.Errorf("Expected mottos 5/3, got %d/%d", updated.MainMotto, updated.SubMotto)
}
}
func TestDisband(t *testing.T) {
repo, _, guildID, charID := setupGuildRepo(t)
if err := repo.Disband(guildID); err != nil {
t.Fatalf("Disband failed: %v", err)
}
guild, err := repo.GetByID(guildID)
if err != nil {
t.Fatalf("GetByID after Disband failed: %v", err)
}
if guild != nil {
t.Errorf("Expected nil after disband, got: %+v", guild)
}
member, err := repo.GetCharacterMembership(charID)
if err != nil {
t.Fatalf("GetCharacterMembership after Disband failed: %v", err)
}
if member != nil {
t.Errorf("Expected nil membership after disband, got: %+v", member)
}
}
func TestGetMembers(t *testing.T) {
repo, db, guildID, leaderID := setupGuildRepo(t)
// Add a second member
user2 := CreateTestUser(t, db, "member_user")
member2 := CreateTestCharacter(t, db, user2, "Member2")
if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 2)", guildID, member2); err != nil {
t.Fatalf("Failed to add member: %v", err)
}
members, err := repo.GetMembers(guildID, false)
if err != nil {
t.Fatalf("GetMembers failed: %v", err)
}
if len(members) != 2 {
t.Fatalf("Expected 2 members, got %d", len(members))
}
ids := map[uint32]bool{leaderID: false, member2: false}
for _, m := range members {
ids[m.CharID] = true
}
if !ids[leaderID] || !ids[member2] {
t.Errorf("Expected members %d and %d, got: %v", leaderID, member2, members)
}
}
func TestGetCharacterMembership(t *testing.T) {
repo, _, guildID, charID := setupGuildRepo(t)
member, err := repo.GetCharacterMembership(charID)
if err != nil {
t.Fatalf("GetCharacterMembership failed: %v", err)
}
if member == nil {
t.Fatal("Expected membership, got nil")
}
if member.GuildID != guildID {
t.Errorf("Expected guild ID %d, got %d", guildID, member.GuildID)
}
if !member.IsLeader {
t.Error("Expected leader flag to be true")
}
}
func TestSaveMember(t *testing.T) {
repo, _, _, charID := setupGuildRepo(t)
member, err := repo.GetCharacterMembership(charID)
if err != nil {
t.Fatalf("GetCharacterMembership failed: %v", err)
}
member.AvoidLeadership = true
member.OrderIndex = 5
if err := repo.SaveMember(member); err != nil {
t.Fatalf("SaveMember failed: %v", err)
}
updated, err := repo.GetCharacterMembership(charID)
if err != nil {
t.Fatalf("GetCharacterMembership after Save failed: %v", err)
}
if !updated.AvoidLeadership {
t.Error("Expected avoid_leadership=true")
}
if updated.OrderIndex != 5 {
t.Errorf("Expected order_index=5, got %d", updated.OrderIndex)
}
}
func TestRemoveCharacter(t *testing.T) {
repo, db, guildID, _ := setupGuildRepo(t)
// Add and remove a member
user2 := CreateTestUser(t, db, "remove_user")
char2 := CreateTestCharacter(t, db, user2, "RemoveMe")
if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 2)", guildID, char2); err != nil {
t.Fatalf("Failed to add member: %v", err)
}
if err := repo.RemoveCharacter(char2); err != nil {
t.Fatalf("RemoveCharacter failed: %v", err)
}
member, err := repo.GetCharacterMembership(char2)
if err != nil {
t.Fatalf("GetCharacterMembership after remove failed: %v", err)
}
if member != nil {
t.Errorf("Expected nil membership after remove, got: %+v", member)
}
}
func TestApplicationWorkflow(t *testing.T) {
repo, db, guildID, _ := setupGuildRepo(t)
user2 := CreateTestUser(t, db, "applicant_user")
applicantID := CreateTestCharacter(t, db, user2, "Applicant")
// Create application
err := repo.CreateApplication(guildID, applicantID, applicantID, GuildApplicationTypeApplied, nil)
if err != nil {
t.Fatalf("CreateApplication failed: %v", err)
}
// Check HasApplication
has, err := repo.HasApplication(guildID, applicantID)
if err != nil {
t.Fatalf("HasApplication failed: %v", err)
}
if !has {
t.Error("Expected application to exist")
}
// Get application
app, err := repo.GetApplication(guildID, applicantID, GuildApplicationTypeApplied)
if err != nil {
t.Fatalf("GetApplication failed: %v", err)
}
if app == nil {
t.Fatal("Expected application, got nil")
}
// Accept
err = repo.AcceptApplication(guildID, applicantID)
if err != nil {
t.Fatalf("AcceptApplication failed: %v", err)
}
// Verify membership
member, err := repo.GetCharacterMembership(applicantID)
if err != nil {
t.Fatalf("GetCharacterMembership after accept failed: %v", err)
}
if member == nil {
t.Fatal("Expected membership after accept")
}
// Verify application removed
has, err = repo.HasApplication(guildID, applicantID)
if err != nil {
t.Fatalf("HasApplication after accept failed: %v", err)
}
if has {
t.Error("Expected no application after accept")
}
}
func TestRejectApplication(t *testing.T) {
repo, db, guildID, _ := setupGuildRepo(t)
user2 := CreateTestUser(t, db, "reject_user")
applicantID := CreateTestCharacter(t, db, user2, "Rejected")
err := repo.CreateApplication(guildID, applicantID, applicantID, GuildApplicationTypeApplied, nil)
if err != nil {
t.Fatalf("CreateApplication failed: %v", err)
}
err = repo.RejectApplication(guildID, applicantID)
if err != nil {
t.Fatalf("RejectApplication failed: %v", err)
}
has, err := repo.HasApplication(guildID, applicantID)
if err != nil {
t.Fatalf("HasApplication after reject failed: %v", err)
}
if has {
t.Error("Expected no application after reject")
}
}
func TestSetRecruiting(t *testing.T) {
repo, db, guildID, _ := setupGuildRepo(t)
if err := repo.SetRecruiting(guildID, false); err != nil {
t.Fatalf("SetRecruiting failed: %v", err)
}
var recruiting bool
if err := db.QueryRow("SELECT recruiting FROM guilds WHERE id=$1", guildID).Scan(&recruiting); err != nil {
t.Fatalf("Verification query failed: %v", err)
}
if recruiting {
t.Error("Expected recruiting=false")
}
}
func TestRPOperations(t *testing.T) {
repo, db, guildID, _ := setupGuildRepo(t)
// AddRankRP
if err := repo.AddRankRP(guildID, 100); err != nil {
t.Fatalf("AddRankRP failed: %v", err)
}
var rankRP uint16
if err := db.QueryRow("SELECT rank_rp FROM guilds WHERE id=$1", guildID).Scan(&rankRP); err != nil {
t.Fatalf("Verification failed: %v", err)
}
if rankRP != 100 {
t.Errorf("Expected rank_rp=100, got %d", rankRP)
}
// AddEventRP
if err := repo.AddEventRP(guildID, 50); err != nil {
t.Fatalf("AddEventRP failed: %v", err)
}
// ExchangeEventRP
balance, err := repo.ExchangeEventRP(guildID, 20)
if err != nil {
t.Fatalf("ExchangeEventRP failed: %v", err)
}
if balance != 30 {
t.Errorf("Expected event_rp balance=30, got %d", balance)
}
// Room RP operations
if err := repo.AddRoomRP(guildID, 10); err != nil {
t.Fatalf("AddRoomRP failed: %v", err)
}
roomRP, err := repo.GetRoomRP(guildID)
if err != nil {
t.Fatalf("GetRoomRP failed: %v", err)
}
if roomRP != 10 {
t.Errorf("Expected room_rp=10, got %d", roomRP)
}
if err := repo.SetRoomRP(guildID, 0); err != nil {
t.Fatalf("SetRoomRP failed: %v", err)
}
roomRP, err = repo.GetRoomRP(guildID)
if err != nil {
t.Fatalf("GetRoomRP after reset failed: %v", err)
}
if roomRP != 0 {
t.Errorf("Expected room_rp=0, got %d", roomRP)
}
// SetRoomExpiry
expiry := time.Date(2025, 7, 1, 0, 0, 0, 0, time.UTC)
if err := repo.SetRoomExpiry(guildID, expiry); err != nil {
t.Fatalf("SetRoomExpiry failed: %v", err)
}
var gotExpiry time.Time
if err := db.QueryRow("SELECT room_expiry FROM guilds WHERE id=$1", guildID).Scan(&gotExpiry); err != nil {
t.Fatalf("Verification failed: %v", err)
}
if !gotExpiry.Equal(expiry) {
t.Errorf("Expected expiry %v, got %v", expiry, gotExpiry)
}
}
func TestItemBox(t *testing.T) {
repo, _, guildID, _ := setupGuildRepo(t)
// Initially nil
data, err := repo.GetItemBox(guildID)
if err != nil {
t.Fatalf("GetItemBox failed: %v", err)
}
if data != nil {
t.Errorf("Expected nil item box initially, got %x", data)
}
// Save and retrieve
blob := []byte{0x01, 0x02, 0x03}
if err := repo.SaveItemBox(guildID, blob); err != nil {
t.Fatalf("SaveItemBox failed: %v", err)
}
data, err = repo.GetItemBox(guildID)
if err != nil {
t.Fatalf("GetItemBox after save failed: %v", err)
}
if len(data) != 3 || data[0] != 0x01 || data[2] != 0x03 {
t.Errorf("Expected %x, got %x", blob, data)
}
}
func TestListAll(t *testing.T) {
repo, db, _, _ := setupGuildRepo(t)
// Create a second guild
user2 := CreateTestUser(t, db, "list_user")
char2 := CreateTestCharacter(t, db, user2, "ListLeader")
CreateTestGuild(t, db, char2, "SecondGuild")
guilds, err := repo.ListAll()
if err != nil {
t.Fatalf("ListAll failed: %v", err)
}
if len(guilds) < 2 {
t.Errorf("Expected at least 2 guilds, got %d", len(guilds))
}
}
func TestArrangeCharacters(t *testing.T) {
repo, db, guildID, leaderID := setupGuildRepo(t)
// Add two more members
user2 := CreateTestUser(t, db, "arrange_user2")
char2 := CreateTestCharacter(t, db, user2, "Char2")
user3 := CreateTestUser(t, db, "arrange_user3")
char3 := CreateTestCharacter(t, db, user3, "Char3")
if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 2)", guildID, char2); err != nil {
t.Fatalf("Failed to add member: %v", err)
}
if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id, order_index) VALUES ($1, $2, 3)", guildID, char3); err != nil {
t.Fatalf("Failed to add member: %v", err)
}
// Rearrange (excludes leader, sets order_index starting at 2)
if err := repo.ArrangeCharacters([]uint32{char3, char2}); err != nil {
t.Fatalf("ArrangeCharacters failed: %v", err)
}
// Verify order changed
var order2, order3 uint16
_ = db.QueryRow("SELECT order_index FROM guild_characters WHERE character_id=$1", char2).Scan(&order2)
_ = db.QueryRow("SELECT order_index FROM guild_characters WHERE character_id=$1", char3).Scan(&order3)
if order3 != 2 || order2 != 3 {
t.Errorf("Expected char3=2, char2=3 but got char3=%d, char2=%d", order3, order2)
}
_ = leaderID
}
func TestSetRecruiter(t *testing.T) {
repo, db, _, charID := setupGuildRepo(t)
if err := repo.SetRecruiter(charID, true); err != nil {
t.Fatalf("SetRecruiter failed: %v", err)
}
var recruiter bool
if err := db.QueryRow("SELECT recruiter FROM guild_characters WHERE character_id=$1", charID).Scan(&recruiter); err != nil {
t.Fatalf("Verification failed: %v", err)
}
if !recruiter {
t.Error("Expected recruiter=true")
}
}
func TestAddMemberDailyRP(t *testing.T) {
repo, db, _, charID := setupGuildRepo(t)
if err := repo.AddMemberDailyRP(charID, 25); err != nil {
t.Fatalf("AddMemberDailyRP failed: %v", err)
}
var rp uint16
if err := db.QueryRow("SELECT rp_today FROM guild_characters WHERE character_id=$1", charID).Scan(&rp); err != nil {
t.Fatalf("Verification failed: %v", err)
}
if rp != 25 {
t.Errorf("Expected rp_today=25, got %d", rp)
}
}

View File

@@ -46,6 +46,7 @@ type Server struct {
logger *zap.Logger
db *sqlx.DB
charRepo *CharacterRepository
guildRepo *GuildRepository
erupeConfig *_config.Config
acceptConns chan net.Conn
deleteConns chan net.Conn
@@ -117,6 +118,7 @@ func NewServer(config *Config) *Server {
}
s.charRepo = NewCharacterRepository(config.DB)
s.guildRepo = NewGuildRepository(config.DB)
// Mezeporta
s.stages["sl1Ns200p0a0u0"] = NewStage("sl1Ns200p0a0u0")

View File

@@ -258,3 +258,38 @@ func CreateTestCharacter(t *testing.T, db *sqlx.DB, userID uint32, name string)
return charID
}
// CreateTestGuild creates a test guild with the given leader and returns the guild ID
func CreateTestGuild(t *testing.T, db *sqlx.DB, leaderCharID uint32, name string) uint32 {
t.Helper()
tx, err := db.Begin()
if err != nil {
t.Fatalf("Failed to begin transaction: %v", err)
}
var guildID uint32
err = tx.QueryRow(
"INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id",
name, leaderCharID,
).Scan(&guildID)
if err != nil {
_ = tx.Rollback()
t.Fatalf("Failed to create test guild: %v", err)
}
_, err = tx.Exec(
"INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)",
guildID, leaderCharID,
)
if err != nil {
_ = tx.Rollback()
t.Fatalf("Failed to add leader to guild: %v", err)
}
if err := tx.Commit(); err != nil {
t.Fatalf("Failed to commit guild creation: %v", err)
}
return guildID
}