eq2go/internal/guilds/guilds.go
2025-08-29 14:10:30 -05:00

1305 lines
35 KiB
Go

package guilds
import (
"fmt"
"sync"
"time"
"eq2emu/internal/database"
"eq2emu/internal/packets"
)
// Entity represents a game entity that can be in guilds (simplified interface)
type Entity interface {
GetID() int32
GetName() string
GetLevel() int8
GetClass() int8
IsPlayer() bool
IsOnline() bool
}
// Client represents a connected client (simplified interface)
type Client interface {
GetVersion() uint32
SendPacket(opcode packets.InternalOpcode, data []byte) error
GetCharacterID() int32
}
// Logger interface for logging operations
type Logger interface {
Debug(msg string, args ...any)
Info(msg string, args ...any)
Error(msg string, args ...any)
}
// PointHistory represents a point modification entry in a member's history
type PointHistory struct {
Date time.Time `json:"date" db:"date"`
ModifiedBy string `json:"modified_by" db:"modified_by"`
Comment string `json:"comment" db:"comment"`
Points float64 `json:"points" db:"points"`
SaveNeeded bool `json:"-" db:"-"`
}
// GuildMember represents a member of a guild (consolidated from multiple files)
type GuildMember struct {
CharacterID int32 `json:"character_id" db:"character_id"`
AccountID int32 `json:"account_id" db:"account_id"`
RecruiterID int32 `json:"recruiter_id" db:"recruiter_id"`
Name string `json:"name" db:"name"`
GuildStatus int32 `json:"guild_status" db:"guild_status"`
Points float64 `json:"points" db:"points"`
AdventureClass int8 `json:"adventure_class" db:"adventure_class"`
AdventureLevel int8 `json:"adventure_level" db:"adventure_level"`
TradeskillClass int8 `json:"tradeskill_class" db:"tradeskill_class"`
TradeskillLevel int8 `json:"tradeskill_level" db:"tradeskill_level"`
Rank int8 `json:"rank" db:"rank"`
MemberFlags int8 `json:"member_flags" db:"member_flags"`
Zone string `json:"zone" db:"zone"`
JoinDate time.Time `json:"join_date" db:"join_date"`
LastLoginDate time.Time `json:"last_login_date" db:"last_login_date"`
Note string `json:"note" db:"note"`
OfficerNote string `json:"officer_note" db:"officer_note"`
RecruiterDescription string `json:"recruiter_description" db:"recruiter_description"`
RecruiterPictureData []byte `json:"recruiter_picture_data" db:"recruiter_picture_data"`
RecruitingShowAdventureClass int8 `json:"recruiting_show_adventure_class" db:"recruiting_show_adventure_class"`
PointHistory []PointHistory `json:"point_history" db:"-"`
// Runtime data
Entity Entity `json:"-" db:"-"`
Client Client `json:"-" db:"-"`
LastUpdate time.Time `json:"-" db:"-"`
SaveNeeded bool `json:"-" db:"-"`
}
// GuildEvent represents an event in the guild's history
type GuildEvent struct {
EventID int64 `json:"event_id" db:"event_id"`
Date time.Time `json:"date" db:"date"`
Type int32 `json:"type" db:"type"`
Description string `json:"description" db:"description"`
Locked int8 `json:"locked" db:"locked"`
SaveNeeded bool `json:"-" db:"-"`
}
// GuildBankEvent represents an event in a guild bank's history
type GuildBankEvent struct {
EventID int64 `json:"event_id" db:"event_id"`
Date time.Time `json:"date" db:"date"`
Type int32 `json:"type" db:"type"`
Description string `json:"description" db:"description"`
}
// Bank represents a guild bank with its event history
type Bank struct {
Name string `json:"name" db:"name"`
Events []GuildBankEvent `json:"events" db:"-"`
}
// GuildInvite represents a pending guild invitation
type GuildInvite struct {
GuildID int32 `json:"guild_id"`
GuildName string `json:"guild_name"`
CharacterID int32 `json:"character_id"`
CharacterName string `json:"character_name"`
InviterID int32 `json:"inviter_id"`
InviterName string `json:"inviter_name"`
Rank int8 `json:"rank"`
InviteDate time.Time `json:"invite_date"`
ExpiresAt time.Time `json:"expires_at"`
}
// Guild represents a guild with all its properties and members (consolidated from multiple files)
type Guild struct {
// Core guild properties
id int32
name string
level int8
formedDate time.Time
motd string
expCurrent int64
expToNextLevel int64
recruitingShortDesc string
recruitingFullDesc string
recruitingMinLevel int8
recruitingPlayStyle int8
// Guild data structures
members map[int32]*GuildMember
guildEvents []GuildEvent
permissions map[int8]map[int8]int8 // rank -> permission -> value
eventFilters map[int8]map[int8]int8 // event_id -> category -> value
recruitingFlags map[int8]int8
recruitingDescTags map[int8]int8
ranks map[int8]string // rank -> name
banks [4]Bank
// Guild hall information
guildHallLocation string
guildHallZoneName string
guildHallFilename string
// Tracking
nextEventID int64
lastModified time.Time
// Save flags
saveNeeded bool
memberSaveNeeded bool
eventsSaveNeeded bool
ranksSaveNeeded bool
eventFiltersSaveNeeded bool
pointsHistorySaveNeeded bool
recruitingSaveNeeded bool
// Thread safety
membersMutex sync.RWMutex
eventsMutex sync.RWMutex
ranksMutex sync.RWMutex
permissionsMutex sync.RWMutex
filtersMutex sync.RWMutex
recruitingMutex sync.RWMutex
coreMutex sync.RWMutex
}
// GuildManager manages all guilds in the system (consolidated from multiple files)
type GuildManager struct {
// Guild storage
guilds map[int32]*Guild
guildsMutex sync.RWMutex
// Guild ID generation
nextGuildID int32
nextGuildIDMutex sync.Mutex
// Pending invitations
pendingInvites map[int32]*GuildInvite // character_id -> invite
invitesMutex sync.RWMutex
// Statistics (simplified)
stats GuildManagerStats
statsMutex sync.RWMutex
// Dependencies
database *database.Database
logger Logger
}
// GuildManagerStats holds essential guild management statistics
type GuildManagerStats struct {
TotalGuilds int64 `json:"total_guilds"`
ActiveGuilds int64 `json:"active_guilds"`
TotalMembers int64 `json:"total_members"`
TotalEvents int64 `json:"total_events"`
TotalRecruiters int64 `json:"total_recruiters"`
UniqueAccounts int64 `json:"unique_accounts"`
TotalInvites int64 `json:"total_invites"`
AcceptedInvites int64 `json:"accepted_invites"`
DeclinedInvites int64 `json:"declined_invites"`
ExpiredInvites int64 `json:"expired_invites"`
LastStatsUpdate time.Time `json:"last_stats_update"`
}
// GuildInfo provides basic guild information
type GuildInfo struct {
ID int32 `json:"id"`
Name string `json:"name"`
Level int8 `json:"level"`
FormedDate time.Time `json:"formed_date"`
MOTD string `json:"motd"`
MemberCount int `json:"member_count"`
OnlineMemberCount int `json:"online_member_count"`
RecruiterCount int `json:"recruiter_count"`
RecruitingShortDesc string `json:"recruiting_short_desc"`
RecruitingFullDesc string `json:"recruiting_full_desc"`
RecruitingMinLevel int8 `json:"recruiting_min_level"`
RecruitingPlayStyle int8 `json:"recruiting_play_style"`
IsRecruiting bool `json:"is_recruiting"`
UniqueAccounts int `json:"unique_accounts"`
ExpCurrent int64 `json:"exp_current"`
ExpToNextLevel int64 `json:"exp_to_next_level"`
}
// NewGuildManager creates a new guild manager
func NewGuildManager(db *database.Database, logger Logger) *GuildManager {
if logger == nil {
logger = &nullLogger{}
}
return &GuildManager{
guilds: make(map[int32]*Guild),
nextGuildID: 1,
pendingInvites: make(map[int32]*GuildInvite),
database: db,
logger: logger,
stats: GuildManagerStats{LastStatsUpdate: time.Now()},
}
}
// Guild creation and management (preserving C++ API)
// CreateGuild creates a new guild with the specified leader and name
func (gm *GuildManager) CreateGuild(leaderID int32, guildName string) (*Guild, error) {
if len(guildName) == 0 || len(guildName) > MaxGuildNameLength {
return nil, fmt.Errorf("invalid guild name length")
}
// Check if guild name already exists
if gm.GuildNameExists(guildName) {
return nil, fmt.Errorf("guild name already exists")
}
// Generate guild ID
gm.nextGuildIDMutex.Lock()
guildID := gm.nextGuildID
gm.nextGuildID++
gm.nextGuildIDMutex.Unlock()
// Create the guild
guild := &Guild{
id: guildID,
name: guildName,
level: 1,
formedDate: time.Now(),
motd: "",
expCurrent: 111,
expToNextLevel: 2521,
recruitingMinLevel: 1,
members: make(map[int32]*GuildMember),
guildEvents: make([]GuildEvent, 0),
permissions: make(map[int8]map[int8]int8),
eventFilters: make(map[int8]map[int8]int8),
recruitingFlags: make(map[int8]int8),
recruitingDescTags: make(map[int8]int8),
ranks: make(map[int8]string),
nextEventID: 1,
lastModified: time.Now(),
saveNeeded: true,
}
// Initialize default recruiting flags
guild.recruitingFlags[RecruitingFlagTraining] = 0
guild.recruitingFlags[RecruitingFlagFighters] = 0
guild.recruitingFlags[RecruitingFlagPriests] = 0
guild.recruitingFlags[RecruitingFlagScouts] = 0
guild.recruitingFlags[RecruitingFlagMages] = 0
guild.recruitingFlags[RecruitingFlagTradeskillers] = 0
// Initialize default description tags
guild.recruitingDescTags[0] = RecruitingDescTagNone
guild.recruitingDescTags[1] = RecruitingDescTagNone
guild.recruitingDescTags[2] = RecruitingDescTagNone
guild.recruitingDescTags[3] = RecruitingDescTagNone
// Initialize default bank names
guild.banks[0].Name = "Bank 1"
guild.banks[1].Name = "Bank 2"
guild.banks[2].Name = "Bank 3"
guild.banks[3].Name = "Bank 4"
// Initialize default rank names
for rank, name := range DefaultRankNames {
guild.ranks[rank] = name
}
// Add leader as first member
leader := &GuildMember{
CharacterID: leaderID,
Rank: RankLeader,
JoinDate: time.Now(),
LastLoginDate: time.Now(),
PointHistory: make([]PointHistory, 0),
SaveNeeded: true,
}
guild.members[leaderID] = leader
guild.memberSaveNeeded = true
// Add guild to manager
gm.guildsMutex.Lock()
gm.guilds[guildID] = guild
gm.guildsMutex.Unlock()
// Add formation event
guild.AddNewGuildEvent(EventGuildFormed, fmt.Sprintf("%s formed the guild", leader.Name), time.Now(), true)
// Update statistics
gm.statsMutex.Lock()
gm.stats.TotalGuilds++
gm.stats.ActiveGuilds++
gm.stats.TotalMembers++
gm.stats.LastStatsUpdate = time.Now()
gm.statsMutex.Unlock()
gm.logger.Info("Created new guild", "guild_id", guildID, "name", guildName, "leader_id", leaderID)
return guild, nil
}
// DeleteGuild removes a guild from the manager
func (gm *GuildManager) DeleteGuild(guildID int32, reason string) error {
gm.guildsMutex.Lock()
guild, exists := gm.guilds[guildID]
if !exists {
gm.guildsMutex.Unlock()
return fmt.Errorf("guild not found: %d", guildID)
}
delete(gm.guilds, guildID)
gm.guildsMutex.Unlock()
// Add disbanding event
guild.AddNewGuildEvent(EventGuildDisbanded, reason, time.Now(), true)
// Clear all members
guild.membersMutex.Lock()
memberCount := len(guild.members)
guild.members = make(map[int32]*GuildMember)
guild.membersMutex.Unlock()
// Update statistics
gm.statsMutex.Lock()
gm.stats.ActiveGuilds--
gm.stats.TotalMembers -= int64(memberCount)
gm.stats.LastStatsUpdate = time.Now()
gm.statsMutex.Unlock()
gm.logger.Info("Deleted guild", "guild_id", guildID, "reason", reason)
return nil
}
// GetGuild retrieves a guild by ID
func (gm *GuildManager) GetGuild(guildID int32) *Guild {
gm.guildsMutex.RLock()
defer gm.guildsMutex.RUnlock()
return gm.guilds[guildID]
}
// GetGuildByName retrieves a guild by name
func (gm *GuildManager) GetGuildByName(name string) *Guild {
gm.guildsMutex.RLock()
defer gm.guildsMutex.RUnlock()
for _, guild := range gm.guilds {
if guild.GetName() == name {
return guild
}
}
return nil
}
// GuildNameExists checks if a guild name already exists
func (gm *GuildManager) GuildNameExists(name string) bool {
return gm.GetGuildByName(name) != nil
}
// GetPlayerGuild returns the guild for a player character
func (gm *GuildManager) GetPlayerGuild(characterID int32) *Guild {
gm.guildsMutex.RLock()
defer gm.guildsMutex.RUnlock()
for _, guild := range gm.guilds {
if guild.HasMember(characterID) {
return guild
}
}
return nil
}
// Guild invitation system (preserving C++ API)
// InvitePlayerToGuild invites a player to join a guild
func (gm *GuildManager) InvitePlayerToGuild(guildID, inviterID, characterID int32, rank int8) int8 {
guild := gm.GetGuild(guildID)
if guild == nil {
return GUILD_INVITE_GUILD_NOT_FOUND
}
// Check if inviter has permission
inviter := guild.GetMember(inviterID)
if inviter == nil {
return GUILD_INVITE_NO_PERMISSION
}
if !guild.HasPermission(inviter.Rank, PermissionInvite) {
return GUILD_INVITE_NO_PERMISSION
}
// Check if player is already in a guild
if gm.GetPlayerGuild(characterID) != nil {
return GUILD_INVITE_ALREADY_IN_GUILD
}
// Check if player already has a pending invite
if gm.HasPendingInvite(characterID) {
return GUILD_INVITE_ALREADY_HAS_INVITE
}
// Create the invitation
invite := &GuildInvite{
GuildID: guildID,
GuildName: guild.GetName(),
CharacterID: characterID,
InviterID: inviterID,
InviterName: inviter.Name,
Rank: rank,
InviteDate: time.Now(),
ExpiresAt: time.Now().Add(GUILD_INVITE_TIMEOUT),
}
gm.invitesMutex.Lock()
gm.pendingInvites[characterID] = invite
gm.invitesMutex.Unlock()
gm.statsMutex.Lock()
gm.stats.TotalInvites++
gm.stats.LastStatsUpdate = time.Now()
gm.statsMutex.Unlock()
gm.logger.Info("Guild invitation sent", "guild_id", guildID, "inviter_id", inviterID, "character_id", characterID)
return GUILD_INVITE_SUCCESS
}
// AcceptGuildInvite handles accepting a guild invitation
func (gm *GuildManager) AcceptGuildInvite(characterID int32) int8 {
gm.invitesMutex.Lock()
invite, exists := gm.pendingInvites[characterID]
if !exists {
gm.invitesMutex.Unlock()
return GUILD_INVITE_NO_INVITE
}
// Check if invite has expired
if invite.IsExpired() {
delete(gm.pendingInvites, characterID)
gm.invitesMutex.Unlock()
gm.statsMutex.Lock()
gm.stats.ExpiredInvites++
gm.statsMutex.Unlock()
return GUILD_INVITE_EXPIRED
}
// Remove the invite
delete(gm.pendingInvites, characterID)
gm.invitesMutex.Unlock()
// Get the guild
guild := gm.GetGuild(invite.GuildID)
if guild == nil {
return GUILD_INVITE_GUILD_NOT_FOUND
}
// Add the member to the guild
success := guild.AddNewGuildMember(characterID, invite.InviterName, time.Now(), invite.Rank)
if !success {
return GUILD_INVITE_FAILED
}
gm.statsMutex.Lock()
gm.stats.AcceptedInvites++
gm.stats.TotalMembers++
gm.stats.LastStatsUpdate = time.Now()
gm.statsMutex.Unlock()
gm.logger.Info("Guild invitation accepted", "guild_id", invite.GuildID, "character_id", characterID)
return GUILD_INVITE_SUCCESS
}
// DeclineGuildInvite handles declining a guild invitation
func (gm *GuildManager) DeclineGuildInvite(characterID int32) {
gm.invitesMutex.Lock()
_, exists := gm.pendingInvites[characterID]
if exists {
delete(gm.pendingInvites, characterID)
}
gm.invitesMutex.Unlock()
if exists {
gm.statsMutex.Lock()
gm.stats.DeclinedInvites++
gm.stats.LastStatsUpdate = time.Now()
gm.statsMutex.Unlock()
gm.logger.Info("Guild invitation declined", "character_id", characterID)
}
}
// HasPendingInvite checks if a character has a pending guild invitation
func (gm *GuildManager) HasPendingInvite(characterID int32) bool {
gm.invitesMutex.RLock()
invite, exists := gm.pendingInvites[characterID]
gm.invitesMutex.RUnlock()
if !exists || invite.IsExpired() {
if exists {
// Clean up expired invite
gm.invitesMutex.Lock()
delete(gm.pendingInvites, characterID)
gm.invitesMutex.Unlock()
}
return false
}
return true
}
// GetPendingInvite returns the pending invite for a character
func (gm *GuildManager) GetPendingInvite(characterID int32) *GuildInvite {
gm.invitesMutex.RLock()
defer gm.invitesMutex.RUnlock()
invite, exists := gm.pendingInvites[characterID]
if !exists || invite.IsExpired() {
return nil
}
return invite
}
// Statistics and utilities
// GetStats returns the guild manager statistics
func (gm *GuildManager) GetStats() GuildManagerStats {
gm.statsMutex.RLock()
defer gm.statsMutex.RUnlock()
return gm.stats
}
// GetGuildCount returns the current number of active guilds
func (gm *GuildManager) GetGuildCount() int32 {
gm.guildsMutex.RLock()
defer gm.guildsMutex.RUnlock()
return int32(len(gm.guilds))
}
// GetAllGuilds returns a copy of all guilds
func (gm *GuildManager) GetAllGuilds() []*Guild {
gm.guildsMutex.RLock()
defer gm.guildsMutex.RUnlock()
guilds := make([]*Guild, 0, len(gm.guilds))
for _, guild := range gm.guilds {
guilds = append(guilds, guild)
}
return guilds
}
// Guild methods (preserving C++ API)
// GetID returns the guild ID
func (g *Guild) GetID() int32 {
g.coreMutex.RLock()
defer g.coreMutex.RUnlock()
return g.id
}
// GetName returns the guild name
func (g *Guild) GetName() string {
g.coreMutex.RLock()
defer g.coreMutex.RUnlock()
return g.name
}
// GetLevel returns the guild level
func (g *Guild) GetLevel() int8 {
g.coreMutex.RLock()
defer g.coreMutex.RUnlock()
return g.level
}
// GetMOTD returns the guild message of the day
func (g *Guild) GetMOTD() string {
g.coreMutex.RLock()
defer g.coreMutex.RUnlock()
return g.motd
}
// GetFormedDate returns the guild formation date
func (g *Guild) GetFormedDate() time.Time {
g.coreMutex.RLock()
defer g.coreMutex.RUnlock()
return g.formedDate
}
// SetName sets the guild name
func (g *Guild) SetName(name string, sendPacket bool) {
if len(name) > MaxGuildNameLength {
name = name[:MaxGuildNameLength]
}
g.coreMutex.Lock()
g.name = name
g.lastModified = time.Now()
g.saveNeeded = true
g.coreMutex.Unlock()
if sendPacket {
g.SendGuildUpdate()
}
}
// SetLevel sets the guild level
func (g *Guild) SetLevel(level int8, sendPacket bool) {
if level > MaxGuildLevel {
level = MaxGuildLevel
}
if level < 1 {
level = 1
}
g.coreMutex.Lock()
g.level = level
g.lastModified = time.Now()
g.saveNeeded = true
g.coreMutex.Unlock()
if sendPacket {
g.SendGuildUpdate()
}
}
// SetMOTD sets the guild message of the day
func (g *Guild) SetMOTD(motd string, sendPacket bool) {
if len(motd) > MaxMOTDLength {
motd = motd[:MaxMOTDLength]
}
g.coreMutex.Lock()
g.motd = motd
g.lastModified = time.Now()
g.saveNeeded = true
g.coreMutex.Unlock()
if sendPacket {
g.SendGuildUpdate()
}
}
// AddEXPCurrent adds experience to the guild
func (g *Guild) AddEXPCurrent(exp int64, sendPacket bool) {
g.coreMutex.Lock()
g.expCurrent += exp
g.lastModified = time.Now()
g.saveNeeded = true
g.coreMutex.Unlock()
if sendPacket {
g.SendGuildUpdate()
}
}
// GetEXPCurrent returns the current guild experience
func (g *Guild) GetEXPCurrent() int64 {
g.coreMutex.RLock()
defer g.coreMutex.RUnlock()
return g.expCurrent
}
// GetEXPToNextLevel returns the experience needed for next level
func (g *Guild) GetEXPToNextLevel() int64 {
g.coreMutex.RLock()
defer g.coreMutex.RUnlock()
return g.expToNextLevel
}
// Member management (preserving C++ API)
// AddNewGuildMember adds a new member to the guild
func (g *Guild) AddNewGuildMember(characterID int32, invitedBy string, joinDate time.Time, rank int8) bool {
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
// Check if member already exists
if _, exists := g.members[characterID]; exists {
return false
}
member := &GuildMember{
CharacterID: characterID,
Rank: rank,
JoinDate: joinDate,
LastLoginDate: time.Now(),
PointHistory: make([]PointHistory, 0),
SaveNeeded: true,
}
g.members[characterID] = member
g.memberSaveNeeded = true
g.lastModified = time.Now()
// Add guild event
g.addNewGuildEventNoLock(EventMemberJoins, fmt.Sprintf("%s has joined the guild", member.Name), time.Now(), true)
return true
}
// RemoveGuildMember removes a member from the guild
func (g *Guild) RemoveGuildMember(characterID int32, sendPacket bool) {
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
if member, exists := g.members[characterID]; exists {
// Add guild event
g.addNewGuildEventNoLock(EventMemberLeaves, fmt.Sprintf("%s has left the guild", member.Name), time.Now(), sendPacket)
delete(g.members, characterID)
g.memberSaveNeeded = true
g.lastModified = time.Now()
}
}
// GetMember returns a guild member by character ID
func (g *Guild) GetMember(characterID int32) *GuildMember {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
return g.members[characterID]
}
// GetMemberByName returns a guild member by name
func (g *Guild) GetMemberByName(playerName string) *GuildMember {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
for _, member := range g.members {
if member.Name == playerName {
return member
}
}
return nil
}
// HasMember checks if a character is a member of the guild
func (g *Guild) HasMember(characterID int32) bool {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
_, exists := g.members[characterID]
return exists
}
// GetMemberCount returns the number of members in the guild
func (g *Guild) GetMemberCount() int {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
return len(g.members)
}
// GetAllMembers returns all guild members
func (g *Guild) GetAllMembers() []*GuildMember {
g.membersMutex.RLock()
defer g.membersMutex.RUnlock()
members := make([]*GuildMember, 0, len(g.members))
for _, member := range g.members {
members = append(members, member)
}
return members
}
// PromoteGuildMember promotes a guild member
func (g *Guild) PromoteGuildMember(characterID int32, promoterName string, sendPacket bool) bool {
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
member, exists := g.members[characterID]
if !exists || member.Rank <= RankLeader {
return false
}
oldRank := member.Rank
member.Rank--
member.SaveNeeded = true
g.memberSaveNeeded = true
g.lastModified = time.Now()
// Add guild event
g.addNewGuildEventNoLock(EventMemberPromoted,
fmt.Sprintf("%s has been promoted from %s to %s by %s",
member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), promoterName),
time.Now(), sendPacket)
return true
}
// DemoteGuildMember demotes a guild member
func (g *Guild) DemoteGuildMember(characterID int32, demoterName string, sendPacket bool) bool {
g.membersMutex.Lock()
defer g.membersMutex.Unlock()
member, exists := g.members[characterID]
if !exists || member.Rank >= RankRecruit {
return false
}
oldRank := member.Rank
member.Rank++
member.SaveNeeded = true
g.memberSaveNeeded = true
g.lastModified = time.Now()
// Add guild event
g.addNewGuildEventNoLock(EventMemberDemoted,
fmt.Sprintf("%s has been demoted from %s to %s by %s",
member.Name, g.getRankNameNoLock(oldRank), g.getRankNameNoLock(member.Rank), demoterName),
time.Now(), sendPacket)
return true
}
// Permission system (preserving C++ API)
// SetPermission sets a guild permission for a rank
func (g *Guild) SetPermission(rank, permission, value int8, sendPacket, saveNeeded bool) bool {
if rank < RankLeader || rank > RankRecruit {
return false
}
g.permissionsMutex.Lock()
defer g.permissionsMutex.Unlock()
if g.permissions[rank] == nil {
g.permissions[rank] = make(map[int8]int8)
}
g.permissions[rank][permission] = value
g.lastModified = time.Now()
if saveNeeded {
g.ranksSaveNeeded = true
}
return true
}
// GetPermission returns a guild permission for a rank
func (g *Guild) GetPermission(rank, permission int8) int8 {
g.permissionsMutex.RLock()
defer g.permissionsMutex.RUnlock()
if rankPerms, exists := g.permissions[rank]; exists {
if value, exists := rankPerms[permission]; exists {
return value
}
}
// Return default permission based on rank
return g.getDefaultPermission(rank, permission)
}
// HasPermission checks if a rank has a specific permission
func (g *Guild) HasPermission(rank, permission int8) bool {
return g.GetPermission(rank, permission) > 0
}
// Rank management (preserving C++ API)
// SetRankName sets a custom rank name
func (g *Guild) SetRankName(rank int8, name string, sendPacket bool) bool {
if rank < RankLeader || rank > RankRecruit {
return false
}
g.ranksMutex.Lock()
g.ranks[rank] = name
g.lastModified = time.Now()
g.ranksSaveNeeded = true
g.ranksMutex.Unlock()
if sendPacket {
g.SendGuildUpdate()
}
return true
}
// GetRankName returns the name for a rank
func (g *Guild) GetRankName(rank int8) string {
g.ranksMutex.RLock()
defer g.ranksMutex.RUnlock()
return g.getRankNameNoLock(rank)
}
// Event system (preserving C++ API)
// AddNewGuildEvent adds a new event to the guild
func (g *Guild) AddNewGuildEvent(eventType int32, description string, date time.Time, sendPacket bool) {
g.eventsMutex.Lock()
defer g.eventsMutex.Unlock()
g.addNewGuildEventNoLock(eventType, description, date, sendPacket)
}
// GetGuildEvent returns a guild event by ID
func (g *Guild) GetGuildEvent(eventID int64) *GuildEvent {
g.eventsMutex.RLock()
defer g.eventsMutex.RUnlock()
for i := range g.guildEvents {
if g.guildEvents[i].EventID == eventID {
return &g.guildEvents[i]
}
}
return nil
}
// GetNextEventID returns the next available event ID
func (g *Guild) GetNextEventID() int64 {
g.eventsMutex.Lock()
defer g.eventsMutex.Unlock()
eventID := g.nextEventID
g.nextEventID++
return eventID
}
// Recruiting system (preserving C++ API)
// SetRecruitingFlag sets a recruiting flag
func (g *Guild) SetRecruitingFlag(flag, value int8, sendPacket bool) bool {
if flag < RecruitingFlagTraining || flag > RecruitingFlagTradeskillers {
return false
}
g.recruitingMutex.Lock()
g.recruitingFlags[flag] = value
g.lastModified = time.Now()
g.recruitingSaveNeeded = true
g.recruitingMutex.Unlock()
if sendPacket {
g.SendGuildUpdate()
}
return true
}
// GetRecruitingFlag returns a recruiting flag
func (g *Guild) GetRecruitingFlag(flag int8) int8 {
g.recruitingMutex.RLock()
defer g.recruitingMutex.RUnlock()
if value, exists := g.recruitingFlags[flag]; exists {
return value
}
return 0
}
// GetGuildInfo returns basic guild information
func (g *Guild) GetGuildInfo() GuildInfo {
g.coreMutex.RLock()
g.membersMutex.RLock()
g.recruitingMutex.RLock()
defer g.coreMutex.RUnlock()
defer g.membersMutex.RUnlock()
defer g.recruitingMutex.RUnlock()
return GuildInfo{
ID: g.id,
Name: g.name,
Level: g.level,
FormedDate: g.formedDate,
MOTD: g.motd,
MemberCount: len(g.members),
OnlineMemberCount: g.getOnlineMemberCountNoLock(),
RecruiterCount: g.getRecruiterCountNoLock(),
RecruitingShortDesc: g.recruitingShortDesc,
RecruitingFullDesc: g.recruitingFullDesc,
RecruitingMinLevel: g.recruitingMinLevel,
RecruitingPlayStyle: g.recruitingPlayStyle,
IsRecruiting: g.getRecruiterCountNoLock() > 0,
UniqueAccounts: g.getUniqueAccountCountNoLock(),
ExpCurrent: g.expCurrent,
ExpToNextLevel: g.expToNextLevel,
}
}
// Packet sending methods
// SendGuildUpdate sends a guild update packet to all members
func (g *Guild) SendGuildUpdate() {
members := g.GetAllMembers()
for _, member := range members {
if member.Client != nil {
g.sendGuildUpdateToClient(member.Client)
}
}
}
// sendGuildUpdateToClient sends a guild update packet to a specific client
func (g *Guild) sendGuildUpdateToClient(client Client) {
clientVersion := client.GetVersion()
packetData, err := g.buildGuildUpdatePacket(clientVersion)
if err != nil {
return
}
client.SendPacket(packets.OP_GuildUpdateMsg, packetData)
}
// Helper methods (internal, no lock versions)
func (g *Guild) getDefaultPermission(rank, permission int8) int8 {
// Leaders have all permissions by default
if rank == RankLeader {
return 1
}
// Default permissions based on rank and permission type
switch permission {
case PermissionSeeGuildChat, PermissionSpeakInGuildChat:
return 1 // All members can see and speak in guild chat
case PermissionReceivePoints:
return 1 // All members can receive points
case PermissionSeeOfficerChat, PermissionSpeakInOfficerChat:
if rank <= RankOfficer {
return 1
}
case PermissionInvite:
if rank <= RankSeniorMember {
return 1
}
}
return 0 // Default to no permission
}
func (g *Guild) getRankNameNoLock(rank int8) string {
if name, exists := g.ranks[rank]; exists {
return name
}
if defaultName, exists := DefaultRankNames[rank]; exists {
return defaultName
}
return "Unknown"
}
func (g *Guild) addNewGuildEventNoLock(eventType int32, description string, date time.Time, sendPacket bool) {
event := GuildEvent{
EventID: g.nextEventID,
Date: date,
Type: eventType,
Description: description,
Locked: 0,
SaveNeeded: true,
}
g.nextEventID++
// Add to front of events list (newest first)
g.guildEvents = append([]GuildEvent{event}, g.guildEvents...)
// Limit event history
if len(g.guildEvents) > MaxEvents {
g.guildEvents = g.guildEvents[:MaxEvents]
}
g.eventsSaveNeeded = true
g.lastModified = time.Now()
}
func (g *Guild) getOnlineMemberCountNoLock() int {
count := 0
for _, member := range g.members {
if member.Entity != nil && member.Entity.IsOnline() {
count++
}
}
return count
}
func (g *Guild) getRecruiterCountNoLock() int {
count := 0
for _, member := range g.members {
if member.MemberFlags&MemberFlagRecruitingForGuild != 0 {
count++
}
}
return count
}
func (g *Guild) getUniqueAccountCountNoLock() int {
accounts := make(map[int32]bool)
for _, member := range g.members {
accounts[member.AccountID] = true
}
return len(accounts)
}
// Helper methods for GuildInvite
// IsExpired checks if the guild invite has expired
func (gi *GuildInvite) IsExpired() bool {
return time.Now().After(gi.ExpiresAt)
}
// TimeRemaining returns the remaining time for the invite
func (gi *GuildInvite) TimeRemaining() time.Duration {
return time.Until(gi.ExpiresAt)
}
// buildGuildUpdatePacket builds a guild update packet using the packet system
func (g *Guild) buildGuildUpdatePacket(clientVersion uint32) ([]byte, error) {
g.coreMutex.RLock()
g.membersMutex.RLock()
g.ranksMutex.RLock()
g.permissionsMutex.RLock()
g.recruitingMutex.RLock()
defer g.coreMutex.RUnlock()
defer g.membersMutex.RUnlock()
defer g.ranksMutex.RUnlock()
defer g.permissionsMutex.RUnlock()
defer g.recruitingMutex.RUnlock()
packet := make(map[string]any)
// Basic guild information
packet["guild_name"] = g.name
packet["guild_motd"] = g.motd
packet["guild_id"] = g.id
packet["guild_level"] = uint8(g.level)
packet["formed_date"] = uint32(g.formedDate.Unix())
packet["unique_accounts"] = uint16(g.getUniqueAccountCountNoLock())
packet["num_members"] = uint16(len(g.members))
packet["exp_current"] = uint64(g.expCurrent)
packet["exp_to_next_level"] = uint64(g.expToNextLevel)
// Version-specific fields
if clientVersion >= 1008 {
packet["guild_hall_location"] = g.guildHallLocation
packet["guild_hall_zonename"] = g.guildHallZoneName
packet["guild_hall_filename"] = g.guildHallFilename
}
// Event filters (simplified - would need proper implementation based on C++)
packet["event_filter_retain1"] = uint32(0xFFFFFFFF)
packet["event_filter_retain2"] = uint32(0xFFFFFFFF)
packet["event_filter_retain3"] = uint32(0xFFFFFFFF)
packet["event_filter_retain4"] = uint32(0xFFFFFFFF)
packet["event_filter_broadcast1"] = uint32(0xFFFFFFFF)
packet["event_filter_broadcast2"] = uint32(0xFFFFFFFF)
packet["event_filter_broadcast3"] = uint32(0xFFFFFFFF)
packet["event_filter_broadcast4"] = uint32(0xFFFFFFFF)
// Recruiting information
if clientVersion >= 562 {
packet["recruiting_looking_for"] = uint8(g.getRecruitingLookingForNoLock())
packet["recruiting_desc_tag1"] = uint8(g.recruitingDescTags[0])
packet["recruiting_desc_tag2"] = uint8(g.recruitingDescTags[1])
packet["recruiting_desc_tag3"] = uint8(g.recruitingDescTags[2])
packet["recruiting_desc_tag4"] = uint8(g.recruitingDescTags[3])
packet["recruiting_playstyle"] = uint8(g.recruitingPlayStyle)
packet["recruiting_min_level"] = uint8(g.recruitingMinLevel)
}
packet["recuiting_short_description"] = g.recruitingShortDesc // Note: typo preserved from C++
packet["recruiting_full_description"] = g.recruitingFullDesc
// Rank names and permissions
for rank := int8(RankLeader); rank <= int8(RankRecruit); rank++ {
rankNameField := fmt.Sprintf("rank%d_name", rank)
perm1Field := fmt.Sprintf("rank%d_permissions1", rank)
perm2Field := fmt.Sprintf("rank%d_permissions2", rank)
packet[rankNameField] = g.getRankNameNoLock(rank)
// Pack permissions into two uint32 values (simplified)
perm1, perm2 := g.packPermissionsNoLock(rank)
packet[perm1Field] = perm1
packet[perm2Field] = perm2
}
// Bank names (for version >= 562)
if clientVersion >= 562 {
packet["bank1_name"] = g.banks[0].Name
packet["bank2_name"] = g.banks[1].Name
packet["bank3_name"] = g.banks[2].Name
packet["bank4_name"] = g.banks[3].Name
}
// Build the packet
return packets.BuildPacket("GuildUpdate", packet, clientVersion, 0)
}
// packPermissionsNoLock packs guild permissions for a rank into two uint32 values
func (g *Guild) packPermissionsNoLock(rank int8) (uint32, uint32) {
var perm1, perm2 uint32
// Pack the first 32 permissions into perm1
for i := int8(0); i < 32; i++ {
if g.getPermissionNoLock(rank, i) > 0 {
perm1 |= 1 << i
}
}
// Pack the next 32 permissions into perm2
for i := int8(32); i < 64; i++ {
if g.getPermissionNoLock(rank, i) > 0 {
perm2 |= 1 << (i - 32)
}
}
return perm1, perm2
}
// getPermissionNoLock returns a guild permission without locking (for internal use)
func (g *Guild) getPermissionNoLock(rank, permission int8) int8 {
if rankPerms, exists := g.permissions[rank]; exists {
if value, exists := rankPerms[permission]; exists {
return value
}
}
// Return default permission based on rank
return g.getDefaultPermission(rank, permission)
}
// getRecruitingLookingForNoLock returns the recruiting flags as a packed byte
func (g *Guild) getRecruitingLookingForNoLock() uint8 {
var flags uint8
if g.recruitingFlags[RecruitingFlagTraining] > 0 {
flags |= 1 << 0
}
if g.recruitingFlags[RecruitingFlagFighters] > 0 {
flags |= 1 << 1
}
if g.recruitingFlags[RecruitingFlagPriests] > 0 {
flags |= 1 << 2
}
if g.recruitingFlags[RecruitingFlagScouts] > 0 {
flags |= 1 << 3
}
if g.recruitingFlags[RecruitingFlagMages] > 0 {
flags |= 1 << 4
}
if g.recruitingFlags[RecruitingFlagTradeskillers] > 0 {
flags |= 1 << 5
}
return flags
}
// nullLogger is a no-op logger implementation
type nullLogger struct{}
func (nl *nullLogger) Debug(msg string, args ...any) {}
func (nl *nullLogger) Info(msg string, args ...any) {}
func (nl *nullLogger) Error(msg string, args ...any) {}