package users import ( "dk/internal/store" "fmt" "slices" "sort" "strings" "sync" "time" "dk/internal/helpers" ) // User represents a user in the game type User struct { ID int `json:"id"` Username string `json:"username"` Password string `json:"password"` Email string `json:"email"` Verified int `json:"verified"` Token string `json:"token"` Registered int64 `json:"registered"` LastOnline int64 `json:"last_online"` Auth int `json:"auth"` X int `json:"x"` Y int `json:"y"` ClassID int `json:"class_id"` Currently string `json:"currently"` Fighting int `json:"fighting"` MonsterID int `json:"monster_id"` MonsterHP int `json:"monster_hp"` MonsterSleep int `json:"monster_sleep"` MonsterImmune int `json:"monster_immune"` UberDamage int `json:"uber_damage"` UberDefense int `json:"uber_defense"` HP int `json:"hp"` MP int `json:"mp"` TP int `json:"tp"` MaxHP int `json:"max_hp"` MaxMP int `json:"max_mp"` MaxTP int `json:"max_tp"` Level int `json:"level"` Gold int `json:"gold"` Exp int `json:"exp"` GoldBonus int `json:"gold_bonus"` ExpBonus int `json:"exp_bonus"` Strength int `json:"strength"` Dexterity int `json:"dexterity"` Attack int `json:"attack"` Defense int `json:"defense"` WeaponID int `json:"weapon_id"` ArmorID int `json:"armor_id"` ShieldID int `json:"shield_id"` Slot1ID int `json:"slot_1_id"` Slot2ID int `json:"slot_2_id"` Slot3ID int `json:"slot_3_id"` WeaponName string `json:"weapon_name"` ArmorName string `json:"armor_name"` ShieldName string `json:"shield_name"` Slot1Name string `json:"slot_1_name"` Slot2Name string `json:"slot_2_name"` Slot3Name string `json:"slot_3_name"` DropCode int `json:"drop_code"` Spells string `json:"spells"` Towns string `json:"towns"` } func (u *User) Save() error { userStore := GetStore() userStore.UpdateUser(u) return nil } func (u *User) Delete() error { userStore := GetStore() userStore.RemoveUser(u.ID) return nil } func New() *User { now := time.Now().Unix() return &User{ Verified: 0, Token: "", Registered: now, LastOnline: now, Auth: 0, X: 0, Y: 0, ClassID: 1, Currently: "In Town", Fighting: 0, HP: 15, MP: 0, TP: 10, MaxHP: 15, MaxMP: 0, MaxTP: 10, Level: 1, Gold: 100, Exp: 0, Strength: 5, Dexterity: 5, Attack: 5, Defense: 5, Spells: "", Towns: "", } } // Validate checks if user has valid values func (u *User) Validate() error { if strings.TrimSpace(u.Username) == "" { return fmt.Errorf("user username cannot be empty") } if strings.TrimSpace(u.Email) == "" { return fmt.Errorf("user email cannot be empty") } if u.Registered <= 0 { return fmt.Errorf("user Registered timestamp must be positive") } if u.LastOnline <= 0 { return fmt.Errorf("user LastOnline timestamp must be positive") } if u.Level < 1 { return fmt.Errorf("user Level must be at least 1") } if u.HP < 0 { return fmt.Errorf("user HP cannot be negative") } if u.MaxHP < 1 { return fmt.Errorf("user MaxHP must be at least 1") } return nil } // UserStore provides in-memory storage with O(1) lookups and user-specific indices type UserStore struct { *store.BaseStore[User] // Embedded generic store byUsername map[string]int // Username (lowercase) -> ID byEmail map[string]int // Email -> ID byLevel map[int][]int // Level -> []ID allByRegistered []int // All IDs sorted by registered DESC, id DESC mu sync.RWMutex // Protects indices } // Global in-memory store var userStore *UserStore var storeOnce sync.Once // Initialize the in-memory store func initStore() { userStore = &UserStore{ BaseStore: store.NewBaseStore[User](), byUsername: make(map[string]int), byEmail: make(map[string]int), byLevel: make(map[int][]int), allByRegistered: make([]int, 0), } } // GetStore returns the global user store func GetStore() *UserStore { storeOnce.Do(initStore) return userStore } // AddUser adds a user to the in-memory store and updates all indices func (us *UserStore) AddUser(user *User) { us.mu.Lock() defer us.mu.Unlock() // Validate user if err := user.Validate(); err != nil { return } // Add to base store us.Add(user.ID, user) // Rebuild indices us.rebuildIndicesUnsafe() } // RemoveUser removes a user from the store and updates indices func (us *UserStore) RemoveUser(id int) { us.mu.Lock() defer us.mu.Unlock() // Remove from base store us.Remove(id) // Rebuild indices us.rebuildIndicesUnsafe() } // UpdateUser updates a user efficiently func (us *UserStore) UpdateUser(user *User) { us.mu.Lock() defer us.mu.Unlock() // Validate user if err := user.Validate(); err != nil { return } // Update base store us.Add(user.ID, user) // Rebuild indices us.rebuildIndicesUnsafe() } // LoadData loads user data from JSON file, or starts with empty store func LoadData(dataPath string) error { us := GetStore() // Load from base store, which handles JSON loading if err := us.BaseStore.LoadData(dataPath); err != nil { return err } // Rebuild indices from loaded data us.rebuildIndices() return nil } // SaveData saves user data to JSON file func SaveData(dataPath string) error { us := GetStore() return us.BaseStore.SaveData(dataPath) } // rebuildIndicesUnsafe rebuilds all indices from base store data (caller must hold lock) func (us *UserStore) rebuildIndicesUnsafe() { // Clear indices us.byUsername = make(map[string]int) us.byEmail = make(map[string]int) us.byLevel = make(map[int][]int) us.allByRegistered = make([]int, 0) // Collect all users and build indices allUsers := us.GetAll() for id, user := range allUsers { // Username index (case-insensitive) us.byUsername[strings.ToLower(user.Username)] = id // Email index us.byEmail[user.Email] = id // Level index us.byLevel[user.Level] = append(us.byLevel[user.Level], id) // All IDs us.allByRegistered = append(us.allByRegistered, id) } // Sort allByRegistered by registered DESC, then ID DESC sort.Slice(us.allByRegistered, func(i, j int) bool { userI, _ := us.GetByID(us.allByRegistered[i]) userJ, _ := us.GetByID(us.allByRegistered[j]) if userI.Registered != userJ.Registered { return userI.Registered > userJ.Registered // DESC } return us.allByRegistered[i] > us.allByRegistered[j] // DESC }) // Sort level indices by exp DESC, then ID ASC for level := range us.byLevel { sort.Slice(us.byLevel[level], func(i, j int) bool { userI, _ := us.GetByID(us.byLevel[level][i]) userJ, _ := us.GetByID(us.byLevel[level][j]) if userI.Exp != userJ.Exp { return userI.Exp > userJ.Exp // DESC } return us.byLevel[level][i] < us.byLevel[level][j] // ASC }) } } // rebuildIndices rebuilds all user-specific indices from base store data func (us *UserStore) rebuildIndices() { us.mu.Lock() defer us.mu.Unlock() us.rebuildIndicesUnsafe() } func Find(id int) (*User, error) { us := GetStore() user, exists := us.GetByID(id) if !exists { return nil, fmt.Errorf("user with ID %d not found", id) } return user, nil } func All() ([]*User, error) { us := GetStore() us.mu.RLock() defer us.mu.RUnlock() result := make([]*User, 0, len(us.allByRegistered)) for _, id := range us.allByRegistered { if user, exists := us.GetByID(id); exists { result = append(result, user) } } return result, nil } func ByUsername(username string) (*User, error) { us := GetStore() us.mu.RLock() defer us.mu.RUnlock() id, exists := us.byUsername[strings.ToLower(username)] if !exists { return nil, fmt.Errorf("user with username '%s' not found", username) } user, exists := us.GetByID(id) if !exists { return nil, fmt.Errorf("user with username '%s' not found", username) } return user, nil } func ByEmail(email string) (*User, error) { us := GetStore() us.mu.RLock() defer us.mu.RUnlock() id, exists := us.byEmail[email] if !exists { return nil, fmt.Errorf("user with email '%s' not found", email) } user, exists := us.GetByID(id) if !exists { return nil, fmt.Errorf("user with email '%s' not found", email) } return user, nil } func ByLevel(level int) ([]*User, error) { us := GetStore() us.mu.RLock() defer us.mu.RUnlock() ids, exists := us.byLevel[level] if !exists { return []*User{}, nil } result := make([]*User, 0, len(ids)) for _, id := range ids { if user, exists := us.GetByID(id); exists { result = append(result, user) } } return result, nil } func Online(within time.Duration) ([]*User, error) { us := GetStore() us.mu.RLock() defer us.mu.RUnlock() cutoff := time.Now().Add(-within).Unix() var result []*User for _, id := range us.allByRegistered { if user, exists := us.GetByID(id); exists && user.LastOnline >= cutoff { result = append(result, user) } } // Sort by last_online DESC, then ID ASC sort.Slice(result, func(i, j int) bool { if result[i].LastOnline != result[j].LastOnline { return result[i].LastOnline > result[j].LastOnline // DESC } return result[i].ID < result[j].ID // ASC }) return result, nil } func (u *User) Insert() error { us := GetStore() // Validate before insertion if err := u.Validate(); err != nil { return fmt.Errorf("validation failed: %w", err) } // Assign new ID if not set if u.ID == 0 { u.ID = us.GetNextID() } // Add to store us.AddUser(u) return nil } func (u *User) RegisteredTime() time.Time { return time.Unix(u.Registered, 0) } func (u *User) LastOnlineTime() time.Time { return time.Unix(u.LastOnline, 0) } func (u *User) UpdateLastOnline() { u.LastOnline = time.Now().Unix() } func (u *User) IsVerified() bool { return u.Verified == 1 } func (u *User) IsAdmin() bool { return u.Auth >= 4 } func (u *User) IsModerator() bool { return u.Auth >= 3 } func (u *User) IsFighting() bool { return u.Fighting == 1 } func (u *User) IsAlive() bool { return u.HP > 0 } func (u *User) GetSpellIDs() []int { return helpers.StringToInts(u.Spells) } func (u *User) SetSpellIDs(spells []int) { u.Spells = helpers.IntsToString(spells) } func (u *User) HasSpell(spellID int) bool { return slices.Contains(u.GetSpellIDs(), spellID) } func (u *User) GetTownIDs() []int { return helpers.StringToInts(u.Towns) } func (u *User) SetTownIDs(towns []int) { u.Towns = helpers.IntsToString(towns) } func (u *User) HasTownMap(townID int) bool { return slices.Contains(u.GetTownIDs(), townID) } func (u *User) GetEquipment() map[string]any { return map[string]any{ "weapon": map[string]any{"id": u.WeaponID, "name": u.WeaponName}, "armor": map[string]any{"id": u.ArmorID, "name": u.ArmorName}, "shield": map[string]any{"id": u.ShieldID, "name": u.ShieldName}, "slot1": map[string]any{"id": u.Slot1ID, "name": u.Slot1Name}, "slot2": map[string]any{"id": u.Slot2ID, "name": u.Slot2Name}, "slot3": map[string]any{"id": u.Slot3ID, "name": u.Slot3Name}, } } func (u *User) GetStats() map[string]int { return map[string]int{ "level": u.Level, "hp": u.HP, "mp": u.MP, "tp": u.TP, "max_hp": u.MaxHP, "max_mp": u.MaxMP, "max_tp": u.MaxTP, "strength": u.Strength, "dexterity": u.Dexterity, "attack": u.Attack, "defense": u.Defense, "uber_damage": u.UberDamage, "uber_defense": u.UberDefense, } } func (u *User) GetPosition() (int, int) { return u.X, u.Y } func (u *User) SetPosition(x, y int) { u.X = x u.Y = y }