Files
Erupe/server/channelserver/repo_house.go
Houmgaor 35d8471d59 fix(channelserver): resolve all golangci-lint issues and add handler tests
Fix errcheck violations across 11 repo files by wrapping deferred
rows.Close() and tx.Rollback() calls to discard the error return.
Fix unchecked Scan/Exec calls in guild store tests. Fix staticcheck
SA9003 empty branch in test helpers.

Add 6 mock-based unit tests for GetCharacterSaveData covering nil
savedata, sql.ErrNoRows, DB errors, compressed round-trip,
new-character skip, and config mode/pointer propagation.
2026-02-21 14:47:25 +01:00

218 lines
7.7 KiB
Go

package channelserver
import (
"fmt"
"github.com/jmoiron/sqlx"
)
// HouseRepository centralizes all database access for house-related tables
// (user_binary house columns, warehouse, titles).
type HouseRepository struct {
db *sqlx.DB
}
// NewHouseRepository creates a new HouseRepository.
func NewHouseRepository(db *sqlx.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
}