refactor(guild): extract disband, resign, leave, and scout logic into GuildService

Move business logic for guild disband, resign leadership, leave,
post scout, and answer scout from handlers into GuildService methods.
Handlers now delegate to the service layer and handle only protocol
concerns (packet parsing, ACK responses, cross-channel notifications).

Adds 22 new table-driven service tests and sentinel errors for typed
error handling (ErrNoEligibleLeader, ErrAlreadyInvited, etc.).
DonateRP left in handler due to Session coupling.
This commit is contained in:
Houmgaor
2026-02-23 23:35:28 +01:00
parent 2abca9fb23
commit 210cfa1fd1
6 changed files with 699 additions and 137 deletions

View File

@@ -1,59 +1,29 @@
package channelserver
import (
"errors"
"erupe-ce/common/byteframe"
"erupe-ce/common/stringsupport"
"erupe-ce/network/mhfpacket"
"fmt"
"go.uber.org/zap"
)
func handleMsgMhfPostGuildScout(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfPostGuildScout)
actorCharGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID)
err := s.server.guildService.PostScout(s.charID, pkt.CharID, ScoutInviteStrings{
Title: s.server.i18n.guild.invite.title,
Body: s.server.i18n.guild.invite.body,
})
if err != nil {
s.logger.Error("Failed to get character guild data for scout", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
if actorCharGuildData == nil || !actorCharGuildData.CanRecruit() {
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
guildInfo, err := s.server.guildRepo.GetByID(actorCharGuildData.GuildID)
if err != nil {
s.logger.Error("Failed to get guild info for scout", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
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))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
if hasApplication {
if errors.Is(err, ErrAlreadyInvited) {
doAckBufSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x04})
return
}
err = s.server.guildRepo.CreateApplicationWithMail(
guildInfo.ID, pkt.CharID, s.charID, GuildApplicationTypeInvited,
s.charID, pkt.CharID,
s.server.i18n.guild.invite.title,
fmt.Sprintf(s.server.i18n.guild.invite.body, guildInfo.Name))
if err != nil {
s.logger.Error("Failed to create guild scout application with mail", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, nil)
s.logger.Error("Failed to post guild scout", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, make([]byte, 4))
return
}
@@ -95,64 +65,37 @@ func handleMsgMhfCancelGuildScout(s *Session, p mhfpacket.MHFPacket) {
func handleMsgMhfAnswerGuildScout(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfAnswerGuildScout)
bf := byteframe.NewByteFrame()
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))
i := s.server.i18n.guild.invite
result, err := s.server.guildService.AnswerScout(s.charID, pkt.LeaderID, pkt.Answer, AnswerScoutStrings{
SuccessTitle: i.success.title,
SuccessBody: i.success.body,
AcceptedTitle: i.accepted.title,
AcceptedBody: i.accepted.body,
RejectedTitle: i.rejected.title,
RejectedBody: i.rejected.body,
DeclinedTitle: i.declined.title,
DeclinedBody: i.declined.body,
})
if err != nil && !errors.Is(err, ErrApplicationMissing) {
s.logger.Error("Failed to answer guild scout", zap.Error(err))
doAckBufFail(s, pkt.AckHandle, nil)
return
}
app, err := s.server.guildRepo.GetApplication(guild.ID, s.charID, GuildApplicationTypeInvited)
if app == nil || err != nil {
s.logger.Warn(
"Guild invite missing, deleted?",
zap.Error(err),
zap.Uint32("guildID", guild.ID),
zap.Uint32("charID", s.charID),
)
bf.WriteUint32(7)
bf.WriteUint32(guild.ID)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
return
}
type mailMsg struct {
senderID uint32
recipientID uint32
subject string
body string
}
var msgs []mailMsg
if pkt.Answer {
err = s.server.guildRepo.AcceptApplication(guild.ID, s.charID)
msgs = append(msgs,
mailMsg{0, s.charID, s.server.i18n.guild.invite.success.title, fmt.Sprintf(s.server.i18n.guild.invite.success.body, guild.Name)},
mailMsg{s.charID, pkt.LeaderID, s.server.i18n.guild.invite.accepted.title, fmt.Sprintf(s.server.i18n.guild.invite.accepted.body, guild.Name)},
)
} else {
err = s.server.guildRepo.RejectApplication(guild.ID, s.charID)
msgs = append(msgs,
mailMsg{0, s.charID, s.server.i18n.guild.invite.rejected.title, fmt.Sprintf(s.server.i18n.guild.invite.rejected.body, guild.Name)},
mailMsg{s.charID, pkt.LeaderID, s.server.i18n.guild.invite.declined.title, fmt.Sprintf(s.server.i18n.guild.invite.declined.body, guild.Name)},
)
}
if err != nil {
bf.WriteUint32(7)
bf.WriteUint32(guild.ID)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
} else {
bf := byteframe.NewByteFrame()
if result != nil && result.Success {
bf.WriteUint32(0)
bf.WriteUint32(guild.ID)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
for _, m := range msgs {
if err := s.server.mailRepo.SendMail(m.senderID, m.recipientID, m.subject, m.body, 0, 0, false, true); err != nil {
s.logger.Warn("Failed to send guild scout response mail", zap.Error(err))
}
} else {
if errors.Is(err, ErrApplicationMissing) {
s.logger.Warn("Guild invite missing, deleted?",
zap.Uint32("charID", s.charID))
}
bf.WriteUint32(7)
}
bf.WriteUint32(result.GuildID)
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}
func handleMsgMhfGetGuildScoutList(s *Session, p mhfpacket.MHFPacket) {