eq2go/internal/guilds/guild.go
2025-08-08 13:08:15 -05:00

927 lines
21 KiB
Go

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()
}