504 lines
12 KiB
Go
504 lines
12 KiB
Go
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
|
|
}
|