first commit

This commit is contained in:
2026-02-26 17:52:48 +09:00
commit dabf1f3ba9
49 changed files with 14883 additions and 0 deletions

47
internal/db/migrations.go Normal file
View File

@@ -0,0 +1,47 @@
package db
var migrations = []string{
// 0: accounts table
`CREATE TABLE IF NOT EXISTS accounts (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(32) UNIQUE NOT NULL,
password VARCHAR(128) NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
// 1: characters table
`CREATE TABLE IF NOT EXISTS characters (
id BIGSERIAL PRIMARY KEY,
account_id BIGINT NOT NULL REFERENCES accounts(id),
name VARCHAR(32) UNIQUE NOT NULL,
level INT NOT NULL DEFAULT 1,
exp BIGINT NOT NULL DEFAULT 0,
hp INT NOT NULL DEFAULT 100,
max_hp INT NOT NULL DEFAULT 100,
mp INT NOT NULL DEFAULT 50,
max_mp INT NOT NULL DEFAULT 50,
str INT NOT NULL DEFAULT 10,
dex INT NOT NULL DEFAULT 10,
int_stat INT NOT NULL DEFAULT 10,
zone_id INT NOT NULL DEFAULT 1,
pos_x REAL NOT NULL DEFAULT 0,
pos_y REAL NOT NULL DEFAULT 0,
pos_z REAL NOT NULL DEFAULT 0,
rotation REAL NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)`,
// 2: inventory table
`CREATE TABLE IF NOT EXISTS inventory (
id BIGSERIAL PRIMARY KEY,
character_id BIGINT NOT NULL REFERENCES characters(id) ON DELETE CASCADE,
slot INT NOT NULL,
item_id INT NOT NULL,
quantity INT NOT NULL DEFAULT 1,
UNIQUE(character_id, slot)
)`,
// 3: index for character lookups by account
`CREATE INDEX IF NOT EXISTS idx_characters_account_id ON characters(account_id)`,
}

51
internal/db/postgres.go Normal file
View File

@@ -0,0 +1,51 @@
package db
import (
"context"
"fmt"
"github.com/jackc/pgx/v5/pgxpool"
"a301_game_server/config"
"a301_game_server/pkg/logger"
)
// Pool wraps a pgx connection pool.
type Pool struct {
*pgxpool.Pool
}
// NewPool creates a connection pool to PostgreSQL.
func NewPool(ctx context.Context, cfg *config.DatabaseConfig) (*Pool, error) {
poolCfg, err := pgxpool.ParseConfig(cfg.DSN())
if err != nil {
return nil, fmt.Errorf("parse dsn: %w", err)
}
poolCfg.MaxConns = cfg.MaxConns
poolCfg.MinConns = cfg.MinConns
pool, err := pgxpool.NewWithConfig(ctx, poolCfg)
if err != nil {
return nil, fmt.Errorf("create pool: %w", err)
}
if err := pool.Ping(ctx); err != nil {
pool.Close()
return nil, fmt.Errorf("ping database: %w", err)
}
logger.Info("database connected", "host", cfg.Host, "db", cfg.DBName)
return &Pool{pool}, nil
}
// RunMigrations executes all schema migrations.
func (p *Pool) RunMigrations(ctx context.Context) error {
for i, m := range migrations {
if _, err := p.Exec(ctx, m); err != nil {
return fmt.Errorf("migration %d failed: %w", i, err)
}
}
logger.Info("database migrations completed", "count", len(migrations))
return nil
}

View File

@@ -0,0 +1,166 @@
package repository
import (
"context"
"fmt"
"a301_game_server/internal/db"
)
// CharacterData holds persisted character state.
type CharacterData struct {
ID int64
AccountID int64
Name string
Level int32
Exp int64
HP int32
MaxHP int32
MP int32
MaxMP int32
Str int32
Dex int32
IntStat int32
ZoneID int32
PosX float32
PosY float32
PosZ float32
Rotation float32
}
// CharacterRepo handles character persistence.
type CharacterRepo struct {
pool *db.Pool
}
// NewCharacterRepo creates a new character repository.
func NewCharacterRepo(pool *db.Pool) *CharacterRepo {
return &CharacterRepo{pool: pool}
}
// Create inserts a new character.
func (r *CharacterRepo) Create(ctx context.Context, accountID int64, name string) (*CharacterData, error) {
c := &CharacterData{
AccountID: accountID,
Name: name,
Level: 1,
HP: 100,
MaxHP: 100,
MP: 50,
MaxMP: 50,
Str: 10,
Dex: 10,
IntStat: 10,
ZoneID: 1,
}
err := r.pool.QueryRow(ctx,
`INSERT INTO characters (account_id, name, level, hp, max_hp, mp, max_mp, str, dex, int_stat, zone_id)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
RETURNING id`,
c.AccountID, c.Name, c.Level, c.HP, c.MaxHP, c.MP, c.MaxMP, c.Str, c.Dex, c.IntStat, c.ZoneID,
).Scan(&c.ID)
if err != nil {
return nil, fmt.Errorf("create character: %w", err)
}
return c, nil
}
// GetByAccountID returns all characters for an account.
func (r *CharacterRepo) GetByAccountID(ctx context.Context, accountID int64) ([]*CharacterData, error) {
rows, err := r.pool.Query(ctx,
`SELECT id, account_id, name, level, exp, hp, max_hp, mp, max_mp, str, dex, int_stat,
zone_id, pos_x, pos_y, pos_z, rotation
FROM characters WHERE account_id = $1`, accountID,
)
if err != nil {
return nil, fmt.Errorf("query characters: %w", err)
}
defer rows.Close()
var chars []*CharacterData
for rows.Next() {
c := &CharacterData{}
if err := rows.Scan(
&c.ID, &c.AccountID, &c.Name, &c.Level, &c.Exp,
&c.HP, &c.MaxHP, &c.MP, &c.MaxMP,
&c.Str, &c.Dex, &c.IntStat,
&c.ZoneID, &c.PosX, &c.PosY, &c.PosZ, &c.Rotation,
); err != nil {
return nil, fmt.Errorf("scan character: %w", err)
}
chars = append(chars, c)
}
return chars, nil
}
// GetByID loads a single character.
func (r *CharacterRepo) GetByID(ctx context.Context, id int64) (*CharacterData, error) {
c := &CharacterData{}
err := r.pool.QueryRow(ctx,
`SELECT id, account_id, name, level, exp, hp, max_hp, mp, max_mp, str, dex, int_stat,
zone_id, pos_x, pos_y, pos_z, rotation
FROM characters WHERE id = $1`, id,
).Scan(
&c.ID, &c.AccountID, &c.Name, &c.Level, &c.Exp,
&c.HP, &c.MaxHP, &c.MP, &c.MaxMP,
&c.Str, &c.Dex, &c.IntStat,
&c.ZoneID, &c.PosX, &c.PosY, &c.PosZ, &c.Rotation,
)
if err != nil {
return nil, fmt.Errorf("get character %d: %w", id, err)
}
return c, nil
}
// Save persists the current character state.
func (r *CharacterRepo) Save(ctx context.Context, c *CharacterData) error {
_, err := r.pool.Exec(ctx,
`UPDATE characters SET
level = $2, exp = $3, hp = $4, max_hp = $5, mp = $6, max_mp = $7,
str = $8, dex = $9, int_stat = $10,
zone_id = $11, pos_x = $12, pos_y = $13, pos_z = $14, rotation = $15,
updated_at = NOW()
WHERE id = $1`,
c.ID, c.Level, c.Exp, c.HP, c.MaxHP, c.MP, c.MaxMP,
c.Str, c.Dex, c.IntStat,
c.ZoneID, c.PosX, c.PosY, c.PosZ, c.Rotation,
)
if err != nil {
return fmt.Errorf("save character %d: %w", c.ID, err)
}
return nil
}
// SaveBatch saves multiple characters in a single transaction.
func (r *CharacterRepo) SaveBatch(ctx context.Context, chars []*CharacterData) error {
if len(chars) == 0 {
return nil
}
tx, err := r.pool.Begin(ctx)
if err != nil {
return fmt.Errorf("begin tx: %w", err)
}
defer tx.Rollback(ctx)
for _, c := range chars {
_, err := tx.Exec(ctx,
`UPDATE characters SET
level = $2, exp = $3, hp = $4, max_hp = $5, mp = $6, max_mp = $7,
str = $8, dex = $9, int_stat = $10,
zone_id = $11, pos_x = $12, pos_y = $13, pos_z = $14, rotation = $15,
updated_at = NOW()
WHERE id = $1`,
c.ID, c.Level, c.Exp, c.HP, c.MaxHP, c.MP, c.MaxMP,
c.Str, c.Dex, c.IntStat,
c.ZoneID, c.PosX, c.PosY, c.PosZ, c.Rotation,
)
if err != nil {
return fmt.Errorf("save character %d in batch: %w", c.ID, err)
}
}
return tx.Commit(ctx)
}