eq2go/internal/chat/channel/channel.go

359 lines
9.0 KiB
Go

package channel
import (
"fmt"
"slices"
"sync"
"time"
)
// Channel type constants
const (
TypeNone = 0
TypeWorld = 1 // Persistent, loaded from database
TypeCustom = 2 // Temporary, deleted when empty
)
// Channel actions for client communication
const (
ActionJoin = 0 // Player joins channel
ActionLeave = 1 // Player leaves channel
ActionOtherJoin = 2 // Another player joins
ActionOtherLeave = 3 // Another player leaves
)
// Channel name and password limits
const (
MaxNameLength = 100
MaxPasswordLength = 100
)
// Channel restrictions
const (
NoLevelRestriction = 0
NoRaceRestriction = 0
NoClassRestriction = 0
)
// Channel represents a chat channel with membership and message routing capabilities
type Channel struct {
mu sync.RWMutex
name string
password string
channelType int
levelRestriction int32
raceRestriction int32
classRestriction int32
members []int32 // Character IDs
discordEnabled bool
created time.Time
}
// ChannelInfo provides basic channel information for client lists
type ChannelInfo struct {
Name string
HasPassword bool
MemberCount int
LevelRestriction int32
RaceRestriction int32
ClassRestriction int32
ChannelType int
}
// ChannelMember represents a member in a channel
type ChannelMember struct {
CharacterID int32
CharacterName string
Level int32
Race int32
Class int32
JoinedAt time.Time
}
// NewChannel creates a new channel instance
func NewChannel(name string) *Channel {
return &Channel{
name: name,
members: make([]int32, 0),
created: time.Now(),
}
}
// SetName sets the channel name
func (c *Channel) SetName(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.name = name
}
// SetPassword sets the channel password
func (c *Channel) SetPassword(password string) {
c.mu.Lock()
defer c.mu.Unlock()
c.password = password
}
// SetType sets the channel type
func (c *Channel) SetType(channelType int) {
c.mu.Lock()
defer c.mu.Unlock()
c.channelType = channelType
}
// SetLevelRestriction sets the minimum level required to join
func (c *Channel) SetLevelRestriction(level int32) {
c.mu.Lock()
defer c.mu.Unlock()
c.levelRestriction = level
}
// SetRacesAllowed sets the race bitmask for allowed races
func (c *Channel) SetRacesAllowed(races int32) {
c.mu.Lock()
defer c.mu.Unlock()
c.raceRestriction = races
}
// SetClassesAllowed sets the class bitmask for allowed classes
func (c *Channel) SetClassesAllowed(classes int32) {
c.mu.Lock()
defer c.mu.Unlock()
c.classRestriction = classes
}
// SetDiscordEnabled enables or disables Discord integration for this channel
func (c *Channel) SetDiscordEnabled(enabled bool) {
c.mu.Lock()
defer c.mu.Unlock()
c.discordEnabled = enabled
}
// GetName returns the channel name
func (c *Channel) GetName() string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.name
}
// GetType returns the channel type
func (c *Channel) GetType() int {
c.mu.RLock()
defer c.mu.RUnlock()
return c.channelType
}
// GetNumClients returns the number of clients in the channel
func (c *Channel) GetNumClients() int {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.members)
}
// GetCreatedTime returns when the channel was created
func (c *Channel) GetCreatedTime() time.Time {
c.mu.RLock()
defer c.mu.RUnlock()
return c.created
}
// HasPassword returns true if the channel has a password
func (c *Channel) HasPassword() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.password != ""
}
// PasswordMatches checks if the provided password matches the channel password
func (c *Channel) PasswordMatches(password string) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.password == password
}
// CanJoinChannelByLevel checks if a player's level meets the channel requirements
func (c *Channel) CanJoinChannelByLevel(level int32) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return level >= c.levelRestriction
}
// CanJoinChannelByRace checks if a player's race is allowed in the channel
func (c *Channel) CanJoinChannelByRace(raceID int32) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.raceRestriction == NoRaceRestriction || (c.raceRestriction&(1<<raceID)) != 0
}
// CanJoinChannelByClass checks if a player's class is allowed in the channel
func (c *Channel) CanJoinChannelByClass(classID int32) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.classRestriction == NoClassRestriction || (c.classRestriction&(1<<classID)) != 0
}
// IsInChannel checks if a character is in the channel
func (c *Channel) IsInChannel(characterID int32) bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.isInChannel(characterID)
}
// isInChannel is the internal implementation without locking
func (c *Channel) isInChannel(characterID int32) bool {
return slices.Contains(c.members, characterID)
}
// JoinChannel adds a character to the channel
func (c *Channel) JoinChannel(characterID int32) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.joinChannel(characterID)
}
// joinChannel is the internal implementation without locking
func (c *Channel) joinChannel(characterID int32) error {
// Check if already in channel
if c.isInChannel(characterID) {
return fmt.Errorf("character %d is already in channel %s", characterID, c.name)
}
// Add to members list
c.members = append(c.members, characterID)
return nil
}
// LeaveChannel removes a character from the channel
func (c *Channel) LeaveChannel(characterID int32) error {
c.mu.Lock()
defer c.mu.Unlock()
return c.leaveChannel(characterID)
}
// leaveChannel is the internal implementation without locking
func (c *Channel) leaveChannel(characterID int32) error {
// Find and remove the character
for i, memberID := range c.members {
if memberID == characterID {
// Remove member by swapping with last element and truncating
c.members[i] = c.members[len(c.members)-1]
c.members = c.members[:len(c.members)-1]
return nil
}
}
return fmt.Errorf("character %d is not in channel %s", characterID, c.name)
}
// GetMembers returns a copy of the current member list
func (c *Channel) GetMembers() []int32 {
c.mu.RLock()
defer c.mu.RUnlock()
// Return a copy to prevent external modification
members := make([]int32, len(c.members))
copy(members, c.members)
return members
}
// GetChannelInfo returns basic channel information
func (c *Channel) GetChannelInfo() ChannelInfo {
c.mu.RLock()
defer c.mu.RUnlock()
return ChannelInfo{
Name: c.name,
HasPassword: c.password != "",
MemberCount: len(c.members),
LevelRestriction: c.levelRestriction,
RaceRestriction: c.raceRestriction,
ClassRestriction: c.classRestriction,
ChannelType: c.channelType,
}
}
// ValidateJoin checks if a character can join the channel based on restrictions
func (c *Channel) ValidateJoin(level, race, class int32, password string) error {
c.mu.RLock()
defer c.mu.RUnlock()
// Check password
if c.password != "" && c.password != password {
return fmt.Errorf("invalid password for channel %s", c.name)
}
// Check level restriction
if !c.CanJoinChannelByLevel(level) {
return fmt.Errorf("level %d does not meet minimum requirement of %d for channel %s",
level, c.levelRestriction, c.name)
}
// Check race restriction
if !c.CanJoinChannelByRace(race) {
return fmt.Errorf("race %d is not allowed in channel %s", race, c.name)
}
// Check class restriction
if !c.CanJoinChannelByClass(class) {
return fmt.Errorf("class %d is not allowed in channel %s", class, c.name)
}
return nil
}
// IsEmpty returns true if the channel has no members
func (c *Channel) IsEmpty() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return len(c.members) == 0
}
// IsDiscordEnabled returns true if Discord integration is enabled for this channel
func (c *Channel) IsDiscordEnabled() bool {
c.mu.RLock()
defer c.mu.RUnlock()
return c.discordEnabled
}
// Copy creates a deep copy of the channel (useful for serialization or backups)
func (c *Channel) Copy() *Channel {
c.mu.RLock()
defer c.mu.RUnlock()
newChannel := &Channel{
name: c.name,
password: c.password,
channelType: c.channelType,
levelRestriction: c.levelRestriction,
raceRestriction: c.raceRestriction,
classRestriction: c.classRestriction,
discordEnabled: c.discordEnabled,
created: c.created,
members: make([]int32, len(c.members)),
}
copy(newChannel.members, c.members)
return newChannel
}
// GetRestrictions returns the channel's access restrictions
func (c *Channel) GetRestrictions() (level, race, class int32) {
c.mu.RLock()
defer c.mu.RUnlock()
return c.levelRestriction, c.raceRestriction, c.classRestriction
}
// GetPassword returns the channel password (for admin purposes)
func (c *Channel) GetPassword() string {
c.mu.RLock()
defer c.mu.RUnlock()
return c.password
}
// UpdateRestrictions updates all access restrictions at once
func (c *Channel) UpdateRestrictions(level, race, class int32) {
c.mu.Lock()
defer c.mu.Unlock()
c.levelRestriction = level
c.raceRestriction = race
c.classRestriction = class
}