diff --git a/server/channelserver/handlers_character.go b/server/channelserver/handlers_character.go index a9cdfc7da..a0cf83348 100644 --- a/server/channelserver/handlers_character.go +++ b/server/channelserver/handlers_character.go @@ -75,14 +75,11 @@ func (save *CharacterSaveData) Save(s *Session) { save.compSave = save.decompSave } - _, err := s.server.db.Exec(`UPDATE characters SET savedata=$1, is_new_character=false, hr=$2, gr=$3, is_female=$4, weapon_type=$5, weapon_id=$6 WHERE id=$7 - `, save.compSave, save.HR, save.GR, save.Gender, save.WeaponType, save.WeaponID, save.CharID) - if err != nil { + if err := s.server.charRepo.SaveCharacterData(save.CharID, save.compSave, save.HR, save.GR, save.Gender, save.WeaponType, save.WeaponID); err != nil { s.logger.Error("Failed to update savedata", zap.Error(err), zap.Uint32("charID", save.CharID)) } - if _, err := s.server.db.Exec(`UPDATE user_binary SET house_tier=$1, house_data=$2, bookshelf=$3, gallery=$4, tore=$5, garden=$6 WHERE id=$7 - `, save.HouseTier, save.HouseData, save.BookshelfData, save.GalleryData, save.ToreData, save.GardenData, s.charID); err != nil { + if err := s.server.charRepo.SaveHouseData(s.charID, save.HouseTier, save.HouseData, save.BookshelfData, save.GalleryData, save.ToreData, save.GardenData); err != nil { s.logger.Error("Failed to update user binary house data", zap.Error(err)) } } diff --git a/server/channelserver/handlers_diva.go b/server/channelserver/handlers_diva.go index 7a54e7a32..78847b2e3 100644 --- a/server/channelserver/handlers_diva.go +++ b/server/channelserver/handlers_diva.go @@ -20,7 +20,7 @@ const ( ) func cleanupDiva(s *Session) { - if _, err := s.server.db.Exec("DELETE FROM events WHERE event_type='diva'"); err != nil { + if err := s.server.divaRepo.DeleteEvents(); err != nil { s.logger.Error("Failed to delete diva events", zap.Error(err)) } } @@ -59,7 +59,7 @@ func generateDivaTimestamps(s *Session, start uint32, debug bool) []uint32 { cleanupDiva(s) // Generate a new diva defense, starting midnight tomorrow start = uint32(midnight.Add(24 * time.Hour).Unix()) - if _, err := s.server.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('diva', to_timestamp($1)::timestamp without time zone)", start); err != nil { + if err := s.server.divaRepo.InsertEvent(start); err != nil { s.logger.Error("Failed to insert diva event", zap.Error(err)) } } @@ -78,7 +78,7 @@ func handleMsgMhfGetUdSchedule(s *Session, p mhfpacket.MHFPacket) { const divaIDSentinel = uint32(0xCAFEBEEF) id, start := divaIDSentinel, uint32(0) - rows, err := s.server.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'") + rows, err := s.server.divaRepo.GetEvents() if err != nil { s.logger.Error("Failed to query diva schedule", zap.Error(err)) } else { diff --git a/server/channelserver/handlers_goocoo.go b/server/channelserver/handlers_goocoo.go index 9cef971b2..6d2001d06 100644 --- a/server/channelserver/handlers_goocoo.go +++ b/server/channelserver/handlers_goocoo.go @@ -9,17 +9,16 @@ import ( ) func getGoocooData(s *Session, cid uint32) [][]byte { - var goocoo []byte var goocoos [][]byte - for i := 0; i < 5; i++ { - err := s.server.db.QueryRow(fmt.Sprintf("SELECT goocoo%d FROM goocoo WHERE id=$1", i), cid).Scan(&goocoo) + for i := uint32(0); i < 5; i++ { + goocoo, err := s.server.goocooRepo.GetSlot(cid, i) if err != nil { - if _, err := s.server.db.Exec("INSERT INTO goocoo (id) VALUES ($1)", s.charID); err != nil { + if err := s.server.goocooRepo.EnsureExists(s.charID); err != nil { s.logger.Error("Failed to insert goocoo record", zap.Error(err)) } return goocoos } - if err == nil && goocoo != nil { + if goocoo != nil { goocoos = append(goocoos, goocoo) } } @@ -45,7 +44,7 @@ func handleMsgMhfUpdateGuacot(s *Session, p mhfpacket.MHFPacket) { continue } if goocoo.Data1[0] == 0 { - if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE goocoo SET goocoo%d=NULL WHERE id=$1", goocoo.Index), s.charID); err != nil { + if err := s.server.goocooRepo.ClearSlot(s.charID, goocoo.Index); err != nil { s.logger.Error("Failed to clear goocoo slot", zap.Error(err)) } } else { @@ -59,7 +58,7 @@ func handleMsgMhfUpdateGuacot(s *Session, p mhfpacket.MHFPacket) { } bf.WriteUint8(uint8(len(goocoo.Name))) bf.WriteBytes(goocoo.Name) - if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE goocoo SET goocoo%d=$1 WHERE id=$2", goocoo.Index), bf.Data(), s.charID); err != nil { + if err := s.server.goocooRepo.SaveSlot(s.charID, goocoo.Index, bf.Data()); err != nil { s.logger.Error("Failed to update goocoo slot", zap.Error(err)) } dumpSaveData(s, bf.Data(), fmt.Sprintf("goocoo-%d", goocoo.Index)) diff --git a/server/channelserver/handlers_mercenary.go b/server/channelserver/handlers_mercenary.go index 71a974c8b..05af0ef8f 100644 --- a/server/channelserver/handlers_mercenary.go +++ b/server/channelserver/handlers_mercenary.go @@ -168,8 +168,8 @@ func handleMsgMhfEnumerateMercenaryLog(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfCreateMercenary(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCreateMercenary) - var nextID uint32 - if err := s.server.db.QueryRow("SELECT nextval('rasta_id_seq')").Scan(&nextID); err != nil { + nextID, err := s.server.mercenaryRepo.NextRastaID() + if err != nil { s.logger.Error("Failed to get next rasta ID", zap.Error(err)) doAckSimpleFail(s, pkt.AckHandle, nil) return @@ -227,7 +227,7 @@ func handleMsgMhfReadMercenaryW(s *Session, p mhfpacket.MHFPacket) { if pkt.Op != 2 && pkt.Op != 5 { var loans uint8 temp := byteframe.NewByteFrame() - rows, err := s.server.db.Query("SELECT name, id, pact_id FROM characters WHERE pact_id=(SELECT rasta_id FROM characters WHERE id=$1)", s.charID) + rows, err := s.server.mercenaryRepo.GetMercenaryLoans(s.charID) if err != nil { s.logger.Error("Failed to query mercenary loans", zap.Error(err)) } else { @@ -323,7 +323,8 @@ func handleMsgMhfSaveOtomoAirou(s *Session, p mhfpacket.MHFPacket) { dataLen := bf.ReadUint32() catID := bf.ReadUint32() if catID == 0 { - if err := s.server.db.QueryRow("SELECT nextval('airou_id_seq')").Scan(&catID); err != nil { + catID, err = s.server.mercenaryRepo.NextAirouID() + if err != nil { s.logger.Error("Failed to get next airou ID", zap.Error(err)) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return @@ -392,9 +393,7 @@ func getGuildAirouList(s *Session) []Airou { if err != nil { return guildCats } - rows, err := s.server.db.Query(`SELECT cats_used FROM guild_hunts gh - INNER JOIN characters c ON gh.host_id = c.id WHERE c.id=$1 - `, s.charID) + rows, err := s.server.mercenaryRepo.GetGuildHuntCatsUsed(s.charID) if err != nil { s.logger.Warn("Failed to get recently used airous", zap.Error(err)) return guildCats @@ -414,10 +413,7 @@ func getGuildAirouList(s *Session) []Airou { } } - rows, err = s.server.db.Query(`SELECT c.otomoairou FROM characters c - INNER JOIN guild_characters gc ON gc.character_id = c.id - WHERE gc.guild_id = $1 AND c.otomoairou IS NOT NULL - ORDER BY c.id LIMIT 60`, guild.ID) + rows, err = s.server.mercenaryRepo.GetGuildAirou(guild.ID) if err != nil { s.logger.Warn("Selecting otomoairou based on guild failed", zap.Error(err)) return guildCats diff --git a/server/channelserver/handlers_misc.go b/server/channelserver/handlers_misc.go index 00bc40d50..8ddf733a8 100644 --- a/server/channelserver/handlers_misc.go +++ b/server/channelserver/handlers_misc.go @@ -259,7 +259,7 @@ func handleMsgMhfGetTrendWeapon(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetTrendWeapon) trendWeapons := [14][3]TrendWeapon{} for i := uint8(0); i < 14; i++ { - rows, err := s.server.db.Query(`SELECT weapon_id FROM trend_weapons WHERE weapon_type=$1 ORDER BY count DESC LIMIT 3`, i) + rows, err := s.server.miscRepo.GetTrendWeapons(i) if err != nil { continue } @@ -288,8 +288,7 @@ func handleMsgMhfGetTrendWeapon(s *Session, p mhfpacket.MHFPacket) { func handleMsgMhfUpdateUseTrendWeaponLog(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUpdateUseTrendWeaponLog) - if _, err := s.server.db.Exec(`INSERT INTO trend_weapons (weapon_id, weapon_type, count) VALUES ($1, $2, 1) ON CONFLICT (weapon_id) DO - UPDATE SET count = trend_weapons.count+1`, pkt.WeaponID, pkt.WeaponType); err != nil { + if err := s.server.miscRepo.UpsertTrendWeapon(pkt.WeaponID, pkt.WeaponType); err != nil { s.logger.Error("Failed to update trend weapon log", zap.Error(err)) } doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) diff --git a/server/channelserver/handlers_scenario.go b/server/channelserver/handlers_scenario.go index 307afa3a5..622350396 100644 --- a/server/channelserver/handlers_scenario.go +++ b/server/channelserver/handlers_scenario.go @@ -22,7 +22,7 @@ func handleMsgMhfInfoScenarioCounter(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfInfoScenarioCounter) var scenarios []Scenario var scenario Scenario - scenarioData, err := s.server.db.Queryx("SELECT scenario_id, category_id FROM scenario_counter") + scenarioData, err := s.server.scenarioRepo.GetCounters() if err != nil { _ = scenarioData.Close() s.logger.Error("Failed to get scenario counter info from db", zap.Error(err)) diff --git a/server/channelserver/repo_character.go b/server/channelserver/repo_character.go index 692c13e18..c118911bd 100644 --- a/server/channelserver/repo_character.go +++ b/server/channelserver/repo_character.go @@ -210,3 +210,17 @@ func (r *CharacterRepository) FindByRastaID(rastaID int) (charID uint32, name st err = r.db.QueryRow("SELECT name, id FROM characters WHERE rasta_id=$1", rastaID).Scan(&name, &charID) return } + +// SaveCharacterData updates the core save fields on a character. +func (r *CharacterRepository) SaveCharacterData(charID uint32, compSave []byte, hr, gr uint16, isFemale bool, weaponType uint8, weaponID uint16) error { + _, err := r.db.Exec(`UPDATE characters SET savedata=$1, is_new_character=false, hr=$2, gr=$3, is_female=$4, weapon_type=$5, weapon_id=$6 WHERE id=$7`, + compSave, hr, gr, isFemale, weaponType, weaponID, charID) + return err +} + +// SaveHouseData updates house-related fields in user_binary. +func (r *CharacterRepository) SaveHouseData(charID uint32, houseTier []byte, houseData, bookshelf, gallery, tore, garden []byte) error { + _, err := r.db.Exec(`UPDATE user_binary SET house_tier=$1, house_data=$2, bookshelf=$3, gallery=$4, tore=$5, garden=$6 WHERE id=$7`, + houseTier, houseData, bookshelf, gallery, tore, garden, charID) + return err +} diff --git a/server/channelserver/repo_diva.go b/server/channelserver/repo_diva.go new file mode 100644 index 000000000..923ced8dd --- /dev/null +++ b/server/channelserver/repo_diva.go @@ -0,0 +1,32 @@ +package channelserver + +import ( + "github.com/jmoiron/sqlx" +) + +// DivaRepository centralizes all database access for diva defense events. +type DivaRepository struct { + db *sqlx.DB +} + +// NewDivaRepository creates a new DivaRepository. +func NewDivaRepository(db *sqlx.DB) *DivaRepository { + return &DivaRepository{db: db} +} + +// DeleteEvents removes all diva events. +func (r *DivaRepository) DeleteEvents() error { + _, err := r.db.Exec("DELETE FROM events WHERE event_type='diva'") + return err +} + +// InsertEvent creates a new diva event with the given start epoch. +func (r *DivaRepository) InsertEvent(startEpoch uint32) error { + _, err := r.db.Exec("INSERT INTO events (event_type, start_time) VALUES ('diva', to_timestamp($1)::timestamp without time zone)", startEpoch) + return err +} + +// GetEvents returns all diva events with their ID and start_time epoch. +func (r *DivaRepository) GetEvents() (*sqlx.Rows, error) { + return r.db.Queryx("SELECT id, (EXTRACT(epoch FROM start_time)::int) as start_time FROM events WHERE event_type='diva'") +} diff --git a/server/channelserver/repo_goocoo.go b/server/channelserver/repo_goocoo.go new file mode 100644 index 000000000..dc9c072da --- /dev/null +++ b/server/channelserver/repo_goocoo.go @@ -0,0 +1,59 @@ +package channelserver + +import ( + "fmt" + + "github.com/jmoiron/sqlx" +) + +// GoocooRepository centralizes all database access for the goocoo table. +type GoocooRepository struct { + db *sqlx.DB +} + +// NewGoocooRepository creates a new GoocooRepository. +func NewGoocooRepository(db *sqlx.DB) *GoocooRepository { + return &GoocooRepository{db: db} +} + +// validGoocooSlot validates the slot index to prevent SQL injection. +func validGoocooSlot(slot uint32) error { + if slot > 4 { + return fmt.Errorf("invalid goocoo slot index: %d", slot) + } + return nil +} + +// EnsureExists creates a goocoo record if it doesn't already exist. +func (r *GoocooRepository) EnsureExists(charID uint32) error { + _, err := r.db.Exec("INSERT INTO goocoo (id) VALUES ($1) ON CONFLICT DO NOTHING", charID) + return err +} + +// GetSlot reads a single goocoo slot by character ID and slot index (0-4). +func (r *GoocooRepository) GetSlot(charID uint32, slot uint32) ([]byte, error) { + if err := validGoocooSlot(slot); err != nil { + return nil, err + } + var data []byte + err := r.db.QueryRow(fmt.Sprintf("SELECT goocoo%d FROM goocoo WHERE id=$1", slot), charID).Scan(&data) + return data, err +} + +// ClearSlot sets a goocoo slot to NULL. +func (r *GoocooRepository) ClearSlot(charID uint32, slot uint32) error { + if err := validGoocooSlot(slot); err != nil { + return err + } + _, err := r.db.Exec(fmt.Sprintf("UPDATE goocoo SET goocoo%d=NULL WHERE id=$1", slot), charID) + return err +} + +// SaveSlot writes data to a goocoo slot. +func (r *GoocooRepository) SaveSlot(charID uint32, slot uint32, data []byte) error { + if err := validGoocooSlot(slot); err != nil { + return err + } + _, err := r.db.Exec(fmt.Sprintf("UPDATE goocoo SET goocoo%d=$1 WHERE id=$2", slot), data, charID) + return err +} diff --git a/server/channelserver/repo_mercenary.go b/server/channelserver/repo_mercenary.go new file mode 100644 index 000000000..ef7e247f3 --- /dev/null +++ b/server/channelserver/repo_mercenary.go @@ -0,0 +1,50 @@ +package channelserver + +import ( + "database/sql" + + "github.com/jmoiron/sqlx" +) + +// MercenaryRepository centralizes database access for mercenary/rasta/airou sequences and queries. +type MercenaryRepository struct { + db *sqlx.DB +} + +// NewMercenaryRepository creates a new MercenaryRepository. +func NewMercenaryRepository(db *sqlx.DB) *MercenaryRepository { + return &MercenaryRepository{db: db} +} + +// NextRastaID returns the next value from the rasta_id_seq sequence. +func (r *MercenaryRepository) NextRastaID() (uint32, error) { + var id uint32 + err := r.db.QueryRow("SELECT nextval('rasta_id_seq')").Scan(&id) + return id, err +} + +// NextAirouID returns the next value from the airou_id_seq sequence. +func (r *MercenaryRepository) NextAirouID() (uint32, error) { + var id uint32 + err := r.db.QueryRow("SELECT nextval('airou_id_seq')").Scan(&id) + return id, err +} + +// GetMercenaryLoans returns characters that have a pact with the given character's rasta_id. +func (r *MercenaryRepository) GetMercenaryLoans(charID uint32) (*sql.Rows, error) { + return r.db.Query("SELECT name, id, pact_id FROM characters WHERE pact_id=(SELECT rasta_id FROM characters WHERE id=$1)", charID) +} + +// GetGuildHuntCatsUsed returns cats_used and start from guild_hunts for a given character. +func (r *MercenaryRepository) GetGuildHuntCatsUsed(charID uint32) (*sql.Rows, error) { + return r.db.Query(`SELECT cats_used, start FROM guild_hunts gh + INNER JOIN characters c ON gh.host_id = c.id WHERE c.id=$1`, charID) +} + +// GetGuildAirou returns otomoairou data for all characters in a guild. +func (r *MercenaryRepository) GetGuildAirou(guildID uint32) (*sql.Rows, error) { + return r.db.Query(`SELECT c.otomoairou FROM characters c + INNER JOIN guild_characters gc ON gc.character_id = c.id + WHERE gc.guild_id = $1 AND c.otomoairou IS NOT NULL + ORDER BY c.id LIMIT 60`, guildID) +} diff --git a/server/channelserver/repo_misc.go b/server/channelserver/repo_misc.go new file mode 100644 index 000000000..d3836e6cd --- /dev/null +++ b/server/channelserver/repo_misc.go @@ -0,0 +1,29 @@ +package channelserver + +import ( + "database/sql" + + "github.com/jmoiron/sqlx" +) + +// MiscRepository centralizes database access for miscellaneous game tables. +type MiscRepository struct { + db *sqlx.DB +} + +// NewMiscRepository creates a new MiscRepository. +func NewMiscRepository(db *sqlx.DB) *MiscRepository { + return &MiscRepository{db: db} +} + +// GetTrendWeapons returns the top 3 weapon IDs for a given weapon type, ordered by count descending. +func (r *MiscRepository) GetTrendWeapons(weaponType uint8) (*sql.Rows, error) { + return r.db.Query("SELECT weapon_id FROM trend_weapons WHERE weapon_type=$1 ORDER BY count DESC LIMIT 3", weaponType) +} + +// UpsertTrendWeapon increments the count for a weapon, inserting it if it doesn't exist. +func (r *MiscRepository) UpsertTrendWeapon(weaponID uint16, weaponType uint8) error { + _, err := r.db.Exec(`INSERT INTO trend_weapons (weapon_id, weapon_type, count) VALUES ($1, $2, 1) ON CONFLICT (weapon_id) DO + UPDATE SET count = trend_weapons.count+1`, weaponID, weaponType) + return err +} diff --git a/server/channelserver/repo_scenario.go b/server/channelserver/repo_scenario.go new file mode 100644 index 000000000..003f52371 --- /dev/null +++ b/server/channelserver/repo_scenario.go @@ -0,0 +1,20 @@ +package channelserver + +import ( + "github.com/jmoiron/sqlx" +) + +// ScenarioRepository centralizes all database access for the scenario_counter table. +type ScenarioRepository struct { + db *sqlx.DB +} + +// NewScenarioRepository creates a new ScenarioRepository. +func NewScenarioRepository(db *sqlx.DB) *ScenarioRepository { + return &ScenarioRepository{db: db} +} + +// GetCounters returns all scenario counters. +func (r *ScenarioRepository) GetCounters() (*sqlx.Rows, error) { + return r.db.Queryx("SELECT scenario_id, category_id FROM scenario_counter") +} diff --git a/server/channelserver/sys_channel_server.go b/server/channelserver/sys_channel_server.go index 6316109ef..3583c40ad 100644 --- a/server/channelserver/sys_channel_server.go +++ b/server/channelserver/sys_channel_server.go @@ -62,6 +62,11 @@ type Server struct { achievementRepo *AchievementRepository shopRepo *ShopRepository cafeRepo *CafeRepository + goocooRepo *GoocooRepository + divaRepo *DivaRepository + miscRepo *MiscRepository + scenarioRepo *ScenarioRepository + mercenaryRepo *MercenaryRepository erupeConfig *cfg.Config acceptConns chan net.Conn deleteConns chan net.Conn @@ -148,6 +153,11 @@ func NewServer(config *Config) *Server { 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) // Mezeporta s.stages["sl1Ns200p0a0u0"] = NewStage("sl1Ns200p0a0u0")