From ed2a9597f222b07713e9c74fc690064adbe1089c Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Wed, 18 Feb 2026 18:24:36 +0100 Subject: [PATCH] refactor(channelserver): extract guild model, chat commands, and seibattle Split three large files into focused modules: - handlers_guild.go: extract types/ORM into guild_model.go - handlers_cast_binary.go: extract command parser into handlers_commands.go - handlers.go: move seibattle types/handlers into handlers_seibattle.go --- server/channelserver/guild_model.go | 628 +++++++++++++++++++ server/channelserver/handlers.go | 143 ----- server/channelserver/handlers_cast_binary.go | 412 ------------ server/channelserver/handlers_commands.go | 424 +++++++++++++ server/channelserver/handlers_guild.go | 621 +----------------- server/channelserver/handlers_seibattle.go | 144 +++++ 6 files changed, 1197 insertions(+), 1175 deletions(-) create mode 100644 server/channelserver/guild_model.go create mode 100644 server/channelserver/handlers_commands.go diff --git a/server/channelserver/guild_model.go b/server/channelserver/guild_model.go new file mode 100644 index 000000000..d484f4ce6 --- /dev/null +++ b/server/channelserver/guild_model.go @@ -0,0 +1,628 @@ +package channelserver + +import ( + "database/sql" + "database/sql/driver" + "encoding/json" + "errors" + "erupe-ce/common/mhfitem" + _config "erupe-ce/config" + "fmt" + "time" + + "erupe-ce/common/byteframe" + "github.com/jmoiron/sqlx" + "go.uber.org/zap" +) + +type FestivalColor string + +const ( + FestivalColorNone FestivalColor = "none" + FestivalColorBlue FestivalColor = "blue" + FestivalColorRed FestivalColor = "red" +) + +var FestivalColorCodes = map[FestivalColor]int16{ + FestivalColorNone: -1, + FestivalColorBlue: 0, + FestivalColorRed: 1, +} + +type GuildApplicationType string + +const ( + GuildApplicationTypeApplied GuildApplicationType = "applied" + GuildApplicationTypeInvited GuildApplicationType = "invited" +) + +type Guild struct { + ID uint32 `db:"id"` + Name string `db:"name"` + MainMotto uint8 `db:"main_motto"` + SubMotto uint8 `db:"sub_motto"` + CreatedAt time.Time `db:"created_at"` + MemberCount uint16 `db:"member_count"` + RankRP uint32 `db:"rank_rp"` + EventRP uint32 `db:"event_rp"` + RoomRP uint16 `db:"room_rp"` + RoomExpiry time.Time `db:"room_expiry"` + Comment string `db:"comment"` + PugiName1 string `db:"pugi_name_1"` + PugiName2 string `db:"pugi_name_2"` + PugiName3 string `db:"pugi_name_3"` + PugiOutfit1 uint8 `db:"pugi_outfit_1"` + PugiOutfit2 uint8 `db:"pugi_outfit_2"` + PugiOutfit3 uint8 `db:"pugi_outfit_3"` + PugiOutfits uint32 `db:"pugi_outfits"` + Recruiting bool `db:"recruiting"` + FestivalColor FestivalColor `db:"festival_color"` + Souls uint32 `db:"souls"` + AllianceID uint32 `db:"alliance_id"` + Icon *GuildIcon `db:"icon"` + + GuildLeader +} + +type GuildLeader struct { + LeaderCharID uint32 `db:"leader_id"` + LeaderName string `db:"leader_name"` +} + +type GuildIconPart struct { + Index uint16 + ID uint16 + Page uint8 + Size uint8 + Rotation uint8 + Red uint8 + Green uint8 + Blue uint8 + PosX uint16 + PosY uint16 +} + +type GuildApplication struct { + ID int `db:"id"` + GuildID uint32 `db:"guild_id"` + CharID uint32 `db:"character_id"` + ActorID uint32 `db:"actor_id"` + ApplicationType GuildApplicationType `db:"application_type"` + CreatedAt time.Time `db:"created_at"` +} + +type GuildIcon struct { + Parts []GuildIconPart +} + +func (gi *GuildIcon) Scan(val interface{}) (err error) { + switch v := val.(type) { + case []byte: + err = json.Unmarshal(v, &gi) + case string: + err = json.Unmarshal([]byte(v), &gi) + } + + return +} + +func (gi *GuildIcon) Value() (valuer driver.Value, err error) { + return json.Marshal(gi) +} + +func (g *Guild) Rank() uint16 { + rpMap := []uint32{ + 24, 48, 96, 144, 192, 240, 288, 360, 432, + 504, 600, 696, 792, 888, 984, 1080, 1200, + } + if _config.ErupeConfig.RealClientMode <= _config.Z2 { + rpMap = []uint32{ + 3500, 6000, 8500, 11000, 13500, 16000, 20000, 24000, 28000, + 33000, 38000, 43000, 48000, 55000, 70000, 90000, 120000, + } + } + for i, u := range rpMap { + if g.RankRP < u { + if _config.ErupeConfig.RealClientMode <= _config.S6 && i >= 12 { + return 12 + } else if _config.ErupeConfig.RealClientMode <= _config.F5 && i >= 13 { + return 13 + } else if _config.ErupeConfig.RealClientMode <= _config.G32 && i >= 14 { + return 14 + } + return uint16(i) + } + } + if _config.ErupeConfig.RealClientMode <= _config.S6 { + return 12 + } else if _config.ErupeConfig.RealClientMode <= _config.F5 { + return 13 + } else if _config.ErupeConfig.RealClientMode <= _config.G32 { + return 14 + } + return 17 +} + +const guildInfoSelectQuery = ` +SELECT + g.id, + g.name, + rank_rp, + event_rp, + room_rp, + COALESCE(room_expiry, '1970-01-01') AS room_expiry, + main_motto, + sub_motto, + created_at, + leader_id, + c.name AS leader_name, + comment, + COALESCE(pugi_name_1, '') AS pugi_name_1, + COALESCE(pugi_name_2, '') AS pugi_name_2, + COALESCE(pugi_name_3, '') AS pugi_name_3, + pugi_outfit_1, + pugi_outfit_2, + pugi_outfit_3, + pugi_outfits, + recruiting, + COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_color, + COALESCE((SELECT SUM(fs.souls) FROM festa_submissions fs WHERE fs.guild_id=g.id), 0) AS souls, + COALESCE(( + SELECT id FROM guild_alliances ga WHERE + ga.parent_id = g.id OR + ga.sub1_id = g.id OR + ga.sub2_id = g.id + ), 0) AS alliance_id, + icon, + (SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count + FROM guilds g + JOIN guild_characters gc ON gc.character_id = leader_id + JOIN characters c on leader_id = c.id +` + +func (guild *Guild) Save(s *Session) error { + _, err := s.server.db.Exec(` + UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, + pugi_outfit_1=$8, pugi_outfit_2=$9, pugi_outfit_3=$10, pugi_outfits=$11, icon=$12, leader_id=$13 WHERE id=$1 + `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, + guild.PugiOutfit1, guild.PugiOutfit2, guild.PugiOutfit3, guild.PugiOutfits, guild.Icon, guild.LeaderCharID) + + if err != nil { + s.logger.Error("failed to update guild data", zap.Error(err), zap.Uint32("guildID", guild.ID)) + return err + } + + return nil +} + +func (guild *Guild) CreateApplication(s *Session, charID uint32, applicationType GuildApplicationType, transaction *sql.Tx) error { + + query := ` + INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) + VALUES ($1, $2, $3, $4) + ` + + var err error + + if transaction == nil { + _, err = s.server.db.Exec(query, guild.ID, charID, s.charID, applicationType) + } else { + _, err = transaction.Exec(query, guild.ID, charID, s.charID, applicationType) + } + + if err != nil { + s.logger.Error( + "failed to add guild application", + zap.Error(err), + zap.Uint32("guildID", guild.ID), + zap.Uint32("charID", charID), + ) + return err + } + + return nil +} + +func (guild *Guild) Disband(s *Session) error { + transaction, err := s.server.db.Begin() + + if err != nil { + s.logger.Error("failed to begin transaction", zap.Error(err)) + return err + } + + _, err = transaction.Exec("DELETE FROM guild_characters WHERE guild_id = $1", guild.ID) + + if err != nil { + s.logger.Error("failed to remove guild characters", zap.Error(err), zap.Uint32("guildId", guild.ID)) + rollbackTransaction(s, transaction) + return err + } + + _, err = transaction.Exec("DELETE FROM guilds WHERE id = $1", guild.ID) + + if err != nil { + s.logger.Error("failed to remove guild", zap.Error(err), zap.Uint32("guildID", guild.ID)) + rollbackTransaction(s, transaction) + return err + } + + _, err = transaction.Exec("DELETE FROM guild_alliances WHERE parent_id=$1", guild.ID) + + if err != nil { + s.logger.Error("failed to remove guild alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) + rollbackTransaction(s, transaction) + return err + } + + _, err = transaction.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guild.ID) + + if err != nil { + s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) + rollbackTransaction(s, transaction) + return err + } + + _, err = transaction.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guild.ID) + + if err != nil { + s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) + rollbackTransaction(s, transaction) + return err + } + + err = transaction.Commit() + + if err != nil { + s.logger.Error("failed to commit transaction", zap.Error(err)) + return err + } + + s.logger.Info("Character disbanded guild", zap.Uint32("charID", s.charID), zap.Uint32("guildID", guild.ID)) + + return nil +} + +func (guild *Guild) RemoveCharacter(s *Session, charID uint32) error { + _, err := s.server.db.Exec("DELETE FROM guild_characters WHERE character_id=$1", charID) + + if err != nil { + s.logger.Error( + "failed to remove character from guild", + zap.Error(err), + zap.Uint32("charID", charID), + zap.Uint32("guildID", guild.ID), + ) + + return err + } + + return nil +} + +func (guild *Guild) AcceptApplication(s *Session, charID uint32) error { + transaction, err := s.server.db.Begin() + + if err != nil { + s.logger.Error("failed to start db transaction", zap.Error(err)) + return err + } + + _, err = transaction.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID) + + if err != nil { + s.logger.Error("failed to accept character's guild application", zap.Error(err)) + rollbackTransaction(s, transaction) + return err + } + + _, err = transaction.Exec(` + INSERT INTO guild_characters (guild_id, character_id, order_index) + VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1)) + `, guild.ID, charID) + + if err != nil { + s.logger.Error( + "failed to add applicant to guild", + zap.Error(err), + zap.Uint32("guildID", guild.ID), + zap.Uint32("charID", charID), + ) + rollbackTransaction(s, transaction) + return err + } + + err = transaction.Commit() + + if err != nil { + s.logger.Error("failed to commit db transaction", zap.Error(err)) + rollbackTransaction(s, transaction) + return err + } + + return nil +} + +// This is relying on the fact that invitation ID is also character ID right now +// if invitation ID changes, this will break. +func (guild *Guild) CancelInvitation(s *Session, charID uint32) error { + _, err := s.server.db.Exec( + `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`, + charID, guild.ID, + ) + + if err != nil { + s.logger.Error( + "failed to cancel guild invitation", + zap.Error(err), + zap.Uint32("guildID", guild.ID), + zap.Uint32("charID", charID), + ) + return err + } + + return nil +} + +func (guild *Guild) RejectApplication(s *Session, charID uint32) error { + _, err := s.server.db.Exec( + `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'applied'`, + charID, guild.ID, + ) + + if err != nil { + s.logger.Error( + "failed to reject guild application", + zap.Error(err), + zap.Uint32("guildID", guild.ID), + zap.Uint32("charID", charID), + ) + return err + } + + return nil +} + +func (guild *Guild) ArrangeCharacters(s *Session, charIDs []uint32) error { + transaction, err := s.server.db.Begin() + + if err != nil { + s.logger.Error("failed to start db transaction", zap.Error(err)) + return err + } + + for i, id := range charIDs { + _, err := transaction.Exec("UPDATE guild_characters SET order_index = $1 WHERE character_id = $2", 2+i, id) + + if err != nil { + err = transaction.Rollback() + + if err != nil { + s.logger.Error("failed to rollback db transaction", zap.Error(err)) + } + + return err + } + } + + err = transaction.Commit() + + if err != nil { + s.logger.Error("failed to commit db transaction", zap.Error(err)) + return err + } + + return nil +} + +func (guild *Guild) GetApplicationForCharID(s *Session, charID uint32, applicationType GuildApplicationType) (*GuildApplication, error) { + row := s.server.db.QueryRowx(` + SELECT * from guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = $3 + `, charID, guild.ID, applicationType) + + application := &GuildApplication{} + + err := row.StructScan(application) + + if errors.Is(err, sql.ErrNoRows) { + return nil, nil + } + + if err != nil { + s.logger.Error( + "failed to retrieve guild application for character", + zap.Error(err), + zap.Uint32("charID", charID), + zap.Uint32("guildID", guild.ID), + ) + return nil, err + } + + return application, nil +} + +func (guild *Guild) HasApplicationForCharID(s *Session, charID uint32) (bool, error) { + row := s.server.db.QueryRowx(` + SELECT 1 from guild_applications WHERE character_id = $1 AND guild_id = $2 + `, charID, guild.ID) + + num := 0 + + err := row.Scan(&num) + + if errors.Is(err, sql.ErrNoRows) { + return false, nil + } + + if err != nil { + s.logger.Error( + "failed to retrieve guild applications for character", + zap.Error(err), + zap.Uint32("charID", charID), + zap.Uint32("guildID", guild.ID), + ) + return false, err + } + + return true, nil +} + +func CreateGuild(s *Session, guildName string) (int32, error) { + transaction, err := s.server.db.Begin() + + if err != nil { + s.logger.Error("failed to start db transaction", zap.Error(err)) + return 0, err + } + + if err != nil { + panic(err) + } + + guildResult, err := transaction.Query( + "INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id", + guildName, s.charID, + ) + + if err != nil { + s.logger.Error("failed to create guild", zap.Error(err)) + rollbackTransaction(s, transaction) + return 0, err + } + + var guildId int32 + + guildResult.Next() + + err = guildResult.Scan(&guildId) + + if err != nil { + s.logger.Error("failed to retrieve guild ID", zap.Error(err)) + rollbackTransaction(s, transaction) + return 0, err + } + + err = guildResult.Close() + + if err != nil { + s.logger.Error("failed to finalise query", zap.Error(err)) + rollbackTransaction(s, transaction) + return 0, err + } + + _, err = transaction.Exec(` + INSERT INTO guild_characters (guild_id, character_id) + VALUES ($1, $2) + `, guildId, s.charID) + + if err != nil { + s.logger.Error("failed to add character to guild", zap.Error(err)) + rollbackTransaction(s, transaction) + return 0, err + } + + err = transaction.Commit() + + if err != nil { + s.logger.Error("failed to commit guild creation", zap.Error(err)) + return 0, err + } + + return guildId, nil +} + +func rollbackTransaction(s *Session, transaction *sql.Tx) { + err := transaction.Rollback() + + if err != nil { + s.logger.Error("failed to rollback transaction", zap.Error(err)) + } +} + +func GetGuildInfoByID(s *Session, guildID uint32) (*Guild, error) { + rows, err := s.server.db.Queryx(fmt.Sprintf(` + %s + WHERE g.id = $1 + LIMIT 1 + `, guildInfoSelectQuery), guildID) + + if err != nil { + s.logger.Error("failed to retrieve guild", zap.Error(err), zap.Uint32("guildID", guildID)) + return nil, err + } + + defer func() { _ = rows.Close() }() + + hasRow := rows.Next() + + if !hasRow { + return nil, nil + } + + return buildGuildObjectFromDbResult(rows, err, s) +} + +func GetGuildInfoByCharacterId(s *Session, charID uint32) (*Guild, error) { + rows, err := s.server.db.Queryx(fmt.Sprintf(` + %s + WHERE EXISTS( + SELECT 1 + FROM guild_characters gc1 + WHERE gc1.character_id = $1 + AND gc1.guild_id = g.id + ) + OR EXISTS( + SELECT 1 + FROM guild_applications ga + WHERE ga.character_id = $1 + AND ga.guild_id = g.id + AND ga.application_type = 'applied' + ) + LIMIT 1 + `, guildInfoSelectQuery), charID) + + if err != nil { + s.logger.Error("failed to retrieve guild for character", zap.Error(err), zap.Uint32("charID", charID)) + return nil, err + } + + defer func() { _ = rows.Close() }() + + hasRow := rows.Next() + + if !hasRow { + return nil, nil + } + + return buildGuildObjectFromDbResult(rows, err, s) +} + +func buildGuildObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*Guild, error) { + guild := &Guild{} + + err := result.StructScan(guild) + + if err != nil { + s.logger.Error("failed to retrieve guild data from database", zap.Error(err)) + return nil, err + } + + return guild, nil +} + +func guildGetItems(s *Session, guildID uint32) []mhfitem.MHFItemStack { + var data []byte + var items []mhfitem.MHFItemStack + if err := s.server.db.QueryRow(`SELECT item_box FROM guilds WHERE id=$1`, guildID).Scan(&data); err != nil && !errors.Is(err, sql.ErrNoRows) { + s.logger.Error("Failed to get guild item box", zap.Error(err)) + } + if len(data) > 0 { + box := byteframe.NewByteFrameFromBytes(data) + numStacks := box.ReadUint16() + box.ReadUint16() // Unused + for i := 0; i < int(numStacks); i++ { + items = append(items, mhfitem.ReadWarehouseItem(box)) + } + } + return items +} diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index a42f05ca5..988f4a938 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -254,149 +254,6 @@ func handleMsgMhfGetSenyuDailyCount(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } -type SeibattleTimetable struct { - Start time.Time - End time.Time -} - -type SeibattleKeyScore struct { - Unk0 uint8 - Unk1 int32 -} - -type SeibattleCareer struct { - Unk0 uint16 - Unk1 uint16 - Unk2 uint16 -} - -type SeibattleOpponent struct { - Unk0 int32 - Unk1 int8 -} - -type SeibattleConventionResult struct { - Unk0 uint32 - Unk1 uint16 - Unk2 uint16 - Unk3 uint16 - Unk4 uint16 -} - -type SeibattleCharScore struct { - Unk0 uint32 -} - -type SeibattleCurResult struct { - Unk0 uint32 - Unk1 uint16 - Unk2 uint16 - Unk3 uint16 -} - -type Seibattle struct { - Timetable []SeibattleTimetable - KeyScore []SeibattleKeyScore - Career []SeibattleCareer - Opponent []SeibattleOpponent - ConventionResult []SeibattleConventionResult - CharScore []SeibattleCharScore - CurResult []SeibattleCurResult -} - -func handleMsgMhfGetSeibattle(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfGetSeibattle) - var data []*byteframe.ByteFrame - seibattle := Seibattle{ - Timetable: []SeibattleTimetable{ - {TimeMidnight(), TimeMidnight().Add(time.Hour * 8)}, - {TimeMidnight().Add(time.Hour * 8), TimeMidnight().Add(time.Hour * 16)}, - {TimeMidnight().Add(time.Hour * 16), TimeMidnight().Add(time.Hour * 24)}, - }, - KeyScore: []SeibattleKeyScore{ - {0, 0}, - }, - Career: []SeibattleCareer{ - {0, 0, 0}, - }, - Opponent: []SeibattleOpponent{ - {1, 1}, - }, - ConventionResult: []SeibattleConventionResult{ - {0, 0, 0, 0, 0}, - }, - CharScore: []SeibattleCharScore{ - {0}, - }, - CurResult: []SeibattleCurResult{ - {0, 0, 0, 0}, - }, - } - - switch pkt.Type { - case 1: - for _, timetable := range seibattle.Timetable { - bf := byteframe.NewByteFrame() - bf.WriteUint32(uint32(timetable.Start.Unix())) - bf.WriteUint32(uint32(timetable.End.Unix())) - data = append(data, bf) - } - case 3: // Key score? - for _, keyScore := range seibattle.KeyScore { - bf := byteframe.NewByteFrame() - bf.WriteUint8(keyScore.Unk0) - bf.WriteInt32(keyScore.Unk1) - data = append(data, bf) - } - case 4: // Career? - for _, career := range seibattle.Career { - bf := byteframe.NewByteFrame() - bf.WriteUint16(career.Unk0) - bf.WriteUint16(career.Unk1) - bf.WriteUint16(career.Unk2) - data = append(data, bf) - } - case 5: // Opponent? - for _, opponent := range seibattle.Opponent { - bf := byteframe.NewByteFrame() - bf.WriteInt32(opponent.Unk0) - bf.WriteInt8(opponent.Unk1) - data = append(data, bf) - } - case 6: // Convention result? - for _, conventionResult := range seibattle.ConventionResult { - bf := byteframe.NewByteFrame() - bf.WriteUint32(conventionResult.Unk0) - bf.WriteUint16(conventionResult.Unk1) - bf.WriteUint16(conventionResult.Unk2) - bf.WriteUint16(conventionResult.Unk3) - bf.WriteUint16(conventionResult.Unk4) - data = append(data, bf) - } - case 7: // Char score? - for _, charScore := range seibattle.CharScore { - bf := byteframe.NewByteFrame() - bf.WriteUint32(charScore.Unk0) - data = append(data, bf) - } - case 8: // Cur result? - for _, curResult := range seibattle.CurResult { - bf := byteframe.NewByteFrame() - bf.WriteUint32(curResult.Unk0) - bf.WriteUint16(curResult.Unk1) - bf.WriteUint16(curResult.Unk2) - bf.WriteUint16(curResult.Unk3) - data = append(data, bf) - } - } - doAckEarthSucceed(s, pkt.AckHandle, data) -} - -func handleMsgMhfPostSeibattle(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfPostSeibattle) - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) -} - func handleMsgMhfGetDailyMissionMaster(s *Session, p mhfpacket.MHFPacket) {} func handleMsgMhfGetDailyMissionPersonal(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/handlers_cast_binary.go b/server/channelserver/handlers_cast_binary.go index 67560b91a..121748bb0 100644 --- a/server/channelserver/handlers_cast_binary.go +++ b/server/channelserver/handlers_cast_binary.go @@ -1,22 +1,13 @@ package channelserver import ( - "crypto/rand" - "encoding/hex" "erupe-ce/common/byteframe" - "erupe-ce/common/mhfcid" - "erupe-ce/common/mhfcourse" "erupe-ce/common/token" - "erupe-ce/config" - "erupe-ce/network" "erupe-ce/network/binpacket" "erupe-ce/network/mhfpacket" "fmt" "math" - "slices" - "strconv" "strings" - "time" "go.uber.org/zap" ) @@ -39,409 +30,6 @@ const ( BroadcastTypeWorld = 0x0a ) -var commands map[string]_config.Command - -func init() { - commands = make(map[string]_config.Command) - zapConfig := zap.NewDevelopmentConfig() - zapConfig.DisableCaller = true - zapLogger, _ := zapConfig.Build() - defer func() { _ = zapLogger.Sync() }() - logger := zapLogger.Named("commands") - cmds := _config.ErupeConfig.Commands - for _, cmd := range cmds { - commands[cmd.Name] = cmd - if cmd.Enabled { - logger.Info(fmt.Sprintf("Command %s: Enabled, prefix: %s", cmd.Name, cmd.Prefix)) - } else { - logger.Info(fmt.Sprintf("Command %s: Disabled", cmd.Name)) - } - } -} - -func sendDisabledCommandMessage(s *Session, cmd _config.Command) { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.disabled, cmd.Name)) -} - -func sendServerChatMessage(s *Session, message string) { - // Make the inside of the casted binary - bf := byteframe.NewByteFrame() - bf.SetLE() - msgBinChat := &binpacket.MsgBinChat{ - Unk0: 0, - Type: 5, - Flags: 0x80, - Message: message, - SenderName: "Erupe", - } - _ = msgBinChat.Build(bf) - - castedBin := &mhfpacket.MsgSysCastedBinary{ - CharID: 0, - MessageType: BinaryMessageTypeChat, - RawDataPayload: bf.Data(), - } - - s.QueueSendMHFNonBlocking(castedBin) -} - -func parseChatCommand(s *Session, command string) { - args := strings.Split(command[len(s.server.erupeConfig.CommandPrefix):], " ") - switch args[0] { - case commands["Ban"].Prefix: - if s.isOp() { - if len(args) > 1 { - var expiry time.Time - if len(args) > 2 { - var length int - var unit string - n, err := fmt.Sscanf(args[2], `%d%s`, &length, &unit) - if err == nil && n == 2 { - switch unit { - case "s", "second", "seconds": - expiry = time.Now().Add(time.Duration(length) * time.Second) - case "m", "mi", "minute", "minutes": - expiry = time.Now().Add(time.Duration(length) * time.Minute) - case "h", "hour", "hours": - expiry = time.Now().Add(time.Duration(length) * time.Hour) - case "d", "day", "days": - expiry = time.Now().Add(time.Duration(length) * time.Hour * 24) - case "mo", "month", "months": - expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 30) - case "y", "year", "years": - expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 365) - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.ban.error) - return - } - } - cid := mhfcid.ConvertCID(args[1]) - if cid > 0 { - var uid uint32 - var uname string - err := s.server.db.QueryRow(`SELECT id, username FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, cid).Scan(&uid, &uname) - if err == nil { - if expiry.IsZero() { - if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1) - ON CONFLICT (user_id) DO UPDATE SET expires=NULL`, uid); err != nil { - s.logger.Error("Failed to ban user", zap.Error(err)) - } - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)) - } else { - if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1, $2) - ON CONFLICT (user_id) DO UPDATE SET expires=$2`, uid, expiry); err != nil { - s.logger.Error("Failed to ban user with expiry", zap.Error(err)) - } - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)+fmt.Sprintf(s.server.i18n.commands.ban.length, expiry.Format(time.DateTime))) - } - s.server.DisconnectUser(uid) - } else { - sendServerChatMessage(s, s.server.i18n.commands.ban.noUser) - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.ban.invalid) - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.ban.error) - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.noOp) - } - case commands["Timer"].Prefix: - if commands["Timer"].Enabled || s.isOp() { - var state bool - if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&state); err != nil { - s.logger.Error("Failed to get timer state", zap.Error(err)) - } - if _, err := s.server.db.Exec(`UPDATE users u SET timer=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, !state, s.charID); err != nil { - s.logger.Error("Failed to update timer setting", zap.Error(err)) - } - if state { - sendServerChatMessage(s, s.server.i18n.commands.timer.disabled) - } else { - sendServerChatMessage(s, s.server.i18n.commands.timer.enabled) - } - } else { - sendDisabledCommandMessage(s, commands["Timer"]) - } - case commands["PSN"].Prefix: - if commands["PSN"].Enabled || s.isOp() { - if len(args) > 1 { - var exists int - if err := s.server.db.QueryRow(`SELECT count(*) FROM users WHERE psn_id = $1`, args[1]).Scan(&exists); err != nil { - s.logger.Error("Failed to check PSN ID existence", zap.Error(err)) - } - if exists == 0 { - _, err := s.server.db.Exec(`UPDATE users u SET psn_id=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, args[1], s.charID) - if err == nil { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.success, args[1])) - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.psn.exists) - } - } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.error, commands["PSN"].Prefix)) - } - } else { - sendDisabledCommandMessage(s, commands["PSN"]) - } - case commands["Reload"].Prefix: - if commands["Reload"].Enabled || s.isOp() { - sendServerChatMessage(s, s.server.i18n.commands.reload) - var temp mhfpacket.MHFPacket - deleteNotif := byteframe.NewByteFrame() - for _, object := range s.stage.objects { - if object.ownerCharID == s.charID { - continue - } - temp = &mhfpacket.MsgSysDeleteObject{ObjID: object.id} - deleteNotif.WriteUint16(uint16(temp.Opcode())) - _ = temp.Build(deleteNotif, s.clientContext) - } - for _, session := range s.server.sessions { - if s == session { - continue - } - temp = &mhfpacket.MsgSysDeleteUser{CharID: session.charID} - deleteNotif.WriteUint16(uint16(temp.Opcode())) - _ = temp.Build(deleteNotif, s.clientContext) - } - deleteNotif.WriteUint16(uint16(network.MSG_SYS_END)) - s.QueueSendNonBlocking(deleteNotif.Data()) - time.Sleep(500 * time.Millisecond) - reloadNotif := byteframe.NewByteFrame() - for _, session := range s.server.sessions { - if s == session { - continue - } - temp = &mhfpacket.MsgSysInsertUser{CharID: session.charID} - reloadNotif.WriteUint16(uint16(temp.Opcode())) - _ = temp.Build(reloadNotif, s.clientContext) - for i := 0; i < 3; i++ { - temp = &mhfpacket.MsgSysNotifyUserBinary{ - CharID: session.charID, - BinaryType: uint8(i + 1), - } - reloadNotif.WriteUint16(uint16(temp.Opcode())) - _ = temp.Build(reloadNotif, s.clientContext) - } - } - for _, obj := range s.stage.objects { - if obj.ownerCharID == s.charID { - continue - } - temp = &mhfpacket.MsgSysDuplicateObject{ - ObjID: obj.id, - X: obj.x, - Y: obj.y, - Z: obj.z, - Unk0: 0, - OwnerCharID: obj.ownerCharID, - } - reloadNotif.WriteUint16(uint16(temp.Opcode())) - _ = temp.Build(reloadNotif, s.clientContext) - } - reloadNotif.WriteUint16(uint16(network.MSG_SYS_END)) - s.QueueSendNonBlocking(reloadNotif.Data()) - } else { - sendDisabledCommandMessage(s, commands["Reload"]) - } - case commands["KeyQuest"].Prefix: - if commands["KeyQuest"].Enabled || s.isOp() { - if s.server.erupeConfig.RealClientMode < _config.G10 { - sendServerChatMessage(s, s.server.i18n.commands.kqf.version) - } else { - if len(args) > 1 { - switch args[1] { - case "get": - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.get, s.kqf)) - case "set": - if len(args) > 2 && len(args[2]) == 16 { - hexd, _ := hex.DecodeString(args[2]) - s.kqf = hexd - s.kqfOverride = true - sendServerChatMessage(s, s.server.i18n.commands.kqf.set.success) - } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.set.error, commands["KeyQuest"].Prefix)) - } - } - } - } - } else { - sendDisabledCommandMessage(s, commands["KeyQuest"]) - } - case commands["Rights"].Prefix: - if commands["Rights"].Enabled || s.isOp() { - if len(args) > 1 { - v, _ := strconv.Atoi(args[1]) - _, err := s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", v, s.charID) - if err == nil { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.success, v)) - } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix)) - } - } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix)) - } - } else { - sendDisabledCommandMessage(s, commands["Rights"]) - } - case commands["Course"].Prefix: - if commands["Course"].Enabled || s.isOp() { - if len(args) > 1 { - for _, course := range mhfcourse.Courses() { - for _, alias := range course.Aliases() { - if strings.EqualFold(args[1], alias) { - if slices.Contains(s.server.erupeConfig.Courses, _config.Course{Name: course.Aliases()[0], Enabled: true}) { - var delta, rightsInt uint32 - if mhfcourse.CourseExists(course.ID, s.courses) { - ei := slices.IndexFunc(s.courses, func(c mhfcourse.Course) bool { - for _, alias := range c.Aliases() { - if strings.EqualFold(args[1], alias) { - return true - } - } - return false - }) - if ei != -1 { - delta = uint32(-1 * math.Pow(2, float64(course.ID))) - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.disabled, course.Aliases()[0])) - } - } else { - delta = uint32(math.Pow(2, float64(course.ID))) - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.enabled, course.Aliases()[0])) - } - err := s.server.db.QueryRow("SELECT rights FROM users u INNER JOIN characters c ON u.id = c.user_id WHERE c.id = $1", s.charID).Scan(&rightsInt) - if err == nil { - if _, err := s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", rightsInt+delta, s.charID); err != nil { - s.logger.Error("Failed to update user rights", zap.Error(err)) - } - } - updateRights(s) - } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.locked, course.Aliases()[0])) - } - return - } - } - } - } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.error, commands["Course"].Prefix)) - } - } else { - sendDisabledCommandMessage(s, commands["Course"]) - } - case commands["Raviente"].Prefix: - if commands["Raviente"].Enabled || s.isOp() { - if len(args) > 1 { - if s.server.getRaviSemaphore() != nil { - switch args[1] { - case "start": - if s.server.raviente.register[1] == 0 { - s.server.raviente.register[1] = s.server.raviente.register[3] - sendServerChatMessage(s, s.server.i18n.commands.ravi.start.success) - s.notifyRavi() - } else { - sendServerChatMessage(s, s.server.i18n.commands.ravi.start.error) - } - case "cm", "check", "checkmultiplier", "multiplier": - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ravi.multiplier, s.server.GetRaviMultiplier())) - case "sr", "sendres", "resurrection", "ss", "sendsed", "rs", "reqsed": - if s.server.erupeConfig.RealClientMode == _config.ZZ { - switch args[1] { - case "sr", "sendres", "resurrection": - if s.server.raviente.state[28] > 0 { - sendServerChatMessage(s, s.server.i18n.commands.ravi.res.success) - s.server.raviente.state[28] = 0 - } else { - sendServerChatMessage(s, s.server.i18n.commands.ravi.res.error) - } - case "ss", "sendsed": - sendServerChatMessage(s, s.server.i18n.commands.ravi.sed.success) - // Total BerRavi HP - HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4] - s.server.raviente.support[1] = HP - case "rs", "reqsed": - sendServerChatMessage(s, s.server.i18n.commands.ravi.request) - // Total BerRavi HP - HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4] - s.server.raviente.support[1] = HP + 1 - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.ravi.version) - } - default: - sendServerChatMessage(s, s.server.i18n.commands.ravi.error) - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.ravi.noPlayers) - } - } else { - sendServerChatMessage(s, s.server.i18n.commands.ravi.error) - } - } else { - sendDisabledCommandMessage(s, commands["Raviente"]) - } - case commands["Teleport"].Prefix: - if commands["Teleport"].Enabled || s.isOp() { - if len(args) > 2 { - x, _ := strconv.ParseInt(args[1], 10, 16) - y, _ := strconv.ParseInt(args[2], 10, 16) - payload := byteframe.NewByteFrame() - payload.SetLE() - payload.WriteUint8(2) // SetState type(position == 2) - payload.WriteInt16(int16(x)) // X - payload.WriteInt16(int16(y)) // Y - payloadBytes := payload.Data() - s.QueueSendMHFNonBlocking(&mhfpacket.MsgSysCastedBinary{ - CharID: s.charID, - MessageType: BinaryMessageTypeState, - RawDataPayload: payloadBytes, - }) - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.success, x, y)) - } else { - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.error, commands["Teleport"].Prefix)) - } - } else { - sendDisabledCommandMessage(s, commands["Teleport"]) - } - case commands["Discord"].Prefix: - if commands["Discord"].Enabled || s.isOp() { - var _token string - err := s.server.db.QueryRow(`SELECT discord_token FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&_token) - if err != nil { - randToken := make([]byte, 4) - _, _ = rand.Read(randToken) - _token = fmt.Sprintf("%x-%x", randToken[:2], randToken[2:]) - if _, err := s.server.db.Exec(`UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, _token, s.charID); err != nil { - s.logger.Error("Failed to update discord token", zap.Error(err)) - } - } - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.discord.success, _token)) - } else { - sendDisabledCommandMessage(s, commands["Discord"]) - } - case commands["Playtime"].Prefix: - if commands["Playtime"].Enabled || s.isOp() { - playtime := s.playtime + uint32(time.Since(s.playtimeTime).Seconds()) - sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.playtime, playtime/60/60, playtime/60%60, playtime%60)) - } else { - sendDisabledCommandMessage(s, commands["Playtime"]) - } - case commands["Help"].Prefix: - if commands["Help"].Enabled || s.isOp() { - for _, command := range commands { - if command.Enabled || s.isOp() { - sendServerChatMessage(s, fmt.Sprintf("%s%s: %s", s.server.erupeConfig.CommandPrefix, command.Prefix, command.Description)) - } - } - } else { - sendDisabledCommandMessage(s, commands["Help"]) - } - } -} - func handleMsgSysCastBinary(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgSysCastBinary) tmp := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload) diff --git a/server/channelserver/handlers_commands.go b/server/channelserver/handlers_commands.go new file mode 100644 index 000000000..34777222b --- /dev/null +++ b/server/channelserver/handlers_commands.go @@ -0,0 +1,424 @@ +package channelserver + +import ( + "crypto/rand" + "encoding/hex" + "erupe-ce/common/byteframe" + "erupe-ce/common/mhfcid" + "erupe-ce/common/mhfcourse" + "erupe-ce/config" + "erupe-ce/network" + "erupe-ce/network/binpacket" + "erupe-ce/network/mhfpacket" + "fmt" + "math" + "slices" + "strconv" + "strings" + "time" + + "go.uber.org/zap" +) + +var commands map[string]_config.Command + +func init() { + commands = make(map[string]_config.Command) + zapConfig := zap.NewDevelopmentConfig() + zapConfig.DisableCaller = true + zapLogger, _ := zapConfig.Build() + defer func() { _ = zapLogger.Sync() }() + logger := zapLogger.Named("commands") + cmds := _config.ErupeConfig.Commands + for _, cmd := range cmds { + commands[cmd.Name] = cmd + if cmd.Enabled { + logger.Info(fmt.Sprintf("Command %s: Enabled, prefix: %s", cmd.Name, cmd.Prefix)) + } else { + logger.Info(fmt.Sprintf("Command %s: Disabled", cmd.Name)) + } + } +} + +func sendDisabledCommandMessage(s *Session, cmd _config.Command) { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.disabled, cmd.Name)) +} + +func sendServerChatMessage(s *Session, message string) { + // Make the inside of the casted binary + bf := byteframe.NewByteFrame() + bf.SetLE() + msgBinChat := &binpacket.MsgBinChat{ + Unk0: 0, + Type: 5, + Flags: 0x80, + Message: message, + SenderName: "Erupe", + } + _ = msgBinChat.Build(bf) + + castedBin := &mhfpacket.MsgSysCastedBinary{ + CharID: 0, + MessageType: BinaryMessageTypeChat, + RawDataPayload: bf.Data(), + } + + s.QueueSendMHFNonBlocking(castedBin) +} + +func parseChatCommand(s *Session, command string) { + args := strings.Split(command[len(s.server.erupeConfig.CommandPrefix):], " ") + switch args[0] { + case commands["Ban"].Prefix: + if s.isOp() { + if len(args) > 1 { + var expiry time.Time + if len(args) > 2 { + var length int + var unit string + n, err := fmt.Sscanf(args[2], `%d%s`, &length, &unit) + if err == nil && n == 2 { + switch unit { + case "s", "second", "seconds": + expiry = time.Now().Add(time.Duration(length) * time.Second) + case "m", "mi", "minute", "minutes": + expiry = time.Now().Add(time.Duration(length) * time.Minute) + case "h", "hour", "hours": + expiry = time.Now().Add(time.Duration(length) * time.Hour) + case "d", "day", "days": + expiry = time.Now().Add(time.Duration(length) * time.Hour * 24) + case "mo", "month", "months": + expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 30) + case "y", "year", "years": + expiry = time.Now().Add(time.Duration(length) * time.Hour * 24 * 365) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.error) + return + } + } + cid := mhfcid.ConvertCID(args[1]) + if cid > 0 { + var uid uint32 + var uname string + err := s.server.db.QueryRow(`SELECT id, username FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, cid).Scan(&uid, &uname) + if err == nil { + if expiry.IsZero() { + if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1) + ON CONFLICT (user_id) DO UPDATE SET expires=NULL`, uid); err != nil { + s.logger.Error("Failed to ban user", zap.Error(err)) + } + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)) + } else { + if _, err := s.server.db.Exec(`INSERT INTO bans VALUES ($1, $2) + ON CONFLICT (user_id) DO UPDATE SET expires=$2`, uid, expiry); err != nil { + s.logger.Error("Failed to ban user with expiry", zap.Error(err)) + } + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ban.success, uname)+fmt.Sprintf(s.server.i18n.commands.ban.length, expiry.Format(time.DateTime))) + } + s.server.DisconnectUser(uid) + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.noUser) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.invalid) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ban.error) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.noOp) + } + case commands["Timer"].Prefix: + if commands["Timer"].Enabled || s.isOp() { + var state bool + if err := s.server.db.QueryRow(`SELECT COALESCE(timer, false) FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&state); err != nil { + s.logger.Error("Failed to get timer state", zap.Error(err)) + } + if _, err := s.server.db.Exec(`UPDATE users u SET timer=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, !state, s.charID); err != nil { + s.logger.Error("Failed to update timer setting", zap.Error(err)) + } + if state { + sendServerChatMessage(s, s.server.i18n.commands.timer.disabled) + } else { + sendServerChatMessage(s, s.server.i18n.commands.timer.enabled) + } + } else { + sendDisabledCommandMessage(s, commands["Timer"]) + } + case commands["PSN"].Prefix: + if commands["PSN"].Enabled || s.isOp() { + if len(args) > 1 { + var exists int + if err := s.server.db.QueryRow(`SELECT count(*) FROM users WHERE psn_id = $1`, args[1]).Scan(&exists); err != nil { + s.logger.Error("Failed to check PSN ID existence", zap.Error(err)) + } + if exists == 0 { + _, err := s.server.db.Exec(`UPDATE users u SET psn_id=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, args[1], s.charID) + if err == nil { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.success, args[1])) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.psn.exists) + } + } else { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.psn.error, commands["PSN"].Prefix)) + } + } else { + sendDisabledCommandMessage(s, commands["PSN"]) + } + case commands["Reload"].Prefix: + if commands["Reload"].Enabled || s.isOp() { + sendServerChatMessage(s, s.server.i18n.commands.reload) + var temp mhfpacket.MHFPacket + deleteNotif := byteframe.NewByteFrame() + for _, object := range s.stage.objects { + if object.ownerCharID == s.charID { + continue + } + temp = &mhfpacket.MsgSysDeleteObject{ObjID: object.id} + deleteNotif.WriteUint16(uint16(temp.Opcode())) + _ = temp.Build(deleteNotif, s.clientContext) + } + for _, session := range s.server.sessions { + if s == session { + continue + } + temp = &mhfpacket.MsgSysDeleteUser{CharID: session.charID} + deleteNotif.WriteUint16(uint16(temp.Opcode())) + _ = temp.Build(deleteNotif, s.clientContext) + } + deleteNotif.WriteUint16(uint16(network.MSG_SYS_END)) + s.QueueSendNonBlocking(deleteNotif.Data()) + time.Sleep(500 * time.Millisecond) + reloadNotif := byteframe.NewByteFrame() + for _, session := range s.server.sessions { + if s == session { + continue + } + temp = &mhfpacket.MsgSysInsertUser{CharID: session.charID} + reloadNotif.WriteUint16(uint16(temp.Opcode())) + _ = temp.Build(reloadNotif, s.clientContext) + for i := 0; i < 3; i++ { + temp = &mhfpacket.MsgSysNotifyUserBinary{ + CharID: session.charID, + BinaryType: uint8(i + 1), + } + reloadNotif.WriteUint16(uint16(temp.Opcode())) + _ = temp.Build(reloadNotif, s.clientContext) + } + } + for _, obj := range s.stage.objects { + if obj.ownerCharID == s.charID { + continue + } + temp = &mhfpacket.MsgSysDuplicateObject{ + ObjID: obj.id, + X: obj.x, + Y: obj.y, + Z: obj.z, + Unk0: 0, + OwnerCharID: obj.ownerCharID, + } + reloadNotif.WriteUint16(uint16(temp.Opcode())) + _ = temp.Build(reloadNotif, s.clientContext) + } + reloadNotif.WriteUint16(uint16(network.MSG_SYS_END)) + s.QueueSendNonBlocking(reloadNotif.Data()) + } else { + sendDisabledCommandMessage(s, commands["Reload"]) + } + case commands["KeyQuest"].Prefix: + if commands["KeyQuest"].Enabled || s.isOp() { + if s.server.erupeConfig.RealClientMode < _config.G10 { + sendServerChatMessage(s, s.server.i18n.commands.kqf.version) + } else { + if len(args) > 1 { + switch args[1] { + case "get": + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.get, s.kqf)) + case "set": + if len(args) > 2 && len(args[2]) == 16 { + hexd, _ := hex.DecodeString(args[2]) + s.kqf = hexd + s.kqfOverride = true + sendServerChatMessage(s, s.server.i18n.commands.kqf.set.success) + } else { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.kqf.set.error, commands["KeyQuest"].Prefix)) + } + } + } + } + } else { + sendDisabledCommandMessage(s, commands["KeyQuest"]) + } + case commands["Rights"].Prefix: + if commands["Rights"].Enabled || s.isOp() { + if len(args) > 1 { + v, _ := strconv.Atoi(args[1]) + _, err := s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", v, s.charID) + if err == nil { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.success, v)) + } else { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix)) + } + } else { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.rights.error, commands["Rights"].Prefix)) + } + } else { + sendDisabledCommandMessage(s, commands["Rights"]) + } + case commands["Course"].Prefix: + if commands["Course"].Enabled || s.isOp() { + if len(args) > 1 { + for _, course := range mhfcourse.Courses() { + for _, alias := range course.Aliases() { + if strings.EqualFold(args[1], alias) { + if slices.Contains(s.server.erupeConfig.Courses, _config.Course{Name: course.Aliases()[0], Enabled: true}) { + var delta, rightsInt uint32 + if mhfcourse.CourseExists(course.ID, s.courses) { + ei := slices.IndexFunc(s.courses, func(c mhfcourse.Course) bool { + for _, alias := range c.Aliases() { + if strings.EqualFold(args[1], alias) { + return true + } + } + return false + }) + if ei != -1 { + delta = uint32(-1 * math.Pow(2, float64(course.ID))) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.disabled, course.Aliases()[0])) + } + } else { + delta = uint32(math.Pow(2, float64(course.ID))) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.enabled, course.Aliases()[0])) + } + err := s.server.db.QueryRow("SELECT rights FROM users u INNER JOIN characters c ON u.id = c.user_id WHERE c.id = $1", s.charID).Scan(&rightsInt) + if err == nil { + if _, err := s.server.db.Exec("UPDATE users u SET rights=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)", rightsInt+delta, s.charID); err != nil { + s.logger.Error("Failed to update user rights", zap.Error(err)) + } + } + updateRights(s) + } else { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.locked, course.Aliases()[0])) + } + return + } + } + } + } else { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.course.error, commands["Course"].Prefix)) + } + } else { + sendDisabledCommandMessage(s, commands["Course"]) + } + case commands["Raviente"].Prefix: + if commands["Raviente"].Enabled || s.isOp() { + if len(args) > 1 { + if s.server.getRaviSemaphore() != nil { + switch args[1] { + case "start": + if s.server.raviente.register[1] == 0 { + s.server.raviente.register[1] = s.server.raviente.register[3] + sendServerChatMessage(s, s.server.i18n.commands.ravi.start.success) + s.notifyRavi() + } else { + sendServerChatMessage(s, s.server.i18n.commands.ravi.start.error) + } + case "cm", "check", "checkmultiplier", "multiplier": + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.ravi.multiplier, s.server.GetRaviMultiplier())) + case "sr", "sendres", "resurrection", "ss", "sendsed", "rs", "reqsed": + if s.server.erupeConfig.RealClientMode == _config.ZZ { + switch args[1] { + case "sr", "sendres", "resurrection": + if s.server.raviente.state[28] > 0 { + sendServerChatMessage(s, s.server.i18n.commands.ravi.res.success) + s.server.raviente.state[28] = 0 + } else { + sendServerChatMessage(s, s.server.i18n.commands.ravi.res.error) + } + case "ss", "sendsed": + sendServerChatMessage(s, s.server.i18n.commands.ravi.sed.success) + // Total BerRavi HP + HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4] + s.server.raviente.support[1] = HP + case "rs", "reqsed": + sendServerChatMessage(s, s.server.i18n.commands.ravi.request) + // Total BerRavi HP + HP := s.server.raviente.state[0] + s.server.raviente.state[1] + s.server.raviente.state[2] + s.server.raviente.state[3] + s.server.raviente.state[4] + s.server.raviente.support[1] = HP + 1 + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ravi.version) + } + default: + sendServerChatMessage(s, s.server.i18n.commands.ravi.error) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ravi.noPlayers) + } + } else { + sendServerChatMessage(s, s.server.i18n.commands.ravi.error) + } + } else { + sendDisabledCommandMessage(s, commands["Raviente"]) + } + case commands["Teleport"].Prefix: + if commands["Teleport"].Enabled || s.isOp() { + if len(args) > 2 { + x, _ := strconv.ParseInt(args[1], 10, 16) + y, _ := strconv.ParseInt(args[2], 10, 16) + payload := byteframe.NewByteFrame() + payload.SetLE() + payload.WriteUint8(2) // SetState type(position == 2) + payload.WriteInt16(int16(x)) // X + payload.WriteInt16(int16(y)) // Y + payloadBytes := payload.Data() + s.QueueSendMHFNonBlocking(&mhfpacket.MsgSysCastedBinary{ + CharID: s.charID, + MessageType: BinaryMessageTypeState, + RawDataPayload: payloadBytes, + }) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.success, x, y)) + } else { + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.teleport.error, commands["Teleport"].Prefix)) + } + } else { + sendDisabledCommandMessage(s, commands["Teleport"]) + } + case commands["Discord"].Prefix: + if commands["Discord"].Enabled || s.isOp() { + var _token string + err := s.server.db.QueryRow(`SELECT discord_token FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&_token) + if err != nil { + randToken := make([]byte, 4) + _, _ = rand.Read(randToken) + _token = fmt.Sprintf("%x-%x", randToken[:2], randToken[2:]) + if _, err := s.server.db.Exec(`UPDATE users u SET discord_token = $1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, _token, s.charID); err != nil { + s.logger.Error("Failed to update discord token", zap.Error(err)) + } + } + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.discord.success, _token)) + } else { + sendDisabledCommandMessage(s, commands["Discord"]) + } + case commands["Playtime"].Prefix: + if commands["Playtime"].Enabled || s.isOp() { + playtime := s.playtime + uint32(time.Since(s.playtimeTime).Seconds()) + sendServerChatMessage(s, fmt.Sprintf(s.server.i18n.commands.playtime, playtime/60/60, playtime/60%60, playtime%60)) + } else { + sendDisabledCommandMessage(s, commands["Playtime"]) + } + case commands["Help"].Prefix: + if commands["Help"].Enabled || s.isOp() { + for _, command := range commands { + if command.Enabled || s.isOp() { + sendServerChatMessage(s, fmt.Sprintf("%s%s: %s", s.server.erupeConfig.CommandPrefix, command.Prefix, command.Description)) + } + } + } else { + sendDisabledCommandMessage(s, commands["Help"]) + } + } +} diff --git a/server/channelserver/handlers_guild.go b/server/channelserver/handlers_guild.go index 8e726ee8e..f3fb60260 100644 --- a/server/channelserver/handlers_guild.go +++ b/server/channelserver/handlers_guild.go @@ -1,618 +1,16 @@ package channelserver import ( - "database/sql" - "database/sql/driver" - "encoding/json" - "errors" + "erupe-ce/common/byteframe" "erupe-ce/common/mhfitem" _config "erupe-ce/config" - "fmt" "sort" - "time" - "erupe-ce/common/byteframe" ps "erupe-ce/common/pascalstring" "erupe-ce/network/mhfpacket" - "github.com/jmoiron/sqlx" "go.uber.org/zap" ) -type FestivalColor string - -const ( - FestivalColorNone FestivalColor = "none" - FestivalColorBlue FestivalColor = "blue" - FestivalColorRed FestivalColor = "red" -) - -var FestivalColorCodes = map[FestivalColor]int16{ - FestivalColorNone: -1, - FestivalColorBlue: 0, - FestivalColorRed: 1, -} - -type GuildApplicationType string - -const ( - GuildApplicationTypeApplied GuildApplicationType = "applied" - GuildApplicationTypeInvited GuildApplicationType = "invited" -) - -type Guild struct { - ID uint32 `db:"id"` - Name string `db:"name"` - MainMotto uint8 `db:"main_motto"` - SubMotto uint8 `db:"sub_motto"` - CreatedAt time.Time `db:"created_at"` - MemberCount uint16 `db:"member_count"` - RankRP uint32 `db:"rank_rp"` - EventRP uint32 `db:"event_rp"` - RoomRP uint16 `db:"room_rp"` - RoomExpiry time.Time `db:"room_expiry"` - Comment string `db:"comment"` - PugiName1 string `db:"pugi_name_1"` - PugiName2 string `db:"pugi_name_2"` - PugiName3 string `db:"pugi_name_3"` - PugiOutfit1 uint8 `db:"pugi_outfit_1"` - PugiOutfit2 uint8 `db:"pugi_outfit_2"` - PugiOutfit3 uint8 `db:"pugi_outfit_3"` - PugiOutfits uint32 `db:"pugi_outfits"` - Recruiting bool `db:"recruiting"` - FestivalColor FestivalColor `db:"festival_color"` - Souls uint32 `db:"souls"` - AllianceID uint32 `db:"alliance_id"` - Icon *GuildIcon `db:"icon"` - - GuildLeader -} - -type GuildLeader struct { - LeaderCharID uint32 `db:"leader_id"` - LeaderName string `db:"leader_name"` -} - -type GuildIconPart struct { - Index uint16 - ID uint16 - Page uint8 - Size uint8 - Rotation uint8 - Red uint8 - Green uint8 - Blue uint8 - PosX uint16 - PosY uint16 -} - -type GuildApplication struct { - ID int `db:"id"` - GuildID uint32 `db:"guild_id"` - CharID uint32 `db:"character_id"` - ActorID uint32 `db:"actor_id"` - ApplicationType GuildApplicationType `db:"application_type"` - CreatedAt time.Time `db:"created_at"` -} - -type GuildIcon struct { - Parts []GuildIconPart -} - -func (gi *GuildIcon) Scan(val interface{}) (err error) { - switch v := val.(type) { - case []byte: - err = json.Unmarshal(v, &gi) - case string: - err = json.Unmarshal([]byte(v), &gi) - } - - return -} - -func (gi *GuildIcon) Value() (valuer driver.Value, err error) { - return json.Marshal(gi) -} - -func (g *Guild) Rank() uint16 { - rpMap := []uint32{ - 24, 48, 96, 144, 192, 240, 288, 360, 432, - 504, 600, 696, 792, 888, 984, 1080, 1200, - } - if _config.ErupeConfig.RealClientMode <= _config.Z2 { - rpMap = []uint32{ - 3500, 6000, 8500, 11000, 13500, 16000, 20000, 24000, 28000, - 33000, 38000, 43000, 48000, 55000, 70000, 90000, 120000, - } - } - for i, u := range rpMap { - if g.RankRP < u { - if _config.ErupeConfig.RealClientMode <= _config.S6 && i >= 12 { - return 12 - } else if _config.ErupeConfig.RealClientMode <= _config.F5 && i >= 13 { - return 13 - } else if _config.ErupeConfig.RealClientMode <= _config.G32 && i >= 14 { - return 14 - } - return uint16(i) - } - } - if _config.ErupeConfig.RealClientMode <= _config.S6 { - return 12 - } else if _config.ErupeConfig.RealClientMode <= _config.F5 { - return 13 - } else if _config.ErupeConfig.RealClientMode <= _config.G32 { - return 14 - } - return 17 -} - -const guildInfoSelectQuery = ` -SELECT - g.id, - g.name, - rank_rp, - event_rp, - room_rp, - COALESCE(room_expiry, '1970-01-01') AS room_expiry, - main_motto, - sub_motto, - created_at, - leader_id, - c.name AS leader_name, - comment, - COALESCE(pugi_name_1, '') AS pugi_name_1, - COALESCE(pugi_name_2, '') AS pugi_name_2, - COALESCE(pugi_name_3, '') AS pugi_name_3, - pugi_outfit_1, - pugi_outfit_2, - pugi_outfit_3, - pugi_outfits, - recruiting, - COALESCE((SELECT team FROM festa_registrations fr WHERE fr.guild_id = g.id), 'none') AS festival_color, - COALESCE((SELECT SUM(fs.souls) FROM festa_submissions fs WHERE fs.guild_id=g.id), 0) AS souls, - COALESCE(( - SELECT id FROM guild_alliances ga WHERE - ga.parent_id = g.id OR - ga.sub1_id = g.id OR - ga.sub2_id = g.id - ), 0) AS alliance_id, - icon, - (SELECT count(1) FROM guild_characters gc WHERE gc.guild_id = g.id) AS member_count - FROM guilds g - JOIN guild_characters gc ON gc.character_id = leader_id - JOIN characters c on leader_id = c.id -` - -func (guild *Guild) Save(s *Session) error { - _, err := s.server.db.Exec(` - UPDATE guilds SET main_motto=$2, sub_motto=$3, comment=$4, pugi_name_1=$5, pugi_name_2=$6, pugi_name_3=$7, - pugi_outfit_1=$8, pugi_outfit_2=$9, pugi_outfit_3=$10, pugi_outfits=$11, icon=$12, leader_id=$13 WHERE id=$1 - `, guild.ID, guild.MainMotto, guild.SubMotto, guild.Comment, guild.PugiName1, guild.PugiName2, guild.PugiName3, - guild.PugiOutfit1, guild.PugiOutfit2, guild.PugiOutfit3, guild.PugiOutfits, guild.Icon, guild.LeaderCharID) - - if err != nil { - s.logger.Error("failed to update guild data", zap.Error(err), zap.Uint32("guildID", guild.ID)) - return err - } - - return nil -} - -func (guild *Guild) CreateApplication(s *Session, charID uint32, applicationType GuildApplicationType, transaction *sql.Tx) error { - - query := ` - INSERT INTO guild_applications (guild_id, character_id, actor_id, application_type) - VALUES ($1, $2, $3, $4) - ` - - var err error - - if transaction == nil { - _, err = s.server.db.Exec(query, guild.ID, charID, s.charID, applicationType) - } else { - _, err = transaction.Exec(query, guild.ID, charID, s.charID, applicationType) - } - - if err != nil { - s.logger.Error( - "failed to add guild application", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - return err - } - - return nil -} - -func (guild *Guild) Disband(s *Session) error { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to begin transaction", zap.Error(err)) - return err - } - - _, err = transaction.Exec("DELETE FROM guild_characters WHERE guild_id = $1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild characters", zap.Error(err), zap.Uint32("guildId", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("DELETE FROM guilds WHERE id = $1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("DELETE FROM guild_alliances WHERE parent_id=$1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("UPDATE guild_alliances SET sub1_id=sub2_id, sub2_id=NULL WHERE sub1_id=$1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec("UPDATE guild_alliances SET sub2_id=NULL WHERE sub2_id=$1", guild.ID) - - if err != nil { - s.logger.Error("failed to remove guild from alliance", zap.Error(err), zap.Uint32("guildID", guild.ID)) - rollbackTransaction(s, transaction) - return err - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit transaction", zap.Error(err)) - return err - } - - s.logger.Info("Character disbanded guild", zap.Uint32("charID", s.charID), zap.Uint32("guildID", guild.ID)) - - return nil -} - -func (guild *Guild) RemoveCharacter(s *Session, charID uint32) error { - _, err := s.server.db.Exec("DELETE FROM guild_characters WHERE character_id=$1", charID) - - if err != nil { - s.logger.Error( - "failed to remove character from guild", - zap.Error(err), - zap.Uint32("charID", charID), - zap.Uint32("guildID", guild.ID), - ) - - return err - } - - return nil -} - -func (guild *Guild) AcceptApplication(s *Session, charID uint32) error { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to start db transaction", zap.Error(err)) - return err - } - - _, err = transaction.Exec(`DELETE FROM guild_applications WHERE character_id = $1`, charID) - - if err != nil { - s.logger.Error("failed to accept character's guild application", zap.Error(err)) - rollbackTransaction(s, transaction) - return err - } - - _, err = transaction.Exec(` - INSERT INTO guild_characters (guild_id, character_id, order_index) - VALUES ($1, $2, (SELECT MAX(order_index) + 1 FROM guild_characters WHERE guild_id = $1)) - `, guild.ID, charID) - - if err != nil { - s.logger.Error( - "failed to add applicant to guild", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - rollbackTransaction(s, transaction) - return err - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit db transaction", zap.Error(err)) - rollbackTransaction(s, transaction) - return err - } - - return nil -} - -// This is relying on the fact that invitation ID is also character ID right now -// if invitation ID changes, this will break. -func (guild *Guild) CancelInvitation(s *Session, charID uint32) error { - _, err := s.server.db.Exec( - `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'invited'`, - charID, guild.ID, - ) - - if err != nil { - s.logger.Error( - "failed to cancel guild invitation", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - return err - } - - return nil -} - -func (guild *Guild) RejectApplication(s *Session, charID uint32) error { - _, err := s.server.db.Exec( - `DELETE FROM guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = 'applied'`, - charID, guild.ID, - ) - - if err != nil { - s.logger.Error( - "failed to reject guild application", - zap.Error(err), - zap.Uint32("guildID", guild.ID), - zap.Uint32("charID", charID), - ) - return err - } - - return nil -} - -func (guild *Guild) ArrangeCharacters(s *Session, charIDs []uint32) error { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to start db transaction", zap.Error(err)) - return err - } - - for i, id := range charIDs { - _, err := transaction.Exec("UPDATE guild_characters SET order_index = $1 WHERE character_id = $2", 2+i, id) - - if err != nil { - err = transaction.Rollback() - - if err != nil { - s.logger.Error("failed to rollback db transaction", zap.Error(err)) - } - - return err - } - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit db transaction", zap.Error(err)) - return err - } - - return nil -} - -func (guild *Guild) GetApplicationForCharID(s *Session, charID uint32, applicationType GuildApplicationType) (*GuildApplication, error) { - row := s.server.db.QueryRowx(` - SELECT * from guild_applications WHERE character_id = $1 AND guild_id = $2 AND application_type = $3 - `, charID, guild.ID, applicationType) - - application := &GuildApplication{} - - err := row.StructScan(application) - - if errors.Is(err, sql.ErrNoRows) { - return nil, nil - } - - if err != nil { - s.logger.Error( - "failed to retrieve guild application for character", - zap.Error(err), - zap.Uint32("charID", charID), - zap.Uint32("guildID", guild.ID), - ) - return nil, err - } - - return application, nil -} - -func (guild *Guild) HasApplicationForCharID(s *Session, charID uint32) (bool, error) { - row := s.server.db.QueryRowx(` - SELECT 1 from guild_applications WHERE character_id = $1 AND guild_id = $2 - `, charID, guild.ID) - - num := 0 - - err := row.Scan(&num) - - if errors.Is(err, sql.ErrNoRows) { - return false, nil - } - - if err != nil { - s.logger.Error( - "failed to retrieve guild applications for character", - zap.Error(err), - zap.Uint32("charID", charID), - zap.Uint32("guildID", guild.ID), - ) - return false, err - } - - return true, nil -} - -func CreateGuild(s *Session, guildName string) (int32, error) { - transaction, err := s.server.db.Begin() - - if err != nil { - s.logger.Error("failed to start db transaction", zap.Error(err)) - return 0, err - } - - if err != nil { - panic(err) - } - - guildResult, err := transaction.Query( - "INSERT INTO guilds (name, leader_id) VALUES ($1, $2) RETURNING id", - guildName, s.charID, - ) - - if err != nil { - s.logger.Error("failed to create guild", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - var guildId int32 - - guildResult.Next() - - err = guildResult.Scan(&guildId) - - if err != nil { - s.logger.Error("failed to retrieve guild ID", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - err = guildResult.Close() - - if err != nil { - s.logger.Error("failed to finalise query", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - _, err = transaction.Exec(` - INSERT INTO guild_characters (guild_id, character_id) - VALUES ($1, $2) - `, guildId, s.charID) - - if err != nil { - s.logger.Error("failed to add character to guild", zap.Error(err)) - rollbackTransaction(s, transaction) - return 0, err - } - - err = transaction.Commit() - - if err != nil { - s.logger.Error("failed to commit guild creation", zap.Error(err)) - return 0, err - } - - return guildId, nil -} - -func rollbackTransaction(s *Session, transaction *sql.Tx) { - err := transaction.Rollback() - - if err != nil { - s.logger.Error("failed to rollback transaction", zap.Error(err)) - } -} - -func GetGuildInfoByID(s *Session, guildID uint32) (*Guild, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf(` - %s - WHERE g.id = $1 - LIMIT 1 - `, guildInfoSelectQuery), guildID) - - if err != nil { - s.logger.Error("failed to retrieve guild", zap.Error(err), zap.Uint32("guildID", guildID)) - return nil, err - } - - defer func() { _ = rows.Close() }() - - hasRow := rows.Next() - - if !hasRow { - return nil, nil - } - - return buildGuildObjectFromDbResult(rows, err, s) -} - -func GetGuildInfoByCharacterId(s *Session, charID uint32) (*Guild, error) { - rows, err := s.server.db.Queryx(fmt.Sprintf(` - %s - WHERE EXISTS( - SELECT 1 - FROM guild_characters gc1 - WHERE gc1.character_id = $1 - AND gc1.guild_id = g.id - ) - OR EXISTS( - SELECT 1 - FROM guild_applications ga - WHERE ga.character_id = $1 - AND ga.guild_id = g.id - AND ga.application_type = 'applied' - ) - LIMIT 1 - `, guildInfoSelectQuery), charID) - - if err != nil { - s.logger.Error("failed to retrieve guild for character", zap.Error(err), zap.Uint32("charID", charID)) - return nil, err - } - - defer func() { _ = rows.Close() }() - - hasRow := rows.Next() - - if !hasRow { - return nil, nil - } - - return buildGuildObjectFromDbResult(rows, err, s) -} - -func buildGuildObjectFromDbResult(result *sqlx.Rows, _ error, s *Session) (*Guild, error) { - guild := &Guild{} - - err := result.StructScan(guild) - - if err != nil { - s.logger.Error("failed to retrieve guild data from database", zap.Error(err)) - return nil, err - } - - return guild, nil -} - func handleMsgMhfCreateGuild(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfCreateGuild) @@ -846,23 +244,6 @@ func handleMsgMhfGetGuildTargetMemberNum(s *Session, p mhfpacket.MHFPacket) { doAckBufSucceed(s, pkt.AckHandle, bf.Data()) } -func guildGetItems(s *Session, guildID uint32) []mhfitem.MHFItemStack { - var data []byte - var items []mhfitem.MHFItemStack - if err := s.server.db.QueryRow(`SELECT item_box FROM guilds WHERE id=$1`, guildID).Scan(&data); err != nil && !errors.Is(err, sql.ErrNoRows) { - s.logger.Error("Failed to get guild item box", zap.Error(err)) - } - if len(data) > 0 { - box := byteframe.NewByteFrameFromBytes(data) - numStacks := box.ReadUint16() - box.ReadUint16() // Unused - for i := 0; i < int(numStacks); i++ { - items = append(items, mhfitem.ReadWarehouseItem(box)) - } - } - return items -} - func handleMsgMhfEnumerateGuildItem(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfEnumerateGuildItem) items := guildGetItems(s, pkt.GuildID) diff --git a/server/channelserver/handlers_seibattle.go b/server/channelserver/handlers_seibattle.go index caf5c19c9..f8d6c8aa8 100644 --- a/server/channelserver/handlers_seibattle.go +++ b/server/channelserver/handlers_seibattle.go @@ -3,8 +3,152 @@ package channelserver import ( "erupe-ce/common/byteframe" "erupe-ce/network/mhfpacket" + "time" ) +type SeibattleTimetable struct { + Start time.Time + End time.Time +} + +type SeibattleKeyScore struct { + Unk0 uint8 + Unk1 int32 +} + +type SeibattleCareer struct { + Unk0 uint16 + Unk1 uint16 + Unk2 uint16 +} + +type SeibattleOpponent struct { + Unk0 int32 + Unk1 int8 +} + +type SeibattleConventionResult struct { + Unk0 uint32 + Unk1 uint16 + Unk2 uint16 + Unk3 uint16 + Unk4 uint16 +} + +type SeibattleCharScore struct { + Unk0 uint32 +} + +type SeibattleCurResult struct { + Unk0 uint32 + Unk1 uint16 + Unk2 uint16 + Unk3 uint16 +} + +type Seibattle struct { + Timetable []SeibattleTimetable + KeyScore []SeibattleKeyScore + Career []SeibattleCareer + Opponent []SeibattleOpponent + ConventionResult []SeibattleConventionResult + CharScore []SeibattleCharScore + CurResult []SeibattleCurResult +} + +func handleMsgMhfGetSeibattle(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfGetSeibattle) + var data []*byteframe.ByteFrame + seibattle := Seibattle{ + Timetable: []SeibattleTimetable{ + {TimeMidnight(), TimeMidnight().Add(time.Hour * 8)}, + {TimeMidnight().Add(time.Hour * 8), TimeMidnight().Add(time.Hour * 16)}, + {TimeMidnight().Add(time.Hour * 16), TimeMidnight().Add(time.Hour * 24)}, + }, + KeyScore: []SeibattleKeyScore{ + {0, 0}, + }, + Career: []SeibattleCareer{ + {0, 0, 0}, + }, + Opponent: []SeibattleOpponent{ + {1, 1}, + }, + ConventionResult: []SeibattleConventionResult{ + {0, 0, 0, 0, 0}, + }, + CharScore: []SeibattleCharScore{ + {0}, + }, + CurResult: []SeibattleCurResult{ + {0, 0, 0, 0}, + }, + } + + switch pkt.Type { + case 1: + for _, timetable := range seibattle.Timetable { + bf := byteframe.NewByteFrame() + bf.WriteUint32(uint32(timetable.Start.Unix())) + bf.WriteUint32(uint32(timetable.End.Unix())) + data = append(data, bf) + } + case 3: // Key score? + for _, keyScore := range seibattle.KeyScore { + bf := byteframe.NewByteFrame() + bf.WriteUint8(keyScore.Unk0) + bf.WriteInt32(keyScore.Unk1) + data = append(data, bf) + } + case 4: // Career? + for _, career := range seibattle.Career { + bf := byteframe.NewByteFrame() + bf.WriteUint16(career.Unk0) + bf.WriteUint16(career.Unk1) + bf.WriteUint16(career.Unk2) + data = append(data, bf) + } + case 5: // Opponent? + for _, opponent := range seibattle.Opponent { + bf := byteframe.NewByteFrame() + bf.WriteInt32(opponent.Unk0) + bf.WriteInt8(opponent.Unk1) + data = append(data, bf) + } + case 6: // Convention result? + for _, conventionResult := range seibattle.ConventionResult { + bf := byteframe.NewByteFrame() + bf.WriteUint32(conventionResult.Unk0) + bf.WriteUint16(conventionResult.Unk1) + bf.WriteUint16(conventionResult.Unk2) + bf.WriteUint16(conventionResult.Unk3) + bf.WriteUint16(conventionResult.Unk4) + data = append(data, bf) + } + case 7: // Char score? + for _, charScore := range seibattle.CharScore { + bf := byteframe.NewByteFrame() + bf.WriteUint32(charScore.Unk0) + data = append(data, bf) + } + case 8: // Cur result? + for _, curResult := range seibattle.CurResult { + bf := byteframe.NewByteFrame() + bf.WriteUint32(curResult.Unk0) + bf.WriteUint16(curResult.Unk1) + bf.WriteUint16(curResult.Unk2) + bf.WriteUint16(curResult.Unk3) + data = append(data, bf) + } + } + doAckEarthSucceed(s, pkt.AckHandle, data) +} + +func handleMsgMhfPostSeibattle(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfPostSeibattle) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + func handleMsgMhfGetBreakSeibatuLevelReward(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfGetBreakSeibatuLevelReward) bf := byteframe.NewByteFrame()