modernize appearance package

This commit is contained in:
Sky Johnson 2025-08-07 16:36:26 -05:00
parent c637793dee
commit 1fc81eea95
12 changed files with 821 additions and 2675 deletions

View 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
}

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

View File

@ -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

View File

@ -11,3 +11,134 @@ const (
MinimumClientVersion = 0
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"
}

View 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

View File

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

View File

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

View 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
}

View File

@ -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,
}
}

View File

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

View File

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