mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-05-07 06:44:31 +02:00
chore(merge): merge develop into main for 9.4.0 cycle
Brings 53 develop commits (i18n, Diva, campaign, guild invites, save transfer, return/rookie guilds, hunting tournament, JSON quest/scenario loaders, Ghidra-derived user binary parsing, and misc fixes) onto main now that 9.3.2 has been tagged and released. Resolves two overlap zones: 1. Migration number collision. Main shipped 0010_fix_zero_rasta_id and 0011_fix_stale_boost_time in 9.3.2; develop had independently numbered 0010_campaign..0015_tournament. The migration runner keys applied versions by integer, so coexisting files with the same numeric prefix would silently skip each other. Develop's files have been renumbered to 0016..0021, leaving main's 0010/0011 intact. A schema_version rename script is required on any server that had already applied the old develop numbers (only frontier.mogapedia.fr at the time of this merge). 2. CHANGELOG.md. Develop's in-progress feature entries move into [Unreleased] with updated migration references; the [9.3.2] section is preserved verbatim. main.go version string bumped to 9.4.0-dev to mark the new cycle. Full test suite (go test -race ./...) passes.
This commit is contained in:
@@ -2,6 +2,7 @@ package channelserver
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
@@ -49,10 +50,24 @@ func (r *CharacterRepository) LoadColumn(charID uint32, column string) ([]byte,
|
||||
return data, err
|
||||
}
|
||||
|
||||
// ErrCharacterNotFound is returned by write methods when no character row is matched.
|
||||
var ErrCharacterNotFound = errors.New("character not found")
|
||||
|
||||
// SaveColumn writes a single []byte column by character ID.
|
||||
// Returns ErrCharacterNotFound if no row was updated (character does not exist).
|
||||
func (r *CharacterRepository) SaveColumn(charID uint32, column string, data []byte) error {
|
||||
_, err := r.db.Exec("UPDATE characters SET "+column+"=$1 WHERE id=$2", data, charID)
|
||||
return err
|
||||
result, err := r.db.Exec("UPDATE characters SET "+column+"=$1 WHERE id=$2", data, charID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("SaveColumn %s for char %d: %w", column, charID, ErrCharacterNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReadInt reads a single integer column (0 for NULL) by character ID.
|
||||
@@ -226,13 +241,26 @@ func (r *CharacterRepository) ReadGuildPostChecked(charID uint32) (time.Time, er
|
||||
// When rastaID is 0, only the mercenary blob is saved — the existing rasta_id
|
||||
// (typically NULL for characters without a mercenary) is preserved. Writing 0
|
||||
// would pollute GetMercenaryLoans queries that match on pact_id.
|
||||
// Returns ErrCharacterNotFound if no row was updated.
|
||||
func (r *CharacterRepository) SaveMercenary(charID uint32, data []byte, rastaID uint32) error {
|
||||
var result sql.Result
|
||||
var err error
|
||||
if rastaID == 0 {
|
||||
_, err := r.db.Exec("UPDATE characters SET savemercenary=$1 WHERE id=$2", data, charID)
|
||||
result, err = r.db.Exec("UPDATE characters SET savemercenary=$1 WHERE id=$2", data, charID)
|
||||
} else {
|
||||
result, err = r.db.Exec("UPDATE characters SET savemercenary=$1, rasta_id=$2 WHERE id=$3", data, rastaID, charID)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err := r.db.Exec("UPDATE characters SET savemercenary=$1, rasta_id=$2 WHERE id=$3", data, rastaID, charID)
|
||||
return err
|
||||
n, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if n == 0 {
|
||||
return fmt.Errorf("SaveMercenary for char %d: %w", charID, ErrCharacterNotFound)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateGCPAndPact updates gcp and pact_id atomically.
|
||||
@@ -241,6 +269,38 @@ func (r *CharacterRepository) UpdateGCPAndPact(charID uint32, gcp uint32, pactID
|
||||
return err
|
||||
}
|
||||
|
||||
// SavedataBackup holds one row from the savedata_backups table.
|
||||
type SavedataBackup struct {
|
||||
Slot int
|
||||
Data []byte
|
||||
SavedAt time.Time
|
||||
}
|
||||
|
||||
// LoadBackupsByRecency returns all backup slots for a character, ordered
|
||||
// most-recent first. Returns an empty (non-nil) slice if no backups exist.
|
||||
func (r *CharacterRepository) LoadBackupsByRecency(charID uint32) ([]SavedataBackup, error) {
|
||||
rows, err := r.db.Query(
|
||||
`SELECT slot, savedata, saved_at FROM savedata_backups
|
||||
WHERE char_id = $1
|
||||
ORDER BY saved_at DESC`,
|
||||
charID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close() //nolint:errcheck // rows.Close error is non-actionable here
|
||||
|
||||
backups := make([]SavedataBackup, 0)
|
||||
for rows.Next() {
|
||||
var b SavedataBackup
|
||||
if err := rows.Scan(&b.Slot, &b.Data, &b.SavedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backups = append(backups, b)
|
||||
}
|
||||
return backups, rows.Err()
|
||||
}
|
||||
|
||||
// SaveBackup upserts a savedata snapshot into the rotating backup table.
|
||||
func (r *CharacterRepository) SaveBackup(charID uint32, slot int, data []byte) error {
|
||||
_, err := r.db.Exec(`
|
||||
|
||||
Reference in New Issue
Block a user