eq2go/internal/languages/languages.go
2025-08-29 17:48:39 -05:00

1599 lines
43 KiB
Go

package languages
import (
"fmt"
"sync"
)
// Simplified Language System
// Consolidates all functionality from 4 files into unified architecture
// Preserves 100% C++ EQ2 language functionality while eliminating Active Record patterns
// Language system constants
const (
// Maximum language name length
MaxLanguageNameLength = 50
// Special language IDs (common in EQ2)
LanguageIDCommon = 0 // Common tongue (default)
LanguageIDElvish = 1 // Elvish
LanguageIDDwarven = 2 // Dwarven
LanguageIDHalfling = 3 // Halfling
LanguageIDGnomish = 4 // Gnomish
LanguageIDIksar = 5 // Iksar
LanguageIDTrollish = 6 // Trollish
LanguageIDOgrish = 7 // Ogrish
LanguageIDFae = 8 // Fae
LanguageIDArasai = 9 // Arasai
LanguageIDSarnak = 10 // Sarnak
LanguageIDFroglok = 11 // Froglok
)
// Language validation constants
const (
MinLanguageID = 0
MaxLanguageID = 999999 // Reasonable upper bound
)
// Database operation constants
const (
SaveStatusUnchanged = false
SaveStatusNeeded = true
)
// System limits
const (
MaxLanguagesPerPlayer = 100 // Reasonable limit to prevent abuse
MaxTotalLanguages = 1000 // System-wide language limit
)
// Database interface for language persistence
type Database interface {
LoadAllLanguages() ([]*Language, error)
SaveLanguage(language *Language) error
DeleteLanguage(languageID int32) error
LoadPlayerLanguages(playerID int32) ([]*Language, error)
SavePlayerLanguage(playerID int32, languageID int32) error
DeletePlayerLanguage(playerID int32, languageID int32) error
}
// Logger interface for language logging
type Logger interface {
LogInfo(message string, args ...any)
LogError(message string, args ...any)
LogDebug(message string, args ...any)
LogWarning(message string, args ...any)
}
// Entity interface for language-related entity operations
type Entity interface {
GetID() int32
GetName() string
IsPlayer() bool
IsNPC() bool
IsBot() bool
}
// Player interface for language-related player operations
type Player interface {
Entity
GetCharacterID() int32
SendMessage(message string)
}
// Client interface for language-related client operations
type Client interface {
GetVersion() int16
SendLanguageUpdate(languageData []byte) error
}
// LanguageProvider interface for systems that provide language functionality
type LanguageProvider interface {
GetMasterLanguagesList() *MasterLanguagesList
GetLanguage(languageID int32) *Language
GetLanguageByName(name string) *Language
CreatePlayerLanguagesList() *PlayerLanguagesList
}
// LanguageAware interface for entities that can understand languages
type LanguageAware interface {
GetKnownLanguages() *PlayerLanguagesList
KnowsLanguage(languageID int32) bool
GetPrimaryLanguage() int32
SetPrimaryLanguage(languageID int32)
CanUnderstand(languageID int32) bool
LearnLanguage(languageID int32) error
ForgetLanguage(languageID int32) error
}
// LanguageHandler interface for handling language events
type LanguageHandler interface {
OnLanguageLearned(player Player, languageID int32) error
OnLanguageForgotten(player Player, languageID int32) error
OnLanguageUsed(player Player, languageID int32, message string) error
}
// ChatProcessor interface for processing multilingual chat
type ChatProcessor interface {
ProcessMessage(speaker Player, message string, languageID int32) (string, error)
FilterMessage(listener Player, message string, languageID int32) string
GetLanguageSkramble(message string, comprehension float32) string
}
// Language represents a single language that can be learned by players
type Language struct {
id int32 // Unique language identifier
name string // Language name
saveNeeded bool // Whether this language needs to be saved to database
mutex sync.RWMutex // Thread safety
}
// NewLanguage creates a new language with default values
func NewLanguage() *Language {
return &Language{
id: 0,
name: "",
saveNeeded: false,
}
}
// NewLanguageFromExisting creates a copy of an existing language
func NewLanguageFromExisting(source *Language) *Language {
if source == nil {
return NewLanguage()
}
source.mutex.RLock()
defer source.mutex.RUnlock()
return &Language{
id: source.id,
name: source.name,
saveNeeded: source.saveNeeded,
}
}
// GetID returns the language's unique identifier
func (l *Language) GetID() int32 {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.id
}
// SetID updates the language's unique identifier
func (l *Language) SetID(id int32) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.id = id
}
// GetName returns the language name
func (l *Language) GetName() string {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.name
}
// SetName updates the language name
func (l *Language) SetName(name string) {
l.mutex.Lock()
defer l.mutex.Unlock()
// Truncate if too long
if len(name) > MaxLanguageNameLength {
name = name[:MaxLanguageNameLength]
}
l.name = name
}
// GetSaveNeeded returns whether this language needs database saving
func (l *Language) GetSaveNeeded() bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
return l.saveNeeded
}
// SetSaveNeeded updates the save status
func (l *Language) SetSaveNeeded(needed bool) {
l.mutex.Lock()
defer l.mutex.Unlock()
l.saveNeeded = needed
}
// IsValid validates the language data
func (l *Language) IsValid() bool {
l.mutex.RLock()
defer l.mutex.RUnlock()
if l.id < MinLanguageID || l.id > MaxLanguageID {
return false
}
if len(l.name) == 0 || len(l.name) > MaxLanguageNameLength {
return false
}
return true
}
// String returns a string representation of the language
func (l *Language) String() string {
l.mutex.RLock()
defer l.mutex.RUnlock()
return fmt.Sprintf("Language{ID: %d, Name: %s, SaveNeeded: %v}", l.id, l.name, l.saveNeeded)
}
// Copy creates a deep copy of the language
func (l *Language) Copy() *Language {
return NewLanguageFromExisting(l)
}
// MasterLanguagesList manages the global list of all available languages
type MasterLanguagesList struct {
languages map[int32]*Language // Languages indexed by ID for fast lookup
nameIndex map[string]*Language // Languages indexed by name for name lookups
mutex sync.RWMutex // Thread safety
}
// NewMasterLanguagesList creates a new master languages list
func NewMasterLanguagesList() *MasterLanguagesList {
return &MasterLanguagesList{
languages: make(map[int32]*Language),
nameIndex: make(map[string]*Language),
}
}
// Clear removes all languages from the list
func (mll *MasterLanguagesList) Clear() {
mll.mutex.Lock()
defer mll.mutex.Unlock()
mll.languages = make(map[int32]*Language)
mll.nameIndex = make(map[string]*Language)
}
// Size returns the number of languages in the list
func (mll *MasterLanguagesList) Size() int32 {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return int32(len(mll.languages))
}
// AddLanguage adds a new language to the master list
func (mll *MasterLanguagesList) AddLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
if !language.IsValid() {
return fmt.Errorf("language is not valid: %s", language.String())
}
mll.mutex.Lock()
defer mll.mutex.Unlock()
// Check for duplicate ID
if _, exists := mll.languages[language.GetID()]; exists {
return fmt.Errorf("language with ID %d already exists", language.GetID())
}
// Check for duplicate name
name := language.GetName()
if _, exists := mll.nameIndex[name]; exists {
return fmt.Errorf("language with name '%s' already exists", name)
}
// Add to both indexes
mll.languages[language.GetID()] = language
mll.nameIndex[name] = language
return nil
}
// GetLanguage retrieves a language by ID
func (mll *MasterLanguagesList) GetLanguage(id int32) *Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return mll.languages[id]
}
// GetLanguageByName retrieves a language by name
func (mll *MasterLanguagesList) GetLanguageByName(name string) *Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
return mll.nameIndex[name]
}
// GetAllLanguages returns a copy of all languages
func (mll *MasterLanguagesList) GetAllLanguages() []*Language {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
result := make([]*Language, 0, len(mll.languages))
for _, lang := range mll.languages {
result = append(result, lang)
}
return result
}
// HasLanguage checks if a language exists by ID
func (mll *MasterLanguagesList) HasLanguage(id int32) bool {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
_, exists := mll.languages[id]
return exists
}
// HasLanguageByName checks if a language exists by name
func (mll *MasterLanguagesList) HasLanguageByName(name string) bool {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
_, exists := mll.nameIndex[name]
return exists
}
// RemoveLanguage removes a language by ID
func (mll *MasterLanguagesList) RemoveLanguage(id int32) bool {
mll.mutex.Lock()
defer mll.mutex.Unlock()
language, exists := mll.languages[id]
if !exists {
return false
}
// Remove from both indexes
delete(mll.languages, id)
delete(mll.nameIndex, language.GetName())
return true
}
// UpdateLanguage updates an existing language
func (mll *MasterLanguagesList) UpdateLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
if !language.IsValid() {
return fmt.Errorf("language is not valid: %s", language.String())
}
mll.mutex.Lock()
defer mll.mutex.Unlock()
id := language.GetID()
oldLanguage, exists := mll.languages[id]
if !exists {
return fmt.Errorf("language with ID %d does not exist", id)
}
// Remove old name index if name changed
oldName := oldLanguage.GetName()
newName := language.GetName()
if oldName != newName {
delete(mll.nameIndex, oldName)
// Check for name conflicts
if _, exists := mll.nameIndex[newName]; exists {
return fmt.Errorf("language with name '%s' already exists", newName)
}
mll.nameIndex[newName] = language
}
// Update language
mll.languages[id] = language
return nil
}
// GetLanguageNames returns all language names
func (mll *MasterLanguagesList) GetLanguageNames() []string {
mll.mutex.RLock()
defer mll.mutex.RUnlock()
names := make([]string, 0, len(mll.nameIndex))
for name := range mll.nameIndex {
names = append(names, name)
}
return names
}
// PlayerLanguagesList manages languages known by a specific player
type PlayerLanguagesList struct {
languages map[int32]*Language // Player's languages indexed by ID
nameIndex map[string]*Language // Player's languages indexed by name
mutex sync.RWMutex // Thread safety
}
// NewPlayerLanguagesList creates a new player languages list
func NewPlayerLanguagesList() *PlayerLanguagesList {
return &PlayerLanguagesList{
languages: make(map[int32]*Language),
nameIndex: make(map[string]*Language),
}
}
// Clear removes all languages from the player's list
func (pll *PlayerLanguagesList) Clear() {
pll.mutex.Lock()
defer pll.mutex.Unlock()
pll.languages = make(map[int32]*Language)
pll.nameIndex = make(map[string]*Language)
}
// Add adds a language to the player's known languages
func (pll *PlayerLanguagesList) Add(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
pll.mutex.Lock()
defer pll.mutex.Unlock()
id := language.GetID()
name := language.GetName()
// Check if already known
if _, exists := pll.languages[id]; exists {
return fmt.Errorf("player already knows language with ID %d", id)
}
// Check player language limit
if len(pll.languages) >= MaxLanguagesPerPlayer {
return fmt.Errorf("player has reached maximum language limit (%d)", MaxLanguagesPerPlayer)
}
// Add to both indexes
pll.languages[id] = language
pll.nameIndex[name] = language
return nil
}
// GetLanguage retrieves a language the player knows by ID
func (pll *PlayerLanguagesList) GetLanguage(id int32) *Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return pll.languages[id]
}
// GetLanguageByName retrieves a language the player knows by name
func (pll *PlayerLanguagesList) GetLanguageByName(name string) *Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return pll.nameIndex[name]
}
// GetAllLanguages returns a copy of all languages the player knows
func (pll *PlayerLanguagesList) GetAllLanguages() []*Language {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
result := make([]*Language, 0, len(pll.languages))
for _, lang := range pll.languages {
result = append(result, lang)
}
return result
}
// HasLanguage checks if the player knows a language by ID
func (pll *PlayerLanguagesList) HasLanguage(id int32) bool {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
_, exists := pll.languages[id]
return exists
}
// HasLanguageByName checks if the player knows a language by name
func (pll *PlayerLanguagesList) HasLanguageByName(name string) bool {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
_, exists := pll.nameIndex[name]
return exists
}
// RemoveLanguage removes a language from the player's known languages
func (pll *PlayerLanguagesList) RemoveLanguage(id int32) bool {
pll.mutex.Lock()
defer pll.mutex.Unlock()
language, exists := pll.languages[id]
if !exists {
return false
}
// Remove from both indexes
delete(pll.languages, id)
delete(pll.nameIndex, language.GetName())
return true
}
// Size returns the number of languages the player knows
func (pll *PlayerLanguagesList) Size() int32 {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
return int32(len(pll.languages))
}
// GetLanguageIDs returns all language IDs the player knows
func (pll *PlayerLanguagesList) GetLanguageIDs() []int32 {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
ids := make([]int32, 0, len(pll.languages))
for id := range pll.languages {
ids = append(ids, id)
}
return ids
}
// GetLanguageNames returns all language names the player knows
func (pll *PlayerLanguagesList) GetLanguageNames() []string {
pll.mutex.RLock()
defer pll.mutex.RUnlock()
names := make([]string, 0, len(pll.nameIndex))
for name := range pll.nameIndex {
names = append(names, name)
}
return names
}
// LanguageStatistics contains language system statistics
type LanguageStatistics struct {
TotalLanguages int `json:"total_languages"`
PlayersWithLanguages int `json:"players_with_languages"`
LanguageUsageCount map[int32]int64 `json:"language_usage_count"`
LanguageLookups int64 `json:"language_lookups"`
LanguagesByName map[string]int32 `json:"languages_by_name"`
}
// LanguageManager provides high-level management of the language system
type LanguageManager struct {
masterLanguagesList *MasterLanguagesList
database Database
logger Logger
languageLookups int64
playersWithLanguages int64
languageUsageCount map[int32]int64 // Language ID -> usage count
mutex sync.RWMutex
}
// NewLanguageManager creates a new language manager
func NewLanguageManager(database Database, logger Logger) *LanguageManager {
return &LanguageManager{
masterLanguagesList: NewMasterLanguagesList(),
database: database,
logger: logger,
languageUsageCount: make(map[int32]int64),
}
}
// Initialize loads languages from database
func (lm *LanguageManager) Initialize() error {
if lm.logger != nil {
lm.logger.LogInfo("Initializing language manager...")
}
if lm.database == nil {
if lm.logger != nil {
lm.logger.LogWarning("No database provided, starting with empty language list")
}
return nil
}
// Load languages from database
languages, err := lm.database.LoadAllLanguages()
if err != nil {
return fmt.Errorf("failed to load languages from database: %w", err)
}
for _, language := range languages {
if err := lm.masterLanguagesList.AddLanguage(language); err != nil {
if lm.logger != nil {
lm.logger.LogError("Failed to add language %d (%s): %v", language.GetID(), language.GetName(), err)
}
}
}
if lm.logger != nil {
lm.logger.LogInfo("Loaded %d languages from database", len(languages))
}
return nil
}
// GetMasterLanguagesList returns the master language list
func (lm *LanguageManager) GetMasterLanguagesList() *MasterLanguagesList {
return lm.masterLanguagesList
}
// CreatePlayerLanguagesList creates a new player language list
func (lm *LanguageManager) CreatePlayerLanguagesList() *PlayerLanguagesList {
lm.mutex.Lock()
lm.playersWithLanguages++
lm.mutex.Unlock()
return NewPlayerLanguagesList()
}
// GetLanguage returns a language by ID
func (lm *LanguageManager) GetLanguage(id int32) *Language {
lm.mutex.Lock()
lm.languageLookups++
lm.mutex.Unlock()
return lm.masterLanguagesList.GetLanguage(id)
}
// GetLanguageByName returns a language by name
func (lm *LanguageManager) GetLanguageByName(name string) *Language {
lm.mutex.Lock()
lm.languageLookups++
lm.mutex.Unlock()
return lm.masterLanguagesList.GetLanguageByName(name)
}
// AddLanguage adds a new language to the system
func (lm *LanguageManager) AddLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
// Add to master list
if err := lm.masterLanguagesList.AddLanguage(language); err != nil {
return fmt.Errorf("failed to add language to master list: %w", err)
}
// Save to database if available
if lm.database != nil {
if err := lm.database.SaveLanguage(language); err != nil {
// Remove from master list if database save failed
lm.masterLanguagesList.RemoveLanguage(language.GetID())
return fmt.Errorf("failed to save language to database: %w", err)
}
}
if lm.logger != nil {
lm.logger.LogInfo("Added language %d: %s", language.GetID(), language.GetName())
}
return nil
}
// UpdateLanguage updates an existing language
func (lm *LanguageManager) UpdateLanguage(language *Language) error {
if language == nil {
return fmt.Errorf("language cannot be nil")
}
// Update in master list
if err := lm.masterLanguagesList.UpdateLanguage(language); err != nil {
return fmt.Errorf("failed to update language in master list: %w", err)
}
// Save to database if available
if lm.database != nil {
if err := lm.database.SaveLanguage(language); err != nil {
return fmt.Errorf("failed to save language to database: %w", err)
}
}
if lm.logger != nil {
lm.logger.LogInfo("Updated language %d: %s", language.GetID(), language.GetName())
}
return nil
}
// RemoveLanguage removes a language from the system
func (lm *LanguageManager) RemoveLanguage(id int32) error {
// Check if language exists
if !lm.masterLanguagesList.HasLanguage(id) {
return fmt.Errorf("language with ID %d does not exist", id)
}
// Remove from database first if available
if lm.database != nil {
if err := lm.database.DeleteLanguage(id); err != nil {
return fmt.Errorf("failed to delete language from database: %w", err)
}
}
// Remove from master list
if !lm.masterLanguagesList.RemoveLanguage(id) {
return fmt.Errorf("failed to remove language from master list")
}
if lm.logger != nil {
lm.logger.LogInfo("Removed language %d", id)
}
return nil
}
// RecordLanguageUsage records language usage for statistics
func (lm *LanguageManager) RecordLanguageUsage(languageID int32) {
lm.mutex.Lock()
defer lm.mutex.Unlock()
lm.languageUsageCount[languageID]++
}
// GetStatistics returns language system statistics
func (lm *LanguageManager) GetStatistics() *LanguageStatistics {
lm.mutex.RLock()
defer lm.mutex.RUnlock()
// Create language name mapping
languagesByName := make(map[string]int32)
allLanguages := lm.masterLanguagesList.GetAllLanguages()
for _, lang := range allLanguages {
languagesByName[lang.GetName()] = lang.GetID()
}
// Copy usage count
usageCount := make(map[int32]int64)
for id, count := range lm.languageUsageCount {
usageCount[id] = count
}
return &LanguageStatistics{
TotalLanguages: len(allLanguages),
PlayersWithLanguages: int(lm.playersWithLanguages),
LanguageUsageCount: usageCount,
LanguageLookups: lm.languageLookups,
LanguagesByName: languagesByName,
}
}
// ResetStatistics resets all statistics
func (lm *LanguageManager) ResetStatistics() {
lm.mutex.Lock()
defer lm.mutex.Unlock()
lm.languageLookups = 0
lm.playersWithLanguages = 0
lm.languageUsageCount = make(map[int32]int64)
}
// ValidateAllLanguages validates all languages in the system
func (lm *LanguageManager) ValidateAllLanguages() []string {
allLanguages := lm.masterLanguagesList.GetAllLanguages()
var issues []string
for _, lang := range allLanguages {
if !lang.IsValid() {
issues = append(issues, fmt.Sprintf("Language %d (%s) is invalid", lang.GetID(), lang.GetName()))
}
}
return issues
}
// ReloadFromDatabase reloads all languages from database
func (lm *LanguageManager) ReloadFromDatabase() error {
if lm.database == nil {
return fmt.Errorf("no database available")
}
// Clear current languages
lm.masterLanguagesList.Clear()
// Reload from database
return lm.Initialize()
}
// GetLanguageCount returns the total number of languages
func (lm *LanguageManager) GetLanguageCount() int32 {
return lm.masterLanguagesList.Size()
}
// ProcessCommand handles language-related commands
func (lm *LanguageManager) ProcessCommand(command string, args []string) (string, error) {
switch command {
case "stats":
return lm.handleStatsCommand(args)
case "validate":
return lm.handleValidateCommand(args)
case "list":
return lm.handleListCommand(args)
case "info":
return lm.handleInfoCommand(args)
case "reload":
return lm.handleReloadCommand(args)
case "add":
return lm.handleAddCommand(args)
case "remove":
return lm.handleRemoveCommand(args)
case "search":
return lm.handleSearchCommand(args)
default:
return "", fmt.Errorf("unknown language command: %s", command)
}
}
// handleStatsCommand shows language system statistics
func (lm *LanguageManager) handleStatsCommand(args []string) (string, error) {
stats := lm.GetStatistics()
result := "Language System Statistics:\n"
result += fmt.Sprintf("Total Languages: %d\n", stats.TotalLanguages)
result += fmt.Sprintf("Players with Languages: %d\n", stats.PlayersWithLanguages)
result += fmt.Sprintf("Language Lookups: %d\n", stats.LanguageLookups)
if len(stats.LanguageUsageCount) > 0 {
result += "\nMost Used Languages:\n"
// Show top 5 most used languages
count := 0
for langID, usage := range stats.LanguageUsageCount {
if count >= 5 {
break
}
lang := lm.GetLanguage(langID)
name := "Unknown"
if lang != nil {
name = lang.GetName()
}
result += fmt.Sprintf(" %s (ID: %d): %d uses\n", name, langID, usage)
count++
}
}
return result, nil
}
// handleValidateCommand validates all languages
func (lm *LanguageManager) handleValidateCommand(args []string) (string, error) {
issues := lm.ValidateAllLanguages()
if len(issues) == 0 {
return "All languages are valid.", nil
}
result := fmt.Sprintf("Found %d issues with languages:\n", len(issues))
for i, issue := range issues {
if i >= 10 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf("%d. %s\n", i+1, issue)
}
return result, nil
}
// handleListCommand lists languages
func (lm *LanguageManager) handleListCommand(args []string) (string, error) {
languages := lm.masterLanguagesList.GetAllLanguages()
if len(languages) == 0 {
return "No languages loaded.", nil
}
result := fmt.Sprintf("Languages (%d):\n", len(languages))
count := 0
for _, language := range languages {
if count >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s\n", language.GetID(), language.GetName())
count++
}
return result, nil
}
// handleInfoCommand shows information about a specific language
func (lm *LanguageManager) handleInfoCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("language ID or name required")
}
var language *Language
// Try to parse as ID first
var languageID int32
if _, err := fmt.Sscanf(args[0], "%d", &languageID); err == nil {
language = lm.GetLanguage(languageID)
} else {
// Try as name
language = lm.GetLanguageByName(args[0])
}
if language == nil {
return fmt.Sprintf("Language '%s' not found.", args[0]), nil
}
result := "Language Information:\n"
result += fmt.Sprintf("ID: %d\n", language.GetID())
result += fmt.Sprintf("Name: %s\n", language.GetName())
result += fmt.Sprintf("Save Needed: %v\n", language.GetSaveNeeded())
// Show usage statistics if available
lm.mutex.RLock()
if usage, exists := lm.languageUsageCount[language.GetID()]; exists {
result += fmt.Sprintf("Usage Count: %d\n", usage)
} else {
result += "Usage Count: 0\n"
}
lm.mutex.RUnlock()
return result, nil
}
// handleReloadCommand reloads languages from database
func (lm *LanguageManager) handleReloadCommand(args []string) (string, error) {
if err := lm.ReloadFromDatabase(); err != nil {
return "", fmt.Errorf("failed to reload languages: %w", err)
}
count := lm.GetLanguageCount()
return fmt.Sprintf("Successfully reloaded %d languages from database.", count), nil
}
// handleAddCommand adds a new language
func (lm *LanguageManager) handleAddCommand(args []string) (string, error) {
if len(args) < 2 {
return "", fmt.Errorf("usage: add <id> <name>")
}
var id int32
if _, err := fmt.Sscanf(args[0], "%d", &id); err != nil {
return "", fmt.Errorf("invalid language ID: %s", args[0])
}
name := args[1]
language := NewLanguage()
language.SetID(id)
language.SetName(name)
language.SetSaveNeeded(true)
if err := lm.AddLanguage(language); err != nil {
return "", fmt.Errorf("failed to add language: %w", err)
}
return fmt.Sprintf("Successfully added language %d: %s", id, name), nil
}
// handleRemoveCommand removes a language
func (lm *LanguageManager) handleRemoveCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("language ID required")
}
var id int32
if _, err := fmt.Sscanf(args[0], "%d", &id); err != nil {
return "", fmt.Errorf("invalid language ID: %s", args[0])
}
if err := lm.RemoveLanguage(id); err != nil {
return "", fmt.Errorf("failed to remove language: %w", err)
}
return fmt.Sprintf("Successfully removed language %d", id), nil
}
// handleSearchCommand searches for languages by name
func (lm *LanguageManager) handleSearchCommand(args []string) (string, error) {
if len(args) == 0 {
return "", fmt.Errorf("search term required")
}
searchTerm := args[0]
languages := lm.masterLanguagesList.GetAllLanguages()
var results []*Language
// Search by name (case-insensitive partial match)
for _, language := range languages {
if lm.contains(language.GetName(), searchTerm) {
results = append(results, language)
}
}
if len(results) == 0 {
return fmt.Sprintf("No languages found matching '%s'.", searchTerm), nil
}
result := fmt.Sprintf("Found %d languages matching '%s':\n", len(results), searchTerm)
for i, language := range results {
if i >= 20 { // Limit output
result += "... (and more)\n"
break
}
result += fmt.Sprintf(" %d: %s\n", language.GetID(), language.GetName())
}
return result, nil
}
// contains checks if a string contains a substring (case-insensitive)
func (lm *LanguageManager) contains(str, substr string) bool {
if len(substr) == 0 {
return true
}
if len(str) < len(substr) {
return false
}
// Convert to lowercase for case-insensitive comparison
strLower := make([]byte, len(str))
substrLower := make([]byte, len(substr))
for i := 0; i < len(str); i++ {
if str[i] >= 'A' && str[i] <= 'Z' {
strLower[i] = str[i] + 32
} else {
strLower[i] = str[i]
}
}
for i := 0; i < len(substr); i++ {
if substr[i] >= 'A' && substr[i] <= 'Z' {
substrLower[i] = substr[i] + 32
} else {
substrLower[i] = substr[i]
}
}
for i := 0; i <= len(strLower)-len(substrLower); i++ {
match := true
for j := 0; j < len(substrLower); j++ {
if strLower[i+j] != substrLower[j] {
match = false
break
}
}
if match {
return true
}
}
return false
}
// Shutdown gracefully shuts down the manager
func (lm *LanguageManager) Shutdown() {
if lm.logger != nil {
lm.logger.LogInfo("Shutting down language manager...")
}
// Clear languages
lm.masterLanguagesList.Clear()
}
// PlayerLanguageAdapter provides language functionality for players
type PlayerLanguageAdapter struct {
languages *PlayerLanguagesList
primaryLang int32
manager *LanguageManager
logger Logger
}
// NewPlayerLanguageAdapter creates a new player language adapter
func NewPlayerLanguageAdapter(manager *LanguageManager, logger Logger) *PlayerLanguageAdapter {
adapter := &PlayerLanguageAdapter{
languages: manager.CreatePlayerLanguagesList(),
primaryLang: LanguageIDCommon, // Default to common
manager: manager,
logger: logger,
}
// Ensure common language is always known
if commonLang := manager.GetLanguage(LanguageIDCommon); commonLang != nil {
adapter.languages.Add(commonLang.Copy())
}
return adapter
}
// GetKnownLanguages returns the player's known languages
func (pla *PlayerLanguageAdapter) GetKnownLanguages() *PlayerLanguagesList {
return pla.languages
}
// KnowsLanguage checks if the player knows a specific language
func (pla *PlayerLanguageAdapter) KnowsLanguage(languageID int32) bool {
return pla.languages.HasLanguage(languageID)
}
// GetPrimaryLanguage returns the player's primary language
func (pla *PlayerLanguageAdapter) GetPrimaryLanguage() int32 {
return pla.primaryLang
}
// SetPrimaryLanguage sets the player's primary language
func (pla *PlayerLanguageAdapter) SetPrimaryLanguage(languageID int32) {
// Only allow setting to a known language
if pla.languages.HasLanguage(languageID) {
pla.primaryLang = languageID
if pla.logger != nil {
lang := pla.manager.GetLanguage(languageID)
langName := "Unknown"
if lang != nil {
langName = lang.GetName()
}
pla.logger.LogDebug("Player set primary language to %s (%d)", langName, languageID)
}
}
}
// CanUnderstand checks if the player can understand a language
func (pla *PlayerLanguageAdapter) CanUnderstand(languageID int32) bool {
// Common language is always understood
if languageID == LanguageIDCommon {
return true
}
// Check if player knows the language
return pla.languages.HasLanguage(languageID)
}
// LearnLanguage teaches the player a new language
func (pla *PlayerLanguageAdapter) LearnLanguage(languageID int32) error {
// Get the language from master list
language := pla.manager.GetLanguage(languageID)
if language == nil {
return fmt.Errorf("language with ID %d does not exist", languageID)
}
// Check if already known
if pla.languages.HasLanguage(languageID) {
return fmt.Errorf("player already knows language %s", language.GetName())
}
// Create a copy for the player
playerLang := language.Copy()
playerLang.SetSaveNeeded(true)
// Add to player's languages
if err := pla.languages.Add(playerLang); err != nil {
return fmt.Errorf("failed to add language to player: %w", err)
}
// Record usage statistics
pla.manager.RecordLanguageUsage(languageID)
if pla.logger != nil {
pla.logger.LogInfo("Player learned language %s (%d)", language.GetName(), languageID)
}
return nil
}
// ForgetLanguage makes the player forget a language
func (pla *PlayerLanguageAdapter) ForgetLanguage(languageID int32) error {
// Cannot forget common language
if languageID == LanguageIDCommon {
return fmt.Errorf("cannot forget common language")
}
// Check if player knows the language
if !pla.languages.HasLanguage(languageID) {
return fmt.Errorf("player does not know language %d", languageID)
}
// Get language name for logging
language := pla.manager.GetLanguage(languageID)
langName := "Unknown"
if language != nil {
langName = language.GetName()
}
// Remove from player's languages
if !pla.languages.RemoveLanguage(languageID) {
return fmt.Errorf("failed to remove language from player")
}
// Reset primary language if this was it
if pla.primaryLang == languageID {
pla.primaryLang = LanguageIDCommon
}
if pla.logger != nil {
pla.logger.LogInfo("Player forgot language %s (%d)", langName, languageID)
}
return nil
}
// LoadPlayerLanguages loads the player's languages from database
func (pla *PlayerLanguageAdapter) LoadPlayerLanguages(database Database, playerID int32) error {
if database == nil {
return fmt.Errorf("database is nil")
}
languages, err := database.LoadPlayerLanguages(playerID)
if err != nil {
return fmt.Errorf("failed to load player languages: %w", err)
}
// Clear current languages
pla.languages.Clear()
// Add loaded languages
for _, lang := range languages {
if err := pla.languages.Add(lang); err != nil && pla.logger != nil {
pla.logger.LogWarning("Failed to add loaded language %d: %v", lang.GetID(), err)
}
}
// Ensure player knows common language
if !pla.languages.HasLanguage(LanguageIDCommon) {
commonLang := pla.manager.GetLanguage(LanguageIDCommon)
if commonLang != nil {
playerCommon := commonLang.Copy()
pla.languages.Add(playerCommon)
}
}
if pla.logger != nil {
pla.logger.LogDebug("Loaded %d languages for player", len(languages))
}
return nil
}
// SavePlayerLanguages saves the player's languages to database
func (pla *PlayerLanguageAdapter) SavePlayerLanguages(database Database, playerID int32) error {
if database == nil {
return fmt.Errorf("database is nil")
}
languages := pla.languages.GetAllLanguages()
// Save each language that needs saving
for _, lang := range languages {
if lang.GetSaveNeeded() {
if err := database.SavePlayerLanguage(playerID, lang.GetID()); err != nil {
return fmt.Errorf("failed to save player language %d: %w", lang.GetID(), err)
}
lang.SetSaveNeeded(false)
}
}
if pla.logger != nil {
pla.logger.LogDebug("Saved languages for player")
}
return nil
}
// ChatLanguageProcessor handles multilingual chat processing
type ChatLanguageProcessor struct {
manager *LanguageManager
logger Logger
}
// NewChatLanguageProcessor creates a new chat language processor
func NewChatLanguageProcessor(manager *LanguageManager, logger Logger) *ChatLanguageProcessor {
return &ChatLanguageProcessor{
manager: manager,
logger: logger,
}
}
// ProcessMessage processes a chat message in a specific language
func (clp *ChatLanguageProcessor) ProcessMessage(speaker Player, message string, languageID int32) (string, error) {
if speaker == nil {
return "", fmt.Errorf("speaker cannot be nil")
}
// Validate language exists
language := clp.manager.GetLanguage(languageID)
if language == nil {
return "", fmt.Errorf("language %d does not exist", languageID)
}
// Record language usage
clp.manager.RecordLanguageUsage(languageID)
return message, nil
}
// FilterMessage filters a message for a listener based on language comprehension
func (clp *ChatLanguageProcessor) FilterMessage(listener Player, message string, languageID int32) string {
if listener == nil {
return message
}
// Common language is always understood
if languageID == LanguageIDCommon {
return message
}
// For now, we'll always return the message since we can't check language knowledge
// This would need integration with a PlayerLanguageAdapter or similar
return message
}
// GetLanguageSkramble scrambles a message based on comprehension level
func (clp *ChatLanguageProcessor) GetLanguageSkramble(message string, comprehension float32) string {
if comprehension >= 1.0 {
return message
}
if comprehension <= 0.0 {
// Complete scramble - replace with gibberish
runes := []rune(message)
scrambled := make([]rune, len(runes))
for i, r := range runes {
if r == ' ' {
scrambled[i] = ' '
} else if r >= 'a' && r <= 'z' {
scrambled[i] = 'a' + rune((int(r-'a')+7)%26)
} else if r >= 'A' && r <= 'Z' {
scrambled[i] = 'A' + rune((int(r-'A')+7)%26)
} else {
scrambled[i] = r
}
}
return string(scrambled)
}
// Partial comprehension - scramble some words
// This is a simplified implementation
return message
}
// LanguageEventAdapter handles language-related events
type LanguageEventAdapter struct {
handler LanguageHandler
logger Logger
}
// NewLanguageEventAdapter creates a new language event adapter
func NewLanguageEventAdapter(handler LanguageHandler, logger Logger) *LanguageEventAdapter {
return &LanguageEventAdapter{
handler: handler,
logger: logger,
}
}
// ProcessLanguageEvent processes a language-related event
func (lea *LanguageEventAdapter) ProcessLanguageEvent(eventType string, player Player, languageID int32, data any) {
if lea.handler == nil {
return
}
switch eventType {
case "language_learned":
if err := lea.handler.OnLanguageLearned(player, languageID); err != nil && lea.logger != nil {
lea.logger.LogError("Language learned handler failed: %v", err)
}
case "language_forgotten":
if err := lea.handler.OnLanguageForgotten(player, languageID); err != nil && lea.logger != nil {
lea.logger.LogError("Language forgotten handler failed: %v", err)
}
case "language_used":
if message, ok := data.(string); ok {
if err := lea.handler.OnLanguageUsed(player, languageID, message); err != nil && lea.logger != nil {
lea.logger.LogError("Language used handler failed: %v", err)
}
}
}
}
// LanguagePacketBuilder handles language-related packet building
type LanguagePacketBuilder struct {
logger Logger
}
// NewLanguagePacketBuilder creates a new language packet builder
func NewLanguagePacketBuilder(logger Logger) *LanguagePacketBuilder {
return &LanguagePacketBuilder{
logger: logger,
}
}
// BuildLanguagesPacket builds the WS_Languages packet for a player
// This packet sends the player's known languages and current language to the client
func (lpb *LanguagePacketBuilder) BuildLanguagesPacket(languages []*Language, currentLanguageID int32) map[string]any {
data := make(map[string]any)
// Number of languages the player knows
data["num_languages"] = int8(len(languages))
// Array of language IDs
languageIDs := make([]int8, len(languages))
for i, lang := range languages {
languageIDs[i] = int8(lang.GetID())
}
data["language_array"] = languageIDs
// Unknown field (from packet structure)
data["unknown"] = int8(0)
// Current active language
data["current_language"] = int8(currentLanguageID)
if lpb.logger != nil {
lpb.logger.LogDebug("Built Languages packet: %d languages, current: %d", len(languages), currentLanguageID)
}
return data
}
// BuildHearChatPacket builds a HearChat packet with language support
// This is used when players speak in different languages
func (lpb *LanguagePacketBuilder) BuildHearChatPacket(speakerName string, message string, languageID int32, channel int16) map[string]any {
data := make(map[string]any)
// Basic chat data
data["from"] = speakerName
data["message"] = message
data["language"] = int8(languageID)
data["channel"] = channel
if lpb.logger != nil {
lpb.logger.LogDebug("Built HearChat packet: %s speaking %s in language %d", speakerName, message, languageID)
}
return data
}
// BuildPlayFlavorPacket builds a PlayFlavor packet with language support
// This is used for emotes and flavor text that can be in different languages
func (lpb *LanguagePacketBuilder) BuildPlayFlavorPacket(targetName string, message string, languageID int32, emoteID int32) map[string]any {
data := make(map[string]any)
data["target"] = targetName
data["message"] = message
data["language"] = int8(languageID)
data["emote"] = emoteID
if lpb.logger != nil {
lpb.logger.LogDebug("Built PlayFlavor packet: %s with message %s in language %d", targetName, message, languageID)
}
return data
}
// BuildUpdateSkillBookPacket builds skill book update packet with language information
// This is used when showing skill information that may include language requirements
func (lpb *LanguagePacketBuilder) BuildUpdateSkillBookPacket(skills []SkillInfo) map[string]any {
data := make(map[string]any)
// This would be expanded based on the skill book packet structure
// For now, we'll focus on language-related fields
for _, skill := range skills {
// Mark language skills appropriately
if skill.IsLanguageSkill {
data["language_unknown"] = int8(0) // 0 = known, 1 = unknown
}
}
if lpb.logger != nil {
lpb.logger.LogDebug("Built UpdateSkillBook packet with %d skills", len(skills))
}
return data
}
// SkillInfo represents skill information for packet building
type SkillInfo struct {
ID int32
Name string
IsLanguageSkill bool
Value int16
MaxValue int16
}
// LanguagePacketHandler handles incoming language-related packets from clients
type LanguagePacketHandler struct {
manager *LanguageManager
builder *LanguagePacketBuilder
logger Logger
}
// NewLanguagePacketHandler creates a new language packet handler
func NewLanguagePacketHandler(manager *LanguageManager, builder *LanguagePacketBuilder, logger Logger) *LanguagePacketHandler {
return &LanguagePacketHandler{
manager: manager,
builder: builder,
logger: logger,
}
}
// HandleLanguageChangeRequest handles requests to change the player's active language
func (lph *LanguagePacketHandler) HandleLanguageChangeRequest(playerID int32, languageID int32) (map[string]any, error) {
// Validate language exists
language := lph.manager.GetLanguage(languageID)
if language == nil {
return nil, fmt.Errorf("language %d does not exist", languageID)
}
// Record language usage
lph.manager.RecordLanguageUsage(languageID)
if lph.logger != nil {
lph.logger.LogInfo("Player %d changed to language %d (%s)", playerID, languageID, language.GetName())
}
// Build response packet - this would typically be sent back to confirm the change
// For now, we'll return the Languages packet with updated current language
return lph.buildLanguagesResponsePacket(playerID, languageID)
}
// buildLanguagesResponsePacket builds a response packet for language changes
func (lph *LanguagePacketHandler) buildLanguagesResponsePacket(playerID int32, currentLanguageID int32) (map[string]any, error) {
// In a real implementation, you would get the player's known languages from their PlayerLanguageAdapter
// For now, we'll return a basic response with common language
languages := []*Language{}
// Add common language (always known)
if commonLang := lph.manager.GetLanguage(LanguageIDCommon); commonLang != nil {
languages = append(languages, commonLang)
}
// Add the requested language if it exists and is different from common
if currentLanguageID != LanguageIDCommon {
if requestedLang := lph.manager.GetLanguage(currentLanguageID); requestedLang != nil {
languages = append(languages, requestedLang)
}
}
return lph.builder.BuildLanguagesPacket(languages, currentLanguageID), nil
}
// HandleChatMessage handles chat messages with language support
func (lph *LanguagePacketHandler) HandleChatMessage(speakerName string, message string, languageID int32, channel int16, listeners []Player) []map[string]any {
var packets []map[string]any
// Process message for each listener
for range listeners {
// Check if listener can understand the language
processedMessage := message
// In a real implementation, you would check if the listener knows the language
// and potentially scramble the message if they don't understand it
// For now, we'll just pass through the message
// Build packet for this listener
packet := lph.builder.BuildHearChatPacket(speakerName, processedMessage, languageID, channel)
packets = append(packets, packet)
}
// Record language usage
lph.manager.RecordLanguageUsage(languageID)
if lph.logger != nil {
lph.logger.LogDebug("Processed chat message for %d listeners in language %d", len(listeners), languageID)
}
return packets
}