Files
Erupe/server/channelserver/handlers_guild_ops.go
Houmgaor 74798fc8b3 fix(channelserver): return error from Save() to prevent misleading success logs
CharacterSaveData.Save() silently returned on failure (nil decompressed
data, compression error, DB error) while the caller unconditionally
logged "Saved character data successfully". This made diagnosing save
failures difficult (ref #163).

Save() now returns an error, and all six call sites check it. The
success log in saveAllCharacterData only fires when the save actually
persisted.
2026-02-27 11:21:37 +01:00

269 lines
8.6 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, err := s.server.guildService.Disband(s.charID, guild.ID)
if err != nil {
s.logger.Error("Failed to disband guild", zap.Error(err))
}
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, err := s.server.guildService.Leave(s.charID, guild.ID, characterGuildInfo.IsApplicant, guild.Name)
if err != nil {
s.logger.Error("Failed to leave guild", zap.Error(err))
}
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
if err := saveData.Save(s); err != nil {
s.logger.Error("Failed to save RP after guild donation", zap.Error(err))
}
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
}
}