Files
Erupe/server/channelserver/handlers_guild_alliance.go
Houmgaor aee53534a2 fix(guild): add nil guards for alliance guild lookups (#171)
scanAllianceWithGuilds dereferences guild pointers returned by GetByID
without checking for nil. Since GetByID returns (nil, nil) when a guild
is missing, alliances referencing deleted guilds cause nil-pointer
panics. The panic is caught by session recovery but no ACK is sent,
softlocking the client.

Add nil checks in scanAllianceWithGuilds, handleMsgMhfOperateJoint,
handleMsgMhfInfoJoint, and handleMsgMhfInfoGuild so that missing
guilds or alliances produce proper error responses instead of panics.
2026-03-02 19:43:11 +01:00

174 lines
6.1 KiB
Go

package channelserver
import (
"erupe-ce/common/byteframe"
ps "erupe-ce/common/pascalstring"
"time"
"erupe-ce/network/mhfpacket"
"go.uber.org/zap"
)
// GuildAlliance represents a multi-guild alliance.
type GuildAlliance struct {
ID uint32 `db:"id"`
Name string `db:"name"`
CreatedAt time.Time `db:"created_at"`
TotalMembers uint16
Recruiting bool `db:"recruiting"`
ParentGuildID uint32 `db:"parent_id"`
SubGuild1ID uint32 `db:"sub1_id"`
SubGuild2ID uint32 `db:"sub2_id"`
ParentGuild Guild
SubGuild1 Guild
SubGuild2 Guild
}
func handleMsgMhfCreateJoint(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfCreateJoint)
if err := s.server.guildRepo.CreateAlliance(pkt.Name, pkt.GuildID); err != nil {
s.logger.Error("Failed to create guild alliance in db", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x01, 0x01, 0x01, 0x01})
}
func handleMsgMhfOperateJoint(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfOperateJoint)
guild, err := s.server.guildRepo.GetByID(pkt.GuildID)
if err != nil {
s.logger.Error("Failed to get guild info", zap.Error(err))
}
if guild == nil {
s.logger.Error("Guild not found for alliance operation", zap.Uint32("GuildID", pkt.GuildID))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
alliance, err := s.server.guildRepo.GetAllianceByID(pkt.AllianceID)
if err != nil {
s.logger.Error("Failed to get alliance info", zap.Error(err))
}
if alliance == nil {
s.logger.Error("Alliance not found for operation", zap.Uint32("AllianceID", pkt.AllianceID))
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
switch pkt.Action {
case mhfpacket.OPERATE_JOINT_DISBAND:
if guild.LeaderCharID == s.charID && alliance.ParentGuildID == guild.ID {
if err := s.server.guildRepo.DeleteAlliance(alliance.ID); err != nil {
s.logger.Error("Failed to disband alliance", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} else {
s.logger.Warn(
"Non-owner of alliance attempted disband",
zap.Uint32("CharID", s.charID),
zap.Uint32("AllyID", alliance.ID),
)
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
}
case mhfpacket.OPERATE_JOINT_LEAVE:
if guild.LeaderCharID == s.charID {
if err := s.server.guildRepo.RemoveGuildFromAlliance(alliance.ID, guild.ID, alliance.SubGuild1ID, alliance.SubGuild2ID); err != nil {
s.logger.Error("Failed to remove guild from alliance", zap.Error(err))
}
// NOTE: Alliance join requests are not yet implemented (no DB table exists),
// so there are no pending applications to clean up on leave.
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} else {
s.logger.Warn(
"Non-owner of guild attempted alliance leave",
zap.Uint32("CharID", s.charID),
)
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
}
case mhfpacket.OPERATE_JOINT_ALLOW:
if guild.LeaderCharID == s.charID && alliance.ParentGuildID == guild.ID {
if err := s.server.guildRepo.SetAllianceRecruiting(alliance.ID, true); err != nil {
s.logger.Error("Failed to allow alliance applications", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} else {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
}
case mhfpacket.OPERATE_JOINT_DENY:
if guild.LeaderCharID == s.charID && alliance.ParentGuildID == guild.ID {
if err := s.server.guildRepo.SetAllianceRecruiting(alliance.ID, false); err != nil {
s.logger.Error("Failed to deny alliance applications", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} else {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
}
case mhfpacket.OPERATE_JOINT_KICK:
if alliance.ParentGuild.LeaderCharID == s.charID {
kickedGuildID := pkt.Data1.ReadUint32()
if err := s.server.guildRepo.RemoveGuildFromAlliance(alliance.ID, kickedGuildID, alliance.SubGuild1ID, alliance.SubGuild2ID); err != nil {
s.logger.Error("Failed to kick guild from alliance", zap.Error(err))
}
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
} else {
s.logger.Warn(
"Non-owner of alliance attempted kick",
zap.Uint32("CharID", s.charID),
zap.Uint32("AllyID", alliance.ID),
)
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
}
default:
s.logger.Error("unhandled operate joint action", zap.Uint8("action", uint8(pkt.Action)))
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
}
}
func handleMsgMhfInfoJoint(s *Session, p mhfpacket.MHFPacket) {
pkt := p.(*mhfpacket.MsgMhfInfoJoint)
bf := byteframe.NewByteFrame()
alliance, err := s.server.guildRepo.GetAllianceByID(pkt.AllianceID)
if err != nil || alliance == nil {
doAckSimpleFail(s, pkt.AckHandle, make([]byte, 4))
return
}
bf.WriteUint32(alliance.ID)
bf.WriteUint32(uint32(alliance.CreatedAt.Unix()))
bf.WriteUint16(alliance.TotalMembers)
bf.WriteUint16(0x0000) // Unk
ps.Uint16(bf, alliance.Name, true)
if alliance.SubGuild1ID > 0 {
if alliance.SubGuild2ID > 0 {
bf.WriteUint8(3)
} else {
bf.WriteUint8(2)
}
} else {
bf.WriteUint8(1)
}
bf.WriteUint32(alliance.ParentGuildID)
bf.WriteUint32(alliance.ParentGuild.LeaderCharID)
bf.WriteUint16(alliance.ParentGuild.Rank(s.server.erupeConfig.RealClientMode))
bf.WriteUint16(alliance.ParentGuild.MemberCount)
ps.Uint16(bf, alliance.ParentGuild.Name, true)
ps.Uint16(bf, alliance.ParentGuild.LeaderName, true)
if alliance.SubGuild1ID > 0 {
bf.WriteUint32(alliance.SubGuild1ID)
bf.WriteUint32(alliance.SubGuild1.LeaderCharID)
bf.WriteUint16(alliance.SubGuild1.Rank(s.server.erupeConfig.RealClientMode))
bf.WriteUint16(alliance.SubGuild1.MemberCount)
ps.Uint16(bf, alliance.SubGuild1.Name, true)
ps.Uint16(bf, alliance.SubGuild1.LeaderName, true)
}
if alliance.SubGuild2ID > 0 {
bf.WriteUint32(alliance.SubGuild2ID)
bf.WriteUint32(alliance.SubGuild2.LeaderCharID)
bf.WriteUint16(alliance.SubGuild2.Rank(s.server.erupeConfig.RealClientMode))
bf.WriteUint16(alliance.SubGuild2.MemberCount)
ps.Uint16(bf, alliance.SubGuild2.Name, true)
ps.Uint16(bf, alliance.SubGuild2.LeaderName, true)
}
doAckBufSucceed(s, pkt.AckHandle, bf.Data())
}