modernize appearance package
This commit is contained in:
parent
c637793dee
commit
1fc81eea95
189
internal/appearances/appearance.go
Normal file
189
internal/appearances/appearance.go
Normal file
@ -0,0 +1,189 @@
|
|||||||
|
package appearances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Appearance represents a single appearance with ID, name, and client version requirements
|
||||||
|
type Appearance struct {
|
||||||
|
ID int32 `json:"id"` // Appearance ID
|
||||||
|
Name string `json:"name"` // Appearance name
|
||||||
|
MinClient int16 `json:"min_client"` // Minimum client version required
|
||||||
|
|
||||||
|
db *database.Database `json:"-"` // Database connection
|
||||||
|
isNew bool `json:"-"` // Whether this is a new appearance
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates a new appearance with the given database
|
||||||
|
func New(db *database.Database) *Appearance {
|
||||||
|
return &Appearance{
|
||||||
|
db: db,
|
||||||
|
isNew: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewWithData creates a new appearance with data
|
||||||
|
func NewWithData(id int32, name string, minClientVersion int16, db *database.Database) *Appearance {
|
||||||
|
return &Appearance{
|
||||||
|
ID: id,
|
||||||
|
Name: name,
|
||||||
|
MinClient: minClientVersion,
|
||||||
|
db: db,
|
||||||
|
isNew: true,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load loads an appearance by ID from the database
|
||||||
|
func Load(db *database.Database, id int32) (*Appearance, error) {
|
||||||
|
appearance := &Appearance{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT appearance_id, name, min_client_version FROM appearances WHERE appearance_id = ?`
|
||||||
|
row := db.QueryRow(query, id)
|
||||||
|
|
||||||
|
err := row.Scan(&appearance.ID, &appearance.Name, &appearance.MinClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to load appearance %d: %w", id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return appearance, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetID returns the appearance ID (implements Identifiable interface)
|
||||||
|
func (a *Appearance) GetID() int32 {
|
||||||
|
return a.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetName returns the appearance name
|
||||||
|
func (a *Appearance) GetName() string {
|
||||||
|
return a.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetMinClientVersion returns the minimum client version required
|
||||||
|
func (a *Appearance) GetMinClientVersion() int16 {
|
||||||
|
return a.MinClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNameString returns the name as a string (alias for GetName for C++ compatibility)
|
||||||
|
func (a *Appearance) GetNameString() string {
|
||||||
|
return a.Name
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetName sets the appearance name
|
||||||
|
func (a *Appearance) SetName(name string) {
|
||||||
|
a.Name = name
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetMinClientVersion sets the minimum client version
|
||||||
|
func (a *Appearance) SetMinClientVersion(version int16) {
|
||||||
|
a.MinClient = version
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCompatibleWithClient returns true if the appearance is compatible with the given client version
|
||||||
|
func (a *Appearance) IsCompatibleWithClient(clientVersion int16) bool {
|
||||||
|
return clientVersion >= a.MinClient
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNew returns true if this is a new appearance not yet saved to database
|
||||||
|
func (a *Appearance) IsNew() bool {
|
||||||
|
return a.isNew
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save saves the appearance to the database
|
||||||
|
func (a *Appearance) Save() error {
|
||||||
|
if a.db == nil {
|
||||||
|
return fmt.Errorf("no database connection available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.isNew {
|
||||||
|
return a.insert()
|
||||||
|
}
|
||||||
|
return a.update()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete removes the appearance from the database
|
||||||
|
func (a *Appearance) Delete() error {
|
||||||
|
if a.db == nil {
|
||||||
|
return fmt.Errorf("no database connection available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.isNew {
|
||||||
|
return fmt.Errorf("cannot delete unsaved appearance")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `DELETE FROM appearances WHERE appearance_id = ?`
|
||||||
|
_, err := a.db.Exec(query, a.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete appearance %d: %w", a.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reload reloads the appearance data from the database
|
||||||
|
func (a *Appearance) Reload() error {
|
||||||
|
if a.db == nil {
|
||||||
|
return fmt.Errorf("no database connection available")
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.isNew {
|
||||||
|
return fmt.Errorf("cannot reload unsaved appearance")
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `SELECT name, min_client_version FROM appearances WHERE appearance_id = ?`
|
||||||
|
row := a.db.QueryRow(query, a.ID)
|
||||||
|
|
||||||
|
err := row.Scan(&a.Name, &a.MinClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to reload appearance %d: %w", a.ID, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clone creates a copy of the appearance
|
||||||
|
func (a *Appearance) Clone() *Appearance {
|
||||||
|
return &Appearance{
|
||||||
|
ID: a.ID,
|
||||||
|
Name: a.Name,
|
||||||
|
MinClient: a.MinClient,
|
||||||
|
db: a.db,
|
||||||
|
isNew: true, // Clone is always new
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert inserts a new appearance into the database
|
||||||
|
func (a *Appearance) insert() error {
|
||||||
|
query := `INSERT INTO appearances (appearance_id, name, min_client_version) VALUES (?, ?, ?)`
|
||||||
|
_, err := a.db.Exec(query, a.ID, a.Name, a.MinClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert appearance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.isNew = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// update updates an existing appearance in the database
|
||||||
|
func (a *Appearance) update() error {
|
||||||
|
query := `UPDATE appearances SET name = ?, min_client_version = ? WHERE appearance_id = ?`
|
||||||
|
result, err := a.db.Exec(query, a.Name, a.MinClient, a.ID)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to update appearance: %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("appearance %d not found for update", a.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
221
internal/appearances/appearance_test.go
Normal file
221
internal/appearances/appearance_test.go
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
package appearances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNew(t *testing.T) {
|
||||||
|
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Test creating a new appearance
|
||||||
|
appearance := New(db)
|
||||||
|
if appearance == nil {
|
||||||
|
t.Fatal("New returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !appearance.IsNew() {
|
||||||
|
t.Error("New appearance should be marked as new")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test setting values
|
||||||
|
appearance.ID = 1001
|
||||||
|
appearance.Name = "Test Appearance"
|
||||||
|
appearance.MinClient = 1096
|
||||||
|
|
||||||
|
if appearance.GetID() != 1001 {
|
||||||
|
t.Errorf("Expected GetID() to return 1001, got %d", appearance.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
if appearance.GetName() != "Test Appearance" {
|
||||||
|
t.Errorf("Expected GetName() to return 'Test Appearance', got %s", appearance.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
if appearance.GetMinClientVersion() != 1096 {
|
||||||
|
t.Errorf("Expected GetMinClientVersion() to return 1096, got %d", appearance.GetMinClientVersion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNewWithData(t *testing.T) {
|
||||||
|
db, err := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create test database: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
appearance := NewWithData(100, "Human Male", 1096, db)
|
||||||
|
if appearance == nil {
|
||||||
|
t.Fatal("NewWithData returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if appearance.GetID() != 100 {
|
||||||
|
t.Errorf("Expected ID 100, got %d", appearance.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
if appearance.GetName() != "Human Male" {
|
||||||
|
t.Errorf("Expected name 'Human Male', got '%s'", appearance.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
if appearance.GetMinClientVersion() != 1096 {
|
||||||
|
t.Errorf("Expected min client 1096, got %d", appearance.GetMinClientVersion())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !appearance.IsNew() {
|
||||||
|
t.Error("NewWithData should create new appearance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppearanceGetters(t *testing.T) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
app := NewWithData(123, "Test Appearance", 1096, db)
|
||||||
|
|
||||||
|
if id := app.GetID(); id != 123 {
|
||||||
|
t.Errorf("GetID() = %v, want 123", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if name := app.GetName(); name != "Test Appearance" {
|
||||||
|
t.Errorf("GetName() = %v, want Test Appearance", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
if nameStr := app.GetNameString(); nameStr != "Test Appearance" {
|
||||||
|
t.Errorf("GetNameString() = %v, want Test Appearance", nameStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if minVer := app.GetMinClientVersion(); minVer != 1096 {
|
||||||
|
t.Errorf("GetMinClientVersion() = %v, want 1096", minVer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppearanceSetters(t *testing.T) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
app := NewWithData(100, "Original", 1000, db)
|
||||||
|
|
||||||
|
app.SetName("Modified Name")
|
||||||
|
if app.GetName() != "Modified Name" {
|
||||||
|
t.Errorf("SetName failed: got %v, want Modified Name", app.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
app.SetMinClientVersion(2000)
|
||||||
|
if app.GetMinClientVersion() != 2000 {
|
||||||
|
t.Errorf("SetMinClientVersion failed: got %v, want 2000", app.GetMinClientVersion())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIsCompatibleWithClient(t *testing.T) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
app := NewWithData(100, "Test", 1096, db)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
clientVersion int16
|
||||||
|
want bool
|
||||||
|
}{
|
||||||
|
{1095, false}, // Below minimum
|
||||||
|
{1096, true}, // Exact minimum
|
||||||
|
{1097, true}, // Above minimum
|
||||||
|
{2000, true}, // Well above minimum
|
||||||
|
{0, false}, // Zero version
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
if got := app.IsCompatibleWithClient(tt.clientVersion); got != tt.want {
|
||||||
|
t.Errorf("IsCompatibleWithClient(%v) = %v, want %v", tt.clientVersion, got, tt.want)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAppearanceClone(t *testing.T) {
|
||||||
|
db, _ := database.NewSQLite("file::memory:?mode=memory&cache=shared")
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
original := NewWithData(500, "Original Appearance", 1200, db)
|
||||||
|
clone := original.Clone()
|
||||||
|
|
||||||
|
if clone == nil {
|
||||||
|
t.Fatal("Clone returned nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clone == original {
|
||||||
|
t.Error("Clone returned same pointer as original")
|
||||||
|
}
|
||||||
|
|
||||||
|
if clone.GetID() != original.GetID() {
|
||||||
|
t.Errorf("Clone ID = %v, want %v", clone.GetID(), original.GetID())
|
||||||
|
}
|
||||||
|
|
||||||
|
if clone.GetName() != original.GetName() {
|
||||||
|
t.Errorf("Clone Name = %v, want %v", clone.GetName(), original.GetName())
|
||||||
|
}
|
||||||
|
|
||||||
|
if clone.GetMinClientVersion() != original.GetMinClientVersion() {
|
||||||
|
t.Errorf("Clone MinClientVersion = %v, want %v", clone.GetMinClientVersion(), original.GetMinClientVersion())
|
||||||
|
}
|
||||||
|
|
||||||
|
if !clone.IsNew() {
|
||||||
|
t.Error("Clone should always be marked as new")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify modification independence
|
||||||
|
clone.SetName("Modified Clone")
|
||||||
|
if original.GetName() == "Modified Clone" {
|
||||||
|
t.Error("Modifying clone affected original")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test appearance type functions
|
||||||
|
func TestGetAppearanceType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
typeName string
|
||||||
|
expected int8
|
||||||
|
}{
|
||||||
|
{"hair_color1", AppearanceHairColor1},
|
||||||
|
{"soga_hair_color1", AppearanceSOGAHairColor1},
|
||||||
|
{"skin_color", AppearanceSkinColor},
|
||||||
|
{"eye_color", AppearanceEyeColor},
|
||||||
|
{"unknown_type", -1},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.typeName, func(t *testing.T) {
|
||||||
|
result := GetAppearanceType(tt.typeName)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("GetAppearanceType(%q) = %d, want %d", tt.typeName, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGetAppearanceTypeName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
typeConst int8
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{AppearanceHairColor1, "hair_color1"},
|
||||||
|
{AppearanceSOGAHairColor1, "soga_hair_color1"},
|
||||||
|
{AppearanceSkinColor, "skin_color"},
|
||||||
|
{AppearanceEyeColor, "eye_color"},
|
||||||
|
{-1, "unknown"},
|
||||||
|
{100, "unknown"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run("", func(t *testing.T) {
|
||||||
|
result := GetAppearanceTypeName(tt.typeConst)
|
||||||
|
if result != tt.expected {
|
||||||
|
t.Errorf("GetAppearanceTypeName(%d) = %q, want %q", tt.typeConst, result, tt.expected)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -1,288 +0,0 @@
|
|||||||
package appearances
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Appearances manages a collection of appearance objects with thread-safe operations
|
|
||||||
type Appearances struct {
|
|
||||||
appearanceMap map[int32]*Appearance // Map of appearance ID to appearance
|
|
||||||
mutex sync.RWMutex // Thread safety for concurrent access
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAppearances creates a new appearances manager
|
|
||||||
func NewAppearances() *Appearances {
|
|
||||||
return &Appearances{
|
|
||||||
appearanceMap: make(map[int32]*Appearance),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reset clears all appearances from the manager
|
|
||||||
func (a *Appearances) Reset() {
|
|
||||||
a.ClearAppearances()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearAppearances removes all appearances from the manager
|
|
||||||
func (a *Appearances) ClearAppearances() {
|
|
||||||
a.mutex.Lock()
|
|
||||||
defer a.mutex.Unlock()
|
|
||||||
|
|
||||||
// Clear the map - Go's garbage collector will handle cleanup
|
|
||||||
a.appearanceMap = make(map[int32]*Appearance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// InsertAppearance adds an appearance to the manager
|
|
||||||
func (a *Appearances) InsertAppearance(appearance *Appearance) error {
|
|
||||||
if appearance == nil {
|
|
||||||
return fmt.Errorf("appearance cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mutex.Lock()
|
|
||||||
defer a.mutex.Unlock()
|
|
||||||
|
|
||||||
a.appearanceMap[appearance.GetID()] = appearance
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAppearanceByID retrieves an appearance by its ID
|
|
||||||
func (a *Appearances) FindAppearanceByID(id int32) *Appearance {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
if appearance, exists := a.appearanceMap[id]; exists {
|
|
||||||
return appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HasAppearance checks if an appearance exists by ID
|
|
||||||
func (a *Appearances) HasAppearance(id int32) bool {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
_, exists := a.appearanceMap[id]
|
|
||||||
return exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearanceCount returns the total number of appearances
|
|
||||||
func (a *Appearances) GetAppearanceCount() int {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
return len(a.appearanceMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAllAppearances returns a copy of all appearances
|
|
||||||
func (a *Appearances) GetAllAppearances() map[int32]*Appearance {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
// Return a copy to prevent external modification
|
|
||||||
result := make(map[int32]*Appearance)
|
|
||||||
for id, appearance := range a.appearanceMap {
|
|
||||||
result[id] = appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearanceIDs returns all appearance IDs
|
|
||||||
func (a *Appearances) GetAppearanceIDs() []int32 {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
ids := make([]int32, 0, len(a.appearanceMap))
|
|
||||||
for id := range a.appearanceMap {
|
|
||||||
ids = append(ids, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ids
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAppearancesByName finds all appearances with names containing the given substring
|
|
||||||
func (a *Appearances) FindAppearancesByName(nameSubstring string) []*Appearance {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
var results []*Appearance
|
|
||||||
|
|
||||||
for _, appearance := range a.appearanceMap {
|
|
||||||
if contains(appearance.GetName(), nameSubstring) {
|
|
||||||
results = append(results, appearance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAppearancesByMinClient finds all appearances with a specific minimum client version
|
|
||||||
func (a *Appearances) FindAppearancesByMinClient(minClient int16) []*Appearance {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
var results []*Appearance
|
|
||||||
|
|
||||||
for _, appearance := range a.appearanceMap {
|
|
||||||
if appearance.GetMinClientVersion() == minClient {
|
|
||||||
results = append(results, appearance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCompatibleAppearances returns all appearances compatible with the given client version
|
|
||||||
func (a *Appearances) GetCompatibleAppearances(clientVersion int16) []*Appearance {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
var results []*Appearance
|
|
||||||
|
|
||||||
for _, appearance := range a.appearanceMap {
|
|
||||||
if appearance.IsCompatibleWithClient(clientVersion) {
|
|
||||||
results = append(results, appearance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveAppearance removes an appearance by ID
|
|
||||||
func (a *Appearances) RemoveAppearance(id int32) bool {
|
|
||||||
a.mutex.Lock()
|
|
||||||
defer a.mutex.Unlock()
|
|
||||||
|
|
||||||
if _, exists := a.appearanceMap[id]; exists {
|
|
||||||
delete(a.appearanceMap, id)
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAppearance updates an existing appearance or inserts it if it doesn't exist
|
|
||||||
func (a *Appearances) UpdateAppearance(appearance *Appearance) error {
|
|
||||||
if appearance == nil {
|
|
||||||
return fmt.Errorf("appearance cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
a.mutex.Lock()
|
|
||||||
defer a.mutex.Unlock()
|
|
||||||
|
|
||||||
a.appearanceMap[appearance.GetID()] = appearance
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearancesByIDRange returns all appearances within the given ID range (inclusive)
|
|
||||||
func (a *Appearances) GetAppearancesByIDRange(minID, maxID int32) []*Appearance {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
var results []*Appearance
|
|
||||||
|
|
||||||
for id, appearance := range a.appearanceMap {
|
|
||||||
if id >= minID && id <= maxID {
|
|
||||||
results = append(results, appearance)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return results
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAppearances checks all appearances for consistency
|
|
||||||
func (a *Appearances) ValidateAppearances() []string {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
var issues []string
|
|
||||||
|
|
||||||
for id, appearance := range a.appearanceMap {
|
|
||||||
if appearance == nil {
|
|
||||||
issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
if appearance.GetID() != id {
|
|
||||||
issues = append(issues, fmt.Sprintf("Appearance ID mismatch: map key %d != appearance ID %d", id, appearance.GetID()))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(appearance.GetName()) == 0 {
|
|
||||||
issues = append(issues, fmt.Sprintf("Appearance ID %d has empty name", id))
|
|
||||||
}
|
|
||||||
|
|
||||||
if appearance.GetMinClientVersion() < 0 {
|
|
||||||
issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return issues
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsValid returns true if all appearances are valid
|
|
||||||
func (a *Appearances) IsValid() bool {
|
|
||||||
issues := a.ValidateAppearances()
|
|
||||||
return len(issues) == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatistics returns statistics about the appearance collection
|
|
||||||
func (a *Appearances) GetStatistics() map[string]any {
|
|
||||||
a.mutex.RLock()
|
|
||||||
defer a.mutex.RUnlock()
|
|
||||||
|
|
||||||
stats := make(map[string]any)
|
|
||||||
stats["total_appearances"] = len(a.appearanceMap)
|
|
||||||
|
|
||||||
// Count by minimum client version
|
|
||||||
versionCounts := make(map[int16]int)
|
|
||||||
for _, appearance := range a.appearanceMap {
|
|
||||||
versionCounts[appearance.GetMinClientVersion()]++
|
|
||||||
}
|
|
||||||
stats["appearances_by_min_client"] = versionCounts
|
|
||||||
|
|
||||||
// Find ID range
|
|
||||||
if len(a.appearanceMap) > 0 {
|
|
||||||
var minID, maxID int32
|
|
||||||
first := true
|
|
||||||
|
|
||||||
for id := range a.appearanceMap {
|
|
||||||
if first {
|
|
||||||
minID = id
|
|
||||||
maxID = id
|
|
||||||
first = false
|
|
||||||
} else {
|
|
||||||
if id < minID {
|
|
||||||
minID = id
|
|
||||||
}
|
|
||||||
if id > maxID {
|
|
||||||
maxID = id
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
stats["min_id"] = minID
|
|
||||||
stats["max_id"] = maxID
|
|
||||||
stats["id_range"] = maxID - minID
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// contains checks if a string contains a substring (case-sensitive)
|
|
||||||
func contains(str, substr string) bool {
|
|
||||||
if len(substr) == 0 {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
if len(str) < len(substr) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for i := 0; i <= len(str)-len(substr); i++ {
|
|
||||||
if str[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
@ -11,3 +11,134 @@ const (
|
|||||||
MinimumClientVersion = 0
|
MinimumClientVersion = 0
|
||||||
DefaultClientVersion = 283
|
DefaultClientVersion = 283
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Appearance type constants from the original EQ2EMu implementation
|
||||||
|
// These correspond to different appearance features like hair, skin, clothing, etc.
|
||||||
|
const (
|
||||||
|
AppearanceSOGAHairFaceHighlightColor = 0 // APPEARANCE_SOGA_HFHC
|
||||||
|
AppearanceSOGAHairTypeHighlightColor = 1 // APPEARANCE_SOGA_HTHC
|
||||||
|
AppearanceSOGAHairFaceColor = 2 // APPEARANCE_SOGA_HFC
|
||||||
|
AppearanceSOGAHairTypeColor = 3 // APPEARANCE_SOGA_HTC
|
||||||
|
AppearanceSOGAHairHighlight = 4 // APPEARANCE_SOGA_HH
|
||||||
|
AppearanceSOGAHairColor1 = 5 // APPEARANCE_SOGA_HC1
|
||||||
|
AppearanceSOGAHairColor2 = 6 // APPEARANCE_SOGA_HC2
|
||||||
|
AppearanceSOGASkinColor = 7 // APPEARANCE_SOGA_SC
|
||||||
|
AppearanceSOGAEyeColor = 8 // APPEARANCE_SOGA_EC
|
||||||
|
AppearanceHairTypeHighlightColor = 9 // APPEARANCE_HTHC
|
||||||
|
AppearanceHairFaceHighlightColor = 10 // APPEARANCE_HFHC
|
||||||
|
AppearanceHairTypeColor = 11 // APPEARANCE_HTC
|
||||||
|
AppearanceHairFaceColor = 12 // APPEARANCE_HFC
|
||||||
|
AppearanceHairHighlight = 13 // APPEARANCE_HH
|
||||||
|
AppearanceHairColor1 = 14 // APPEARANCE_HC1
|
||||||
|
AppearanceHairColor2 = 15 // APPEARANCE_HC2
|
||||||
|
AppearanceWingColor1 = 16 // APPEARANCE_WC1
|
||||||
|
AppearanceWingColor2 = 17 // APPEARANCE_WC2
|
||||||
|
AppearanceSkinColor = 18 // APPEARANCE_SC
|
||||||
|
AppearanceEyeColor = 19 // APPEARANCE_EC
|
||||||
|
AppearanceShirt = 20 // APPEARANCE_SHIRT
|
||||||
|
AppearanceUnknownClothingColor = 21 // APPEARANCE_UCC
|
||||||
|
AppearancePants = 22 // APPEARANCE_PANTS
|
||||||
|
AppearanceUnknownLegColor = 23 // APPEARANCE_ULC
|
||||||
|
AppearanceUnknown9 = 24 // APPEARANCE_U9
|
||||||
|
AppearanceBodySize = 25 // APPEARANCE_BODY_SIZE
|
||||||
|
AppearanceSOGAWingColor1 = 26 // APPEARANCE_SOGA_WC1
|
||||||
|
AppearanceSOGAWingColor2 = 27 // APPEARANCE_SOGA_WC2
|
||||||
|
AppearanceSOGAShirt = 28 // APPEARANCE_SOGA_SHIRT
|
||||||
|
AppearanceSOGAUnknownClothingColor = 29 // APPEARANCE_SOGA_UCC
|
||||||
|
AppearanceSOGAPants = 30 // APPEARANCE_SOGA_PANTS
|
||||||
|
AppearanceSOGAUnknownLegColor = 31 // APPEARANCE_SOGA_ULC
|
||||||
|
AppearanceSOGAUnknown13 = 32 // APPEARANCE_SOGA_U13
|
||||||
|
AppearanceSOGAEyebrowType = 33 // APPEARANCE_SOGA_EBT
|
||||||
|
AppearanceSOGACheekType = 34 // APPEARANCE_SOGA_CHEEKT
|
||||||
|
AppearanceSOGANoseType = 35 // APPEARANCE_SOGA_NT
|
||||||
|
AppearanceSOGAChinType = 36 // APPEARANCE_SOGA_CHINT
|
||||||
|
AppearanceSOGALipType = 37 // APPEARANCE_SOGA_LT
|
||||||
|
AppearanceSOGAEarType = 38 // APPEARANCE_SOGA_EART
|
||||||
|
AppearanceSOGAEyeType = 39 // APPEARANCE_SOGA_EYET
|
||||||
|
AppearanceEyebrowType = 40 // APPEARANCE_EBT
|
||||||
|
AppearanceCheekType = 41 // APPEARANCE_CHEEKT
|
||||||
|
AppearanceNoseType = 42 // APPEARANCE_NT
|
||||||
|
AppearanceChinType = 43 // APPEARANCE_CHINT
|
||||||
|
AppearanceEarType = 44 // APPEARANCE_EART
|
||||||
|
AppearanceEyeType = 45 // APPEARANCE_EYET
|
||||||
|
AppearanceLipType = 46 // APPEARANCE_LT
|
||||||
|
AppearanceBodyAge = 47 // APPEARANCE_BODY_AGE
|
||||||
|
AppearanceModelColor = 48 // APPEARANCE_MC
|
||||||
|
AppearanceSOGAModelColor = 49 // APPEARANCE_SMC
|
||||||
|
AppearanceSOGABodySize = 50 // APPEARANCE_SBS
|
||||||
|
AppearanceSOGABodyAge = 51 // APPEARANCE_SBA
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppearanceTypeNames maps appearance type constants to human-readable names
|
||||||
|
var AppearanceTypeNames = map[int8]string{
|
||||||
|
AppearanceSOGAHairFaceHighlightColor: "soga_hair_face_highlight_color",
|
||||||
|
AppearanceSOGAHairTypeHighlightColor: "soga_hair_type_highlight_color",
|
||||||
|
AppearanceSOGAHairFaceColor: "soga_hair_face_color",
|
||||||
|
AppearanceSOGAHairTypeColor: "soga_hair_type_color",
|
||||||
|
AppearanceSOGAHairHighlight: "soga_hair_highlight",
|
||||||
|
AppearanceSOGAHairColor1: "soga_hair_color1",
|
||||||
|
AppearanceSOGAHairColor2: "soga_hair_color2",
|
||||||
|
AppearanceSOGASkinColor: "soga_skin_color",
|
||||||
|
AppearanceSOGAEyeColor: "soga_eye_color",
|
||||||
|
AppearanceHairTypeHighlightColor: "hair_type_highlight_color",
|
||||||
|
AppearanceHairFaceHighlightColor: "hair_face_highlight_color",
|
||||||
|
AppearanceHairTypeColor: "hair_type_color",
|
||||||
|
AppearanceHairFaceColor: "hair_face_color",
|
||||||
|
AppearanceHairHighlight: "hair_highlight",
|
||||||
|
AppearanceHairColor1: "hair_color1",
|
||||||
|
AppearanceHairColor2: "hair_color2",
|
||||||
|
AppearanceWingColor1: "wing_color1",
|
||||||
|
AppearanceWingColor2: "wing_color2",
|
||||||
|
AppearanceSkinColor: "skin_color",
|
||||||
|
AppearanceEyeColor: "eye_color",
|
||||||
|
AppearanceShirt: "shirt_color",
|
||||||
|
AppearanceUnknownClothingColor: "unknown_chest_color",
|
||||||
|
AppearancePants: "pants_color",
|
||||||
|
AppearanceUnknownLegColor: "unknown_leg_color",
|
||||||
|
AppearanceUnknown9: "unknown9",
|
||||||
|
AppearanceBodySize: "body_size",
|
||||||
|
AppearanceSOGAWingColor1: "soga_wing_color1",
|
||||||
|
AppearanceSOGAWingColor2: "soga_wing_color2",
|
||||||
|
AppearanceSOGAShirt: "soga_shirt_color",
|
||||||
|
AppearanceSOGAUnknownClothingColor: "soga_unknown_chest_color",
|
||||||
|
AppearanceSOGAPants: "soga_pants_color",
|
||||||
|
AppearanceSOGAUnknownLegColor: "soga_unknown_leg_color",
|
||||||
|
AppearanceSOGAUnknown13: "soga_unknown13",
|
||||||
|
AppearanceSOGAEyebrowType: "soga_eye_brow_type",
|
||||||
|
AppearanceSOGACheekType: "soga_cheek_type",
|
||||||
|
AppearanceSOGANoseType: "soga_nose_type",
|
||||||
|
AppearanceSOGAChinType: "soga_chin_type",
|
||||||
|
AppearanceSOGALipType: "soga_lip_type",
|
||||||
|
AppearanceSOGAEarType: "soga_ear_type",
|
||||||
|
AppearanceSOGAEyeType: "soga_eye_type",
|
||||||
|
AppearanceEyebrowType: "eye_brow_type",
|
||||||
|
AppearanceCheekType: "cheek_type",
|
||||||
|
AppearanceNoseType: "nose_type",
|
||||||
|
AppearanceChinType: "chin_type",
|
||||||
|
AppearanceEarType: "ear_type",
|
||||||
|
AppearanceEyeType: "eye_type",
|
||||||
|
AppearanceLipType: "lip_type",
|
||||||
|
AppearanceBodyAge: "body_age",
|
||||||
|
AppearanceModelColor: "model_color",
|
||||||
|
AppearanceSOGAModelColor: "soga_model_color",
|
||||||
|
AppearanceSOGABodySize: "soga_body_size",
|
||||||
|
AppearanceSOGABodyAge: "soga_body_age",
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppearanceType returns the appearance type constant for a given type name
|
||||||
|
func GetAppearanceType(typeName string) int8 {
|
||||||
|
for typeConst, name := range AppearanceTypeNames {
|
||||||
|
if name == typeName {
|
||||||
|
return typeConst
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1 // Unknown type
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppearanceTypeName returns the type name for a given appearance type constant
|
||||||
|
func GetAppearanceTypeName(typeConst int8) string {
|
||||||
|
if name, exists := AppearanceTypeNames[typeConst]; exists {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
45
internal/appearances/doc.go
Normal file
45
internal/appearances/doc.go
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
// Package appearances provides appearance management for the EverQuest II server emulator.
|
||||||
|
//
|
||||||
|
// Appearances define the visual characteristics of entities in the game world, including
|
||||||
|
// player character customization options, NPC appearances, and visual variations.
|
||||||
|
// The system supports client version compatibility to ensure proper rendering across
|
||||||
|
// different EQ2 client versions.
|
||||||
|
//
|
||||||
|
// Basic Usage:
|
||||||
|
//
|
||||||
|
// db, _ := database.NewSQLite("appearances.db")
|
||||||
|
//
|
||||||
|
// // Create new appearance
|
||||||
|
// appearance := appearances.New(db)
|
||||||
|
// appearance.ID = 1001
|
||||||
|
// appearance.Name = "Human Male"
|
||||||
|
// appearance.MinClient = 1096
|
||||||
|
// appearance.Save()
|
||||||
|
//
|
||||||
|
// // Load existing appearance
|
||||||
|
// loaded, _ := appearances.Load(db, 1001)
|
||||||
|
// loaded.Delete()
|
||||||
|
//
|
||||||
|
// Master List:
|
||||||
|
//
|
||||||
|
// masterList := appearances.NewMasterList()
|
||||||
|
// masterList.LoadAllAppearances(db)
|
||||||
|
// masterList.AddAppearance(appearance)
|
||||||
|
//
|
||||||
|
// // Find appearances
|
||||||
|
// found := masterList.GetAppearance(1001)
|
||||||
|
// compatible := masterList.GetCompatibleAppearances(1096)
|
||||||
|
// humans := masterList.FindAppearancesByName("Human")
|
||||||
|
//
|
||||||
|
// Appearance Types:
|
||||||
|
//
|
||||||
|
// // Get appearance type from name
|
||||||
|
// hairType := appearances.GetAppearanceType("hair_color1")
|
||||||
|
//
|
||||||
|
// // Get name from type constant
|
||||||
|
// typeName := appearances.GetAppearanceTypeName(appearances.AppearanceHairColor1)
|
||||||
|
//
|
||||||
|
// The package includes all appearance type constants from the original EQ2EMu
|
||||||
|
// implementation, supporting hair colors, skin tones, facial features, clothing,
|
||||||
|
// and body customization options for both standard and SOGA character models.
|
||||||
|
package appearances
|
@ -1,308 +0,0 @@
|
|||||||
package appearances
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Database interface for appearance persistence
|
|
||||||
type Database interface {
|
|
||||||
LoadAllAppearances() ([]*Appearance, error)
|
|
||||||
SaveAppearance(appearance *Appearance) error
|
|
||||||
DeleteAppearance(id int32) error
|
|
||||||
LoadAppearancesByClientVersion(minClientVersion int16) ([]*Appearance, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Logger interface for appearance logging
|
|
||||||
type Logger interface {
|
|
||||||
LogInfo(message string, args ...any)
|
|
||||||
LogError(message string, args ...any)
|
|
||||||
LogDebug(message string, args ...any)
|
|
||||||
LogWarning(message string, args ...any)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppearanceProvider interface for entities that provide appearances
|
|
||||||
type AppearanceProvider interface {
|
|
||||||
GetAppearanceID() int32
|
|
||||||
SetAppearanceID(id int32)
|
|
||||||
GetAppearance() *Appearance
|
|
||||||
IsCompatibleWithClient(clientVersion int16) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppearanceAware interface for entities that use appearances
|
|
||||||
type AppearanceAware interface {
|
|
||||||
GetAppearanceManager() *Manager
|
|
||||||
FindAppearanceByID(id int32) *Appearance
|
|
||||||
GetCompatibleAppearances(clientVersion int16) []*Appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Client interface for appearance-related client operations
|
|
||||||
type Client interface {
|
|
||||||
GetVersion() int16
|
|
||||||
SendAppearanceUpdate(appearanceID int32) error
|
|
||||||
}
|
|
||||||
|
|
||||||
// AppearanceCache interface for caching appearance data
|
|
||||||
type AppearanceCache interface {
|
|
||||||
Get(id int32) *Appearance
|
|
||||||
Set(id int32, appearance *Appearance)
|
|
||||||
Remove(id int32)
|
|
||||||
Clear()
|
|
||||||
GetSize() int
|
|
||||||
}
|
|
||||||
|
|
||||||
// EntityAppearanceAdapter provides appearance functionality for entities
|
|
||||||
type EntityAppearanceAdapter struct {
|
|
||||||
entity Entity
|
|
||||||
appearanceID int32
|
|
||||||
manager *Manager
|
|
||||||
logger Logger
|
|
||||||
}
|
|
||||||
|
|
||||||
// Entity interface for things that can have appearances
|
|
||||||
type Entity interface {
|
|
||||||
GetID() int32
|
|
||||||
GetName() string
|
|
||||||
GetDatabaseID() int32
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewEntityAppearanceAdapter creates a new entity appearance adapter
|
|
||||||
func NewEntityAppearanceAdapter(entity Entity, manager *Manager, logger Logger) *EntityAppearanceAdapter {
|
|
||||||
return &EntityAppearanceAdapter{
|
|
||||||
entity: entity,
|
|
||||||
appearanceID: 0,
|
|
||||||
manager: manager,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearanceID returns the entity's appearance ID
|
|
||||||
func (eaa *EntityAppearanceAdapter) GetAppearanceID() int32 {
|
|
||||||
return eaa.appearanceID
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetAppearanceID sets the entity's appearance ID
|
|
||||||
func (eaa *EntityAppearanceAdapter) SetAppearanceID(id int32) {
|
|
||||||
eaa.appearanceID = id
|
|
||||||
|
|
||||||
if eaa.logger != nil {
|
|
||||||
eaa.logger.LogDebug("Entity %d (%s): Set appearance ID to %d",
|
|
||||||
eaa.entity.GetID(), eaa.entity.GetName(), id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearance returns the entity's appearance object
|
|
||||||
func (eaa *EntityAppearanceAdapter) GetAppearance() *Appearance {
|
|
||||||
if eaa.appearanceID == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if eaa.manager == nil {
|
|
||||||
if eaa.logger != nil {
|
|
||||||
eaa.logger.LogError("Entity %d (%s): No appearance manager available",
|
|
||||||
eaa.entity.GetID(), eaa.entity.GetName())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return eaa.manager.FindAppearanceByID(eaa.appearanceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCompatibleWithClient checks if the entity's appearance is compatible with client version
|
|
||||||
func (eaa *EntityAppearanceAdapter) IsCompatibleWithClient(clientVersion int16) bool {
|
|
||||||
appearance := eaa.GetAppearance()
|
|
||||||
if appearance == nil {
|
|
||||||
return true // No appearance means compatible with all clients
|
|
||||||
}
|
|
||||||
|
|
||||||
return appearance.IsCompatibleWithClient(clientVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearanceName returns the name of the entity's appearance
|
|
||||||
func (eaa *EntityAppearanceAdapter) GetAppearanceName() string {
|
|
||||||
appearance := eaa.GetAppearance()
|
|
||||||
if appearance == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return appearance.GetName()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAppearance validates that the entity's appearance exists and is valid
|
|
||||||
func (eaa *EntityAppearanceAdapter) ValidateAppearance() error {
|
|
||||||
if eaa.appearanceID == 0 {
|
|
||||||
return nil // No appearance is valid
|
|
||||||
}
|
|
||||||
|
|
||||||
appearance := eaa.GetAppearance()
|
|
||||||
if appearance == nil {
|
|
||||||
return fmt.Errorf("appearance ID %d not found", eaa.appearanceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAppearance updates the entity's appearance from the manager
|
|
||||||
func (eaa *EntityAppearanceAdapter) UpdateAppearance(id int32) error {
|
|
||||||
if eaa.manager == nil {
|
|
||||||
return fmt.Errorf("no appearance manager available")
|
|
||||||
}
|
|
||||||
|
|
||||||
appearance := eaa.manager.FindAppearanceByID(id)
|
|
||||||
if appearance == nil {
|
|
||||||
return fmt.Errorf("appearance ID %d not found", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
eaa.SetAppearanceID(id)
|
|
||||||
|
|
||||||
if eaa.logger != nil {
|
|
||||||
eaa.logger.LogInfo("Entity %d (%s): Updated appearance to %d (%s)",
|
|
||||||
eaa.entity.GetID(), eaa.entity.GetName(), id, appearance.GetName())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// SendAppearanceToClient sends the appearance to a client
|
|
||||||
func (eaa *EntityAppearanceAdapter) SendAppearanceToClient(client Client) error {
|
|
||||||
if client == nil {
|
|
||||||
return fmt.Errorf("client is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if eaa.appearanceID == 0 {
|
|
||||||
return nil // No appearance to send
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check client compatibility
|
|
||||||
if !eaa.IsCompatibleWithClient(client.GetVersion()) {
|
|
||||||
if eaa.logger != nil {
|
|
||||||
eaa.logger.LogWarning("Entity %d (%s): Appearance %d not compatible with client version %d",
|
|
||||||
eaa.entity.GetID(), eaa.entity.GetName(), eaa.appearanceID, client.GetVersion())
|
|
||||||
}
|
|
||||||
return fmt.Errorf("appearance not compatible with client version %d", client.GetVersion())
|
|
||||||
}
|
|
||||||
|
|
||||||
return client.SendAppearanceUpdate(eaa.appearanceID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SimpleAppearanceCache is a basic in-memory appearance cache
|
|
||||||
type SimpleAppearanceCache struct {
|
|
||||||
cache map[int32]*Appearance
|
|
||||||
mutex sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewSimpleAppearanceCache creates a new simple appearance cache
|
|
||||||
func NewSimpleAppearanceCache() *SimpleAppearanceCache {
|
|
||||||
return &SimpleAppearanceCache{
|
|
||||||
cache: make(map[int32]*Appearance),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get retrieves an appearance from cache
|
|
||||||
func (sac *SimpleAppearanceCache) Get(id int32) *Appearance {
|
|
||||||
sac.mutex.RLock()
|
|
||||||
defer sac.mutex.RUnlock()
|
|
||||||
|
|
||||||
return sac.cache[id]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set stores an appearance in cache
|
|
||||||
func (sac *SimpleAppearanceCache) Set(id int32, appearance *Appearance) {
|
|
||||||
sac.mutex.Lock()
|
|
||||||
defer sac.mutex.Unlock()
|
|
||||||
|
|
||||||
sac.cache[id] = appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes an appearance from cache
|
|
||||||
func (sac *SimpleAppearanceCache) Remove(id int32) {
|
|
||||||
sac.mutex.Lock()
|
|
||||||
defer sac.mutex.Unlock()
|
|
||||||
|
|
||||||
delete(sac.cache, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all appearances from cache
|
|
||||||
func (sac *SimpleAppearanceCache) Clear() {
|
|
||||||
sac.mutex.Lock()
|
|
||||||
defer sac.mutex.Unlock()
|
|
||||||
|
|
||||||
sac.cache = make(map[int32]*Appearance)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetSize returns the number of cached appearances
|
|
||||||
func (sac *SimpleAppearanceCache) GetSize() int {
|
|
||||||
sac.mutex.RLock()
|
|
||||||
defer sac.mutex.RUnlock()
|
|
||||||
|
|
||||||
return len(sac.cache)
|
|
||||||
}
|
|
||||||
|
|
||||||
// CachedAppearanceManager wraps a Manager with caching functionality
|
|
||||||
type CachedAppearanceManager struct {
|
|
||||||
*Manager
|
|
||||||
cache AppearanceCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCachedAppearanceManager creates a new cached appearance manager
|
|
||||||
func NewCachedAppearanceManager(manager *Manager, cache AppearanceCache) *CachedAppearanceManager {
|
|
||||||
return &CachedAppearanceManager{
|
|
||||||
Manager: manager,
|
|
||||||
cache: cache,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAppearanceByID finds an appearance with caching
|
|
||||||
func (cam *CachedAppearanceManager) FindAppearanceByID(id int32) *Appearance {
|
|
||||||
// Check cache first
|
|
||||||
if appearance := cam.cache.Get(id); appearance != nil {
|
|
||||||
return appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load from manager
|
|
||||||
appearance := cam.Manager.FindAppearanceByID(id)
|
|
||||||
if appearance != nil {
|
|
||||||
// Cache the result
|
|
||||||
cam.cache.Set(id, appearance)
|
|
||||||
}
|
|
||||||
|
|
||||||
return appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAppearance adds an appearance and updates cache
|
|
||||||
func (cam *CachedAppearanceManager) AddAppearance(appearance *Appearance) error {
|
|
||||||
err := cam.Manager.AddAppearance(appearance)
|
|
||||||
if err == nil {
|
|
||||||
// Update cache
|
|
||||||
cam.cache.Set(appearance.GetID(), appearance)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAppearance updates an appearance and cache
|
|
||||||
func (cam *CachedAppearanceManager) UpdateAppearance(appearance *Appearance) error {
|
|
||||||
err := cam.Manager.UpdateAppearance(appearance)
|
|
||||||
if err == nil {
|
|
||||||
// Update cache
|
|
||||||
cam.cache.Set(appearance.GetID(), appearance)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveAppearance removes an appearance and updates cache
|
|
||||||
func (cam *CachedAppearanceManager) RemoveAppearance(id int32) error {
|
|
||||||
err := cam.Manager.RemoveAppearance(id)
|
|
||||||
if err == nil {
|
|
||||||
// Remove from cache
|
|
||||||
cam.cache.Remove(id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClearCache clears the appearance cache
|
|
||||||
func (cam *CachedAppearanceManager) ClearCache() {
|
|
||||||
cam.cache.Clear()
|
|
||||||
}
|
|
@ -1,392 +0,0 @@
|
|||||||
package appearances
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Manager provides high-level management of the appearance system
|
|
||||||
type Manager struct {
|
|
||||||
appearances *Appearances
|
|
||||||
database Database
|
|
||||||
logger Logger
|
|
||||||
mutex sync.RWMutex
|
|
||||||
|
|
||||||
// Statistics
|
|
||||||
totalLookups int64
|
|
||||||
successfulLookups int64
|
|
||||||
failedLookups int64
|
|
||||||
cacheHits int64
|
|
||||||
cacheMisses int64
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewManager creates a new appearance manager
|
|
||||||
func NewManager(database Database, logger Logger) *Manager {
|
|
||||||
return &Manager{
|
|
||||||
appearances: NewAppearances(),
|
|
||||||
database: database,
|
|
||||||
logger: logger,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Initialize loads appearances from database
|
|
||||||
func (m *Manager) Initialize() error {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Initializing appearance manager...")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.database == nil {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogWarning("No database provided, starting with empty appearance list")
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
appearances, err := m.database.LoadAllAppearances()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load appearances from database: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, appearance := range appearances {
|
|
||||||
if err := m.appearances.InsertAppearance(appearance); err != nil {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogError("Failed to insert appearance %d: %v", appearance.GetID(), err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Loaded %d appearances from database", len(appearances))
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearances returns the appearances collection
|
|
||||||
func (m *Manager) GetAppearances() *Appearances {
|
|
||||||
return m.appearances
|
|
||||||
}
|
|
||||||
|
|
||||||
// FindAppearanceByID finds an appearance by ID with statistics tracking
|
|
||||||
func (m *Manager) FindAppearanceByID(id int32) *Appearance {
|
|
||||||
m.mutex.Lock()
|
|
||||||
m.totalLookups++
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
appearance := m.appearances.FindAppearanceByID(id)
|
|
||||||
|
|
||||||
m.mutex.Lock()
|
|
||||||
if appearance != nil {
|
|
||||||
m.successfulLookups++
|
|
||||||
m.cacheHits++
|
|
||||||
} else {
|
|
||||||
m.failedLookups++
|
|
||||||
m.cacheMisses++
|
|
||||||
}
|
|
||||||
m.mutex.Unlock()
|
|
||||||
|
|
||||||
if m.logger != nil && appearance == nil {
|
|
||||||
m.logger.LogDebug("Appearance lookup failed for ID: %d", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return appearance
|
|
||||||
}
|
|
||||||
|
|
||||||
// AddAppearance adds a new appearance
|
|
||||||
func (m *Manager) AddAppearance(appearance *Appearance) error {
|
|
||||||
if appearance == nil {
|
|
||||||
return fmt.Errorf("appearance cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate the appearance
|
|
||||||
if len(appearance.GetName()) == 0 {
|
|
||||||
return fmt.Errorf("appearance name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if appearance.GetID() <= 0 {
|
|
||||||
return fmt.Errorf("appearance ID must be positive")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if appearance already exists
|
|
||||||
if m.appearances.HasAppearance(appearance.GetID()) {
|
|
||||||
return fmt.Errorf("appearance with ID %d already exists", appearance.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add to collection
|
|
||||||
if err := m.appearances.InsertAppearance(appearance); err != nil {
|
|
||||||
return fmt.Errorf("failed to insert appearance: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to database if available
|
|
||||||
if m.database != nil {
|
|
||||||
if err := m.database.SaveAppearance(appearance); err != nil {
|
|
||||||
// Remove from collection if database save failed
|
|
||||||
m.appearances.RemoveAppearance(appearance.GetID())
|
|
||||||
return fmt.Errorf("failed to save appearance to database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Added appearance %d: %s (min client: %d)",
|
|
||||||
appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// UpdateAppearance updates an existing appearance
|
|
||||||
func (m *Manager) UpdateAppearance(appearance *Appearance) error {
|
|
||||||
if appearance == nil {
|
|
||||||
return fmt.Errorf("appearance cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if appearance exists
|
|
||||||
if !m.appearances.HasAppearance(appearance.GetID()) {
|
|
||||||
return fmt.Errorf("appearance with ID %d does not exist", appearance.GetID())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update in collection
|
|
||||||
if err := m.appearances.UpdateAppearance(appearance); err != nil {
|
|
||||||
return fmt.Errorf("failed to update appearance: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save to database if available
|
|
||||||
if m.database != nil {
|
|
||||||
if err := m.database.SaveAppearance(appearance); err != nil {
|
|
||||||
return fmt.Errorf("failed to save appearance to database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Updated appearance %d: %s", appearance.GetID(), appearance.GetName())
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RemoveAppearance removes an appearance
|
|
||||||
func (m *Manager) RemoveAppearance(id int32) error {
|
|
||||||
// Check if appearance exists
|
|
||||||
if !m.appearances.HasAppearance(id) {
|
|
||||||
return fmt.Errorf("appearance with ID %d does not exist", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from database first if available
|
|
||||||
if m.database != nil {
|
|
||||||
if err := m.database.DeleteAppearance(id); err != nil {
|
|
||||||
return fmt.Errorf("failed to delete appearance from database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove from collection
|
|
||||||
if !m.appearances.RemoveAppearance(id) {
|
|
||||||
return fmt.Errorf("failed to remove appearance from collection")
|
|
||||||
}
|
|
||||||
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Removed appearance %d", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCompatibleAppearances returns appearances compatible with client version
|
|
||||||
func (m *Manager) GetCompatibleAppearances(clientVersion int16) []*Appearance {
|
|
||||||
return m.appearances.GetCompatibleAppearances(clientVersion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// SearchAppearancesByName searches for appearances by name substring
|
|
||||||
func (m *Manager) SearchAppearancesByName(nameSubstring string) []*Appearance {
|
|
||||||
return m.appearances.FindAppearancesByName(nameSubstring)
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetStatistics returns appearance system statistics
|
|
||||||
func (m *Manager) GetStatistics() map[string]any {
|
|
||||||
m.mutex.RLock()
|
|
||||||
defer m.mutex.RUnlock()
|
|
||||||
|
|
||||||
// Get basic appearance statistics
|
|
||||||
stats := m.appearances.GetStatistics()
|
|
||||||
|
|
||||||
// Add manager statistics
|
|
||||||
stats["total_lookups"] = m.totalLookups
|
|
||||||
stats["successful_lookups"] = m.successfulLookups
|
|
||||||
stats["failed_lookups"] = m.failedLookups
|
|
||||||
stats["cache_hits"] = m.cacheHits
|
|
||||||
stats["cache_misses"] = m.cacheMisses
|
|
||||||
|
|
||||||
if m.totalLookups > 0 {
|
|
||||||
stats["success_rate"] = float64(m.successfulLookups) / float64(m.totalLookups) * 100
|
|
||||||
stats["cache_hit_rate"] = float64(m.cacheHits) / float64(m.totalLookups) * 100
|
|
||||||
}
|
|
||||||
|
|
||||||
return stats
|
|
||||||
}
|
|
||||||
|
|
||||||
// ResetStatistics resets all statistics
|
|
||||||
func (m *Manager) ResetStatistics() {
|
|
||||||
m.mutex.Lock()
|
|
||||||
defer m.mutex.Unlock()
|
|
||||||
|
|
||||||
m.totalLookups = 0
|
|
||||||
m.successfulLookups = 0
|
|
||||||
m.failedLookups = 0
|
|
||||||
m.cacheHits = 0
|
|
||||||
m.cacheMisses = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateAllAppearances validates all appearances in the system
|
|
||||||
func (m *Manager) ValidateAllAppearances() []string {
|
|
||||||
return m.appearances.ValidateAppearances()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReloadFromDatabase reloads all appearances from database
|
|
||||||
func (m *Manager) ReloadFromDatabase() error {
|
|
||||||
if m.database == nil {
|
|
||||||
return fmt.Errorf("no database available")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear current appearances
|
|
||||||
m.appearances.ClearAppearances()
|
|
||||||
|
|
||||||
// Reload from database
|
|
||||||
return m.Initialize()
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetAppearanceCount returns the total number of appearances
|
|
||||||
func (m *Manager) GetAppearanceCount() int {
|
|
||||||
return m.appearances.GetAppearanceCount()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProcessCommand handles appearance-related commands
|
|
||||||
func (m *Manager) ProcessCommand(command string, args []string) (string, error) {
|
|
||||||
switch command {
|
|
||||||
case "stats":
|
|
||||||
return m.handleStatsCommand(args)
|
|
||||||
case "validate":
|
|
||||||
return m.handleValidateCommand(args)
|
|
||||||
case "search":
|
|
||||||
return m.handleSearchCommand(args)
|
|
||||||
case "info":
|
|
||||||
return m.handleInfoCommand(args)
|
|
||||||
case "reload":
|
|
||||||
return m.handleReloadCommand(args)
|
|
||||||
default:
|
|
||||||
return "", fmt.Errorf("unknown appearance command: %s", command)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleStatsCommand shows appearance system statistics
|
|
||||||
func (m *Manager) handleStatsCommand(args []string) (string, error) {
|
|
||||||
stats := m.GetStatistics()
|
|
||||||
|
|
||||||
result := "Appearance System Statistics:\n"
|
|
||||||
result += fmt.Sprintf("Total Appearances: %d\n", stats["total_appearances"])
|
|
||||||
result += fmt.Sprintf("Total Lookups: %d\n", stats["total_lookups"])
|
|
||||||
result += fmt.Sprintf("Successful Lookups: %d\n", stats["successful_lookups"])
|
|
||||||
result += fmt.Sprintf("Failed Lookups: %d\n", stats["failed_lookups"])
|
|
||||||
|
|
||||||
if successRate, exists := stats["success_rate"]; exists {
|
|
||||||
result += fmt.Sprintf("Success Rate: %.1f%%\n", successRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cacheHitRate, exists := stats["cache_hit_rate"]; exists {
|
|
||||||
result += fmt.Sprintf("Cache Hit Rate: %.1f%%\n", cacheHitRate)
|
|
||||||
}
|
|
||||||
|
|
||||||
if minID, exists := stats["min_id"]; exists {
|
|
||||||
result += fmt.Sprintf("ID Range: %d - %d\n", minID, stats["max_id"])
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleValidateCommand validates all appearances
|
|
||||||
func (m *Manager) handleValidateCommand(_ []string) (string, error) {
|
|
||||||
issues := m.ValidateAllAppearances()
|
|
||||||
|
|
||||||
if len(issues) == 0 {
|
|
||||||
return "All appearances are valid.", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fmt.Sprintf("Found %d issues with appearances:\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
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleSearchCommand searches for appearances by name
|
|
||||||
func (m *Manager) handleSearchCommand(args []string) (string, error) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return "", fmt.Errorf("search term required")
|
|
||||||
}
|
|
||||||
|
|
||||||
searchTerm := args[0]
|
|
||||||
results := m.SearchAppearancesByName(searchTerm)
|
|
||||||
|
|
||||||
if len(results) == 0 {
|
|
||||||
return fmt.Sprintf("No appearances found matching '%s'.", searchTerm), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := fmt.Sprintf("Found %d appearances matching '%s':\n", len(results), searchTerm)
|
|
||||||
for i, appearance := range results {
|
|
||||||
if i >= 20 { // Limit output
|
|
||||||
result += "... (and more)\n"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result += fmt.Sprintf(" %d: %s (min client: %d)\n",
|
|
||||||
appearance.GetID(), appearance.GetName(), appearance.GetMinClientVersion())
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleInfoCommand shows information about a specific appearance
|
|
||||||
func (m *Manager) handleInfoCommand(args []string) (string, error) {
|
|
||||||
if len(args) == 0 {
|
|
||||||
return "", fmt.Errorf("appearance ID required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var appearanceID int32
|
|
||||||
if _, err := fmt.Sscanf(args[0], "%d", &appearanceID); err != nil {
|
|
||||||
return "", fmt.Errorf("invalid appearance ID: %s", args[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
appearance := m.FindAppearanceByID(appearanceID)
|
|
||||||
if appearance == nil {
|
|
||||||
return fmt.Sprintf("Appearance %d not found.", appearanceID), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
result := "Appearance Information:\n"
|
|
||||||
result += fmt.Sprintf("ID: %d\n", appearance.GetID())
|
|
||||||
result += fmt.Sprintf("Name: %s\n", appearance.GetName())
|
|
||||||
result += fmt.Sprintf("Min Client Version: %d\n", appearance.GetMinClientVersion())
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// handleReloadCommand reloads appearances from database
|
|
||||||
func (m *Manager) handleReloadCommand(_ []string) (string, error) {
|
|
||||||
if err := m.ReloadFromDatabase(); err != nil {
|
|
||||||
return "", fmt.Errorf("failed to reload appearances: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
count := m.GetAppearanceCount()
|
|
||||||
return fmt.Sprintf("Successfully reloaded %d appearances from database.", count), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the manager
|
|
||||||
func (m *Manager) Shutdown() {
|
|
||||||
if m.logger != nil {
|
|
||||||
m.logger.LogInfo("Shutting down appearance manager...")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear appearances
|
|
||||||
m.appearances.ClearAppearances()
|
|
||||||
}
|
|
235
internal/appearances/master.go
Normal file
235
internal/appearances/master.go
Normal file
@ -0,0 +1,235 @@
|
|||||||
|
package appearances
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"eq2emu/internal/common"
|
||||||
|
"eq2emu/internal/database"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MasterList manages a collection of appearances using the generic MasterList base
|
||||||
|
type MasterList struct {
|
||||||
|
*common.MasterList[int32, *Appearance]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMasterList creates a new appearance master list
|
||||||
|
func NewMasterList() *MasterList {
|
||||||
|
return &MasterList{
|
||||||
|
MasterList: common.NewMasterList[int32, *Appearance](),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddAppearance adds an appearance to the master list
|
||||||
|
func (ml *MasterList) AddAppearance(appearance *Appearance) bool {
|
||||||
|
return ml.Add(appearance)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppearance retrieves an appearance by ID
|
||||||
|
func (ml *MasterList) GetAppearance(id int32) *Appearance {
|
||||||
|
return ml.Get(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppearanceSafe retrieves an appearance by ID with existence check
|
||||||
|
func (ml *MasterList) GetAppearanceSafe(id int32) (*Appearance, bool) {
|
||||||
|
return ml.GetSafe(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasAppearance checks if an appearance exists by ID
|
||||||
|
func (ml *MasterList) HasAppearance(id int32) bool {
|
||||||
|
return ml.Exists(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RemoveAppearance removes an appearance by ID
|
||||||
|
func (ml *MasterList) RemoveAppearance(id int32) bool {
|
||||||
|
return ml.Remove(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAppearances returns all appearances as a map
|
||||||
|
func (ml *MasterList) GetAllAppearances() map[int32]*Appearance {
|
||||||
|
return ml.GetAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAllAppearancesList returns all appearances as a slice
|
||||||
|
func (ml *MasterList) GetAllAppearancesList() []*Appearance {
|
||||||
|
return ml.GetAllSlice()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppearanceCount returns the number of appearances
|
||||||
|
func (ml *MasterList) GetAppearanceCount() int {
|
||||||
|
return ml.Size()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClearAppearances removes all appearances from the list
|
||||||
|
func (ml *MasterList) ClearAppearances() {
|
||||||
|
ml.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindAppearancesByName finds appearances containing the given name substring
|
||||||
|
func (ml *MasterList) FindAppearancesByName(nameSubstring string) []*Appearance {
|
||||||
|
return ml.Filter(func(appearance *Appearance) bool {
|
||||||
|
return contains(appearance.GetName(), nameSubstring)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindAppearancesByMinClient finds appearances with specific minimum client version
|
||||||
|
func (ml *MasterList) FindAppearancesByMinClient(minClient int16) []*Appearance {
|
||||||
|
return ml.Filter(func(appearance *Appearance) bool {
|
||||||
|
return appearance.GetMinClientVersion() == minClient
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCompatibleAppearances returns appearances compatible with the given client version
|
||||||
|
func (ml *MasterList) GetCompatibleAppearances(clientVersion int16) []*Appearance {
|
||||||
|
return ml.Filter(func(appearance *Appearance) bool {
|
||||||
|
return appearance.IsCompatibleWithClient(clientVersion)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAppearancesByIDRange returns appearances within the given ID range (inclusive)
|
||||||
|
func (ml *MasterList) GetAppearancesByIDRange(minID, maxID int32) []*Appearance {
|
||||||
|
return ml.Filter(func(appearance *Appearance) bool {
|
||||||
|
id := appearance.GetID()
|
||||||
|
return id >= minID && id <= maxID
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateAppearances checks all appearances for consistency
|
||||||
|
func (ml *MasterList) ValidateAppearances() []string {
|
||||||
|
var issues []string
|
||||||
|
|
||||||
|
ml.ForEach(func(id int32, appearance *Appearance) {
|
||||||
|
if appearance == nil {
|
||||||
|
issues = append(issues, fmt.Sprintf("Appearance ID %d is nil", id))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if appearance.GetID() != id {
|
||||||
|
issues = append(issues, fmt.Sprintf("Appearance ID mismatch: map key %d != appearance ID %d", id, appearance.GetID()))
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(appearance.GetName()) == 0 {
|
||||||
|
issues = append(issues, fmt.Sprintf("Appearance ID %d has empty name", id))
|
||||||
|
}
|
||||||
|
|
||||||
|
if appearance.GetMinClientVersion() < 0 {
|
||||||
|
issues = append(issues, fmt.Sprintf("Appearance ID %d has negative min client version: %d", id, appearance.GetMinClientVersion()))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return issues
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns true if all appearances are valid
|
||||||
|
func (ml *MasterList) IsValid() bool {
|
||||||
|
issues := ml.ValidateAppearances()
|
||||||
|
return len(issues) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetStatistics returns statistics about the appearance collection
|
||||||
|
func (ml *MasterList) GetStatistics() map[string]any {
|
||||||
|
stats := make(map[string]any)
|
||||||
|
stats["total_appearances"] = ml.Size()
|
||||||
|
|
||||||
|
if ml.IsEmpty() {
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// Count by minimum client version
|
||||||
|
versionCounts := make(map[int16]int)
|
||||||
|
var minID, maxID int32
|
||||||
|
first := true
|
||||||
|
|
||||||
|
ml.ForEach(func(id int32, appearance *Appearance) {
|
||||||
|
versionCounts[appearance.GetMinClientVersion()]++
|
||||||
|
|
||||||
|
if first {
|
||||||
|
minID = id
|
||||||
|
maxID = id
|
||||||
|
first = false
|
||||||
|
} else {
|
||||||
|
if id < minID {
|
||||||
|
minID = id
|
||||||
|
}
|
||||||
|
if id > maxID {
|
||||||
|
maxID = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
stats["appearances_by_min_client"] = versionCounts
|
||||||
|
stats["min_id"] = minID
|
||||||
|
stats["max_id"] = maxID
|
||||||
|
stats["id_range"] = maxID - minID
|
||||||
|
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAllAppearances loads all appearances from the database into the master list
|
||||||
|
func (ml *MasterList) LoadAllAppearances(db *database.Database) error {
|
||||||
|
if db == nil {
|
||||||
|
return fmt.Errorf("database connection is nil")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear existing appearances
|
||||||
|
ml.Clear()
|
||||||
|
|
||||||
|
query := `SELECT appearance_id, name, min_client_version FROM appearances ORDER BY appearance_id`
|
||||||
|
rows, err := db.Query(query)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to query appearances: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for rows.Next() {
|
||||||
|
appearance := &Appearance{
|
||||||
|
db: db,
|
||||||
|
isNew: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := rows.Scan(&appearance.ID, &appearance.Name, &appearance.MinClient)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to scan appearance: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !ml.AddAppearance(appearance) {
|
||||||
|
return fmt.Errorf("failed to add appearance %d to master list", appearance.ID)
|
||||||
|
}
|
||||||
|
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return fmt.Errorf("error iterating appearance rows: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadAllAppearancesFromDatabase is a convenience function that creates a master list and loads all appearances
|
||||||
|
func LoadAllAppearancesFromDatabase(db *database.Database) (*MasterList, error) {
|
||||||
|
masterList := NewMasterList()
|
||||||
|
err := masterList.LoadAllAppearances(db)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return masterList, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// contains checks if a string contains a substring (case-sensitive)
|
||||||
|
func contains(str, substr string) bool {
|
||||||
|
if len(substr) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(str) < len(substr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i <= len(str)-len(substr); i++ {
|
||||||
|
if str[i:i+len(substr)] == substr {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
@ -1,65 +0,0 @@
|
|||||||
package appearances
|
|
||||||
|
|
||||||
// Appearance represents a single appearance with ID, name, and client version requirements
|
|
||||||
type Appearance struct {
|
|
||||||
id int32 // Appearance ID
|
|
||||||
name string // Appearance name
|
|
||||||
minClient int16 // Minimum client version required
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAppearance creates a new appearance with the given parameters
|
|
||||||
func NewAppearance(id int32, name string, minClientVersion int16) *Appearance {
|
|
||||||
if len(name) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &Appearance{
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
minClient: minClientVersion,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetID returns the appearance ID
|
|
||||||
func (a *Appearance) GetID() int32 {
|
|
||||||
return a.id
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetName returns the appearance name
|
|
||||||
func (a *Appearance) GetName() string {
|
|
||||||
return a.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetMinClientVersion returns the minimum client version required
|
|
||||||
func (a *Appearance) GetMinClientVersion() int16 {
|
|
||||||
return a.minClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetNameString returns the name as a string (alias for GetName for C++ compatibility)
|
|
||||||
func (a *Appearance) GetNameString() string {
|
|
||||||
return a.name
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetName sets the appearance name
|
|
||||||
func (a *Appearance) SetName(name string) {
|
|
||||||
a.name = name
|
|
||||||
}
|
|
||||||
|
|
||||||
// SetMinClientVersion sets the minimum client version
|
|
||||||
func (a *Appearance) SetMinClientVersion(version int16) {
|
|
||||||
a.minClient = version
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsCompatibleWithClient returns true if the appearance is compatible with the given client version
|
|
||||||
func (a *Appearance) IsCompatibleWithClient(clientVersion int16) bool {
|
|
||||||
return clientVersion >= a.minClient
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clone creates a copy of the appearance
|
|
||||||
func (a *Appearance) Clone() *Appearance {
|
|
||||||
return &Appearance{
|
|
||||||
id: a.id,
|
|
||||||
name: a.name,
|
|
||||||
minClient: a.minClient,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,33 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"eq2emu/internal/packets"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Enable all logging to see warnings
|
|
||||||
log.SetFlags(log.LstdFlags | log.Lshortfile)
|
|
||||||
|
|
||||||
// This will trigger the init() function in the packets loader
|
|
||||||
count := packets.GetPacketCount()
|
|
||||||
fmt.Printf("Loaded %d packet definitions\n", count)
|
|
||||||
|
|
||||||
// Get all packet names to look for any patterns
|
|
||||||
names := packets.GetPacketNames()
|
|
||||||
if len(names) == 0 {
|
|
||||||
fmt.Println("No packets loaded!")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Look for packets that might be empty
|
|
||||||
fmt.Println("Checking for potentially problematic packets...")
|
|
||||||
for _, name := range names {
|
|
||||||
if packet, exists := packets.GetPacket(name); exists {
|
|
||||||
if len(packet.Fields) == 0 {
|
|
||||||
fmt.Printf("Empty packet found: %s\n", name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,39 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"eq2emu/internal/packets/parser"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// Test parsing an empty packet that might fail
|
|
||||||
emptyPacketXML := `<packet name="EmptyTest">
|
|
||||||
<version number="1">
|
|
||||||
</version>
|
|
||||||
</packet>`
|
|
||||||
|
|
||||||
fmt.Println("Testing empty packet parsing...")
|
|
||||||
packets, err := parser.Parse(emptyPacketXML)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("ERROR parsing empty packet: %v\n", err)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("SUCCESS: Parsed %d packets\n", len(packets))
|
|
||||||
if packet, exists := packets["EmptyTest"]; exists {
|
|
||||||
fmt.Printf("EmptyTest packet has %d fields\n", len(packet.Fields))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test a completely self-closing packet
|
|
||||||
selfClosingXML := `<packet name="SelfClosingTest" />`
|
|
||||||
|
|
||||||
fmt.Println("\nTesting self-closing packet parsing...")
|
|
||||||
packets2, err2 := parser.Parse(selfClosingXML)
|
|
||||||
if err2 != nil {
|
|
||||||
fmt.Printf("ERROR parsing self-closing packet: %v\n", err2)
|
|
||||||
} else {
|
|
||||||
fmt.Printf("SUCCESS: Parsed %d packets\n", len(packets2))
|
|
||||||
if packet, exists := packets2["SelfClosingTest"]; exists {
|
|
||||||
fmt.Printf("SelfClosingTest packet has %d fields\n", len(packet.Fields))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
x
Reference in New Issue
Block a user