first pass migrate back to sqlite
This commit is contained in:
parent
68ec8ce5ea
commit
412baeb46d
142
data/fights.json
142
data/fights.json
@ -962,5 +962,147 @@
|
||||
],
|
||||
"created": 1755789095,
|
||||
"updated": 1755789098
|
||||
},
|
||||
{
|
||||
"id": 17,
|
||||
"user_id": 1,
|
||||
"monster_id": 7,
|
||||
"monster_hp": 0,
|
||||
"monster_max_hp": 12,
|
||||
"monster_sleep": 0,
|
||||
"monster_immune": 1,
|
||||
"uber_damage": 0,
|
||||
"uber_defense": 0,
|
||||
"first_strike": false,
|
||||
"turn": 6,
|
||||
"ran_away": false,
|
||||
"victory": true,
|
||||
"won": true,
|
||||
"reward_gold": 3,
|
||||
"reward_exp": 9,
|
||||
"actions": [
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Shade"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Shade"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Shade"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Shade"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Shade"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 11,
|
||||
"n": "Shade"
|
||||
}
|
||||
],
|
||||
"created": 1755806329,
|
||||
"updated": 1755806335
|
||||
},
|
||||
{
|
||||
"id": 18,
|
||||
"user_id": 1,
|
||||
"monster_id": 4,
|
||||
"monster_hp": 4,
|
||||
"monster_max_hp": 10,
|
||||
"monster_sleep": 0,
|
||||
"monster_immune": 0,
|
||||
"uber_damage": 0,
|
||||
"uber_defense": 0,
|
||||
"first_strike": false,
|
||||
"turn": 5,
|
||||
"ran_away": false,
|
||||
"victory": true,
|
||||
"won": false,
|
||||
"reward_gold": 0,
|
||||
"reward_exp": 0,
|
||||
"actions": [
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Creature"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Creature"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Creature"
|
||||
},
|
||||
{
|
||||
"t": 7,
|
||||
"n": "You failed to run away!"
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Creature"
|
||||
},
|
||||
{
|
||||
"t": 7,
|
||||
"n": "You failed to run away!"
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Creature"
|
||||
}
|
||||
],
|
||||
"created": 1755806697,
|
||||
"updated": 1755806703
|
||||
}
|
||||
]
|
11
go.mod
11
go.mod
@ -10,8 +10,19 @@ require (
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
modernc.org/libc v1.65.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.37.1 // indirect
|
||||
zombiezen.com/go/sqlite v1.4.2 // indirect
|
||||
)
|
||||
|
23
go.sum
23
go.sum
@ -4,8 +4,18 @@ git.sharkk.net/Sharkk/Sushi v1.1.1 h1:ynU16l6vAhY/JUwHlI4zMQiPuL9lcs88W/mAGZsL4R
|
||||
git.sharkk.net/Sharkk/Sushi v1.1.1/go.mod h1:S84ACGkuZ+BKzBO4lb5WQnm5aw9+l7VSO2T1bjzxL3o=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||
@ -14,5 +24,18 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||
zombiezen.com/go/sqlite v1.4.2 h1:KZXLrBuJ7tKNEm+VJcApLMeQbhmAUOKA5VWS93DfFRo=
|
||||
zombiezen.com/go/sqlite v1.4.2/go.mod h1:5Kd4taTAD4MkBzT25mQ9uaAlLjyR0rFhsR6iINO70jc=
|
||||
|
351
internal/database/wrapper.go
Normal file
351
internal/database/wrapper.go
Normal file
@ -0,0 +1,351 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"zombiezen.com/go/sqlite"
|
||||
"zombiezen.com/go/sqlite/sqlitex"
|
||||
)
|
||||
|
||||
var placeholderRegex = regexp.MustCompile(`%[sd]`)
|
||||
|
||||
var db *sqlite.Conn
|
||||
|
||||
// Init initializes the database connection with WAL mode and cache settings
|
||||
func Init(dbPath string) error {
|
||||
conn, err := sqlite.OpenConn(dbPath, sqlite.OpenReadWrite|sqlite.OpenCreate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open database: %w", err)
|
||||
}
|
||||
|
||||
// Enable WAL mode
|
||||
if err := sqlitex.Execute(conn, "PRAGMA journal_mode=WAL", nil); err != nil {
|
||||
conn.Close()
|
||||
return fmt.Errorf("failed to enable WAL mode: %w", err)
|
||||
}
|
||||
|
||||
// Set generous cache size (64MB)
|
||||
if err := sqlitex.Execute(conn, "PRAGMA cache_size=-65536", nil); err != nil {
|
||||
conn.Close()
|
||||
return fmt.Errorf("failed to set cache size: %w", err)
|
||||
}
|
||||
|
||||
// Enable foreign keys
|
||||
if err := sqlitex.Execute(conn, "PRAGMA foreign_keys=ON", nil); err != nil {
|
||||
conn.Close()
|
||||
return fmt.Errorf("failed to enable foreign keys: %w", err)
|
||||
}
|
||||
|
||||
db = conn
|
||||
return nil
|
||||
}
|
||||
|
||||
// DB returns the global database connection
|
||||
func DB() *sqlite.Conn {
|
||||
if db == nil {
|
||||
panic("database not initialized - call Init() first")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
// Scan scans a SQLite statement result into a struct using field names
|
||||
func Scan(stmt *sqlite.Stmt, dest any) error {
|
||||
v := reflect.ValueOf(dest)
|
||||
if v.Kind() != reflect.Pointer || v.Elem().Kind() != reflect.Struct {
|
||||
return fmt.Errorf("dest must be a pointer to struct")
|
||||
}
|
||||
|
||||
elem := v.Elem()
|
||||
typ := elem.Type()
|
||||
|
||||
for i := 0; i < typ.NumField(); i++ {
|
||||
field := typ.Field(i)
|
||||
columnName := toSnakeCase(field.Name)
|
||||
|
||||
fieldValue := elem.Field(i)
|
||||
if !fieldValue.CanSet() {
|
||||
continue
|
||||
}
|
||||
|
||||
// Find column index by name
|
||||
colIndex := -1
|
||||
for j := 0; j < stmt.ColumnCount(); j++ {
|
||||
if stmt.ColumnName(j) == columnName {
|
||||
colIndex = j
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if colIndex == -1 {
|
||||
continue // Column not found
|
||||
}
|
||||
|
||||
switch fieldValue.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
fieldValue.SetInt(stmt.ColumnInt64(colIndex))
|
||||
case reflect.String:
|
||||
fieldValue.SetString(stmt.ColumnText(colIndex))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
fieldValue.SetFloat(stmt.ColumnFloat(colIndex))
|
||||
case reflect.Bool:
|
||||
fieldValue.SetBool(stmt.ColumnInt(colIndex) != 0)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query executes a query with fmt-style placeholders and automatically binds parameters
|
||||
func Query(query string, args ...any) (*sqlite.Stmt, error) {
|
||||
// Replace fmt placeholders with SQLite placeholders
|
||||
convertedQuery, paramTypes := convertPlaceholders(query)
|
||||
|
||||
stmt, err := DB().Prepare(convertedQuery)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Bind parameters with correct types
|
||||
for i, arg := range args {
|
||||
if i >= len(paramTypes) {
|
||||
break
|
||||
}
|
||||
|
||||
switch paramTypes[i] {
|
||||
case "s": // string
|
||||
if s, ok := arg.(string); ok {
|
||||
stmt.BindText(i+1, s)
|
||||
} else {
|
||||
stmt.BindText(i+1, fmt.Sprintf("%v", arg))
|
||||
}
|
||||
case "d": // integer
|
||||
switch v := arg.(type) {
|
||||
case int:
|
||||
stmt.BindInt64(i+1, int64(v))
|
||||
case int32:
|
||||
stmt.BindInt64(i+1, int64(v))
|
||||
case int64:
|
||||
stmt.BindInt64(i+1, v)
|
||||
case float64:
|
||||
stmt.BindInt64(i+1, int64(v))
|
||||
default:
|
||||
if i64, err := strconv.ParseInt(fmt.Sprintf("%v", arg), 10, 64); err == nil {
|
||||
stmt.BindInt64(i+1, i64)
|
||||
} else {
|
||||
stmt.BindInt64(i+1, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return stmt, nil
|
||||
}
|
||||
|
||||
// Get executes a query and returns the first row
|
||||
func Get(dest any, query string, args ...any) error {
|
||||
stmt, err := Query(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Finalize()
|
||||
|
||||
hasRow, err := stmt.Step()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasRow {
|
||||
return fmt.Errorf("no rows found")
|
||||
}
|
||||
|
||||
return Scan(stmt, dest)
|
||||
}
|
||||
|
||||
// Select executes a query and scans all rows into a slice
|
||||
func Select(dest any, query string, args ...any) error {
|
||||
destValue := reflect.ValueOf(dest)
|
||||
if destValue.Kind() != reflect.Ptr || destValue.Elem().Kind() != reflect.Slice {
|
||||
return fmt.Errorf("dest must be a pointer to slice")
|
||||
}
|
||||
|
||||
sliceValue := destValue.Elem()
|
||||
elemType := sliceValue.Type().Elem()
|
||||
|
||||
// Ensure element type is a pointer to struct
|
||||
if elemType.Kind() != reflect.Ptr || elemType.Elem().Kind() != reflect.Struct {
|
||||
return fmt.Errorf("slice elements must be pointers to structs")
|
||||
}
|
||||
|
||||
stmt, err := Query(query, args...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stmt.Finalize()
|
||||
|
||||
for {
|
||||
hasRow, err := stmt.Step()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasRow {
|
||||
break
|
||||
}
|
||||
|
||||
// Create new instance of the element type
|
||||
newElem := reflect.New(elemType.Elem())
|
||||
if err := Scan(stmt, newElem.Interface()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
sliceValue.Set(reflect.Append(sliceValue, newElem))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Exec executes a statement with fmt-style placeholders
|
||||
func Exec(query string, args ...any) error {
|
||||
convertedQuery, paramTypes := convertPlaceholders(query)
|
||||
|
||||
sqlArgs := make([]any, len(args))
|
||||
for i, arg := range args {
|
||||
if i < len(paramTypes) && paramTypes[i] == "d" {
|
||||
// Convert to int64 for integer parameters
|
||||
switch v := arg.(type) {
|
||||
case int:
|
||||
sqlArgs[i] = int64(v)
|
||||
case int32:
|
||||
sqlArgs[i] = int64(v)
|
||||
case int64:
|
||||
sqlArgs[i] = v
|
||||
default:
|
||||
sqlArgs[i] = arg
|
||||
}
|
||||
} else {
|
||||
sqlArgs[i] = arg
|
||||
}
|
||||
}
|
||||
|
||||
return sqlitex.Execute(DB(), convertedQuery, &sqlitex.ExecOptions{
|
||||
Args: sqlArgs,
|
||||
})
|
||||
}
|
||||
|
||||
// Update updates specific fields in the database
|
||||
func Update(tableName string, fields map[string]any, whereField string, whereValue any) error {
|
||||
if len(fields) == 0 {
|
||||
return nil // No changes
|
||||
}
|
||||
|
||||
// Build UPDATE query
|
||||
setParts := make([]string, 0, len(fields))
|
||||
args := make([]any, 0, len(fields)+1)
|
||||
|
||||
for field, value := range fields {
|
||||
setParts = append(setParts, field+" = ?")
|
||||
args = append(args, value)
|
||||
}
|
||||
|
||||
args = append(args, whereValue)
|
||||
|
||||
query := fmt.Sprintf("UPDATE %s SET %s WHERE %s = ?",
|
||||
tableName, strings.Join(setParts, ", "), whereField)
|
||||
|
||||
return sqlitex.Execute(DB(), query, &sqlitex.ExecOptions{
|
||||
Args: args,
|
||||
})
|
||||
}
|
||||
|
||||
// Insert inserts a struct into the database
|
||||
func Insert(tableName string, obj any, excludeFields ...string) (int64, error) {
|
||||
v := reflect.ValueOf(obj)
|
||||
if v.Kind() == reflect.Pointer {
|
||||
v = v.Elem()
|
||||
}
|
||||
t := v.Type()
|
||||
|
||||
exclude := make(map[string]bool)
|
||||
for _, field := range excludeFields {
|
||||
exclude[toSnakeCase(field)] = true
|
||||
}
|
||||
|
||||
var columns []string
|
||||
var placeholders []string
|
||||
var args []any
|
||||
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
columnName := toSnakeCase(field.Name)
|
||||
if exclude[columnName] {
|
||||
continue
|
||||
}
|
||||
|
||||
columns = append(columns, columnName)
|
||||
placeholders = append(placeholders, "?")
|
||||
args = append(args, v.Field(i).Interface())
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("INSERT INTO %s (%s) VALUES (%s)",
|
||||
tableName, strings.Join(columns, ", "), strings.Join(placeholders, ", "))
|
||||
|
||||
stmt, err := DB().Prepare(query)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer stmt.Finalize()
|
||||
|
||||
// Bind parameters
|
||||
for i, arg := range args {
|
||||
switch v := arg.(type) {
|
||||
case string:
|
||||
stmt.BindText(i+1, v)
|
||||
case int, int32, int64:
|
||||
stmt.BindInt64(i+1, reflect.ValueOf(v).Int())
|
||||
case float32, float64:
|
||||
stmt.BindFloat(i+1, reflect.ValueOf(v).Float())
|
||||
default:
|
||||
stmt.BindText(i+1, fmt.Sprintf("%v", v))
|
||||
}
|
||||
}
|
||||
|
||||
_, err = stmt.Step()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
return DB().LastInsertRowID(), nil
|
||||
}
|
||||
|
||||
func convertPlaceholders(query string) (string, []string) {
|
||||
var paramTypes []string
|
||||
|
||||
convertedQuery := placeholderRegex.ReplaceAllStringFunc(query, func(match string) string {
|
||||
paramTypes = append(paramTypes, match[1:]) // Remove % prefix
|
||||
return "?"
|
||||
})
|
||||
|
||||
return convertedQuery, paramTypes
|
||||
}
|
||||
|
||||
// toSnakeCase converts PascalCase to snake_case
|
||||
func toSnakeCase(s string) string {
|
||||
var result strings.Builder
|
||||
runes := []rune(s)
|
||||
|
||||
for i, r := range runes {
|
||||
if i > 0 && unicode.IsUpper(r) {
|
||||
// Don't add underscore if previous char was also uppercase
|
||||
// unless next char is lowercase (end of acronym)
|
||||
if !unicode.IsUpper(runes[i-1]) ||
|
||||
(i+1 < len(runes) && unicode.IsLower(runes[i+1])) {
|
||||
result.WriteByte('_')
|
||||
}
|
||||
}
|
||||
result.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
return result.String()
|
||||
}
|
@ -5,80 +5,33 @@ import (
|
||||
"math"
|
||||
"slices"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"dk/internal/database"
|
||||
"dk/internal/helpers"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Town represents a town in the game
|
||||
type Town struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name" db:"required,unique"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
InnCost int `json:"inn_cost"`
|
||||
MapCost int `json:"map_cost"`
|
||||
TPCost int `json:"tp_cost"`
|
||||
ShopList string `json:"shop_list"`
|
||||
ID int
|
||||
Name string
|
||||
X int
|
||||
Y int
|
||||
InnCost int
|
||||
MapCost int
|
||||
TPCost int
|
||||
ShopList string
|
||||
}
|
||||
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Town]
|
||||
|
||||
// coordsKey creates a key for coordinate-based lookup
|
||||
func coordsKey(x, y int) string {
|
||||
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
||||
}
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
store = nigiri.NewBaseStore[Town]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byName", nigiri.BuildCaseInsensitiveLookupIndex(func(t *Town) string {
|
||||
return t.Name
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byCoords", nigiri.BuildStringLookupIndex(func(t *Town) string {
|
||||
return coordsKey(t.X, t.Y)
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byInnCost", nigiri.BuildIntGroupIndex(func(t *Town) int {
|
||||
return t.InnCost
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byTPCost", nigiri.BuildIntGroupIndex(func(t *Town) int {
|
||||
return t.TPCost
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByID", nigiri.BuildSortedListIndex(func(a, b *Town) bool {
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
// GetStore returns the towns store
|
||||
func GetStore() *nigiri.BaseStore[Town] {
|
||||
if store == nil {
|
||||
panic("towns store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Creates a new Town with sensible defaults
|
||||
// New creates a new Town with sensible defaults
|
||||
func New() *Town {
|
||||
return &Town{
|
||||
Name: "",
|
||||
X: 0, // Default coordinates
|
||||
X: 0,
|
||||
Y: 0,
|
||||
InnCost: 50, // Default inn cost
|
||||
MapCost: 100, // Default map cost
|
||||
TPCost: 25, // Default teleport cost
|
||||
ShopList: "", // No items by default
|
||||
InnCost: 50,
|
||||
MapCost: 100,
|
||||
TPCost: 25,
|
||||
ShopList: "",
|
||||
}
|
||||
}
|
||||
|
||||
@ -100,85 +53,86 @@ func (t *Town) Validate() error {
|
||||
}
|
||||
|
||||
// CRUD operations
|
||||
func (t *Town) Save() error {
|
||||
if t.ID == 0 {
|
||||
id, err := store.Create(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(t.ID, t)
|
||||
}
|
||||
|
||||
func (t *Town) Delete() error {
|
||||
store.Remove(t.ID)
|
||||
return nil
|
||||
return database.Exec("DELETE FROM towns WHERE id = %d", t.ID)
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (t *Town) Insert() error {
|
||||
id, err := store.Create(t)
|
||||
id, err := database.Insert("towns", t, "ID")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.ID = id
|
||||
t.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query functions
|
||||
func Find(id int) (*Town, error) {
|
||||
town, exists := store.Find(id)
|
||||
if !exists {
|
||||
var town Town
|
||||
err := database.Get(&town, "SELECT * FROM towns WHERE id = %d", id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("town with ID %d not found", id)
|
||||
}
|
||||
return town, nil
|
||||
return &town, nil
|
||||
}
|
||||
|
||||
func All() ([]*Town, error) {
|
||||
return store.AllSorted("allByID"), nil
|
||||
var towns []*Town
|
||||
err := database.Select(&towns, "SELECT * FROM towns ORDER BY id ASC")
|
||||
return towns, err
|
||||
}
|
||||
|
||||
func ByName(name string) (*Town, error) {
|
||||
town, exists := store.LookupByIndex("byName", strings.ToLower(name))
|
||||
if !exists {
|
||||
var town Town
|
||||
err := database.Get(&town, "SELECT * FROM towns WHERE name = %s COLLATE NOCASE", name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("town with name '%s' not found", name)
|
||||
}
|
||||
return town, nil
|
||||
return &town, nil
|
||||
}
|
||||
|
||||
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
||||
return store.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return t.InnCost <= maxCost
|
||||
}), nil
|
||||
var towns []*Town
|
||||
err := database.Select(&towns, "SELECT * FROM towns WHERE inn_cost <= %d ORDER BY id ASC", maxCost)
|
||||
return towns, err
|
||||
}
|
||||
|
||||
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
||||
return store.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return t.TPCost <= maxCost
|
||||
}), nil
|
||||
var towns []*Town
|
||||
err := database.Select(&towns, "SELECT * FROM towns WHERE tp_cost <= %d ORDER BY id ASC", maxCost)
|
||||
return towns, err
|
||||
}
|
||||
|
||||
func ByCoords(x, y int) (*Town, error) {
|
||||
town, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
||||
if !exists {
|
||||
var town Town
|
||||
err := database.Get(&town, "SELECT * FROM towns WHERE x = %d AND y = %d", x, y)
|
||||
if err != nil {
|
||||
return nil, nil // Return nil if not found (like original)
|
||||
}
|
||||
return town, nil
|
||||
return &town, nil
|
||||
}
|
||||
|
||||
func ExistsAt(x, y int) bool {
|
||||
_, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
||||
return exists
|
||||
var count int
|
||||
err := database.Get(&count, "SELECT COUNT(*) FROM towns WHERE x = %d AND y = %d", x, y)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
||||
maxDistance2 := float64(maxDistance * maxDistance)
|
||||
var towns []*Town
|
||||
err := database.Select(&towns, "SELECT * FROM towns")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := store.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return t.DistanceFromSquared(fromX, fromY) <= maxDistance2
|
||||
})
|
||||
maxDistance2 := float64(maxDistance * maxDistance)
|
||||
var result []*Town
|
||||
|
||||
for _, town := range towns {
|
||||
if town.DistanceFromSquared(fromX, fromY) <= maxDistance2 {
|
||||
result = append(result, town)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by distance, then by ID
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
@ -244,14 +198,3 @@ func (t *Town) SetPosition(x, y int) {
|
||||
t.X = x
|
||||
t.Y = y
|
||||
}
|
||||
|
||||
// Legacy compatibility functions (will be removed later)
|
||||
func LoadData(dataPath string) error {
|
||||
// No longer needed - Nigiri handles this
|
||||
return nil
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
// No longer needed - Nigiri handles this
|
||||
return nil
|
||||
}
|
||||
|
@ -3,109 +3,59 @@ package users
|
||||
import (
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"dk/internal/database"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/helpers/exp"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// User represents a user in the game
|
||||
type User struct {
|
||||
ID int `json:"id"`
|
||||
Username string `json:"username" db:"required,unique"`
|
||||
Password string `json:"password" db:"required"`
|
||||
Email string `json:"email" db:"required,unique"`
|
||||
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"`
|
||||
FightID int `json:"fight_id"`
|
||||
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" db:"index"`
|
||||
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"`
|
||||
Spells string `json:"spells"`
|
||||
Towns string `json:"towns"`
|
||||
}
|
||||
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[User]
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
store = nigiri.NewBaseStore[User]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byUsername", nigiri.BuildCaseInsensitiveLookupIndex(func(u *User) string {
|
||||
return u.Username
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byEmail", nigiri.BuildStringLookupIndex(func(u *User) string {
|
||||
return u.Email
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(u *User) int {
|
||||
return u.Level
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByRegistered", nigiri.BuildSortedListIndex(func(a, b *User) bool {
|
||||
if a.Registered != b.Registered {
|
||||
return a.Registered > b.Registered // DESC
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByLevelExp", nigiri.BuildSortedListIndex(func(a, b *User) bool {
|
||||
if a.Level != b.Level {
|
||||
return a.Level > b.Level // Level DESC
|
||||
}
|
||||
if a.Exp != b.Exp {
|
||||
return a.Exp > b.Exp // Exp DESC
|
||||
}
|
||||
return a.ID < b.ID // ID ASC
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
// GetStore returns the users store
|
||||
func GetStore() *nigiri.BaseStore[User] {
|
||||
if store == nil {
|
||||
panic("users store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
ID int
|
||||
Username string
|
||||
Password string
|
||||
Email string
|
||||
Verified int
|
||||
Token string
|
||||
Registered int64
|
||||
LastOnline int64
|
||||
Auth int
|
||||
X int
|
||||
Y int
|
||||
ClassID int
|
||||
Currently string
|
||||
FightID int
|
||||
HP int
|
||||
MP int
|
||||
TP int
|
||||
MaxHP int
|
||||
MaxMP int
|
||||
MaxTP int
|
||||
Level int
|
||||
Gold int
|
||||
Exp int
|
||||
GoldBonus int
|
||||
ExpBonus int
|
||||
Strength int
|
||||
Dexterity int
|
||||
Attack int
|
||||
Defense int
|
||||
WeaponID int
|
||||
ArmorID int
|
||||
ShieldID int
|
||||
Slot1ID int
|
||||
Slot2ID int
|
||||
Slot3ID int
|
||||
WeaponName string
|
||||
ArmorName string
|
||||
ShieldName string
|
||||
Slot1Name string
|
||||
Slot2Name string
|
||||
Slot3Name string
|
||||
Spells string
|
||||
Towns string
|
||||
}
|
||||
|
||||
// New creates a new User with sensible defaults
|
||||
@ -166,94 +116,73 @@ func (u *User) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CRUD operations
|
||||
func (u *User) Save() error {
|
||||
if u.ID == 0 {
|
||||
id, err := store.Create(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(u.ID, u)
|
||||
}
|
||||
|
||||
func (u *User) Delete() error {
|
||||
store.Remove(u.ID)
|
||||
return nil
|
||||
return database.Exec("DELETE FROM users WHERE id = %d", u.ID)
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (u *User) Insert() error {
|
||||
id, err := store.Create(u)
|
||||
id, err := database.Insert("users", u, "id")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.ID = id
|
||||
u.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query functions
|
||||
func Find(id int) (*User, error) {
|
||||
user, exists := store.Find(id)
|
||||
if !exists {
|
||||
var user User
|
||||
err := database.Get(&user, "SELECT * FROM users WHERE id = %d", id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user with ID %d not found", id)
|
||||
}
|
||||
return user, nil
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func GetByID(id int) *User {
|
||||
user, exists := store.Find(id)
|
||||
if !exists {
|
||||
user, err := Find(id)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func All() ([]*User, error) {
|
||||
return store.AllSorted("allByRegistered"), nil
|
||||
var users []*User
|
||||
err := database.Select(&users, "SELECT * FROM users ORDER BY registered DESC, id DESC")
|
||||
return users, err
|
||||
}
|
||||
|
||||
func ByUsername(username string) (*User, error) {
|
||||
user, exists := store.LookupByIndex("byUsername", strings.ToLower(username))
|
||||
if !exists {
|
||||
var user User
|
||||
err := database.Get(&user, "SELECT * FROM users WHERE username = %s COLLATE NOCASE", username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user with username '%s' not found", username)
|
||||
}
|
||||
return user, nil
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func ByEmail(email string) (*User, error) {
|
||||
user, exists := store.LookupByIndex("Email_idx", email)
|
||||
if !exists {
|
||||
var user User
|
||||
err := database.Get(&user, "SELECT * FROM users WHERE email = %s", email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("user with email '%s' not found", email)
|
||||
}
|
||||
return user, nil
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
func ByLevel(level int) ([]*User, error) {
|
||||
return store.GroupByIndex("level_idx", level), nil
|
||||
var users []*User
|
||||
err := database.Select(&users, "SELECT * FROM users WHERE level = %d ORDER BY exp DESC, id ASC", level)
|
||||
return users, err
|
||||
}
|
||||
|
||||
func Online(within time.Duration) ([]*User, error) {
|
||||
cutoff := time.Now().Add(-within).Unix()
|
||||
|
||||
result := store.FilterByIndex("allByRegistered", func(u *User) bool {
|
||||
return u.LastOnline >= cutoff
|
||||
})
|
||||
|
||||
// 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
|
||||
var users []*User
|
||||
err := database.Select(&users, "SELECT * FROM users WHERE last_online >= %d ORDER BY last_online DESC, id ASC", cutoff)
|
||||
return users, err
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (u *User) RegisteredTime() time.Time {
|
||||
return time.Unix(u.Registered, 0)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user