diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 8b0d9e323..1f2fc99b3 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -4,8 +4,6 @@ on:
push:
branches:
- main
- tags:
- - 'v*'
env:
REGISTRY: ghcr.io
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ee56e460e..ad36a3181 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -7,6 +7,13 @@ on:
permissions:
contents: write
+ packages: write
+ attestations: write
+ id-token: write
+
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
jobs:
build:
@@ -56,9 +63,49 @@ jobs:
path: erupe-${{ matrix.os_name }}.zip
retention-days: 1
+ docker:
+ name: Build and Push Docker Image
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Log in to the Container registry
+ uses: docker/login-action@v3
+ with:
+ registry: ${{ env.REGISTRY }}
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Extract metadata (tags, labels) for Docker
+ id: meta
+ uses: docker/metadata-action@v5
+ with:
+ images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ tags: |
+ type=semver,pattern={{version}}
+ type=semver,pattern={{major}}.{{minor}}
+
+ - name: Build and push Docker image
+ id: push
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ push: true
+ tags: ${{ steps.meta.outputs.tags }}
+ labels: ${{ steps.meta.outputs.labels }}
+
+ - name: Generate artifact attestation
+ uses: actions/attest-build-provenance@v2
+ with:
+ subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
+ subject-digest: ${{ steps.push.outputs.digest }}
+ push-to-registry: true
+
release:
name: Create Release
- needs: build
+ needs: [build, docker]
runs-on: ubuntu-latest
steps:
@@ -76,6 +123,15 @@ jobs:
uses: softprops/action-gh-release@v2
with:
generate_release_notes: true
+ body: |
+ ## Docker Image
+
+ This release is also available as a Docker image:
+
+ ```bash
+ docker pull ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
+ ```
+ append_body: true
files: |
artifacts/Linux-amd64/erupe-Linux-amd64.zip
artifacts/Windows-amd64/erupe-Windows-amd64.zip
diff --git a/common/db/adapter.go b/common/db/adapter.go
new file mode 100644
index 000000000..de71b85bf
--- /dev/null
+++ b/common/db/adapter.go
@@ -0,0 +1,68 @@
+// Package db provides a database adapter that transparently translates
+// PostgreSQL-style SQL to the active driver's dialect.
+//
+// When the driver is "sqlite", queries are rewritten on the fly:
+// - $1, $2, ... → ?, ?, ...
+// - now() → CURRENT_TIMESTAMP
+// - ::type casts → removed
+// - ILIKE → LIKE (SQLite LIKE is case-insensitive for ASCII)
+package db
+
+import (
+ "regexp"
+ "strings"
+
+ "github.com/jmoiron/sqlx"
+)
+
+// IsSQLite reports whether the given sqlx.DB is backed by a SQLite driver.
+func IsSQLite(db *sqlx.DB) bool {
+ return db.DriverName() == "sqlite" || db.DriverName() == "sqlite3"
+}
+
+// Adapt rewrites a PostgreSQL query for the active driver.
+// For Postgres it's a no-op. For SQLite it translates placeholders and
+// Postgres-specific syntax.
+func Adapt(db *sqlx.DB, query string) string {
+ if !IsSQLite(db) {
+ return query
+ }
+ return AdaptSQL(query)
+}
+
+// castRe matches Postgres type casts like ::int, ::text, ::timestamptz,
+// ::character varying, etc.
+// castRe matches Postgres type casts: ::int, ::text, ::timestamptz,
+// ::character varying(N), etc. The space is allowed only when followed
+// by a word char (e.g. "character varying") to avoid eating trailing spaces.
+var castRe = regexp.MustCompile(`::[a-zA-Z_]\w*(?:\s+\w+)*(?:\([^)]*\))?`)
+
+// dollarParamRe matches Postgres-style positional parameters: $1, $2, etc.
+var dollarParamRe = regexp.MustCompile(`\$\d+`)
+
+// AdaptSQL translates a PostgreSQL query to SQLite-compatible SQL.
+// Exported so it can be tested without a real DB connection.
+func AdaptSQL(query string) string {
+ // 1. Replace now() with CURRENT_TIMESTAMP
+ query = strings.ReplaceAll(query, "now()", "CURRENT_TIMESTAMP")
+ query = strings.ReplaceAll(query, "NOW()", "CURRENT_TIMESTAMP")
+
+ // 2. Strip Postgres type casts (::int, ::text, ::timestamptz, etc.)
+ query = castRe.ReplaceAllString(query, "")
+
+ // 3. ILIKE → LIKE (SQLite LIKE is case-insensitive for ASCII by default)
+ query = strings.ReplaceAll(query, " ILIKE ", " LIKE ")
+ query = strings.ReplaceAll(query, " ilike ", " LIKE ")
+
+ // 4. Strip "public." schema prefix (SQLite has no schemas)
+ query = strings.ReplaceAll(query, "public.", "")
+
+ // 5. TRUNCATE → DELETE FROM (SQLite has no TRUNCATE)
+ query = strings.ReplaceAll(query, "TRUNCATE ", "DELETE FROM ")
+ query = strings.ReplaceAll(query, "truncate ", "DELETE FROM ")
+
+ // 6. Replace $1,$2,... → ?,?,...
+ query = dollarParamRe.ReplaceAllString(query, "?")
+
+ return query
+}
diff --git a/common/db/adapter_test.go b/common/db/adapter_test.go
new file mode 100644
index 000000000..08ff6f402
--- /dev/null
+++ b/common/db/adapter_test.go
@@ -0,0 +1,83 @@
+package db
+
+import (
+ "testing"
+)
+
+func TestAdaptSQL(t *testing.T) {
+ tests := []struct {
+ name string
+ input string
+ want string
+ }{
+ {
+ name: "placeholder rebind",
+ input: "SELECT * FROM users WHERE id=$1 AND name=$2",
+ want: "SELECT * FROM users WHERE id=? AND name=?",
+ },
+ {
+ name: "now() replacement",
+ input: "UPDATE characters SET guild_post_checked=now() WHERE id=$1",
+ want: "UPDATE characters SET guild_post_checked=CURRENT_TIMESTAMP WHERE id=?",
+ },
+ {
+ name: "type cast removal",
+ input: "UPDATE users SET frontier_points=frontier_points::int - $1 WHERE id=$2 RETURNING frontier_points",
+ want: "UPDATE users SET frontier_points=frontier_points - ? WHERE id=? RETURNING frontier_points",
+ },
+ {
+ name: "text cast removal",
+ input: "SELECT COALESCE(friends, ''::text) FROM characters WHERE id=$1",
+ want: "SELECT COALESCE(friends, '') FROM characters WHERE id=?",
+ },
+ {
+ name: "timestamptz cast removal",
+ input: "SELECT COALESCE(created_at, '2000-01-01'::timestamptz) FROM guilds WHERE id=$1",
+ want: "SELECT COALESCE(created_at, '2000-01-01') FROM guilds WHERE id=?",
+ },
+ {
+ name: "ILIKE to LIKE",
+ input: "SELECT * FROM characters WHERE name ILIKE $1",
+ want: "SELECT * FROM characters WHERE name LIKE ?",
+ },
+ {
+ name: "character varying cast",
+ input: "DEFAULT ''::character varying",
+ want: "DEFAULT ''",
+ },
+ {
+ name: "no changes for standard SQL",
+ input: "SELECT COUNT(*) FROM users",
+ want: "SELECT COUNT(*) FROM users",
+ },
+ {
+ name: "NOW uppercase",
+ input: "INSERT INTO events (start_time) VALUES (NOW())",
+ want: "INSERT INTO events (start_time) VALUES (CURRENT_TIMESTAMP)",
+ },
+ {
+ name: "multi-digit params",
+ input: "INSERT INTO t (a,b,c) VALUES ($1,$2,$10)",
+ want: "INSERT INTO t (a,b,c) VALUES (?,?,?)",
+ },
+ {
+ name: "public schema prefix",
+ input: "INSERT INTO public.distributions_accepted VALUES ($1, $2)",
+ want: "INSERT INTO distributions_accepted VALUES (?, ?)",
+ },
+ {
+ name: "TRUNCATE to DELETE FROM",
+ input: "TRUNCATE public.cafebonus;",
+ want: "DELETE FROM cafebonus;",
+ },
+ }
+
+ for _, tc := range tests {
+ t.Run(tc.name, func(t *testing.T) {
+ got := AdaptSQL(tc.input)
+ if got != tc.want {
+ t.Errorf("\ngot: %s\nwant: %s", got, tc.want)
+ }
+ })
+ }
+}
diff --git a/common/db/db.go b/common/db/db.go
new file mode 100644
index 000000000..0a5171d30
--- /dev/null
+++ b/common/db/db.go
@@ -0,0 +1,149 @@
+// Package db provides a transparent database wrapper that rewrites
+// PostgreSQL-style SQL for SQLite when needed.
+package db
+
+import (
+ "context"
+ "database/sql"
+
+ "github.com/jmoiron/sqlx"
+)
+
+// DB wraps *sqlx.DB and transparently adapts PostgreSQL queries for SQLite.
+// For PostgreSQL, all methods are simple pass-throughs with zero overhead.
+type DB struct {
+ *sqlx.DB
+ sqlite bool
+}
+
+// Wrap creates a DB wrapper around an existing *sqlx.DB connection.
+// Returns nil if db is nil.
+func Wrap(db *sqlx.DB) *DB {
+ if db == nil {
+ return nil
+ }
+ return &DB{
+ DB: db,
+ sqlite: IsSQLite(db),
+ }
+}
+
+func (d *DB) adapt(query string) string {
+ if !d.sqlite {
+ return query
+ }
+ return AdaptSQL(query)
+}
+
+// Exec executes a query without returning any rows.
+func (d *DB) Exec(query string, args ...interface{}) (sql.Result, error) {
+ return d.DB.Exec(d.adapt(query), args...)
+}
+
+// ExecContext executes a query without returning any rows.
+func (d *DB) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
+ return d.DB.ExecContext(ctx, d.adapt(query), args...)
+}
+
+// Query executes a query that returns rows.
+func (d *DB) Query(query string, args ...interface{}) (*sql.Rows, error) {
+ return d.DB.Query(d.adapt(query), args...)
+}
+
+// QueryContext executes a query that returns rows.
+func (d *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error) {
+ return d.DB.QueryContext(ctx, d.adapt(query), args...)
+}
+
+// QueryRow executes a query that is expected to return at most one row.
+func (d *DB) QueryRow(query string, args ...interface{}) *sql.Row {
+ return d.DB.QueryRow(d.adapt(query), args...)
+}
+
+// QueryRowContext executes a query that is expected to return at most one row.
+func (d *DB) QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row {
+ return d.DB.QueryRowContext(ctx, d.adapt(query), args...)
+}
+
+// Get queries a single row and scans it into dest.
+func (d *DB) Get(dest interface{}, query string, args ...interface{}) error {
+ return d.DB.Get(dest, d.adapt(query), args...)
+}
+
+// Select queries multiple rows and scans them into dest.
+func (d *DB) Select(dest interface{}, query string, args ...interface{}) error {
+ return d.DB.Select(dest, d.adapt(query), args...)
+}
+
+// Queryx executes a query that returns sqlx.Rows.
+func (d *DB) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) {
+ return d.DB.Queryx(d.adapt(query), args...)
+}
+
+// QueryRowx executes a query that returns a sqlx.Row.
+func (d *DB) QueryRowx(query string, args ...interface{}) *sqlx.Row {
+ return d.DB.QueryRowx(d.adapt(query), args...)
+}
+
+// QueryRowxContext executes a query that returns a sqlx.Row.
+func (d *DB) QueryRowxContext(ctx context.Context, query string, args ...interface{}) *sqlx.Row {
+ return d.DB.QueryRowxContext(ctx, d.adapt(query), args...)
+}
+
+// BeginTxx starts a new transaction with context and options.
+// The returned Tx wrapper adapts queries the same way as DB.
+func (d *DB) BeginTxx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) {
+ tx, err := d.DB.BeginTxx(ctx, opts)
+ if err != nil {
+ return nil, err
+ }
+ return &Tx{Tx: tx, sqlite: d.sqlite}, nil
+}
+
+// Tx wraps *sqlx.Tx and transparently adapts PostgreSQL queries for SQLite.
+type Tx struct {
+ *sqlx.Tx
+ sqlite bool
+}
+
+func (t *Tx) adapt(query string) string {
+ if !t.sqlite {
+ return query
+ }
+ return AdaptSQL(query)
+}
+
+// Exec executes a query without returning any rows.
+func (t *Tx) Exec(query string, args ...interface{}) (sql.Result, error) {
+ return t.Tx.Exec(t.adapt(query), args...)
+}
+
+// ExecContext executes a query without returning any rows.
+func (t *Tx) ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error) {
+ return t.Tx.ExecContext(ctx, t.adapt(query), args...)
+}
+
+// Query executes a query that returns rows.
+func (t *Tx) Query(query string, args ...interface{}) (*sql.Rows, error) {
+ return t.Tx.Query(t.adapt(query), args...)
+}
+
+// QueryRow executes a query that is expected to return at most one row.
+func (t *Tx) QueryRow(query string, args ...interface{}) *sql.Row {
+ return t.Tx.QueryRow(t.adapt(query), args...)
+}
+
+// Queryx executes a query that returns sqlx.Rows.
+func (t *Tx) Queryx(query string, args ...interface{}) (*sqlx.Rows, error) {
+ return t.Tx.Queryx(t.adapt(query), args...)
+}
+
+// QueryRowx executes a query that returns a sqlx.Row.
+func (t *Tx) QueryRowx(query string, args ...interface{}) *sqlx.Row {
+ return t.Tx.QueryRowx(t.adapt(query), args...)
+}
+
+// Get queries a single row and scans it into dest.
+func (t *Tx) Get(dest interface{}, query string, args ...interface{}) error {
+ return t.Tx.Get(dest, t.adapt(query), args...)
+}
diff --git a/config/config.go b/config/config.go
index 8555fe701..1c558d06c 100644
--- a/config/config.go
+++ b/config/config.go
@@ -231,8 +231,12 @@ type Course struct {
Enabled bool
}
-// Database holds the postgres database config.
+// Database holds the database config.
+// Driver can be "postgres" (default) or "sqlite".
+// When Driver is "sqlite", only Database (file path) is used;
+// Host, Port, User, and Password are ignored.
type Database struct {
+ Driver string // "postgres" or "sqlite"
Host string
Port int
User string
@@ -454,7 +458,8 @@ func registerDefaults() {
{Name: "EXRenewing", Enabled: true},
})
- // Database (Password deliberately has no default)
+ // Database (Password deliberately has no default for postgres)
+ viper.SetDefault("Database.Driver", "postgres")
viper.SetDefault("Database.Host", "localhost")
viper.SetDefault("Database.Port", 5432)
viper.SetDefault("Database.User", "postgres")
diff --git a/go.mod b/go.mod
index b7b834a12..23fa722e1 100644
--- a/go.mod
+++ b/go.mod
@@ -16,13 +16,18 @@ require (
require (
github.com/DATA-DOG/go-sqlmock v1.5.2 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/pelletier/go-toml/v2 v2.1.0 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/sagikazarmark/locafero v0.3.0 // indirect
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
@@ -31,9 +36,13 @@ require (
github.com/spf13/pflag v1.0.5 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
- golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa // indirect
+ golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect
golang.org/x/net v0.49.0 // indirect
golang.org/x/sys v0.41.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
+ modernc.org/libc v1.67.6 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.11.0 // indirect
+ modernc.org/sqlite v1.46.1 // indirect
)
diff --git a/go.sum b/go.sum
index 576eb28df..c58be6bb9 100644
--- a/go.sum
+++ b/go.sum
@@ -54,6 +54,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
@@ -109,6 +111,7 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
@@ -124,6 +127,8 @@ github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLe
github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g=
@@ -159,10 +164,14 @@ github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.6 h1:dNPt6NO46WmLVt2DLNpwczCmdV5boIZ6g/tlDrlRUbg=
github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
+github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
@@ -171,6 +180,8 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@@ -237,6 +248,8 @@ golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EH
golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa h1:FRnLl4eNAQl8hwxVVC17teOw8kdjVDVAiFMtgUdTSRQ=
golang.org/x/exp v0.0.0-20231110203233-9a3e6036ecaa/go.mod h1:zk2irFbV9DP96SEBUUAy67IdHUaZuSnrz1n472HUCLE=
+golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY=
+golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -348,6 +361,7 @@ golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
@@ -521,6 +535,14 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+modernc.org/libc v1.67.6 h1:eVOQvpModVLKOdT+LvBPjdQqfrZq+pC39BygcT+E7OI=
+modernc.org/libc v1.67.6/go.mod h1:JAhxUVlolfYDErnwiqaLvUqc8nfb2r6S6slAgZOnaiE=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
+modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
+modernc.org/sqlite v1.46.1 h1:eFJ2ShBLIEnUWlLy12raN0Z1plqmFX9Qe3rjQTKt6sU=
+modernc.org/sqlite v1.46.1/go.mod h1:CzbrU2lSB1DKUusvwGz7rqEKIq+NUd8GWuBBZDs9/nA=
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/main.go b/main.go
index edee6de24..8f764f6a3 100644
--- a/main.go
+++ b/main.go
@@ -25,6 +25,7 @@ import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"go.uber.org/zap"
+ _ "modernc.org/sqlite"
)
// Temporary DB auto clean on startup for quick development & testing.
@@ -108,7 +109,7 @@ func main() {
logger.Info(fmt.Sprintf("Starting Erupe (9.3b-%s)", Commit()))
logger.Info(fmt.Sprintf("Client Mode: %s (%d)", config.ClientMode, config.RealClientMode))
- if config.Database.Password == "" {
+ if config.Database.Driver != "sqlite" && config.Database.Password == "" {
preventClose(config, "Database password is blank")
}
@@ -136,19 +137,38 @@ func main() {
logger.Info("Discord: Disabled")
}
- // Create the postgres DB pool.
- connectString := fmt.Sprintf(
- "host='%s' port='%d' user='%s' password='%s' dbname='%s' sslmode=disable",
- config.Database.Host,
- config.Database.Port,
- config.Database.User,
- config.Database.Password,
- config.Database.Database,
- )
-
- db, err := sqlx.Open("postgres", connectString)
- if err != nil {
- preventClose(config, fmt.Sprintf("Database: Failed to open, %s", err.Error()))
+ // Create the DB pool.
+ var db *sqlx.DB
+ if config.Database.Driver == "sqlite" {
+ dbPath := config.Database.Database
+ if dbPath == "" || dbPath == "erupe" {
+ dbPath = "erupe.db"
+ }
+ db, err = sqlx.Open("sqlite", dbPath+"?_pragma=journal_mode(WAL)&_pragma=foreign_keys(1)")
+ if err != nil {
+ preventClose(config, fmt.Sprintf("Database: Failed to open SQLite %s, %s", dbPath, err.Error()))
+ }
+ // SQLite only supports one writer at a time.
+ db.SetMaxOpenConns(1)
+ logger.Info(fmt.Sprintf("Database: SQLite opened (%s)", dbPath))
+ } else {
+ connectString := fmt.Sprintf(
+ "host='%s' port='%d' user='%s' password='%s' dbname='%s' sslmode=disable",
+ config.Database.Host,
+ config.Database.Port,
+ config.Database.User,
+ config.Database.Password,
+ config.Database.Database,
+ )
+ db, err = sqlx.Open("postgres", connectString)
+ if err != nil {
+ preventClose(config, fmt.Sprintf("Database: Failed to open, %s", err.Error()))
+ }
+ // Configure connection pool to avoid exhausting PostgreSQL under load.
+ db.SetMaxOpenConns(50)
+ db.SetMaxIdleConns(10)
+ db.SetConnMaxLifetime(5 * time.Minute)
+ db.SetConnMaxIdleTime(2 * time.Minute)
}
// Test the DB connection.
@@ -157,12 +177,6 @@ func main() {
preventClose(config, fmt.Sprintf("Database: Failed to ping, %s", err.Error()))
}
- // Configure connection pool to avoid exhausting PostgreSQL under load.
- db.SetMaxOpenConns(50)
- db.SetMaxIdleConns(10)
- db.SetConnMaxLifetime(5 * time.Minute)
- db.SetConnMaxIdleTime(2 * time.Minute)
-
logger.Info("Database: Started successfully")
// Run database migrations
diff --git a/server/api/api_server.go b/server/api/api_server.go
index 6e106a088..13a3297cf 100644
--- a/server/api/api_server.go
+++ b/server/api/api_server.go
@@ -9,6 +9,8 @@ import (
"sync"
"time"
+ dbutil "erupe-ce/common/db"
+
"github.com/gorilla/handlers"
"github.com/gorilla/mux"
"github.com/jmoiron/sqlx"
@@ -33,6 +35,7 @@ type APIServer struct {
sessionRepo APISessionRepo
eventRepo APIEventRepo
httpServer *http.Server
+ startTime time.Time
isShuttingDown bool
}
@@ -45,19 +48,26 @@ func NewAPIServer(config *Config) *APIServer {
httpServer: &http.Server{},
}
if config.DB != nil {
- s.userRepo = NewAPIUserRepository(config.DB)
- s.charRepo = NewAPICharacterRepository(config.DB)
- s.sessionRepo = NewAPISessionRepository(config.DB)
- s.eventRepo = NewAPIEventRepository(config.DB)
+ wdb := dbutil.Wrap(config.DB)
+ s.userRepo = NewAPIUserRepository(wdb)
+ s.charRepo = NewAPICharacterRepository(wdb)
+ s.sessionRepo = NewAPISessionRepository(wdb)
+ s.eventRepo = NewAPIEventRepository(wdb)
}
return s
}
// Start starts the server in a new goroutine.
func (s *APIServer) Start() error {
+ s.startTime = time.Now()
+
// Set up the routes responsible for serving the launcher HTML, serverlist, unique name check, and JP auth.
r := mux.NewRouter()
+ // Dashboard routes (before catch-all)
+ r.HandleFunc("/dashboard", s.Dashboard)
+ r.HandleFunc("/api/dashboard/stats", s.DashboardStatsJSON).Methods("GET")
+
// Legacy routes (unchanged, no method enforcement)
r.HandleFunc("/launcher", s.Launcher)
r.HandleFunc("/login", s.Login)
diff --git a/server/api/dashboard.go b/server/api/dashboard.go
new file mode 100644
index 000000000..3b1b9ac66
--- /dev/null
+++ b/server/api/dashboard.go
@@ -0,0 +1,137 @@
+package api
+
+import (
+ _ "embed"
+ "encoding/json"
+ "fmt"
+ "html/template"
+ "net/http"
+ "time"
+
+ "go.uber.org/zap"
+)
+
+//go:embed dashboard.html
+var dashboardHTML string
+
+var dashboardTmpl = template.Must(template.New("dashboard").Parse(dashboardHTML))
+
+// DashboardStats is the JSON payload returned by GET /api/dashboard/stats.
+type DashboardStats struct {
+ Uptime string `json:"uptime"`
+ ServerVersion string `json:"serverVersion"`
+ ClientMode string `json:"clientMode"`
+ OnlinePlayers int `json:"onlinePlayers"`
+ TotalAccounts int `json:"totalAccounts"`
+ TotalCharacters int `json:"totalCharacters"`
+ Channels []ChannelInfo `json:"channels"`
+ DatabaseOK bool `json:"databaseOK"`
+}
+
+// ChannelInfo describes a single channel server entry from the servers table.
+type ChannelInfo struct {
+ Name string `json:"name"`
+ Port int `json:"port"`
+ Players int `json:"players"`
+}
+
+// Dashboard serves the embedded HTML dashboard page at /dashboard.
+func (s *APIServer) Dashboard(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ if err := dashboardTmpl.Execute(w, nil); err != nil {
+ s.logger.Error("Failed to render dashboard", zap.Error(err))
+ http.Error(w, "Internal Server Error", http.StatusInternalServerError)
+ }
+}
+
+// DashboardStatsJSON serves GET /api/dashboard/stats with live server statistics.
+func (s *APIServer) DashboardStatsJSON(w http.ResponseWriter, r *http.Request) {
+ stats := DashboardStats{
+ ServerVersion: "Erupe-CE",
+ ClientMode: s.erupeConfig.ClientMode,
+ }
+
+ // Compute uptime.
+ if !s.startTime.IsZero() {
+ stats.Uptime = formatDuration(time.Since(s.startTime))
+ } else {
+ stats.Uptime = "unknown"
+ }
+
+ // Check database connectivity.
+ if s.db != nil {
+ if err := s.db.Ping(); err != nil {
+ s.logger.Warn("Dashboard: database ping failed", zap.Error(err))
+ stats.DatabaseOK = false
+ } else {
+ stats.DatabaseOK = true
+ }
+ }
+
+ // Query total accounts.
+ if s.db != nil {
+ if err := s.db.QueryRow("SELECT COUNT(*) FROM users").Scan(&stats.TotalAccounts); err != nil {
+ s.logger.Warn("Dashboard: failed to count users", zap.Error(err))
+ }
+ }
+
+ // Query total characters.
+ if s.db != nil {
+ if err := s.db.QueryRow("SELECT COUNT(*) FROM characters").Scan(&stats.TotalCharacters); err != nil {
+ s.logger.Warn("Dashboard: failed to count characters", zap.Error(err))
+ }
+ }
+
+ // Query channel info from servers table.
+ if s.db != nil {
+ rows, err := s.db.Query("SELECT server_id, current_players, world_name, land FROM servers ORDER BY server_id")
+ if err != nil {
+ s.logger.Warn("Dashboard: failed to query servers", zap.Error(err))
+ } else {
+ defer func() { _ = rows.Close() }()
+ for rows.Next() {
+ var serverID, players, land int
+ var worldName *string
+ if err := rows.Scan(&serverID, &players, &worldName, &land); err != nil {
+ s.logger.Warn("Dashboard: failed to scan server row", zap.Error(err))
+ continue
+ }
+ name := "Channel"
+ if worldName != nil {
+ name = *worldName
+ }
+ ch := ChannelInfo{
+ Name: name,
+ Port: 54000 + serverID,
+ Players: players,
+ }
+ stats.Channels = append(stats.Channels, ch)
+ stats.OnlinePlayers += players
+ }
+ }
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ if err := json.NewEncoder(w).Encode(stats); err != nil {
+ s.logger.Error("Dashboard: failed to encode stats", zap.Error(err))
+ }
+}
+
+// formatDuration produces a human-readable duration string like "2d 5h 32m 10s".
+func formatDuration(d time.Duration) string {
+ days := int(d.Hours()) / 24
+ hours := int(d.Hours()) % 24
+ minutes := int(d.Minutes()) % 60
+ seconds := int(d.Seconds()) % 60
+
+ if days > 0 {
+ return fmt.Sprintf("%dd %dh %dm %ds", days, hours, minutes, seconds)
+ }
+ if hours > 0 {
+ return fmt.Sprintf("%dh %dm %ds", hours, minutes, seconds)
+ }
+ if minutes > 0 {
+ return fmt.Sprintf("%dm %ds", minutes, seconds)
+ }
+ return fmt.Sprintf("%ds", seconds)
+}
diff --git a/server/api/dashboard.html b/server/api/dashboard.html
new file mode 100644
index 000000000..f5553fe83
--- /dev/null
+++ b/server/api/dashboard.html
@@ -0,0 +1,145 @@
+
+
+
+
+
+Erupe Dashboard
+
+
+
+
+
+Failed to fetch server stats
+
+
+
+
+
+
+
Total Characters
+
--
+
+
+
+
+
+
+
+
+
+
+
diff --git a/server/api/repo_character.go b/server/api/repo_character.go
index 6bddc5815..7d3ffa3f6 100644
--- a/server/api/repo_character.go
+++ b/server/api/repo_character.go
@@ -3,16 +3,16 @@ package api
import (
"context"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// APICharacterRepository implements APICharacterRepo with PostgreSQL.
type APICharacterRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewAPICharacterRepository creates a new APICharacterRepository.
-func NewAPICharacterRepository(db *sqlx.DB) *APICharacterRepository {
+func NewAPICharacterRepository(db *dbutil.DB) *APICharacterRepository {
return &APICharacterRepository{db: db}
}
diff --git a/server/api/repo_event.go b/server/api/repo_event.go
index c6a2cef2c..40694fb66 100644
--- a/server/api/repo_event.go
+++ b/server/api/repo_event.go
@@ -6,15 +6,15 @@ import (
"errors"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
type apiEventRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewAPIEventRepository creates an APIEventRepo backed by PostgreSQL.
-func NewAPIEventRepository(db *sqlx.DB) APIEventRepo {
+func NewAPIEventRepository(db *dbutil.DB) APIEventRepo {
return &apiEventRepository{db: db}
}
diff --git a/server/api/repo_session.go b/server/api/repo_session.go
index 80a842d00..dffc3fa25 100644
--- a/server/api/repo_session.go
+++ b/server/api/repo_session.go
@@ -3,16 +3,16 @@ package api
import (
"context"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// APISessionRepository implements APISessionRepo with PostgreSQL.
type APISessionRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewAPISessionRepository creates a new APISessionRepository.
-func NewAPISessionRepository(db *sqlx.DB) *APISessionRepository {
+func NewAPISessionRepository(db *dbutil.DB) *APISessionRepository {
return &APISessionRepository{db: db}
}
diff --git a/server/api/repo_user.go b/server/api/repo_user.go
index dfb25664f..37af109b1 100644
--- a/server/api/repo_user.go
+++ b/server/api/repo_user.go
@@ -4,16 +4,16 @@ import (
"context"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// APIUserRepository implements APIUserRepo with PostgreSQL.
type APIUserRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewAPIUserRepository creates a new APIUserRepository.
-func NewAPIUserRepository(db *sqlx.DB) *APIUserRepository {
+func NewAPIUserRepository(db *dbutil.DB) *APIUserRepository {
return &APIUserRepository{db: db}
}
diff --git a/server/channelserver/handlers_cafe.go b/server/channelserver/handlers_cafe.go
index 2dfe0d6e3..2f2ff4fb8 100644
--- a/server/channelserver/handlers_cafe.go
+++ b/server/channelserver/handlers_cafe.go
@@ -170,8 +170,8 @@ func handleMsgMhfPostCafeDurationBonusReceived(s *Session, p mhfpacket.MHFPacket
if err == nil {
if itemType == 17 {
if err := addPointNetcafe(s, int(quantity)); err != nil {
- s.logger.Error("Failed to add cafe bonus netcafe points", zap.Error(err))
- }
+ s.logger.Error("Failed to add cafe bonus netcafe points", zap.Error(err))
+ }
}
}
if err := s.server.cafeRepo.AcceptBonus(cbID, s.charID); err != nil {
diff --git a/server/channelserver/handlers_cafe_test.go b/server/channelserver/handlers_cafe_test.go
index 0e99cb765..f46dcc769 100644
--- a/server/channelserver/handlers_cafe_test.go
+++ b/server/channelserver/handlers_cafe_test.go
@@ -1,10 +1,10 @@
package channelserver
import (
- cfg "erupe-ce/config"
- "erupe-ce/common/mhfcourse"
- "erupe-ce/network/mhfpacket"
"errors"
+ "erupe-ce/common/mhfcourse"
+ cfg "erupe-ce/config"
+ "erupe-ce/network/mhfpacket"
"testing"
"time"
)
diff --git a/server/channelserver/handlers_distitem.go b/server/channelserver/handlers_distitem.go
index 1ee5eb7b8..7f4d53260 100644
--- a/server/channelserver/handlers_distitem.go
+++ b/server/channelserver/handlers_distitem.go
@@ -140,8 +140,8 @@ func handleMsgMhfAcquireDistItem(s *Session, p mhfpacket.MHFPacket) {
switch item.ItemType {
case 17:
if err := addPointNetcafe(s, int(item.Quantity)); err != nil {
- s.logger.Error("Failed to add dist item netcafe points", zap.Error(err))
- }
+ s.logger.Error("Failed to add dist item netcafe points", zap.Error(err))
+ }
case 19:
if err := s.server.userRepo.AddPremiumCoins(s.userID, item.Quantity); err != nil {
s.logger.Error("Failed to update gacha premium", zap.Error(err))
diff --git a/server/channelserver/handlers_diva_test.go b/server/channelserver/handlers_diva_test.go
index 9d53d44da..64f6501f5 100644
--- a/server/channelserver/handlers_diva_test.go
+++ b/server/channelserver/handlers_diva_test.go
@@ -3,9 +3,9 @@ package channelserver
import (
"testing"
+ cfg "erupe-ce/config"
"erupe-ce/network/mhfpacket"
"time"
- cfg "erupe-ce/config"
)
func TestHandleMsgMhfGetUdInfo(t *testing.T) {
diff --git a/server/channelserver/handlers_guild_ops_test.go b/server/channelserver/handlers_guild_ops_test.go
index d61284082..3769a0733 100644
--- a/server/channelserver/handlers_guild_ops_test.go
+++ b/server/channelserver/handlers_guild_ops_test.go
@@ -4,8 +4,8 @@ import (
"testing"
"erupe-ce/common/byteframe"
- "erupe-ce/network/mhfpacket"
"erupe-ce/common/stringsupport"
+ "erupe-ce/network/mhfpacket"
)
// --- handleMsgMhfOperateGuild tests ---
diff --git a/server/channelserver/handlers_guild_scout_test.go b/server/channelserver/handlers_guild_scout_test.go
index 82e981619..f41973a05 100644
--- a/server/channelserver/handlers_guild_scout_test.go
+++ b/server/channelserver/handlers_guild_scout_test.go
@@ -297,7 +297,7 @@ func TestPostGuildScout_Success(t *testing.T) {
func TestPostGuildScout_AlreadyInvited(t *testing.T) {
server := createMockServer()
guildMock := &mockGuildRepo{
- membership: &GuildMember{GuildID: 10, Recruiter: true},
+ membership: &GuildMember{GuildID: 10, Recruiter: true},
createAppErr: ErrAlreadyInvited,
}
guildMock.guild = &Guild{ID: 10, Name: "TestGuild"}
diff --git a/server/channelserver/handlers_mercenary_test.go b/server/channelserver/handlers_mercenary_test.go
index 74d963379..f0e3a3a38 100644
--- a/server/channelserver/handlers_mercenary_test.go
+++ b/server/channelserver/handlers_mercenary_test.go
@@ -6,8 +6,8 @@ import (
"testing"
"erupe-ce/common/byteframe"
- "erupe-ce/network/mhfpacket"
cfg "erupe-ce/config"
+ "erupe-ce/network/mhfpacket"
)
func TestHandleMsgMhfLoadLegendDispatch(t *testing.T) {
diff --git a/server/channelserver/handlers_shop.go b/server/channelserver/handlers_shop.go
index 081528256..a1747de5e 100644
--- a/server/channelserver/handlers_shop.go
+++ b/server/channelserver/handlers_shop.go
@@ -156,13 +156,13 @@ func handleMsgMhfEnumerateShop(s *Session, p mhfpacket.MHFPacket) {
// |5|1|1|7|9|3|0|0|0|0|0|null|
// |8|1|100|7|1|4|1000|0|0|0|0|null|
// |9|1|100|7|2|5|9000|0|0|0|0|null|
- bf.WriteUint8(ge.EntryType)
- bf.WriteUint32(ge.ID)
- bf.WriteUint8(ge.ItemType)
- bf.WriteUint32(ge.ItemNumber)
- bf.WriteUint16(ge.ItemQuantity)
+ bf.WriteUint8(ge.EntryType)
+ bf.WriteUint32(ge.ID)
+ bf.WriteUint8(ge.ItemType)
+ bf.WriteUint32(ge.ItemNumber)
+ bf.WriteUint16(ge.ItemQuantity)
var weightPr uint16
- if gachaType >= 4 { // If box
+ if gachaType >= 4 { // If box
weightPr = 1
} else {
weightPr = uint16(ge.Weight / divisor)
diff --git a/server/channelserver/repo_achievement.go b/server/channelserver/repo_achievement.go
index 26e12dd79..578fdbcee 100644
--- a/server/channelserver/repo_achievement.go
+++ b/server/channelserver/repo_achievement.go
@@ -3,16 +3,16 @@ package channelserver
import (
"fmt"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// AchievementRepository centralizes all database access for the achievements table.
type AchievementRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewAchievementRepository creates a new AchievementRepository.
-func NewAchievementRepository(db *sqlx.DB) *AchievementRepository {
+func NewAchievementRepository(db *dbutil.DB) *AchievementRepository {
return &AchievementRepository{db: db}
}
diff --git a/server/channelserver/repo_achievement_test.go b/server/channelserver/repo_achievement_test.go
index ae9a08cc8..371ba3d94 100644
--- a/server/channelserver/repo_achievement_test.go
+++ b/server/channelserver/repo_achievement_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -11,7 +12,7 @@ func setupAchievementRepo(t *testing.T) (*AchievementRepository, *sqlx.DB, uint3
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "ach_test_user")
charID := CreateTestCharacter(t, db, userID, "AchChar")
- repo := NewAchievementRepository(db)
+ repo := NewAchievementRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_cafe.go b/server/channelserver/repo_cafe.go
index 064ef4ffe..7781f7192 100644
--- a/server/channelserver/repo_cafe.go
+++ b/server/channelserver/repo_cafe.go
@@ -1,16 +1,16 @@
package channelserver
import (
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// CafeRepository centralizes all database access for cafe-related tables.
type CafeRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewCafeRepository creates a new CafeRepository.
-func NewCafeRepository(db *sqlx.DB) *CafeRepository {
+func NewCafeRepository(db *dbutil.DB) *CafeRepository {
return &CafeRepository{db: db}
}
diff --git a/server/channelserver/repo_cafe_test.go b/server/channelserver/repo_cafe_test.go
index e1c43e98a..5601ece73 100644
--- a/server/channelserver/repo_cafe_test.go
+++ b/server/channelserver/repo_cafe_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -11,7 +12,7 @@ func setupCafeRepo(t *testing.T) (*CafeRepository, *sqlx.DB, uint32) {
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "cafe_test_user")
charID := CreateTestCharacter(t, db, userID, "CafeChar")
- repo := NewCafeRepository(db)
+ repo := NewCafeRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_character.go b/server/channelserver/repo_character.go
index 89028c6c8..6a72baf9a 100644
--- a/server/channelserver/repo_character.go
+++ b/server/channelserver/repo_character.go
@@ -4,16 +4,16 @@ import (
"database/sql"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// CharacterRepository centralizes all database access for the characters table.
type CharacterRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewCharacterRepository creates a new CharacterRepository.
-func NewCharacterRepository(db *sqlx.DB) *CharacterRepository {
+func NewCharacterRepository(db *dbutil.DB) *CharacterRepository {
return &CharacterRepository{db: db}
}
diff --git a/server/channelserver/repo_character_test.go b/server/channelserver/repo_character_test.go
index 52ebdd780..945bb4967 100644
--- a/server/channelserver/repo_character_test.go
+++ b/server/channelserver/repo_character_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"time"
@@ -12,7 +13,7 @@ func setupCharRepo(t *testing.T) (*CharacterRepository, *sqlx.DB, uint32) {
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "repo_test_user")
charID := CreateTestCharacter(t, db, userID, "RepoChar")
- repo := NewCharacterRepository(db)
+ repo := NewCharacterRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_distribution.go b/server/channelserver/repo_distribution.go
index 7ef8a42f6..d4193956d 100644
--- a/server/channelserver/repo_distribution.go
+++ b/server/channelserver/repo_distribution.go
@@ -1,17 +1,17 @@
package channelserver
import (
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// DistributionRepository centralizes all database access for the distribution,
// distribution_items, and distributions_accepted tables.
type DistributionRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewDistributionRepository creates a new DistributionRepository.
-func NewDistributionRepository(db *sqlx.DB) *DistributionRepository {
+func NewDistributionRepository(db *dbutil.DB) *DistributionRepository {
return &DistributionRepository{db: db}
}
diff --git a/server/channelserver/repo_distribution_test.go b/server/channelserver/repo_distribution_test.go
index fc2286142..fae6f556b 100644
--- a/server/channelserver/repo_distribution_test.go
+++ b/server/channelserver/repo_distribution_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -11,7 +12,7 @@ func setupDistributionRepo(t *testing.T) (*DistributionRepository, *sqlx.DB, uin
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "dist_test_user")
charID := CreateTestCharacter(t, db, userID, "DistChar")
- repo := NewDistributionRepository(db)
+ repo := NewDistributionRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_diva.go b/server/channelserver/repo_diva.go
index 90b53e201..4cd16ee49 100644
--- a/server/channelserver/repo_diva.go
+++ b/server/channelserver/repo_diva.go
@@ -1,16 +1,16 @@
package channelserver
import (
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// DivaRepository centralizes all database access for diva defense events.
type DivaRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewDivaRepository creates a new DivaRepository.
-func NewDivaRepository(db *sqlx.DB) *DivaRepository {
+func NewDivaRepository(db *dbutil.DB) *DivaRepository {
return &DivaRepository{db: db}
}
diff --git a/server/channelserver/repo_diva_test.go b/server/channelserver/repo_diva_test.go
index bd6ab0d60..b7b623657 100644
--- a/server/channelserver/repo_diva_test.go
+++ b/server/channelserver/repo_diva_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -9,7 +10,7 @@ import (
func setupDivaRepo(t *testing.T) (*DivaRepository, *sqlx.DB) {
t.Helper()
db := SetupTestDB(t)
- repo := NewDivaRepository(db)
+ repo := NewDivaRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db
}
diff --git a/server/channelserver/repo_event.go b/server/channelserver/repo_event.go
index eaae596e7..649a4ea5e 100644
--- a/server/channelserver/repo_event.go
+++ b/server/channelserver/repo_event.go
@@ -4,7 +4,7 @@ import (
"context"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// EventQuest represents a row from the event_quests table.
@@ -22,11 +22,11 @@ type EventQuest struct {
// EventRepository centralizes all database access for event-related tables.
type EventRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewEventRepository creates a new EventRepository.
-func NewEventRepository(db *sqlx.DB) *EventRepository {
+func NewEventRepository(db *dbutil.DB) *EventRepository {
return &EventRepository{db: db}
}
diff --git a/server/channelserver/repo_event_test.go b/server/channelserver/repo_event_test.go
index 36ad33b56..5ad3d6f06 100644
--- a/server/channelserver/repo_event_test.go
+++ b/server/channelserver/repo_event_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"time"
@@ -10,7 +11,7 @@ import (
func setupEventRepo(t *testing.T) (*EventRepository, *sqlx.DB) {
t.Helper()
db := SetupTestDB(t)
- repo := NewEventRepository(db)
+ repo := NewEventRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db
}
diff --git a/server/channelserver/repo_festa.go b/server/channelserver/repo_festa.go
index dbac352dd..bc694658d 100644
--- a/server/channelserver/repo_festa.go
+++ b/server/channelserver/repo_festa.go
@@ -4,17 +4,17 @@ import (
"context"
"database/sql"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// FestaRepository centralizes all database access for festa-related tables
// (events, festa_registrations, festa_submissions, festa_prizes, festa_prizes_accepted, festa_trials, guild_characters).
type FestaRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewFestaRepository creates a new FestaRepository.
-func NewFestaRepository(db *sqlx.DB) *FestaRepository {
+func NewFestaRepository(db *dbutil.DB) *FestaRepository {
return &FestaRepository{db: db}
}
diff --git a/server/channelserver/repo_festa_test.go b/server/channelserver/repo_festa_test.go
index 0ef98bb77..f94f137f2 100644
--- a/server/channelserver/repo_festa_test.go
+++ b/server/channelserver/repo_festa_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"time"
@@ -13,7 +14,7 @@ func setupFestaRepo(t *testing.T) (*FestaRepository, *sqlx.DB, uint32, uint32) {
userID := CreateTestUser(t, db, "festa_test_user")
charID := CreateTestCharacter(t, db, userID, "FestaChar")
guildID := CreateTestGuild(t, db, charID, "FestaGuild")
- repo := NewFestaRepository(db)
+ repo := NewFestaRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID, guildID
}
diff --git a/server/channelserver/repo_gacha.go b/server/channelserver/repo_gacha.go
index b2efcf303..8a6011660 100644
--- a/server/channelserver/repo_gacha.go
+++ b/server/channelserver/repo_gacha.go
@@ -5,17 +5,17 @@ import (
"errors"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// GachaRepository centralizes all database access for gacha-related tables
// (gacha_shop, gacha_entries, gacha_items, gacha_stepup, gacha_box).
type GachaRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewGachaRepository creates a new GachaRepository.
-func NewGachaRepository(db *sqlx.DB) *GachaRepository {
+func NewGachaRepository(db *dbutil.DB) *GachaRepository {
return &GachaRepository{db: db}
}
diff --git a/server/channelserver/repo_gacha_test.go b/server/channelserver/repo_gacha_test.go
index 64f8c4a71..6b4d63683 100644
--- a/server/channelserver/repo_gacha_test.go
+++ b/server/channelserver/repo_gacha_test.go
@@ -3,6 +3,7 @@ package channelserver
import (
"database/sql"
"errors"
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -13,7 +14,7 @@ func setupGachaRepo(t *testing.T) (*GachaRepository, *sqlx.DB, uint32) {
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "gacha_test_user")
charID := CreateTestCharacter(t, db, userID, "GachaChar")
- repo := NewGachaRepository(db)
+ repo := NewGachaRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_goocoo.go b/server/channelserver/repo_goocoo.go
index dc9c072da..f84608677 100644
--- a/server/channelserver/repo_goocoo.go
+++ b/server/channelserver/repo_goocoo.go
@@ -3,16 +3,16 @@ package channelserver
import (
"fmt"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// GoocooRepository centralizes all database access for the goocoo table.
type GoocooRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewGoocooRepository creates a new GoocooRepository.
-func NewGoocooRepository(db *sqlx.DB) *GoocooRepository {
+func NewGoocooRepository(db *dbutil.DB) *GoocooRepository {
return &GoocooRepository{db: db}
}
diff --git a/server/channelserver/repo_goocoo_test.go b/server/channelserver/repo_goocoo_test.go
index 0b390402d..a1d8981c7 100644
--- a/server/channelserver/repo_goocoo_test.go
+++ b/server/channelserver/repo_goocoo_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -11,7 +12,7 @@ func setupGoocooRepo(t *testing.T) (*GoocooRepository, *sqlx.DB, uint32) {
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "goocoo_test_user")
charID := CreateTestCharacter(t, db, userID, "GoocooChar")
- repo := NewGoocooRepository(db)
+ repo := NewGoocooRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_guild.go b/server/channelserver/repo_guild.go
index f9eca11e5..a005437d0 100644
--- a/server/channelserver/repo_guild.go
+++ b/server/channelserver/repo_guild.go
@@ -6,17 +6,19 @@ import (
"errors"
"fmt"
+ dbutil "erupe-ce/common/db"
+
"github.com/jmoiron/sqlx"
)
// GuildRepository centralizes all database access for guild-related tables
// (guilds, guild_characters, guild_applications).
type GuildRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewGuildRepository creates a new GuildRepository.
-func NewGuildRepository(db *sqlx.DB) *GuildRepository {
+func NewGuildRepository(db *dbutil.DB) *GuildRepository {
return &GuildRepository{db: db}
}
diff --git a/server/channelserver/repo_guild_test.go b/server/channelserver/repo_guild_test.go
index fc2ff6c3c..06aae1354 100644
--- a/server/channelserver/repo_guild_test.go
+++ b/server/channelserver/repo_guild_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"fmt"
"testing"
"time"
@@ -13,7 +14,7 @@ func setupGuildRepo(t *testing.T) (*GuildRepository, *sqlx.DB, uint32, uint32) {
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "guild_test_user")
charID := CreateTestCharacter(t, db, userID, "GuildLeader")
- repo := NewGuildRepository(db)
+ repo := NewGuildRepository(dbutil.Wrap(db))
guildID := CreateTestGuild(t, db, charID, "TestGuild")
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, guildID, charID
@@ -82,7 +83,7 @@ func TestGetByCharIDNotFound(t *testing.T) {
func TestCreate(t *testing.T) {
db := SetupTestDB(t)
defer TeardownTestDB(t, db)
- repo := NewGuildRepository(db)
+ repo := NewGuildRepository(dbutil.Wrap(db))
userID := CreateTestUser(t, db, "create_guild_user")
charID := CreateTestCharacter(t, db, userID, "CreateLeader")
diff --git a/server/channelserver/repo_house.go b/server/channelserver/repo_house.go
index f976a71f5..61185d3f7 100644
--- a/server/channelserver/repo_house.go
+++ b/server/channelserver/repo_house.go
@@ -3,17 +3,17 @@ package channelserver
import (
"fmt"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// HouseRepository centralizes all database access for house-related tables
// (user_binary house columns, warehouse, titles).
type HouseRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewHouseRepository creates a new HouseRepository.
-func NewHouseRepository(db *sqlx.DB) *HouseRepository {
+func NewHouseRepository(db *dbutil.DB) *HouseRepository {
return &HouseRepository{db: db}
}
diff --git a/server/channelserver/repo_house_test.go b/server/channelserver/repo_house_test.go
index 2bd841bd7..21ec0a670 100644
--- a/server/channelserver/repo_house_test.go
+++ b/server/channelserver/repo_house_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -12,7 +13,7 @@ func setupHouseRepo(t *testing.T) (*HouseRepository, *sqlx.DB, uint32) {
userID := CreateTestUser(t, db, "house_test_user")
charID := CreateTestCharacter(t, db, userID, "HouseChar")
CreateTestUserBinary(t, db, charID)
- repo := NewHouseRepository(db)
+ repo := NewHouseRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_mail.go b/server/channelserver/repo_mail.go
index b9023f6da..dbd3a06ae 100644
--- a/server/channelserver/repo_mail.go
+++ b/server/channelserver/repo_mail.go
@@ -1,16 +1,16 @@
package channelserver
import (
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// MailRepository centralizes all database access for the mail table.
type MailRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewMailRepository creates a new MailRepository.
-func NewMailRepository(db *sqlx.DB) *MailRepository {
+func NewMailRepository(db *dbutil.DB) *MailRepository {
return &MailRepository{db: db}
}
diff --git a/server/channelserver/repo_mail_test.go b/server/channelserver/repo_mail_test.go
index 101b93ef7..125f86cfe 100644
--- a/server/channelserver/repo_mail_test.go
+++ b/server/channelserver/repo_mail_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -13,7 +14,7 @@ func setupMailRepo(t *testing.T) (*MailRepository, *sqlx.DB, uint32, uint32) {
senderID := CreateTestCharacter(t, db, userID, "Sender")
userID2 := CreateTestUser(t, db, "mail_recipient")
recipientID := CreateTestCharacter(t, db, userID2, "Recipient")
- repo := NewMailRepository(db)
+ repo := NewMailRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, senderID, recipientID
}
diff --git a/server/channelserver/repo_mercenary.go b/server/channelserver/repo_mercenary.go
index e844ae24a..da600a34a 100644
--- a/server/channelserver/repo_mercenary.go
+++ b/server/channelserver/repo_mercenary.go
@@ -4,16 +4,16 @@ import (
"fmt"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// MercenaryRepository centralizes database access for mercenary/rasta/airou sequences and queries.
type MercenaryRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewMercenaryRepository creates a new MercenaryRepository.
-func NewMercenaryRepository(db *sqlx.DB) *MercenaryRepository {
+func NewMercenaryRepository(db *dbutil.DB) *MercenaryRepository {
return &MercenaryRepository{db: db}
}
diff --git a/server/channelserver/repo_mercenary_test.go b/server/channelserver/repo_mercenary_test.go
index 660b5995b..80a5263c7 100644
--- a/server/channelserver/repo_mercenary_test.go
+++ b/server/channelserver/repo_mercenary_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -12,7 +13,7 @@ func setupMercenaryRepo(t *testing.T) (*MercenaryRepository, *sqlx.DB, uint32, u
userID := CreateTestUser(t, db, "merc_test_user")
charID := CreateTestCharacter(t, db, userID, "MercChar")
guildID := CreateTestGuild(t, db, charID, "MercGuild")
- repo := NewMercenaryRepository(db)
+ repo := NewMercenaryRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID, guildID
}
diff --git a/server/channelserver/repo_misc.go b/server/channelserver/repo_misc.go
index f99eaa536..f650fd4bf 100644
--- a/server/channelserver/repo_misc.go
+++ b/server/channelserver/repo_misc.go
@@ -3,16 +3,16 @@ package channelserver
import (
"fmt"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// MiscRepository centralizes database access for miscellaneous game tables.
type MiscRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewMiscRepository creates a new MiscRepository.
-func NewMiscRepository(db *sqlx.DB) *MiscRepository {
+func NewMiscRepository(db *dbutil.DB) *MiscRepository {
return &MiscRepository{db: db}
}
diff --git a/server/channelserver/repo_misc_test.go b/server/channelserver/repo_misc_test.go
index 7a16def39..ef3b0fbaa 100644
--- a/server/channelserver/repo_misc_test.go
+++ b/server/channelserver/repo_misc_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -9,7 +10,7 @@ import (
func setupMiscRepo(t *testing.T) (*MiscRepository, *sqlx.DB) {
t.Helper()
db := SetupTestDB(t)
- repo := NewMiscRepository(db)
+ repo := NewMiscRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db
}
diff --git a/server/channelserver/repo_mocks_test.go b/server/channelserver/repo_mocks_test.go
index fdd262e4a..b30707cd2 100644
--- a/server/channelserver/repo_mocks_test.go
+++ b/server/channelserver/repo_mocks_test.go
@@ -304,15 +304,15 @@ type mockGuildRepo struct {
deletedPostID uint32
// Alliance
- alliance *GuildAlliance
- getAllianceErr error
- createAllianceErr error
- deleteAllianceErr error
- removeAllyErr error
- setAllianceRecruitErr error
- deletedAllianceID uint32
- removedAllyArgs []uint32
- allianceRecruitingSet *bool
+ alliance *GuildAlliance
+ getAllianceErr error
+ createAllianceErr error
+ deleteAllianceErr error
+ removeAllyErr error
+ setAllianceRecruitErr error
+ deletedAllianceID uint32
+ removedAllyArgs []uint32
+ allianceRecruitingSet *bool
// Cooking
meals []*GuildMeal
@@ -552,28 +552,28 @@ func (m *mockGuildRepo) Create(_ uint32, _ string) (int32, error) { return 0, ni
func (m *mockGuildRepo) CreateApplicationWithMail(_, _, _ uint32, _ GuildApplicationType, _, _ uint32, _, _ string) error {
return nil
}
-func (m *mockGuildRepo) CancelInvitation(_, _ uint32) error { return nil }
-func (m *mockGuildRepo) ArrangeCharacters(_ []uint32) error { return nil }
-func (m *mockGuildRepo) GetItemBox(_ uint32) ([]byte, error) { return nil, nil }
-func (m *mockGuildRepo) SaveItemBox(_ uint32, _ []byte) error { return nil }
-func (m *mockGuildRepo) SetRecruiting(_ uint32, _ bool) error { return nil }
-func (m *mockGuildRepo) SetPugiOutfits(_ uint32, _ uint32) error { return nil }
-func (m *mockGuildRepo) SetRecruiter(_ uint32, _ bool) error { return nil }
-func (m *mockGuildRepo) AddMemberDailyRP(_ uint32, _ uint16) error { return nil }
-func (m *mockGuildRepo) ExchangeEventRP(_ uint32, _ uint16) (uint32, error) { return 0, nil }
-func (m *mockGuildRepo) AddRankRP(_ uint32, _ uint16) error { return nil }
-func (m *mockGuildRepo) AddEventRP(_ uint32, _ uint16) error { return nil }
-func (m *mockGuildRepo) GetRoomRP(_ uint32) (uint16, error) { return 0, nil }
-func (m *mockGuildRepo) SetRoomRP(_ uint32, _ uint16) error { return nil }
-func (m *mockGuildRepo) AddRoomRP(_ uint32, _ uint16) error { return nil }
-func (m *mockGuildRepo) SetRoomExpiry(_ uint32, _ time.Time) error { return nil }
-func (m *mockGuildRepo) UpdatePost(_ uint32, _, _ string) error { return nil }
-func (m *mockGuildRepo) UpdatePostStamp(_, _ uint32) error { return nil }
-func (m *mockGuildRepo) GetPostLikedBy(_ uint32) (string, error) { return "", nil }
-func (m *mockGuildRepo) SetPostLikedBy(_ uint32, _ string) error { return nil }
-func (m *mockGuildRepo) CountNewPosts(_ uint32, _ time.Time) (int, error) { return 0, nil }
-func (m *mockGuildRepo) ListAlliances() ([]*GuildAlliance, error) { return nil, nil }
-func (m *mockGuildRepo) ClearTreasureHunt(_ uint32) error { return nil }
+func (m *mockGuildRepo) CancelInvitation(_, _ uint32) error { return nil }
+func (m *mockGuildRepo) ArrangeCharacters(_ []uint32) error { return nil }
+func (m *mockGuildRepo) GetItemBox(_ uint32) ([]byte, error) { return nil, nil }
+func (m *mockGuildRepo) SaveItemBox(_ uint32, _ []byte) error { return nil }
+func (m *mockGuildRepo) SetRecruiting(_ uint32, _ bool) error { return nil }
+func (m *mockGuildRepo) SetPugiOutfits(_ uint32, _ uint32) error { return nil }
+func (m *mockGuildRepo) SetRecruiter(_ uint32, _ bool) error { return nil }
+func (m *mockGuildRepo) AddMemberDailyRP(_ uint32, _ uint16) error { return nil }
+func (m *mockGuildRepo) ExchangeEventRP(_ uint32, _ uint16) (uint32, error) { return 0, nil }
+func (m *mockGuildRepo) AddRankRP(_ uint32, _ uint16) error { return nil }
+func (m *mockGuildRepo) AddEventRP(_ uint32, _ uint16) error { return nil }
+func (m *mockGuildRepo) GetRoomRP(_ uint32) (uint16, error) { return 0, nil }
+func (m *mockGuildRepo) SetRoomRP(_ uint32, _ uint16) error { return nil }
+func (m *mockGuildRepo) AddRoomRP(_ uint32, _ uint16) error { return nil }
+func (m *mockGuildRepo) SetRoomExpiry(_ uint32, _ time.Time) error { return nil }
+func (m *mockGuildRepo) UpdatePost(_ uint32, _, _ string) error { return nil }
+func (m *mockGuildRepo) UpdatePostStamp(_, _ uint32) error { return nil }
+func (m *mockGuildRepo) GetPostLikedBy(_ uint32) (string, error) { return "", nil }
+func (m *mockGuildRepo) SetPostLikedBy(_ uint32, _ string) error { return nil }
+func (m *mockGuildRepo) CountNewPosts(_ uint32, _ time.Time) (int, error) { return 0, nil }
+func (m *mockGuildRepo) ListAlliances() ([]*GuildAlliance, error) { return nil, nil }
+func (m *mockGuildRepo) ClearTreasureHunt(_ uint32) error { return nil }
func (m *mockGuildRepo) InsertKillLog(_ uint32, _ int, _ uint8, _ time.Time) error { return nil }
func (m *mockGuildRepo) ListInvitedCharacters(_ uint32) ([]*ScoutedCharacter, error) {
return nil, nil
@@ -816,7 +816,6 @@ type mockGachaRepo struct {
allEntries []GachaEntry
allEntriesErr error
weightDivisor float64
-
}
func (m *mockGachaRepo) GetEntryForTransaction(_ uint32, _ uint8) (uint8, uint16, int, error) {
@@ -957,26 +956,26 @@ type mockTowerRepo struct {
gemsErr error
updatedGems string
- progress TenrouiraiProgressData
- progressErr error
- scores []TenrouiraiCharScore
- scoresErr error
- guildRP uint32
- guildRPErr error
- page int
- donated int
- pageRPErr error
- advanceErr error
- advanceCalled bool
- donateErr error
- donatedRP uint16
+ progress TenrouiraiProgressData
+ progressErr error
+ scores []TenrouiraiCharScore
+ scoresErr error
+ guildRP uint32
+ guildRPErr error
+ page int
+ donated int
+ pageRPErr error
+ advanceErr error
+ advanceCalled bool
+ donateErr error
+ donatedRP uint16
}
-func (m *mockTowerRepo) GetTowerData(_ uint32) (TowerData, error) { return m.towerData, m.towerDataErr }
-func (m *mockTowerRepo) GetSkills(_ uint32) (string, error) { return m.skills, m.skillsErr }
-func (m *mockTowerRepo) UpdateSkills(_ uint32, _ string, _ int32) error { return nil }
+func (m *mockTowerRepo) GetTowerData(_ uint32) (TowerData, error) { return m.towerData, m.towerDataErr }
+func (m *mockTowerRepo) GetSkills(_ uint32) (string, error) { return m.skills, m.skillsErr }
+func (m *mockTowerRepo) UpdateSkills(_ uint32, _ string, _ int32) error { return nil }
func (m *mockTowerRepo) UpdateProgress(_ uint32, _, _, _, _ int32) error { return nil }
-func (m *mockTowerRepo) GetGems(_ uint32) (string, error) { return m.gems, m.gemsErr }
+func (m *mockTowerRepo) GetGems(_ uint32) (string, error) { return m.gems, m.gemsErr }
func (m *mockTowerRepo) UpdateGems(_ uint32, gems string) error {
m.updatedGems = gems
return nil
@@ -1003,21 +1002,21 @@ func (m *mockTowerRepo) DonateGuildTowerRP(_ uint32, rp uint16) error {
// --- mockFestaRepo ---
type mockFestaRepo struct {
- events []FestaEvent
- eventsErr error
- teamSouls uint32
- teamErr error
- trials []FestaTrial
- trialsErr error
- topGuild FestaGuildRanking
- topErr error
- topWindow FestaGuildRanking
- topWinErr error
- charSouls uint32
- charErr error
- hasClaimed bool
- prizes []Prize
- prizesErr error
+ events []FestaEvent
+ eventsErr error
+ teamSouls uint32
+ teamErr error
+ trials []FestaTrial
+ trialsErr error
+ topGuild FestaGuildRanking
+ topErr error
+ topWindow FestaGuildRanking
+ topWinErr error
+ charSouls uint32
+ charErr error
+ hasClaimed bool
+ prizes []Prize
+ prizesErr error
cleanupErr error
cleanupCalled bool
@@ -1035,8 +1034,8 @@ func (m *mockFestaRepo) InsertEvent(start uint32) error {
m.insertedStart = start
return m.insertErr
}
-func (m *mockFestaRepo) GetFestaEvents() ([]FestaEvent, error) { return m.events, m.eventsErr }
-func (m *mockFestaRepo) GetTeamSouls(_ string) (uint32, error) { return m.teamSouls, m.teamErr }
+func (m *mockFestaRepo) GetFestaEvents() ([]FestaEvent, error) { return m.events, m.eventsErr }
+func (m *mockFestaRepo) GetTeamSouls(_ string) (uint32, error) { return m.teamSouls, m.teamErr }
func (m *mockFestaRepo) GetTrialsWithMonopoly() ([]FestaTrial, error) {
return m.trials, m.trialsErr
}
@@ -1046,15 +1045,15 @@ func (m *mockFestaRepo) GetTopGuildForTrial(_ uint16) (FestaGuildRanking, error)
func (m *mockFestaRepo) GetTopGuildInWindow(_, _ uint32) (FestaGuildRanking, error) {
return m.topWindow, m.topWinErr
}
-func (m *mockFestaRepo) GetCharSouls(_ uint32) (uint32, error) { return m.charSouls, m.charErr }
-func (m *mockFestaRepo) HasClaimedMainPrize(_ uint32) bool { return m.hasClaimed }
-func (m *mockFestaRepo) VoteTrial(_ uint32, _ uint32) error { return nil }
-func (m *mockFestaRepo) RegisterGuild(_ uint32, _ string) error { return nil }
+func (m *mockFestaRepo) GetCharSouls(_ uint32) (uint32, error) { return m.charSouls, m.charErr }
+func (m *mockFestaRepo) HasClaimedMainPrize(_ uint32) bool { return m.hasClaimed }
+func (m *mockFestaRepo) VoteTrial(_ uint32, _ uint32) error { return nil }
+func (m *mockFestaRepo) RegisterGuild(_ uint32, _ string) error { return nil }
func (m *mockFestaRepo) SubmitSouls(_, _ uint32, souls []uint16) error {
m.submittedSouls = souls
return m.submitErr
}
-func (m *mockFestaRepo) ClaimPrize(_ uint32, _ uint32) error { return nil }
+func (m *mockFestaRepo) ClaimPrize(_ uint32, _ uint32) error { return nil }
func (m *mockFestaRepo) ListPrizes(_ uint32, _ string) ([]Prize, error) {
return m.prizes, m.prizesErr
}
@@ -1078,9 +1077,9 @@ type mockDivaRepo struct {
eventsErr error
}
-func (m *mockDivaRepo) DeleteEvents() error { return nil }
-func (m *mockDivaRepo) InsertEvent(_ uint32) error { return nil }
-func (m *mockDivaRepo) GetEvents() ([]DivaEvent, error) { return m.events, m.eventsErr }
+func (m *mockDivaRepo) DeleteEvents() error { return nil }
+func (m *mockDivaRepo) InsertEvent(_ uint32) error { return nil }
+func (m *mockDivaRepo) GetEvents() ([]DivaEvent, error) { return m.events, m.eventsErr }
// --- mockEventRepo ---
@@ -1122,20 +1121,20 @@ func (m *mockMiscRepo) UpsertTrendWeapon(_ uint16, _ uint8) error { return nil }
// --- mockMercenaryRepo ---
type mockMercenaryRepo struct {
- nextRastaID uint32
- rastaIDErr error
- nextAirouID uint32
- airouIDErr error
- loans []MercenaryLoan
- loansErr error
- catUsages []GuildHuntCatUsage
- catUsagesErr error
- guildAirou [][]byte
+ nextRastaID uint32
+ rastaIDErr error
+ nextAirouID uint32
+ airouIDErr error
+ loans []MercenaryLoan
+ loansErr error
+ catUsages []GuildHuntCatUsage
+ catUsagesErr error
+ guildAirou [][]byte
guildAirouErr error
}
-func (m *mockMercenaryRepo) NextRastaID() (uint32, error) { return m.nextRastaID, m.rastaIDErr }
-func (m *mockMercenaryRepo) NextAirouID() (uint32, error) { return m.nextAirouID, m.airouIDErr }
+func (m *mockMercenaryRepo) NextRastaID() (uint32, error) { return m.nextRastaID, m.rastaIDErr }
+func (m *mockMercenaryRepo) NextAirouID() (uint32, error) { return m.nextAirouID, m.airouIDErr }
func (m *mockMercenaryRepo) GetMercenaryLoans(_ uint32) ([]MercenaryLoan, error) {
return m.loans, m.loansErr
}
@@ -1149,17 +1148,17 @@ func (m *mockMercenaryRepo) GetGuildAirou(_ uint32) ([][]byte, error) {
// --- mockCafeRepo ---
type mockCafeRepo struct {
- bonuses []CafeBonus
- bonusesErr error
- claimable []CafeBonus
- claimableErr error
+ bonuses []CafeBonus
+ bonusesErr error
+ claimable []CafeBonus
+ claimableErr error
bonusItemType uint32
bonusItemQty uint32
bonusItemErr error
}
-func (m *mockCafeRepo) ResetAccepted(_ uint32) error { return nil }
-func (m *mockCafeRepo) GetBonuses(_ uint32) ([]CafeBonus, error) { return m.bonuses, m.bonusesErr }
+func (m *mockCafeRepo) ResetAccepted(_ uint32) error { return nil }
+func (m *mockCafeRepo) GetBonuses(_ uint32) ([]CafeBonus, error) { return m.bonuses, m.bonusesErr }
func (m *mockCafeRepo) GetClaimable(_ uint32, _ int64) ([]CafeBonus, error) {
return m.claimable, m.claimableErr
}
diff --git a/server/channelserver/repo_rengoku.go b/server/channelserver/repo_rengoku.go
index 4020667b4..b38fcd3b6 100644
--- a/server/channelserver/repo_rengoku.go
+++ b/server/channelserver/repo_rengoku.go
@@ -3,16 +3,16 @@ package channelserver
import (
"fmt"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// RengokuRepository centralizes all database access for the rengoku_score table.
type RengokuRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewRengokuRepository creates a new RengokuRepository.
-func NewRengokuRepository(db *sqlx.DB) *RengokuRepository {
+func NewRengokuRepository(db *dbutil.DB) *RengokuRepository {
return &RengokuRepository{db: db}
}
diff --git a/server/channelserver/repo_rengoku_test.go b/server/channelserver/repo_rengoku_test.go
index 3a8c377e3..a1be17a6e 100644
--- a/server/channelserver/repo_rengoku_test.go
+++ b/server/channelserver/repo_rengoku_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -12,7 +13,7 @@ func setupRengokuRepo(t *testing.T) (*RengokuRepository, *sqlx.DB, uint32, uint3
userID := CreateTestUser(t, db, "rengoku_test_user")
charID := CreateTestCharacter(t, db, userID, "RengokuChar")
guildID := CreateTestGuild(t, db, charID, "RengokuGuild")
- repo := NewRengokuRepository(db)
+ repo := NewRengokuRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID, guildID
}
diff --git a/server/channelserver/repo_scenario.go b/server/channelserver/repo_scenario.go
index 242e98f6d..967c3fff0 100644
--- a/server/channelserver/repo_scenario.go
+++ b/server/channelserver/repo_scenario.go
@@ -3,16 +3,16 @@ package channelserver
import (
"fmt"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// ScenarioRepository centralizes all database access for the scenario_counter table.
type ScenarioRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewScenarioRepository creates a new ScenarioRepository.
-func NewScenarioRepository(db *sqlx.DB) *ScenarioRepository {
+func NewScenarioRepository(db *dbutil.DB) *ScenarioRepository {
return &ScenarioRepository{db: db}
}
diff --git a/server/channelserver/repo_scenario_test.go b/server/channelserver/repo_scenario_test.go
index f27694b51..17e43d518 100644
--- a/server/channelserver/repo_scenario_test.go
+++ b/server/channelserver/repo_scenario_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -9,7 +10,7 @@ import (
func setupScenarioRepo(t *testing.T) (*ScenarioRepository, *sqlx.DB) {
t.Helper()
db := SetupTestDB(t)
- repo := NewScenarioRepository(db)
+ repo := NewScenarioRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db
}
diff --git a/server/channelserver/repo_session.go b/server/channelserver/repo_session.go
index bb8a0dc6e..de9b9ba09 100644
--- a/server/channelserver/repo_session.go
+++ b/server/channelserver/repo_session.go
@@ -1,16 +1,16 @@
package channelserver
import (
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// SessionRepository centralizes all database access for sign_sessions and servers tables.
type SessionRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewSessionRepository creates a new SessionRepository.
-func NewSessionRepository(db *sqlx.DB) *SessionRepository {
+func NewSessionRepository(db *dbutil.DB) *SessionRepository {
return &SessionRepository{db: db}
}
diff --git a/server/channelserver/repo_session_test.go b/server/channelserver/repo_session_test.go
index e4d7d78bf..e97dc2171 100644
--- a/server/channelserver/repo_session_test.go
+++ b/server/channelserver/repo_session_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -13,7 +14,7 @@ func setupSessionRepo(t *testing.T) (*SessionRepository, *sqlx.DB, uint32, uint3
charID := CreateTestCharacter(t, db, userID, "SessionChar")
token := "test_token_12345"
sessionID := CreateTestSignSession(t, db, userID, token)
- repo := NewSessionRepository(db)
+ repo := NewSessionRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, userID, charID, sessionID, token
}
diff --git a/server/channelserver/repo_shop.go b/server/channelserver/repo_shop.go
index 04f1c18f1..860762d56 100644
--- a/server/channelserver/repo_shop.go
+++ b/server/channelserver/repo_shop.go
@@ -1,16 +1,16 @@
package channelserver
import (
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// ShopRepository centralizes all database access for shop-related tables.
type ShopRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewShopRepository creates a new ShopRepository.
-func NewShopRepository(db *sqlx.DB) *ShopRepository {
+func NewShopRepository(db *dbutil.DB) *ShopRepository {
return &ShopRepository{db: db}
}
diff --git a/server/channelserver/repo_shop_test.go b/server/channelserver/repo_shop_test.go
index 9a84b1070..1fbc61a40 100644
--- a/server/channelserver/repo_shop_test.go
+++ b/server/channelserver/repo_shop_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -11,7 +12,7 @@ func setupShopRepo(t *testing.T) (*ShopRepository, *sqlx.DB, uint32) {
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "shop_test_user")
charID := CreateTestCharacter(t, db, userID, "ShopChar")
- repo := NewShopRepository(db)
+ repo := NewShopRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_stamp.go b/server/channelserver/repo_stamp.go
index 26a9f4d64..595becd44 100644
--- a/server/channelserver/repo_stamp.go
+++ b/server/channelserver/repo_stamp.go
@@ -4,16 +4,16 @@ import (
"fmt"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// StampRepository centralizes all database access for the stamps table.
type StampRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewStampRepository creates a new StampRepository.
-func NewStampRepository(db *sqlx.DB) *StampRepository {
+func NewStampRepository(db *dbutil.DB) *StampRepository {
return &StampRepository{db: db}
}
diff --git a/server/channelserver/repo_stamp_test.go b/server/channelserver/repo_stamp_test.go
index ef0b2e556..c8867e02f 100644
--- a/server/channelserver/repo_stamp_test.go
+++ b/server/channelserver/repo_stamp_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"time"
@@ -12,7 +13,7 @@ func setupStampRepo(t *testing.T) (*StampRepository, *sqlx.DB, uint32) {
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "stamp_test_user")
charID := CreateTestCharacter(t, db, userID, "StampChar")
- repo := NewStampRepository(db)
+ repo := NewStampRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID
}
diff --git a/server/channelserver/repo_tower.go b/server/channelserver/repo_tower.go
index 1e653476e..8194d815e 100644
--- a/server/channelserver/repo_tower.go
+++ b/server/channelserver/repo_tower.go
@@ -3,17 +3,17 @@ package channelserver
import (
"fmt"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// TowerRepository centralizes all database access for tower-related tables
// (tower, guilds tower columns, guild_characters tower columns).
type TowerRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewTowerRepository creates a new TowerRepository.
-func NewTowerRepository(db *sqlx.DB) *TowerRepository {
+func NewTowerRepository(db *dbutil.DB) *TowerRepository {
return &TowerRepository{db: db}
}
diff --git a/server/channelserver/repo_tower_test.go b/server/channelserver/repo_tower_test.go
index 5c3f2e01a..5405c9dc1 100644
--- a/server/channelserver/repo_tower_test.go
+++ b/server/channelserver/repo_tower_test.go
@@ -1,6 +1,7 @@
package channelserver
import (
+ dbutil "erupe-ce/common/db"
"testing"
"github.com/jmoiron/sqlx"
@@ -17,7 +18,7 @@ func setupTowerRepo(t *testing.T) (*TowerRepository, *sqlx.DB, uint32, uint32) {
if _, err := db.Exec("INSERT INTO guild_characters (guild_id, character_id) VALUES ($1, $2)", guildID, charID); err != nil {
t.Fatalf("Failed to add char to guild: %v", err)
}
- repo := NewTowerRepository(db)
+ repo := NewTowerRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, charID, guildID
}
diff --git a/server/channelserver/repo_user.go b/server/channelserver/repo_user.go
index 919f33dd1..5e5b8b9d0 100644
--- a/server/channelserver/repo_user.go
+++ b/server/channelserver/repo_user.go
@@ -4,16 +4,16 @@ import (
"database/sql"
"time"
- "github.com/jmoiron/sqlx"
+ dbutil "erupe-ce/common/db"
)
// UserRepository centralizes all database access for the users table.
type UserRepository struct {
- db *sqlx.DB
+ db *dbutil.DB
}
// NewUserRepository creates a new UserRepository.
-func NewUserRepository(db *sqlx.DB) *UserRepository {
+func NewUserRepository(db *dbutil.DB) *UserRepository {
return &UserRepository{db: db}
}
diff --git a/server/channelserver/repo_user_test.go b/server/channelserver/repo_user_test.go
index 5a95e815c..f7f6bdb21 100644
--- a/server/channelserver/repo_user_test.go
+++ b/server/channelserver/repo_user_test.go
@@ -2,6 +2,7 @@ package channelserver
import (
"database/sql"
+ dbutil "erupe-ce/common/db"
"testing"
"time"
@@ -12,7 +13,7 @@ func setupUserRepo(t *testing.T) (*UserRepository, *sqlx.DB, uint32) {
t.Helper()
db := SetupTestDB(t)
userID := CreateTestUser(t, db, "user_repo_test")
- repo := NewUserRepository(db)
+ repo := NewUserRepository(dbutil.Wrap(db))
t.Cleanup(func() { TeardownTestDB(t, db) })
return repo, db, userID
}
diff --git a/server/channelserver/session_lifecycle_integration_test.go b/server/channelserver/session_lifecycle_integration_test.go
index f00f6864f..b32a3cd7c 100644
--- a/server/channelserver/session_lifecycle_integration_test.go
+++ b/server/channelserver/session_lifecycle_integration_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"time"
+ dbutil "erupe-ce/common/db"
"erupe-ce/common/mhfitem"
cfg "erupe-ce/config"
"erupe-ce/network/clientctx"
@@ -597,18 +598,19 @@ func createTestServerWithDB(t *testing.T, db *sqlx.DB) *Server {
server.logger = logger
// Initialize repositories
- server.charRepo = NewCharacterRepository(db)
- server.guildRepo = NewGuildRepository(db)
- server.userRepo = NewUserRepository(db)
- server.gachaRepo = NewGachaRepository(db)
- server.houseRepo = NewHouseRepository(db)
- server.festaRepo = NewFestaRepository(db)
- server.towerRepo = NewTowerRepository(db)
- server.rengokuRepo = NewRengokuRepository(db)
- server.mailRepo = NewMailRepository(db)
- server.stampRepo = NewStampRepository(db)
- server.distRepo = NewDistributionRepository(db)
- server.sessionRepo = NewSessionRepository(db)
+ wdb := dbutil.Wrap(db)
+ server.charRepo = NewCharacterRepository(wdb)
+ server.guildRepo = NewGuildRepository(wdb)
+ server.userRepo = NewUserRepository(wdb)
+ server.gachaRepo = NewGachaRepository(wdb)
+ server.houseRepo = NewHouseRepository(wdb)
+ server.festaRepo = NewFestaRepository(wdb)
+ server.towerRepo = NewTowerRepository(wdb)
+ server.rengokuRepo = NewRengokuRepository(wdb)
+ server.mailRepo = NewMailRepository(wdb)
+ server.stampRepo = NewStampRepository(wdb)
+ server.distRepo = NewDistributionRepository(wdb)
+ server.sessionRepo = NewSessionRepository(wdb)
return server
}
diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go
index b569d5dce..b419b4a17 100644
--- a/server/channelserver/sys_channel_server.go
+++ b/server/channelserver/sys_channel_server.go
@@ -11,6 +11,7 @@ import (
"time"
"erupe-ce/common/byteframe"
+ dbutil "erupe-ce/common/db"
cfg "erupe-ce/config"
"erupe-ce/network"
"erupe-ce/network/binpacket"
@@ -142,27 +143,28 @@ func NewServer(config *Config) *Server {
handlerTable: buildHandlerTable(),
}
- s.charRepo = NewCharacterRepository(config.DB)
- s.guildRepo = NewGuildRepository(config.DB)
- s.userRepo = NewUserRepository(config.DB)
- s.gachaRepo = NewGachaRepository(config.DB)
- s.houseRepo = NewHouseRepository(config.DB)
- s.festaRepo = NewFestaRepository(config.DB)
- s.towerRepo = NewTowerRepository(config.DB)
- s.rengokuRepo = NewRengokuRepository(config.DB)
- s.mailRepo = NewMailRepository(config.DB)
- s.stampRepo = NewStampRepository(config.DB)
- s.distRepo = NewDistributionRepository(config.DB)
- s.sessionRepo = NewSessionRepository(config.DB)
- s.eventRepo = NewEventRepository(config.DB)
- s.achievementRepo = NewAchievementRepository(config.DB)
- s.shopRepo = NewShopRepository(config.DB)
- s.cafeRepo = NewCafeRepository(config.DB)
- s.goocooRepo = NewGoocooRepository(config.DB)
- s.divaRepo = NewDivaRepository(config.DB)
- s.miscRepo = NewMiscRepository(config.DB)
- s.scenarioRepo = NewScenarioRepository(config.DB)
- s.mercenaryRepo = NewMercenaryRepository(config.DB)
+ wdb := dbutil.Wrap(config.DB)
+ s.charRepo = NewCharacterRepository(wdb)
+ s.guildRepo = NewGuildRepository(wdb)
+ s.userRepo = NewUserRepository(wdb)
+ s.gachaRepo = NewGachaRepository(wdb)
+ s.houseRepo = NewHouseRepository(wdb)
+ s.festaRepo = NewFestaRepository(wdb)
+ s.towerRepo = NewTowerRepository(wdb)
+ s.rengokuRepo = NewRengokuRepository(wdb)
+ s.mailRepo = NewMailRepository(wdb)
+ s.stampRepo = NewStampRepository(wdb)
+ s.distRepo = NewDistributionRepository(wdb)
+ s.sessionRepo = NewSessionRepository(wdb)
+ s.eventRepo = NewEventRepository(wdb)
+ s.achievementRepo = NewAchievementRepository(wdb)
+ s.shopRepo = NewShopRepository(wdb)
+ s.cafeRepo = NewCafeRepository(wdb)
+ s.goocooRepo = NewGoocooRepository(wdb)
+ s.divaRepo = NewDivaRepository(wdb)
+ s.miscRepo = NewMiscRepository(wdb)
+ s.scenarioRepo = NewScenarioRepository(wdb)
+ s.mercenaryRepo = NewMercenaryRepository(wdb)
s.mailService = NewMailService(s.mailRepo, s.guildRepo, s.logger)
s.guildService = NewGuildService(s.guildRepo, s.mailService, s.charRepo, s.logger)
diff --git a/server/channelserver/testhelpers_db.go b/server/channelserver/testhelpers_db.go
index 5fb6f36ed..45bfaa727 100644
--- a/server/channelserver/testhelpers_db.go
+++ b/server/channelserver/testhelpers_db.go
@@ -8,6 +8,7 @@ import (
"testing"
"time"
+ dbutil "erupe-ce/common/db"
"erupe-ce/server/channelserver/compression/nullcomp"
"erupe-ce/server/migrations"
"github.com/jmoiron/sqlx"
@@ -330,25 +331,26 @@ func CreateTestGachaItem(t *testing.T, db *sqlx.DB, entryID uint32, itemType uin
// Use this in integration tests instead of setting s.server.db directly.
func SetTestDB(s *Server, db *sqlx.DB) {
s.db = db
- s.charRepo = NewCharacterRepository(db)
- s.guildRepo = NewGuildRepository(db)
- s.userRepo = NewUserRepository(db)
- s.gachaRepo = NewGachaRepository(db)
- s.houseRepo = NewHouseRepository(db)
- s.festaRepo = NewFestaRepository(db)
- s.towerRepo = NewTowerRepository(db)
- s.rengokuRepo = NewRengokuRepository(db)
- s.mailRepo = NewMailRepository(db)
- s.stampRepo = NewStampRepository(db)
- s.distRepo = NewDistributionRepository(db)
- s.sessionRepo = NewSessionRepository(db)
- s.eventRepo = NewEventRepository(db)
- s.achievementRepo = NewAchievementRepository(db)
- s.shopRepo = NewShopRepository(db)
- s.cafeRepo = NewCafeRepository(db)
- s.goocooRepo = NewGoocooRepository(db)
- s.divaRepo = NewDivaRepository(db)
- s.miscRepo = NewMiscRepository(db)
- s.scenarioRepo = NewScenarioRepository(db)
- s.mercenaryRepo = NewMercenaryRepository(db)
+ wdb := dbutil.Wrap(db)
+ s.charRepo = NewCharacterRepository(wdb)
+ s.guildRepo = NewGuildRepository(wdb)
+ s.userRepo = NewUserRepository(wdb)
+ s.gachaRepo = NewGachaRepository(wdb)
+ s.houseRepo = NewHouseRepository(wdb)
+ s.festaRepo = NewFestaRepository(wdb)
+ s.towerRepo = NewTowerRepository(wdb)
+ s.rengokuRepo = NewRengokuRepository(wdb)
+ s.mailRepo = NewMailRepository(wdb)
+ s.stampRepo = NewStampRepository(wdb)
+ s.distRepo = NewDistributionRepository(wdb)
+ s.sessionRepo = NewSessionRepository(wdb)
+ s.eventRepo = NewEventRepository(wdb)
+ s.achievementRepo = NewAchievementRepository(wdb)
+ s.shopRepo = NewShopRepository(wdb)
+ s.cafeRepo = NewCafeRepository(wdb)
+ s.goocooRepo = NewGoocooRepository(wdb)
+ s.divaRepo = NewDivaRepository(wdb)
+ s.miscRepo = NewMiscRepository(wdb)
+ s.scenarioRepo = NewScenarioRepository(wdb)
+ s.mercenaryRepo = NewMercenaryRepository(wdb)
}
diff --git a/server/discordbot/discord_bot_test.go b/server/discordbot/discord_bot_test.go
index 964e19fea..ab04d0516 100644
--- a/server/discordbot/discord_bot_test.go
+++ b/server/discordbot/discord_bot_test.go
@@ -12,18 +12,18 @@ import (
// mockSession implements the Session interface for testing.
type mockSession struct {
- openErr error
- channelResult *discordgo.Channel
- channelErr error
- userResults map[string]*discordgo.User
- userErr error
- messageSentTo string
- messageSentContent string
- messageErr error
- addHandlerCalls int
- bulkOverwriteAppID string
- bulkOverwriteCommands []*discordgo.ApplicationCommand
- bulkOverwriteErr error
+ openErr error
+ channelResult *discordgo.Channel
+ channelErr error
+ userResults map[string]*discordgo.User
+ userErr error
+ messageSentTo string
+ messageSentContent string
+ messageErr error
+ addHandlerCalls int
+ bulkOverwriteAppID string
+ bulkOverwriteCommands []*discordgo.ApplicationCommand
+ bulkOverwriteErr error
}
func (m *mockSession) Open() error {
diff --git a/server/migrations/migrations.go b/server/migrations/migrations.go
index 1172f56fa..879dc428c 100644
--- a/server/migrations/migrations.go
+++ b/server/migrations/migrations.go
@@ -8,6 +8,8 @@ import (
"strconv"
"strings"
+ dbutil "erupe-ce/common/db"
+
"github.com/jmoiron/sqlx"
"go.uber.org/zap"
)
@@ -15,6 +17,9 @@ import (
//go:embed sql/*.sql
var migrationFS embed.FS
+//go:embed sqlite/*.sql
+var sqliteMigrationFS embed.FS
+
//go:embed seed/*.sql
var seedFS embed.FS
@@ -22,15 +27,17 @@ var seedFS embed.FS
// (auto-marks baseline as applied), then runs all pending migrations in order.
// Each migration runs in its own transaction.
func Migrate(db *sqlx.DB, logger *zap.Logger) (int, error) {
- if err := ensureVersionTable(db); err != nil {
+ sqlite := dbutil.IsSQLite(db)
+
+ if err := ensureVersionTable(db, sqlite); err != nil {
return 0, fmt.Errorf("creating schema_version table: %w", err)
}
- if err := detectExistingDB(db, logger); err != nil {
+ if err := detectExistingDB(db, logger, sqlite); err != nil {
return 0, fmt.Errorf("detecting existing database: %w", err)
}
- migrations, err := readMigrations()
+ migrations, err := readMigrations(sqlite)
if err != nil {
return 0, fmt.Errorf("reading migration files: %w", err)
}
@@ -46,7 +53,7 @@ func Migrate(db *sqlx.DB, logger *zap.Logger) (int, error) {
continue
}
logger.Info(fmt.Sprintf("Applying migration %04d: %s", m.version, m.filename))
- if err := applyMigration(db, m); err != nil {
+ if err := applyMigration(db, m, sqlite); err != nil {
return count, fmt.Errorf("applying %s: %w", m.filename, err)
}
count++
@@ -58,6 +65,7 @@ func Migrate(db *sqlx.DB, logger *zap.Logger) (int, error) {
// ApplySeedData runs all seed/*.sql files. Not tracked in schema_version.
// Safe to run multiple times if seed files use ON CONFLICT DO NOTHING.
func ApplySeedData(db *sqlx.DB, logger *zap.Logger) (int, error) {
+ sqlite := dbutil.IsSQLite(db)
files, err := fs.ReadDir(seedFS, "seed")
if err != nil {
return 0, fmt.Errorf("reading seed directory: %w", err)
@@ -78,7 +86,11 @@ func ApplySeedData(db *sqlx.DB, logger *zap.Logger) (int, error) {
return count, fmt.Errorf("reading seed file %s: %w", name, err)
}
logger.Info(fmt.Sprintf("Applying seed data: %s", name))
- if _, err := db.Exec(string(data)); err != nil {
+ sql := string(data)
+ if sqlite {
+ sql = dbutil.Adapt(db, sql)
+ }
+ if _, err := db.Exec(sql); err != nil {
return count, fmt.Errorf("executing seed file %s: %w", name, err)
}
count++
@@ -88,20 +100,30 @@ func ApplySeedData(db *sqlx.DB, logger *zap.Logger) (int, error) {
// Version returns the highest applied migration number, or 0 if none.
func Version(db *sqlx.DB) (int, error) {
+ sqlite := dbutil.IsSQLite(db)
+
var exists bool
- err := db.QueryRow(`SELECT EXISTS(
- SELECT 1 FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name = 'schema_version'
- )`).Scan(&exists)
- if err != nil {
- return 0, err
+ if sqlite {
+ err := db.QueryRow(`SELECT COUNT(*) > 0 FROM sqlite_master
+ WHERE type='table' AND name='schema_version'`).Scan(&exists)
+ if err != nil {
+ return 0, err
+ }
+ } else {
+ err := db.QueryRow(`SELECT EXISTS(
+ SELECT 1 FROM information_schema.tables
+ WHERE table_schema = 'public' AND table_name = 'schema_version'
+ )`).Scan(&exists)
+ if err != nil {
+ return 0, err
+ }
}
if !exists {
return 0, nil
}
var version int
- err = db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM schema_version").Scan(&version)
+ err := db.QueryRow("SELECT COALESCE(MAX(version), 0) FROM schema_version").Scan(&version)
return version, err
}
@@ -111,18 +133,26 @@ type migration struct {
sql string
}
-func ensureVersionTable(db *sqlx.DB) error {
- _, err := db.Exec(`CREATE TABLE IF NOT EXISTS schema_version (
+func ensureVersionTable(db *sqlx.DB, sqlite bool) error {
+ q := `CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
filename TEXT NOT NULL,
applied_at TIMESTAMPTZ DEFAULT now()
- )`)
+ )`
+ if sqlite {
+ q = `CREATE TABLE IF NOT EXISTS schema_version (
+ version INTEGER PRIMARY KEY,
+ filename TEXT NOT NULL,
+ applied_at TEXT DEFAULT CURRENT_TIMESTAMP
+ )`
+ }
+ _, err := db.Exec(q)
return err
}
// detectExistingDB checks if the database has tables but no schema_version rows.
// If so, it marks the baseline migration (version 1) as already applied.
-func detectExistingDB(db *sqlx.DB, logger *zap.Logger) error {
+func detectExistingDB(db *sqlx.DB, logger *zap.Logger, sqlite bool) error {
var count int
if err := db.QueryRow("SELECT COUNT(*) FROM schema_version").Scan(&count); err != nil {
return err
@@ -133,10 +163,18 @@ func detectExistingDB(db *sqlx.DB, logger *zap.Logger) error {
// Check if the database has any user tables (beyond schema_version itself)
var tableCount int
- err := db.QueryRow(`SELECT COUNT(*) FROM information_schema.tables
- WHERE table_schema = 'public' AND table_name != 'schema_version'`).Scan(&tableCount)
- if err != nil {
- return err
+ if sqlite {
+ err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master
+ WHERE type='table' AND name != 'schema_version'`).Scan(&tableCount)
+ if err != nil {
+ return err
+ }
+ } else {
+ err := db.QueryRow(`SELECT COUNT(*) FROM information_schema.tables
+ WHERE table_schema = 'public' AND table_name != 'schema_version'`).Scan(&tableCount)
+ if err != nil {
+ return err
+ }
}
if tableCount == 0 {
return nil // Fresh database
@@ -144,12 +182,22 @@ func detectExistingDB(db *sqlx.DB, logger *zap.Logger) error {
// Existing database without migration tracking — mark baseline as applied
logger.Info("Detected existing database without schema_version tracking, marking baseline as applied")
- _, err = db.Exec("INSERT INTO schema_version (version, filename) VALUES (1, '0001_init.sql')")
+ _, err := db.Exec("INSERT INTO schema_version (version, filename) VALUES (1, '0001_init.sql')")
return err
}
-func readMigrations() ([]migration, error) {
- files, err := fs.ReadDir(migrationFS, "sql")
+func readMigrations(sqlite bool) ([]migration, error) {
+ var embedFS embed.FS
+ var dir string
+ if sqlite {
+ embedFS = sqliteMigrationFS
+ dir = "sqlite"
+ } else {
+ embedFS = migrationFS
+ dir = "sql"
+ }
+
+ files, err := fs.ReadDir(embedFS, dir)
if err != nil {
return nil, err
}
@@ -163,7 +211,7 @@ func readMigrations() ([]migration, error) {
if err != nil {
return nil, fmt.Errorf("parsing version from %s: %w", f.Name(), err)
}
- data, err := migrationFS.ReadFile("sql/" + f.Name())
+ data, err := embedFS.ReadFile(dir + "/" + f.Name())
if err != nil {
return nil, err
}
@@ -206,7 +254,7 @@ func appliedVersions(db *sqlx.DB) (map[int]bool, error) {
return applied, rows.Err()
}
-func applyMigration(db *sqlx.DB, m migration) error {
+func applyMigration(db *sqlx.DB, m migration, sqlite bool) error {
tx, err := db.Begin()
if err != nil {
return err
@@ -217,10 +265,11 @@ func applyMigration(db *sqlx.DB, m migration) error {
return err
}
- if _, err := tx.Exec(
- "INSERT INTO schema_version (version, filename) VALUES ($1, $2)",
- m.version, m.filename,
- ); err != nil {
+ insertQ := "INSERT INTO schema_version (version, filename) VALUES ($1, $2)"
+ if sqlite {
+ insertQ = "INSERT INTO schema_version (version, filename) VALUES (?, ?)"
+ }
+ if _, err := tx.Exec(insertQ, m.version, m.filename); err != nil {
_ = tx.Rollback()
return err
}
diff --git a/server/migrations/migrations_test.go b/server/migrations/migrations_test.go
index 37f3a45b9..ee2667ef5 100644
--- a/server/migrations/migrations_test.go
+++ b/server/migrations/migrations_test.go
@@ -8,6 +8,7 @@ import (
"github.com/jmoiron/sqlx"
_ "github.com/lib/pq"
"go.uber.org/zap"
+ _ "modernc.org/sqlite"
)
func testDB(t *testing.T) *sqlx.DB {
@@ -194,7 +195,7 @@ func TestParseVersion(t *testing.T) {
}
func TestReadMigrations(t *testing.T) {
- migrations, err := readMigrations()
+ migrations, err := readMigrations(false)
if err != nil {
t.Fatalf("readMigrations failed: %v", err)
}
@@ -210,7 +211,7 @@ func TestReadMigrations(t *testing.T) {
}
func TestReadMigrations_Sorted(t *testing.T) {
- migrations, err := readMigrations()
+ migrations, err := readMigrations(false)
if err != nil {
t.Fatalf("readMigrations failed: %v", err)
}
@@ -223,7 +224,7 @@ func TestReadMigrations_Sorted(t *testing.T) {
}
func TestReadMigrations_AllHaveSQL(t *testing.T) {
- migrations, err := readMigrations()
+ migrations, err := readMigrations(false)
if err != nil {
t.Fatalf("readMigrations failed: %v", err)
}
@@ -235,7 +236,7 @@ func TestReadMigrations_AllHaveSQL(t *testing.T) {
}
func TestReadMigrations_BaselineIsLargest(t *testing.T) {
- migrations, err := readMigrations()
+ migrations, err := readMigrations(false)
if err != nil {
t.Fatalf("readMigrations failed: %v", err)
}
@@ -252,6 +253,61 @@ func TestReadMigrations_BaselineIsLargest(t *testing.T) {
}
}
+func TestReadMigrations_SQLite(t *testing.T) {
+ migrations, err := readMigrations(true)
+ if err != nil {
+ t.Fatalf("readMigrations(sqlite) failed: %v", err)
+ }
+ if len(migrations) != 5 {
+ t.Fatalf("expected 5 SQLite migrations, got %d", len(migrations))
+ }
+ if migrations[0].filename != "0001_init.sql" {
+ t.Errorf("first SQLite migration = %q, want 0001_init.sql", migrations[0].filename)
+ }
+ for _, m := range migrations {
+ if m.sql == "" {
+ t.Errorf("SQLite migration %s has empty SQL", m.filename)
+ }
+ }
+}
+
+func TestSQLiteMigrateInMemory(t *testing.T) {
+ db, err := sqlx.Open("sqlite", ":memory:?_pragma=foreign_keys(1)")
+ if err != nil {
+ t.Fatalf("Failed to open in-memory SQLite: %v", err)
+ }
+ defer func() { _ = db.Close() }()
+
+ logger, _ := zap.NewDevelopment()
+ applied, err := Migrate(db, logger)
+ if err != nil {
+ t.Fatalf("Migrate to SQLite failed: %v", err)
+ }
+ if applied != 5 {
+ t.Errorf("expected 5 migrations applied, got %d", applied)
+ }
+
+ ver, err := Version(db)
+ if err != nil {
+ t.Fatalf("Version failed: %v", err)
+ }
+ if ver != 5 {
+ t.Errorf("expected version 5, got %d", ver)
+ }
+
+ // Verify a few key tables exist
+ for _, table := range []string{"users", "characters", "guilds", "guild_characters", "sign_sessions"} {
+ var count int
+ err := db.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name=?", table).Scan(&count)
+ if err != nil {
+ t.Errorf("Failed to check table %s: %v", table, err)
+ }
+ if count != 1 {
+ t.Errorf("Table %s not found in SQLite schema", table)
+ }
+ }
+}
+
func TestParseVersion_Comprehensive(t *testing.T) {
tests := []struct {
filename string
diff --git a/server/migrations/sqlite/0001_init.sql b/server/migrations/sqlite/0001_init.sql
new file mode 100644
index 000000000..e3624ef61
--- /dev/null
+++ b/server/migrations/sqlite/0001_init.sql
@@ -0,0 +1,855 @@
+-- Erupe consolidated database schema (SQLite)
+-- Translated from PostgreSQL 0001_init.sql
+-- Compatible with modernc.org/sqlite
+--
+-- Includes: init.sql (v9.1.0) + 9.2-update.sql + all 33 patch schemas
+
+PRAGMA journal_mode=WAL;
+PRAGMA foreign_keys=ON;
+
+
+--
+-- Name: achievements; Type: TABLE
+--
+
+CREATE TABLE achievements (
+ id integer NOT NULL,
+ ach0 integer DEFAULT 0,
+ ach1 integer DEFAULT 0,
+ ach2 integer DEFAULT 0,
+ ach3 integer DEFAULT 0,
+ ach4 integer DEFAULT 0,
+ ach5 integer DEFAULT 0,
+ ach6 integer DEFAULT 0,
+ ach7 integer DEFAULT 0,
+ ach8 integer DEFAULT 0,
+ ach9 integer DEFAULT 0,
+ ach10 integer DEFAULT 0,
+ ach11 integer DEFAULT 0,
+ ach12 integer DEFAULT 0,
+ ach13 integer DEFAULT 0,
+ ach14 integer DEFAULT 0,
+ ach15 integer DEFAULT 0,
+ ach16 integer DEFAULT 0,
+ ach17 integer DEFAULT 0,
+ ach18 integer DEFAULT 0,
+ ach19 integer DEFAULT 0,
+ ach20 integer DEFAULT 0,
+ ach21 integer DEFAULT 0,
+ ach22 integer DEFAULT 0,
+ ach23 integer DEFAULT 0,
+ ach24 integer DEFAULT 0,
+ ach25 integer DEFAULT 0,
+ ach26 integer DEFAULT 0,
+ ach27 integer DEFAULT 0,
+ ach28 integer DEFAULT 0,
+ ach29 integer DEFAULT 0,
+ ach30 integer DEFAULT 0,
+ ach31 integer DEFAULT 0,
+ ach32 integer DEFAULT 0,
+ PRIMARY KEY (id)
+);
+
+
+--
+-- Name: bans; Type: TABLE
+--
+
+CREATE TABLE bans (
+ user_id integer NOT NULL,
+ expires TEXT,
+ PRIMARY KEY (user_id)
+);
+
+
+--
+-- Name: cafe_accepted; Type: TABLE
+--
+
+CREATE TABLE cafe_accepted (
+ cafe_id integer NOT NULL,
+ character_id integer NOT NULL
+);
+
+
+--
+-- Name: cafebonus; Type: TABLE
+--
+
+CREATE TABLE cafebonus (
+ id INTEGER PRIMARY KEY,
+ time_req integer NOT NULL,
+ item_type integer NOT NULL,
+ item_id integer NOT NULL,
+ quantity integer NOT NULL
+);
+
+
+--
+-- Name: characters; Type: TABLE
+--
+
+CREATE TABLE characters (
+ id INTEGER PRIMARY KEY,
+ user_id bigint,
+ is_female boolean,
+ is_new_character boolean,
+ name TEXT,
+ unk_desc_string TEXT,
+ gr INTEGER,
+ hr INTEGER,
+ weapon_type INTEGER,
+ last_login integer,
+ savedata BLOB,
+ decomyset BLOB,
+ hunternavi BLOB,
+ otomoairou BLOB,
+ partner BLOB,
+ platebox BLOB,
+ platedata BLOB,
+ platemyset BLOB,
+ rengokudata BLOB,
+ savemercenary BLOB,
+ restrict_guild_scout boolean DEFAULT false NOT NULL,
+ gacha_items BLOB,
+ daily_time TEXT,
+ house_info BLOB,
+ login_boost BLOB,
+ skin_hist BLOB,
+ kouryou_point integer,
+ gcp integer,
+ guild_post_checked TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ time_played integer DEFAULT 0 NOT NULL,
+ weapon_id integer DEFAULT 0 NOT NULL,
+ scenariodata BLOB,
+ savefavoritequest BLOB,
+ friends text DEFAULT '' NOT NULL,
+ blocked text DEFAULT '' NOT NULL,
+ deleted boolean DEFAULT false NOT NULL,
+ cafe_time integer DEFAULT 0,
+ netcafe_points integer DEFAULT 0,
+ boost_time TEXT,
+ cafe_reset TEXT,
+ bonus_quests integer DEFAULT 0 NOT NULL,
+ daily_quests integer DEFAULT 0 NOT NULL,
+ promo_points integer DEFAULT 0 NOT NULL,
+ rasta_id integer,
+ pact_id integer,
+ stampcard integer DEFAULT 0 NOT NULL,
+ mezfes BLOB,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+);
+
+
+--
+-- Name: distribution; Type: TABLE
+--
+
+CREATE TABLE distribution (
+ id INTEGER PRIMARY KEY,
+ character_id integer,
+ type integer NOT NULL,
+ deadline TEXT,
+ event_name text DEFAULT 'GM Gift!' NOT NULL,
+ description text DEFAULT '~C05You received a gift!' NOT NULL,
+ times_acceptable integer DEFAULT 1 NOT NULL,
+ min_hr integer,
+ max_hr integer,
+ min_sr integer,
+ max_sr integer,
+ min_gr integer,
+ max_gr integer,
+ rights integer,
+ selection boolean
+);
+
+
+--
+-- Name: distribution_items; Type: TABLE
+--
+
+CREATE TABLE distribution_items (
+ id INTEGER PRIMARY KEY,
+ distribution_id integer NOT NULL,
+ item_type integer NOT NULL,
+ item_id integer,
+ quantity integer
+);
+
+
+--
+-- Name: distributions_accepted; Type: TABLE
+--
+
+CREATE TABLE distributions_accepted (
+ distribution_id integer,
+ character_id integer
+);
+
+
+--
+-- Name: event_quests; Type: TABLE
+--
+
+CREATE TABLE event_quests (
+ id INTEGER PRIMARY KEY,
+ max_players integer,
+ quest_type integer NOT NULL,
+ quest_id integer NOT NULL,
+ mark integer,
+ flags integer,
+ start_time TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ active_days integer,
+ inactive_days integer
+);
+
+
+--
+-- Name: events; Type: TABLE
+--
+
+CREATE TABLE events (
+ id INTEGER PRIMARY KEY,
+ event_type TEXT NOT NULL CHECK (event_type IN ('festa','diva','vs','mezfes')),
+ start_time TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+
+--
+-- Name: feature_weapon; Type: TABLE
+--
+
+CREATE TABLE feature_weapon (
+ start_time TEXT NOT NULL,
+ featured integer NOT NULL
+);
+
+
+--
+-- Name: festa_prizes; Type: TABLE
+--
+
+CREATE TABLE festa_prizes (
+ id INTEGER PRIMARY KEY,
+ type TEXT NOT NULL CHECK (type IN ('personal','guild')),
+ tier integer NOT NULL,
+ souls_req integer NOT NULL,
+ item_id integer NOT NULL,
+ num_item integer NOT NULL
+);
+
+
+--
+-- Name: festa_prizes_accepted; Type: TABLE
+--
+
+CREATE TABLE festa_prizes_accepted (
+ prize_id integer NOT NULL,
+ character_id integer NOT NULL
+);
+
+
+--
+-- Name: festa_registrations; Type: TABLE
+--
+
+CREATE TABLE festa_registrations (
+ guild_id integer NOT NULL,
+ team TEXT NOT NULL CHECK (team IN ('none','red','blue'))
+);
+
+
+--
+-- Name: festa_submissions; Type: TABLE
+--
+
+CREATE TABLE festa_submissions (
+ character_id integer NOT NULL,
+ guild_id integer NOT NULL,
+ trial_type integer NOT NULL,
+ souls integer NOT NULL,
+ "timestamp" TEXT NOT NULL
+);
+
+
+--
+-- Name: festa_trials; Type: TABLE
+--
+
+CREATE TABLE festa_trials (
+ id INTEGER PRIMARY KEY,
+ objective integer NOT NULL,
+ goal_id integer NOT NULL,
+ times_req integer NOT NULL,
+ locale_req integer DEFAULT 0 NOT NULL,
+ reward integer NOT NULL
+);
+
+
+--
+-- Name: fpoint_items; Type: TABLE
+--
+
+CREATE TABLE fpoint_items (
+ id INTEGER PRIMARY KEY,
+ item_type integer NOT NULL,
+ item_id integer NOT NULL,
+ quantity integer NOT NULL,
+ fpoints integer NOT NULL,
+ buyable boolean DEFAULT false NOT NULL
+);
+
+
+--
+-- Name: gacha_box; Type: TABLE
+--
+
+CREATE TABLE gacha_box (
+ gacha_id integer,
+ entry_id integer,
+ character_id integer
+);
+
+
+--
+-- Name: gacha_entries; Type: TABLE
+--
+
+CREATE TABLE gacha_entries (
+ id INTEGER PRIMARY KEY,
+ gacha_id integer,
+ entry_type integer,
+ item_type integer,
+ item_number integer,
+ item_quantity integer,
+ weight integer,
+ rarity integer,
+ rolls integer,
+ frontier_points integer,
+ daily_limit integer,
+ name text
+);
+
+
+--
+-- Name: gacha_items; Type: TABLE
+--
+
+CREATE TABLE gacha_items (
+ id INTEGER PRIMARY KEY,
+ entry_id integer,
+ item_type integer,
+ item_id integer,
+ quantity integer
+);
+
+
+--
+-- Name: gacha_shop; Type: TABLE
+--
+
+CREATE TABLE gacha_shop (
+ id INTEGER PRIMARY KEY,
+ min_gr integer,
+ min_hr integer,
+ name text,
+ url_banner text,
+ url_feature text,
+ url_thumbnail text,
+ wide boolean,
+ recommended boolean,
+ gacha_type integer,
+ hidden boolean
+);
+
+
+--
+-- Name: gacha_stepup; Type: TABLE
+--
+
+CREATE TABLE gacha_stepup (
+ gacha_id integer,
+ step integer,
+ character_id integer,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+);
+
+
+--
+-- Name: goocoo; Type: TABLE
+--
+
+CREATE TABLE goocoo (
+ id INTEGER PRIMARY KEY,
+ goocoo0 BLOB,
+ goocoo1 BLOB,
+ goocoo2 BLOB,
+ goocoo3 BLOB,
+ goocoo4 BLOB
+);
+
+
+--
+-- Name: guild_adventures; Type: TABLE
+--
+
+CREATE TABLE guild_adventures (
+ id INTEGER PRIMARY KEY,
+ guild_id integer NOT NULL,
+ destination integer NOT NULL,
+ charge integer DEFAULT 0 NOT NULL,
+ depart integer NOT NULL,
+ "return" integer NOT NULL,
+ collected_by text DEFAULT '' NOT NULL
+);
+
+
+--
+-- Name: guild_alliances; Type: TABLE
+--
+
+CREATE TABLE guild_alliances (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ parent_id integer NOT NULL,
+ sub1_id integer,
+ sub2_id integer
+);
+
+
+--
+-- Name: guild_applications; Type: TABLE
+--
+
+CREATE TABLE guild_applications (
+ id INTEGER PRIMARY KEY,
+ guild_id integer NOT NULL,
+ character_id integer NOT NULL,
+ actor_id integer NOT NULL,
+ application_type TEXT NOT NULL CHECK (application_type IN ('applied','invited')),
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ UNIQUE (guild_id, character_id),
+ FOREIGN KEY (actor_id) REFERENCES characters(id),
+ FOREIGN KEY (character_id) REFERENCES characters(id),
+ FOREIGN KEY (guild_id) REFERENCES guilds(id)
+);
+
+
+--
+-- Name: guild_characters; Type: TABLE
+--
+
+CREATE TABLE guild_characters (
+ id INTEGER PRIMARY KEY,
+ guild_id bigint,
+ character_id bigint,
+ joined_at TEXT DEFAULT CURRENT_TIMESTAMP,
+ avoid_leadership boolean DEFAULT false NOT NULL,
+ order_index integer DEFAULT 1 NOT NULL,
+ recruiter boolean DEFAULT false NOT NULL,
+ rp_today integer DEFAULT 0,
+ rp_yesterday integer DEFAULT 0,
+ tower_mission_1 integer,
+ tower_mission_2 integer,
+ tower_mission_3 integer,
+ box_claimed TEXT DEFAULT CURRENT_TIMESTAMP,
+ treasure_hunt integer,
+ trial_vote integer,
+ FOREIGN KEY (character_id) REFERENCES characters(id),
+ FOREIGN KEY (guild_id) REFERENCES guilds(id)
+);
+
+
+--
+-- Name: guild_hunts; Type: TABLE
+--
+
+CREATE TABLE guild_hunts (
+ id INTEGER PRIMARY KEY,
+ guild_id integer NOT NULL,
+ host_id integer NOT NULL,
+ destination integer NOT NULL,
+ level integer NOT NULL,
+ acquired boolean DEFAULT false NOT NULL,
+ collected boolean DEFAULT false NOT NULL,
+ hunt_data BLOB NOT NULL,
+ cats_used text NOT NULL,
+ start TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL
+);
+
+
+--
+-- Name: guild_hunts_claimed; Type: TABLE
+--
+
+CREATE TABLE guild_hunts_claimed (
+ hunt_id integer NOT NULL,
+ character_id integer NOT NULL
+);
+
+
+--
+-- Name: guild_meals; Type: TABLE
+--
+
+CREATE TABLE guild_meals (
+ id INTEGER PRIMARY KEY,
+ guild_id integer NOT NULL,
+ meal_id integer NOT NULL,
+ level integer NOT NULL,
+ created_at TEXT
+);
+
+
+--
+-- Name: guild_posts; Type: TABLE
+--
+
+CREATE TABLE guild_posts (
+ id INTEGER PRIMARY KEY,
+ guild_id integer NOT NULL,
+ author_id integer NOT NULL,
+ post_type integer NOT NULL,
+ stamp_id integer NOT NULL,
+ title text NOT NULL,
+ body text NOT NULL,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ liked_by text DEFAULT '' NOT NULL,
+ deleted boolean DEFAULT false NOT NULL
+);
+
+
+--
+-- Name: guilds; Type: TABLE
+--
+
+CREATE TABLE guilds (
+ id INTEGER PRIMARY KEY,
+ name TEXT,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP,
+ leader_id integer NOT NULL,
+ main_motto integer DEFAULT 0,
+ rank_rp integer DEFAULT 0 NOT NULL,
+ comment TEXT DEFAULT '' NOT NULL,
+ icon BLOB,
+ sub_motto integer DEFAULT 0,
+ item_box BLOB,
+ event_rp integer DEFAULT 0 NOT NULL,
+ pugi_name_1 TEXT DEFAULT '',
+ pugi_name_2 TEXT DEFAULT '',
+ pugi_name_3 TEXT DEFAULT '',
+ recruiting boolean DEFAULT true NOT NULL,
+ pugi_outfit_1 integer DEFAULT 0 NOT NULL,
+ pugi_outfit_2 integer DEFAULT 0 NOT NULL,
+ pugi_outfit_3 integer DEFAULT 0 NOT NULL,
+ pugi_outfits integer DEFAULT 0 NOT NULL,
+ tower_mission_page integer DEFAULT 1,
+ tower_rp integer DEFAULT 0,
+ room_rp integer DEFAULT 0,
+ room_expiry TEXT,
+ weekly_bonus_users integer DEFAULT 0 NOT NULL,
+ rp_reset_at TEXT
+);
+
+
+--
+-- Name: kill_logs; Type: TABLE
+--
+
+CREATE TABLE kill_logs (
+ id INTEGER PRIMARY KEY,
+ character_id integer NOT NULL,
+ monster integer NOT NULL,
+ quantity integer NOT NULL,
+ "timestamp" TEXT NOT NULL
+);
+
+
+--
+-- Name: login_boost; Type: TABLE
+--
+
+CREATE TABLE login_boost (
+ char_id integer,
+ week_req integer,
+ expiration TEXT,
+ reset TEXT
+);
+
+
+--
+-- Name: mail; Type: TABLE
+--
+
+CREATE TABLE mail (
+ id INTEGER PRIMARY KEY,
+ sender_id integer NOT NULL,
+ recipient_id integer NOT NULL,
+ subject TEXT DEFAULT '' NOT NULL,
+ body TEXT DEFAULT '' NOT NULL,
+ read boolean DEFAULT false NOT NULL,
+ attached_item_received boolean DEFAULT false NOT NULL,
+ attached_item integer,
+ attached_item_amount integer DEFAULT 1 NOT NULL,
+ is_guild_invite boolean DEFAULT false NOT NULL,
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL,
+ deleted boolean DEFAULT false NOT NULL,
+ locked boolean DEFAULT false NOT NULL,
+ is_sys_message boolean DEFAULT false NOT NULL,
+ FOREIGN KEY (recipient_id) REFERENCES characters(id),
+ FOREIGN KEY (sender_id) REFERENCES characters(id)
+);
+
+
+--
+-- Name: rengoku_score; Type: TABLE
+--
+
+CREATE TABLE rengoku_score (
+ character_id integer NOT NULL,
+ max_stages_mp integer,
+ max_points_mp integer,
+ max_stages_sp integer,
+ max_points_sp integer,
+ PRIMARY KEY (character_id)
+);
+
+
+--
+-- Name: scenario_counter; Type: TABLE
+--
+
+CREATE TABLE scenario_counter (
+ id INTEGER PRIMARY KEY,
+ scenario_id numeric NOT NULL,
+ category_id numeric NOT NULL
+);
+
+
+--
+-- Name: servers; Type: TABLE
+--
+
+CREATE TABLE servers (
+ server_id integer NOT NULL,
+ current_players integer NOT NULL,
+ world_name text,
+ world_description text,
+ land integer
+);
+
+
+--
+-- Name: shop_items; Type: TABLE
+--
+
+CREATE TABLE shop_items (
+ shop_type integer,
+ shop_id integer,
+ id INTEGER PRIMARY KEY,
+ item_id INTEGER,
+ cost integer,
+ quantity INTEGER,
+ min_hr INTEGER,
+ min_sr INTEGER,
+ min_gr INTEGER,
+ store_level INTEGER,
+ max_quantity INTEGER,
+ road_floors INTEGER,
+ road_fatalis INTEGER
+);
+
+
+--
+-- Name: shop_items_bought; Type: TABLE
+--
+
+CREATE TABLE shop_items_bought (
+ character_id integer,
+ shop_item_id integer,
+ bought integer
+);
+
+CREATE UNIQUE INDEX IF NOT EXISTS shop_items_bought_character_item_unique
+ ON shop_items_bought (character_id, shop_item_id);
+
+
+--
+-- Name: sign_sessions; Type: TABLE
+--
+
+CREATE TABLE sign_sessions (
+ user_id integer,
+ char_id integer,
+ token TEXT NOT NULL,
+ server_id integer,
+ id INTEGER PRIMARY KEY,
+ psn_id text
+);
+
+
+--
+-- Name: stamps; Type: TABLE
+--
+
+CREATE TABLE stamps (
+ character_id integer NOT NULL,
+ hl_total integer DEFAULT 0,
+ hl_redeemed integer DEFAULT 0,
+ hl_checked TEXT,
+ ex_total integer DEFAULT 0,
+ ex_redeemed integer DEFAULT 0,
+ ex_checked TEXT,
+ monthly_claimed TEXT,
+ monthly_hl_claimed TEXT,
+ monthly_ex_claimed TEXT,
+ PRIMARY KEY (character_id)
+);
+
+
+--
+-- Name: titles; Type: TABLE
+--
+
+CREATE TABLE titles (
+ id integer NOT NULL,
+ char_id integer NOT NULL,
+ unlocked_at TEXT,
+ updated_at TEXT
+);
+
+
+--
+-- Name: tower; Type: TABLE
+--
+
+CREATE TABLE tower (
+ char_id integer,
+ tr integer,
+ trp integer,
+ tsp integer,
+ block1 integer,
+ block2 integer,
+ skills text,
+ gems text
+);
+
+
+--
+-- Name: trend_weapons; Type: TABLE
+--
+
+CREATE TABLE trend_weapons (
+ weapon_id integer NOT NULL,
+ weapon_type integer NOT NULL,
+ count integer DEFAULT 0,
+ PRIMARY KEY (weapon_id)
+);
+
+
+--
+-- Name: user_binary; Type: TABLE
+--
+
+CREATE TABLE user_binary (
+ id INTEGER PRIMARY KEY,
+ house_tier BLOB,
+ house_state integer,
+ house_password text,
+ house_data BLOB,
+ house_furniture BLOB,
+ bookshelf BLOB,
+ gallery BLOB,
+ tore BLOB,
+ garden BLOB,
+ mission BLOB
+);
+
+
+--
+-- Name: users; Type: TABLE
+--
+
+CREATE TABLE users (
+ id INTEGER PRIMARY KEY,
+ username text NOT NULL UNIQUE,
+ password text NOT NULL,
+ item_box BLOB,
+ rights integer DEFAULT 12 NOT NULL,
+ last_character integer DEFAULT 0,
+ last_login TEXT,
+ return_expires TEXT,
+ gacha_premium integer,
+ gacha_trial integer,
+ frontier_points integer,
+ psn_id text,
+ wiiu_key text,
+ discord_token text,
+ discord_id text,
+ op boolean,
+ timer boolean
+);
+
+
+--
+-- Name: warehouse; Type: TABLE
+--
+
+CREATE TABLE warehouse (
+ character_id integer NOT NULL,
+ item0 BLOB,
+ item1 BLOB,
+ item2 BLOB,
+ item3 BLOB,
+ item4 BLOB,
+ item5 BLOB,
+ item6 BLOB,
+ item7 BLOB,
+ item8 BLOB,
+ item9 BLOB,
+ item10 BLOB,
+ item0name text,
+ item1name text,
+ item2name text,
+ item3name text,
+ item4name text,
+ item5name text,
+ item6name text,
+ item7name text,
+ item8name text,
+ item9name text,
+ equip0 BLOB,
+ equip1 BLOB,
+ equip2 BLOB,
+ equip3 BLOB,
+ equip4 BLOB,
+ equip5 BLOB,
+ equip6 BLOB,
+ equip7 BLOB,
+ equip8 BLOB,
+ equip9 BLOB,
+ equip10 BLOB,
+ equip0name text,
+ equip1name text,
+ equip2name text,
+ equip3name text,
+ equip4name text,
+ equip5name text,
+ equip6name text,
+ equip7name text,
+ equip8name text,
+ equip9name text,
+ PRIMARY KEY (character_id)
+);
+
+
+--
+-- Indexes
+--
+
+CREATE INDEX guild_application_type_index ON guild_applications (application_type);
+
+CREATE UNIQUE INDEX guild_character_unique_index ON guild_characters (character_id);
+
+CREATE INDEX mail_recipient_deleted_created_id_index ON mail (recipient_id, deleted, created_at DESC, id DESC);
diff --git a/server/migrations/sqlite/0002_catch_up_patches.sql b/server/migrations/sqlite/0002_catch_up_patches.sql
new file mode 100644
index 000000000..938f181ba
--- /dev/null
+++ b/server/migrations/sqlite/0002_catch_up_patches.sql
@@ -0,0 +1,3 @@
+-- SQLite: no-op. The consolidated 0001_init.sql already includes all patch columns.
+-- This file exists so the migration version numbering stays in sync with PostgreSQL.
+SELECT 1;
diff --git a/server/migrations/sqlite/0003_shop_items_bought_unique.sql b/server/migrations/sqlite/0003_shop_items_bought_unique.sql
new file mode 100644
index 000000000..35abff9f7
--- /dev/null
+++ b/server/migrations/sqlite/0003_shop_items_bought_unique.sql
@@ -0,0 +1,2 @@
+-- SQLite: no-op. The unique index is already in 0001_init.sql.
+SELECT 1;
diff --git a/server/migrations/sqlite/0004_alliance_recruiting.sql b/server/migrations/sqlite/0004_alliance_recruiting.sql
new file mode 100644
index 000000000..0c123cb58
--- /dev/null
+++ b/server/migrations/sqlite/0004_alliance_recruiting.sql
@@ -0,0 +1,2 @@
+-- SQLite: no-op. The recruiting column is already in 0001_init.sql.
+SELECT 1;
diff --git a/server/migrations/sqlite/0005_distribution_drop_data.sql b/server/migrations/sqlite/0005_distribution_drop_data.sql
new file mode 100644
index 000000000..75bdcb878
--- /dev/null
+++ b/server/migrations/sqlite/0005_distribution_drop_data.sql
@@ -0,0 +1,2 @@
+-- SQLite: no-op. The data column was never included in 0001_init.sql.
+SELECT 1;
diff --git a/server/setup/handlers.go b/server/setup/handlers.go
index 37dbd1cfb..b62a54354 100644
--- a/server/setup/handlers.go
+++ b/server/setup/handlers.go
@@ -45,6 +45,15 @@ func (ws *wizardServer) handleClientModes(w http.ResponseWriter, _ *http.Request
writeJSON(w, http.StatusOK, map[string]interface{}{"modes": clientModes()})
}
+func (ws *wizardServer) handleCheckQuests(w http.ResponseWriter, _ *http.Request) {
+ status := checkQuestFiles("")
+ writeJSON(w, http.StatusOK, status)
+}
+
+func (ws *wizardServer) handlePresets(w http.ResponseWriter, _ *http.Request) {
+ writeJSON(w, http.StatusOK, map[string]interface{}{"presets": availablePresets()})
+}
+
// testDBRequest is the JSON body for POST /api/setup/test-db.
type testDBRequest struct {
Host string `json:"host"`
diff --git a/server/setup/setup.go b/server/setup/setup.go
index 51068d96d..f11e08094 100644
--- a/server/setup/setup.go
+++ b/server/setup/setup.go
@@ -23,6 +23,8 @@ func Run(logger *zap.Logger, port int) error {
r.HandleFunc("/api/setup/client-modes", ws.handleClientModes).Methods("GET")
r.HandleFunc("/api/setup/test-db", ws.handleTestDB).Methods("POST")
r.HandleFunc("/api/setup/init-db", ws.handleInitDB).Methods("POST")
+ r.HandleFunc("/api/setup/check-quests", ws.handleCheckQuests).Methods("GET")
+ r.HandleFunc("/api/setup/presets", ws.handlePresets).Methods("GET")
r.HandleFunc("/api/setup/finish", ws.handleFinish).Methods("POST")
srv := &http.Server{
diff --git a/server/setup/wizard.go b/server/setup/wizard.go
index e6410a5a5..40ff6b3cb 100644
--- a/server/setup/wizard.go
+++ b/server/setup/wizard.go
@@ -6,6 +6,7 @@ import (
"fmt"
"net"
"os"
+ "path/filepath"
"github.com/lib/pq"
)
@@ -31,6 +32,7 @@ type FinishRequest struct {
Language string `json:"language"`
ClientMode string `json:"clientMode"`
AutoCreateAccount bool `json:"autoCreateAccount"`
+ Preset string `json:"preset"`
}
// buildDefaultConfig produces a minimal config map with only user-provided values.
@@ -40,7 +42,7 @@ func buildDefaultConfig(req FinishRequest) map[string]interface{} {
if lang == "" {
lang = "jp"
}
- return map[string]interface{}{
+ cfg := map[string]interface{}{
"Host": req.Host,
"Language": lang,
"ClientMode": req.ClientMode,
@@ -53,6 +55,156 @@ func buildDefaultConfig(req FinishRequest) map[string]interface{} {
"Database": req.DBName,
},
}
+
+ // Apply preset overrides. The "community" preset uses Viper defaults and
+ // adds nothing to the config file.
+ if overrides, ok := presetConfigs()[req.Preset]; ok {
+ for k, v := range overrides {
+ cfg[k] = v
+ }
+ }
+
+ return cfg
+}
+
+// presetConfigs returns config overrides keyed by preset ID.
+// The "community" preset is intentionally absent — it relies entirely on
+// Viper defaults.
+func presetConfigs() map[string]map[string]interface{} {
+ return map[string]map[string]interface{}{
+ "solo": {
+ "GameplayOptions": map[string]interface{}{
+ "HRPMultiplier": 3.0,
+ "SRPMultiplier": 3.0,
+ "GRPMultiplier": 3.0,
+ "GSRPMultiplier": 3.0,
+ "ZennyMultiplier": 2.0,
+ "GZennyMultiplier": 2.0,
+ "MaterialMultiplier": 2.0,
+ "GMaterialMultiplier": 2.0,
+ "ExtraCarves": 2,
+ "GExtraCarves": 2,
+ },
+ "Entrance": map[string]interface{}{
+ "Entries": []map[string]interface{}{
+ {
+ "Name": "Solo",
+ "Type": 1,
+ "Channels": []map[string]interface{}{
+ {"Port": 54001, "MaxPlayers": 100},
+ },
+ },
+ },
+ },
+ },
+ "small": {
+ "Entrance": map[string]interface{}{
+ "Entries": []map[string]interface{}{
+ {
+ "Name": "World 1",
+ "Type": 1,
+ "Channels": []map[string]interface{}{
+ {"Port": 54001, "MaxPlayers": 100},
+ {"Port": 54002, "MaxPlayers": 100},
+ },
+ },
+ },
+ },
+ },
+ "rebalanced": {
+ "GameplayOptions": map[string]interface{}{
+ "HRPMultiplier": 2.0,
+ "SRPMultiplier": 2.0,
+ "GRPMultiplier": 2.0,
+ "GSRPMultiplier": 2.0,
+ "ExtraCarves": 1,
+ "GExtraCarves": 1,
+ },
+ "Entrance": map[string]interface{}{
+ "Entries": []map[string]interface{}{
+ {
+ "Name": "Normal",
+ "Type": 1,
+ "Channels": []map[string]interface{}{
+ {"Port": 54001, "MaxPlayers": 100},
+ {"Port": 54002, "MaxPlayers": 100},
+ },
+ },
+ {
+ "Name": "Cities",
+ "Type": 2,
+ "Channels": []map[string]interface{}{
+ {"Port": 54003, "MaxPlayers": 100},
+ {"Port": 54004, "MaxPlayers": 100},
+ },
+ },
+ },
+ },
+ },
+ }
+}
+
+// QuestStatus holds the result of a quest files check.
+type QuestStatus struct {
+ QuestsFound bool `json:"questsFound"`
+ QuestCount int `json:"questCount"`
+}
+
+// checkQuestFiles checks if quest files exist in the bin/quests/ directory.
+func checkQuestFiles(binPath string) QuestStatus {
+ if binPath == "" {
+ binPath = "bin"
+ }
+ questDir := filepath.Join(binPath, "quests")
+ entries, err := os.ReadDir(questDir)
+ if err != nil {
+ return QuestStatus{QuestsFound: false, QuestCount: 0}
+ }
+ count := 0
+ for _, e := range entries {
+ if !e.IsDir() {
+ count++
+ }
+ }
+ return QuestStatus{QuestsFound: count > 0, QuestCount: count}
+}
+
+// PresetInfo describes a gameplay preset for the wizard UI.
+type PresetInfo struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Channels int `json:"channels"`
+}
+
+// availablePresets returns the list of gameplay presets shown in the wizard.
+func availablePresets() []PresetInfo {
+ return []PresetInfo{
+ {
+ ID: "solo",
+ Name: "Solo / Testing",
+ Description: "Single channel, boosted XP rates (3x), relaxed grind. Ideal for solo play or development testing.",
+ Channels: 1,
+ },
+ {
+ ID: "small",
+ Name: "Small Group (2-8 players)",
+ Description: "Two channels with vanilla rates. Good for friends playing together.",
+ Channels: 2,
+ },
+ {
+ ID: "community",
+ Name: "Community Server",
+ Description: "Full 8-channel topology with vanilla rates. Ready for a public community.",
+ Channels: 8,
+ },
+ {
+ ID: "rebalanced",
+ Name: "Rebalanced",
+ Description: "Community-tuned rates: 2x GRP, 2x HRP, extra carves. Addresses G-Rank grind without trivializing content.",
+ Channels: 4,
+ },
+ }
}
// writeConfig writes the config map to config.json with pretty formatting.
diff --git a/server/setup/wizard.html b/server/setup/wizard.html
index be089c012..5a5dc9b03 100644
--- a/server/setup/wizard.html
+++ b/server/setup/wizard.html
@@ -87,12 +87,16 @@ h1{font-size:1.75rem;margin-bottom:.5rem;color:#e94560;text-align:center}
+
+
1. Database
2. Schema
- 3. Server
- 4. Finish
+ 3. Quest Files
+ 4. Preset
+ 5. Server
+ 6. Finish
@@ -134,8 +138,39 @@ h1{font-size:1.75rem;margin-bottom:.5rem;color:#e94560;text-align:center}
-
+
+
Quest Files
+
Quest files are needed for gameplay. The server checks the bin/quests/ directory.
+
Checking quest files...
+
+
No quest files found.
+
Download the quest archive:
+
https://files.catbox.moe/xf0l7w.7z
+
Download the archive, extract it, and place the quests/ and scenarios/ folders into the bin/ directory next to the server binary.
+
+
+
+
+
+
+
+
+
+
+
+
+
Gameplay Preset
+
Choose a preset that matches your intended use. This configures channels and gameplay rates.
+
+
+
+
+
+
+
+
+
-
-
+
+
Review & Finish
Verify your settings before creating config.json.
-
+
@@ -184,10 +219,13 @@ h1{font-size:1.75rem;margin-bottom:.5rem;color:#e94560;text-align:center}