migrate to sushi/nigiri
This commit is contained in:
parent
2d958bf8c4
commit
e5e9167854
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
||||
_sessions.json
|
||||
users.json
|
||||
/tmp
|
||||
wal.log
|
@ -1,8 +1,11 @@
|
||||
{
|
||||
"world_size": 200,
|
||||
"open": 1,
|
||||
"admin_email": "",
|
||||
"class_1_name": "Mage",
|
||||
"class_2_name": "Warrior",
|
||||
"class_3_name": "Paladin"
|
||||
}
|
||||
[
|
||||
{
|
||||
"id": 1,
|
||||
"world_size": 200,
|
||||
"open": 1,
|
||||
"admin_email": "",
|
||||
"class_1_name": "Mage",
|
||||
"class_2_name": "Warrior",
|
||||
"class_3_name": "Paladin"
|
||||
}
|
||||
]
|
166
data/fights.json
166
data/fights.json
@ -824,165 +824,29 @@
|
||||
"won": false,
|
||||
"reward_gold": 0,
|
||||
"reward_exp": 0,
|
||||
"actions": [
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 1
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Drakelor"
|
||||
}
|
||||
],
|
||||
"created": 1755274841,
|
||||
"updated": 1755275436
|
||||
"actions": [],
|
||||
"created": 1755222893,
|
||||
"updated": 1755222893
|
||||
},
|
||||
{
|
||||
"id": 14,
|
||||
"id": 5,
|
||||
"user_id": 1,
|
||||
"monster_id": 4,
|
||||
"monster_hp": 0,
|
||||
"monster_id": 5,
|
||||
"monster_hp": 10,
|
||||
"monster_max_hp": 10,
|
||||
"monster_sleep": 0,
|
||||
"monster_immune": 0,
|
||||
"monster_immune": 1,
|
||||
"uber_damage": 0,
|
||||
"uber_defense": 0,
|
||||
"first_strike": true,
|
||||
"turn": 5,
|
||||
"turn": 1,
|
||||
"ran_away": false,
|
||||
"victory": true,
|
||||
"won": true,
|
||||
"reward_gold": 1,
|
||||
"reward_exp": 3,
|
||||
"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": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 8,
|
||||
"d": 1,
|
||||
"n": "Creature"
|
||||
},
|
||||
{
|
||||
"t": 1,
|
||||
"d": 2
|
||||
},
|
||||
{
|
||||
"t": 11,
|
||||
"n": "Creature"
|
||||
}
|
||||
],
|
||||
"created": 1755275442,
|
||||
"updated": 1755275447
|
||||
"victory": false,
|
||||
"won": false,
|
||||
"reward_gold": 0,
|
||||
"reward_exp": 0,
|
||||
"actions": [],
|
||||
"created": 1755608716,
|
||||
"updated": 1755608716
|
||||
}
|
||||
]
|
8
go.mod
8
go.mod
@ -1,15 +1,17 @@
|
||||
module dk
|
||||
|
||||
go 1.24.6
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
github.com/valyala/fasthttp v1.64.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
git.sharkk.net/Sharkk/Nigiri v1.0.0
|
||||
git.sharkk.net/Sharkk/Sushi v1.1.0
|
||||
github.com/valyala/fasthttp v1.65.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
golang.org/x/crypto v0.41.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
)
|
||||
|
8
go.sum
8
go.sum
@ -1,11 +1,15 @@
|
||||
git.sharkk.net/Sharkk/Nigiri v1.0.0 h1:N0MvWOoX54iXjR8D1LqGIFrtMAPdaoj/32n13Ou/p90=
|
||||
git.sharkk.net/Sharkk/Nigiri v1.0.0/go.mod h1:HWpMtXaodPXE7dZXQ6tbZNL0DRV9PT65D0DOV0NAwsM=
|
||||
git.sharkk.net/Sharkk/Sushi v1.1.0 h1:mOcQlcLEl941ozjbOzHOnBAmsOcZ7Q5BkFowILwxNow=
|
||||
git.sharkk.net/Sharkk/Sushi v1.1.0/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/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/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.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
|
||||
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
|
||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
|
@ -1,171 +0,0 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"dk/internal/cookies"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
"dk/internal/session"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
const SessionCookieName = "dk_session"
|
||||
|
||||
func Middleware() router.Middleware {
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
sessionID := cookies.GetCookie(ctx, SessionCookieName)
|
||||
var sess *session.Session
|
||||
|
||||
if sessionID != "" {
|
||||
if existingSess, exists := session.Get(sessionID); exists {
|
||||
sess = existingSess
|
||||
sess.Touch()
|
||||
|
||||
if sess.UserID > 0 { // User session
|
||||
user, err := users.Find(sess.UserID)
|
||||
if err == nil && user != nil {
|
||||
ctx.SetUserValue("user", user)
|
||||
} else {
|
||||
// User not found, reset to guest session
|
||||
sess.SetUserID(0)
|
||||
}
|
||||
}
|
||||
session.Store(sess)
|
||||
setSessionCookie(ctx, sessionID)
|
||||
}
|
||||
}
|
||||
|
||||
// Create guest session if none exists
|
||||
if sess == nil {
|
||||
sess = session.Create(0) // Guest session
|
||||
setSessionCookie(ctx, sess.ID)
|
||||
}
|
||||
|
||||
ctx.SetUserValue("session", sess)
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAuth(paths ...string) router.Middleware {
|
||||
redirect := "/login"
|
||||
if len(paths) > 0 && paths[0] != "" {
|
||||
redirect = paths[0]
|
||||
}
|
||||
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
if !IsAuthenticated(ctx) {
|
||||
ctx.Redirect(redirect, fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
user.UpdateLastOnline()
|
||||
user.Save()
|
||||
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func RequireGuest(paths ...string) router.Middleware {
|
||||
redirect := "/"
|
||||
if len(paths) > 0 && paths[0] != "" {
|
||||
redirect = paths[0]
|
||||
}
|
||||
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
if IsAuthenticated(ctx) {
|
||||
fmt.Println("RequireGuest: user is authenticated")
|
||||
ctx.Redirect(redirect, fasthttp.StatusFound)
|
||||
return
|
||||
}
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func IsAuthenticated(ctx router.Ctx) bool {
|
||||
if user, ok := ctx.UserValue("user").(*users.User); ok && user != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetCurrentUser(ctx router.Ctx) *users.User {
|
||||
if user, ok := ctx.UserValue("user").(*users.User); ok {
|
||||
return user
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetCurrentSession(ctx router.Ctx) *session.Session {
|
||||
if sess, ok := ctx.UserValue("session").(*session.Session); ok {
|
||||
return sess
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func Login(ctx router.Ctx, user *users.User) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
|
||||
// Update the session to be authenticated
|
||||
sess.SetUserID(user.ID) // This updates the struct field
|
||||
sess.RegenerateID() // Generate new ID for security
|
||||
sess.SetFlash("success", fmt.Sprintf("Welcome back, %s!", user.Username))
|
||||
|
||||
// Remove any old user_id from session data if it exists
|
||||
sess.Delete("user_id")
|
||||
|
||||
session.Store(sess)
|
||||
|
||||
// Update context values
|
||||
ctx.SetUserValue("session", sess)
|
||||
ctx.SetUserValue("user", user)
|
||||
|
||||
// Update cookie with new session ID
|
||||
setSessionCookie(ctx, sess.ID)
|
||||
}
|
||||
|
||||
func Logout(ctx router.Ctx) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
if sess != nil {
|
||||
// Convert back to guest session
|
||||
sess.SetUserID(0) // Reset to guest
|
||||
sess.RegenerateID() // Generate new ID for security
|
||||
|
||||
// Clean up any user-related session data
|
||||
sess.Delete("user_id")
|
||||
|
||||
session.Store(sess)
|
||||
ctx.SetUserValue("session", sess)
|
||||
|
||||
// Update cookie with new session ID
|
||||
setSessionCookie(ctx, sess.ID)
|
||||
}
|
||||
|
||||
ctx.SetUserValue("user", nil)
|
||||
}
|
||||
|
||||
// Helper functions for session cookies
|
||||
func setSessionCookie(ctx router.Ctx, sessionID string) {
|
||||
cookies.SetSecureCookie(ctx, cookies.CookieOptions{
|
||||
Name: SessionCookieName,
|
||||
Value: sessionID,
|
||||
Path: "/",
|
||||
Expires: time.Now().Add(24 * time.Hour),
|
||||
HTTPOnly: true,
|
||||
Secure: helpers.IsHTTPS(ctx),
|
||||
SameSite: "lax",
|
||||
})
|
||||
}
|
||||
|
||||
func deleteSessionCookie(ctx router.Ctx) {
|
||||
cookies.DeleteCookie(ctx, SessionCookieName)
|
||||
}
|
@ -1,21 +1,21 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"dk/internal/auth"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/models/spells"
|
||||
"dk/internal/models/towns"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
"fmt"
|
||||
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
)
|
||||
|
||||
// LeftAside generates the data map for the left sidebar.
|
||||
// Returns an empty map when not auth'd.
|
||||
func LeftAside(ctx router.Ctx) map[string]any {
|
||||
func LeftAside(ctx sushi.Ctx) map[string]any {
|
||||
data := map[string]any{}
|
||||
|
||||
if !auth.IsAuthenticated(ctx) {
|
||||
if !ctx.IsAuthenticated() {
|
||||
return data
|
||||
}
|
||||
|
||||
@ -37,10 +37,10 @@ func LeftAside(ctx router.Ctx) map[string]any {
|
||||
|
||||
// RightAside generates the data map for the right sidebar.
|
||||
// Returns an empty map when not auth'd.
|
||||
func RightAside(ctx router.Ctx) map[string]any {
|
||||
func RightAside(ctx sushi.Ctx) map[string]any {
|
||||
data := map[string]any{}
|
||||
|
||||
if !auth.IsAuthenticated(ctx) {
|
||||
if !ctx.IsAuthenticated() {
|
||||
return data
|
||||
}
|
||||
|
||||
|
@ -6,16 +6,14 @@ import (
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"dk/internal/auth"
|
||||
"dk/internal/csrf"
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/router"
|
||||
"dk/internal/session"
|
||||
"dk/internal/template"
|
||||
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
"git.sharkk.net/Sharkk/Sushi/csrf"
|
||||
)
|
||||
|
||||
// RenderPage renders a page using the layout template with common data and additional custom data
|
||||
func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[string]any) error {
|
||||
func RenderPage(ctx sushi.Ctx, title, tmplPath string, additionalData map[string]any) error {
|
||||
if template.Cache == nil {
|
||||
return fmt.Errorf("template.Cache not initialized")
|
||||
}
|
||||
@ -25,19 +23,19 @@ func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[strin
|
||||
return fmt.Errorf("failed to load layout template: %w", err)
|
||||
}
|
||||
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
|
||||
var m runtime.MemStats
|
||||
runtime.ReadMemStats(&m)
|
||||
|
||||
sess := ctx.GetCurrentSession()
|
||||
|
||||
data := map[string]any{
|
||||
"_title": PageTitle(title),
|
||||
"authenticated": auth.IsAuthenticated(ctx),
|
||||
"authenticated": ctx.IsAuthenticated(),
|
||||
"csrf": csrf.HiddenField(ctx),
|
||||
"_totaltime": middleware.GetRequestTime(ctx),
|
||||
"_totaltime": ctx.UserValue("request_time"),
|
||||
"_version": "1.0.0",
|
||||
"_build": "dev",
|
||||
"user": auth.GetCurrentUser(ctx),
|
||||
"user": ctx.GetCurrentUser(),
|
||||
"_memalloc": m.Alloc / 1024 / 1024,
|
||||
"_errormsg": sess.GetFlashMessage("error"),
|
||||
"_successmsg": sess.GetFlashMessage("success"),
|
||||
@ -47,8 +45,7 @@ func RenderPage(ctx router.Ctx, title, tmplPath string, additionalData map[strin
|
||||
maps.Copy(data, RightAside(ctx))
|
||||
maps.Copy(data, additionalData)
|
||||
|
||||
tmpl.WriteTo(ctx, data)
|
||||
return nil
|
||||
return tmpl.WriteTo(ctx, data)
|
||||
}
|
||||
|
||||
// PageTitle returns a proper title for a rendered page. If an empty string
|
||||
|
@ -1,77 +0,0 @@
|
||||
package cookies
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type CookieOptions struct {
|
||||
Name string
|
||||
Value string
|
||||
Path string
|
||||
Domain string
|
||||
Expires time.Time
|
||||
MaxAge int
|
||||
Secure bool
|
||||
HTTPOnly bool
|
||||
SameSite string
|
||||
}
|
||||
|
||||
func SetSecureCookie(ctx *fasthttp.RequestCtx, opts CookieOptions) {
|
||||
cookie := &fasthttp.Cookie{}
|
||||
|
||||
cookie.SetKey(opts.Name)
|
||||
cookie.SetValue(opts.Value)
|
||||
|
||||
if opts.Path != "" {
|
||||
cookie.SetPath(opts.Path)
|
||||
} else {
|
||||
cookie.SetPath("/")
|
||||
}
|
||||
|
||||
if opts.Domain != "" {
|
||||
cookie.SetDomain(opts.Domain)
|
||||
}
|
||||
|
||||
if !opts.Expires.IsZero() {
|
||||
cookie.SetExpire(opts.Expires)
|
||||
}
|
||||
|
||||
if opts.MaxAge > 0 {
|
||||
cookie.SetMaxAge(opts.MaxAge)
|
||||
}
|
||||
|
||||
cookie.SetSecure(opts.Secure)
|
||||
cookie.SetHTTPOnly(opts.HTTPOnly)
|
||||
|
||||
switch opts.SameSite {
|
||||
case "strict":
|
||||
cookie.SetSameSite(fasthttp.CookieSameSiteStrictMode)
|
||||
case "lax":
|
||||
cookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
|
||||
case "none":
|
||||
cookie.SetSameSite(fasthttp.CookieSameSiteNoneMode)
|
||||
default:
|
||||
cookie.SetSameSite(fasthttp.CookieSameSiteLaxMode)
|
||||
}
|
||||
|
||||
ctx.Response.Header.SetCookie(cookie)
|
||||
}
|
||||
|
||||
func GetCookie(ctx *fasthttp.RequestCtx, name string) string {
|
||||
return string(ctx.Request.Header.Cookie(name))
|
||||
}
|
||||
|
||||
func DeleteCookie(ctx *fasthttp.RequestCtx, name string) {
|
||||
SetSecureCookie(ctx, CookieOptions{
|
||||
Name: name,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
Expires: time.Unix(0, 0),
|
||||
MaxAge: -1,
|
||||
HTTPOnly: true,
|
||||
Secure: true,
|
||||
SameSite: "lax",
|
||||
})
|
||||
}
|
@ -1,195 +0,0 @@
|
||||
// Package csrf provides Cross-Site Request Forgery (CSRF) protection
|
||||
// with session-based token storage and form helpers.
|
||||
//
|
||||
// # Basic Usage
|
||||
//
|
||||
// // Generate token and store in session
|
||||
// token := csrf.GenerateToken(ctx)
|
||||
//
|
||||
// // In templates - generate hidden input field
|
||||
// hiddenField := csrf.HiddenField(ctx)
|
||||
//
|
||||
// // Verify form submission
|
||||
// if !csrf.ValidateToken(ctx, formToken) {
|
||||
// // Handle CSRF validation failure
|
||||
// }
|
||||
//
|
||||
// # Middleware Integration
|
||||
//
|
||||
// // Add CSRF middleware to protected routes
|
||||
// r.Use(csrf.Middleware())
|
||||
package csrf
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
|
||||
"dk/internal/router"
|
||||
"dk/internal/session"
|
||||
)
|
||||
|
||||
const (
|
||||
TokenLength = 32
|
||||
TokenFieldName = "_csrf_token"
|
||||
SessionKey = "csrf_token"
|
||||
SessionCtxKey = "session"
|
||||
)
|
||||
|
||||
// GetCurrentSession retrieves the session from context
|
||||
func GetCurrentSession(ctx router.Ctx) *session.Session {
|
||||
if sess, ok := ctx.UserValue(SessionCtxKey).(*session.Session); ok {
|
||||
return sess
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateToken creates a new CSRF token and stores it in the session
|
||||
func GenerateToken(ctx router.Ctx) string {
|
||||
// Generate cryptographically secure random bytes
|
||||
tokenBytes := make([]byte, TokenLength)
|
||||
if _, err := rand.Read(tokenBytes); err != nil {
|
||||
// Fallback - this should never happen in practice
|
||||
return ""
|
||||
}
|
||||
|
||||
token := base64.URLEncoding.EncodeToString(tokenBytes)
|
||||
|
||||
// Store token in session (both guests and authenticated users have sessions)
|
||||
if sess := GetCurrentSession(ctx); sess != nil {
|
||||
StoreToken(sess, token)
|
||||
session.Store(sess)
|
||||
}
|
||||
|
||||
return token
|
||||
}
|
||||
|
||||
// GetToken retrieves the current CSRF token from session, generating one if needed
|
||||
func GetToken(ctx router.Ctx) string {
|
||||
sess := GetCurrentSession(ctx)
|
||||
if sess == nil {
|
||||
return "" // No session available
|
||||
}
|
||||
|
||||
// Check for existing token
|
||||
if existingToken := GetStoredToken(sess); existingToken != "" {
|
||||
return existingToken
|
||||
}
|
||||
|
||||
// Generate new token if none exists
|
||||
return GenerateToken(ctx)
|
||||
}
|
||||
|
||||
// ValidateToken verifies a CSRF token against the stored session token
|
||||
func ValidateToken(ctx router.Ctx, submittedToken string) bool {
|
||||
if submittedToken == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
sess := GetCurrentSession(ctx)
|
||||
if sess == nil {
|
||||
return false // No session
|
||||
}
|
||||
|
||||
storedToken := GetStoredToken(sess)
|
||||
if storedToken == "" {
|
||||
return false // No stored token
|
||||
}
|
||||
|
||||
// Use constant-time comparison to prevent timing attacks
|
||||
return subtle.ConstantTimeCompare([]byte(submittedToken), []byte(storedToken)) == 1
|
||||
}
|
||||
|
||||
// StoreToken saves a CSRF token in the session
|
||||
func StoreToken(sess *session.Session, token string) {
|
||||
sess.Set(SessionKey, token)
|
||||
}
|
||||
|
||||
// GetStoredToken retrieves the CSRF token from session
|
||||
func GetStoredToken(sess *session.Session) string {
|
||||
if token, ok := sess.Get(SessionKey); ok {
|
||||
if tokenStr, ok := token.(string); ok {
|
||||
return tokenStr
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// RotateToken generates a new token and replaces the old one in the session
|
||||
func RotateToken(ctx router.Ctx) string {
|
||||
sess := GetCurrentSession(ctx)
|
||||
if sess == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
// Generate new token (this will automatically store it)
|
||||
newToken := GenerateToken(ctx)
|
||||
return newToken
|
||||
}
|
||||
|
||||
// HiddenField generates an HTML hidden input field with the CSRF token
|
||||
func HiddenField(ctx router.Ctx) string {
|
||||
token := GetToken(ctx)
|
||||
if token == "" {
|
||||
return "" // No token available
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<input type="hidden" name="%s" value="%s">`,
|
||||
TokenFieldName, token)
|
||||
}
|
||||
|
||||
// TokenMeta generates HTML meta tag for JavaScript access to CSRF token
|
||||
func TokenMeta(ctx router.Ctx) string {
|
||||
token := GetToken(ctx)
|
||||
if token == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<meta name="csrf-token" content="%s">`, token)
|
||||
}
|
||||
|
||||
// ValidateFormToken is a convenience function to validate CSRF token from form data
|
||||
func ValidateFormToken(ctx router.Ctx) bool {
|
||||
// Try to get token from form data
|
||||
tokenBytes := ctx.PostArgs().Peek(TokenFieldName)
|
||||
if len(tokenBytes) == 0 {
|
||||
// Try from query parameters as fallback
|
||||
tokenBytes = ctx.QueryArgs().Peek(TokenFieldName)
|
||||
}
|
||||
|
||||
if len(tokenBytes) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return ValidateToken(ctx, string(tokenBytes))
|
||||
}
|
||||
|
||||
// GetTokenFromCookie retrieves a CSRF token from cookie (legacy support)
|
||||
func GetTokenFromCookie(ctx router.Ctx) string {
|
||||
return string(ctx.Request.Header.Cookie("_csrf"))
|
||||
}
|
||||
|
||||
// Middleware returns a middleware function that automatically validates CSRF tokens
|
||||
// for state-changing HTTP methods (POST, PUT, PATCH, DELETE)
|
||||
func Middleware() router.Middleware {
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
method := string(ctx.Method())
|
||||
|
||||
// Only validate CSRF for state-changing methods
|
||||
if method == "POST" || method == "PUT" || method == "PATCH" || method == "DELETE" {
|
||||
if !ValidateFormToken(ctx) {
|
||||
fmt.Println("Failed CSRF validation.")
|
||||
RotateToken(ctx)
|
||||
currentPath := string(ctx.Path())
|
||||
ctx.Redirect(currentPath, 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Continue to next handler
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
"strings"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// RequireFighting ensures the user is in a fight when accessing fight routes
|
||||
func RequireFighting() router.Middleware {
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
user, ok := ctx.UserValue("user").(*users.User)
|
||||
if !ok || user == nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
|
||||
ctx.SetBodyString("Not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
if !user.IsFighting() {
|
||||
ctx.Redirect("/", 303)
|
||||
return
|
||||
}
|
||||
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// HandleFightRedirect redirects users to appropriate locations based on fight status
|
||||
func HandleFightRedirect() router.Middleware {
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
user, ok := ctx.UserValue("user").(*users.User)
|
||||
if !ok || user == nil {
|
||||
next(ctx, params)
|
||||
return
|
||||
}
|
||||
|
||||
currentPath := string(ctx.URI().Path())
|
||||
|
||||
// If user is fighting and not on fight page, redirect to fight
|
||||
if user.IsFighting() && !strings.HasPrefix(currentPath, "/fight") {
|
||||
ctx.Redirect("/fight", 303)
|
||||
return
|
||||
}
|
||||
|
||||
// If user is not fighting and on fight page, redirect to home
|
||||
if !user.IsFighting() && strings.HasPrefix(currentPath, "/fight") {
|
||||
ctx.Redirect("/", 303)
|
||||
return
|
||||
}
|
||||
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"dk/internal/router"
|
||||
)
|
||||
|
||||
const RequestTimerKey = "request_start_time"
|
||||
|
||||
// Timing adds request timing functionality
|
||||
func Timing() router.Middleware {
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
startTime := time.Now()
|
||||
ctx.SetUserValue(RequestTimerKey, startTime)
|
||||
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetRequestTime returns the total request processing time in seconds (formatted)
|
||||
func GetRequestTime(ctx router.Ctx) string {
|
||||
startTime, ok := ctx.UserValue(RequestTimerKey).(time.Time)
|
||||
if !ok {
|
||||
return "0"
|
||||
}
|
||||
|
||||
duration := time.Since(startTime)
|
||||
seconds := duration.Seconds()
|
||||
|
||||
if seconds < 0.001 {
|
||||
return "0"
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%.3f", seconds)
|
||||
}
|
||||
|
||||
// GetRequestDuration returns the raw duration
|
||||
func GetRequestDuration(ctx router.Ctx) time.Duration {
|
||||
startTime, ok := ctx.UserValue(RequestTimerKey).(time.Time)
|
||||
if !ok {
|
||||
return 0
|
||||
}
|
||||
|
||||
return time.Since(startTime)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"dk/internal/models/towns"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// RequireTown ensures the user is in town at valid coordinates
|
||||
func RequireTown() router.Middleware {
|
||||
return func(next router.Handler) router.Handler {
|
||||
return func(ctx router.Ctx, params []string) {
|
||||
user, ok := ctx.UserValue("user").(*users.User)
|
||||
if !ok || user == nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusUnauthorized)
|
||||
ctx.SetBodyString("Not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
if user.Currently != "In Town" {
|
||||
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
||||
ctx.SetBodyString("You must be in town")
|
||||
return
|
||||
}
|
||||
|
||||
town, err := towns.ByCoords(user.X, user.Y)
|
||||
if err != nil || town == nil || town.ID == 0 {
|
||||
ctx.SetStatusCode(fasthttp.StatusForbidden)
|
||||
ctx.SetBodyString("Invalid town location")
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetUserValue("town", town)
|
||||
next(ctx, params)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +1,22 @@
|
||||
package babble
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Babble represents a global chat message in the game
|
||||
type Babble struct {
|
||||
ID int `json:"id"`
|
||||
Posted int64 `json:"posted"`
|
||||
Author string `json:"author"`
|
||||
Author string `json:"author" db:"index"`
|
||||
Babble string `json:"babble"`
|
||||
}
|
||||
|
||||
func (b *Babble) Save() error {
|
||||
return GetStore().UpdateWithRebuild(b.ID, b)
|
||||
}
|
||||
|
||||
func (b *Babble) Delete() error {
|
||||
GetStore().RemoveWithRebuild(b.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Creates a new Babble with sensible defaults
|
||||
func New() *Babble {
|
||||
return &Babble{
|
||||
Posted: time.Now().Unix(),
|
||||
Author: "",
|
||||
Babble: "",
|
||||
}
|
||||
}
|
||||
|
||||
// Validate checks if babble has valid values
|
||||
func (b *Babble) Validate() error {
|
||||
if b.Posted <= 0 {
|
||||
@ -48,58 +31,78 @@ func (b *Babble) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// BabbleStore with enhanced BaseStore
|
||||
type BabbleStore struct {
|
||||
*store.BaseStore[Babble]
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *BabbleStore {
|
||||
bs := &BabbleStore{BaseStore: store.NewBaseStore[Babble]()}
|
||||
var store *nigiri.BaseStore[Babble]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Register indices
|
||||
bs.RegisterIndex("byAuthor", store.BuildStringGroupIndex(func(b *Babble) string {
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Babble]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byAuthor", nigiri.BuildStringGroupIndex(func(b *Babble) string {
|
||||
return strings.ToLower(b.Author)
|
||||
}))
|
||||
|
||||
bs.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *Babble) bool {
|
||||
store.RegisterIndex("allByPosted", nigiri.BuildSortedListIndex(func(a, b *Babble) bool {
|
||||
if a.Posted != b.Posted {
|
||||
return a.Posted > b.Posted // DESC
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
return bs
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (bs *BabbleStore) AddBabble(babble *Babble) error {
|
||||
return bs.AddWithRebuild(babble.ID, babble)
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (bs *BabbleStore) RemoveBabble(id int) {
|
||||
bs.RemoveWithRebuild(id)
|
||||
// GetStore returns the babble store
|
||||
func GetStore() *nigiri.BaseStore[Babble] {
|
||||
if store == nil {
|
||||
panic("babble store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
func (bs *BabbleStore) UpdateBabble(babble *Babble) error {
|
||||
return bs.UpdateWithRebuild(babble.ID, babble)
|
||||
// Creates a new Babble with sensible defaults
|
||||
func New() *Babble {
|
||||
return &Babble{
|
||||
Posted: time.Now().Unix(),
|
||||
Author: "",
|
||||
Babble: "",
|
||||
}
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
bs := GetStore()
|
||||
return bs.BaseStore.LoadData(dataPath)
|
||||
// CRUD operations
|
||||
func (b *Babble) Save() error {
|
||||
if b.ID == 0 {
|
||||
id, err := store.Create(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(b.ID, b)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
bs := GetStore()
|
||||
return bs.BaseStore.SaveData(dataPath)
|
||||
func (b *Babble) Delete() error {
|
||||
store.Remove(b.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Insert with ID assignment
|
||||
func (b *Babble) Insert() error {
|
||||
id, err := store.Create(b)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
b.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Query functions
|
||||
func Find(id int) (*Babble, error) {
|
||||
bs := GetStore()
|
||||
babble, exists := bs.Find(id)
|
||||
babble, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("babble with ID %d not found", id)
|
||||
}
|
||||
@ -107,13 +110,11 @@ func Find(id int) (*Babble, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Babble, error) {
|
||||
bs := GetStore()
|
||||
return bs.AllSorted("allByPosted"), nil
|
||||
return store.AllSorted("allByPosted"), nil
|
||||
}
|
||||
|
||||
func ByAuthor(author string) ([]*Babble, error) {
|
||||
bs := GetStore()
|
||||
messages := bs.GroupByIndex("byAuthor", strings.ToLower(author))
|
||||
messages := store.GroupByIndex("byAuthor", strings.ToLower(author))
|
||||
|
||||
// Sort by posted DESC, then ID DESC
|
||||
sort.Slice(messages, func(i, j int) bool {
|
||||
@ -127,8 +128,7 @@ func ByAuthor(author string) ([]*Babble, error) {
|
||||
}
|
||||
|
||||
func Recent(limit int) ([]*Babble, error) {
|
||||
bs := GetStore()
|
||||
all := bs.AllSorted("allByPosted")
|
||||
all := store.AllSorted("allByPosted")
|
||||
if limit > len(all) {
|
||||
limit = len(all)
|
||||
}
|
||||
@ -136,23 +136,20 @@ func Recent(limit int) ([]*Babble, error) {
|
||||
}
|
||||
|
||||
func Since(since int64) ([]*Babble, error) {
|
||||
bs := GetStore()
|
||||
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||
return store.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||
return b.Posted >= since
|
||||
}), nil
|
||||
}
|
||||
|
||||
func Between(start, end int64) ([]*Babble, error) {
|
||||
bs := GetStore()
|
||||
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||
return store.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||
return b.Posted >= start && b.Posted <= end
|
||||
}), nil
|
||||
}
|
||||
|
||||
func Search(term string) ([]*Babble, error) {
|
||||
bs := GetStore()
|
||||
lowerTerm := strings.ToLower(term)
|
||||
return bs.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||
return store.FilterByIndex("allByPosted", func(b *Babble) bool {
|
||||
return strings.Contains(strings.ToLower(b.Babble), lowerTerm)
|
||||
}), nil
|
||||
}
|
||||
@ -168,15 +165,6 @@ func RecentByAuthor(author string, limit int) ([]*Babble, error) {
|
||||
return messages[:limit], nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (b *Babble) Insert() error {
|
||||
bs := GetStore()
|
||||
if b.ID == 0 {
|
||||
b.ID = bs.GetNextID()
|
||||
}
|
||||
return bs.AddBabble(b)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (b *Babble) PostedTime() time.Time {
|
||||
return time.Unix(b.Posted, 0)
|
||||
@ -279,3 +267,14 @@ func (b *Babble) HasMention(username string) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,24 +1,22 @@
|
||||
package control
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
var (
|
||||
global *Control
|
||||
configPath string
|
||||
mu sync.RWMutex
|
||||
store *nigiri.BaseStore[Control]
|
||||
db *nigiri.Collection
|
||||
global *Control
|
||||
mu sync.RWMutex
|
||||
)
|
||||
|
||||
func init() {
|
||||
global = New()
|
||||
}
|
||||
|
||||
// Control represents the game control settings
|
||||
type Control struct {
|
||||
ID int `json:"id"`
|
||||
WorldSize int `json:"world_size"`
|
||||
Open int `json:"open"`
|
||||
AdminEmail string `json:"admin_email"`
|
||||
@ -27,9 +25,46 @@ type Control struct {
|
||||
Class3Name string `json:"class_3_name"`
|
||||
}
|
||||
|
||||
// Init sets up the Nigiri store for control settings
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Control]()
|
||||
|
||||
// Load or create the singleton control instance
|
||||
all := store.GetAll()
|
||||
if len(all) == 0 {
|
||||
// Create default control settings
|
||||
global = New()
|
||||
global.ID = 1
|
||||
store.Add(1, global)
|
||||
} else {
|
||||
// Use the first (and only) control entry
|
||||
for _, ctrl := range all {
|
||||
global = ctrl
|
||||
break
|
||||
}
|
||||
// Apply defaults for any missing fields
|
||||
defaults := New()
|
||||
if global.WorldSize == 0 {
|
||||
global.WorldSize = defaults.WorldSize
|
||||
}
|
||||
if global.Class1Name == "" {
|
||||
global.Class1Name = defaults.Class1Name
|
||||
}
|
||||
if global.Class2Name == "" {
|
||||
global.Class2Name = defaults.Class2Name
|
||||
}
|
||||
if global.Class3Name == "" {
|
||||
global.Class3Name = defaults.Class3Name
|
||||
}
|
||||
store.Update(global.ID, global)
|
||||
}
|
||||
}
|
||||
|
||||
// New creates a new Control with sensible defaults
|
||||
func New() *Control {
|
||||
return &Control{
|
||||
ID: 1, // Singleton
|
||||
WorldSize: 200,
|
||||
Open: 1,
|
||||
AdminEmail: "",
|
||||
@ -39,84 +74,56 @@ func New() *Control {
|
||||
}
|
||||
}
|
||||
|
||||
// Load loads control settings from JSON file into global instance
|
||||
func Load(filename string) error {
|
||||
mu.Lock()
|
||||
configPath = filename
|
||||
mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil // Keep defaults
|
||||
}
|
||||
return fmt.Errorf("failed to read config file: %w", err)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
control := &Control{}
|
||||
if err := json.Unmarshal(data, control); err != nil {
|
||||
return fmt.Errorf("failed to parse config: %w", err)
|
||||
}
|
||||
|
||||
// Apply defaults for any missing fields
|
||||
defaults := New()
|
||||
if control.WorldSize == 0 {
|
||||
control.WorldSize = defaults.WorldSize
|
||||
}
|
||||
if control.Class1Name == "" {
|
||||
control.Class1Name = defaults.Class1Name
|
||||
}
|
||||
if control.Class2Name == "" {
|
||||
control.Class2Name = defaults.Class2Name
|
||||
}
|
||||
if control.Class3Name == "" {
|
||||
control.Class3Name = defaults.Class3Name
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
global = control
|
||||
mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// Save saves global control settings to the loaded path
|
||||
func Save() error {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
|
||||
if configPath == "" {
|
||||
return fmt.Errorf("no config path set, call Load() first")
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(global, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(configPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config file: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get returns the global control instance (thread-safe)
|
||||
func Get() *Control {
|
||||
mu.RLock()
|
||||
defer mu.RUnlock()
|
||||
if global == nil {
|
||||
panic("control not initialized - call Initialize first")
|
||||
}
|
||||
return global
|
||||
}
|
||||
|
||||
// Set updates the global control instance (thread-safe)
|
||||
func Set(control *Control) {
|
||||
func Set(control *Control) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
control.ID = 1 // Ensure it's always ID 1 (singleton)
|
||||
if err := control.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.Update(1, control); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
global = control
|
||||
return nil
|
||||
}
|
||||
|
||||
// Update updates specific fields of the control settings
|
||||
func Update(updater func(*Control)) error {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
// Create a copy to work with
|
||||
updated := *global
|
||||
updater(&updated)
|
||||
|
||||
if err := updated.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := store.Update(1, &updated); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
global = &updated
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate checks if control settings have valid values
|
||||
func (c *Control) Validate() error {
|
||||
if c.WorldSize <= 0 || c.WorldSize > 10000 {
|
||||
return fmt.Errorf("WorldSize must be between 1 and 10000")
|
||||
@ -124,6 +131,15 @@ func (c *Control) Validate() error {
|
||||
if c.Open != 0 && c.Open != 1 {
|
||||
return fmt.Errorf("Open must be 0 or 1")
|
||||
}
|
||||
if c.Class1Name == "" {
|
||||
return fmt.Errorf("Class1Name cannot be empty")
|
||||
}
|
||||
if c.Class2Name == "" {
|
||||
return fmt.Errorf("Class2Name cannot be empty")
|
||||
}
|
||||
if c.Class3Name == "" {
|
||||
return fmt.Errorf("Class3Name cannot be empty")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -132,6 +148,17 @@ func (c *Control) IsOpen() bool {
|
||||
return c.Open == 1
|
||||
}
|
||||
|
||||
// SetOpen sets whether the game world is open for new players
|
||||
func SetOpen(open bool) error {
|
||||
return Update(func(c *Control) {
|
||||
if open {
|
||||
c.Open = 1
|
||||
} else {
|
||||
c.Open = 0
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// GetClassNames returns all class names as a slice
|
||||
func (c *Control) GetClassNames() []string {
|
||||
classes := make([]string, 0, 3)
|
||||
@ -209,3 +236,14 @@ func (c *Control) GetWorldBounds() (minX, minY, maxX, maxY int) {
|
||||
radius := c.GetWorldRadius()
|
||||
return -radius, -radius, radius, radius
|
||||
}
|
||||
|
||||
// Legacy compatibility functions (will be removed later)
|
||||
func Load(filename string) error {
|
||||
// No longer needed - Nigiri handles this
|
||||
return nil
|
||||
}
|
||||
|
||||
func Save() error {
|
||||
// No longer needed - Nigiri handles this
|
||||
return nil
|
||||
}
|
||||
|
@ -1,26 +1,56 @@
|
||||
package drops
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Drop represents a drop item in the game
|
||||
type Drop struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Level int `json:"level"`
|
||||
Type int `json:"type"`
|
||||
Name string `json:"name" db:"required"`
|
||||
Level int `json:"level" db:"index"`
|
||||
Type int `json:"type" db:"index"`
|
||||
Att string `json:"att"`
|
||||
}
|
||||
|
||||
func (d *Drop) Save() error {
|
||||
return GetStore().UpdateWithRebuild(d.ID, d)
|
||||
// DropType constants for drop types
|
||||
const (
|
||||
TypeConsumable = 1
|
||||
)
|
||||
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Drop]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Drop]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(d *Drop) int {
|
||||
return d.Level
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byType", nigiri.BuildIntGroupIndex(func(d *Drop) int {
|
||||
return d.Type
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByID", nigiri.BuildSortedListIndex(func(a, b *Drop) bool {
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (d *Drop) Delete() error {
|
||||
GetStore().RemoveWithRebuild(d.ID)
|
||||
return nil
|
||||
// GetStore returns the drops store
|
||||
func GetStore() *nigiri.BaseStore[Drop] {
|
||||
if store == nil {
|
||||
panic("drops store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Creates a new Drop with sensible defaults
|
||||
@ -47,64 +77,37 @@ func (d *Drop) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DropType constants for drop types
|
||||
const (
|
||||
TypeConsumable = 1
|
||||
)
|
||||
|
||||
// DropStore with enhanced BaseStore
|
||||
type DropStore struct {
|
||||
*store.BaseStore[Drop]
|
||||
// CRUD operations
|
||||
func (d *Drop) Save() error {
|
||||
if d.ID == 0 {
|
||||
id, err := store.Create(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(d.ID, d)
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *DropStore {
|
||||
ds := &DropStore{BaseStore: store.NewBaseStore[Drop]()}
|
||||
|
||||
// Register indices
|
||||
ds.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(d *Drop) int {
|
||||
return d.Level
|
||||
}))
|
||||
|
||||
ds.RegisterIndex("byType", store.BuildIntGroupIndex(func(d *Drop) int {
|
||||
return d.Type
|
||||
}))
|
||||
|
||||
ds.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Drop) bool {
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
return ds
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (ds *DropStore) AddDrop(drop *Drop) error {
|
||||
return ds.AddWithRebuild(drop.ID, drop)
|
||||
func (d *Drop) Delete() error {
|
||||
store.Remove(d.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DropStore) RemoveDrop(id int) {
|
||||
ds.RemoveWithRebuild(id)
|
||||
// Insert with ID assignment
|
||||
func (d *Drop) Insert() error {
|
||||
id, err := store.Create(d)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
d.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DropStore) UpdateDrop(drop *Drop) error {
|
||||
return ds.UpdateWithRebuild(drop.ID, drop)
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
ds := GetStore()
|
||||
return ds.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
ds := GetStore()
|
||||
return ds.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*Drop, error) {
|
||||
ds := GetStore()
|
||||
drop, exists := ds.Find(id)
|
||||
drop, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("drop with ID %d not found", id)
|
||||
}
|
||||
@ -112,29 +115,17 @@ func Find(id int) (*Drop, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Drop, error) {
|
||||
ds := GetStore()
|
||||
return ds.AllSorted("allByID"), nil
|
||||
return store.AllSorted("allByID"), nil
|
||||
}
|
||||
|
||||
func ByLevel(minLevel int) ([]*Drop, error) {
|
||||
ds := GetStore()
|
||||
return ds.FilterByIndex("allByID", func(d *Drop) bool {
|
||||
return store.FilterByIndex("allByID", func(d *Drop) bool {
|
||||
return d.Level <= minLevel
|
||||
}), nil
|
||||
}
|
||||
|
||||
func ByType(dropType int) ([]*Drop, error) {
|
||||
ds := GetStore()
|
||||
return ds.GroupByIndex("byType", dropType), nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (d *Drop) Insert() error {
|
||||
ds := GetStore()
|
||||
if d.ID == 0 {
|
||||
d.ID = ds.GetNextID()
|
||||
}
|
||||
return ds.AddDrop(d)
|
||||
return store.GroupByIndex("byType", dropType), nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
@ -150,3 +141,14 @@ func (d *Drop) TypeName() string {
|
||||
return "Unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,16 +1,17 @@
|
||||
package fights
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Fight represents a fight, past or present
|
||||
type Fight struct {
|
||||
ID int `json:"id"`
|
||||
UserID int `json:"user_id"`
|
||||
MonsterID int `json:"monster_id"`
|
||||
UserID int `json:"user_id" db:"index"`
|
||||
MonsterID int `json:"monster_id" db:"index"`
|
||||
MonsterHP int `json:"monster_hp"`
|
||||
MonsterMaxHP int `json:"monster_max_hp"`
|
||||
MonsterSleep int `json:"monster_sleep"`
|
||||
@ -29,16 +30,59 @@ type Fight struct {
|
||||
Updated int64 `json:"updated"`
|
||||
}
|
||||
|
||||
func (f *Fight) Save() error {
|
||||
f.Updated = time.Now().Unix()
|
||||
return GetStore().UpdateWithRebuild(f.ID, f)
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Fight]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Fight]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byUserID", nigiri.BuildIntGroupIndex(func(f *Fight) int {
|
||||
return f.UserID
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byMonsterID", nigiri.BuildIntGroupIndex(func(f *Fight) int {
|
||||
return f.MonsterID
|
||||
}))
|
||||
|
||||
store.RegisterIndex("activeFights", nigiri.BuildFilteredIntGroupIndex(
|
||||
func(f *Fight) bool {
|
||||
return !f.RanAway && !f.Victory
|
||||
},
|
||||
func(f *Fight) int {
|
||||
return f.UserID
|
||||
},
|
||||
))
|
||||
|
||||
store.RegisterIndex("allByCreated", nigiri.BuildSortedListIndex(func(a, b *Fight) bool {
|
||||
if a.Created != b.Created {
|
||||
return a.Created > b.Created // DESC
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByUpdated", nigiri.BuildSortedListIndex(func(a, b *Fight) bool {
|
||||
if a.Updated != b.Updated {
|
||||
return a.Updated > b.Updated // DESC
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (f *Fight) Delete() error {
|
||||
GetStore().RemoveWithRebuild(f.ID)
|
||||
return nil
|
||||
// GetStore returns the fights store
|
||||
func GetStore() *nigiri.BaseStore[Fight] {
|
||||
if store == nil {
|
||||
panic("fights store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// New creates a new Fight with sensible defaults
|
||||
func New(userID, monsterID int) *Fight {
|
||||
now := time.Now().Unix()
|
||||
return &Fight{
|
||||
@ -86,78 +130,39 @@ func (f *Fight) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// FightStore with enhanced BaseStore
|
||||
type FightStore struct {
|
||||
*store.BaseStore[Fight]
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *FightStore {
|
||||
fs := &FightStore{BaseStore: store.NewBaseStore[Fight]()}
|
||||
|
||||
// Register indices
|
||||
fs.RegisterIndex("byUserID", store.BuildIntGroupIndex(func(f *Fight) int {
|
||||
return f.UserID
|
||||
}))
|
||||
|
||||
fs.RegisterIndex("byMonsterID", store.BuildIntGroupIndex(func(f *Fight) int {
|
||||
return f.MonsterID
|
||||
}))
|
||||
|
||||
fs.RegisterIndex("activeFights", store.BuildFilteredIntGroupIndex(
|
||||
func(f *Fight) bool {
|
||||
return !f.RanAway && !f.Victory
|
||||
},
|
||||
func(f *Fight) int {
|
||||
return f.UserID
|
||||
},
|
||||
))
|
||||
|
||||
fs.RegisterIndex("allByCreated", store.BuildSortedListIndex(func(a, b *Fight) bool {
|
||||
if a.Created != b.Created {
|
||||
return a.Created > b.Created // DESC
|
||||
// CRUD operations
|
||||
func (f *Fight) Save() error {
|
||||
f.Updated = time.Now().Unix()
|
||||
if f.ID == 0 {
|
||||
id, err := store.Create(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
fs.RegisterIndex("allByUpdated", store.BuildSortedListIndex(func(a, b *Fight) bool {
|
||||
if a.Updated != b.Updated {
|
||||
return a.Updated > b.Updated // DESC
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
return fs
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (fs *FightStore) AddFight(fight *Fight) error {
|
||||
return fs.AddWithRebuild(fight.ID, fight)
|
||||
f.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(f.ID, f)
|
||||
}
|
||||
|
||||
func (fs *FightStore) RemoveFight(id int) {
|
||||
fs.RemoveWithRebuild(id)
|
||||
func (f *Fight) Delete() error {
|
||||
store.Remove(f.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *FightStore) UpdateFight(fight *Fight) error {
|
||||
return fs.UpdateWithRebuild(fight.ID, fight)
|
||||
// Insert with ID assignment
|
||||
func (f *Fight) Insert() error {
|
||||
f.Updated = time.Now().Unix()
|
||||
id, err := store.Create(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
fs := GetStore()
|
||||
return fs.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
fs := GetStore()
|
||||
return fs.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*Fight, error) {
|
||||
fs := GetStore()
|
||||
fight, exists := fs.Find(id)
|
||||
fight, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("fight with ID %d not found", id)
|
||||
}
|
||||
@ -165,54 +170,38 @@ func Find(id int) (*Fight, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Fight, error) {
|
||||
fs := GetStore()
|
||||
return fs.AllSorted("allByCreated"), nil
|
||||
return store.AllSorted("allByCreated"), nil
|
||||
}
|
||||
|
||||
func ByUserID(userID int) ([]*Fight, error) {
|
||||
fs := GetStore()
|
||||
return fs.GroupByIndex("byUserID", userID), nil
|
||||
return store.GroupByIndex("byUserID", userID), nil
|
||||
}
|
||||
|
||||
func ByMonsterID(monsterID int) ([]*Fight, error) {
|
||||
fs := GetStore()
|
||||
return fs.GroupByIndex("byMonsterID", monsterID), nil
|
||||
return store.GroupByIndex("byMonsterID", monsterID), nil
|
||||
}
|
||||
|
||||
func ActiveByUserID(userID int) ([]*Fight, error) {
|
||||
fs := GetStore()
|
||||
return fs.GroupByIndex("activeFights", userID), nil
|
||||
return store.GroupByIndex("activeFights", userID), nil
|
||||
}
|
||||
|
||||
func Active() ([]*Fight, error) {
|
||||
fs := GetStore()
|
||||
result := fs.FilterByIndex("allByCreated", func(f *Fight) bool {
|
||||
result := store.FilterByIndex("allByCreated", func(f *Fight) bool {
|
||||
return !f.RanAway && !f.Victory
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func Recent(within time.Duration) ([]*Fight, error) {
|
||||
fs := GetStore()
|
||||
cutoff := time.Now().Add(-within).Unix()
|
||||
|
||||
result := fs.FilterByIndex("allByCreated", func(f *Fight) bool {
|
||||
result := store.FilterByIndex("allByCreated", func(f *Fight) bool {
|
||||
return f.Created >= cutoff
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (f *Fight) Insert() error {
|
||||
fs := GetStore()
|
||||
if f.ID == 0 {
|
||||
f.ID = fs.GetNextID()
|
||||
}
|
||||
f.Updated = time.Now().Unix()
|
||||
return fs.AddFight(f)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (f *Fight) CreatedTime() time.Time {
|
||||
return time.Unix(f.Created, 0)
|
||||
|
@ -1,11 +1,12 @@
|
||||
package forum
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Forum represents a forum post or thread in the game
|
||||
@ -13,20 +14,47 @@ type Forum struct {
|
||||
ID int `json:"id"`
|
||||
Posted int64 `json:"posted"`
|
||||
LastPost int64 `json:"last_post"`
|
||||
Author int `json:"author"`
|
||||
Parent int `json:"parent"`
|
||||
Author int `json:"author" db:"index"`
|
||||
Parent int `json:"parent" db:"index"`
|
||||
Replies int `json:"replies"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Title string `json:"title" db:"required"`
|
||||
Content string `json:"content" db:"required"`
|
||||
}
|
||||
|
||||
func (f *Forum) Save() error {
|
||||
return GetStore().UpdateWithRebuild(f.ID, f)
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Forum]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Forum]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byParent", nigiri.BuildIntGroupIndex(func(f *Forum) int {
|
||||
return f.Parent
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byAuthor", nigiri.BuildIntGroupIndex(func(f *Forum) int {
|
||||
return f.Author
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByLastPost", nigiri.BuildSortedListIndex(func(a, b *Forum) bool {
|
||||
if a.LastPost != b.LastPost {
|
||||
return a.LastPost > b.LastPost // DESC
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (f *Forum) Delete() error {
|
||||
GetStore().RemoveWithRebuild(f.ID)
|
||||
return nil
|
||||
// GetStore returns the forum store
|
||||
func GetStore() *nigiri.BaseStore[Forum] {
|
||||
if store == nil {
|
||||
panic("forum store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Creates a new Forum with sensible defaults
|
||||
@ -66,62 +94,37 @@ func (f *Forum) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ForumStore with enhanced BaseStore
|
||||
type ForumStore struct {
|
||||
*store.BaseStore[Forum]
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *ForumStore {
|
||||
fs := &ForumStore{BaseStore: store.NewBaseStore[Forum]()}
|
||||
|
||||
// Register indices
|
||||
fs.RegisterIndex("byParent", store.BuildIntGroupIndex(func(f *Forum) int {
|
||||
return f.Parent
|
||||
}))
|
||||
|
||||
fs.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(f *Forum) int {
|
||||
return f.Author
|
||||
}))
|
||||
|
||||
fs.RegisterIndex("allByLastPost", store.BuildSortedListIndex(func(a, b *Forum) bool {
|
||||
if a.LastPost != b.LastPost {
|
||||
return a.LastPost > b.LastPost // DESC
|
||||
// CRUD operations
|
||||
func (f *Forum) Save() error {
|
||||
if f.ID == 0 {
|
||||
id, err := store.Create(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
return fs
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (fs *ForumStore) AddForum(forum *Forum) error {
|
||||
return fs.AddWithRebuild(forum.ID, forum)
|
||||
f.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(f.ID, f)
|
||||
}
|
||||
|
||||
func (fs *ForumStore) RemoveForum(id int) {
|
||||
fs.RemoveWithRebuild(id)
|
||||
func (f *Forum) Delete() error {
|
||||
store.Remove(f.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *ForumStore) UpdateForum(forum *Forum) error {
|
||||
return fs.UpdateWithRebuild(forum.ID, forum)
|
||||
// Insert with ID assignment
|
||||
func (f *Forum) Insert() error {
|
||||
id, err := store.Create(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
f.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
fs := GetStore()
|
||||
return fs.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
fs := GetStore()
|
||||
return fs.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*Forum, error) {
|
||||
fs := GetStore()
|
||||
forum, exists := fs.Find(id)
|
||||
forum, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("forum post with ID %d not found", id)
|
||||
}
|
||||
@ -129,20 +132,17 @@ func Find(id int) (*Forum, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Forum, error) {
|
||||
fs := GetStore()
|
||||
return fs.AllSorted("allByLastPost"), nil
|
||||
return store.AllSorted("allByLastPost"), nil
|
||||
}
|
||||
|
||||
func Threads() ([]*Forum, error) {
|
||||
fs := GetStore()
|
||||
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||
return store.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||
return f.Parent == 0
|
||||
}), nil
|
||||
}
|
||||
|
||||
func ByParent(parentID int) ([]*Forum, error) {
|
||||
fs := GetStore()
|
||||
replies := fs.GroupByIndex("byParent", parentID)
|
||||
replies := store.GroupByIndex("byParent", parentID)
|
||||
|
||||
// Sort replies chronologically (posted ASC, then ID ASC)
|
||||
if parentID > 0 && len(replies) > 1 {
|
||||
@ -158,8 +158,7 @@ func ByParent(parentID int) ([]*Forum, error) {
|
||||
}
|
||||
|
||||
func ByAuthor(authorID int) ([]*Forum, error) {
|
||||
fs := GetStore()
|
||||
posts := fs.GroupByIndex("byAuthor", authorID)
|
||||
posts := store.GroupByIndex("byAuthor", authorID)
|
||||
|
||||
// Sort by posted DESC, then ID DESC
|
||||
sort.Slice(posts, func(i, j int) bool {
|
||||
@ -173,8 +172,7 @@ func ByAuthor(authorID int) ([]*Forum, error) {
|
||||
}
|
||||
|
||||
func Recent(limit int) ([]*Forum, error) {
|
||||
fs := GetStore()
|
||||
all := fs.AllSorted("allByLastPost")
|
||||
all := store.AllSorted("allByLastPost")
|
||||
if limit > len(all) {
|
||||
limit = len(all)
|
||||
}
|
||||
@ -182,30 +180,19 @@ func Recent(limit int) ([]*Forum, error) {
|
||||
}
|
||||
|
||||
func Search(term string) ([]*Forum, error) {
|
||||
fs := GetStore()
|
||||
lowerTerm := strings.ToLower(term)
|
||||
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||
return store.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||
return strings.Contains(strings.ToLower(f.Title), lowerTerm) ||
|
||||
strings.Contains(strings.ToLower(f.Content), lowerTerm)
|
||||
}), nil
|
||||
}
|
||||
|
||||
func Since(since int64) ([]*Forum, error) {
|
||||
fs := GetStore()
|
||||
return fs.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||
return store.FilterByIndex("allByLastPost", func(f *Forum) bool {
|
||||
return f.LastPost >= since
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (f *Forum) Insert() error {
|
||||
fs := GetStore()
|
||||
if f.ID == 0 {
|
||||
f.ID = fs.GetNextID()
|
||||
}
|
||||
return fs.AddForum(f)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (f *Forum) PostedTime() time.Time {
|
||||
return time.Unix(f.Posted, 0)
|
||||
@ -324,3 +311,14 @@ func (f *Forum) GetThread() (*Forum, error) {
|
||||
}
|
||||
return Find(f.Parent)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,27 +1,55 @@
|
||||
package items
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Item represents an item in the game
|
||||
type Item struct {
|
||||
ID int `json:"id"`
|
||||
Type int `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Type int `json:"type" db:"index"`
|
||||
Name string `json:"name" db:"required"`
|
||||
Value int `json:"value"`
|
||||
Att int `json:"att"`
|
||||
Special string `json:"special"`
|
||||
}
|
||||
|
||||
func (i *Item) Save() error {
|
||||
return GetStore().UpdateWithRebuild(i.ID, i)
|
||||
// ItemType constants for item types
|
||||
const (
|
||||
TypeWeapon = 1
|
||||
TypeArmor = 2
|
||||
TypeShield = 3
|
||||
)
|
||||
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Item]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Item]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byType", nigiri.BuildIntGroupIndex(func(i *Item) int {
|
||||
return i.Type
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByID", nigiri.BuildSortedListIndex(func(a, b *Item) bool {
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (i *Item) Delete() error {
|
||||
GetStore().RemoveWithRebuild(i.ID)
|
||||
return nil
|
||||
// GetStore returns the items store
|
||||
func GetStore() *nigiri.BaseStore[Item] {
|
||||
if store == nil {
|
||||
panic("items store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Creates a new Item with sensible defaults
|
||||
@ -52,62 +80,37 @@ func (i *Item) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ItemType constants for item types
|
||||
const (
|
||||
TypeWeapon = 1
|
||||
TypeArmor = 2
|
||||
TypeShield = 3
|
||||
)
|
||||
|
||||
// ItemStore with enhanced BaseStore
|
||||
type ItemStore struct {
|
||||
*store.BaseStore[Item]
|
||||
// CRUD operations
|
||||
func (i *Item) Save() error {
|
||||
if i.ID == 0 {
|
||||
id, err := store.Create(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(i.ID, i)
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *ItemStore {
|
||||
is := &ItemStore{BaseStore: store.NewBaseStore[Item]()}
|
||||
|
||||
// Register indices
|
||||
is.RegisterIndex("byType", store.BuildIntGroupIndex(func(i *Item) int {
|
||||
return i.Type
|
||||
}))
|
||||
|
||||
is.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Item) bool {
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
return is
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (is *ItemStore) AddItem(item *Item) error {
|
||||
return is.AddWithRebuild(item.ID, item)
|
||||
func (i *Item) Delete() error {
|
||||
store.Remove(i.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *ItemStore) RemoveItem(id int) {
|
||||
is.RemoveWithRebuild(id)
|
||||
// Insert with ID assignment
|
||||
func (i *Item) Insert() error {
|
||||
id, err := store.Create(i)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
i.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (is *ItemStore) UpdateItem(item *Item) error {
|
||||
return is.UpdateWithRebuild(item.ID, item)
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
is := GetStore()
|
||||
return is.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
is := GetStore()
|
||||
return is.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*Item, error) {
|
||||
is := GetStore()
|
||||
item, exists := is.Find(id)
|
||||
item, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("item with ID %d not found", id)
|
||||
}
|
||||
@ -115,22 +118,11 @@ func Find(id int) (*Item, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Item, error) {
|
||||
is := GetStore()
|
||||
return is.AllSorted("allByID"), nil
|
||||
return store.AllSorted("allByID"), nil
|
||||
}
|
||||
|
||||
func ByType(itemType int) ([]*Item, error) {
|
||||
is := GetStore()
|
||||
return is.GroupByIndex("byType", itemType), nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (i *Item) Insert() error {
|
||||
is := GetStore()
|
||||
if i.ID == 0 {
|
||||
i.ID = is.GetNextID()
|
||||
}
|
||||
return is.AddItem(i)
|
||||
return store.GroupByIndex("byType", itemType), nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
@ -166,3 +158,14 @@ func (i *Item) HasSpecial() bool {
|
||||
func (i *Item) IsEquippable() bool {
|
||||
return i.Type == TypeWeapon || i.Type == TypeArmor || i.Type == TypeShield
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,30 +1,65 @@
|
||||
package monsters
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Monster represents a monster in the game
|
||||
type Monster struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Name string `json:"name" db:"required"`
|
||||
MaxHP int `json:"max_hp"`
|
||||
MaxDmg int `json:"max_dmg"`
|
||||
Armor int `json:"armor"`
|
||||
Level int `json:"level"`
|
||||
Level int `json:"level" db:"index"`
|
||||
MaxExp int `json:"max_exp"`
|
||||
MaxGold int `json:"max_gold"`
|
||||
Immune int `json:"immune"`
|
||||
Immune int `json:"immune" db:"index"`
|
||||
}
|
||||
|
||||
func (m *Monster) Save() error {
|
||||
return GetStore().UpdateWithRebuild(m.ID, m)
|
||||
// Immunity constants
|
||||
const (
|
||||
ImmuneNone = 0
|
||||
ImmuneHurt = 1
|
||||
ImmuneSleep = 2
|
||||
)
|
||||
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Monster]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Monster]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byLevel", nigiri.BuildIntGroupIndex(func(m *Monster) int {
|
||||
return m.Level
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byImmunity", nigiri.BuildIntGroupIndex(func(m *Monster) int {
|
||||
return m.Immune
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByLevel", nigiri.BuildSortedListIndex(func(a, b *Monster) bool {
|
||||
if a.Level == b.Level {
|
||||
return a.ID < b.ID
|
||||
}
|
||||
return a.Level < b.Level
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (m *Monster) Delete() error {
|
||||
GetStore().RemoveWithRebuild(m.ID)
|
||||
return nil
|
||||
// GetStore returns the monsters store
|
||||
func GetStore() *nigiri.BaseStore[Monster] {
|
||||
if store == nil {
|
||||
panic("monsters store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Creates a new Monster with sensible defaults
|
||||
@ -58,69 +93,37 @@ func (m *Monster) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Immunity constants
|
||||
const (
|
||||
ImmuneNone = 0
|
||||
ImmuneHurt = 1
|
||||
ImmuneSleep = 2
|
||||
)
|
||||
|
||||
// MonsterStore with enhanced BaseStore
|
||||
type MonsterStore struct {
|
||||
*store.BaseStore[Monster]
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *MonsterStore {
|
||||
ms := &MonsterStore{BaseStore: store.NewBaseStore[Monster]()}
|
||||
|
||||
// Register indices
|
||||
ms.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(m *Monster) int {
|
||||
return m.Level
|
||||
}))
|
||||
|
||||
ms.RegisterIndex("byImmunity", store.BuildIntGroupIndex(func(m *Monster) int {
|
||||
return m.Immune
|
||||
}))
|
||||
|
||||
ms.RegisterIndex("allByLevel", store.BuildSortedListIndex(func(a, b *Monster) bool {
|
||||
if a.Level == b.Level {
|
||||
return a.ID < b.ID
|
||||
// CRUD operations
|
||||
func (m *Monster) Save() error {
|
||||
if m.ID == 0 {
|
||||
id, err := store.Create(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.Level < b.Level
|
||||
}))
|
||||
|
||||
return ms
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (ms *MonsterStore) AddMonster(monster *Monster) error {
|
||||
return ms.AddWithRebuild(monster.ID, monster)
|
||||
m.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(m.ID, m)
|
||||
}
|
||||
|
||||
func (ms *MonsterStore) RemoveMonster(id int) {
|
||||
ms.RemoveWithRebuild(id)
|
||||
func (m *Monster) Delete() error {
|
||||
store.Remove(m.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ms *MonsterStore) UpdateMonster(monster *Monster) error {
|
||||
return ms.UpdateWithRebuild(monster.ID, monster)
|
||||
// Insert with ID assignment
|
||||
func (m *Monster) Insert() error {
|
||||
id, err := store.Create(m)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
ms := GetStore()
|
||||
return ms.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
ms := GetStore()
|
||||
return ms.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*Monster, error) {
|
||||
ms := GetStore()
|
||||
monster, exists := ms.Find(id)
|
||||
monster, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("monster with ID %d not found", id)
|
||||
}
|
||||
@ -128,37 +131,24 @@ func Find(id int) (*Monster, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Monster, error) {
|
||||
ms := GetStore()
|
||||
return ms.AllSorted("allByLevel"), nil
|
||||
return store.AllSorted("allByLevel"), nil
|
||||
}
|
||||
|
||||
func ByLevel(level int) ([]*Monster, error) {
|
||||
ms := GetStore()
|
||||
return ms.GroupByIndex("byLevel", level), nil
|
||||
return store.GroupByIndex("byLevel", level), nil
|
||||
}
|
||||
|
||||
func ByLevelRange(minLevel, maxLevel int) ([]*Monster, error) {
|
||||
ms := GetStore()
|
||||
var result []*Monster
|
||||
for level := minLevel; level <= maxLevel; level++ {
|
||||
monsters := ms.GroupByIndex("byLevel", level)
|
||||
monsters := store.GroupByIndex("byLevel", level)
|
||||
result = append(result, monsters...)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func ByImmunity(immunityType int) ([]*Monster, error) {
|
||||
ms := GetStore()
|
||||
return ms.GroupByIndex("byImmunity", immunityType), nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (m *Monster) Insert() error {
|
||||
ms := GetStore()
|
||||
if m.ID == 0 {
|
||||
m.ID = ms.GetNextID()
|
||||
}
|
||||
return ms.AddMonster(m)
|
||||
return store.GroupByIndex("byImmunity", immunityType), nil
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
@ -207,3 +197,14 @@ func (m *Monster) GoldPerHP() float64 {
|
||||
}
|
||||
return float64(m.MaxGold) / float64(m.MaxHP)
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,27 +1,51 @@
|
||||
package news
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// News represents a news post in the game
|
||||
type News struct {
|
||||
ID int `json:"id"`
|
||||
Author int `json:"author"`
|
||||
Author int `json:"author" db:"index"`
|
||||
Posted int64 `json:"posted"`
|
||||
Content string `json:"content"`
|
||||
Content string `json:"content" db:"required"`
|
||||
}
|
||||
|
||||
func (n *News) Save() error {
|
||||
return GetStore().UpdateWithRebuild(n.ID, n)
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[News]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[News]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byAuthor", nigiri.BuildIntGroupIndex(func(n *News) int {
|
||||
return n.Author
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByPosted", nigiri.BuildSortedListIndex(func(a, b *News) bool {
|
||||
if a.Posted != b.Posted {
|
||||
return a.Posted > b.Posted // DESC
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (n *News) Delete() error {
|
||||
GetStore().RemoveWithRebuild(n.ID)
|
||||
return nil
|
||||
// GetStore returns the news store
|
||||
func GetStore() *nigiri.BaseStore[News] {
|
||||
if store == nil {
|
||||
panic("news store not initialized - call Init first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Creates a new News with sensible defaults
|
||||
@ -44,58 +68,37 @@ func (n *News) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewsStore with enhanced BaseStore
|
||||
type NewsStore struct {
|
||||
*store.BaseStore[News]
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *NewsStore {
|
||||
ns := &NewsStore{BaseStore: store.NewBaseStore[News]()}
|
||||
|
||||
// Register indices
|
||||
ns.RegisterIndex("byAuthor", store.BuildIntGroupIndex(func(n *News) int {
|
||||
return n.Author
|
||||
}))
|
||||
|
||||
ns.RegisterIndex("allByPosted", store.BuildSortedListIndex(func(a, b *News) bool {
|
||||
if a.Posted != b.Posted {
|
||||
return a.Posted > b.Posted // DESC
|
||||
// CRUD operations
|
||||
func (n *News) Save() error {
|
||||
if n.ID == 0 {
|
||||
id, err := store.Create(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
return ns
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (ns *NewsStore) AddNews(news *News) error {
|
||||
return ns.AddWithRebuild(news.ID, news)
|
||||
n.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(n.ID, n)
|
||||
}
|
||||
|
||||
func (ns *NewsStore) RemoveNews(id int) {
|
||||
ns.RemoveWithRebuild(id)
|
||||
func (n *News) Delete() error {
|
||||
store.Remove(n.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ns *NewsStore) UpdateNews(news *News) error {
|
||||
return ns.UpdateWithRebuild(news.ID, news)
|
||||
// Insert with ID assignment
|
||||
func (n *News) Insert() error {
|
||||
id, err := store.Create(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
ns := GetStore()
|
||||
return ns.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
ns := GetStore()
|
||||
return ns.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*News, error) {
|
||||
ns := GetStore()
|
||||
news, exists := ns.Find(id)
|
||||
news, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("news with ID %d not found", id)
|
||||
}
|
||||
@ -103,18 +106,15 @@ func Find(id int) (*News, error) {
|
||||
}
|
||||
|
||||
func All() ([]*News, error) {
|
||||
ns := GetStore()
|
||||
return ns.AllSorted("allByPosted"), nil
|
||||
return store.AllSorted("allByPosted"), nil
|
||||
}
|
||||
|
||||
func ByAuthor(authorID int) ([]*News, error) {
|
||||
ns := GetStore()
|
||||
return ns.GroupByIndex("byAuthor", authorID), nil
|
||||
return store.GroupByIndex("byAuthor", authorID), nil
|
||||
}
|
||||
|
||||
func Recent(limit int) ([]*News, error) {
|
||||
ns := GetStore()
|
||||
all := ns.AllSorted("allByPosted")
|
||||
all := store.AllSorted("allByPosted")
|
||||
if limit > len(all) {
|
||||
limit = len(all)
|
||||
}
|
||||
@ -122,36 +122,24 @@ func Recent(limit int) ([]*News, error) {
|
||||
}
|
||||
|
||||
func Since(since int64) ([]*News, error) {
|
||||
ns := GetStore()
|
||||
return ns.FilterByIndex("allByPosted", func(n *News) bool {
|
||||
return store.FilterByIndex("allByPosted", func(n *News) bool {
|
||||
return n.Posted >= since
|
||||
}), nil
|
||||
}
|
||||
|
||||
func Between(start, end int64) ([]*News, error) {
|
||||
ns := GetStore()
|
||||
return ns.FilterByIndex("allByPosted", func(n *News) bool {
|
||||
return store.FilterByIndex("allByPosted", func(n *News) bool {
|
||||
return n.Posted >= start && n.Posted <= end
|
||||
}), nil
|
||||
}
|
||||
|
||||
func Search(term string) ([]*News, error) {
|
||||
ns := GetStore()
|
||||
lowerTerm := strings.ToLower(term)
|
||||
return ns.FilterByIndex("allByPosted", func(n *News) bool {
|
||||
return store.FilterByIndex("allByPosted", func(n *News) bool {
|
||||
return strings.Contains(strings.ToLower(n.Content), lowerTerm)
|
||||
}), nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (n *News) Insert() error {
|
||||
ns := GetStore()
|
||||
if n.ID == 0 {
|
||||
n.ID = ns.GetNextID()
|
||||
}
|
||||
return ns.AddNews(n)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (n *News) PostedTime() time.Time {
|
||||
return time.Unix(n.Posted, 0)
|
||||
@ -227,3 +215,14 @@ func (n *News) Contains(term string) bool {
|
||||
func (n *News) IsEmpty() bool {
|
||||
return strings.TrimSpace(n.Content) == ""
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,27 +1,71 @@
|
||||
package spells
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// Spell represents a spell in the game
|
||||
type Spell struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
MP int `json:"mp"`
|
||||
Name string `json:"name" db:"required,unique"`
|
||||
MP int `json:"mp" db:"index"`
|
||||
Attribute int `json:"attribute"`
|
||||
Type int `json:"type"`
|
||||
Type int `json:"type" db:"index"`
|
||||
}
|
||||
|
||||
func (s *Spell) Save() error {
|
||||
return GetStore().UpdateWithRebuild(s.ID, s)
|
||||
// SpellType constants for spell types
|
||||
const (
|
||||
TypeHealing = 1
|
||||
TypeHurt = 2
|
||||
TypeSleep = 3
|
||||
TypeAttackBoost = 4
|
||||
TypeDefenseBoost = 5
|
||||
)
|
||||
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Spell]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = collection
|
||||
store = nigiri.NewBaseStore[Spell]()
|
||||
|
||||
// Register custom indices
|
||||
store.RegisterIndex("byType", nigiri.BuildIntGroupIndex(func(s *Spell) int {
|
||||
return s.Type
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byName", nigiri.BuildCaseInsensitiveLookupIndex(func(s *Spell) string {
|
||||
return s.Name
|
||||
}))
|
||||
|
||||
store.RegisterIndex("byMP", nigiri.BuildIntGroupIndex(func(s *Spell) int {
|
||||
return s.MP
|
||||
}))
|
||||
|
||||
store.RegisterIndex("allByTypeMP", nigiri.BuildSortedListIndex(func(a, b *Spell) bool {
|
||||
if a.Type != b.Type {
|
||||
return a.Type < b.Type
|
||||
}
|
||||
if a.MP != b.MP {
|
||||
return a.MP < b.MP
|
||||
}
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
store.RebuildIndices()
|
||||
}
|
||||
|
||||
func (s *Spell) Delete() error {
|
||||
GetStore().RemoveWithRebuild(s.ID)
|
||||
return nil
|
||||
// GetStore returns the spells store
|
||||
func GetStore() *nigiri.BaseStore[Spell] {
|
||||
if store == nil {
|
||||
panic("spells store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// Creates a new Spell with sensible defaults
|
||||
@ -51,78 +95,37 @@ func (s *Spell) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// SpellType constants for spell types
|
||||
const (
|
||||
TypeHealing = 1
|
||||
TypeHurt = 2
|
||||
TypeSleep = 3
|
||||
TypeAttackBoost = 4
|
||||
TypeDefenseBoost = 5
|
||||
)
|
||||
|
||||
// SpellStore with enhanced BaseStore
|
||||
type SpellStore struct {
|
||||
*store.BaseStore[Spell]
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *SpellStore {
|
||||
ss := &SpellStore{BaseStore: store.NewBaseStore[Spell]()}
|
||||
|
||||
// Register indices
|
||||
ss.RegisterIndex("byType", store.BuildIntGroupIndex(func(s *Spell) int {
|
||||
return s.Type
|
||||
}))
|
||||
|
||||
ss.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(s *Spell) string {
|
||||
return s.Name
|
||||
}))
|
||||
|
||||
ss.RegisterIndex("byMP", store.BuildIntGroupIndex(func(s *Spell) int {
|
||||
return s.MP
|
||||
}))
|
||||
|
||||
ss.RegisterIndex("allByTypeMP", store.BuildSortedListIndex(func(a, b *Spell) bool {
|
||||
if a.Type != b.Type {
|
||||
return a.Type < b.Type
|
||||
// CRUD operations
|
||||
func (s *Spell) Save() error {
|
||||
if s.ID == 0 {
|
||||
id, err := store.Create(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if a.MP != b.MP {
|
||||
return a.MP < b.MP
|
||||
}
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
return ss
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (ss *SpellStore) AddSpell(spell *Spell) error {
|
||||
return ss.AddWithRebuild(spell.ID, spell)
|
||||
s.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(s.ID, s)
|
||||
}
|
||||
|
||||
func (ss *SpellStore) RemoveSpell(id int) {
|
||||
ss.RemoveWithRebuild(id)
|
||||
func (s *Spell) Delete() error {
|
||||
store.Remove(s.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ss *SpellStore) UpdateSpell(spell *Spell) error {
|
||||
return ss.UpdateWithRebuild(spell.ID, spell)
|
||||
// Insert with ID assignment
|
||||
func (s *Spell) Insert() error {
|
||||
id, err := store.Create(s)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
ss := GetStore()
|
||||
return ss.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
ss := GetStore()
|
||||
return ss.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*Spell, error) {
|
||||
ss := GetStore()
|
||||
spell, exists := ss.Find(id)
|
||||
spell, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("spell with ID %d not found", id)
|
||||
}
|
||||
@ -130,47 +133,33 @@ func Find(id int) (*Spell, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Spell, error) {
|
||||
ss := GetStore()
|
||||
return ss.AllSorted("allByTypeMP"), nil
|
||||
return store.AllSorted("allByTypeMP"), nil
|
||||
}
|
||||
|
||||
func ByType(spellType int) ([]*Spell, error) {
|
||||
ss := GetStore()
|
||||
return ss.GroupByIndex("byType", spellType), nil
|
||||
return store.GroupByIndex("byType", spellType), nil
|
||||
}
|
||||
|
||||
func ByMaxMP(maxMP int) ([]*Spell, error) {
|
||||
ss := GetStore()
|
||||
return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
||||
return store.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
||||
return s.MP <= maxMP
|
||||
}), nil
|
||||
}
|
||||
|
||||
func ByTypeAndMaxMP(spellType, maxMP int) ([]*Spell, error) {
|
||||
ss := GetStore()
|
||||
return ss.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
||||
return store.FilterByIndex("allByTypeMP", func(s *Spell) bool {
|
||||
return s.Type == spellType && s.MP <= maxMP
|
||||
}), nil
|
||||
}
|
||||
|
||||
func ByName(name string) (*Spell, error) {
|
||||
ss := GetStore()
|
||||
spell, exists := ss.LookupByIndex("byName", strings.ToLower(name))
|
||||
spell, exists := store.LookupByIndex("byName", strings.ToLower(name))
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("spell with name '%s' not found", name)
|
||||
}
|
||||
return spell, nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (s *Spell) Insert() error {
|
||||
ss := GetStore()
|
||||
if s.ID == 0 {
|
||||
s.ID = ss.GetNextID()
|
||||
}
|
||||
return ss.AddSpell(s)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (s *Spell) IsHealing() bool {
|
||||
return s.Type == TypeHealing
|
||||
@ -227,3 +216,14 @@ func (s *Spell) IsOffensive() bool {
|
||||
func (s *Spell) IsSupport() bool {
|
||||
return s.Type == TypeHealing || s.Type == TypeAttackBoost || s.Type == TypeDefenseBoost
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
@ -1,7 +1,6 @@
|
||||
package towns
|
||||
|
||||
import (
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
"math"
|
||||
"slices"
|
||||
@ -10,12 +9,14 @@ import (
|
||||
"strings"
|
||||
|
||||
"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"`
|
||||
Name string `json:"name" db:"required,unique"`
|
||||
X int `json:"x"`
|
||||
Y int `json:"y"`
|
||||
InnCost int `json:"inn_cost"`
|
||||
@ -24,13 +25,50 @@ type Town struct {
|
||||
ShopList string `json:"shop_list"`
|
||||
}
|
||||
|
||||
func (t *Town) Save() error {
|
||||
return GetStore().UpdateWithRebuild(t.ID, t)
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[Town]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// coordsKey creates a key for coordinate-based lookup
|
||||
func coordsKey(x, y int) string {
|
||||
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
||||
}
|
||||
|
||||
func (t *Town) Delete() error {
|
||||
GetStore().RemoveWithRebuild(t.ID)
|
||||
return nil
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = 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
|
||||
@ -63,72 +101,37 @@ func (t *Town) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// coordsKey creates a key for coordinate-based lookup
|
||||
func coordsKey(x, y int) string {
|
||||
return strconv.Itoa(x) + "," + strconv.Itoa(y)
|
||||
// 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)
|
||||
}
|
||||
|
||||
// TownStore with enhanced BaseStore
|
||||
type TownStore struct {
|
||||
*store.BaseStore[Town]
|
||||
func (t *Town) Delete() error {
|
||||
store.Remove(t.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *TownStore {
|
||||
ts := &TownStore{BaseStore: store.NewBaseStore[Town]()}
|
||||
|
||||
// Register indices
|
||||
ts.RegisterIndex("byName", store.BuildCaseInsensitiveLookupIndex(func(t *Town) string {
|
||||
return t.Name
|
||||
}))
|
||||
|
||||
ts.RegisterIndex("byCoords", store.BuildStringLookupIndex(func(t *Town) string {
|
||||
return coordsKey(t.X, t.Y)
|
||||
}))
|
||||
|
||||
ts.RegisterIndex("byInnCost", store.BuildIntGroupIndex(func(t *Town) int {
|
||||
return t.InnCost
|
||||
}))
|
||||
|
||||
ts.RegisterIndex("byTPCost", store.BuildIntGroupIndex(func(t *Town) int {
|
||||
return t.TPCost
|
||||
}))
|
||||
|
||||
ts.RegisterIndex("allByID", store.BuildSortedListIndex(func(a, b *Town) bool {
|
||||
return a.ID < b.ID
|
||||
}))
|
||||
|
||||
return ts
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (ts *TownStore) AddTown(town *Town) error {
|
||||
return ts.AddWithRebuild(town.ID, town)
|
||||
// Insert with ID assignment
|
||||
func (t *Town) Insert() error {
|
||||
id, err := store.Create(t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ts *TownStore) RemoveTown(id int) {
|
||||
ts.RemoveWithRebuild(id)
|
||||
}
|
||||
|
||||
func (ts *TownStore) UpdateTown(town *Town) error {
|
||||
return ts.UpdateWithRebuild(town.ID, town)
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
ts := GetStore()
|
||||
return ts.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
ts := GetStore()
|
||||
return ts.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*Town, error) {
|
||||
ts := GetStore()
|
||||
town, exists := ts.Find(id)
|
||||
town, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("town with ID %d not found", id)
|
||||
}
|
||||
@ -136,13 +139,11 @@ func Find(id int) (*Town, error) {
|
||||
}
|
||||
|
||||
func All() ([]*Town, error) {
|
||||
ts := GetStore()
|
||||
return ts.AllSorted("allByID"), nil
|
||||
return store.AllSorted("allByID"), nil
|
||||
}
|
||||
|
||||
func ByName(name string) (*Town, error) {
|
||||
ts := GetStore()
|
||||
town, exists := ts.LookupByIndex("byName", strings.ToLower(name))
|
||||
town, exists := store.LookupByIndex("byName", strings.ToLower(name))
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("town with name '%s' not found", name)
|
||||
}
|
||||
@ -150,22 +151,19 @@ func ByName(name string) (*Town, error) {
|
||||
}
|
||||
|
||||
func ByMaxInnCost(maxCost int) ([]*Town, error) {
|
||||
ts := GetStore()
|
||||
return ts.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return store.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return t.InnCost <= maxCost
|
||||
}), nil
|
||||
}
|
||||
|
||||
func ByMaxTPCost(maxCost int) ([]*Town, error) {
|
||||
ts := GetStore()
|
||||
return ts.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return store.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return t.TPCost <= maxCost
|
||||
}), nil
|
||||
}
|
||||
|
||||
func ByCoords(x, y int) (*Town, error) {
|
||||
ts := GetStore()
|
||||
town, exists := ts.LookupByIndex("byCoords", coordsKey(x, y))
|
||||
town, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
||||
if !exists {
|
||||
return nil, nil // Return nil if not found (like original)
|
||||
}
|
||||
@ -173,16 +171,14 @@ func ByCoords(x, y int) (*Town, error) {
|
||||
}
|
||||
|
||||
func ExistsAt(x, y int) bool {
|
||||
ts := GetStore()
|
||||
_, exists := ts.LookupByIndex("byCoords", coordsKey(x, y))
|
||||
_, exists := store.LookupByIndex("byCoords", coordsKey(x, y))
|
||||
return exists
|
||||
}
|
||||
|
||||
func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
||||
ts := GetStore()
|
||||
maxDistance2 := float64(maxDistance * maxDistance)
|
||||
|
||||
result := ts.FilterByIndex("allByID", func(t *Town) bool {
|
||||
result := store.FilterByIndex("allByID", func(t *Town) bool {
|
||||
return t.DistanceFromSquared(fromX, fromY) <= maxDistance2
|
||||
})
|
||||
|
||||
@ -199,15 +195,6 @@ func ByDistance(fromX, fromY, maxDistance int) ([]*Town, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (t *Town) Insert() error {
|
||||
ts := GetStore()
|
||||
if t.ID == 0 {
|
||||
t.ID = ts.GetNextID()
|
||||
}
|
||||
return ts.AddTown(t)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (t *Town) GetShopItems() []int {
|
||||
return helpers.StringToInts(t.ShopList)
|
||||
@ -259,3 +246,14 @@ 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
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
package users
|
||||
|
||||
import (
|
||||
"dk/internal/helpers/exp"
|
||||
"dk/internal/store"
|
||||
"fmt"
|
||||
"slices"
|
||||
"sort"
|
||||
@ -10,14 +8,16 @@ import (
|
||||
"time"
|
||||
|
||||
"dk/internal/helpers"
|
||||
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
)
|
||||
|
||||
// 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"`
|
||||
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"`
|
||||
@ -34,7 +34,7 @@ type User struct {
|
||||
MaxHP int `json:"max_hp"`
|
||||
MaxMP int `json:"max_mp"`
|
||||
MaxTP int `json:"max_tp"`
|
||||
Level int `json:"level"`
|
||||
Level int `json:"level" db:"index"`
|
||||
Gold int `json:"gold"`
|
||||
Exp int `json:"exp"`
|
||||
GoldBonus int `json:"gold_bonus"`
|
||||
@ -59,15 +59,57 @@ type User struct {
|
||||
Towns string `json:"towns"`
|
||||
}
|
||||
|
||||
func (u *User) Save() error {
|
||||
return GetStore().UpdateWithRebuild(u.ID, u)
|
||||
// Global store
|
||||
var store *nigiri.BaseStore[User]
|
||||
var db *nigiri.Collection
|
||||
|
||||
// Init sets up the Nigiri store and indices
|
||||
func Init(collection *nigiri.Collection) {
|
||||
db = 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()
|
||||
}
|
||||
|
||||
func (u *User) Delete() error {
|
||||
GetStore().RemoveWithRebuild(u.ID)
|
||||
return nil
|
||||
// GetStore returns the users store
|
||||
func GetStore() *nigiri.BaseStore[User] {
|
||||
if store == nil {
|
||||
panic("users store not initialized - call Initialize first")
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
// New creates a new User with sensible defaults
|
||||
func New() *User {
|
||||
now := time.Now().Unix()
|
||||
return &User{
|
||||
@ -125,90 +167,57 @@ func (u *User) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UserStore with enhanced BaseStore
|
||||
type UserStore struct {
|
||||
*store.BaseStore[User]
|
||||
}
|
||||
|
||||
// Global store with singleton pattern
|
||||
var GetStore = store.NewSingleton(func() *UserStore {
|
||||
us := &UserStore{BaseStore: store.NewBaseStore[User]()}
|
||||
|
||||
// Register indices
|
||||
us.RegisterIndex("byUsername", store.BuildCaseInsensitiveLookupIndex(func(u *User) string {
|
||||
return u.Username
|
||||
}))
|
||||
|
||||
us.RegisterIndex("byEmail", store.BuildStringLookupIndex(func(u *User) string {
|
||||
return u.Email
|
||||
}))
|
||||
|
||||
us.RegisterIndex("byLevel", store.BuildIntGroupIndex(func(u *User) int {
|
||||
return u.Level
|
||||
}))
|
||||
|
||||
us.RegisterIndex("allByRegistered", store.BuildSortedListIndex(func(a, b *User) bool {
|
||||
if a.Registered != b.Registered {
|
||||
return a.Registered > b.Registered // DESC
|
||||
// CRUD operations
|
||||
func (u *User) Save() error {
|
||||
if u.ID == 0 {
|
||||
id, err := store.Create(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return a.ID > b.ID // DESC
|
||||
}))
|
||||
|
||||
us.RegisterIndex("allByLevelExp", store.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
|
||||
}))
|
||||
|
||||
return us
|
||||
})
|
||||
|
||||
// Enhanced CRUD operations
|
||||
func (us *UserStore) AddUser(user *User) error {
|
||||
return us.AddWithRebuild(user.ID, user)
|
||||
u.ID = id
|
||||
return nil
|
||||
}
|
||||
return store.Update(u.ID, u)
|
||||
}
|
||||
|
||||
func (us *UserStore) RemoveUser(id int) {
|
||||
us.RemoveWithRebuild(id)
|
||||
func (u *User) Delete() error {
|
||||
store.Remove(u.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (us *UserStore) UpdateUser(user *User) error {
|
||||
return us.UpdateWithRebuild(user.ID, user)
|
||||
// Insert with ID assignment
|
||||
func (u *User) Insert() error {
|
||||
id, err := store.Create(u)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.ID = id
|
||||
return nil
|
||||
}
|
||||
|
||||
// Data persistence
|
||||
func LoadData(dataPath string) error {
|
||||
us := GetStore()
|
||||
return us.BaseStore.LoadData(dataPath)
|
||||
}
|
||||
|
||||
func SaveData(dataPath string) error {
|
||||
us := GetStore()
|
||||
return us.BaseStore.SaveData(dataPath)
|
||||
}
|
||||
|
||||
// Query functions using enhanced store
|
||||
// Query functions
|
||||
func Find(id int) (*User, error) {
|
||||
us := GetStore()
|
||||
user, exists := us.Find(id)
|
||||
user, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("user with ID %d not found", id)
|
||||
}
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func GetByID(id int) *User {
|
||||
user, exists := store.Find(id)
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
return user
|
||||
}
|
||||
|
||||
func All() ([]*User, error) {
|
||||
us := GetStore()
|
||||
return us.AllSorted("allByRegistered"), nil
|
||||
return store.AllSorted("allByRegistered"), nil
|
||||
}
|
||||
|
||||
func ByUsername(username string) (*User, error) {
|
||||
us := GetStore()
|
||||
user, exists := us.LookupByIndex("byUsername", strings.ToLower(username))
|
||||
user, exists := store.LookupByIndex("byUsername", strings.ToLower(username))
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("user with username '%s' not found", username)
|
||||
}
|
||||
@ -216,8 +225,7 @@ func ByUsername(username string) (*User, error) {
|
||||
}
|
||||
|
||||
func ByEmail(email string) (*User, error) {
|
||||
us := GetStore()
|
||||
user, exists := us.LookupByIndex("byEmail", email)
|
||||
user, exists := store.LookupByIndex("Email_idx", email)
|
||||
if !exists {
|
||||
return nil, fmt.Errorf("user with email '%s' not found", email)
|
||||
}
|
||||
@ -225,15 +233,13 @@ func ByEmail(email string) (*User, error) {
|
||||
}
|
||||
|
||||
func ByLevel(level int) ([]*User, error) {
|
||||
us := GetStore()
|
||||
return us.GroupByIndex("byLevel", level), nil
|
||||
return store.GroupByIndex("level_idx", level), nil
|
||||
}
|
||||
|
||||
func Online(within time.Duration) ([]*User, error) {
|
||||
us := GetStore()
|
||||
cutoff := time.Now().Add(-within).Unix()
|
||||
|
||||
result := us.FilterByIndex("allByRegistered", func(u *User) bool {
|
||||
result := store.FilterByIndex("allByRegistered", func(u *User) bool {
|
||||
return u.LastOnline >= cutoff
|
||||
})
|
||||
|
||||
@ -248,15 +254,6 @@ func Online(within time.Duration) ([]*User, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Insert with ID assignment
|
||||
func (u *User) Insert() error {
|
||||
us := GetStore()
|
||||
if u.ID == 0 {
|
||||
u.ID = us.GetNextID()
|
||||
}
|
||||
return us.AddUser(u)
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (u *User) RegisteredTime() time.Time {
|
||||
return time.Unix(u.Registered, 0)
|
||||
@ -351,7 +348,7 @@ func (u *User) SetPosition(x, y int) {
|
||||
}
|
||||
|
||||
func (u *User) ExpNeededForNextLevel() int {
|
||||
return exp.Calc(u.Level + 1)
|
||||
return u.Level * u.Level * u.Level
|
||||
}
|
||||
|
||||
func (u *User) GrantExp(expAmount int) {
|
||||
@ -384,7 +381,7 @@ func (u *User) ExpProgress() float64 {
|
||||
return float64(u.Exp) / float64(u.ExpNeededForNextLevel()) * 100
|
||||
}
|
||||
|
||||
currentLevelExp := exp.Calc(u.Level)
|
||||
currentLevelExp := u.Level * u.Level * u.Level
|
||||
nextLevelExp := u.ExpNeededForNextLevel()
|
||||
progressExp := u.Exp
|
||||
|
||||
|
@ -1,80 +0,0 @@
|
||||
package password
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/crypto/argon2"
|
||||
)
|
||||
|
||||
const (
|
||||
time = 1
|
||||
memory = 64 * 1024
|
||||
threads = 4
|
||||
keyLen = 32
|
||||
)
|
||||
|
||||
// Hash creates an argon2id hash of the password
|
||||
func Hash(password string) string {
|
||||
salt := make([]byte, 16)
|
||||
rand.Read(salt)
|
||||
|
||||
hash := argon2.IDKey([]byte(password), salt, time, memory, threads, keyLen)
|
||||
|
||||
b64Salt := base64.RawStdEncoding.EncodeToString(salt)
|
||||
b64Hash := base64.RawStdEncoding.EncodeToString(hash)
|
||||
|
||||
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
|
||||
argon2.Version, memory, time, threads, b64Salt, b64Hash)
|
||||
|
||||
return encoded
|
||||
}
|
||||
|
||||
// Verify checks if a password matches the hash
|
||||
func Verify(password, encodedHash string) (bool, error) {
|
||||
parts := strings.Split(encodedHash, "$")
|
||||
if len(parts) != 6 {
|
||||
return false, fmt.Errorf("invalid hash format")
|
||||
}
|
||||
|
||||
if parts[1] != "argon2id" {
|
||||
return false, fmt.Errorf("invalid hash variant")
|
||||
}
|
||||
|
||||
var version int
|
||||
_, err := fmt.Sscanf(parts[2], "v=%d", &version)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if version != argon2.Version {
|
||||
return false, fmt.Errorf("incompatible argon2 version")
|
||||
}
|
||||
|
||||
var m, t, p uint32
|
||||
_, err = fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &m, &t, &p)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
expectedHash, err := base64.RawStdEncoding.DecodeString(parts[5])
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
hash := argon2.IDKey([]byte(password), salt, t, m, uint8(p), uint32(len(expectedHash)))
|
||||
|
||||
// Use constant-time comparison to prevent timing attacks
|
||||
if subtle.ConstantTimeCompare(hash, expectedHash) == 1 {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, nil
|
||||
}
|
@ -1,435 +0,0 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
type Ctx = *fasthttp.RequestCtx
|
||||
|
||||
// Handler is a request handler with parameters.
|
||||
type Handler func(ctx Ctx, params []string)
|
||||
|
||||
func (h Handler) Serve(ctx Ctx, params []string) {
|
||||
h(ctx, params)
|
||||
}
|
||||
|
||||
type Middleware func(Handler) Handler
|
||||
|
||||
type node struct {
|
||||
segment string
|
||||
handler Handler
|
||||
children []*node
|
||||
isDynamic bool
|
||||
isWildcard bool
|
||||
maxParams uint8
|
||||
}
|
||||
|
||||
type Router struct {
|
||||
get *node
|
||||
post *node
|
||||
put *node
|
||||
patch *node
|
||||
delete *node
|
||||
middleware []Middleware
|
||||
paramsBuffer []string // Pre-allocated buffer for parameters
|
||||
}
|
||||
|
||||
type Group struct {
|
||||
router *Router
|
||||
prefix string
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
// Creates a new Router instance.
|
||||
func New() *Router {
|
||||
return &Router{
|
||||
get: &node{},
|
||||
post: &node{},
|
||||
put: &node{},
|
||||
patch: &node{},
|
||||
delete: &node{},
|
||||
middleware: []Middleware{},
|
||||
paramsBuffer: make([]string, 64),
|
||||
}
|
||||
}
|
||||
|
||||
// Implements the Handler interface for fasthttp
|
||||
func (r *Router) ServeHTTP(ctx *fasthttp.RequestCtx) {
|
||||
path := string(ctx.Path())
|
||||
method := string(ctx.Method())
|
||||
|
||||
h, params, found := r.Lookup(method, path)
|
||||
if !found {
|
||||
ctx.SetStatusCode(fasthttp.StatusNotFound)
|
||||
return
|
||||
}
|
||||
|
||||
h(ctx, params)
|
||||
}
|
||||
|
||||
// Returns a fasthttp request handler
|
||||
func (r *Router) Handler() fasthttp.RequestHandler {
|
||||
return r.ServeHTTP
|
||||
}
|
||||
|
||||
// Adds middleware to the router.
|
||||
func (r *Router) Use(mw ...Middleware) *Router {
|
||||
r.middleware = append(r.middleware, mw...)
|
||||
return r
|
||||
}
|
||||
|
||||
// Creates a new route group.
|
||||
func (r *Router) Group(prefix string) *Group {
|
||||
return &Group{router: r, prefix: prefix, middleware: []Middleware{}}
|
||||
}
|
||||
|
||||
// Adds middleware to the group.
|
||||
func (g *Group) Use(mw ...Middleware) *Group {
|
||||
g.middleware = append(g.middleware, mw...)
|
||||
return g
|
||||
}
|
||||
|
||||
// Creates a nested group.
|
||||
func (g *Group) Group(prefix string) *Group {
|
||||
return &Group{
|
||||
router: g.router,
|
||||
prefix: g.prefix + prefix,
|
||||
middleware: append([]Middleware{}, g.middleware...),
|
||||
}
|
||||
}
|
||||
|
||||
// Applies middleware in reverse order.
|
||||
func applyMiddleware(h Handler, mw []Middleware) Handler {
|
||||
for i := len(mw) - 1; i >= 0; i-- {
|
||||
h = mw[i](h)
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
// Registers a handler for the given method and path.
|
||||
func (r *Router) Handle(method, path string, h Handler) error {
|
||||
root := r.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return r.addRoute(root, path, h, r.middleware)
|
||||
}
|
||||
|
||||
func (r *Router) methodNode(method string) *node {
|
||||
switch method {
|
||||
case "GET":
|
||||
return r.get
|
||||
case "POST":
|
||||
return r.post
|
||||
case "PUT":
|
||||
return r.put
|
||||
case "PATCH":
|
||||
return r.patch
|
||||
case "DELETE":
|
||||
return r.delete
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Registers a GET handler.
|
||||
func (r *Router) Get(path string, h Handler) error {
|
||||
return r.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler.
|
||||
func (r *Router) Post(path string, h Handler) error {
|
||||
return r.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler.
|
||||
func (r *Router) Put(path string, h Handler) error {
|
||||
return r.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler.
|
||||
func (r *Router) Patch(path string, h Handler) error {
|
||||
return r.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler.
|
||||
func (r *Router) Delete(path string, h Handler) error {
|
||||
return r.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
func (g *Group) buildGroupMiddleware() []Middleware {
|
||||
mw := append([]Middleware{}, g.router.middleware...)
|
||||
return append(mw, g.middleware...)
|
||||
}
|
||||
|
||||
// Registers a handler in the group.
|
||||
func (g *Group) Handle(method, path string, h Handler) error {
|
||||
root := g.router.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return g.router.addRoute(root, g.prefix+path, h, g.buildGroupMiddleware())
|
||||
}
|
||||
|
||||
// Registers a GET handler in the group.
|
||||
func (g *Group) Get(path string, h Handler) error {
|
||||
return g.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler in the group.
|
||||
func (g *Group) Post(path string, h Handler) error {
|
||||
return g.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler in the group.
|
||||
func (g *Group) Put(path string, h Handler) error {
|
||||
return g.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler in the group.
|
||||
func (g *Group) Patch(path string, h Handler) error {
|
||||
return g.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler in the group.
|
||||
func (g *Group) Delete(path string, h Handler) error {
|
||||
return g.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
// Applies specific middleware for next registration.
|
||||
func (r *Router) WithMiddleware(mw ...Middleware) *MiddlewareRouter {
|
||||
return &MiddlewareRouter{router: r, middleware: mw}
|
||||
}
|
||||
|
||||
// Applies specific middleware for next group route.
|
||||
func (g *Group) WithMiddleware(mw ...Middleware) *MiddlewareGroup {
|
||||
return &MiddlewareGroup{group: g, middleware: mw}
|
||||
}
|
||||
|
||||
type MiddlewareRouter struct {
|
||||
router *Router
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
type MiddlewareGroup struct {
|
||||
group *Group
|
||||
middleware []Middleware
|
||||
}
|
||||
|
||||
func (mr *MiddlewareRouter) buildMiddleware() []Middleware {
|
||||
mw := append([]Middleware{}, mr.router.middleware...)
|
||||
return append(mw, mr.middleware...)
|
||||
}
|
||||
|
||||
// Registers a handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Handle(method, path string, h Handler) error {
|
||||
root := mr.router.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return mr.router.addRoute(root, path, h, mr.buildMiddleware())
|
||||
}
|
||||
|
||||
// Registers a GET handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Get(path string, h Handler) error {
|
||||
return mr.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Post(path string, h Handler) error {
|
||||
return mr.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Put(path string, h Handler) error {
|
||||
return mr.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Patch(path string, h Handler) error {
|
||||
return mr.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler with middleware router.
|
||||
func (mr *MiddlewareRouter) Delete(path string, h Handler) error {
|
||||
return mr.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
func (mg *MiddlewareGroup) buildMiddleware() []Middleware {
|
||||
mw := append([]Middleware{}, mg.group.router.middleware...)
|
||||
mw = append(mw, mg.group.middleware...)
|
||||
return append(mw, mg.middleware...)
|
||||
}
|
||||
|
||||
// Registers a handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Handle(method, path string, h Handler) error {
|
||||
root := mg.group.router.methodNode(method)
|
||||
if root == nil {
|
||||
return fmt.Errorf("unsupported method: %s", method)
|
||||
}
|
||||
return mg.group.router.addRoute(root, mg.group.prefix+path, h, mg.buildMiddleware())
|
||||
}
|
||||
|
||||
// Registers a GET handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Get(path string, h Handler) error {
|
||||
return mg.Handle("GET", path, h)
|
||||
}
|
||||
|
||||
// Registers a POST handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Post(path string, h Handler) error {
|
||||
return mg.Handle("POST", path, h)
|
||||
}
|
||||
|
||||
// Registers a PUT handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Put(path string, h Handler) error {
|
||||
return mg.Handle("PUT", path, h)
|
||||
}
|
||||
|
||||
// Registers a PATCH handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Patch(path string, h Handler) error {
|
||||
return mg.Handle("PATCH", path, h)
|
||||
}
|
||||
|
||||
// Registers a DELETE handler with middleware group.
|
||||
func (mg *MiddlewareGroup) Delete(path string, h Handler) error {
|
||||
return mg.Handle("DELETE", path, h)
|
||||
}
|
||||
|
||||
// Adapts a standard fasthttp.RequestHandler to the router's Handler
|
||||
func StandardHandler(handler fasthttp.RequestHandler) Handler {
|
||||
return func(ctx Ctx, _ []string) {
|
||||
handler(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
// Extracts the next path segment.
|
||||
func readSegment(path string, start int) (segment string, end int, hasMore bool) {
|
||||
if start >= len(path) {
|
||||
return "", start, false
|
||||
}
|
||||
if path[start] == '/' {
|
||||
start++
|
||||
}
|
||||
if start >= len(path) {
|
||||
return "", start, false
|
||||
}
|
||||
end = start
|
||||
for end < len(path) && path[end] != '/' {
|
||||
end++
|
||||
}
|
||||
return path[start:end], end, end < len(path)
|
||||
}
|
||||
|
||||
// Adds a new route to the trie.
|
||||
func (r *Router) addRoute(root *node, path string, h Handler, mw []Middleware) error {
|
||||
h = applyMiddleware(h, mw)
|
||||
if path == "/" {
|
||||
root.handler = h
|
||||
return nil
|
||||
}
|
||||
current := root
|
||||
pos := 0
|
||||
lastWC := false
|
||||
count := uint8(0)
|
||||
for {
|
||||
seg, newPos, more := readSegment(path, pos)
|
||||
if seg == "" {
|
||||
break
|
||||
}
|
||||
isDyn := len(seg) > 1 && seg[0] == ':'
|
||||
isWC := len(seg) > 0 && seg[0] == '*'
|
||||
if isWC {
|
||||
if lastWC || more {
|
||||
return fmt.Errorf("wildcard must be the last segment in the path")
|
||||
}
|
||||
lastWC = true
|
||||
}
|
||||
if isDyn || isWC {
|
||||
count++
|
||||
}
|
||||
var child *node
|
||||
for _, c := range current.children {
|
||||
if c.segment == seg {
|
||||
child = c
|
||||
break
|
||||
}
|
||||
}
|
||||
if child == nil {
|
||||
child = &node{segment: seg, isDynamic: isDyn, isWildcard: isWC}
|
||||
current.children = append(current.children, child)
|
||||
}
|
||||
if child.maxParams < count {
|
||||
child.maxParams = count
|
||||
}
|
||||
current = child
|
||||
pos = newPos
|
||||
}
|
||||
current.handler = h
|
||||
return nil
|
||||
}
|
||||
|
||||
// Finds a handler matching method and path.
|
||||
func (r *Router) Lookup(method, path string) (Handler, []string, bool) {
|
||||
root := r.methodNode(method)
|
||||
if root == nil {
|
||||
return nil, nil, false
|
||||
}
|
||||
if path == "/" {
|
||||
return root.handler, nil, root.handler != nil
|
||||
}
|
||||
|
||||
buffer := r.paramsBuffer
|
||||
if cap(buffer) < int(root.maxParams) {
|
||||
buffer = make([]string, root.maxParams)
|
||||
r.paramsBuffer = buffer
|
||||
}
|
||||
buffer = buffer[:0]
|
||||
|
||||
h, paramCount, found := match(root, path, 0, &buffer)
|
||||
if !found {
|
||||
return nil, nil, false
|
||||
}
|
||||
|
||||
return h, buffer[:paramCount], true
|
||||
}
|
||||
|
||||
// Traverses the trie to find a handler.
|
||||
func match(current *node, path string, start int, params *[]string) (Handler, int, bool) {
|
||||
paramCount := 0
|
||||
|
||||
for _, c := range current.children {
|
||||
if c.isWildcard {
|
||||
rem := path[start:]
|
||||
if len(rem) > 0 && rem[0] == '/' {
|
||||
rem = rem[1:]
|
||||
}
|
||||
*params = append(*params, rem)
|
||||
return c.handler, 1, c.handler != nil
|
||||
}
|
||||
}
|
||||
|
||||
seg, pos, more := readSegment(path, start)
|
||||
if seg == "" {
|
||||
return current.handler, 0, current.handler != nil
|
||||
}
|
||||
|
||||
for _, c := range current.children {
|
||||
if c.segment == seg || c.isDynamic {
|
||||
if c.isDynamic {
|
||||
*params = append(*params, seg)
|
||||
paramCount++
|
||||
}
|
||||
if !more {
|
||||
return c.handler, paramCount, c.handler != nil
|
||||
}
|
||||
h, nestedCount, ok := match(c, path, pos, params)
|
||||
if ok {
|
||||
return h, paramCount + nestedCount, true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, 0, false
|
||||
}
|
@ -4,35 +4,31 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"dk/internal/auth"
|
||||
"dk/internal/components"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/password"
|
||||
"dk/internal/router"
|
||||
"dk/internal/session"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||
"git.sharkk.net/Sharkk/Sushi/password"
|
||||
)
|
||||
|
||||
// RegisterAuthRoutes sets up authentication routes
|
||||
func RegisterAuthRoutes(r *router.Router) {
|
||||
guests := r.Group("")
|
||||
guests.Use(auth.RequireGuest())
|
||||
|
||||
guests.Get("/login", showLogin)
|
||||
guests.Post("/login", processLogin)
|
||||
guests.Get("/register", showRegister)
|
||||
guests.Post("/register", processRegister)
|
||||
|
||||
authed := r.Group("")
|
||||
authed.Use(auth.RequireAuth())
|
||||
func RegisterAuthRoutes(app *sushi.App) {
|
||||
// Public routes (no auth required)
|
||||
app.Get("/login", showLogin)
|
||||
app.Post("/login", processLogin)
|
||||
app.Get("/register", showRegister)
|
||||
app.Post("/register", processRegister)
|
||||
|
||||
// Protected routes
|
||||
authed := app.Group("")
|
||||
authed.Use(auth.RequireAuth("/login"))
|
||||
authed.Post("/logout", processLogout)
|
||||
}
|
||||
|
||||
// showLogin displays the login form
|
||||
func showLogin(ctx router.Ctx, _ []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func showLogin(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
var id string
|
||||
|
||||
if formData, exists := sess.Get("form_data"); exists {
|
||||
@ -41,7 +37,6 @@ func showLogin(ctx router.Ctx, _ []string) {
|
||||
}
|
||||
}
|
||||
sess.Delete("form_data")
|
||||
session.Store(sess)
|
||||
|
||||
components.RenderPage(ctx, "Log In", "auth/login.html", map[string]any{
|
||||
"id": id,
|
||||
@ -49,33 +44,35 @@ func showLogin(ctx router.Ctx, _ []string) {
|
||||
}
|
||||
|
||||
// processLogin handles login form submission
|
||||
func processLogin(ctx router.Ctx, _ []string) {
|
||||
func processLogin(ctx sushi.Ctx) {
|
||||
email := strings.TrimSpace(string(ctx.PostArgs().Peek("id")))
|
||||
userPassword := string(ctx.PostArgs().Peek("password"))
|
||||
|
||||
if email == "" || userPassword == "" {
|
||||
setFlashAndFormData(ctx, "Email and password are required", map[string]string{"id": email})
|
||||
ctx.Redirect("/login", fasthttp.StatusFound)
|
||||
ctx.Redirect("/login")
|
||||
return
|
||||
}
|
||||
|
||||
user, err := authenticate(email, userPassword)
|
||||
if err != nil {
|
||||
setFlashAndFormData(ctx, "Invalid email or password", map[string]string{"id": email})
|
||||
ctx.Redirect("/login", fasthttp.StatusFound)
|
||||
ctx.Redirect("/login")
|
||||
return
|
||||
}
|
||||
|
||||
auth.Login(ctx, user)
|
||||
ctx.Login(user.ID, user)
|
||||
|
||||
// CSRF token is already in session, no need to transfer from cookie
|
||||
// Set success message
|
||||
sess := ctx.GetCurrentSession()
|
||||
sess.SetFlash("success", fmt.Sprintf("Welcome back, %s!", user.Username))
|
||||
|
||||
ctx.Redirect("/", fasthttp.StatusFound)
|
||||
ctx.Redirect("/")
|
||||
}
|
||||
|
||||
// showRegister displays the registration form
|
||||
func showRegister(ctx router.Ctx, _ []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func showRegister(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
var username, email string
|
||||
|
||||
if formData, exists := sess.Get("form_data"); exists {
|
||||
@ -85,16 +82,16 @@ func showRegister(ctx router.Ctx, _ []string) {
|
||||
}
|
||||
}
|
||||
sess.Delete("form_data")
|
||||
session.Store(sess)
|
||||
|
||||
components.RenderPage(ctx, "Register", "auth/register.html", map[string]any{
|
||||
"username": username,
|
||||
"email": email,
|
||||
"error_message": sess.GetFlashMessage("error"),
|
||||
})
|
||||
}
|
||||
|
||||
// processRegister handles registration form submission
|
||||
func processRegister(ctx router.Ctx, _ []string) {
|
||||
func processRegister(ctx sushi.Ctx) {
|
||||
username := strings.TrimSpace(string(ctx.PostArgs().Peek("username")))
|
||||
email := strings.TrimSpace(string(ctx.PostArgs().Peek("email")))
|
||||
userPassword := string(ctx.PostArgs().Peek("password"))
|
||||
@ -107,53 +104,49 @@ func processRegister(ctx router.Ctx, _ []string) {
|
||||
|
||||
if err := validateRegistration(username, email, userPassword, confirmPassword); err != nil {
|
||||
setFlashAndFormData(ctx, err.Error(), formData)
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
ctx.Redirect("/register")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := users.ByUsername(username); err == nil {
|
||||
setFlashAndFormData(ctx, "Username already exists", formData)
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
ctx.Redirect("/register")
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := users.ByEmail(email); err == nil {
|
||||
setFlashAndFormData(ctx, "Email already registered", formData)
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
ctx.Redirect("/register")
|
||||
return
|
||||
}
|
||||
|
||||
user := users.New()
|
||||
user.Username = username
|
||||
user.Email = email
|
||||
user.Password = password.Hash(userPassword)
|
||||
user.Password = password.HashPassword(userPassword)
|
||||
user.ClassID = 1
|
||||
user.Auth = 1
|
||||
|
||||
if err := user.Insert(); err != nil {
|
||||
setFlashAndFormData(ctx, "Failed to create account", formData)
|
||||
ctx.Redirect("/register", fasthttp.StatusFound)
|
||||
ctx.Redirect("/register")
|
||||
return
|
||||
}
|
||||
|
||||
// Auto-login after registration (this will update the current session)
|
||||
auth.Login(ctx, user)
|
||||
// Auto-login after registration
|
||||
ctx.Login(user.ID, user)
|
||||
|
||||
// Update success message (Login already sets a message, so override it)
|
||||
if sess := ctx.UserValue("session").(*session.Session); sess != nil {
|
||||
sess.SetFlash("success", fmt.Sprintf("Greetings, %s!", user.Username))
|
||||
session.Store(sess)
|
||||
}
|
||||
// Set success message
|
||||
sess := ctx.GetCurrentSession()
|
||||
sess.SetFlash("success", fmt.Sprintf("Greetings, %s!", user.Username))
|
||||
|
||||
// CSRF token is already in session, no need to transfer from cookie
|
||||
|
||||
ctx.Redirect("/", fasthttp.StatusFound)
|
||||
ctx.Redirect("/")
|
||||
}
|
||||
|
||||
// processLogout handles logout
|
||||
func processLogout(ctx router.Ctx, params []string) {
|
||||
auth.Logout(ctx)
|
||||
ctx.Redirect("/", fasthttp.StatusFound)
|
||||
func processLogout(ctx sushi.Ctx) {
|
||||
ctx.Logout()
|
||||
ctx.Redirect("/")
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
@ -183,11 +176,10 @@ func validateRegistration(username, email, password, confirmPassword string) err
|
||||
return nil
|
||||
}
|
||||
|
||||
func setFlashAndFormData(ctx router.Ctx, message string, formData map[string]string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func setFlashAndFormData(ctx sushi.Ctx, message string, formData map[string]string) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
sess.SetFlash("error", message)
|
||||
sess.Set("form_data", formData)
|
||||
session.Store(sess)
|
||||
}
|
||||
|
||||
func authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
|
||||
@ -196,13 +188,15 @@ func authenticate(usernameOrEmail, plainPassword string) (*users.User, error) {
|
||||
|
||||
user, err = users.ByUsername(usernameOrEmail)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
user, err = users.ByEmail(usernameOrEmail)
|
||||
if err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
isValid, err := password.Verify(plainPassword, user.Password)
|
||||
isValid, err := password.VerifyPassword(plainPassword, user.Password)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -2,45 +2,61 @@ package routes
|
||||
|
||||
import (
|
||||
"dk/internal/actions"
|
||||
"dk/internal/auth"
|
||||
"dk/internal/components"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/models/fights"
|
||||
"dk/internal/models/monsters"
|
||||
"dk/internal/models/spells"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
"dk/internal/session"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"strconv"
|
||||
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||
)
|
||||
|
||||
func RegisterFightRoutes(r *router.Router) {
|
||||
group := r.Group("/fight")
|
||||
group.Use(auth.RequireAuth())
|
||||
group.Use(middleware.RequireFighting())
|
||||
func RegisterFightRoutes(app *sushi.App) {
|
||||
group := app.Group("/fight")
|
||||
group.Use(auth.RequireAuth("/login"))
|
||||
group.Use(requireFighting())
|
||||
|
||||
group.Get("/", showFight)
|
||||
group.Post("/", handleFightAction)
|
||||
}
|
||||
|
||||
func showFight(ctx router.Ctx, _ []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
// requireFighting middleware ensures the user is in a fight
|
||||
func requireFighting() sushi.Middleware {
|
||||
return func(ctx sushi.Ctx, next func()) {
|
||||
user := ctx.GetCurrentUser()
|
||||
if user == nil {
|
||||
ctx.SendError(401, "Not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
userModel := user.(*users.User)
|
||||
if !userModel.IsFighting() {
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
func showFight(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
|
||||
fight, err := fights.Find(user.FightID)
|
||||
if err != nil {
|
||||
ctx.SetContentType("text/plain")
|
||||
ctx.SetBodyString("Fight not found")
|
||||
ctx.SendError(404, "Fight not found")
|
||||
return
|
||||
}
|
||||
|
||||
monster, err := monsters.Find(fight.MonsterID)
|
||||
if err != nil {
|
||||
ctx.SetContentType("text/plain")
|
||||
ctx.SetBodyString("Monster not found for fight")
|
||||
ctx.SendError(404, "Monster not found for fight")
|
||||
return
|
||||
}
|
||||
|
||||
@ -82,9 +98,9 @@ func showFight(ctx router.Ctx, _ []string) {
|
||||
})
|
||||
}
|
||||
|
||||
func handleFightAction(ctx router.Ctx, _ []string) {
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func handleFightAction(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
|
||||
fight, err := fights.Find(user.FightID)
|
||||
if err != nil {
|
||||
|
@ -5,52 +5,51 @@ import (
|
||||
"dk/internal/components"
|
||||
"dk/internal/models/towns"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
"dk/internal/session"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
)
|
||||
|
||||
func Index(ctx router.Ctx, _ []string) {
|
||||
user, ok := ctx.UserValue("user").(*users.User)
|
||||
if !ok || user == nil {
|
||||
func Index(ctx sushi.Ctx) {
|
||||
if !ctx.IsAuthenticated() {
|
||||
components.RenderPage(ctx, "", "intro.html", nil)
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
|
||||
switch user.Currently {
|
||||
case "In Town":
|
||||
ctx.Redirect("/town", 303)
|
||||
ctx.Redirect("/town")
|
||||
case "Exploring":
|
||||
ctx.Redirect("/explore", 303)
|
||||
ctx.Redirect("/explore")
|
||||
case "Fighting":
|
||||
ctx.Redirect("/fight", 303)
|
||||
ctx.Redirect("/fight")
|
||||
default:
|
||||
ctx.Redirect("/explore", 303)
|
||||
ctx.Redirect("/explore")
|
||||
}
|
||||
}
|
||||
|
||||
func Move(ctx router.Ctx, _ []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
func Move(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
|
||||
if user.Currently == "Fighting" {
|
||||
sess.SetFlash("error", "You can't just run from a fight!")
|
||||
ctx.Redirect("/fight", 303)
|
||||
ctx.Redirect("/fight")
|
||||
return
|
||||
}
|
||||
|
||||
dir, err := strconv.Atoi(string(ctx.PostArgs().Peek("direction")))
|
||||
if err != nil {
|
||||
ctx.SetContentType("text/plain")
|
||||
ctx.SetBodyString("move form parsing error")
|
||||
ctx.SendError(400, "move form parsing error")
|
||||
return
|
||||
}
|
||||
|
||||
currently, newX, newY, err := actions.Move(user, actions.Direction(dir))
|
||||
if err != nil {
|
||||
ctx.SetContentType("text/plain")
|
||||
ctx.SetBodyString("move error: " + err.Error())
|
||||
ctx.SendError(400, "move error: "+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
@ -60,50 +59,45 @@ func Move(ctx router.Ctx, _ []string) {
|
||||
|
||||
switch currently {
|
||||
case "In Town":
|
||||
ctx.Redirect("/town", 303)
|
||||
ctx.Redirect("/town")
|
||||
case "Fighting":
|
||||
ctx.Redirect("/fight", 303)
|
||||
ctx.Redirect("/fight")
|
||||
default:
|
||||
ctx.Redirect("/explore", 303)
|
||||
ctx.Redirect("/explore")
|
||||
}
|
||||
}
|
||||
|
||||
func Explore(ctx router.Ctx, _ []string) {
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
func Explore(ctx sushi.Ctx) {
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
if user.Currently != "Exploring" {
|
||||
ctx.Redirect("/", 303)
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
components.RenderPage(ctx, "", "explore.html", nil)
|
||||
}
|
||||
|
||||
func Teleport(ctx router.Ctx, params []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func Teleport(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
|
||||
id, err := strconv.Atoi(params[0])
|
||||
if err != nil {
|
||||
sess.SetFlash("error", "Error teleporting; "+err.Error())
|
||||
ctx.Redirect("/", 302)
|
||||
return
|
||||
}
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
town, err := towns.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", "Failed to teleport. Unknown town.")
|
||||
ctx.Redirect("/", 302)
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
if !slices.Contains(user.GetTownIDs(), id) {
|
||||
sess.SetFlash("error", "You don't have a map to "+town.Name+".")
|
||||
ctx.Redirect("/", 302)
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
|
||||
if user.TP < town.TPCost {
|
||||
sess.SetFlash("error", "You don't have enough TP to teleport to "+town.Name+".")
|
||||
ctx.Redirect("/", 302)
|
||||
ctx.Redirect("/")
|
||||
return
|
||||
}
|
||||
|
||||
@ -113,5 +107,5 @@ func Teleport(ctx router.Ctx, params []string) {
|
||||
user.Save()
|
||||
|
||||
sess.SetFlash("success", "You teleported to "+town.Name+" successfully!")
|
||||
ctx.Redirect("/town", 302)
|
||||
ctx.Redirect("/town")
|
||||
}
|
||||
|
@ -2,17 +2,16 @@ package routes
|
||||
|
||||
import (
|
||||
"dk/internal/actions"
|
||||
"dk/internal/auth"
|
||||
"dk/internal/components"
|
||||
"dk/internal/helpers"
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/models/items"
|
||||
"dk/internal/models/towns"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
"dk/internal/session"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strconv"
|
||||
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||
)
|
||||
|
||||
// Map acts as a representation of owned/unowned maps in the town stores.
|
||||
@ -26,10 +25,10 @@ type Map struct {
|
||||
TP int
|
||||
}
|
||||
|
||||
func RegisterTownRoutes(r *router.Router) {
|
||||
group := r.Group("/town")
|
||||
group.Use(auth.RequireAuth())
|
||||
group.Use(middleware.RequireTown())
|
||||
func RegisterTownRoutes(app *sushi.App) {
|
||||
group := app.Group("/town")
|
||||
group.Use(auth.RequireAuth("/login"))
|
||||
group.Use(requireTown())
|
||||
|
||||
group.Get("/", showTown)
|
||||
group.Get("/inn", showInn)
|
||||
@ -40,7 +39,33 @@ func RegisterTownRoutes(r *router.Router) {
|
||||
group.Get("/maps/buy/:id", buyMap)
|
||||
}
|
||||
|
||||
func showTown(ctx router.Ctx, _ []string) {
|
||||
// requireTown middleware ensures the user is in a town
|
||||
func requireTown() sushi.Middleware {
|
||||
return func(ctx sushi.Ctx, next func()) {
|
||||
user := ctx.GetCurrentUser()
|
||||
if user == nil {
|
||||
ctx.SendError(401, "Not authenticated")
|
||||
return
|
||||
}
|
||||
|
||||
userModel := user.(*users.User)
|
||||
if userModel.Currently != "In Town" {
|
||||
ctx.SendError(403, "You must be in town")
|
||||
return
|
||||
}
|
||||
|
||||
town, err := towns.ByCoords(userModel.X, userModel.Y)
|
||||
if err != nil || town == nil || town.ID == 0 {
|
||||
ctx.SendError(403, fmt.Sprintf("Invalid town location (%d, %d)", userModel.X, userModel.Y))
|
||||
return
|
||||
}
|
||||
|
||||
ctx.SetUserValue("town", town)
|
||||
next()
|
||||
}
|
||||
}
|
||||
|
||||
func showTown(ctx sushi.Ctx) {
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
components.RenderPage(ctx, town.Name, "town/town.html", map[string]any{
|
||||
"town": town,
|
||||
@ -49,7 +74,7 @@ func showTown(ctx router.Ctx, _ []string) {
|
||||
})
|
||||
}
|
||||
|
||||
func showInn(ctx router.Ctx, _ []string) {
|
||||
func showInn(ctx sushi.Ctx) {
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
|
||||
"town": town,
|
||||
@ -57,19 +82,20 @@ func showInn(ctx router.Ctx, _ []string) {
|
||||
})
|
||||
}
|
||||
|
||||
func rest(ctx router.Ctx, _ []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func rest(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
|
||||
if user.Gold < town.InnCost {
|
||||
sess.SetFlash("error", "You can't afford to stay here tonight.")
|
||||
ctx.Redirect("/town/inn", 303)
|
||||
ctx.Redirect("/town/inn")
|
||||
return
|
||||
}
|
||||
|
||||
user.Gold -= town.InnCost
|
||||
user.HP, user.MP, user.TP = user.MaxHP, user.MaxMP, user.MaxTP
|
||||
user.Save()
|
||||
|
||||
components.RenderPage(ctx, town.Name+" Inn", "town/inn.html", map[string]any{
|
||||
"town": town,
|
||||
@ -77,14 +103,13 @@ func rest(ctx router.Ctx, _ []string) {
|
||||
})
|
||||
}
|
||||
|
||||
func showShop(ctx router.Ctx, _ []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func showShop(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
var errorHTML string
|
||||
|
||||
if flash, exists := sess.GetFlash("error"); exists {
|
||||
if msg, ok := flash.(string); ok {
|
||||
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + msg + "</div>"
|
||||
}
|
||||
errorMsg := sess.GetFlashMessage("error")
|
||||
if errorMsg != "" {
|
||||
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + errorMsg + "</div>"
|
||||
}
|
||||
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
@ -107,34 +132,29 @@ func showShop(ctx router.Ctx, _ []string) {
|
||||
})
|
||||
}
|
||||
|
||||
func buyItem(ctx router.Ctx, params []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func buyItem(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
|
||||
id, err := strconv.Atoi(params[0])
|
||||
if err != nil {
|
||||
sess.SetFlash("error", "Error purchasing item; "+err.Error())
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
return
|
||||
}
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
if !slices.Contains(town.GetShopItems(), id) {
|
||||
sess.SetFlash("error", "The item doesn't exist in this shop.")
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
ctx.Redirect("/town/shop")
|
||||
return
|
||||
}
|
||||
|
||||
item, err := items.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", "Error purchasing item; "+err.Error())
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
ctx.Redirect("/town/shop")
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
if user.Gold < item.Value {
|
||||
sess.SetFlash("error", "You don't have enough gold to buy "+item.Name)
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
ctx.Redirect("/town/shop")
|
||||
return
|
||||
}
|
||||
|
||||
@ -142,21 +162,20 @@ func buyItem(ctx router.Ctx, params []string) {
|
||||
actions.UserEquipItem(user, item)
|
||||
user.Save()
|
||||
|
||||
ctx.Redirect("/town/shop", 302)
|
||||
ctx.Redirect("/town/shop")
|
||||
}
|
||||
|
||||
func showMaps(ctx router.Ctx, _ []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func showMaps(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
var errorHTML string
|
||||
|
||||
if flash, exists := sess.GetFlash("error"); exists {
|
||||
if msg, ok := flash.(string); ok {
|
||||
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + msg + "</div>"
|
||||
}
|
||||
errorMsg := sess.GetFlashMessage("error")
|
||||
if errorMsg != "" {
|
||||
errorHTML = `<div style="color: red; margin-bottom: 1rem;">` + errorMsg + "</div>"
|
||||
}
|
||||
|
||||
town := ctx.UserValue("town").(*towns.Town)
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
|
||||
maplist := helpers.NewOrderedMap[int, Map]()
|
||||
towns, _ := towns.All()
|
||||
@ -190,37 +209,30 @@ func showMaps(ctx router.Ctx, _ []string) {
|
||||
})
|
||||
}
|
||||
|
||||
func buyMap(ctx router.Ctx, params []string) {
|
||||
sess := ctx.UserValue("session").(*session.Session)
|
||||
func buyMap(ctx sushi.Ctx) {
|
||||
sess := ctx.GetCurrentSession()
|
||||
|
||||
id, err := strconv.Atoi(params[0])
|
||||
if err != nil {
|
||||
sess.SetFlash("error", "Error purchasing map; "+err.Error())
|
||||
ctx.Redirect("/town/maps", 302)
|
||||
return
|
||||
}
|
||||
id := ctx.Param("id").Int()
|
||||
|
||||
mapped, err := towns.Find(id)
|
||||
if err != nil {
|
||||
sess.SetFlash("error", "Error purchasing map; "+err.Error())
|
||||
ctx.Redirect("/town/maps", 302)
|
||||
ctx.Redirect("/town/maps")
|
||||
return
|
||||
}
|
||||
|
||||
user := ctx.UserValue("user").(*users.User)
|
||||
user := ctx.GetCurrentUser().(*users.User)
|
||||
if user.Gold < mapped.MapCost {
|
||||
sess.SetFlash("error", "You don't have enough gold to buy the map to "+mapped.Name)
|
||||
ctx.Redirect("/town/maps", 302)
|
||||
ctx.Redirect("/town/maps")
|
||||
return
|
||||
}
|
||||
|
||||
user.Gold -= mapped.MapCost
|
||||
if user.Towns == "" {
|
||||
user.Towns = params[0]
|
||||
} else {
|
||||
user.Towns += "," + params[0]
|
||||
}
|
||||
townIDs := user.GetTownIDs()
|
||||
townIDs = append(townIDs, id)
|
||||
user.SetTownIDs(townIDs)
|
||||
user.Save()
|
||||
|
||||
ctx.Redirect("/town/maps", 302)
|
||||
ctx.Redirect("/town/maps")
|
||||
}
|
||||
|
@ -1,180 +0,0 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SessionManager handles session storage and persistence
|
||||
type SessionManager struct {
|
||||
mu sync.RWMutex
|
||||
sessions map[string]*Session
|
||||
filePath string
|
||||
}
|
||||
|
||||
var Manager *SessionManager
|
||||
|
||||
// sessionData represents session data for JSON serialization (excludes ID)
|
||||
type sessionData struct {
|
||||
UserID int `json:"user_id"`
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
// Init initializes the global session manager
|
||||
func Init(filePath string) {
|
||||
if Manager != nil {
|
||||
panic("session manager already initialized")
|
||||
}
|
||||
|
||||
Manager = &SessionManager{
|
||||
sessions: make(map[string]*Session),
|
||||
filePath: filePath,
|
||||
}
|
||||
|
||||
Manager.load()
|
||||
}
|
||||
|
||||
// GetManager returns the global session manager
|
||||
func GetManager() *SessionManager {
|
||||
if Manager == nil {
|
||||
panic("session manager not initialized")
|
||||
}
|
||||
return Manager
|
||||
}
|
||||
|
||||
// Create creates and stores a new session
|
||||
func (sm *SessionManager) Create(userID int) *Session {
|
||||
sess := New(userID)
|
||||
sm.mu.Lock()
|
||||
sm.sessions[sess.ID] = sess
|
||||
sm.mu.Unlock()
|
||||
return sess
|
||||
}
|
||||
|
||||
// Get retrieves a session by ID
|
||||
func (sm *SessionManager) Get(sessionID string) (*Session, bool) {
|
||||
sm.mu.RLock()
|
||||
sess, exists := sm.sessions[sessionID]
|
||||
sm.mu.RUnlock()
|
||||
|
||||
if !exists || sess.IsExpired() {
|
||||
if exists {
|
||||
sm.Delete(sessionID)
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return sess, true
|
||||
}
|
||||
|
||||
// Store saves a session in memory (updates existing or creates new)
|
||||
func (sm *SessionManager) Store(sess *Session) {
|
||||
sm.mu.Lock()
|
||||
sm.sessions[sess.ID] = sess
|
||||
sm.mu.Unlock()
|
||||
}
|
||||
|
||||
// Delete removes a session
|
||||
func (sm *SessionManager) Delete(sessionID string) {
|
||||
sm.mu.Lock()
|
||||
delete(sm.sessions, sessionID)
|
||||
sm.mu.Unlock()
|
||||
}
|
||||
|
||||
// Cleanup removes expired sessions
|
||||
func (sm *SessionManager) Cleanup() {
|
||||
sm.mu.Lock()
|
||||
for id, sess := range sm.sessions {
|
||||
if sess.IsExpired() {
|
||||
delete(sm.sessions, id)
|
||||
}
|
||||
}
|
||||
sm.mu.Unlock()
|
||||
}
|
||||
|
||||
// Stats returns session statistics
|
||||
func (sm *SessionManager) Stats() (total, active int) {
|
||||
sm.mu.RLock()
|
||||
defer sm.mu.RUnlock()
|
||||
|
||||
total = len(sm.sessions)
|
||||
for _, sess := range sm.sessions {
|
||||
if !sess.IsExpired() {
|
||||
active++
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// load reads sessions from the JSON file
|
||||
func (sm *SessionManager) load() {
|
||||
if sm.filePath == "" {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(sm.filePath)
|
||||
if err != nil {
|
||||
return // File doesn't exist or can't be read
|
||||
}
|
||||
|
||||
var sessionsData map[string]*sessionData
|
||||
if err := json.Unmarshal(data, &sessionsData); err != nil {
|
||||
return // Invalid JSON
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
sm.mu.Lock()
|
||||
for id, data := range sessionsData {
|
||||
if data != nil && data.ExpiresAt > now {
|
||||
sess := &Session{
|
||||
ID: id,
|
||||
UserID: data.UserID, // Make sure we restore the UserID properly
|
||||
ExpiresAt: data.ExpiresAt,
|
||||
Data: data.Data,
|
||||
}
|
||||
if sess.Data == nil {
|
||||
sess.Data = make(map[string]any)
|
||||
}
|
||||
sm.sessions[id] = sess
|
||||
}
|
||||
}
|
||||
sm.mu.Unlock()
|
||||
}
|
||||
|
||||
// Save writes sessions to the JSON file
|
||||
func (sm *SessionManager) Save() error {
|
||||
if sm.filePath == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
sm.Cleanup() // Remove expired sessions before saving
|
||||
|
||||
sm.mu.RLock()
|
||||
|
||||
// Convert sessions to sessionData (without ID field)
|
||||
sessionsData := make(map[string]*sessionData, len(sm.sessions))
|
||||
for id, sess := range sm.sessions {
|
||||
sessionsData[id] = &sessionData{
|
||||
UserID: sess.UserID, // Save the actual UserID from the struct
|
||||
ExpiresAt: sess.ExpiresAt,
|
||||
Data: sess.Data,
|
||||
}
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(sessionsData, "", "\t")
|
||||
sm.mu.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(sm.filePath, data, 0600)
|
||||
}
|
||||
|
||||
// Close saves sessions and cleans up
|
||||
func (sm *SessionManager) Close() error {
|
||||
return sm.Save()
|
||||
}
|
@ -1,146 +0,0 @@
|
||||
// session.go
|
||||
package session
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultExpiration = 24 * time.Hour
|
||||
IDLength = 32
|
||||
)
|
||||
|
||||
// Session represents a user session
|
||||
type Session struct {
|
||||
ID string `json:"id"`
|
||||
UserID int `json:"user_id"` // 0 for guest sessions
|
||||
ExpiresAt int64 `json:"expires_at"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
|
||||
// New creates a new session
|
||||
func New(userID int) *Session {
|
||||
return &Session{
|
||||
ID: generateID(),
|
||||
UserID: userID,
|
||||
ExpiresAt: time.Now().Add(DefaultExpiration).Unix(),
|
||||
Data: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// IsExpired checks if the session has expired
|
||||
func (s *Session) IsExpired() bool {
|
||||
return time.Now().Unix() > s.ExpiresAt
|
||||
}
|
||||
|
||||
// Touch extends the session expiration
|
||||
func (s *Session) Touch() {
|
||||
s.ExpiresAt = time.Now().Add(DefaultExpiration).Unix()
|
||||
}
|
||||
|
||||
// Set stores a value in the session
|
||||
func (s *Session) Set(key string, value any) {
|
||||
s.Data[key] = value
|
||||
}
|
||||
|
||||
// Get retrieves a value from the session
|
||||
func (s *Session) Get(key string) (any, bool) {
|
||||
value, exists := s.Data[key]
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// Delete removes a value from the session
|
||||
func (s *Session) Delete(key string) {
|
||||
delete(s.Data, key)
|
||||
}
|
||||
|
||||
// SetFlash stores a flash message (consumed on next Get)
|
||||
func (s *Session) SetFlash(key string, value any) {
|
||||
s.Set("flash_"+key, value)
|
||||
}
|
||||
|
||||
// GetFlash retrieves and removes a flash message
|
||||
func (s *Session) GetFlash(key string) (any, bool) {
|
||||
flashKey := "flash_" + key
|
||||
value, exists := s.Get(flashKey)
|
||||
if exists {
|
||||
s.Delete(flashKey)
|
||||
}
|
||||
return value, exists
|
||||
}
|
||||
|
||||
// GetFlashMessage retrieves and removes a flash message as string or empty string
|
||||
func (s *Session) GetFlashMessage(key string) string {
|
||||
if flash, exists := s.GetFlash(key); exists {
|
||||
if msg, ok := flash.(string); ok {
|
||||
return msg
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// DeleteFlash removes a flash from the session.
|
||||
func (s *Session) DeleteFlash(key string) {
|
||||
s.GetFlash(key)
|
||||
}
|
||||
|
||||
// RegenerateID creates a new session ID and updates storage
|
||||
func (s *Session) RegenerateID() {
|
||||
oldID := s.ID
|
||||
s.ID = generateID()
|
||||
|
||||
if Manager != nil {
|
||||
Manager.mu.Lock()
|
||||
delete(Manager.sessions, oldID)
|
||||
Manager.sessions[s.ID] = s
|
||||
Manager.mu.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// SetUserID updates the session's user ID (for login/logout)
|
||||
func (s *Session) SetUserID(userID int) {
|
||||
s.UserID = userID
|
||||
}
|
||||
|
||||
// generateID creates a random session ID
|
||||
func generateID() string {
|
||||
bytes := make([]byte, IDLength)
|
||||
rand.Read(bytes)
|
||||
return hex.EncodeToString(bytes)
|
||||
}
|
||||
|
||||
// Package-level convenience functions
|
||||
func Create(userID int) *Session {
|
||||
return Manager.Create(userID)
|
||||
}
|
||||
|
||||
func Get(sessionID string) (*Session, bool) {
|
||||
return Manager.Get(sessionID)
|
||||
}
|
||||
|
||||
func Store(sess *Session) {
|
||||
Manager.Store(sess)
|
||||
}
|
||||
|
||||
func Delete(sessionID string) {
|
||||
Manager.Delete(sessionID)
|
||||
}
|
||||
|
||||
func Cleanup() {
|
||||
Manager.Cleanup()
|
||||
}
|
||||
|
||||
func Stats() (total, active int) {
|
||||
return Manager.Stats()
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
return Manager.Close()
|
||||
}
|
||||
|
||||
// RegenerateID regenerates the session ID for security (package-level convenience)
|
||||
func RegenerateID(sess *Session) {
|
||||
sess.RegenerateID()
|
||||
}
|
@ -1,526 +0,0 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Validatable interface for entities that can validate themselves
|
||||
type Validatable interface {
|
||||
Validate() error
|
||||
}
|
||||
|
||||
// IndexBuilder function type for building custom indices
|
||||
type IndexBuilder[T any] func(allItems map[int]*T) any
|
||||
|
||||
// BaseStore provides generic storage with index management
|
||||
type BaseStore[T any] struct {
|
||||
items map[int]*T
|
||||
maxID int
|
||||
mu sync.RWMutex
|
||||
itemType reflect.Type
|
||||
indices map[string]any
|
||||
indexBuilders map[string]IndexBuilder[T]
|
||||
}
|
||||
|
||||
// NewBaseStore creates a new base store for type T
|
||||
func NewBaseStore[T any]() *BaseStore[T] {
|
||||
var zero T
|
||||
return &BaseStore[T]{
|
||||
items: make(map[int]*T),
|
||||
maxID: 0,
|
||||
itemType: reflect.TypeOf(zero),
|
||||
indices: make(map[string]any),
|
||||
indexBuilders: make(map[string]IndexBuilder[T]),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterIndex registers an index builder function
|
||||
func (bs *BaseStore[T]) RegisterIndex(name string, builder IndexBuilder[T]) {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
bs.indexBuilders[name] = builder
|
||||
}
|
||||
|
||||
// GetIndex retrieves a named index
|
||||
func (bs *BaseStore[T]) GetIndex(name string) (any, bool) {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
index, exists := bs.indices[name]
|
||||
return index, exists
|
||||
}
|
||||
|
||||
// RebuildIndices rebuilds all registered indices
|
||||
func (bs *BaseStore[T]) RebuildIndices() {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
bs.rebuildIndicesUnsafe()
|
||||
}
|
||||
|
||||
func (bs *BaseStore[T]) rebuildIndicesUnsafe() {
|
||||
allItems := make(map[int]*T, len(bs.items))
|
||||
maps.Copy(allItems, bs.items)
|
||||
|
||||
for name, builder := range bs.indexBuilders {
|
||||
bs.indices[name] = builder(allItems)
|
||||
}
|
||||
}
|
||||
|
||||
// AddWithRebuild adds item with validation and index rebuild
|
||||
func (bs *BaseStore[T]) AddWithRebuild(id int, item *T) error {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
|
||||
if validatable, ok := any(item).(Validatable); ok {
|
||||
if err := validatable.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
bs.items[id] = item
|
||||
if id > bs.maxID {
|
||||
bs.maxID = id
|
||||
}
|
||||
|
||||
bs.rebuildIndicesUnsafe()
|
||||
return nil
|
||||
}
|
||||
|
||||
// RemoveWithRebuild removes item and rebuilds indices
|
||||
func (bs *BaseStore[T]) RemoveWithRebuild(id int) {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
delete(bs.items, id)
|
||||
bs.rebuildIndicesUnsafe()
|
||||
}
|
||||
|
||||
// UpdateWithRebuild updates item with validation and index rebuild
|
||||
func (bs *BaseStore[T]) UpdateWithRebuild(id int, item *T) error {
|
||||
return bs.AddWithRebuild(id, item)
|
||||
}
|
||||
|
||||
// Common Query Methods
|
||||
|
||||
// Find retrieves an item by ID
|
||||
func (bs *BaseStore[T]) Find(id int) (*T, bool) {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
item, exists := bs.items[id]
|
||||
return item, exists
|
||||
}
|
||||
|
||||
// AllSorted returns all items using named sorted index
|
||||
func (bs *BaseStore[T]) AllSorted(indexName string) []*T {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
|
||||
if index, exists := bs.indices[indexName]; exists {
|
||||
if sortedIDs, ok := index.([]int); ok {
|
||||
result := make([]*T, 0, len(sortedIDs))
|
||||
for _, id := range sortedIDs {
|
||||
if item, exists := bs.items[id]; exists {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: return all items by ID order
|
||||
ids := make([]int, 0, len(bs.items))
|
||||
for id := range bs.items {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Ints(ids)
|
||||
|
||||
result := make([]*T, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
result = append(result, bs.items[id])
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// LookupByIndex finds single item using string lookup index
|
||||
func (bs *BaseStore[T]) LookupByIndex(indexName, key string) (*T, bool) {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
|
||||
if index, exists := bs.indices[indexName]; exists {
|
||||
if lookupMap, ok := index.(map[string]int); ok {
|
||||
if id, found := lookupMap[key]; found {
|
||||
if item, exists := bs.items[id]; exists {
|
||||
return item, true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// GroupByIndex returns items grouped by key
|
||||
func (bs *BaseStore[T]) GroupByIndex(indexName string, key any) []*T {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
|
||||
if index, exists := bs.indices[indexName]; exists {
|
||||
switch groupMap := index.(type) {
|
||||
case map[int][]int:
|
||||
if intKey, ok := key.(int); ok {
|
||||
if ids, found := groupMap[intKey]; found {
|
||||
result := make([]*T, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if item, exists := bs.items[id]; exists {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
case map[string][]int:
|
||||
if strKey, ok := key.(string); ok {
|
||||
if ids, found := groupMap[strKey]; found {
|
||||
result := make([]*T, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if item, exists := bs.items[id]; exists {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return []*T{}
|
||||
}
|
||||
|
||||
// FilterByIndex returns items matching filter criteria
|
||||
func (bs *BaseStore[T]) FilterByIndex(indexName string, filterFunc func(*T) bool) []*T {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
|
||||
var sourceIDs []int
|
||||
|
||||
if index, exists := bs.indices[indexName]; exists {
|
||||
if sortedIDs, ok := index.([]int); ok {
|
||||
sourceIDs = sortedIDs
|
||||
}
|
||||
}
|
||||
|
||||
if sourceIDs == nil {
|
||||
for id := range bs.items {
|
||||
sourceIDs = append(sourceIDs, id)
|
||||
}
|
||||
sort.Ints(sourceIDs)
|
||||
}
|
||||
|
||||
var result []*T
|
||||
for _, id := range sourceIDs {
|
||||
if item, exists := bs.items[id]; exists && filterFunc(item) {
|
||||
result = append(result, item)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// BuildStringLookupIndex creates string-to-ID mapping
|
||||
func BuildStringLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
||||
return func(allItems map[int]*T) any {
|
||||
index := make(map[string]int)
|
||||
for id, item := range allItems {
|
||||
key := keyFunc(item)
|
||||
index[key] = id
|
||||
}
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
// BuildCaseInsensitiveLookupIndex creates lowercase string-to-ID mapping
|
||||
func BuildCaseInsensitiveLookupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
||||
return func(allItems map[int]*T) any {
|
||||
index := make(map[string]int)
|
||||
for id, item := range allItems {
|
||||
key := strings.ToLower(keyFunc(item))
|
||||
index[key] = id
|
||||
}
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
// BuildIntGroupIndex creates int-to-[]ID mapping
|
||||
func BuildIntGroupIndex[T any](keyFunc func(*T) int) IndexBuilder[T] {
|
||||
return func(allItems map[int]*T) any {
|
||||
index := make(map[int][]int)
|
||||
for id, item := range allItems {
|
||||
key := keyFunc(item)
|
||||
index[key] = append(index[key], id)
|
||||
}
|
||||
|
||||
// Sort each group by ID
|
||||
for key := range index {
|
||||
sort.Ints(index[key])
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
// BuildStringGroupIndex creates string-to-[]ID mapping
|
||||
func BuildStringGroupIndex[T any](keyFunc func(*T) string) IndexBuilder[T] {
|
||||
return func(allItems map[int]*T) any {
|
||||
index := make(map[string][]int)
|
||||
for id, item := range allItems {
|
||||
key := keyFunc(item)
|
||||
index[key] = append(index[key], id)
|
||||
}
|
||||
|
||||
// Sort each group by ID
|
||||
for key := range index {
|
||||
sort.Ints(index[key])
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
// BuildSortedListIndex creates sorted []ID list
|
||||
func BuildSortedListIndex[T any](sortFunc func(*T, *T) bool) IndexBuilder[T] {
|
||||
return func(allItems map[int]*T) any {
|
||||
ids := make([]int, 0, len(allItems))
|
||||
for id := range allItems {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
|
||||
sort.Slice(ids, func(i, j int) bool {
|
||||
return sortFunc(allItems[ids[i]], allItems[ids[j]])
|
||||
})
|
||||
|
||||
return ids
|
||||
}
|
||||
}
|
||||
|
||||
// NewSingleton creates singleton store pattern with sync.Once
|
||||
func NewSingleton[S any](initFunc func() *S) func() *S {
|
||||
var store *S
|
||||
var once sync.Once
|
||||
|
||||
return func() *S {
|
||||
once.Do(func() {
|
||||
store = initFunc()
|
||||
})
|
||||
return store
|
||||
}
|
||||
}
|
||||
|
||||
// GetNextID returns the next available ID atomically
|
||||
func (bs *BaseStore[T]) GetNextID() int {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
bs.maxID++
|
||||
return bs.maxID
|
||||
}
|
||||
|
||||
// GetByID retrieves an item by ID
|
||||
func (bs *BaseStore[T]) GetByID(id int) (*T, bool) {
|
||||
return bs.Find(id)
|
||||
}
|
||||
|
||||
// Add adds an item to the store
|
||||
func (bs *BaseStore[T]) Add(id int, item *T) {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
bs.items[id] = item
|
||||
if id > bs.maxID {
|
||||
bs.maxID = id
|
||||
}
|
||||
}
|
||||
|
||||
// Remove removes an item from the store
|
||||
func (bs *BaseStore[T]) Remove(id int) {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
delete(bs.items, id)
|
||||
}
|
||||
|
||||
// GetAll returns all items
|
||||
func (bs *BaseStore[T]) GetAll() map[int]*T {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
result := make(map[int]*T, len(bs.items))
|
||||
maps.Copy(result, bs.items)
|
||||
return result
|
||||
}
|
||||
|
||||
// Clear removes all items
|
||||
func (bs *BaseStore[T]) Clear() {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
bs.items = make(map[int]*T)
|
||||
bs.maxID = 0
|
||||
bs.rebuildIndicesUnsafe()
|
||||
}
|
||||
|
||||
// LoadFromJSON loads items from JSON using reflection
|
||||
func (bs *BaseStore[T]) LoadFromJSON(filename string) error {
|
||||
bs.mu.Lock()
|
||||
defer bs.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to read JSON: %w", err)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Create slice of pointers to T
|
||||
sliceType := reflect.SliceOf(reflect.PointerTo(bs.itemType))
|
||||
slicePtr := reflect.New(sliceType)
|
||||
|
||||
if err := json.Unmarshal(data, slicePtr.Interface()); err != nil {
|
||||
return fmt.Errorf("failed to unmarshal JSON: %w", err)
|
||||
}
|
||||
|
||||
// Clear existing data
|
||||
bs.items = make(map[int]*T)
|
||||
bs.maxID = 0
|
||||
|
||||
// Extract items using reflection
|
||||
slice := slicePtr.Elem()
|
||||
for i := 0; i < slice.Len(); i++ {
|
||||
item := slice.Index(i).Interface().(*T)
|
||||
|
||||
// Get ID using reflection
|
||||
itemValue := reflect.ValueOf(item).Elem()
|
||||
idField := itemValue.FieldByName("ID")
|
||||
if !idField.IsValid() {
|
||||
return fmt.Errorf("item type must have an ID field")
|
||||
}
|
||||
|
||||
id := int(idField.Int())
|
||||
bs.items[id] = item
|
||||
if id > bs.maxID {
|
||||
bs.maxID = id
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveToJSON saves items to JSON atomically with consistent ID ordering
|
||||
func (bs *BaseStore[T]) SaveToJSON(filename string) error {
|
||||
bs.mu.RLock()
|
||||
defer bs.mu.RUnlock()
|
||||
|
||||
// Get sorted IDs for consistent ordering
|
||||
ids := make([]int, 0, len(bs.items))
|
||||
for id := range bs.items {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
sort.Ints(ids)
|
||||
|
||||
// Build items slice in ID order
|
||||
items := make([]*T, 0, len(bs.items))
|
||||
for _, id := range ids {
|
||||
items = append(items, bs.items[id])
|
||||
}
|
||||
|
||||
data, err := json.MarshalIndent(items, "", "\t")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal to JSON: %w", err)
|
||||
}
|
||||
|
||||
// Atomic write
|
||||
tempFile := filename + ".tmp"
|
||||
if err := os.WriteFile(tempFile, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write temp JSON: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tempFile, filename); err != nil {
|
||||
os.Remove(tempFile)
|
||||
return fmt.Errorf("failed to rename temp JSON: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadData loads from JSON file or starts empty
|
||||
func (bs *BaseStore[T]) LoadData(dataPath string) error {
|
||||
if err := bs.LoadFromJSON(dataPath); err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("No existing data found, starting with empty store")
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("failed to load from JSON: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Loaded %d items from %s\n", len(bs.items), dataPath)
|
||||
bs.RebuildIndices() // Rebuild indices after loading
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveData saves to JSON file
|
||||
func (bs *BaseStore[T]) SaveData(dataPath string) error {
|
||||
// Ensure directory exists
|
||||
dataDir := filepath.Dir(dataPath)
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
if err := bs.SaveToJSON(dataPath); err != nil {
|
||||
return fmt.Errorf("failed to save to JSON: %w", err)
|
||||
}
|
||||
|
||||
fmt.Printf("Saved %d items to %s\n", len(bs.items), dataPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
// BuildFilteredIntGroupIndex creates int-to-[]ID mapping for items passing filter
|
||||
func BuildFilteredIntGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) int) IndexBuilder[T] {
|
||||
return func(allItems map[int]*T) any {
|
||||
index := make(map[int][]int)
|
||||
for id, item := range allItems {
|
||||
if filterFunc(item) {
|
||||
key := keyFunc(item)
|
||||
index[key] = append(index[key], id)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each group by ID
|
||||
for key := range index {
|
||||
sort.Ints(index[key])
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
// BuildFilteredStringGroupIndex creates string-to-[]ID mapping for items passing filter
|
||||
func BuildFilteredStringGroupIndex[T any](filterFunc func(*T) bool, keyFunc func(*T) string) IndexBuilder[T] {
|
||||
return func(allItems map[int]*T) any {
|
||||
index := make(map[string][]int)
|
||||
for id, item := range allItems {
|
||||
if filterFunc(item) {
|
||||
key := keyFunc(item)
|
||||
index[key] = append(index[key], id)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort each group by ID
|
||||
for key := range index {
|
||||
sort.Ints(index[key])
|
||||
}
|
||||
|
||||
return index
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
)
|
||||
|
||||
type Template struct {
|
||||
@ -43,14 +43,12 @@ func (t *Template) RenderNamed(data map[string]any) string {
|
||||
return result
|
||||
}
|
||||
|
||||
func (t *Template) WriteTo(ctx *fasthttp.RequestCtx, data any) {
|
||||
var result string
|
||||
|
||||
func (t *Template) Render(data any) string {
|
||||
switch v := data.(type) {
|
||||
case map[string]any:
|
||||
result = t.RenderNamed(v)
|
||||
return t.RenderNamed(v)
|
||||
case []any:
|
||||
result = t.RenderPositional(v...)
|
||||
return t.RenderPositional(v...)
|
||||
default:
|
||||
rv := reflect.ValueOf(data)
|
||||
if rv.Kind() == reflect.Slice {
|
||||
@ -58,14 +56,17 @@ func (t *Template) WriteTo(ctx *fasthttp.RequestCtx, data any) {
|
||||
for i := 0; i < rv.Len(); i++ {
|
||||
args[i] = rv.Index(i).Interface()
|
||||
}
|
||||
result = t.RenderPositional(args...)
|
||||
return t.RenderPositional(args...)
|
||||
} else {
|
||||
result = t.RenderPositional(data)
|
||||
return t.RenderPositional(data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.WriteString(result)
|
||||
func (t *Template) WriteTo(ctx sushi.Ctx, data any) error {
|
||||
result := t.Render(data)
|
||||
ctx.SendHTML(result)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *Template) processBlocks(content string, blocks map[string]string) string {
|
||||
|
233
main.go
233
main.go
@ -9,9 +9,6 @@ import (
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"dk/internal/auth"
|
||||
"dk/internal/csrf"
|
||||
"dk/internal/middleware"
|
||||
"dk/internal/models/babble"
|
||||
"dk/internal/models/control"
|
||||
"dk/internal/models/drops"
|
||||
@ -23,12 +20,15 @@ import (
|
||||
"dk/internal/models/spells"
|
||||
"dk/internal/models/towns"
|
||||
"dk/internal/models/users"
|
||||
"dk/internal/router"
|
||||
"dk/internal/routes"
|
||||
"dk/internal/session"
|
||||
"dk/internal/template"
|
||||
|
||||
"github.com/valyala/fasthttp"
|
||||
nigiri "git.sharkk.net/Sharkk/Nigiri"
|
||||
sushi "git.sharkk.net/Sharkk/Sushi"
|
||||
"git.sharkk.net/Sharkk/Sushi/auth"
|
||||
"git.sharkk.net/Sharkk/Sushi/csrf"
|
||||
"git.sharkk.net/Sharkk/Sushi/session"
|
||||
"git.sharkk.net/Sharkk/Sushi/timing"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@ -53,106 +53,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadModels(dataDir string) error {
|
||||
if err := os.MkdirAll(dataDir, 0755); err != nil {
|
||||
return fmt.Errorf("failed to create data directory: %w", err)
|
||||
}
|
||||
|
||||
if err := users.LoadData(filepath.Join(dataDir, "users.json")); err != nil {
|
||||
return fmt.Errorf("failed to load users data: %w", err)
|
||||
}
|
||||
|
||||
if err := towns.LoadData(filepath.Join(dataDir, "towns.json")); err != nil {
|
||||
return fmt.Errorf("failed to load towns data: %w", err)
|
||||
}
|
||||
|
||||
if err := spells.LoadData(filepath.Join(dataDir, "spells.json")); err != nil {
|
||||
return fmt.Errorf("failed to load spells data: %w", err)
|
||||
}
|
||||
|
||||
if err := news.LoadData(filepath.Join(dataDir, "news.json")); err != nil {
|
||||
return fmt.Errorf("failed to load news data: %w", err)
|
||||
}
|
||||
|
||||
if err := monsters.LoadData(filepath.Join(dataDir, "monsters.json")); err != nil {
|
||||
return fmt.Errorf("failed to load monsters data: %w", err)
|
||||
}
|
||||
|
||||
if err := items.LoadData(filepath.Join(dataDir, "items.json")); err != nil {
|
||||
return fmt.Errorf("failed to load items data: %w", err)
|
||||
}
|
||||
|
||||
if err := forum.LoadData(filepath.Join(dataDir, "forum.json")); err != nil {
|
||||
return fmt.Errorf("failed to load forum data: %w", err)
|
||||
}
|
||||
|
||||
if err := drops.LoadData(filepath.Join(dataDir, "drops.json")); err != nil {
|
||||
return fmt.Errorf("failed to load drops data: %w", err)
|
||||
}
|
||||
|
||||
if err := babble.LoadData(filepath.Join(dataDir, "babble.json")); err != nil {
|
||||
return fmt.Errorf("failed to load babble data: %w", err)
|
||||
}
|
||||
|
||||
if err := control.Load(filepath.Join(dataDir, "control.json")); err != nil {
|
||||
return fmt.Errorf("failed to load control data: %w", err)
|
||||
}
|
||||
|
||||
if err := fights.LoadData(filepath.Join(dataDir, "fights.json")); err != nil {
|
||||
return fmt.Errorf("failed to load fights data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveModels(dataDir string) error {
|
||||
if err := users.SaveData(filepath.Join(dataDir, "users.json")); err != nil {
|
||||
return fmt.Errorf("failed to save users data: %w", err)
|
||||
}
|
||||
|
||||
if err := towns.SaveData(filepath.Join(dataDir, "towns.json")); err != nil {
|
||||
return fmt.Errorf("failed to save towns data: %w", err)
|
||||
}
|
||||
|
||||
if err := spells.SaveData(filepath.Join(dataDir, "spells.json")); err != nil {
|
||||
return fmt.Errorf("failed to save spells data: %w", err)
|
||||
}
|
||||
|
||||
if err := news.SaveData(filepath.Join(dataDir, "news.json")); err != nil {
|
||||
return fmt.Errorf("failed to save news data: %w", err)
|
||||
}
|
||||
|
||||
if err := monsters.SaveData(filepath.Join(dataDir, "monsters.json")); err != nil {
|
||||
return fmt.Errorf("failed to save monsters data: %w", err)
|
||||
}
|
||||
|
||||
if err := items.SaveData(filepath.Join(dataDir, "items.json")); err != nil {
|
||||
return fmt.Errorf("failed to save items data: %w", err)
|
||||
}
|
||||
|
||||
if err := forum.SaveData(filepath.Join(dataDir, "forum.json")); err != nil {
|
||||
return fmt.Errorf("failed to save forum data: %w", err)
|
||||
}
|
||||
|
||||
if err := drops.SaveData(filepath.Join(dataDir, "drops.json")); err != nil {
|
||||
return fmt.Errorf("failed to save drops data: %w", err)
|
||||
}
|
||||
|
||||
if err := babble.SaveData(filepath.Join(dataDir, "babble.json")); err != nil {
|
||||
return fmt.Errorf("failed to save babble data: %w", err)
|
||||
}
|
||||
|
||||
if err := control.Save(); err != nil {
|
||||
return fmt.Errorf("failed to save control data: %w", err)
|
||||
}
|
||||
|
||||
if err := fights.SaveData(filepath.Join(dataDir, "fights.json")); err != nil {
|
||||
return fmt.Errorf("failed to save fights data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startServer(port string) {
|
||||
fmt.Println("Starting Dragon Knight server...")
|
||||
if err := start(port); err != nil {
|
||||
@ -168,94 +68,85 @@ func start(port string) error {
|
||||
|
||||
template.InitializeCache(cwd)
|
||||
|
||||
if err := loadModels(filepath.Join(cwd, "data")); err != nil {
|
||||
return fmt.Errorf("failed to load models: %w", err)
|
||||
db := nigiri.NewCollection(filepath.Join(cwd, "data"))
|
||||
if err := setupStores(db); err != nil {
|
||||
return fmt.Errorf("failed to setup Nigiri stores: %w", err)
|
||||
}
|
||||
|
||||
session.Init(filepath.Join(cwd, "data/_sessions.json"))
|
||||
app := sushi.New()
|
||||
sushi.InitSessions(filepath.Join(cwd, "data/_sessions.json"))
|
||||
|
||||
r := router.New()
|
||||
r.Use(middleware.Timing())
|
||||
r.Use(auth.Middleware())
|
||||
r.Use(csrf.Middleware())
|
||||
app.Use(session.Middleware())
|
||||
app.Use(auth.Middleware(getUserByID))
|
||||
app.Use(csrf.Middleware())
|
||||
app.Use(timing.Middleware())
|
||||
|
||||
r.Get("/", routes.Index)
|
||||
app.Get("/", routes.Index)
|
||||
|
||||
actions := r.Group("")
|
||||
actions.Use(auth.RequireAuth())
|
||||
actions.Get("/explore", routes.Explore)
|
||||
actions.Post("/move", routes.Move)
|
||||
actions.Get("/teleport/:to", routes.Teleport)
|
||||
protected := app.Group("")
|
||||
protected.Use(auth.RequireAuth("/login"))
|
||||
protected.Get("/explore", routes.Explore)
|
||||
protected.Post("/move", routes.Move)
|
||||
protected.Get("/teleport/:to", routes.Teleport)
|
||||
|
||||
routes.RegisterAuthRoutes(r)
|
||||
routes.RegisterTownRoutes(r)
|
||||
routes.RegisterFightRoutes(r)
|
||||
routes.RegisterAuthRoutes(app)
|
||||
routes.RegisterTownRoutes(app)
|
||||
routes.RegisterFightRoutes(app)
|
||||
|
||||
// Use current working directory for static files
|
||||
assetsDir := filepath.Join(cwd, "assets")
|
||||
|
||||
// Static file server for /assets
|
||||
fs := &fasthttp.FS{
|
||||
Root: assetsDir,
|
||||
Compress: false,
|
||||
}
|
||||
assetsHandler := fs.NewRequestHandler()
|
||||
|
||||
// Combined handler
|
||||
requestHandler := func(ctx *fasthttp.RequestCtx) {
|
||||
path := string(ctx.Path())
|
||||
|
||||
// Handle static assets - strip /assets prefix
|
||||
if len(path) >= 7 && path[:7] == "/assets" {
|
||||
// Strip the /assets prefix for the file system handler
|
||||
originalPath := ctx.Path()
|
||||
ctx.Request.URI().SetPath(path[7:]) // Remove "/assets" prefix
|
||||
assetsHandler(ctx)
|
||||
ctx.Request.URI().SetPathBytes(originalPath) // Restore original path
|
||||
return
|
||||
}
|
||||
|
||||
// Handle routes
|
||||
r.ServeHTTP(ctx)
|
||||
}
|
||||
app.Get("/assets/*path", sushi.Static(cwd))
|
||||
|
||||
addr := ":" + port
|
||||
log.Printf("Server starting on %s", addr)
|
||||
|
||||
// Setup graceful shutdown
|
||||
server := &fasthttp.Server{
|
||||
Handler: requestHandler,
|
||||
}
|
||||
|
||||
// Channel to listen for interrupt signal
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
|
||||
// Start server in a goroutine
|
||||
go func() {
|
||||
if err := server.ListenAndServe(addr); err != nil {
|
||||
log.Printf("Server error: %v", err)
|
||||
}
|
||||
app.Listen(addr)
|
||||
}()
|
||||
|
||||
// Wait for interrupt signal
|
||||
<-c
|
||||
log.Println("Received shutdown signal, shutting down gracefully...")
|
||||
log.Println("\nReceived shutdown signal, shutting down gracefully...")
|
||||
|
||||
// Save all model data before shutdown
|
||||
log.Println("Saving model data...")
|
||||
if err := saveModels(filepath.Join(cwd, "data")); err != nil {
|
||||
log.Printf("Error saving model data: %v", err)
|
||||
log.Println("Saving database...")
|
||||
if err := db.Save(); err != nil {
|
||||
log.Printf("Error saving database: %v", err)
|
||||
}
|
||||
|
||||
// Save sessions before shutdown
|
||||
log.Println("Saving sessions...")
|
||||
if err := session.Close(); err != nil {
|
||||
log.Printf("Error saving sessions: %v", err)
|
||||
}
|
||||
sushi.SaveSessions()
|
||||
|
||||
// FastHTTP doesn't have a graceful Shutdown method like net/http
|
||||
// We just let the server stop naturally when the main function exits
|
||||
log.Println("Server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
func setupStores(db *nigiri.Collection) error {
|
||||
users.Init(db)
|
||||
towns.Init(db)
|
||||
spells.Init(db)
|
||||
news.Init(db)
|
||||
monsters.Init(db)
|
||||
items.Init(db)
|
||||
forum.Init(db)
|
||||
drops.Init(db)
|
||||
babble.Init(db)
|
||||
fights.Init(db)
|
||||
control.Init(db)
|
||||
|
||||
db.Add("users", users.GetStore())
|
||||
db.Add("towns", towns.GetStore())
|
||||
db.Add("spells", spells.GetStore())
|
||||
db.Add("news", news.GetStore())
|
||||
db.Add("monsters", monsters.GetStore())
|
||||
db.Add("items", items.GetStore())
|
||||
db.Add("forum", forum.GetStore())
|
||||
db.Add("drops", drops.GetStore())
|
||||
db.Add("babble", babble.GetStore())
|
||||
db.Add("fights", fights.GetStore())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getUserByID(userID int) any {
|
||||
return users.GetByID(userID)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user