Files
Erupe/server/channelserver/handlers_guild_ops.go
Houmgaor 210cfa1fd1 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.
2026-02-23 23:35:28 +01:00

261 lines
8.3 KiB
Go

package channelserver
import (
"time"
"erupe-ce/common/byteframe"
"erupe-ce/common/stringsupport"
"erupe-ce/network/mhfpacket"
"go.uber.org/zap"
)
func handleMsgMhfOperateGuild(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateGuild)
guild, err := s.server.guildRepo.GetByID(pkt.GuildID)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
characterGuildInfo, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
bf := byteframe.NewByteFrame()
switch pkt.Action {
case mhfpacket.OperateGuildDisband:
result, _ := s.server.guildService.Disband(s.charID, guild.ID)
response := 0
if result != nil && result.Success {
response = 1
}
bf.WriteUint32(uint32(response))
case mhfpacket.OperateGuildResign:
result, err := s.server.guildService.ResignLeadership(s.charID, guild.ID)
if err == nil && result.NewLeaderCharID != 0 {
bf.WriteUint32(result.NewLeaderCharID)
}
case mhfpacket.OperateGuildApply:
err = s.server.guildRepo.CreateApplication(guild.ID, s.charID, s.charID, GuildApplicationTypeApplied)
if err == nil {
bf.WriteUint32(guild.LeaderCharID)
} else {
bf.WriteUint32(0)
}
case mhfpacket.OperateGuildLeave:
result, _ := s.server.guildService.Leave(s.charID, guild.ID, characterGuildInfo.IsApplicant, guild.Name)
response := 0
if result != nil && result.Success {
response = 1
}
bf.WriteUint32(uint32(response))
case mhfpacket.OperateGuildDonateRank:
bf.WriteBytes(handleDonateRP(s, uint16(pkt.Data1.ReadUint32()), guild, 0))
case mhfpacket.OperateGuildSetApplicationDeny:
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.guildRepo.SetRecruiting(guild.ID, true); err != nil {
s.logger.Error("Failed to allow guild applications", zap.Error(err))
}
case mhfpacket.OperateGuildSetAvoidLeadershipTrue:
handleAvoidLeadershipUpdate(s, pkt, true)
case mhfpacket.OperateGuildSetAvoidLeadershipFalse:
handleAvoidLeadershipUpdate(s, pkt, false)
case mhfpacket.OperateGuildUpdateComment:
if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
guild.Comment = stringsupport.SJISToUTF8Lossy(pkt.Data2.ReadNullTerminatedBytes())
if err := s.server.guildRepo.Save(guild); err != nil {
s.logger.Error("Failed to save guild comment", zap.Error(err))
}
case mhfpacket.OperateGuildUpdateMotto:
if !characterGuildInfo.IsLeader && !characterGuildInfo.IsSubLeader() {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
_ = pkt.Data1.ReadUint16()
guild.SubMotto = pkt.Data1.ReadUint8()
guild.MainMotto = pkt.Data1.ReadUint8()
if err := s.server.guildRepo.Save(guild); err != nil {
s.logger.Error("Failed to save guild motto", zap.Error(err))
}
case mhfpacket.OperateGuildRenamePugi1:
handleRenamePugi(s, pkt.Data2, guild, 1)
case mhfpacket.OperateGuildRenamePugi2:
handleRenamePugi(s, pkt.Data2, guild, 2)
case mhfpacket.OperateGuildRenamePugi3:
handleRenamePugi(s, pkt.Data2, guild, 3)
case mhfpacket.OperateGuildChangePugi1:
handleChangePugi(s, uint8(pkt.Data1.ReadUint32()), guild, 1)
case mhfpacket.OperateGuildChangePugi2:
handleChangePugi(s, uint8(pkt.Data1.ReadUint32()), guild, 2)
case mhfpacket.OperateGuildChangePugi3:
handleChangePugi(s, uint8(pkt.Data1.ReadUint32()), guild, 3)
case mhfpacket.OperateGuildUnlockOutfit:
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:
quantity := uint16(pkt.Data1.ReadUint32())
bf.WriteBytes(handleDonateRP(s, quantity, guild, 2))
case mhfpacket.OperateGuildDonateEvent:
quantity := uint16(pkt.Data1.ReadUint32())
bf.WriteBytes(handleDonateRP(s, quantity, guild, 1))
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())
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)
default:
s.logger.Error("unhandled operate guild action", zap.Uint8("action", uint8(pkt.Action)))
}
if len(bf.Data()) > 0 {
doAckSimpleSucceed(s, pkt.AckHandle, bf.Data())
} else {
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
}
func handleRenamePugi(s *Session, bf *byteframe.ByteFrame, guild *Guild, num int) {
name := stringsupport.SJISToUTF8Lossy(bf.ReadNullTerminatedBytes())
switch num {
case 1:
guild.PugiName1 = name
case 2:
guild.PugiName2 = name
default:
guild.PugiName3 = name
}
if err := s.server.guildRepo.Save(guild); err != nil {
s.logger.Error("Failed to save guild pugi name", zap.Error(err))
}
}
func handleChangePugi(s *Session, outfit uint8, guild *Guild, num int) {
switch num {
case 1:
guild.PugiOutfit1 = outfit
case 2:
guild.PugiOutfit2 = outfit
case 3:
guild.PugiOutfit3 = outfit
}
if err := s.server.guildRepo.Save(guild); err != nil {
s.logger.Error("Failed to save guild pugi outfit", zap.Error(err))
}
}
func handleDonateRP(s *Session, amount uint16, guild *Guild, _type int) []byte {
bf := byteframe.NewByteFrame()
bf.WriteUint32(0)
saveData, err := GetCharacterSaveData(s, s.charID)
if err != nil {
return bf.Data()
}
var resetRoom bool
if _type == 2 {
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 {
amount = 30 - currentRP
resetRoom = true
}
}
saveData.RP -= amount
saveData.Save(s)
switch _type {
case 0:
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.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.guildRepo.SetRoomRP(guild.ID, 0); err != nil {
s.logger.Error("Failed to reset guild room RP", zap.Error(err))
}
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.guildRepo.AddRoomRP(guild.ID, amount); err != nil {
s.logger.Error("Failed to update guild room RP", zap.Error(err))
}
}
}
_, _ = bf.Seek(0, 0)
bf.WriteUint32(uint32(saveData.RP))
return bf.Data()
}
func handleAvoidLeadershipUpdate(s *Session, pkt *mhfpacket.MsgMhfOperateGuild, avoidLeadership bool) {
characterGuildData, err := s.server.guildRepo.GetCharacterMembership(s.charID)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
characterGuildData.AvoidLeadership = avoidLeadership
err = s.server.guildRepo.SaveMember(characterGuildData)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func handleMsgMhfOperateGuildMember(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateGuildMember)
action, ok := mapMemberAction(pkt.Action)
if !ok {
s.logger.Warn("Unhandled operateGuildMember action", zap.Uint8("action", pkt.Action))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
result, err := s.server.guildService.OperateMember(s.charID, pkt.CharID, action)
if err != nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
s.server.Registry.NotifyMailToCharID(result.MailRecipientID, s, &result.Mail)
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
func mapMemberAction(proto uint8) (GuildMemberAction, bool) {
switch proto {
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_ACCEPT:
return GuildMemberActionAccept, true
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_REJECT:
return GuildMemberActionReject, true
case mhfpacket.OPERATE_GUILD_MEMBER_ACTION_KICK:
return GuildMemberActionKick, true
default:
return 0, false
}
}