eq2go/internal/chat/channel.go

399 lines
12 KiB
Go

package chat
import (
"fmt"
"slices"
"time"
"eq2emu/internal/database"
)
// Channel represents a chat channel with membership and message routing capabilities
type Channel struct {
ID int32 `json:"id"` // Channel ID
Name string `json:"name"` // Channel name
Password string `json:"-"` // Channel password (hidden from JSON)
ChannelType int `json:"type"` // Channel type (world/custom)
LevelRestriction int32 `json:"level_restriction"`
RaceRestriction int32 `json:"race_restriction"`
ClassRestriction int32 `json:"class_restriction"`
DiscordEnabled bool `json:"discord_enabled"`
Created time.Time `json:"created"`
Updated time.Time `json:"updated"`
members []int32 `json:"-"` // Character IDs (not persisted)
db *database.Database `json:"-"` // Database connection
isNew bool `json:"-"` // Whether this is a new channel
}
// New creates a new channel with the given database
func New(db *database.Database) *Channel {
return &Channel{
db: db,
isNew: true,
members: make([]int32, 0),
Created: time.Now(),
Updated: time.Now(),
}
}
// NewWithData creates a new channel with data
func NewWithData(id int32, name string, channelType int, db *database.Database) *Channel {
return &Channel{
ID: id,
Name: name,
ChannelType: channelType,
db: db,
isNew: true,
members: make([]int32, 0),
Created: time.Now(),
Updated: time.Now(),
}
}
// Load loads a channel by ID from the database
func Load(db *database.Database, id int32) (*Channel, error) {
channel := &Channel{
db: db,
isNew: false,
members: make([]int32, 0),
}
query := `SELECT id, name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels WHERE id = ?`
row := db.QueryRow(query, id)
err := row.Scan(&channel.ID, &channel.Name, &channel.Password, &channel.ChannelType,
&channel.LevelRestriction, &channel.RaceRestriction, &channel.ClassRestriction,
&channel.DiscordEnabled, &channel.Created, &channel.Updated)
if err != nil {
return nil, fmt.Errorf("failed to load channel %d: %w", id, err)
}
return channel, nil
}
// LoadByName loads a channel by name from the database
func LoadByName(db *database.Database, name string) (*Channel, error) {
channel := &Channel{
db: db,
isNew: false,
members: make([]int32, 0),
}
query := `SELECT id, name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels WHERE name = ?`
row := db.QueryRow(query, name)
err := row.Scan(&channel.ID, &channel.Name, &channel.Password, &channel.ChannelType,
&channel.LevelRestriction, &channel.RaceRestriction, &channel.ClassRestriction,
&channel.DiscordEnabled, &channel.Created, &channel.Updated)
if err != nil {
return nil, fmt.Errorf("failed to load channel %s: %w", name, err)
}
return channel, nil
}
// GetID returns the channel ID (implements Identifiable interface)
func (c *Channel) GetID() int32 {
return c.ID
}
// SetName sets the channel name
func (c *Channel) SetName(name string) {
c.Name = name
c.Updated = time.Now()
}
// SetPassword sets the channel password
func (c *Channel) SetPassword(password string) {
c.Password = password
c.Updated = time.Now()
}
// SetType sets the channel type
func (c *Channel) SetType(channelType int) {
c.ChannelType = channelType
c.Updated = time.Now()
}
// SetLevelRestriction sets the minimum level required to join
func (c *Channel) SetLevelRestriction(level int32) {
c.LevelRestriction = level
c.Updated = time.Now()
}
// SetRacesAllowed sets the race bitmask for allowed races
func (c *Channel) SetRacesAllowed(races int32) {
c.RaceRestriction = races
c.Updated = time.Now()
}
// SetClassesAllowed sets the class bitmask for allowed classes
func (c *Channel) SetClassesAllowed(classes int32) {
c.ClassRestriction = classes
c.Updated = time.Now()
}
// GetName returns the channel name
func (c *Channel) GetName() string {
return c.Name
}
// GetType returns the channel type
func (c *Channel) GetType() int {
return c.ChannelType
}
// GetNumClients returns the number of clients in the channel
func (c *Channel) GetNumClients() int {
return len(c.members)
}
// HasPassword returns true if the channel has a password
func (c *Channel) HasPassword() bool {
return c.Password != ""
}
// PasswordMatches checks if the provided password matches the channel password
func (c *Channel) PasswordMatches(password string) bool {
return c.Password == password
}
// CanJoinChannelByLevel checks if a player's level meets the channel requirements
func (c *Channel) CanJoinChannelByLevel(level int32) bool {
return level >= c.LevelRestriction
}
// CanJoinChannelByRace checks if a player's race is allowed in the channel
func (c *Channel) CanJoinChannelByRace(raceID int32) bool {
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 {
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 {
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 {
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 {
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 {
// 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 {
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 {
// 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 {
return len(c.members) == 0
}
// Copy creates a deep copy of the channel (useful for serialization or backups)
func (c *Channel) Copy() *Channel {
newChannel := &Channel{
ID: c.ID,
Name: c.Name,
Password: c.Password,
ChannelType: c.ChannelType,
LevelRestriction: c.LevelRestriction,
RaceRestriction: c.RaceRestriction,
ClassRestriction: c.ClassRestriction,
DiscordEnabled: c.DiscordEnabled,
Created: c.Created,
Updated: c.Updated,
db: c.db,
isNew: true, // Copy is always new
members: make([]int32, len(c.members)),
}
copy(newChannel.members, c.members)
return newChannel
}
// IsNew returns true if this is a new channel not yet saved to database
func (c *Channel) IsNew() bool {
return c.isNew
}
// Save saves the channel to the database
func (c *Channel) Save() error {
if c.db == nil {
return fmt.Errorf("no database connection available")
}
if c.isNew {
return c.insert()
}
return c.update()
}
// Delete removes the channel from the database
func (c *Channel) Delete() error {
if c.db == nil {
return fmt.Errorf("no database connection available")
}
if c.isNew {
return fmt.Errorf("cannot delete unsaved channel")
}
query := `DELETE FROM channels WHERE id = ?`
_, err := c.db.Exec(query, c.ID)
if err != nil {
return fmt.Errorf("failed to delete channel %d: %w", c.ID, err)
}
return nil
}
// Reload reloads the channel data from the database
func (c *Channel) Reload() error {
if c.db == nil {
return fmt.Errorf("no database connection available")
}
if c.isNew {
return fmt.Errorf("cannot reload unsaved channel")
}
query := `SELECT name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at FROM channels WHERE id = ?`
row := c.db.QueryRow(query, c.ID)
err := row.Scan(&c.Name, &c.Password, &c.ChannelType, &c.LevelRestriction,
&c.RaceRestriction, &c.ClassRestriction, &c.DiscordEnabled, &c.Created, &c.Updated)
if err != nil {
return fmt.Errorf("failed to reload channel %d: %w", c.ID, err)
}
return nil
}
// insert inserts a new channel into the database
func (c *Channel) insert() error {
query := `INSERT INTO channels (name, password, type, level_restriction, race_restriction, class_restriction, discord_enabled, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`
result, err := c.db.Exec(query, c.Name, c.Password, c.ChannelType, c.LevelRestriction,
c.RaceRestriction, c.ClassRestriction, c.DiscordEnabled, c.Created, c.Updated)
if err != nil {
return fmt.Errorf("failed to insert channel: %w", err)
}
// Get the generated ID
id, err := result.LastInsertId()
if err != nil {
return fmt.Errorf("failed to get inserted channel ID: %w", err)
}
c.ID = int32(id)
c.isNew = false
return nil
}
// update updates an existing channel in the database
func (c *Channel) update() error {
c.Updated = time.Now()
query := `UPDATE channels SET name = ?, password = ?, type = ?, level_restriction = ?,
race_restriction = ?, class_restriction = ?, discord_enabled = ?, updated_at = ?
WHERE id = ?`
result, err := c.db.Exec(query, c.Name, c.Password, c.ChannelType, c.LevelRestriction,
c.RaceRestriction, c.ClassRestriction, c.DiscordEnabled, c.Updated, c.ID)
if err != nil {
return fmt.Errorf("failed to update channel: %w", err)
}
rowsAffected, err := result.RowsAffected()
if err != nil {
return fmt.Errorf("failed to get rows affected: %w", err)
}
if rowsAffected == 0 {
return fmt.Errorf("channel %d not found for update", c.ID)
}
return nil
}