Files
Erupe/server/channelserver/repo_house.go
Houmgaor ecfe58ffb4 feat: add SQLite support, setup wizard enhancements, and live dashboard
Add zero-dependency SQLite mode so users can run Erupe without
PostgreSQL. A transparent db.DB wrapper auto-translates PostgreSQL
SQL ($N placeholders, now(), ::casts, ILIKE, public. prefix,
TRUNCATE) for SQLite at runtime — all 28 repo files use the wrapper
with no per-query changes needed.

Setup wizard gains two new steps: quest file detection with download
link, and gameplay presets (solo/small/community/rebalanced). The API
server gets a /dashboard endpoint with auto-refreshing stats.

CI release workflow now builds and pushes Docker images to GHCR
alongside binary artifacts on tag push.

Key changes:
- common/db: DB/Tx wrapper with 6 SQL translation rules
- server/migrations/sqlite: full SQLite schema (0001-0005)
- config: Database.Driver field ("postgres" or "sqlite")
- main.go: SQLite connection with WAL mode, single writer
- server/setup: quest check + preset selection steps
- server/api: /dashboard with live stats
- .github/workflows: Docker in release, deduplicate docker.yml
2026-03-05 18:00:30 +01:00

218 lines
7.7 KiB
Go

package channelserver
import (
"fmt"
dbutil "erupe-ce/common/db"
)
// HouseRepository centralizes all database access for house-related tables
// (user_binary house columns, warehouse, titles).
type HouseRepository struct {
db *dbutil.DB
}
// NewHouseRepository creates a new HouseRepository.
func NewHouseRepository(db *dbutil.DB) *HouseRepository {
return &HouseRepository{db: db}
}
// user_binary house columns
// UpdateInterior saves the house furniture layout.
func (r *HouseRepository) UpdateInterior(charID uint32, data []byte) error {
_, err := r.db.Exec(`UPDATE user_binary SET house_furniture=$1 WHERE id=$2`, data, charID)
return err
}
const houseQuery = `SELECT c.id, hr, gr, name, COALESCE(ub.house_state, 2) as house_state, COALESCE(ub.house_password, '') as house_password
FROM characters c LEFT JOIN user_binary ub ON ub.id = c.id WHERE c.id=$1`
// GetHouseByCharID returns house data for a single character.
func (r *HouseRepository) GetHouseByCharID(charID uint32) (HouseData, error) {
var house HouseData
err := r.db.QueryRowx(houseQuery, charID).StructScan(&house)
return house, err
}
// SearchHousesByName returns houses matching a name pattern (case-insensitive).
func (r *HouseRepository) SearchHousesByName(name string) ([]HouseData, error) {
var houses []HouseData
rows, err := r.db.Queryx(
`SELECT c.id, hr, gr, name, COALESCE(ub.house_state, 2) as house_state, COALESCE(ub.house_password, '') as house_password
FROM characters c LEFT JOIN user_binary ub ON ub.id = c.id WHERE name ILIKE $1`,
fmt.Sprintf(`%%%s%%`, name),
)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var house HouseData
if err := rows.StructScan(&house); err == nil {
houses = append(houses, house)
}
}
return houses, nil
}
// UpdateHouseState sets the house visibility state and password.
func (r *HouseRepository) UpdateHouseState(charID uint32, state uint8, password string) error {
_, err := r.db.Exec(`UPDATE user_binary SET house_state=$1, house_password=$2 WHERE id=$3`, state, password, charID)
return err
}
// GetHouseAccess returns the house state and password for access control checks.
func (r *HouseRepository) GetHouseAccess(charID uint32) (state uint8, password string, err error) {
state = 2 // default to password-protected
err = r.db.QueryRow(
`SELECT COALESCE(house_state, 2) as house_state, COALESCE(house_password, '') as house_password FROM user_binary WHERE id=$1`,
charID,
).Scan(&state, &password)
return
}
// GetHouseContents returns all house content columns for rendering a house visit.
func (r *HouseRepository) GetHouseContents(charID uint32) (houseTier, houseData, houseFurniture, bookshelf, gallery, tore, garden []byte, err error) {
err = r.db.QueryRow(
`SELECT house_tier, house_data, house_furniture, bookshelf, gallery, tore, garden FROM user_binary WHERE id=$1`,
charID,
).Scan(&houseTier, &houseData, &houseFurniture, &bookshelf, &gallery, &tore, &garden)
return
}
// GetMission returns the myhouse mission data.
func (r *HouseRepository) GetMission(charID uint32) ([]byte, error) {
var data []byte
err := r.db.QueryRow(`SELECT mission FROM user_binary WHERE id=$1`, charID).Scan(&data)
return data, err
}
// UpdateMission saves the myhouse mission data.
func (r *HouseRepository) UpdateMission(charID uint32, data []byte) error {
_, err := r.db.Exec(`UPDATE user_binary SET mission=$1 WHERE id=$2`, data, charID)
return err
}
// Warehouse methods
// InitializeWarehouse ensures a warehouse row exists for the character.
func (r *HouseRepository) InitializeWarehouse(charID uint32) error {
var t int
err := r.db.QueryRow(`SELECT character_id FROM warehouse WHERE character_id=$1`, charID).Scan(&t)
if err != nil {
_, err = r.db.Exec(`INSERT INTO warehouse (character_id) VALUES ($1)`, charID)
return err
}
return nil
}
const warehouseNamesSQL = `
SELECT
COALESCE(item0name, ''),
COALESCE(item1name, ''),
COALESCE(item2name, ''),
COALESCE(item3name, ''),
COALESCE(item4name, ''),
COALESCE(item5name, ''),
COALESCE(item6name, ''),
COALESCE(item7name, ''),
COALESCE(item8name, ''),
COALESCE(item9name, ''),
COALESCE(equip0name, ''),
COALESCE(equip1name, ''),
COALESCE(equip2name, ''),
COALESCE(equip3name, ''),
COALESCE(equip4name, ''),
COALESCE(equip5name, ''),
COALESCE(equip6name, ''),
COALESCE(equip7name, ''),
COALESCE(equip8name, ''),
COALESCE(equip9name, '')
FROM warehouse WHERE character_id=$1`
// GetWarehouseNames returns item and equipment box names.
func (r *HouseRepository) GetWarehouseNames(charID uint32) (itemNames, equipNames [10]string, err error) {
err = r.db.QueryRow(warehouseNamesSQL, charID).Scan(
&itemNames[0], &itemNames[1], &itemNames[2], &itemNames[3], &itemNames[4],
&itemNames[5], &itemNames[6], &itemNames[7], &itemNames[8], &itemNames[9],
&equipNames[0], &equipNames[1], &equipNames[2], &equipNames[3], &equipNames[4],
&equipNames[5], &equipNames[6], &equipNames[7], &equipNames[8], &equipNames[9],
)
return
}
// RenameWarehouseBox renames an item or equipment warehouse box.
// boxType 0 = items, 1 = equipment. boxIndex must be 0-9.
func (r *HouseRepository) RenameWarehouseBox(charID uint32, boxType uint8, boxIndex uint8, name string) error {
var col string
switch boxType {
case 0:
col = fmt.Sprintf("item%dname", boxIndex)
case 1:
col = fmt.Sprintf("equip%dname", boxIndex)
default:
return fmt.Errorf("invalid box type: %d", boxType)
}
_, err := r.db.Exec(fmt.Sprintf("UPDATE warehouse SET %s=$1 WHERE character_id=$2", col), name, charID)
return err
}
// GetWarehouseItemData returns raw serialized item data for a warehouse box.
// index 0-10 (10 = gift box).
func (r *HouseRepository) GetWarehouseItemData(charID uint32, index uint8) ([]byte, error) {
var data []byte
err := r.db.QueryRow(fmt.Sprintf(`SELECT item%d FROM warehouse WHERE character_id=$1`, index), charID).Scan(&data)
return data, err
}
// SetWarehouseItemData saves raw serialized item data for a warehouse box.
func (r *HouseRepository) SetWarehouseItemData(charID uint32, index uint8, data []byte) error {
_, err := r.db.Exec(fmt.Sprintf(`UPDATE warehouse SET item%d=$1 WHERE character_id=$2`, index), data, charID)
return err
}
// GetWarehouseEquipData returns raw serialized equipment data for a warehouse box.
func (r *HouseRepository) GetWarehouseEquipData(charID uint32, index uint8) ([]byte, error) {
var data []byte
err := r.db.QueryRow(fmt.Sprintf(`SELECT equip%d FROM warehouse WHERE character_id=$1`, index), charID).Scan(&data)
return data, err
}
// SetWarehouseEquipData saves raw serialized equipment data for a warehouse box.
func (r *HouseRepository) SetWarehouseEquipData(charID uint32, index uint8, data []byte) error {
_, err := r.db.Exec(fmt.Sprintf(`UPDATE warehouse SET equip%d=$1 WHERE character_id=$2`, index), data, charID)
return err
}
// Title methods
// GetTitles returns all titles for a character.
func (r *HouseRepository) GetTitles(charID uint32) ([]Title, error) {
var titles []Title
rows, err := r.db.Queryx(`SELECT id, unlocked_at, updated_at FROM titles WHERE char_id=$1`, charID)
if err != nil {
return nil, err
}
defer func() { _ = rows.Close() }()
for rows.Next() {
var title Title
if err := rows.StructScan(&title); err == nil {
titles = append(titles, title)
}
}
return titles, nil
}
// AcquireTitle inserts a new title or updates its timestamp if it already exists.
func (r *HouseRepository) AcquireTitle(titleID uint16, charID uint32) error {
var exists int
err := r.db.QueryRow(`SELECT count(*) FROM titles WHERE id=$1 AND char_id=$2`, titleID, charID).Scan(&exists)
if err != nil || exists == 0 {
_, err = r.db.Exec(`INSERT INTO titles VALUES ($1, $2, now(), now())`, titleID, charID)
} else {
_, err = r.db.Exec(`UPDATE titles SET updated_at=now() WHERE id=$1 AND char_id=$2`, titleID, charID)
}
return err
}