diff --git a/internal/users/users.go b/internal/users/users.go index b5e16f8..f565cc1 100644 --- a/internal/users/users.go +++ b/internal/users/users.go @@ -6,128 +6,77 @@ import ( "time" "dk/internal/database" + "dk/internal/utils/scanner" "zombiezen.com/go/sqlite" ) // User represents a user in the database 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"` + ID int `db:"id" json:"id"` + Username string `db:"username" json:"username"` + Password string `db:"password" json:"password"` + Email string `db:"email" json:"email"` + Verified int `db:"verified" json:"verified"` + Token string `db:"token" json:"token"` + Registered int64 `db:"registered" json:"registered"` + LastOnline int64 `db:"last_online" json:"last_online"` + Auth int `db:"auth" json:"auth"` + X int `db:"x" json:"x"` + Y int `db:"y" json:"y"` + ClassID int `db:"class_id" json:"class_id"` + Currently string `db:"currently" json:"currently"` + Fighting int `db:"fighting" json:"fighting"` + MonsterID int `db:"monster_id" json:"monster_id"` + MonsterHP int `db:"monster_hp" json:"monster_hp"` + MonsterSleep int `db:"monster_sleep" json:"monster_sleep"` + MonsterImmune int `db:"monster_immune" json:"monster_immune"` + UberDamage int `db:"uber_damage" json:"uber_damage"` + UberDefense int `db:"uber_defense" json:"uber_defense"` + HP int `db:"hp" json:"hp"` + MP int `db:"mp" json:"mp"` + TP int `db:"tp" json:"tp"` + MaxHP int `db:"max_hp" json:"max_hp"` + MaxMP int `db:"max_mp" json:"max_mp"` + MaxTP int `db:"max_tp" json:"max_tp"` + Level int `db:"level" json:"level"` + Gold int `db:"gold" json:"gold"` + Exp int `db:"exp" json:"exp"` + GoldBonus int `db:"gold_bonus" json:"gold_bonus"` + ExpBonus int `db:"exp_bonus" json:"exp_bonus"` + Strength int `db:"strength" json:"strength"` + Dexterity int `db:"dexterity" json:"dexterity"` + Attack int `db:"attack" json:"attack"` + Defense int `db:"defense" json:"defense"` + WeaponID int `db:"weapon_id" json:"weapon_id"` + ArmorID int `db:"armor_id" json:"armor_id"` + ShieldID int `db:"shield_id" json:"shield_id"` + Slot1ID int `db:"slot_1_id" json:"slot_1_id"` + Slot2ID int `db:"slot_2_id" json:"slot_2_id"` + Slot3ID int `db:"slot_3_id" json:"slot_3_id"` + WeaponName string `db:"weapon_name" json:"weapon_name"` + ArmorName string `db:"armor_name" json:"armor_name"` + ShieldName string `db:"shield_name" json:"shield_name"` + Slot1Name string `db:"slot_1_name" json:"slot_1_name"` + Slot2Name string `db:"slot_2_name" json:"slot_2_name"` + Slot3Name string `db:"slot_3_name" json:"slot_3_name"` + DropCode int `db:"drop_code" json:"drop_code"` + Spells string `db:"spells" json:"spells"` + Towns string `db:"towns" json:"towns"` } -// userColumns returns the column list for user queries (excluding id for inserts) +var userScanner = scanner.New[User]() + +// userColumns returns the column list for user queries func userColumns() string { - return `id, username, password, email, verified, token, registered, last_online, auth, - x, y, class_id, currently, fighting, monster_id, monster_hp, monster_sleep, monster_immune, - uber_damage, uber_defense, hp, mp, tp, max_hp, max_mp, max_tp, level, gold, exp, - gold_bonus, exp_bonus, strength, dexterity, attack, defense, weapon_id, armor_id, shield_id, - slot_1_id, slot_2_id, slot_3_id, weapon_name, armor_name, shield_name, - slot_1_name, slot_2_name, slot_3_name, drop_code, spells, towns` + return userScanner.Columns() } -// scanUser populates a User struct from a sqlite.Stmt +// scanUser populates a User struct using the fast scanner func scanUser(stmt *sqlite.Stmt) *User { - return &User{ - ID: stmt.ColumnInt(0), - Username: stmt.ColumnText(1), - Password: stmt.ColumnText(2), - Email: stmt.ColumnText(3), - Verified: stmt.ColumnInt(4), - Token: stmt.ColumnText(5), - Registered: stmt.ColumnInt64(6), - LastOnline: stmt.ColumnInt64(7), - Auth: stmt.ColumnInt(8), - X: stmt.ColumnInt(9), - Y: stmt.ColumnInt(10), - ClassID: stmt.ColumnInt(11), - Currently: stmt.ColumnText(12), - Fighting: stmt.ColumnInt(13), - MonsterID: stmt.ColumnInt(14), - MonsterHP: stmt.ColumnInt(15), - MonsterSleep: stmt.ColumnInt(16), - MonsterImmune: stmt.ColumnInt(17), - UberDamage: stmt.ColumnInt(18), - UberDefense: stmt.ColumnInt(19), - HP: stmt.ColumnInt(20), - MP: stmt.ColumnInt(21), - TP: stmt.ColumnInt(22), - MaxHP: stmt.ColumnInt(23), - MaxMP: stmt.ColumnInt(24), - MaxTP: stmt.ColumnInt(25), - Level: stmt.ColumnInt(26), - Gold: stmt.ColumnInt(27), - Exp: stmt.ColumnInt(28), - GoldBonus: stmt.ColumnInt(29), - ExpBonus: stmt.ColumnInt(30), - Strength: stmt.ColumnInt(31), - Dexterity: stmt.ColumnInt(32), - Attack: stmt.ColumnInt(33), - Defense: stmt.ColumnInt(34), - WeaponID: stmt.ColumnInt(35), - ArmorID: stmt.ColumnInt(36), - ShieldID: stmt.ColumnInt(37), - Slot1ID: stmt.ColumnInt(38), - Slot2ID: stmt.ColumnInt(39), - Slot3ID: stmt.ColumnInt(40), - WeaponName: stmt.ColumnText(41), - ArmorName: stmt.ColumnText(42), - ShieldName: stmt.ColumnText(43), - Slot1Name: stmt.ColumnText(44), - Slot2Name: stmt.ColumnText(45), - Slot3Name: stmt.ColumnText(46), - DropCode: stmt.ColumnInt(47), - Spells: stmt.ColumnText(48), - Towns: stmt.ColumnText(49), - } + user := &User{} + userScanner.Scan(stmt, user) + return user } // Find retrieves a user by ID @@ -515,19 +464,19 @@ func (u *User) ToMap() map[string]any { "DropCode": u.DropCode, "Spells": u.Spells, "Towns": u.Towns, - + // Computed values - "IsVerified": u.IsVerified(), - "IsAdmin": u.IsAdmin(), - "IsModerator": u.IsModerator(), - "IsFighting": u.IsFighting(), - "IsAlive": u.IsAlive(), + "IsVerified": u.IsVerified(), + "IsAdmin": u.IsAdmin(), + "IsModerator": u.IsModerator(), + "IsFighting": u.IsFighting(), + "IsAlive": u.IsAlive(), "RegisteredTime": u.RegisteredTime(), "LastOnlineTime": u.LastOnlineTime(), - "Equipment": u.GetEquipment(), - "Stats": u.GetStats(), - "Position": map[string]int{"X": u.X, "Y": u.Y}, - "SpellIDs": u.GetSpellIDs(), - "TownIDs": u.GetTownIDs(), + "Equipment": u.GetEquipment(), + "Stats": u.GetStats(), + "Position": map[string]int{"X": u.X, "Y": u.Y}, + "SpellIDs": u.GetSpellIDs(), + "TownIDs": u.GetTownIDs(), } } diff --git a/internal/utils/scanner/scanner.go b/internal/utils/scanner/scanner.go new file mode 100644 index 0000000..1ecc13b --- /dev/null +++ b/internal/utils/scanner/scanner.go @@ -0,0 +1,106 @@ +// Package scanner provides fast struct scanning for SQLite results without runtime reflection +package scanner + +import ( + "reflect" + "strings" + "unsafe" + + "zombiezen.com/go/sqlite" +) + +// ScanFunc defines how to scan a column into a field +type ScanFunc func(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) + +// Scanner holds pre-compiled scanning information for a struct type +type Scanner struct { + scanners []ScanFunc + offsets []uintptr + columns []string +} + +// Predefined scan functions for common types +func scanInt(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) { + *(*int)(fieldPtr) = stmt.ColumnInt(colIndex) +} + +func scanInt64(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) { + *(*int64)(fieldPtr) = stmt.ColumnInt64(colIndex) +} + +func scanString(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) { + *(*string)(fieldPtr) = stmt.ColumnText(colIndex) +} + +func scanFloat64(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) { + *(*float64)(fieldPtr) = stmt.ColumnFloat(colIndex) +} + +func scanBool(stmt *sqlite.Stmt, colIndex int, fieldPtr unsafe.Pointer) { + *(*bool)(fieldPtr) = stmt.ColumnInt(colIndex) != 0 +} + +// New creates a scanner for the given struct type using reflection once at creation time +func New[T any]() *Scanner { + var zero T + typ := reflect.TypeOf(zero) + + var scanners []ScanFunc + var offsets []uintptr + var columns []string + + for i := 0; i < typ.NumField(); i++ { + field := typ.Field(i) + + // Skip fields without db tag or with "-" + dbTag := field.Tag.Get("db") + if dbTag == "" || dbTag == "-" { + continue + } + + columns = append(columns, dbTag) + offsets = append(offsets, field.Offset) + + // Map field types to scan functions + switch field.Type.Kind() { + case reflect.Int: + scanners = append(scanners, scanInt) + case reflect.Int64: + scanners = append(scanners, scanInt64) + case reflect.String: + scanners = append(scanners, scanString) + case reflect.Float64: + scanners = append(scanners, scanFloat64) + case reflect.Bool: + scanners = append(scanners, scanBool) + default: + // Fallback to string for unknown types + scanners = append(scanners, scanString) + } + } + + return &Scanner{ + scanners: scanners, + offsets: offsets, + columns: columns, + } +} + +// Columns returns the comma-separated column list for SQL queries +func (s *Scanner) Columns() string { + return strings.Join(s.columns, ", ") +} + +// Scan fills the destination struct with data from the SQLite statement +// This method uses no reflection and operates at near-native performance +func (s *Scanner) Scan(stmt *sqlite.Stmt, dest any) { + // Get pointer to the struct data + ptr := (*[2]uintptr)(unsafe.Pointer(&dest)) + structPtr := unsafe.Pointer(ptr[1]) + + // Scan each field using pre-compiled function pointers and offsets + for i := 0; i < len(s.scanners); i++ { + fieldPtr := unsafe.Add(structPtr, s.offsets[i]) + s.scanners[i](stmt, i, fieldPtr) + } +}