diff --git a/SIMPLIFICATION.md b/SIMPLIFICATION.md index 7e17ab7..655a5e6 100644 --- a/SIMPLIFICATION.md +++ b/SIMPLIFICATION.md @@ -13,6 +13,8 @@ This document outlines how we successfully simplified the EverQuest II housing p - Entity - Factions - Ground Spawn +- Groups +- Guilds ## Before: Complex Architecture (8 Files, ~2000+ Lines) diff --git a/internal/guilds/constants.go b/internal/guilds/constants.go index f2ec563..2eb0975 100644 --- a/internal/guilds/constants.go +++ b/internal/guilds/constants.go @@ -1,5 +1,7 @@ package guilds +import "time" + // Guild rank constants const ( RankLeader = 0 @@ -228,3 +230,27 @@ var DefaultRankNames = map[int8]string{ RankInitiate: "Initiate", RankRecruit: "Recruit", } + +// Additional guild-related constants + +// Guild event types (additional) +const ( + EventGuildFormed = 200 // Custom event for guild formation + EventGuildDisbanded = 201 // Custom event for guild disbanding +) + +// Guild invitation result codes (preserved from C++) +const ( + GUILD_INVITE_SUCCESS = 0 + GUILD_INVITE_GUILD_NOT_FOUND = 1 + GUILD_INVITE_NO_PERMISSION = 2 + GUILD_INVITE_ALREADY_IN_GUILD = 3 + GUILD_INVITE_ALREADY_HAS_INVITE = 4 + GUILD_INVITE_TARGET_NOT_FOUND = 5 + GUILD_INVITE_NO_INVITE = 6 + GUILD_INVITE_EXPIRED = 7 + GUILD_INVITE_FAILED = 8 +) + +// Guild invitation timeout (5 minutes) +const GUILD_INVITE_TIMEOUT = 5 * time.Minute diff --git a/internal/guilds/guild.go b/internal/guilds/guild.go deleted file mode 100644 index fa4505d..0000000 --- a/internal/guilds/guild.go +++ /dev/null @@ -1,926 +0,0 @@ -package guilds - -import ( - "context" - "fmt" - "strings" - "time" - - "eq2emu/internal/database" -) - -// New creates a new guild instance -func New(db *database.Database) *Guild { - guild := &Guild{ - db: db, - isNew: true, - 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), - level: 1, - expCurrent: 111, - expToNextLevel: 2521, - recruitingMinLevel: 1, - recruitingPlayStyle: RecruitingPlayStyleNone, - nextEventID: 1, - lastModified: time.Now(), - } - - // 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 - } - - return guild -} - -// Load loads a guild from the database by ID -func Load(db *database.Database, id int32) (*Guild, error) { - guild := &Guild{ - db: db, - isNew: false, - id: id, - } - - if err := guild.Reload(); err != nil { - return nil, fmt.Errorf("failed to load guild %d: %w", id, err) - } - - return guild, nil -} - -// Save saves the guild to the database -func (g *Guild) Save() error { - ctx := context.Background() - - if g.isNew { - return g.create(ctx) - } - return g.update(ctx) -} - -// Delete removes the guild from the database -func (g *Guild) Delete() error { - ctx := context.Background() - return g.delete(ctx) -} - -// Reload refreshes the guild data from the database -func (g *Guild) Reload() error { - ctx := context.Background() - return g.load(ctx) -} - -// SetID sets the guild ID -func (g *Guild) SetID(id int32) { - g.mu.Lock() - defer g.mu.Unlock() - g.id = id -} - -// SetName sets the guild name -func (g *Guild) SetName(name string, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - if len(name) > MaxGuildNameLength { - name = name[:MaxGuildNameLength] - } - g.name = name - g.lastModified = time.Now() - - if sendPacket { - g.saveNeeded = true - } -} - -// SetLevel sets the guild level -func (g *Guild) SetLevel(level int8, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - if level > MaxGuildLevel { - level = MaxGuildLevel - } - if level < 1 { - level = 1 - } - - g.level = level - g.lastModified = time.Now() - - if sendPacket { - g.saveNeeded = true - } -} - -// SetFormedDate sets the guild formation date -func (g *Guild) SetFormedDate(formedDate time.Time) { - g.mu.Lock() - defer g.mu.Unlock() - g.formedDate = formedDate -} - -// SetMOTD sets the guild message of the day -func (g *Guild) SetMOTD(motd string, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - if len(motd) > MaxMOTDLength { - motd = motd[:MaxMOTDLength] - } - g.motd = motd - g.lastModified = time.Now() - - if sendPacket { - g.saveNeeded = true - } -} - -// GetID returns the guild ID -func (g *Guild) GetID() int32 { - g.mu.RLock() - defer g.mu.RUnlock() - return g.id -} - -// GetName returns the guild name -func (g *Guild) GetName() string { - g.mu.RLock() - defer g.mu.RUnlock() - return g.name -} - -// GetLevel returns the guild level -func (g *Guild) GetLevel() int8 { - g.mu.RLock() - defer g.mu.RUnlock() - return g.level -} - -// GetFormedDate returns the guild formation date -func (g *Guild) GetFormedDate() time.Time { - g.mu.RLock() - defer g.mu.RUnlock() - return g.formedDate -} - -// GetMOTD returns the guild message of the day -func (g *Guild) GetMOTD() string { - g.mu.RLock() - defer g.mu.RUnlock() - return g.motd -} - -// SetEXPCurrent sets the current guild experience -func (g *Guild) SetEXPCurrent(exp int64, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - g.expCurrent = exp - g.lastModified = time.Now() - - if sendPacket { - g.saveNeeded = true - } -} - -// AddEXPCurrent adds experience to the guild -func (g *Guild) AddEXPCurrent(exp int64, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - g.expCurrent += exp - g.lastModified = time.Now() - - if sendPacket { - g.saveNeeded = true - } -} - -// GetEXPCurrent returns the current guild experience -func (g *Guild) GetEXPCurrent() int64 { - g.mu.RLock() - defer g.mu.RUnlock() - return g.expCurrent -} - -// SetEXPToNextLevel sets the experience needed for next level -func (g *Guild) SetEXPToNextLevel(exp int64, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - g.expToNextLevel = exp - g.lastModified = time.Now() - - if sendPacket { - g.saveNeeded = true - } -} - -// GetEXPToNextLevel returns the experience needed for next level -func (g *Guild) GetEXPToNextLevel() int64 { - g.mu.RLock() - defer g.mu.RUnlock() - return g.expToNextLevel -} - -// SetRecruitingShortDesc sets the short recruiting description -func (g *Guild) SetRecruitingShortDesc(desc string, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - g.recruitingShortDesc = desc - g.lastModified = time.Now() - - if sendPacket { - g.recruitingSaveNeeded = true - } -} - -// GetRecruitingShortDesc returns the short recruiting description -func (g *Guild) GetRecruitingShortDesc() string { - g.mu.RLock() - defer g.mu.RUnlock() - return g.recruitingShortDesc -} - -// SetRecruitingFullDesc sets the full recruiting description -func (g *Guild) SetRecruitingFullDesc(desc string, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - if len(desc) > MaxRecruitingDescLength { - desc = desc[:MaxRecruitingDescLength] - } - g.recruitingFullDesc = desc - g.lastModified = time.Now() - - if sendPacket { - g.recruitingSaveNeeded = true - } -} - -// GetRecruitingFullDesc returns the full recruiting description -func (g *Guild) GetRecruitingFullDesc() string { - g.mu.RLock() - defer g.mu.RUnlock() - return g.recruitingFullDesc -} - -// SetRecruitingMinLevel sets the minimum level for recruiting -func (g *Guild) SetRecruitingMinLevel(level int8, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - g.recruitingMinLevel = level - g.lastModified = time.Now() - - if sendPacket { - g.recruitingSaveNeeded = true - } -} - -// GetRecruitingMinLevel returns the minimum level for recruiting -func (g *Guild) GetRecruitingMinLevel() int8 { - g.mu.RLock() - defer g.mu.RUnlock() - return g.recruitingMinLevel -} - -// SetRecruitingPlayStyle sets the recruiting play style -func (g *Guild) SetRecruitingPlayStyle(playStyle int8, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - - g.recruitingPlayStyle = playStyle - g.lastModified = time.Now() - - if sendPacket { - g.recruitingSaveNeeded = true - } -} - -// GetRecruitingPlayStyle returns the recruiting play style -func (g *Guild) GetRecruitingPlayStyle() int8 { - g.mu.RLock() - defer g.mu.RUnlock() - return g.recruitingPlayStyle -} - -// SetRecruitingDescTag sets a recruiting description tag -func (g *Guild) SetRecruitingDescTag(index, tag int8, sendPacket bool) bool { - g.mu.Lock() - defer g.mu.Unlock() - - if index < 0 || index > 3 { - return false - } - - g.recruitingDescTags[index] = tag - g.lastModified = time.Now() - - if sendPacket { - g.recruitingSaveNeeded = true - } - - return true -} - -// GetRecruitingDescTag returns a recruiting description tag -func (g *Guild) GetRecruitingDescTag(index int8) int8 { - g.mu.RLock() - defer g.mu.RUnlock() - - if tag, exists := g.recruitingDescTags[index]; exists { - return tag - } - return RecruitingDescTagNone -} - -// SetPermission sets a guild permission for a rank -func (g *Guild) SetPermission(rank, permission, value int8, sendPacket, saveNeeded bool) bool { - g.mu.Lock() - defer g.mu.Unlock() - - if rank < RankLeader || rank > RankRecruit { - return false - } - - 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.mu.RLock() - defer g.mu.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) -} - -// SetEventFilter sets an event filter -func (g *Guild) SetEventFilter(eventID, category, value int8, sendPacket, saveNeeded bool) bool { - g.mu.Lock() - defer g.mu.Unlock() - - if g.eventFilters[eventID] == nil { - g.eventFilters[eventID] = make(map[int8]int8) - } - - g.eventFilters[eventID][category] = value - g.lastModified = time.Now() - - if saveNeeded { - g.eventFiltersSaveNeeded = true - } - - return true -} - -// GetEventFilter returns an event filter -func (g *Guild) GetEventFilter(eventID, category int8) int8 { - g.mu.RLock() - defer g.mu.RUnlock() - - if eventFilters, exists := g.eventFilters[eventID]; exists { - if value, exists := eventFilters[category]; exists { - return value - } - } - - return 1 // Default to enabled -} - -// GetNumUniqueAccounts returns the number of unique accounts in the guild -func (g *Guild) GetNumUniqueAccounts() int32 { - g.mu.RLock() - defer g.mu.RUnlock() - - accounts := make(map[int32]bool) - for _, member := range g.members { - accounts[member.AccountID] = true - } - - return int32(len(accounts)) -} - -// GetNumRecruiters returns the number of recruiters in the guild -func (g *Guild) GetNumRecruiters() int32 { - g.mu.RLock() - defer g.mu.RUnlock() - - count := int32(0) - for _, member := range g.members { - if member.MemberFlags&MemberFlagRecruitingForGuild != 0 { - count++ - } - } - - return count -} - -// GetNextRecruiterID returns the next available recruiter ID -func (g *Guild) GetNextRecruiterID() int32 { - g.mu.RLock() - defer g.mu.RUnlock() - - maxID := int32(0) - for _, member := range g.members { - if member.RecruiterID > maxID { - maxID = member.RecruiterID - } - } - - return maxID + 1 -} - -// GetNextEventID returns the next available event ID -func (g *Guild) GetNextEventID() int64 { - g.mu.Lock() - defer g.mu.Unlock() - - eventID := g.nextEventID - g.nextEventID++ - return eventID -} - -// GetGuildMember returns a guild member by character ID -func (g *Guild) GetGuildMember(characterID int32) *GuildMember { - g.mu.RLock() - defer g.mu.RUnlock() - - return g.members[characterID] -} - -// GetGuildMemberByName returns a guild member by name -func (g *Guild) GetGuildMemberByName(playerName string) *GuildMember { - g.mu.RLock() - defer g.mu.RUnlock() - - for _, member := range g.members { - if strings.EqualFold(member.Name, playerName) { - return member - } - } - - return nil -} - -// GetGuildRecruiters returns all guild recruiters -func (g *Guild) GetGuildRecruiters() []*GuildMember { - g.mu.RLock() - defer g.mu.RUnlock() - - var recruiters []*GuildMember - for _, member := range g.members { - if member.MemberFlags&MemberFlagRecruitingForGuild != 0 { - recruiters = append(recruiters, member) - } - } - - return recruiters -} - -// GetGuildEvent returns a guild event by ID -func (g *Guild) GetGuildEvent(eventID int64) *GuildEvent { - g.mu.RLock() - defer g.mu.RUnlock() - - for i := range g.guildEvents { - if g.guildEvents[i].EventID == eventID { - return &g.guildEvents[i] - } - } - - return nil -} - -// SetRankName sets a custom rank name -func (g *Guild) SetRankName(rank int8, name string, sendPacket bool) bool { - g.mu.Lock() - defer g.mu.Unlock() - - if rank < RankLeader || rank > RankRecruit { - return false - } - - g.ranks[rank] = name - g.lastModified = time.Now() - - if sendPacket { - g.ranksSaveNeeded = true - } - - return true -} - -// GetRankName returns the name for a rank -func (g *Guild) GetRankName(rank int8) string { - g.mu.RLock() - defer g.mu.RUnlock() - - if name, exists := g.ranks[rank]; exists { - return name - } - - // Return default rank name - if defaultName, exists := DefaultRankNames[rank]; exists { - return defaultName - } - - return "Unknown" -} - -// SetRecruitingFlag sets a recruiting flag -func (g *Guild) SetRecruitingFlag(flag, value int8, sendPacket bool) bool { - g.mu.Lock() - defer g.mu.Unlock() - - if flag < RecruitingFlagTraining || flag > RecruitingFlagTradeskillers { - return false - } - - g.recruitingFlags[flag] = value - g.lastModified = time.Now() - - if sendPacket { - g.recruitingSaveNeeded = true - } - - return true -} - -// GetRecruitingFlag returns a recruiting flag -func (g *Guild) GetRecruitingFlag(flag int8) int8 { - g.mu.RLock() - defer g.mu.RUnlock() - - if value, exists := g.recruitingFlags[flag]; exists { - return value - } - - return 0 -} - -// AddNewGuildMember adds a new member to the guild -func (g *Guild) AddNewGuildMember(characterID int32, invitedBy string, joinDate time.Time, rank int8) bool { - g.mu.Lock() - defer g.mu.Unlock() - - // Check if member already exists - if _, exists := g.members[characterID]; exists { - return false - } - - member := &GuildMember{ - CharacterID: characterID, - Rank: rank, - JoinDate: joinDate, - PointHistory: make([]PointHistory, 0), - } - - 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.mu.Lock() - defer g.mu.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() - } -} - -// PromoteGuildMember promotes a guild member -func (g *Guild) PromoteGuildMember(characterID int32, promoterName string, sendPacket bool) bool { - g.mu.Lock() - defer g.mu.Unlock() - - member, exists := g.members[characterID] - if !exists || member.Rank <= RankLeader { - return false - } - - oldRank := member.Rank - member.Rank-- - 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.mu.Lock() - defer g.mu.Unlock() - - member, exists := g.members[characterID] - if !exists || member.Rank >= RankRecruit { - return false - } - - oldRank := member.Rank - member.Rank++ - 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 -} - -// AddPointsToGuildMember adds points to a specific guild member -func (g *Guild) AddPointsToGuildMember(characterID int32, points float64, modifiedBy, comment string, sendPacket bool) bool { - g.mu.Lock() - defer g.mu.Unlock() - - member, exists := g.members[characterID] - if !exists { - return false - } - - member.Points += points - - // Add to point history - if len(member.PointHistory) >= MaxPointHistory { - // Remove oldest entry - member.PointHistory = member.PointHistory[1:] - } - - member.PointHistory = append(member.PointHistory, PointHistory{ - Date: time.Now(), - ModifiedBy: modifiedBy, - Comment: comment, - Points: points, - SaveNeeded: true, - }) - - g.pointsHistorySaveNeeded = true - g.lastModified = time.Now() - - return true -} - -// AddNewGuildEvent adds a new event to the guild -func (g *Guild) AddNewGuildEvent(eventType int32, description string, date time.Time, sendPacket bool) { - g.mu.Lock() - defer g.mu.Unlock() - g.addNewGuildEventNoLock(eventType, description, date, sendPacket) -} - -// addNewGuildEventNoLock is the internal implementation without locking -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() -} - -// GetGuildInfo returns basic guild information -func (g *Guild) GetGuildInfo() GuildInfo { - g.mu.RLock() - defer g.mu.RUnlock() - - return GuildInfo{ - ID: g.id, - Name: g.name, - Level: g.level, - FormedDate: g.formedDate, - MOTD: g.motd, - MemberCount: len(g.members), - RecruiterCount: int(g.getNumRecruitersNoLock()), - RecruitingShortDesc: g.recruitingShortDesc, - RecruitingFullDesc: g.recruitingFullDesc, - RecruitingMinLevel: g.recruitingMinLevel, - RecruitingPlayStyle: g.recruitingPlayStyle, - IsRecruiting: g.getNumRecruitersNoLock() > 0, - } -} - -// GetAllMembers returns all guild members -func (g *Guild) GetAllMembers() []*GuildMember { - g.mu.RLock() - defer g.mu.RUnlock() - - members := make([]*GuildMember, 0, len(g.members)) - for _, member := range g.members { - members = append(members, member) - } - - return members -} - -// Save flag methods -func (g *Guild) SetSaveNeeded(val bool) { - g.mu.Lock() - defer g.mu.Unlock() - g.saveNeeded = val -} - -func (g *Guild) GetSaveNeeded() bool { - g.mu.RLock() - defer g.mu.RUnlock() - return g.saveNeeded -} - -// 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) getNumRecruitersNoLock() int32 { - count := int32(0) - for _, member := range g.members { - if member.MemberFlags&MemberFlagRecruitingForGuild != 0 { - count++ - } - } - return count -} - -// Database operations - -func (g *Guild) create(ctx context.Context) error { - // Use MySQL-compatible approach for both databases - result, err := g.db.Exec(`INSERT INTO guilds (name, motd, level, xp, xp_needed, formed_on) VALUES (?, ?, ?, ?, ?, ?)`, - g.name, g.motd, g.level, g.expCurrent, g.expToNextLevel, g.formedDate.Unix()) - if err != nil { - return err - } - - id, err := result.LastInsertId() - if err != nil { - return err - } - - g.id = int32(id) - g.isNew = false - return nil -} - -func (g *Guild) update(ctx context.Context) error { - // Use MySQL-compatible approach for both databases - _, err := g.db.Exec(`UPDATE guilds SET name = ?, motd = ?, level = ?, xp = ?, xp_needed = ? WHERE id = ?`, - g.name, g.motd, g.level, g.expCurrent, g.expToNextLevel, g.id) - return err -} - -func (g *Guild) delete(ctx context.Context) error { - // Use MySQL-compatible approach for both databases - _, err := g.db.Exec(`DELETE FROM guilds WHERE id = ?`, g.id) - return err -} - -func (g *Guild) load(ctx context.Context) error { - // Use MySQL-compatible approach for both databases - row := g.db.QueryRow(`SELECT name, motd, level, xp, xp_needed, formed_on FROM guilds WHERE id = ?`, g.id) - var formedUnix int64 - err := row.Scan(&g.name, &g.motd, &g.level, &g.expCurrent, &g.expToNextLevel, &formedUnix) - if err != nil { - return fmt.Errorf("guild %d not found: %w", g.id, err) - } - - g.formedDate = time.Unix(formedUnix, 0) - return g.loadMembers(ctx) -} - -func (g *Guild) loadMembers(ctx context.Context) error { - g.members = make(map[int32]*GuildMember) - - // Use MySQL-compatible approach for both databases - rows, err := g.db.Query(`SELECT char_id, name, rank, points, adventure_class, adventure_level, - tradeskill_class, tradeskill_level, join_date, last_login_date FROM guild_members WHERE guild_id = ?`, g.id) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - member := &GuildMember{} - var joinUnix, lastLoginUnix int64 - err := rows.Scan(&member.CharacterID, &member.Name, &member.Rank, &member.Points, - &member.AdventureClass, &member.AdventureLevel, &member.TradeskillClass, &member.TradeskillLevel, - &joinUnix, &lastLoginUnix) - if err != nil { - return err - } - member.JoinDate = time.Unix(joinUnix, 0) - member.LastLoginDate = time.Unix(lastLoginUnix, 0) - g.members[member.CharacterID] = member - } - return rows.Err() -} diff --git a/internal/guilds/guilds.go b/internal/guilds/guilds.go new file mode 100644 index 0000000..d2de058 --- /dev/null +++ b/internal/guilds/guilds.go @@ -0,0 +1,1305 @@ +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) {} \ No newline at end of file diff --git a/internal/guilds/interfaces.go b/internal/guilds/interfaces.go deleted file mode 100644 index b446e5a..0000000 --- a/internal/guilds/interfaces.go +++ /dev/null @@ -1,345 +0,0 @@ -package guilds - -import ( - "context" - "time" -) - -// GuildDatabase defines database operations for guilds -type GuildDatabase interface { - // LoadGuilds retrieves all guilds from database - LoadGuilds(ctx context.Context) ([]GuildData, error) - - // LoadGuild retrieves a specific guild from database - LoadGuild(ctx context.Context, guildID int32) (*GuildData, error) - - // LoadGuildMembers retrieves all members for a guild - LoadGuildMembers(ctx context.Context, guildID int32) ([]GuildMemberData, error) - - // LoadGuildEvents retrieves events for a guild - LoadGuildEvents(ctx context.Context, guildID int32) ([]GuildEventData, error) - - // LoadGuildRanks retrieves custom rank names for a guild - LoadGuildRanks(ctx context.Context, guildID int32) ([]GuildRankData, error) - - // LoadGuildPermissions retrieves permissions for a guild - LoadGuildPermissions(ctx context.Context, guildID int32) ([]GuildPermissionData, error) - - // LoadGuildEventFilters retrieves event filters for a guild - LoadGuildEventFilters(ctx context.Context, guildID int32) ([]GuildEventFilterData, error) - - // LoadGuildRecruiting retrieves recruiting settings for a guild - LoadGuildRecruiting(ctx context.Context, guildID int32) ([]GuildRecruitingData, error) - - // LoadPointHistory retrieves point history for a member - LoadPointHistory(ctx context.Context, characterID int32) ([]PointHistoryData, error) - - // SaveGuild saves guild basic data - SaveGuild(ctx context.Context, guild *Guild) error - - // SaveGuildMembers saves all guild members - SaveGuildMembers(ctx context.Context, guildID int32, members []*GuildMember) error - - // SaveGuildEvents saves guild events - SaveGuildEvents(ctx context.Context, guildID int32, events []GuildEvent) error - - // SaveGuildRanks saves guild rank names - SaveGuildRanks(ctx context.Context, guildID int32, ranks map[int8]string) error - - // SaveGuildPermissions saves guild permissions - SaveGuildPermissions(ctx context.Context, guildID int32, permissions map[int8]map[int8]int8) error - - // SaveGuildEventFilters saves guild event filters - SaveGuildEventFilters(ctx context.Context, guildID int32, filters map[int8]map[int8]int8) error - - // SaveGuildRecruiting saves guild recruiting settings - SaveGuildRecruiting(ctx context.Context, guildID int32, flags, descTags map[int8]int8) error - - // SavePointHistory saves point history for a member - SavePointHistory(ctx context.Context, characterID int32, history []PointHistory) error - - // GetGuildIDByCharacterID returns guild ID for a character - GetGuildIDByCharacterID(ctx context.Context, characterID int32) (int32, error) - - // CreateGuild creates a new guild - CreateGuild(ctx context.Context, guildData GuildData) (int32, error) - - // DeleteGuild removes a guild and all related data - DeleteGuild(ctx context.Context, guildID int32) error - - // GetNextGuildID returns the next available guild ID - GetNextGuildID(ctx context.Context) (int32, error) - - // GetNextEventID returns the next available event ID for a guild - GetNextEventID(ctx context.Context, guildID int32) (int64, error) -} - -// ClientManager handles client communication for guilds -type ClientManager interface { - // SendGuildUpdate sends guild information update to client - SendGuildUpdate(characterID int32, guild *Guild) error - - // SendGuildMemberList sends guild member list to client - SendGuildMemberList(characterID int32, members []MemberInfo) error - - // SendGuildMember sends single member info to client - SendGuildMember(characterID int32, member *GuildMember) error - - // SendGuildMOTD sends message of the day to client - SendGuildMOTD(characterID int32, motd string) error - - // SendGuildEvent sends guild event to client - SendGuildEvent(characterID int32, event *GuildEvent) error - - // SendGuildEventList sends guild event list to client - SendGuildEventList(characterID int32, events []GuildEventInfo) error - - // SendGuildChatMessage sends guild chat message to client - SendGuildChatMessage(characterID int32, senderName, message string, language int8) error - - // SendOfficerChatMessage sends officer chat message to client - SendOfficerChatMessage(characterID int32, senderName, message string, language int8) error - - // SendGuildInvite sends guild invitation to client - SendGuildInvite(characterID int32, invite GuildInvite) error - - // SendGuildRecruitingInfo sends recruiting information to client - SendGuildRecruitingInfo(characterID int32, info RecruitingInfo) error - - // SendGuildPermissions sends guild permissions to client - SendGuildPermissions(characterID int32, permissions map[int8]map[int8]int8) error - - // IsClientOnline checks if a character is currently online - IsClientOnline(characterID int32) bool - - // GetClientLanguage returns the language setting for a client - GetClientLanguage(characterID int32) int8 -} - -// PlayerManager provides player information for guilds -type PlayerManager interface { - // GetPlayerInfo retrieves basic player information - GetPlayerInfo(characterID int32) (PlayerInfo, error) - - // IsPlayerOnline checks if a player is currently online - IsPlayerOnline(characterID int32) bool - - // GetPlayerZone returns the current zone for a player - GetPlayerZone(characterID int32) string - - // GetPlayerLevel returns player's current level - GetPlayerLevel(characterID int32) (int8, int8) // adventure, tradeskill - - // GetPlayerClass returns player's current class - GetPlayerClass(characterID int32) (int8, int8) // adventure, tradeskill - - // GetPlayerName returns player's character name - GetPlayerName(characterID int32) string - - // ValidatePlayerExists checks if a player exists - ValidatePlayerExists(characterName string) (int32, error) - - // GetAccountID returns the account ID for a character - GetAccountID(characterID int32) int32 -} - -// GuildEventHandler handles guild-related events -type GuildEventHandler interface { - // OnGuildCreated called when a guild is created - OnGuildCreated(guild *Guild) - - // OnGuildDeleted called when a guild is deleted - OnGuildDeleted(guildID int32, guildName string) - - // OnMemberJoined called when a member joins a guild - OnMemberJoined(guild *Guild, member *GuildMember, inviterName string) - - // OnMemberLeft called when a member leaves a guild - OnMemberLeft(guild *Guild, member *GuildMember, reason string) - - // OnMemberPromoted called when a member is promoted - OnMemberPromoted(guild *Guild, member *GuildMember, oldRank, newRank int8, promoterName string) - - // OnMemberDemoted called when a member is demoted - OnMemberDemoted(guild *Guild, member *GuildMember, oldRank, newRank int8, demoterName string) - - // OnPointsAwarded called when points are awarded to members - OnPointsAwarded(guild *Guild, members []int32, points float64, comment, awardedBy string) - - // OnGuildEvent called when a guild event occurs - OnGuildEvent(guild *Guild, event *GuildEvent) - - // OnGuildLevelUp called when a guild levels up - OnGuildLevelUp(guild *Guild, oldLevel, newLevel int8) - - // OnGuildChatMessage called when a guild chat message is sent - OnGuildChatMessage(guild *Guild, senderID int32, senderName, message string, language int8) - - // OnOfficerChatMessage called when an officer chat message is sent - OnOfficerChatMessage(guild *Guild, senderID int32, senderName, message string, language int8) -} - -// LogHandler provides logging functionality -type LogHandler interface { - // LogDebug logs debug messages - LogDebug(category, message string, args ...any) - - // LogInfo logs informational messages - LogInfo(category, message string, args ...any) - - // LogError logs error messages - LogError(category, message string, args ...any) - - // LogWarning logs warning messages - LogWarning(category, message string, args ...any) -} - -// PlayerInfo contains basic player information -type PlayerInfo struct { - CharacterID int32 `json:"character_id"` - CharacterName string `json:"character_name"` - AccountID int32 `json:"account_id"` - AdventureLevel int8 `json:"adventure_level"` - AdventureClass int8 `json:"adventure_class"` - TradeskillLevel int8 `json:"tradeskill_level"` - TradeskillClass int8 `json:"tradeskill_class"` - Zone string `json:"zone"` - IsOnline bool `json:"is_online"` - LastLogin time.Time `json:"last_login"` -} - -// GuildAware interface for entities that can participate in guilds -type GuildAware interface { - GetCharacterID() int32 - GetGuildID() int32 - GetGuildRank() int8 - IsInGuild() bool - HasGuildPermission(permission int8) bool -} - -// EntityGuildAdapter adapts entities to work with guild system -type EntityGuildAdapter struct { - entity interface { - GetID() int32 - // Add other entity methods as needed - } - guildManager *GuildManager -} - -// GetCharacterID returns the character ID from the adapted entity -func (a *EntityGuildAdapter) GetCharacterID() int32 { - return a.entity.GetID() -} - -// GetGuildID returns the guild ID for the character -func (a *EntityGuildAdapter) GetGuildID() int32 { - // TODO: Implement guild lookup through guild manager - return 0 -} - -// GetGuildRank returns the guild rank for the character -func (a *EntityGuildAdapter) GetGuildRank() int8 { - // TODO: Implement rank lookup through guild manager - return RankRecruit -} - -// IsInGuild checks if the character is in a guild -func (a *EntityGuildAdapter) IsInGuild() bool { - return a.GetGuildID() > 0 -} - -// HasGuildPermission checks if the character has a specific guild permission -func (a *EntityGuildAdapter) HasGuildPermission(permission int8) bool { - // TODO: Implement permission checking through guild manager - return false -} - -// InviteManager handles guild invitations -type InviteManager interface { - // SendInvite sends a guild invitation - SendInvite(guildID, characterID, inviterID int32, rank int8) error - - // AcceptInvite accepts a guild invitation - AcceptInvite(characterID, guildID int32) error - - // DeclineInvite declines a guild invitation - DeclineInvite(characterID, guildID int32) error - - // GetPendingInvites returns pending invites for a character - GetPendingInvites(characterID int32) ([]GuildInvite, error) - - // ClearExpiredInvites removes expired invitations - ClearExpiredInvites() error -} - -// PermissionChecker provides permission validation -type PermissionChecker interface { - // CanInvite checks if a member can invite players - CanInvite(guild *Guild, memberRank int8) bool - - // CanRemoveMember checks if a member can remove other members - CanRemoveMember(guild *Guild, memberRank, targetRank int8) bool - - // CanPromote checks if a member can promote other members - CanPromote(guild *Guild, memberRank, targetRank int8) bool - - // CanDemote checks if a member can demote other members - CanDemote(guild *Guild, memberRank, targetRank int8) bool - - // CanEditPermissions checks if a member can edit guild permissions - CanEditPermissions(guild *Guild, memberRank int8) bool - - // CanUseBankSlot checks if a member can access a specific bank slot - CanUseBankSlot(guild *Guild, memberRank int8, bankSlot int, action string) bool - - // CanSpeakInOfficerChat checks if a member can speak in officer chat - CanSpeakInOfficerChat(guild *Guild, memberRank int8) bool - - // CanAssignPoints checks if a member can assign guild points - CanAssignPoints(guild *Guild, memberRank int8) bool -} - -// NotificationManager handles guild notifications -type NotificationManager interface { - // NotifyMemberLogin notifies guild of member login - NotifyMemberLogin(guild *Guild, member *GuildMember) - - // NotifyMemberLogout notifies guild of member logout - NotifyMemberLogout(guild *Guild, member *GuildMember) - - // NotifyGuildMessage sends a message to all guild members - NotifyGuildMessage(guild *Guild, eventType int8, message string, args ...any) - - // NotifyOfficers sends a message to officers only - NotifyOfficers(guild *Guild, message string, args ...any) - - // NotifyGuildUpdate notifies guild members of guild changes - NotifyGuildUpdate(guild *Guild) -} - -// BankManager handles guild bank operations -type BankManager interface { - // GetBankContents returns contents of a guild bank - GetBankContents(guildID int32, bankSlot int) ([]BankItem, error) - - // DepositItem deposits an item into guild bank - DepositItem(guildID int32, bankSlot int, item BankItem, depositorID int32) error - - // WithdrawItem withdraws an item from guild bank - WithdrawItem(guildID int32, bankSlot int, itemSlot int, withdrawerID int32) error - - // LogBankEvent logs a bank event - LogBankEvent(guildID int32, bankSlot int, eventType int32, description string) error - - // GetBankEventHistory returns bank event history - GetBankEventHistory(guildID int32, bankSlot int) ([]GuildBankEvent, error) -} - -// BankItem represents an item in the guild bank -type BankItem struct { - Slot int `json:"slot"` - ItemID int32 `json:"item_id"` - Quantity int32 `json:"quantity"` - DepositorID int32 `json:"depositor_id"` - DepositDate time.Time `json:"deposit_date"` -} diff --git a/internal/guilds/master.go b/internal/guilds/master.go deleted file mode 100644 index e6aa928..0000000 --- a/internal/guilds/master.go +++ /dev/null @@ -1,514 +0,0 @@ -package guilds - -import ( - "sort" - "strings" - "sync" - - "eq2emu/internal/database" -) - -// MasterList provides a bespoke, performance-optimized guild collection with specialized indices -type MasterList struct { - // Core storage - guilds map[int32]*Guild - mutex sync.RWMutex - - // Specialized indices for O(1) lookups - byName map[string]*Guild // name -> guild (case-insensitive) - byLevel map[int8][]*Guild // level -> guilds - recruiting []*Guild // guilds with recruiters - - // Cached metadata with invalidation - levels []int8 - recruitingMap map[int32]bool // guildID -> isRecruiting - metaStale bool -} - -// NewMasterList creates a new bespoke master list optimized for guild operations -func NewMasterList() *MasterList { - return &MasterList{ - guilds: make(map[int32]*Guild), - byName: make(map[string]*Guild), - byLevel: make(map[int8][]*Guild), - recruiting: make([]*Guild, 0), - recruitingMap: make(map[int32]bool), - } -} - -// Add adds a guild to the master list with O(1) core operations -func (m *MasterList) Add(guild *Guild) bool { - m.mutex.Lock() - defer m.mutex.Unlock() - - guildID := guild.GetID() - - // Check existence - if _, exists := m.guilds[guildID]; exists { - return false - } - - // Add to core storage - m.guilds[guildID] = guild - - // Update specialized indices - name := strings.ToLower(guild.GetName()) - m.byName[name] = guild - - level := guild.GetLevel() - m.byLevel[level] = append(m.byLevel[level], guild) - - // Update recruiting index - isRecruiting := guild.GetNumRecruiters() > 0 - m.recruitingMap[guildID] = isRecruiting - if isRecruiting { - m.recruiting = append(m.recruiting, guild) - } - - // Invalidate metadata cache - m.metaStale = true - - return true -} - -// Get retrieves a guild by ID with O(1) performance -func (m *MasterList) Get(guildID int32) *Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - return m.guilds[guildID] -} - -// GetByName retrieves a guild by name with O(1) performance -func (m *MasterList) GetByName(name string) *Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - return m.byName[strings.ToLower(name)] -} - -// GetByLevel returns all guilds of a specific level with O(1) performance -func (m *MasterList) GetByLevel(level int8) []*Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - - guilds := m.byLevel[level] - if guilds == nil { - return nil - } - - // Return a copy to prevent external modification - result := make([]*Guild, len(guilds)) - copy(result, guilds) - return result -} - -// GetRecruiting returns all guilds that are recruiting with O(1) performance -func (m *MasterList) GetRecruiting() []*Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - - // Return a copy to prevent external modification - result := make([]*Guild, len(m.recruiting)) - copy(result, m.recruiting) - return result -} - -// GetByLevelRange returns guilds within a level range using optimized range queries -func (m *MasterList) GetByLevelRange(minLevel, maxLevel int8) []*Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - - var result []*Guild - for level := minLevel; level <= maxLevel; level++ { - if guilds := m.byLevel[level]; guilds != nil { - result = append(result, guilds...) - } - } - - return result -} - -// GetRecruitingByLevel returns recruiting guilds of a specific level using set intersection -func (m *MasterList) GetRecruitingByLevel(level int8) []*Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - - levelGuilds := m.byLevel[level] - if levelGuilds == nil { - return nil - } - - var result []*Guild - for _, guild := range levelGuilds { - if m.recruitingMap[guild.GetID()] { - result = append(result, guild) - } - } - - return result -} - -// Remove removes a guild from the master list -func (m *MasterList) Remove(guildID int32) bool { - m.mutex.Lock() - defer m.mutex.Unlock() - - guild := m.guilds[guildID] - if guild == nil { - return false - } - - // Remove from core storage - delete(m.guilds, guildID) - - // Remove from name index - name := strings.ToLower(guild.GetName()) - delete(m.byName, name) - - // Remove from level index - level := guild.GetLevel() - if levelGuilds := m.byLevel[level]; levelGuilds != nil { - for i, g := range levelGuilds { - if g.GetID() == guildID { - m.byLevel[level] = append(levelGuilds[:i], levelGuilds[i+1:]...) - break - } - } - // Clean up empty level arrays - if len(m.byLevel[level]) == 0 { - delete(m.byLevel, level) - } - } - - // Remove from recruiting index - delete(m.recruitingMap, guildID) - for i, g := range m.recruiting { - if g.GetID() == guildID { - m.recruiting = append(m.recruiting[:i], m.recruiting[i+1:]...) - break - } - } - - // Invalidate metadata cache - m.metaStale = true - - return true -} - -// Update updates a guild's indices when its properties change -func (m *MasterList) Update(guild *Guild) { - m.mutex.Lock() - defer m.mutex.Unlock() - - guildID := guild.GetID() - oldGuild := m.guilds[guildID] - if oldGuild == nil { - return - } - - // Update core storage - m.guilds[guildID] = guild - - // Update name index if changed - oldName := strings.ToLower(oldGuild.GetName()) - newName := strings.ToLower(guild.GetName()) - if oldName != newName { - delete(m.byName, oldName) - m.byName[newName] = guild - } - - // Update level index if changed - oldLevel := oldGuild.GetLevel() - newLevel := guild.GetLevel() - if oldLevel != newLevel { - // Remove from old level - if levelGuilds := m.byLevel[oldLevel]; levelGuilds != nil { - for i, g := range levelGuilds { - if g.GetID() == guildID { - m.byLevel[oldLevel] = append(levelGuilds[:i], levelGuilds[i+1:]...) - break - } - } - if len(m.byLevel[oldLevel]) == 0 { - delete(m.byLevel, oldLevel) - } - } - - // Add to new level - m.byLevel[newLevel] = append(m.byLevel[newLevel], guild) - } - - // Update recruiting index if changed - oldRecruiting := m.recruitingMap[guildID] - newRecruiting := guild.GetNumRecruiters() > 0 - if oldRecruiting != newRecruiting { - m.recruitingMap[guildID] = newRecruiting - - if oldRecruiting && !newRecruiting { - // Remove from recruiting list - for i, g := range m.recruiting { - if g.GetID() == guildID { - m.recruiting = append(m.recruiting[:i], m.recruiting[i+1:]...) - break - } - } - } else if !oldRecruiting && newRecruiting { - // Add to recruiting list - m.recruiting = append(m.recruiting, guild) - } - } - - // Invalidate metadata cache - m.metaStale = true -} - -// GetAll returns all guilds -func (m *MasterList) GetAll() []*Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - - result := make([]*Guild, 0, len(m.guilds)) - for _, guild := range m.guilds { - result = append(result, guild) - } - - return result -} - -// Count returns the total number of guilds -func (m *MasterList) Count() int { - m.mutex.RLock() - defer m.mutex.RUnlock() - return len(m.guilds) -} - -// GetLevels returns all guild levels with lazy caching -func (m *MasterList) GetLevels() []int8 { - m.mutex.Lock() - defer m.mutex.Unlock() - - if !m.metaStale && m.levels != nil { - return m.levels - } - - // Rebuild level cache - levelSet := make(map[int8]bool) - for level := range m.byLevel { - levelSet[level] = true - } - - m.levels = make([]int8, 0, len(levelSet)) - for level := range levelSet { - m.levels = append(m.levels, level) - } - - sort.Slice(m.levels, func(i, j int) bool { - return m.levels[i] < m.levels[j] - }) - - m.metaStale = false - return m.levels -} - -// GetStatistics returns detailed statistics about the guild system -func (m *MasterList) GetStatistics() GuildStatistics { - m.mutex.RLock() - defer m.mutex.RUnlock() - - stats := GuildStatistics{ - TotalGuilds: len(m.guilds), - } - - totalMembers := 0 - activeGuilds := 0 - totalRecruiters := len(m.recruiting) - uniqueAccounts := make(map[int32]bool) - highestLevel := int8(1) - - for _, guild := range m.guilds { - members := guild.GetAllMembers() - memberCount := len(members) - - totalMembers += memberCount - - if memberCount > 0 { - activeGuilds++ - } - - // Track unique accounts - for _, member := range members { - uniqueAccounts[member.AccountID] = true - } - - // Find highest level - if guild.GetLevel() > highestLevel { - highestLevel = guild.GetLevel() - } - } - - stats.TotalMembers = totalMembers - stats.ActiveGuilds = activeGuilds - stats.TotalRecruiters = totalRecruiters - stats.UniqueAccounts = len(uniqueAccounts) - stats.HighestGuildLevel = highestLevel - - if len(m.guilds) > 0 { - stats.AverageGuildSize = float64(totalMembers) / float64(len(m.guilds)) - } - - return stats -} - -// Search performs efficient guild searches using specialized indices -func (m *MasterList) Search(criteria GuildSearchCriteria) []*Guild { - m.mutex.RLock() - defer m.mutex.RUnlock() - - // Start with all guilds or use specialized indices for optimization - var candidates []*Guild - - // Use level range optimization if specified - if criteria.MinLevel > 0 || criteria.MaxLevel > 0 { - minLevel := criteria.MinLevel - maxLevel := criteria.MaxLevel - - if minLevel == 0 { - minLevel = 1 - } - if maxLevel == 0 { - maxLevel = 100 // reasonable max - } - - for level := minLevel; level <= maxLevel; level++ { - if guilds := m.byLevel[level]; guilds != nil { - candidates = append(candidates, guilds...) - } - } - } else if criteria.RecruitingOnly { - // Use recruiting index for optimization - candidates = make([]*Guild, len(m.recruiting)) - copy(candidates, m.recruiting) - } else { - // Use all guilds - candidates = make([]*Guild, 0, len(m.guilds)) - for _, guild := range m.guilds { - candidates = append(candidates, guild) - } - } - - // Apply remaining filters - var results []*Guild - for _, guild := range candidates { - if m.matchesCriteria(guild, criteria) { - results = append(results, guild) - } - } - - // Sort by name - sort.Slice(results, func(i, j int) bool { - return results[i].GetName() < results[j].GetName() - }) - - return results -} - -// LoadFromDatabase loads all guilds from database into the master list -func (m *MasterList) LoadFromDatabase(db *database.Database) error { - // Clear existing data - m.mutex.Lock() - m.guilds = make(map[int32]*Guild) - m.byName = make(map[string]*Guild) - m.byLevel = make(map[int8][]*Guild) - m.recruiting = make([]*Guild, 0) - m.recruitingMap = make(map[int32]bool) - m.metaStale = true - m.mutex.Unlock() - - // Load guild IDs first using MySQL-compatible approach - var guildIDs []int32 - - rows, err := db.Query(`SELECT id FROM guilds ORDER BY id`) - if err != nil { - return err - } - defer rows.Close() - - for rows.Next() { - var id int32 - if err := rows.Scan(&id); err != nil { - return err - } - guildIDs = append(guildIDs, id) - } - - if err := rows.Err(); err != nil { - return err - } - - // Load each guild and add to master list - for _, guildID := range guildIDs { - guild, err := Load(db, guildID) - if err != nil { - continue // Skip failed loads - } - - m.Add(guild) - } - - return nil -} - -// Helper method for criteria matching -func (m *MasterList) matchesCriteria(guild *Guild, criteria GuildSearchCriteria) bool { - // Name pattern matching - if criteria.NamePattern != "" { - if !strings.Contains(strings.ToLower(guild.GetName()), strings.ToLower(criteria.NamePattern)) { - return false - } - } - - // Member count range - memberCount := len(guild.GetAllMembers()) - if criteria.MinMembers > 0 && memberCount < criteria.MinMembers { - return false - } - if criteria.MaxMembers > 0 && memberCount > criteria.MaxMembers { - return false - } - - // Play style - if criteria.PlayStyle > 0 && guild.GetRecruitingPlayStyle() != criteria.PlayStyle { - return false - } - - // Required flags - for _, flag := range criteria.RequiredFlags { - if guild.GetRecruitingFlag(flag) == 0 { - return false - } - } - - // Required description tags - for _, tag := range criteria.RequiredDescTags { - found := false - for i := int8(0); i < 4; i++ { - if guild.GetRecruitingDescTag(i) == tag { - found = true - break - } - } - if !found { - return false - } - } - - // Excluded description tags - for _, tag := range criteria.ExcludedDescTags { - for i := int8(0); i < 4; i++ { - if guild.GetRecruitingDescTag(i) == tag { - return false - } - } - } - - return true -} diff --git a/internal/guilds/member.go b/internal/guilds/member.go deleted file mode 100644 index 8511200..0000000 --- a/internal/guilds/member.go +++ /dev/null @@ -1,490 +0,0 @@ -package guilds - -import ( - "time" -) - -// NewGuildMember creates a new guild member instance -func NewGuildMember(characterID int32, name string, rank int8) *GuildMember { - return &GuildMember{ - CharacterID: characterID, - Name: name, - Rank: rank, - JoinDate: time.Now(), - LastLoginDate: time.Now(), - PointHistory: make([]PointHistory, 0), - RecruitingShowAdventureClass: 1, - } -} - -// GetCharacterID returns the character ID -func (gm *GuildMember) GetCharacterID() int32 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.CharacterID -} - -// GetName returns the member name -func (gm *GuildMember) GetName() string { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.Name -} - -// SetName sets the member name -func (gm *GuildMember) SetName(name string) { - gm.mu.Lock() - defer gm.mu.Unlock() - if len(name) > MaxMemberNameLength { - name = name[:MaxMemberNameLength] - } - gm.Name = name -} - -// GetRank returns the member rank -func (gm *GuildMember) GetRank() int8 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.Rank -} - -// SetRank sets the member rank -func (gm *GuildMember) SetRank(rank int8) { - gm.mu.Lock() - defer gm.mu.Unlock() - if rank >= RankLeader && rank <= RankRecruit { - gm.Rank = rank - } -} - -// GetPoints returns the member's guild points -func (gm *GuildMember) GetPoints() float64 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.Points -} - -// SetPoints sets the member's guild points -func (gm *GuildMember) SetPoints(points float64) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.Points = points -} - -// AddPoints adds points to the member -func (gm *GuildMember) AddPoints(points float64) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.Points += points -} - -// GetAdventureLevel returns the adventure level -func (gm *GuildMember) GetAdventureLevel() int8 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.AdventureLevel -} - -// SetAdventureLevel sets the adventure level -func (gm *GuildMember) SetAdventureLevel(level int8) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.AdventureLevel = level -} - -// GetAdventureClass returns the adventure class -func (gm *GuildMember) GetAdventureClass() int8 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.AdventureClass -} - -// SetAdventureClass sets the adventure class -func (gm *GuildMember) SetAdventureClass(class int8) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.AdventureClass = class -} - -// GetTradeskillLevel returns the tradeskill level -func (gm *GuildMember) GetTradeskillLevel() int8 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.TradeskillLevel -} - -// SetTradeskillLevel sets the tradeskill level -func (gm *GuildMember) SetTradeskillLevel(level int8) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.TradeskillLevel = level -} - -// GetTradeskillClass returns the tradeskill class -func (gm *GuildMember) GetTradeskillClass() int8 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.TradeskillClass -} - -// SetTradeskillClass sets the tradeskill class -func (gm *GuildMember) SetTradeskillClass(class int8) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.TradeskillClass = class -} - -// GetZone returns the member's current zone -func (gm *GuildMember) GetZone() string { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.Zone -} - -// SetZone sets the member's current zone -func (gm *GuildMember) SetZone(zone string) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.Zone = zone -} - -// GetJoinDate returns when the member joined the guild -func (gm *GuildMember) GetJoinDate() time.Time { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.JoinDate -} - -// SetJoinDate sets when the member joined the guild -func (gm *GuildMember) SetJoinDate(date time.Time) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.JoinDate = date -} - -// GetLastLoginDate returns the member's last login date -func (gm *GuildMember) GetLastLoginDate() time.Time { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.LastLoginDate -} - -// SetLastLoginDate sets the member's last login date -func (gm *GuildMember) SetLastLoginDate(date time.Time) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.LastLoginDate = date -} - -// GetNote returns the member's personal note -func (gm *GuildMember) GetNote() string { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.Note -} - -// SetNote sets the member's personal note -func (gm *GuildMember) SetNote(note string) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.Note = note -} - -// GetOfficerNote returns the member's officer note -func (gm *GuildMember) GetOfficerNote() string { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.OfficerNote -} - -// SetOfficerNote sets the member's officer note -func (gm *GuildMember) SetOfficerNote(note string) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.OfficerNote = note -} - -// GetMemberFlags returns the member flags -func (gm *GuildMember) GetMemberFlags() int8 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.MemberFlags -} - -// SetMemberFlags sets the member flags -func (gm *GuildMember) SetMemberFlags(flags int8) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.MemberFlags = flags -} - -// HasMemberFlag checks if the member has a specific flag -func (gm *GuildMember) HasMemberFlag(flag int8) bool { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.MemberFlags&flag != 0 -} - -// SetMemberFlag sets or unsets a specific member flag -func (gm *GuildMember) SetMemberFlag(flag int8, value bool) { - gm.mu.Lock() - defer gm.mu.Unlock() - - if value { - gm.MemberFlags |= flag - } else { - gm.MemberFlags &^= flag - } -} - -// IsRecruiter checks if the member is a recruiter -func (gm *GuildMember) IsRecruiter() bool { - return gm.HasMemberFlag(MemberFlagRecruitingForGuild) -} - -// SetRecruiter sets or unsets the recruiter flag -func (gm *GuildMember) SetRecruiter(isRecruiter bool) { - gm.SetMemberFlag(MemberFlagRecruitingForGuild, isRecruiter) -} - -// GetRecruiterID returns the recruiter ID -func (gm *GuildMember) GetRecruiterID() int32 { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.RecruiterID -} - -// SetRecruiterID sets the recruiter ID -func (gm *GuildMember) SetRecruiterID(id int32) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.RecruiterID = id -} - -// GetRecruiterDescription returns the recruiter description -func (gm *GuildMember) GetRecruiterDescription() string { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.RecruiterDescription -} - -// SetRecruiterDescription sets the recruiter description -func (gm *GuildMember) SetRecruiterDescription(description string) { - gm.mu.Lock() - defer gm.mu.Unlock() - gm.RecruiterDescription = description -} - -// GetRecruiterPictureData returns the recruiter picture data -func (gm *GuildMember) GetRecruiterPictureData() []byte { - gm.mu.RLock() - defer gm.mu.RUnlock() - - // Return a copy to prevent external modification - if gm.RecruiterPictureData == nil { - return nil - } - - data := make([]byte, len(gm.RecruiterPictureData)) - copy(data, gm.RecruiterPictureData) - return data -} - -// SetRecruiterPictureData sets the recruiter picture data -func (gm *GuildMember) SetRecruiterPictureData(data []byte) { - gm.mu.Lock() - defer gm.mu.Unlock() - - if data == nil { - gm.RecruiterPictureData = nil - return - } - - // Make a copy to prevent external modification - gm.RecruiterPictureData = make([]byte, len(data)) - copy(gm.RecruiterPictureData, data) -} - -// GetRecruitingShowAdventureClass returns whether to show adventure class -func (gm *GuildMember) GetRecruitingShowAdventureClass() bool { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.RecruitingShowAdventureClass != 0 -} - -// SetRecruitingShowAdventureClass sets whether to show adventure class -func (gm *GuildMember) SetRecruitingShowAdventureClass(show bool) { - gm.mu.Lock() - defer gm.mu.Unlock() - - if show { - gm.RecruitingShowAdventureClass = 1 - } else { - gm.RecruitingShowAdventureClass = 0 - } -} - -// GetPointHistory returns a copy of the point history -func (gm *GuildMember) GetPointHistory() []PointHistory { - gm.mu.RLock() - defer gm.mu.RUnlock() - - history := make([]PointHistory, len(gm.PointHistory)) - copy(history, gm.PointHistory) - return history -} - -// AddPointHistory adds a point history entry -func (gm *GuildMember) AddPointHistory(date time.Time, modifiedBy string, points float64, comment string) { - gm.mu.Lock() - defer gm.mu.Unlock() - - // Limit history size - if len(gm.PointHistory) >= MaxPointHistory { - // Remove oldest entry - gm.PointHistory = gm.PointHistory[1:] - } - - history := PointHistory{ - Date: date, - ModifiedBy: modifiedBy, - Points: points, - Comment: comment, - SaveNeeded: true, - } - - gm.PointHistory = append(gm.PointHistory, history) -} - -// GetMemberInfo returns formatted member information -func (gm *GuildMember) GetMemberInfo(rankName string, isOnline bool) MemberInfo { - gm.mu.RLock() - defer gm.mu.RUnlock() - - return MemberInfo{ - CharacterID: gm.CharacterID, - Name: gm.Name, - Rank: gm.Rank, - RankName: rankName, - Points: gm.Points, - AdventureClass: gm.AdventureClass, - AdventureLevel: gm.AdventureLevel, - TradeskillClass: gm.TradeskillClass, - TradeskillLevel: gm.TradeskillLevel, - Zone: gm.Zone, - JoinDate: gm.JoinDate, - LastLoginDate: gm.LastLoginDate, - IsOnline: isOnline, - IsRecruiter: gm.MemberFlags&MemberFlagRecruitingForGuild != 0, - Note: gm.Note, - OfficerNote: gm.OfficerNote, - } -} - -// GetRecruiterInfo returns formatted recruiter information -func (gm *GuildMember) GetRecruiterInfo(isOnline bool) RecruiterInfo { - gm.mu.RLock() - defer gm.mu.RUnlock() - - return RecruiterInfo{ - CharacterID: gm.CharacterID, - Name: gm.Name, - Description: gm.RecruiterDescription, - PictureData: gm.GetRecruiterPictureData(), // This will make a copy - ShowAdventureClass: gm.RecruitingShowAdventureClass != 0, - AdventureClass: gm.AdventureClass, - IsOnline: isOnline, - } -} - -// UpdatePlayerInfo updates member info from player data -func (gm *GuildMember) UpdatePlayerInfo(playerInfo PlayerInfo) { - gm.mu.Lock() - defer gm.mu.Unlock() - - gm.AdventureLevel = playerInfo.AdventureLevel - gm.AdventureClass = playerInfo.AdventureClass - gm.TradeskillLevel = playerInfo.TradeskillLevel - gm.TradeskillClass = playerInfo.TradeskillClass - gm.Zone = playerInfo.Zone - - if playerInfo.IsOnline { - gm.LastLoginDate = time.Now() - } -} - -// ValidateRank checks if the rank is valid -func (gm *GuildMember) ValidateRank() bool { - gm.mu.RLock() - defer gm.mu.RUnlock() - return gm.Rank >= RankLeader && gm.Rank <= RankRecruit -} - -// CanPromote checks if this member can promote another member -func (gm *GuildMember) CanPromote(targetRank int8) bool { - gm.mu.RLock() - defer gm.mu.RUnlock() - - // Can only promote members with lower rank (higher rank number) - // Cannot promote to same or higher rank than self - return gm.Rank < targetRank && targetRank > RankLeader -} - -// CanDemote checks if this member can demote another member -func (gm *GuildMember) CanDemote(targetRank int8) bool { - gm.mu.RLock() - defer gm.mu.RUnlock() - - // Can only demote members with equal or lower rank (same or higher rank number) - // Cannot demote to recruit (already lowest) - return gm.Rank <= targetRank && targetRank < RankRecruit -} - -// CanKick checks if this member can kick another member -func (gm *GuildMember) CanKick(targetRank int8) bool { - gm.mu.RLock() - defer gm.mu.RUnlock() - - // Can only kick members with lower rank (higher rank number) - return gm.Rank < targetRank -} - -// Copy creates a deep copy of the guild member -func (gm *GuildMember) Copy() *GuildMember { - gm.mu.RLock() - defer gm.mu.RUnlock() - - newMember := &GuildMember{ - CharacterID: gm.CharacterID, - AccountID: gm.AccountID, - RecruiterID: gm.RecruiterID, - Name: gm.Name, - GuildStatus: gm.GuildStatus, - Points: gm.Points, - AdventureClass: gm.AdventureClass, - AdventureLevel: gm.AdventureLevel, - TradeskillClass: gm.TradeskillClass, - TradeskillLevel: gm.TradeskillLevel, - Rank: gm.Rank, - MemberFlags: gm.MemberFlags, - Zone: gm.Zone, - JoinDate: gm.JoinDate, - LastLoginDate: gm.LastLoginDate, - Note: gm.Note, - OfficerNote: gm.OfficerNote, - RecruiterDescription: gm.RecruiterDescription, - RecruitingShowAdventureClass: gm.RecruitingShowAdventureClass, - PointHistory: make([]PointHistory, len(gm.PointHistory)), - } - - // Deep copy point history - copy(newMember.PointHistory, gm.PointHistory) - - // Deep copy picture data - if gm.RecruiterPictureData != nil { - newMember.RecruiterPictureData = make([]byte, len(gm.RecruiterPictureData)) - copy(newMember.RecruiterPictureData, gm.RecruiterPictureData) - } - - return newMember -} diff --git a/internal/guilds/types.go b/internal/guilds/types.go deleted file mode 100644 index 257948c..0000000 --- a/internal/guilds/types.go +++ /dev/null @@ -1,330 +0,0 @@ -package guilds - -import ( - "sync" - "time" - - "eq2emu/internal/database" -) - -// 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 -type GuildMember struct { - mu sync.RWMutex - 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:"-"` -} - -// 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:"-"` -} - -// Guild represents a guild with all its properties and members -type Guild struct { - mu sync.RWMutex - - // Database integration - db *database.Database - isNew bool - - id int32 - name string - level int8 - formedDate time.Time - motd string - expCurrent int64 - expToNextLevel int64 - recruitingShortDesc string - recruitingFullDesc string - recruitingMinLevel int8 - recruitingPlayStyle int8 - 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 - - // Save flags - saveNeeded bool - memberSaveNeeded bool - eventsSaveNeeded bool - ranksSaveNeeded bool - eventFiltersSaveNeeded bool - pointsHistorySaveNeeded bool - recruitingSaveNeeded bool - - // Tracking - nextEventID int64 - lastModified time.Time -} - -// GuildData represents guild data for database operations -type GuildData struct { - ID int32 `json:"id" db:"id"` - Name string `json:"name" db:"name"` - Level int8 `json:"level" db:"level"` - FormedDate time.Time `json:"formed_date" db:"formed_date"` - MOTD string `json:"motd" db:"motd"` - EXPCurrent int64 `json:"exp_current" db:"exp_current"` - EXPToNextLevel int64 `json:"exp_to_next_level" db:"exp_to_next_level"` - RecruitingShortDesc string `json:"recruiting_short_desc" db:"recruiting_short_desc"` - RecruitingFullDesc string `json:"recruiting_full_desc" db:"recruiting_full_desc"` - RecruitingMinLevel int8 `json:"recruiting_min_level" db:"recruiting_min_level"` - RecruitingPlayStyle int8 `json:"recruiting_play_style" db:"recruiting_play_style"` -} - -// GuildMemberData represents guild member data for database operations -type GuildMemberData struct { - CharacterID int32 `json:"character_id" db:"character_id"` - GuildID int32 `json:"guild_id" db:"guild_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"` -} - -// GuildEventData represents guild event data for database operations -type GuildEventData struct { - EventID int64 `json:"event_id" db:"event_id"` - GuildID int32 `json:"guild_id" db:"guild_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"` -} - -// GuildRankData represents guild rank data for database operations -type GuildRankData struct { - GuildID int32 `json:"guild_id" db:"guild_id"` - Rank int8 `json:"rank" db:"rank"` - Name string `json:"name" db:"name"` -} - -// GuildPermissionData represents guild permission data for database operations -type GuildPermissionData struct { - GuildID int32 `json:"guild_id" db:"guild_id"` - Rank int8 `json:"rank" db:"rank"` - Permission int8 `json:"permission" db:"permission"` - Value int8 `json:"value" db:"value"` -} - -// GuildEventFilterData represents guild event filter data for database operations -type GuildEventFilterData struct { - GuildID int32 `json:"guild_id" db:"guild_id"` - EventID int8 `json:"event_id" db:"event_id"` - Category int8 `json:"category" db:"category"` - Value int8 `json:"value" db:"value"` -} - -// GuildRecruitingData represents guild recruiting data for database operations -type GuildRecruitingData struct { - GuildID int32 `json:"guild_id" db:"guild_id"` - Flag int8 `json:"flag" db:"flag"` - Value int8 `json:"value" db:"value"` -} - -// PointHistoryData represents point history data for database operations -type PointHistoryData struct { - CharacterID int32 `json:"character_id" db:"character_id"` - 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"` -} - -// GuildList manages all guilds in the system -type GuildList struct { - mu sync.RWMutex - guilds map[int32]*Guild -} - -// GuildManager provides high-level guild management -type GuildManager struct { - guildList *GuildList - database GuildDatabase - clientManager ClientManager - playerManager PlayerManager - eventHandler GuildEventHandler - logger LogHandler -} - -// GuildStatistics provides guild system usage statistics -type GuildStatistics struct { - TotalGuilds int `json:"total_guilds"` - TotalMembers int `json:"total_members"` - ActiveGuilds int `json:"active_guilds"` - AverageGuildSize float64 `json:"average_guild_size"` - TotalEvents int `json:"total_events"` - TotalRecruiters int `json:"total_recruiters"` - UniqueAccounts int `json:"unique_accounts"` - HighestGuildLevel int8 `json:"highest_guild_level"` -} - -// 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"` -} - -// MemberInfo provides guild member information -type MemberInfo struct { - CharacterID int32 `json:"character_id"` - Name string `json:"name"` - Rank int8 `json:"rank"` - RankName string `json:"rank_name"` - Points float64 `json:"points"` - AdventureClass int8 `json:"adventure_class"` - AdventureLevel int8 `json:"adventure_level"` - TradeskillClass int8 `json:"tradeskill_class"` - TradeskillLevel int8 `json:"tradeskill_level"` - Zone string `json:"zone"` - JoinDate time.Time `json:"join_date"` - LastLoginDate time.Time `json:"last_login_date"` - IsOnline bool `json:"is_online"` - IsRecruiter bool `json:"is_recruiter"` - Note string `json:"note"` - OfficerNote string `json:"officer_note"` -} - -// GuildRoster represents the complete guild roster -type GuildRoster struct { - GuildInfo GuildInfo `json:"guild_info"` - Members []MemberInfo `json:"members"` -} - -// 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"` -} - -// GuildEventInfo provides formatted guild event information -type GuildEventInfo struct { - EventID int64 `json:"event_id"` - Date time.Time `json:"date"` - Type int32 `json:"type"` - TypeName string `json:"type_name"` - Description string `json:"description"` - Locked bool `json:"locked"` -} - -// GuildSearchCriteria represents guild search parameters -type GuildSearchCriteria struct { - NamePattern string `json:"name_pattern"` - MinLevel int8 `json:"min_level"` - MaxLevel int8 `json:"max_level"` - MinMembers int `json:"min_members"` - MaxMembers int `json:"max_members"` - RecruitingOnly bool `json:"recruiting_only"` - PlayStyle int8 `json:"play_style"` - RequiredFlags []int8 `json:"required_flags"` - RequiredDescTags []int8 `json:"required_desc_tags"` - ExcludedDescTags []int8 `json:"excluded_desc_tags"` -} - -// RecruitingInfo provides detailed recruiting information -type RecruitingInfo struct { - GuildID int32 `json:"guild_id"` - GuildName string `json:"guild_name"` - ShortDesc string `json:"short_desc"` - FullDesc string `json:"full_desc"` - MinLevel int8 `json:"min_level"` - PlayStyle int8 `json:"play_style"` - Flags map[int8]int8 `json:"flags"` - DescTags map[int8]int8 `json:"desc_tags"` - Recruiters []RecruiterInfo `json:"recruiters"` -} - -// RecruiterInfo provides recruiter information -type RecruiterInfo struct { - CharacterID int32 `json:"character_id"` - Name string `json:"name"` - Description string `json:"description"` - PictureData []byte `json:"picture_data"` - ShowAdventureClass bool `json:"show_adventure_class"` - AdventureClass int8 `json:"adventure_class"` - IsOnline bool `json:"is_online"` -} diff --git a/internal/packets/opcodes.go b/internal/packets/opcodes.go index 71eb79f..7579cfe 100644 --- a/internal/packets/opcodes.go +++ b/internal/packets/opcodes.go @@ -147,6 +147,21 @@ const ( OP_DefaultGroupOptionsMsg OP_DefaultGroupOptionsRequestMsg + // Guild system opcodes + OP_GuildUpdateMsg + OP_GuildEventListMsg + OP_GuildEventDetailsMsg + OP_GuildEventAddMsg + OP_GuildMembershipResponseMsg + OP_JoinGuildNotifyMsg + OP_LeaveGuildNotifyMsg + OP_GuildInviteMsg + OP_GuildDeclineMsg + OP_GuildRecruitingMsg + OP_GuildRecruitingDetailsMsg + OP_ModifyGuildMsg + OP_RequestGuildInfoMsg + // Add more opcodes as needed... _maxInternalOpcode // Sentinel value ) @@ -253,6 +268,21 @@ var OpcodeNames = map[InternalOpcode]string{ OP_GroupOptionsMsg: "OP_GroupOptionsMsg", OP_DefaultGroupOptionsMsg: "OP_DefaultGroupOptionsMsg", OP_DefaultGroupOptionsRequestMsg: "OP_DefaultGroupOptionsRequestMsg", + + // Guild system opcodes + OP_GuildUpdateMsg: "OP_GuildUpdateMsg", + OP_GuildEventListMsg: "OP_GuildEventListMsg", + OP_GuildEventDetailsMsg: "OP_GuildEventDetailsMsg", + OP_GuildEventAddMsg: "OP_GuildEventAddMsg", + OP_GuildMembershipResponseMsg: "OP_GuildMembershipResponseMsg", + OP_JoinGuildNotifyMsg: "OP_JoinGuildNotifyMsg", + OP_LeaveGuildNotifyMsg: "OP_LeaveGuildNotifyMsg", + OP_GuildInviteMsg: "OP_GuildInviteMsg", + OP_GuildDeclineMsg: "OP_GuildDeclineMsg", + OP_GuildRecruitingMsg: "OP_GuildRecruitingMsg", + OP_GuildRecruitingDetailsMsg: "OP_GuildRecruitingDetailsMsg", + OP_ModifyGuildMsg: "OP_ModifyGuildMsg", + OP_RequestGuildInfoMsg: "OP_RequestGuildInfoMsg", } // OpcodeManager handles the mapping between client-specific opcodes and internal opcodes