From cc7883b8a104dde5bf84bd767e0fec4a2081eed9 Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Wed, 18 Feb 2026 17:59:15 +0100 Subject: [PATCH] refactor(channelserver): split handlers.go into sub-files Extract from the 1638-line catch-all handlers.go into focused files: - handlers_helpers.go: shared doAck* helpers and updateRights - handlers_session.go: login/logout, save, system protocol handlers - handlers_items.go: items, prices, stamps, stampcard - handlers.go: remaining misc handlers (goocoo, earth, seibattle, etc.) --- server/channelserver/handlers.go | 1107 +--------------------- server/channelserver/handlers_helpers.go | 76 ++ server/channelserver/handlers_items.go | 343 +++++++ server/channelserver/handlers_session.go | 711 ++++++++++++++ 4 files changed, 1133 insertions(+), 1104 deletions(-) create mode 100644 server/channelserver/handlers_helpers.go create mode 100644 server/channelserver/handlers_items.go create mode 100644 server/channelserver/handlers_session.go diff --git a/server/channelserver/handlers.go b/server/channelserver/handlers.go index f17818bfa..a42f05ca5 100644 --- a/server/channelserver/handlers.go +++ b/server/channelserver/handlers.go @@ -1,1058 +1,16 @@ package channelserver import ( - "encoding/binary" - "erupe-ce/common/mhfcourse" - "erupe-ce/common/mhfitem" - "erupe-ce/common/mhfmon" - ps "erupe-ce/common/pascalstring" - "erupe-ce/common/stringsupport" - _config "erupe-ce/config" - "fmt" - "io" - "net" - "strings" - "time" - - "crypto/rand" "erupe-ce/common/byteframe" + _config "erupe-ce/config" "erupe-ce/network/mhfpacket" + "fmt" "math/bits" + "time" "go.uber.org/zap" ) -// Temporary function to just return no results for a MSG_MHF_ENUMERATE* packet -func stubEnumerateNoResults(s *Session, ackHandle uint32) { - enumBf := byteframe.NewByteFrame() - enumBf.WriteUint32(0) // Entry count (count for quests, rankings, events, etc.) - - doAckBufSucceed(s, ackHandle, enumBf.Data()) -} - -func doAckEarthSucceed(s *Session, ackHandle uint32, data []*byteframe.ByteFrame) { - bf := byteframe.NewByteFrame() - bf.WriteUint32(uint32(s.server.erupeConfig.EarthID)) - bf.WriteUint32(0) - bf.WriteUint32(0) - bf.WriteUint32(uint32(len(data))) - for i := range data { - bf.WriteBytes(data[i].Data()) - } - doAckBufSucceed(s, ackHandle, bf.Data()) -} - -func doAckBufSucceed(s *Session, ackHandle uint32, data []byte) { - s.QueueSendMHF(&mhfpacket.MsgSysAck{ - AckHandle: ackHandle, - IsBufferResponse: true, - ErrorCode: 0, - AckData: data, - }) -} - -func doAckBufFail(s *Session, ackHandle uint32, data []byte) { - s.QueueSendMHF(&mhfpacket.MsgSysAck{ - AckHandle: ackHandle, - IsBufferResponse: true, - ErrorCode: 1, - AckData: data, - }) -} - -func doAckSimpleSucceed(s *Session, ackHandle uint32, data []byte) { - s.QueueSendMHF(&mhfpacket.MsgSysAck{ - AckHandle: ackHandle, - IsBufferResponse: false, - ErrorCode: 0, - AckData: data, - }) -} - -func doAckSimpleFail(s *Session, ackHandle uint32, data []byte) { - s.QueueSendMHF(&mhfpacket.MsgSysAck{ - AckHandle: ackHandle, - IsBufferResponse: false, - ErrorCode: 1, - AckData: data, - }) -} - -func updateRights(s *Session) { - rightsInt := uint32(2) - _ = 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) - s.courses, rightsInt = mhfcourse.GetCourseStruct(rightsInt) - update := &mhfpacket.MsgSysUpdateRight{ - ClientRespAckHandle: 0, - Bitfield: rightsInt, - Rights: s.courses, - UnkSize: 0, - } - s.QueueSendMHFNonBlocking(update) -} - -func handleMsgHead(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysExtendThreshold(s *Session, p mhfpacket.MHFPacket) { - // No data aside from header, no resp required. -} - -func handleMsgSysEnd(s *Session, p mhfpacket.MHFPacket) { - // No data aside from header, no resp required. -} - -func handleMsgSysNop(s *Session, p mhfpacket.MHFPacket) { - // No data aside from header, no resp required. -} - -func handleMsgSysAck(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysTerminalLog(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysTerminalLog) - for i := range pkt.Entries { - s.server.logger.Info("SysTerminalLog", - zap.Uint8("Type1", pkt.Entries[i].Type1), - zap.Uint8("Type2", pkt.Entries[i].Type2), - zap.Int16("Unk0", pkt.Entries[i].Unk0), - zap.Int32("Unk1", pkt.Entries[i].Unk1), - zap.Int32("Unk2", pkt.Entries[i].Unk2), - zap.Int32("Unk3", pkt.Entries[i].Unk3), - zap.Int32s("Unk4", pkt.Entries[i].Unk4), - ) - } - resp := byteframe.NewByteFrame() - resp.WriteUint32(pkt.LogID + 1) // LogID to use for requests after this. - doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgSysLogin(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysLogin) - - if !s.server.erupeConfig.DebugOptions.DisableTokenCheck { - var token string - err := s.server.db.QueryRow("SELECT token FROM sign_sessions ss INNER JOIN public.users u on ss.user_id = u.id WHERE token=$1 AND ss.id=$2 AND u.id=(SELECT c.user_id FROM characters c WHERE c.id=$3)", pkt.LoginTokenString, pkt.LoginTokenNumber, pkt.CharID0).Scan(&token) - if err != nil { - _ = s.rawConn.Close() - s.logger.Warn(fmt.Sprintf("Invalid login token, offending CID: (%d)", pkt.CharID0)) - return - } - } - - s.Lock() - s.charID = pkt.CharID0 - s.token = pkt.LoginTokenString - s.Unlock() - - bf := byteframe.NewByteFrame() - bf.WriteUint32(uint32(TimeAdjusted().Unix())) // Unix timestamp - - _, err := s.server.db.Exec("UPDATE servers SET current_players=$1 WHERE server_id=$2", len(s.server.sessions), s.server.ID) - if err != nil { - panic(err) - } - - _, err = s.server.db.Exec("UPDATE sign_sessions SET server_id=$1, char_id=$2 WHERE token=$3", s.server.ID, s.charID, s.token) - if err != nil { - panic(err) - } - - _, err = s.server.db.Exec("UPDATE characters SET last_login=$1 WHERE id=$2", TimeAdjusted().Unix(), s.charID) - if err != nil { - panic(err) - } - - _, err = s.server.db.Exec("UPDATE users u SET last_character=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)", s.charID) - if err != nil { - panic(err) - } - - doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) - - updateRights(s) - - s.server.BroadcastMHF(&mhfpacket.MsgSysInsertUser{CharID: s.charID}, s) -} - -func handleMsgSysLogout(s *Session, p mhfpacket.MHFPacket) { - logoutPlayer(s) -} - -// saveAllCharacterData saves all character data to the database with proper error handling. -// This function ensures data persistence even if the client disconnects unexpectedly. -// It handles: -// - Main savedata blob (compressed) -// - User binary data (house, gallery, etc.) -// - Plate data (transmog appearance, storage, equipment sets) -// - Playtime updates -// - RP updates -// - Name corruption prevention -func saveAllCharacterData(s *Session, rpToAdd int) error { - saveStart := time.Now() - - // Get current savedata from database - characterSaveData, err := GetCharacterSaveData(s, s.charID) - if err != nil { - s.logger.Error("Failed to retrieve character save data", - zap.Error(err), - zap.Uint32("charID", s.charID), - zap.String("name", s.Name), - ) - return err - } - - if characterSaveData == nil { - s.logger.Warn("Character save data is nil, skipping save", - zap.Uint32("charID", s.charID), - zap.String("name", s.Name), - ) - return nil - } - - // Force name to match to prevent corruption detection issues - // This handles SJIS/UTF-8 encoding differences across game versions - if characterSaveData.Name != s.Name { - s.logger.Debug("Correcting name mismatch before save", - zap.String("savedata_name", characterSaveData.Name), - zap.String("session_name", s.Name), - zap.Uint32("charID", s.charID), - ) - characterSaveData.Name = s.Name - characterSaveData.updateSaveDataWithStruct() - } - - // Update playtime from session - if !s.playtimeTime.IsZero() { - sessionPlaytime := uint32(time.Since(s.playtimeTime).Seconds()) - s.playtime += sessionPlaytime - s.logger.Debug("Updated playtime", - zap.Uint32("session_playtime_seconds", sessionPlaytime), - zap.Uint32("total_playtime", s.playtime), - zap.Uint32("charID", s.charID), - ) - } - characterSaveData.Playtime = s.playtime - - // Update RP if any gained during session - if rpToAdd > 0 { - characterSaveData.RP += uint16(rpToAdd) - if characterSaveData.RP >= s.server.erupeConfig.GameplayOptions.MaximumRP { - characterSaveData.RP = s.server.erupeConfig.GameplayOptions.MaximumRP - s.logger.Debug("RP capped at maximum", - zap.Uint16("max_rp", s.server.erupeConfig.GameplayOptions.MaximumRP), - zap.Uint32("charID", s.charID), - ) - } - s.logger.Debug("Added RP", - zap.Int("rp_gained", rpToAdd), - zap.Uint16("new_rp", characterSaveData.RP), - zap.Uint32("charID", s.charID), - ) - } - - // Save to database (main savedata + user_binary) - characterSaveData.Save(s) - - // Save auxiliary data types - // Note: Plate data saves immediately when client sends save packets, - // so this is primarily a safety net for monitoring and consistency - if err := savePlateDataToDatabase(s); err != nil { - s.logger.Error("Failed to save plate data during logout", - zap.Error(err), - zap.Uint32("charID", s.charID), - ) - // Don't return error - continue with logout even if plate save fails - } - - saveDuration := time.Since(saveStart) - s.logger.Info("Saved character data successfully", - zap.Uint32("charID", s.charID), - zap.String("name", s.Name), - zap.Duration("duration", saveDuration), - zap.Int("rp_added", rpToAdd), - zap.Uint32("playtime", s.playtime), - ) - - return nil -} - -func logoutPlayer(s *Session) { - logoutStart := time.Now() - - // Log logout initiation with session details - sessionDuration := time.Duration(0) - if s.sessionStart > 0 { - sessionDuration = time.Since(time.Unix(s.sessionStart, 0)) - } - - s.logger.Info("Player logout initiated", - zap.Uint32("charID", s.charID), - zap.String("name", s.Name), - zap.Duration("session_duration", sessionDuration), - ) - - // Calculate session metrics FIRST (before cleanup) - var timePlayed int - var sessionTime int - var rpGained int - - if s.charID != 0 { - _ = s.server.db.QueryRow("SELECT time_played FROM characters WHERE id = $1", s.charID).Scan(&timePlayed) - sessionTime = int(TimeAdjusted().Unix()) - int(s.sessionStart) - timePlayed += sessionTime - - if mhfcourse.CourseExists(30, s.courses) { - rpGained = timePlayed / 900 - timePlayed = timePlayed % 900 - if _, err := s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID); err != nil { - s.logger.Error("Failed to update cafe time", zap.Error(err)) - } - } else { - rpGained = timePlayed / 1800 - timePlayed = timePlayed % 1800 - } - - s.logger.Debug("Session metrics calculated", - zap.Uint32("charID", s.charID), - zap.Int("session_time_seconds", sessionTime), - zap.Int("rp_gained", rpGained), - zap.Int("time_played_remainder", timePlayed), - ) - - // Save all character data ONCE with all updates - // This is the safety net that ensures data persistence even if client - // didn't send save packets before disconnecting - if err := saveAllCharacterData(s, rpGained); err != nil { - s.logger.Error("Failed to save character data during logout", - zap.Error(err), - zap.Uint32("charID", s.charID), - zap.String("name", s.Name), - ) - // Continue with logout even if save fails - } - - // Update time_played and guild treasure hunt - if _, err := s.server.db.Exec("UPDATE characters SET time_played = $1 WHERE id = $2", timePlayed, s.charID); err != nil { - s.logger.Error("Failed to update time played", zap.Error(err)) - } - if _, err := s.server.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE character_id=$1`, s.charID); err != nil { - s.logger.Error("Failed to clear treasure hunt", zap.Error(err)) - } - } - - // NOW do cleanup (after save is complete) - s.server.Lock() - delete(s.server.sessions, s.rawConn) - _ = s.rawConn.Close() - s.server.Unlock() - - // Stage cleanup - for _, stage := range s.server.stages { - // Tell sessions registered to disconnecting players quest to unregister - if stage.host != nil && stage.host.charID == s.charID { - for _, sess := range s.server.sessions { - for rSlot := range stage.reservedClientSlots { - if sess.charID == rSlot && sess.stage != nil && sess.stage.id[3:5] != "Qs" { - sess.QueueSendMHFNonBlocking(&mhfpacket.MsgSysStageDestruct{}) - } - } - } - } - for session := range stage.clients { - if session.charID == s.charID { - delete(stage.clients, session) - } - } - } - - // Update sign sessions and server player count - _, err := s.server.db.Exec("UPDATE sign_sessions SET server_id=NULL, char_id=NULL WHERE token=$1", s.token) - if err != nil { - panic(err) - } - - _, err = s.server.db.Exec("UPDATE servers SET current_players=$1 WHERE server_id=$2", len(s.server.sessions), s.server.ID) - if err != nil { - panic(err) - } - - if s.stage == nil { - logoutDuration := time.Since(logoutStart) - s.logger.Info("Player logout completed", - zap.Uint32("charID", s.charID), - zap.String("name", s.Name), - zap.Duration("logout_duration", logoutDuration), - ) - return - } - - // Broadcast user deletion and final cleanup - s.server.BroadcastMHF(&mhfpacket.MsgSysDeleteUser{ - CharID: s.charID, - }, s) - - s.server.Lock() - for _, stage := range s.server.stages { - delete(stage.reservedClientSlots, s.charID) - } - s.server.Unlock() - - removeSessionFromSemaphore(s) - removeSessionFromStage(s) - - logoutDuration := time.Since(logoutStart) - s.logger.Info("Player logout completed", - zap.Uint32("charID", s.charID), - zap.String("name", s.Name), - zap.Duration("logout_duration", logoutDuration), - zap.Int("rp_gained", rpGained), - ) -} - -func handleMsgSysSetStatus(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysPing(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysPing) - doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) -} - -func handleMsgSysTime(s *Session, p mhfpacket.MHFPacket) { - resp := &mhfpacket.MsgSysTime{ - GetRemoteTime: false, - Timestamp: uint32(TimeAdjusted().Unix()), // JP timezone - } - s.QueueSendMHF(resp) - s.notifyRavi() -} - -func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysIssueLogkey) - - // Make a random log key for this session. - logKey := make([]byte, 16) - _, err := rand.Read(logKey) - if err != nil { - panic(err) - } - - // TODO(Andoryuuta): In the offical client, the log key index is off by one, - // cutting off the last byte in _most uses_. Find and document these accordingly. - s.Lock() - s.logKey = logKey - s.Unlock() - - // Issue it. - resp := byteframe.NewByteFrame() - resp.WriteBytes(logKey) - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgSysRecordLog(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysRecordLog) - if _config.ErupeConfig.RealClientMode == _config.ZZ { - bf := byteframe.NewByteFrameFromBytes(pkt.Data) - _, _ = bf.Seek(32, 0) - var val uint8 - for i := 0; i < 176; i++ { - val = bf.ReadUint8() - if val > 0 && mhfmon.Monsters[i].Large { - if _, err := s.server.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, s.charID, i, val, TimeAdjusted()); err != nil { - s.logger.Error("Failed to insert kill log", zap.Error(err)) - } - } - } - } - // remove a client returning to town from reserved slots to make sure the stage is hidden from board - delete(s.stage.reservedClientSlots, s.charID) - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) -} - -func handleMsgSysEcho(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysLockGlobalSema(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysLockGlobalSema) - var sgid string - for _, channel := range s.server.Channels { - for id := range channel.stages { - if strings.HasSuffix(id, pkt.UserIDString) { - sgid = channel.GlobalID - } - } - } - bf := byteframe.NewByteFrame() - if len(sgid) > 0 && sgid != s.server.GlobalID { - bf.WriteUint8(0) - bf.WriteUint8(0) - ps.Uint16(bf, sgid, false) - } else { - bf.WriteUint8(2) - bf.WriteUint8(0) - ps.Uint16(bf, pkt.ServerChannelIDString, false) - } - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) -} - -func handleMsgSysUnlockGlobalSema(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysUnlockGlobalSema) - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) -} - -func handleMsgSysUpdateRight(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysAuthQuery(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysAuthTerminal(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysRightsReload(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgSysRightsReload) - updateRights(s) - doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) -} - -func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfTransitMessage) - - local := strings.Split(s.rawConn.RemoteAddr().String(), ":")[0] == "127.0.0.1" - - var maxResults, port, count uint16 - var cid uint32 - var term, ip string - bf := byteframe.NewByteFrameFromBytes(pkt.MessageData) - switch pkt.SearchType { - case 1: - maxResults = 1 - cid = bf.ReadUint32() - case 2: - bf.ReadUint16() // term length - maxResults = bf.ReadUint16() - bf.ReadUint8() // Unk - term = stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes()) - case 3: - _ip := bf.ReadBytes(4) - ip = fmt.Sprintf("%d.%d.%d.%d", _ip[3], _ip[2], _ip[1], _ip[0]) - port = bf.ReadUint16() - bf.ReadUint16() // term length - maxResults = bf.ReadUint16() - bf.ReadUint8() - term = string(bf.ReadNullTerminatedBytes()) - } - - resp := byteframe.NewByteFrame() - resp.WriteUint16(0) - switch pkt.SearchType { - case 1, 2, 3: // usersearchidx, usersearchname, lobbysearchname - for _, c := range s.server.Channels { - for _, session := range c.sessions { - if count == maxResults { - break - } - if pkt.SearchType == 1 && session.charID != cid { - continue - } - if pkt.SearchType == 2 && !strings.Contains(session.Name, term) { - continue - } - if pkt.SearchType == 3 && session.server.IP != ip && session.server.Port != port && session.stage.id != term { - continue - } - count++ - sessionName := stringsupport.UTF8ToSJIS(session.Name) - sessionStage := stringsupport.UTF8ToSJIS(session.stage.id) - if !local { - resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4())) - } else { - resp.WriteUint32(0x0100007F) - } - resp.WriteUint16(c.Port) - resp.WriteUint32(session.charID) - resp.WriteUint8(uint8(len(sessionStage) + 1)) - resp.WriteUint8(uint8(len(sessionName) + 1)) - resp.WriteUint16(uint16(len(c.userBinaryParts[userBinaryPartID{charID: session.charID, index: 3}]))) - - // TODO: This case might be <=G2 - if _config.ErupeConfig.RealClientMode <= _config.G1 { - resp.WriteBytes(make([]byte, 8)) - } else { - resp.WriteBytes(make([]byte, 40)) - } - resp.WriteBytes(make([]byte, 8)) - - resp.WriteNullTerminatedBytes(sessionStage) - resp.WriteNullTerminatedBytes(sessionName) - resp.WriteBytes(c.userBinaryParts[userBinaryPartID{session.charID, 3}]) - } - } - case 4: // lobbysearch - type FindPartyParams struct { - StagePrefix string - RankRestriction int16 - Targets []int16 - Unk0 []int16 - Unk1 []int16 - QuestID []int16 - } - findPartyParams := FindPartyParams{ - StagePrefix: "sl2Ls210", - } - numParams := bf.ReadUint8() - maxResults = bf.ReadUint16() - for i := uint8(0); i < numParams; i++ { - switch bf.ReadUint8() { - case 0: - values := bf.ReadUint8() - for i := uint8(0); i < values; i++ { - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - findPartyParams.RankRestriction = bf.ReadInt16() - } else { - findPartyParams.RankRestriction = int16(bf.ReadInt8()) - } - } - case 1: - values := bf.ReadUint8() - for i := uint8(0); i < values; i++ { - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - findPartyParams.Targets = append(findPartyParams.Targets, bf.ReadInt16()) - } else { - findPartyParams.Targets = append(findPartyParams.Targets, int16(bf.ReadInt8())) - } - } - case 2: - values := bf.ReadUint8() - for i := uint8(0); i < values; i++ { - var value int16 - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - value = bf.ReadInt16() - } else { - value = int16(bf.ReadInt8()) - } - switch value { - case 0: // Public Bar - findPartyParams.StagePrefix = "sl2Ls210" - case 1: // Tokotoko Partnya - findPartyParams.StagePrefix = "sl2Ls463" - case 2: // Hunting Prowess Match - findPartyParams.StagePrefix = "sl2Ls286" - case 3: // Volpakkun Together - findPartyParams.StagePrefix = "sl2Ls465" - case 5: // Quick Party - // Unk - } - } - case 3: // Unknown - values := bf.ReadUint8() - for i := uint8(0); i < values; i++ { - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - findPartyParams.Unk0 = append(findPartyParams.Unk0, bf.ReadInt16()) - } else { - findPartyParams.Unk0 = append(findPartyParams.Unk0, int16(bf.ReadInt8())) - } - } - case 4: // Looking for n or already have n - values := bf.ReadUint8() - for i := uint8(0); i < values; i++ { - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - findPartyParams.Unk1 = append(findPartyParams.Unk1, bf.ReadInt16()) - } else { - findPartyParams.Unk1 = append(findPartyParams.Unk1, int16(bf.ReadInt8())) - } - } - case 5: - values := bf.ReadUint8() - for i := uint8(0); i < values; i++ { - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - findPartyParams.QuestID = append(findPartyParams.QuestID, bf.ReadInt16()) - } else { - findPartyParams.QuestID = append(findPartyParams.QuestID, int16(bf.ReadInt8())) - } - } - } - } - for _, c := range s.server.Channels { - for _, stage := range c.stages { - if count == maxResults { - break - } - if strings.HasPrefix(stage.id, findPartyParams.StagePrefix) { - sb3 := byteframe.NewByteFrameFromBytes(stage.rawBinaryData[stageBinaryKey{1, 3}]) - _, _ = sb3.Seek(4, 0) - - stageDataParams := 7 - if _config.ErupeConfig.RealClientMode <= _config.G10 { - stageDataParams = 4 - } else if _config.ErupeConfig.RealClientMode <= _config.Z1 { - stageDataParams = 6 - } - - var stageData []int16 - for i := 0; i < stageDataParams; i++ { - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - stageData = append(stageData, sb3.ReadInt16()) - } else { - stageData = append(stageData, int16(sb3.ReadInt8())) - } - } - - if findPartyParams.RankRestriction >= 0 { - if stageData[0] > findPartyParams.RankRestriction { - continue - } - } - - var hasTarget bool - if len(findPartyParams.Targets) > 0 { - for _, target := range findPartyParams.Targets { - if target == stageData[1] { - hasTarget = true - break - } - } - if !hasTarget { - continue - } - } - - count++ - if !local { - resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4())) - } else { - resp.WriteUint32(0x0100007F) - } - resp.WriteUint16(c.Port) - - resp.WriteUint16(0) // Static? - resp.WriteUint16(0) // Unk, [0 1 2] - resp.WriteUint16(uint16(len(stage.clients) + len(stage.reservedClientSlots))) - resp.WriteUint16(stage.maxPlayers) - // TODO: Retail returned the number of clients in quests, not workshop/my series - resp.WriteUint16(uint16(len(stage.reservedClientSlots))) - - resp.WriteUint8(0) // Static? - resp.WriteUint8(uint8(stage.maxPlayers)) - resp.WriteUint8(1) // Static? - resp.WriteUint8(uint8(len(stage.id) + 1)) - resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 0}]))) - resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 1}]))) - - for i := range stageData { - if _config.ErupeConfig.RealClientMode >= _config.Z1 { - resp.WriteInt16(stageData[i]) - } else { - resp.WriteInt8(int8(stageData[i])) - } - } - resp.WriteUint8(0) // Unk - resp.WriteUint8(0) // Unk - - resp.WriteNullTerminatedBytes([]byte(stage.id)) - resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 0}]) - resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 1}]) - } - } - } - } - _, _ = resp.Seek(0, io.SeekStart) - resp.WriteUint16(count) - doAckBufSucceed(s, pkt.AckHandle, resp.Data()) -} - -func handleMsgCaExchangeItem(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfServerCommand(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfAnnounce(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfAnnounce) - s.server.BroadcastRaviente(pkt.IPAddress, pkt.Port, pkt.StageID, pkt.Data.ReadUint8()) - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) -} - -func handleMsgMhfSetLoginwindow(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysTransBinary(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysCollectBinary(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysGetState(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysSerialize(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysEnumlobby(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysEnumuser(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgSysInfokyserver(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfGetCaUniqueID(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfTransferItem(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfTransferItem) - doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) -} - -func handleMsgMhfEnumeratePrice(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfEnumeratePrice) - bf := byteframe.NewByteFrame() - var lbPrices []struct { - Unk0 uint16 - Unk1 uint16 - Unk2 uint32 - } - var wantedList []struct { - Unk0 uint32 - Unk1 uint32 - Unk2 uint32 - Unk3 uint16 - Unk4 uint16 - Unk5 uint16 - Unk6 uint16 - Unk7 uint16 - Unk8 uint16 - Unk9 uint16 - } - gzPrices := []struct { - Unk0 uint16 - Gz uint16 - Unk1 uint16 - Unk2 uint16 - MonID uint16 - Unk3 uint16 - Unk4 uint8 - }{ - {0, 1000, 0, 0, mhfmon.Pokaradon, 100, 1}, - {0, 800, 0, 0, mhfmon.YianKutKu, 100, 1}, - {0, 800, 0, 0, mhfmon.DaimyoHermitaur, 100, 1}, - {0, 1100, 0, 0, mhfmon.Farunokku, 100, 1}, - {0, 900, 0, 0, mhfmon.Congalala, 100, 1}, - {0, 900, 0, 0, mhfmon.Gypceros, 100, 1}, - {0, 1300, 0, 0, mhfmon.Hyujikiki, 100, 1}, - {0, 1000, 0, 0, mhfmon.Basarios, 100, 1}, - {0, 1000, 0, 0, mhfmon.Rathian, 100, 1}, - {0, 800, 0, 0, mhfmon.ShogunCeanataur, 100, 1}, - {0, 1400, 0, 0, mhfmon.Midogaron, 100, 1}, - {0, 900, 0, 0, mhfmon.Blangonga, 100, 1}, - {0, 1100, 0, 0, mhfmon.Rathalos, 100, 1}, - {0, 1000, 0, 0, mhfmon.Khezu, 100, 1}, - {0, 1600, 0, 0, mhfmon.Giaorugu, 100, 1}, - {0, 1100, 0, 0, mhfmon.Gravios, 100, 1}, - {0, 1400, 0, 0, mhfmon.Tigrex, 100, 1}, - {0, 1000, 0, 0, mhfmon.Pariapuria, 100, 1}, - {0, 1700, 0, 0, mhfmon.Anorupatisu, 100, 1}, - {0, 1500, 0, 0, mhfmon.Lavasioth, 100, 1}, - {0, 1500, 0, 0, mhfmon.Espinas, 100, 1}, - {0, 1600, 0, 0, mhfmon.Rajang, 100, 1}, - {0, 1800, 0, 0, mhfmon.Rebidiora, 100, 1}, - {0, 1100, 0, 0, mhfmon.YianGaruga, 100, 1}, - {0, 1500, 0, 0, mhfmon.AqraVashimu, 100, 1}, - {0, 1600, 0, 0, mhfmon.Gurenzeburu, 100, 1}, - {0, 1500, 0, 0, mhfmon.Dyuragaua, 100, 1}, - {0, 1300, 0, 0, mhfmon.Gougarf, 100, 1}, - {0, 1000, 0, 0, mhfmon.Shantien, 100, 1}, - {0, 1800, 0, 0, mhfmon.Disufiroa, 100, 1}, - {0, 600, 0, 0, mhfmon.Velocidrome, 100, 1}, - {0, 600, 0, 0, mhfmon.Gendrome, 100, 1}, - {0, 700, 0, 0, mhfmon.Iodrome, 100, 1}, - {0, 1700, 0, 0, mhfmon.Baruragaru, 100, 1}, - {0, 800, 0, 0, mhfmon.Cephadrome, 100, 1}, - {0, 1000, 0, 0, mhfmon.Plesioth, 100, 1}, - {0, 1800, 0, 0, mhfmon.Zerureusu, 100, 1}, - {0, 1100, 0, 0, mhfmon.Diablos, 100, 1}, - {0, 1600, 0, 0, mhfmon.Berukyurosu, 100, 1}, - {0, 2000, 0, 0, mhfmon.Fatalis, 100, 1}, - {0, 1500, 0, 0, mhfmon.BlackGravios, 100, 1}, - {0, 1600, 0, 0, mhfmon.GoldRathian, 100, 1}, - {0, 1900, 0, 0, mhfmon.Meraginasu, 100, 1}, - {0, 700, 0, 0, mhfmon.Bulldrome, 100, 1}, - {0, 900, 0, 0, mhfmon.NonoOrugaron, 100, 1}, - {0, 1600, 0, 0, mhfmon.KamuOrugaron, 100, 1}, - {0, 1700, 0, 0, mhfmon.Forokururu, 100, 1}, - {0, 1900, 0, 0, mhfmon.Diorex, 100, 1}, - {0, 1500, 0, 0, mhfmon.AqraJebia, 100, 1}, - {0, 1600, 0, 0, mhfmon.SilverRathalos, 100, 1}, - {0, 2400, 0, 0, mhfmon.CrimsonFatalis, 100, 1}, - {0, 2000, 0, 0, mhfmon.Inagami, 100, 1}, - {0, 2100, 0, 0, mhfmon.GarubaDaora, 100, 1}, - {0, 900, 0, 0, mhfmon.Monoblos, 100, 1}, - {0, 1000, 0, 0, mhfmon.RedKhezu, 100, 1}, - {0, 900, 0, 0, mhfmon.Hypnocatrice, 100, 1}, - {0, 1700, 0, 0, mhfmon.PearlEspinas, 100, 1}, - {0, 900, 0, 0, mhfmon.PurpleGypceros, 100, 1}, - {0, 1800, 0, 0, mhfmon.Poborubarumu, 100, 1}, - {0, 1900, 0, 0, mhfmon.Lunastra, 100, 1}, - {0, 1600, 0, 0, mhfmon.Kuarusepusu, 100, 1}, - {0, 1100, 0, 0, mhfmon.PinkRathian, 100, 1}, - {0, 1200, 0, 0, mhfmon.AzureRathalos, 100, 1}, - {0, 1800, 0, 0, mhfmon.Varusaburosu, 100, 1}, - {0, 1000, 0, 0, mhfmon.Gogomoa, 100, 1}, - {0, 1600, 0, 0, mhfmon.BurningEspinas, 100, 1}, - {0, 2000, 0, 0, mhfmon.Harudomerugu, 100, 1}, - {0, 1800, 0, 0, mhfmon.Akantor, 100, 1}, - {0, 900, 0, 0, mhfmon.BrightHypnoc, 100, 1}, - {0, 2200, 0, 0, mhfmon.Gureadomosu, 100, 1}, - {0, 1200, 0, 0, mhfmon.GreenPlesioth, 100, 1}, - {0, 2400, 0, 0, mhfmon.Zinogre, 100, 1}, - {0, 1900, 0, 0, mhfmon.Gasurabazura, 100, 1}, - {0, 1300, 0, 0, mhfmon.Abiorugu, 100, 1}, - {0, 1200, 0, 0, mhfmon.BlackDiablos, 100, 1}, - {0, 1000, 0, 0, mhfmon.WhiteMonoblos, 100, 1}, - {0, 3000, 0, 0, mhfmon.Deviljho, 100, 1}, - {0, 2300, 0, 0, mhfmon.YamaKurai, 100, 1}, - {0, 2800, 0, 0, mhfmon.Brachydios, 100, 1}, - {0, 1700, 0, 0, mhfmon.Toridcless, 100, 1}, - {0, 1100, 0, 0, mhfmon.WhiteHypnoc, 100, 1}, - {0, 1500, 0, 0, mhfmon.RedLavasioth, 100, 1}, - {0, 2200, 0, 0, mhfmon.Barioth, 100, 1}, - {0, 1800, 0, 0, mhfmon.Odibatorasu, 100, 1}, - {0, 1600, 0, 0, mhfmon.Doragyurosu, 100, 1}, - {0, 900, 0, 0, mhfmon.BlueYianKutKu, 100, 1}, - {0, 2300, 0, 0, mhfmon.ToaTesukatora, 100, 1}, - {0, 2000, 0, 0, mhfmon.Uragaan, 100, 1}, - {0, 1900, 0, 0, mhfmon.Teostra, 100, 1}, - {0, 1700, 0, 0, mhfmon.Chameleos, 100, 1}, - {0, 1800, 0, 0, mhfmon.KushalaDaora, 100, 1}, - {0, 2100, 0, 0, mhfmon.Nargacuga, 100, 1}, - {0, 2600, 0, 0, mhfmon.Guanzorumu, 100, 1}, - {0, 1900, 0, 0, mhfmon.Kirin, 100, 1}, - {0, 2000, 0, 0, mhfmon.Rukodiora, 100, 1}, - {0, 2700, 0, 0, mhfmon.StygianZinogre, 100, 1}, - {0, 2200, 0, 0, mhfmon.Voljang, 100, 1}, - {0, 1800, 0, 0, mhfmon.Zenaserisu, 100, 1}, - {0, 3100, 0, 0, mhfmon.GoreMagala, 100, 1}, - {0, 3200, 0, 0, mhfmon.ShagaruMagala, 100, 1}, - {0, 3500, 0, 0, mhfmon.Eruzerion, 100, 1}, - {0, 3200, 0, 0, mhfmon.Amatsu, 100, 1}, - } - - bf.WriteUint16(uint16(len(lbPrices))) - for _, lb := range lbPrices { - bf.WriteUint16(lb.Unk0) - bf.WriteUint16(lb.Unk1) - bf.WriteUint32(lb.Unk2) - } - bf.WriteUint16(uint16(len(wantedList))) - for _, wanted := range wantedList { - bf.WriteUint32(wanted.Unk0) - bf.WriteUint32(wanted.Unk1) - bf.WriteUint32(wanted.Unk2) - bf.WriteUint16(wanted.Unk3) - bf.WriteUint16(wanted.Unk4) - bf.WriteUint16(wanted.Unk5) - bf.WriteUint16(wanted.Unk6) - bf.WriteUint16(wanted.Unk7) - bf.WriteUint16(wanted.Unk8) - bf.WriteUint16(wanted.Unk9) - } - bf.WriteUint8(uint8(len(gzPrices))) - for _, gz := range gzPrices { - bf.WriteUint16(gz.Unk0) - bf.WriteUint16(gz.Gz) - bf.WriteUint16(gz.Unk1) - bf.WriteUint16(gz.Unk2) - bf.WriteUint16(gz.MonID) - bf.WriteUint16(gz.Unk3) - bf.WriteUint8(gz.Unk4) - } - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) -} - -func handleMsgMhfEnumerateOrder(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfEnumerateOrder) - stubEnumerateNoResults(s, pkt.AckHandle) -} - -func handleMsgMhfGetExtraInfo(s *Session, p mhfpacket.MHFPacket) {} - -func userGetItems(s *Session) []mhfitem.MHFItemStack { - var data []byte - var items []mhfitem.MHFItemStack - _ = s.server.db.QueryRow(`SELECT item_box FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&data) - 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 handleMsgMhfEnumerateUnionItem(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfEnumerateUnionItem) - items := userGetItems(s) - bf := byteframe.NewByteFrame() - bf.WriteBytes(mhfitem.SerializeWarehouseItems(items)) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) -} - -func handleMsgMhfUpdateUnionItem(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfUpdateUnionItem) - newStacks := mhfitem.DiffItemStacks(userGetItems(s), pkt.UpdatedItems) - if _, err := s.server.db.Exec(`UPDATE users u SET item_box=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, mhfitem.SerializeWarehouseItems(newStacks), s.charID); err != nil { - s.logger.Error("Failed to update union item box", zap.Error(err)) - } - doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) -} - -func handleMsgMhfGetCogInfo(s *Session, p mhfpacket.MHFPacket) {} - -func handleMsgMhfCheckWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfCheckWeeklyStamp) - var total, redeemed, updated uint16 - var lastCheck time.Time - err := s.server.db.QueryRow(fmt.Sprintf("SELECT %s_checked FROM stamps WHERE character_id=$1", pkt.StampType), s.charID).Scan(&lastCheck) - if err != nil { - lastCheck = TimeAdjusted() - if _, err := s.server.db.Exec("INSERT INTO stamps (character_id, hl_checked, ex_checked) VALUES ($1, $2, $2)", s.charID, TimeAdjusted()); err != nil { - s.logger.Error("Failed to insert stamps record", zap.Error(err)) - } - } else { - if _, err := s.server.db.Exec(fmt.Sprintf(`UPDATE stamps SET %s_checked=$1 WHERE character_id=$2`, pkt.StampType), TimeAdjusted(), s.charID); err != nil { - s.logger.Error("Failed to update stamp check time", zap.Error(err)) - } - } - - if lastCheck.Before(TimeWeekStart()) { - if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE stamps SET %s_total=%s_total+1 WHERE character_id=$1", pkt.StampType, pkt.StampType), s.charID); err != nil { - s.logger.Error("Failed to increment stamp total", zap.Error(err)) - } - updated = 1 - } - - _ = s.server.db.QueryRow(fmt.Sprintf("SELECT %s_total, %s_redeemed FROM stamps WHERE character_id=$1", pkt.StampType, pkt.StampType), s.charID).Scan(&total, &redeemed) - bf := byteframe.NewByteFrame() - bf.WriteUint16(total) - bf.WriteUint16(redeemed) - bf.WriteUint16(updated) - bf.WriteUint16(0) - bf.WriteUint16(0) - bf.WriteUint32(uint32(TimeWeekStart().Unix())) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) -} - -func handleMsgMhfExchangeWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfExchangeWeeklyStamp) - var total, redeemed uint16 - var tktStack mhfitem.MHFItemStack - if pkt.Unk1 == 10 { // Yearly Sub Ex - _ = s.server.db.QueryRow("UPDATE stamps SET hl_total=hl_total-48, hl_redeemed=hl_redeemed-48 WHERE character_id=$1 RETURNING hl_total, hl_redeemed", s.charID).Scan(&total, &redeemed) - tktStack = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: 2210}, Quantity: 1} - } else { - _ = s.server.db.QueryRow(fmt.Sprintf("UPDATE stamps SET %s_redeemed=%s_redeemed+8 WHERE character_id=$1 RETURNING %s_total, %s_redeemed", pkt.StampType, pkt.StampType, pkt.StampType, pkt.StampType), s.charID).Scan(&total, &redeemed) - if pkt.StampType == "hl" { - tktStack = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: 1630}, Quantity: 5} - } else { - tktStack = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: 1631}, Quantity: 5} - } - } - addWarehouseItem(s, tktStack) - bf := byteframe.NewByteFrame() - bf.WriteUint16(total) - bf.WriteUint16(redeemed) - bf.WriteUint16(0) - bf.WriteUint16(tktStack.Item.ItemID) - bf.WriteUint16(tktStack.Quantity) - bf.WriteUint32(uint32(TimeWeekStart().Unix())) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) -} - func getGoocooData(s *Session, cid uint32) [][]byte { var goocoo []byte var goocoos [][]byte @@ -1210,65 +168,6 @@ func handleMsgMhfUpdateEtcPoint(s *Session, p mhfpacket.MHFPacket) { doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) } -func handleMsgMhfStampcardStamp(s *Session, p mhfpacket.MHFPacket) { - pkt := p.(*mhfpacket.MsgMhfStampcardStamp) - - rewards := []struct { - HR uint16 - Item1 uint16 - Quantity1 uint16 - Item2 uint16 - Quantity2 uint16 - }{ - {0, 6164, 1, 6164, 2}, - {50, 6164, 2, 6164, 3}, - {100, 6164, 3, 5392, 1}, - {300, 5392, 1, 5392, 3}, - {999, 5392, 1, 5392, 4}, - } - if _config.ErupeConfig.RealClientMode <= _config.Z1 { - for _, reward := range rewards { - if pkt.HR >= reward.HR { - pkt.Item1 = reward.Item1 - pkt.Quantity1 = reward.Quantity1 - pkt.Item2 = reward.Item2 - pkt.Quantity2 = reward.Quantity2 - } - } - } - - bf := byteframe.NewByteFrame() - bf.WriteUint16(pkt.HR) - if _config.ErupeConfig.RealClientMode >= _config.G1 { - bf.WriteUint16(pkt.GR) - } - var stamps, rewardTier, rewardUnk uint16 - reward := mhfitem.MHFItemStack{Item: mhfitem.MHFItem{}} - _ = s.server.db.QueryRow(`UPDATE characters SET stampcard = stampcard + $1 WHERE id = $2 RETURNING stampcard`, pkt.Stamps, s.charID).Scan(&stamps) - bf.WriteUint16(stamps - pkt.Stamps) - bf.WriteUint16(stamps) - - if stamps/30 > (stamps-pkt.Stamps)/30 { - rewardTier = 2 - rewardUnk = pkt.Reward2 - reward = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: pkt.Item2}, Quantity: pkt.Quantity2} - addWarehouseItem(s, reward) - } else if stamps/15 > (stamps-pkt.Stamps)/15 { - rewardTier = 1 - rewardUnk = pkt.Reward1 - reward = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: pkt.Item1}, Quantity: pkt.Quantity1} - addWarehouseItem(s, reward) - } - - bf.WriteUint16(rewardTier) - bf.WriteUint16(rewardUnk) - bf.WriteUint16(reward.Item.ItemID) - bf.WriteUint16(reward.Quantity) - doAckBufSucceed(s, pkt.AckHandle, bf.Data()) -} - -func handleMsgMhfStampcardPrize(s *Session, p mhfpacket.MHFPacket) {} - func handleMsgMhfUnreserveSrg(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfUnreserveSrg) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) diff --git a/server/channelserver/handlers_helpers.go b/server/channelserver/handlers_helpers.go new file mode 100644 index 000000000..e4a09e0e4 --- /dev/null +++ b/server/channelserver/handlers_helpers.go @@ -0,0 +1,76 @@ +package channelserver + +import ( + "erupe-ce/common/byteframe" + "erupe-ce/common/mhfcourse" + "erupe-ce/network/mhfpacket" +) + +// Temporary function to just return no results for a MSG_MHF_ENUMERATE* packet +func stubEnumerateNoResults(s *Session, ackHandle uint32) { + enumBf := byteframe.NewByteFrame() + enumBf.WriteUint32(0) // Entry count (count for quests, rankings, events, etc.) + + doAckBufSucceed(s, ackHandle, enumBf.Data()) +} + +func doAckEarthSucceed(s *Session, ackHandle uint32, data []*byteframe.ByteFrame) { + bf := byteframe.NewByteFrame() + bf.WriteUint32(uint32(s.server.erupeConfig.EarthID)) + bf.WriteUint32(0) + bf.WriteUint32(0) + bf.WriteUint32(uint32(len(data))) + for i := range data { + bf.WriteBytes(data[i].Data()) + } + doAckBufSucceed(s, ackHandle, bf.Data()) +} + +func doAckBufSucceed(s *Session, ackHandle uint32, data []byte) { + s.QueueSendMHF(&mhfpacket.MsgSysAck{ + AckHandle: ackHandle, + IsBufferResponse: true, + ErrorCode: 0, + AckData: data, + }) +} + +func doAckBufFail(s *Session, ackHandle uint32, data []byte) { + s.QueueSendMHF(&mhfpacket.MsgSysAck{ + AckHandle: ackHandle, + IsBufferResponse: true, + ErrorCode: 1, + AckData: data, + }) +} + +func doAckSimpleSucceed(s *Session, ackHandle uint32, data []byte) { + s.QueueSendMHF(&mhfpacket.MsgSysAck{ + AckHandle: ackHandle, + IsBufferResponse: false, + ErrorCode: 0, + AckData: data, + }) +} + +func doAckSimpleFail(s *Session, ackHandle uint32, data []byte) { + s.QueueSendMHF(&mhfpacket.MsgSysAck{ + AckHandle: ackHandle, + IsBufferResponse: false, + ErrorCode: 1, + AckData: data, + }) +} + +func updateRights(s *Session) { + rightsInt := uint32(2) + _ = 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) + s.courses, rightsInt = mhfcourse.GetCourseStruct(rightsInt) + update := &mhfpacket.MsgSysUpdateRight{ + ClientRespAckHandle: 0, + Bitfield: rightsInt, + Rights: s.courses, + UnkSize: 0, + } + s.QueueSendMHFNonBlocking(update) +} diff --git a/server/channelserver/handlers_items.go b/server/channelserver/handlers_items.go new file mode 100644 index 000000000..a4c783b39 --- /dev/null +++ b/server/channelserver/handlers_items.go @@ -0,0 +1,343 @@ +package channelserver + +import ( + "erupe-ce/common/byteframe" + "erupe-ce/common/mhfitem" + "erupe-ce/common/mhfmon" + _config "erupe-ce/config" + "erupe-ce/network/mhfpacket" + "fmt" + "time" + + "go.uber.org/zap" +) + +func handleMsgMhfTransferItem(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfTransferItem) + doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) +} + +func handleMsgMhfEnumeratePrice(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEnumeratePrice) + bf := byteframe.NewByteFrame() + var lbPrices []struct { + Unk0 uint16 + Unk1 uint16 + Unk2 uint32 + } + var wantedList []struct { + Unk0 uint32 + Unk1 uint32 + Unk2 uint32 + Unk3 uint16 + Unk4 uint16 + Unk5 uint16 + Unk6 uint16 + Unk7 uint16 + Unk8 uint16 + Unk9 uint16 + } + gzPrices := []struct { + Unk0 uint16 + Gz uint16 + Unk1 uint16 + Unk2 uint16 + MonID uint16 + Unk3 uint16 + Unk4 uint8 + }{ + {0, 1000, 0, 0, mhfmon.Pokaradon, 100, 1}, + {0, 800, 0, 0, mhfmon.YianKutKu, 100, 1}, + {0, 800, 0, 0, mhfmon.DaimyoHermitaur, 100, 1}, + {0, 1100, 0, 0, mhfmon.Farunokku, 100, 1}, + {0, 900, 0, 0, mhfmon.Congalala, 100, 1}, + {0, 900, 0, 0, mhfmon.Gypceros, 100, 1}, + {0, 1300, 0, 0, mhfmon.Hyujikiki, 100, 1}, + {0, 1000, 0, 0, mhfmon.Basarios, 100, 1}, + {0, 1000, 0, 0, mhfmon.Rathian, 100, 1}, + {0, 800, 0, 0, mhfmon.ShogunCeanataur, 100, 1}, + {0, 1400, 0, 0, mhfmon.Midogaron, 100, 1}, + {0, 900, 0, 0, mhfmon.Blangonga, 100, 1}, + {0, 1100, 0, 0, mhfmon.Rathalos, 100, 1}, + {0, 1000, 0, 0, mhfmon.Khezu, 100, 1}, + {0, 1600, 0, 0, mhfmon.Giaorugu, 100, 1}, + {0, 1100, 0, 0, mhfmon.Gravios, 100, 1}, + {0, 1400, 0, 0, mhfmon.Tigrex, 100, 1}, + {0, 1000, 0, 0, mhfmon.Pariapuria, 100, 1}, + {0, 1700, 0, 0, mhfmon.Anorupatisu, 100, 1}, + {0, 1500, 0, 0, mhfmon.Lavasioth, 100, 1}, + {0, 1500, 0, 0, mhfmon.Espinas, 100, 1}, + {0, 1600, 0, 0, mhfmon.Rajang, 100, 1}, + {0, 1800, 0, 0, mhfmon.Rebidiora, 100, 1}, + {0, 1100, 0, 0, mhfmon.YianGaruga, 100, 1}, + {0, 1500, 0, 0, mhfmon.AqraVashimu, 100, 1}, + {0, 1600, 0, 0, mhfmon.Gurenzeburu, 100, 1}, + {0, 1500, 0, 0, mhfmon.Dyuragaua, 100, 1}, + {0, 1300, 0, 0, mhfmon.Gougarf, 100, 1}, + {0, 1000, 0, 0, mhfmon.Shantien, 100, 1}, + {0, 1800, 0, 0, mhfmon.Disufiroa, 100, 1}, + {0, 600, 0, 0, mhfmon.Velocidrome, 100, 1}, + {0, 600, 0, 0, mhfmon.Gendrome, 100, 1}, + {0, 700, 0, 0, mhfmon.Iodrome, 100, 1}, + {0, 1700, 0, 0, mhfmon.Baruragaru, 100, 1}, + {0, 800, 0, 0, mhfmon.Cephadrome, 100, 1}, + {0, 1000, 0, 0, mhfmon.Plesioth, 100, 1}, + {0, 1800, 0, 0, mhfmon.Zerureusu, 100, 1}, + {0, 1100, 0, 0, mhfmon.Diablos, 100, 1}, + {0, 1600, 0, 0, mhfmon.Berukyurosu, 100, 1}, + {0, 2000, 0, 0, mhfmon.Fatalis, 100, 1}, + {0, 1500, 0, 0, mhfmon.BlackGravios, 100, 1}, + {0, 1600, 0, 0, mhfmon.GoldRathian, 100, 1}, + {0, 1900, 0, 0, mhfmon.Meraginasu, 100, 1}, + {0, 700, 0, 0, mhfmon.Bulldrome, 100, 1}, + {0, 900, 0, 0, mhfmon.NonoOrugaron, 100, 1}, + {0, 1600, 0, 0, mhfmon.KamuOrugaron, 100, 1}, + {0, 1700, 0, 0, mhfmon.Forokururu, 100, 1}, + {0, 1900, 0, 0, mhfmon.Diorex, 100, 1}, + {0, 1500, 0, 0, mhfmon.AqraJebia, 100, 1}, + {0, 1600, 0, 0, mhfmon.SilverRathalos, 100, 1}, + {0, 2400, 0, 0, mhfmon.CrimsonFatalis, 100, 1}, + {0, 2000, 0, 0, mhfmon.Inagami, 100, 1}, + {0, 2100, 0, 0, mhfmon.GarubaDaora, 100, 1}, + {0, 900, 0, 0, mhfmon.Monoblos, 100, 1}, + {0, 1000, 0, 0, mhfmon.RedKhezu, 100, 1}, + {0, 900, 0, 0, mhfmon.Hypnocatrice, 100, 1}, + {0, 1700, 0, 0, mhfmon.PearlEspinas, 100, 1}, + {0, 900, 0, 0, mhfmon.PurpleGypceros, 100, 1}, + {0, 1800, 0, 0, mhfmon.Poborubarumu, 100, 1}, + {0, 1900, 0, 0, mhfmon.Lunastra, 100, 1}, + {0, 1600, 0, 0, mhfmon.Kuarusepusu, 100, 1}, + {0, 1100, 0, 0, mhfmon.PinkRathian, 100, 1}, + {0, 1200, 0, 0, mhfmon.AzureRathalos, 100, 1}, + {0, 1800, 0, 0, mhfmon.Varusaburosu, 100, 1}, + {0, 1000, 0, 0, mhfmon.Gogomoa, 100, 1}, + {0, 1600, 0, 0, mhfmon.BurningEspinas, 100, 1}, + {0, 2000, 0, 0, mhfmon.Harudomerugu, 100, 1}, + {0, 1800, 0, 0, mhfmon.Akantor, 100, 1}, + {0, 900, 0, 0, mhfmon.BrightHypnoc, 100, 1}, + {0, 2200, 0, 0, mhfmon.Gureadomosu, 100, 1}, + {0, 1200, 0, 0, mhfmon.GreenPlesioth, 100, 1}, + {0, 2400, 0, 0, mhfmon.Zinogre, 100, 1}, + {0, 1900, 0, 0, mhfmon.Gasurabazura, 100, 1}, + {0, 1300, 0, 0, mhfmon.Abiorugu, 100, 1}, + {0, 1200, 0, 0, mhfmon.BlackDiablos, 100, 1}, + {0, 1000, 0, 0, mhfmon.WhiteMonoblos, 100, 1}, + {0, 3000, 0, 0, mhfmon.Deviljho, 100, 1}, + {0, 2300, 0, 0, mhfmon.YamaKurai, 100, 1}, + {0, 2800, 0, 0, mhfmon.Brachydios, 100, 1}, + {0, 1700, 0, 0, mhfmon.Toridcless, 100, 1}, + {0, 1100, 0, 0, mhfmon.WhiteHypnoc, 100, 1}, + {0, 1500, 0, 0, mhfmon.RedLavasioth, 100, 1}, + {0, 2200, 0, 0, mhfmon.Barioth, 100, 1}, + {0, 1800, 0, 0, mhfmon.Odibatorasu, 100, 1}, + {0, 1600, 0, 0, mhfmon.Doragyurosu, 100, 1}, + {0, 900, 0, 0, mhfmon.BlueYianKutKu, 100, 1}, + {0, 2300, 0, 0, mhfmon.ToaTesukatora, 100, 1}, + {0, 2000, 0, 0, mhfmon.Uragaan, 100, 1}, + {0, 1900, 0, 0, mhfmon.Teostra, 100, 1}, + {0, 1700, 0, 0, mhfmon.Chameleos, 100, 1}, + {0, 1800, 0, 0, mhfmon.KushalaDaora, 100, 1}, + {0, 2100, 0, 0, mhfmon.Nargacuga, 100, 1}, + {0, 2600, 0, 0, mhfmon.Guanzorumu, 100, 1}, + {0, 1900, 0, 0, mhfmon.Kirin, 100, 1}, + {0, 2000, 0, 0, mhfmon.Rukodiora, 100, 1}, + {0, 2700, 0, 0, mhfmon.StygianZinogre, 100, 1}, + {0, 2200, 0, 0, mhfmon.Voljang, 100, 1}, + {0, 1800, 0, 0, mhfmon.Zenaserisu, 100, 1}, + {0, 3100, 0, 0, mhfmon.GoreMagala, 100, 1}, + {0, 3200, 0, 0, mhfmon.ShagaruMagala, 100, 1}, + {0, 3500, 0, 0, mhfmon.Eruzerion, 100, 1}, + {0, 3200, 0, 0, mhfmon.Amatsu, 100, 1}, + } + + bf.WriteUint16(uint16(len(lbPrices))) + for _, lb := range lbPrices { + bf.WriteUint16(lb.Unk0) + bf.WriteUint16(lb.Unk1) + bf.WriteUint32(lb.Unk2) + } + bf.WriteUint16(uint16(len(wantedList))) + for _, wanted := range wantedList { + bf.WriteUint32(wanted.Unk0) + bf.WriteUint32(wanted.Unk1) + bf.WriteUint32(wanted.Unk2) + bf.WriteUint16(wanted.Unk3) + bf.WriteUint16(wanted.Unk4) + bf.WriteUint16(wanted.Unk5) + bf.WriteUint16(wanted.Unk6) + bf.WriteUint16(wanted.Unk7) + bf.WriteUint16(wanted.Unk8) + bf.WriteUint16(wanted.Unk9) + } + bf.WriteUint8(uint8(len(gzPrices))) + for _, gz := range gzPrices { + bf.WriteUint16(gz.Unk0) + bf.WriteUint16(gz.Gz) + bf.WriteUint16(gz.Unk1) + bf.WriteUint16(gz.Unk2) + bf.WriteUint16(gz.MonID) + bf.WriteUint16(gz.Unk3) + bf.WriteUint8(gz.Unk4) + } + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfEnumerateOrder(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEnumerateOrder) + stubEnumerateNoResults(s, pkt.AckHandle) +} + +func handleMsgMhfGetExtraInfo(s *Session, p mhfpacket.MHFPacket) {} + +func userGetItems(s *Session) []mhfitem.MHFItemStack { + var data []byte + var items []mhfitem.MHFItemStack + _ = s.server.db.QueryRow(`SELECT item_box FROM users u WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)`, s.charID).Scan(&data) + 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 handleMsgMhfEnumerateUnionItem(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfEnumerateUnionItem) + items := userGetItems(s) + bf := byteframe.NewByteFrame() + bf.WriteBytes(mhfitem.SerializeWarehouseItems(items)) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfUpdateUnionItem(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfUpdateUnionItem) + newStacks := mhfitem.DiffItemStacks(userGetItems(s), pkt.UpdatedItems) + if _, err := s.server.db.Exec(`UPDATE users u SET item_box=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$2)`, mhfitem.SerializeWarehouseItems(newStacks), s.charID); err != nil { + s.logger.Error("Failed to update union item box", zap.Error(err)) + } + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + +func handleMsgMhfGetCogInfo(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgMhfCheckWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfCheckWeeklyStamp) + var total, redeemed, updated uint16 + var lastCheck time.Time + err := s.server.db.QueryRow(fmt.Sprintf("SELECT %s_checked FROM stamps WHERE character_id=$1", pkt.StampType), s.charID).Scan(&lastCheck) + if err != nil { + lastCheck = TimeAdjusted() + if _, err := s.server.db.Exec("INSERT INTO stamps (character_id, hl_checked, ex_checked) VALUES ($1, $2, $2)", s.charID, TimeAdjusted()); err != nil { + s.logger.Error("Failed to insert stamps record", zap.Error(err)) + } + } else { + if _, err := s.server.db.Exec(fmt.Sprintf(`UPDATE stamps SET %s_checked=$1 WHERE character_id=$2`, pkt.StampType), TimeAdjusted(), s.charID); err != nil { + s.logger.Error("Failed to update stamp check time", zap.Error(err)) + } + } + + if lastCheck.Before(TimeWeekStart()) { + if _, err := s.server.db.Exec(fmt.Sprintf("UPDATE stamps SET %s_total=%s_total+1 WHERE character_id=$1", pkt.StampType, pkt.StampType), s.charID); err != nil { + s.logger.Error("Failed to increment stamp total", zap.Error(err)) + } + updated = 1 + } + + _ = s.server.db.QueryRow(fmt.Sprintf("SELECT %s_total, %s_redeemed FROM stamps WHERE character_id=$1", pkt.StampType, pkt.StampType), s.charID).Scan(&total, &redeemed) + bf := byteframe.NewByteFrame() + bf.WriteUint16(total) + bf.WriteUint16(redeemed) + bf.WriteUint16(updated) + bf.WriteUint16(0) + bf.WriteUint16(0) + bf.WriteUint32(uint32(TimeWeekStart().Unix())) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfExchangeWeeklyStamp(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfExchangeWeeklyStamp) + var total, redeemed uint16 + var tktStack mhfitem.MHFItemStack + if pkt.Unk1 == 10 { // Yearly Sub Ex + _ = s.server.db.QueryRow("UPDATE stamps SET hl_total=hl_total-48, hl_redeemed=hl_redeemed-48 WHERE character_id=$1 RETURNING hl_total, hl_redeemed", s.charID).Scan(&total, &redeemed) + tktStack = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: 2210}, Quantity: 1} + } else { + _ = s.server.db.QueryRow(fmt.Sprintf("UPDATE stamps SET %s_redeemed=%s_redeemed+8 WHERE character_id=$1 RETURNING %s_total, %s_redeemed", pkt.StampType, pkt.StampType, pkt.StampType, pkt.StampType), s.charID).Scan(&total, &redeemed) + if pkt.StampType == "hl" { + tktStack = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: 1630}, Quantity: 5} + } else { + tktStack = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: 1631}, Quantity: 5} + } + } + addWarehouseItem(s, tktStack) + bf := byteframe.NewByteFrame() + bf.WriteUint16(total) + bf.WriteUint16(redeemed) + bf.WriteUint16(0) + bf.WriteUint16(tktStack.Item.ItemID) + bf.WriteUint16(tktStack.Quantity) + bf.WriteUint32(uint32(TimeWeekStart().Unix())) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfStampcardStamp(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfStampcardStamp) + + rewards := []struct { + HR uint16 + Item1 uint16 + Quantity1 uint16 + Item2 uint16 + Quantity2 uint16 + }{ + {0, 6164, 1, 6164, 2}, + {50, 6164, 2, 6164, 3}, + {100, 6164, 3, 5392, 1}, + {300, 5392, 1, 5392, 3}, + {999, 5392, 1, 5392, 4}, + } + if _config.ErupeConfig.RealClientMode <= _config.Z1 { + for _, reward := range rewards { + if pkt.HR >= reward.HR { + pkt.Item1 = reward.Item1 + pkt.Quantity1 = reward.Quantity1 + pkt.Item2 = reward.Item2 + pkt.Quantity2 = reward.Quantity2 + } + } + } + + bf := byteframe.NewByteFrame() + bf.WriteUint16(pkt.HR) + if _config.ErupeConfig.RealClientMode >= _config.G1 { + bf.WriteUint16(pkt.GR) + } + var stamps, rewardTier, rewardUnk uint16 + reward := mhfitem.MHFItemStack{Item: mhfitem.MHFItem{}} + _ = s.server.db.QueryRow(`UPDATE characters SET stampcard = stampcard + $1 WHERE id = $2 RETURNING stampcard`, pkt.Stamps, s.charID).Scan(&stamps) + bf.WriteUint16(stamps - pkt.Stamps) + bf.WriteUint16(stamps) + + if stamps/30 > (stamps-pkt.Stamps)/30 { + rewardTier = 2 + rewardUnk = pkt.Reward2 + reward = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: pkt.Item2}, Quantity: pkt.Quantity2} + addWarehouseItem(s, reward) + } else if stamps/15 > (stamps-pkt.Stamps)/15 { + rewardTier = 1 + rewardUnk = pkt.Reward1 + reward = mhfitem.MHFItemStack{Item: mhfitem.MHFItem{ItemID: pkt.Item1}, Quantity: pkt.Quantity1} + addWarehouseItem(s, reward) + } + + bf.WriteUint16(rewardTier) + bf.WriteUint16(rewardUnk) + bf.WriteUint16(reward.Item.ItemID) + bf.WriteUint16(reward.Quantity) + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgMhfStampcardPrize(s *Session, p mhfpacket.MHFPacket) {} diff --git a/server/channelserver/handlers_session.go b/server/channelserver/handlers_session.go new file mode 100644 index 000000000..6e805a7dc --- /dev/null +++ b/server/channelserver/handlers_session.go @@ -0,0 +1,711 @@ +package channelserver + +import ( + "crypto/rand" + "encoding/binary" + "erupe-ce/common/byteframe" + "erupe-ce/common/mhfcourse" + "erupe-ce/common/mhfmon" + ps "erupe-ce/common/pascalstring" + "erupe-ce/common/stringsupport" + _config "erupe-ce/config" + "erupe-ce/network/mhfpacket" + "fmt" + "io" + "net" + "strings" + "time" + + "go.uber.org/zap" +) + +func handleMsgHead(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysExtendThreshold(s *Session, p mhfpacket.MHFPacket) { + // No data aside from header, no resp required. +} + +func handleMsgSysEnd(s *Session, p mhfpacket.MHFPacket) { + // No data aside from header, no resp required. +} + +func handleMsgSysNop(s *Session, p mhfpacket.MHFPacket) { + // No data aside from header, no resp required. +} + +func handleMsgSysAck(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysTerminalLog(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysTerminalLog) + for i := range pkt.Entries { + s.server.logger.Info("SysTerminalLog", + zap.Uint8("Type1", pkt.Entries[i].Type1), + zap.Uint8("Type2", pkt.Entries[i].Type2), + zap.Int16("Unk0", pkt.Entries[i].Unk0), + zap.Int32("Unk1", pkt.Entries[i].Unk1), + zap.Int32("Unk2", pkt.Entries[i].Unk2), + zap.Int32("Unk3", pkt.Entries[i].Unk3), + zap.Int32s("Unk4", pkt.Entries[i].Unk4), + ) + } + resp := byteframe.NewByteFrame() + resp.WriteUint32(pkt.LogID + 1) // LogID to use for requests after this. + doAckSimpleSucceed(s, pkt.AckHandle, resp.Data()) +} + +func handleMsgSysLogin(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysLogin) + + if !s.server.erupeConfig.DebugOptions.DisableTokenCheck { + var token string + err := s.server.db.QueryRow("SELECT token FROM sign_sessions ss INNER JOIN public.users u on ss.user_id = u.id WHERE token=$1 AND ss.id=$2 AND u.id=(SELECT c.user_id FROM characters c WHERE c.id=$3)", pkt.LoginTokenString, pkt.LoginTokenNumber, pkt.CharID0).Scan(&token) + if err != nil { + _ = s.rawConn.Close() + s.logger.Warn(fmt.Sprintf("Invalid login token, offending CID: (%d)", pkt.CharID0)) + return + } + } + + s.Lock() + s.charID = pkt.CharID0 + s.token = pkt.LoginTokenString + s.Unlock() + + bf := byteframe.NewByteFrame() + bf.WriteUint32(uint32(TimeAdjusted().Unix())) // Unix timestamp + + _, err := s.server.db.Exec("UPDATE servers SET current_players=$1 WHERE server_id=$2", len(s.server.sessions), s.server.ID) + if err != nil { + panic(err) + } + + _, err = s.server.db.Exec("UPDATE sign_sessions SET server_id=$1, char_id=$2 WHERE token=$3", s.server.ID, s.charID, s.token) + if err != nil { + panic(err) + } + + _, err = s.server.db.Exec("UPDATE characters SET last_login=$1 WHERE id=$2", TimeAdjusted().Unix(), s.charID) + if err != nil { + panic(err) + } + + _, err = s.server.db.Exec("UPDATE users u SET last_character=$1 WHERE u.id=(SELECT c.user_id FROM characters c WHERE c.id=$1)", s.charID) + if err != nil { + panic(err) + } + + doAckSimpleSucceed(s, pkt.AckHandle, bf.Data()) + + updateRights(s) + + s.server.BroadcastMHF(&mhfpacket.MsgSysInsertUser{CharID: s.charID}, s) +} + +func handleMsgSysLogout(s *Session, p mhfpacket.MHFPacket) { + logoutPlayer(s) +} + +// saveAllCharacterData saves all character data to the database with proper error handling. +// This function ensures data persistence even if the client disconnects unexpectedly. +// It handles: +// - Main savedata blob (compressed) +// - User binary data (house, gallery, etc.) +// - Plate data (transmog appearance, storage, equipment sets) +// - Playtime updates +// - RP updates +// - Name corruption prevention +func saveAllCharacterData(s *Session, rpToAdd int) error { + saveStart := time.Now() + + // Get current savedata from database + characterSaveData, err := GetCharacterSaveData(s, s.charID) + if err != nil { + s.logger.Error("Failed to retrieve character save data", + zap.Error(err), + zap.Uint32("charID", s.charID), + zap.String("name", s.Name), + ) + return err + } + + if characterSaveData == nil { + s.logger.Warn("Character save data is nil, skipping save", + zap.Uint32("charID", s.charID), + zap.String("name", s.Name), + ) + return nil + } + + // Force name to match to prevent corruption detection issues + // This handles SJIS/UTF-8 encoding differences across game versions + if characterSaveData.Name != s.Name { + s.logger.Debug("Correcting name mismatch before save", + zap.String("savedata_name", characterSaveData.Name), + zap.String("session_name", s.Name), + zap.Uint32("charID", s.charID), + ) + characterSaveData.Name = s.Name + characterSaveData.updateSaveDataWithStruct() + } + + // Update playtime from session + if !s.playtimeTime.IsZero() { + sessionPlaytime := uint32(time.Since(s.playtimeTime).Seconds()) + s.playtime += sessionPlaytime + s.logger.Debug("Updated playtime", + zap.Uint32("session_playtime_seconds", sessionPlaytime), + zap.Uint32("total_playtime", s.playtime), + zap.Uint32("charID", s.charID), + ) + } + characterSaveData.Playtime = s.playtime + + // Update RP if any gained during session + if rpToAdd > 0 { + characterSaveData.RP += uint16(rpToAdd) + if characterSaveData.RP >= s.server.erupeConfig.GameplayOptions.MaximumRP { + characterSaveData.RP = s.server.erupeConfig.GameplayOptions.MaximumRP + s.logger.Debug("RP capped at maximum", + zap.Uint16("max_rp", s.server.erupeConfig.GameplayOptions.MaximumRP), + zap.Uint32("charID", s.charID), + ) + } + s.logger.Debug("Added RP", + zap.Int("rp_gained", rpToAdd), + zap.Uint16("new_rp", characterSaveData.RP), + zap.Uint32("charID", s.charID), + ) + } + + // Save to database (main savedata + user_binary) + characterSaveData.Save(s) + + // Save auxiliary data types + // Note: Plate data saves immediately when client sends save packets, + // so this is primarily a safety net for monitoring and consistency + if err := savePlateDataToDatabase(s); err != nil { + s.logger.Error("Failed to save plate data during logout", + zap.Error(err), + zap.Uint32("charID", s.charID), + ) + // Don't return error - continue with logout even if plate save fails + } + + saveDuration := time.Since(saveStart) + s.logger.Info("Saved character data successfully", + zap.Uint32("charID", s.charID), + zap.String("name", s.Name), + zap.Duration("duration", saveDuration), + zap.Int("rp_added", rpToAdd), + zap.Uint32("playtime", s.playtime), + ) + + return nil +} + +func logoutPlayer(s *Session) { + logoutStart := time.Now() + + // Log logout initiation with session details + sessionDuration := time.Duration(0) + if s.sessionStart > 0 { + sessionDuration = time.Since(time.Unix(s.sessionStart, 0)) + } + + s.logger.Info("Player logout initiated", + zap.Uint32("charID", s.charID), + zap.String("name", s.Name), + zap.Duration("session_duration", sessionDuration), + ) + + // Calculate session metrics FIRST (before cleanup) + var timePlayed int + var sessionTime int + var rpGained int + + if s.charID != 0 { + _ = s.server.db.QueryRow("SELECT time_played FROM characters WHERE id = $1", s.charID).Scan(&timePlayed) + sessionTime = int(TimeAdjusted().Unix()) - int(s.sessionStart) + timePlayed += sessionTime + + if mhfcourse.CourseExists(30, s.courses) { + rpGained = timePlayed / 900 + timePlayed = timePlayed % 900 + if _, err := s.server.db.Exec("UPDATE characters SET cafe_time=cafe_time+$1 WHERE id=$2", sessionTime, s.charID); err != nil { + s.logger.Error("Failed to update cafe time", zap.Error(err)) + } + } else { + rpGained = timePlayed / 1800 + timePlayed = timePlayed % 1800 + } + + s.logger.Debug("Session metrics calculated", + zap.Uint32("charID", s.charID), + zap.Int("session_time_seconds", sessionTime), + zap.Int("rp_gained", rpGained), + zap.Int("time_played_remainder", timePlayed), + ) + + // Save all character data ONCE with all updates + // This is the safety net that ensures data persistence even if client + // didn't send save packets before disconnecting + if err := saveAllCharacterData(s, rpGained); err != nil { + s.logger.Error("Failed to save character data during logout", + zap.Error(err), + zap.Uint32("charID", s.charID), + zap.String("name", s.Name), + ) + // Continue with logout even if save fails + } + + // Update time_played and guild treasure hunt + if _, err := s.server.db.Exec("UPDATE characters SET time_played = $1 WHERE id = $2", timePlayed, s.charID); err != nil { + s.logger.Error("Failed to update time played", zap.Error(err)) + } + if _, err := s.server.db.Exec(`UPDATE guild_characters SET treasure_hunt=NULL WHERE character_id=$1`, s.charID); err != nil { + s.logger.Error("Failed to clear treasure hunt", zap.Error(err)) + } + } + + // NOW do cleanup (after save is complete) + s.server.Lock() + delete(s.server.sessions, s.rawConn) + _ = s.rawConn.Close() + s.server.Unlock() + + // Stage cleanup + for _, stage := range s.server.stages { + // Tell sessions registered to disconnecting players quest to unregister + if stage.host != nil && stage.host.charID == s.charID { + for _, sess := range s.server.sessions { + for rSlot := range stage.reservedClientSlots { + if sess.charID == rSlot && sess.stage != nil && sess.stage.id[3:5] != "Qs" { + sess.QueueSendMHFNonBlocking(&mhfpacket.MsgSysStageDestruct{}) + } + } + } + } + for session := range stage.clients { + if session.charID == s.charID { + delete(stage.clients, session) + } + } + } + + // Update sign sessions and server player count + _, err := s.server.db.Exec("UPDATE sign_sessions SET server_id=NULL, char_id=NULL WHERE token=$1", s.token) + if err != nil { + panic(err) + } + + _, err = s.server.db.Exec("UPDATE servers SET current_players=$1 WHERE server_id=$2", len(s.server.sessions), s.server.ID) + if err != nil { + panic(err) + } + + if s.stage == nil { + logoutDuration := time.Since(logoutStart) + s.logger.Info("Player logout completed", + zap.Uint32("charID", s.charID), + zap.String("name", s.Name), + zap.Duration("logout_duration", logoutDuration), + ) + return + } + + // Broadcast user deletion and final cleanup + s.server.BroadcastMHF(&mhfpacket.MsgSysDeleteUser{ + CharID: s.charID, + }, s) + + s.server.Lock() + for _, stage := range s.server.stages { + delete(stage.reservedClientSlots, s.charID) + } + s.server.Unlock() + + removeSessionFromSemaphore(s) + removeSessionFromStage(s) + + logoutDuration := time.Since(logoutStart) + s.logger.Info("Player logout completed", + zap.Uint32("charID", s.charID), + zap.String("name", s.Name), + zap.Duration("logout_duration", logoutDuration), + zap.Int("rp_gained", rpGained), + ) +} + +func handleMsgSysSetStatus(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysPing(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysPing) + doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) +} + +func handleMsgSysTime(s *Session, p mhfpacket.MHFPacket) { + resp := &mhfpacket.MsgSysTime{ + GetRemoteTime: false, + Timestamp: uint32(TimeAdjusted().Unix()), // JP timezone + } + s.QueueSendMHF(resp) + s.notifyRavi() +} + +func handleMsgSysIssueLogkey(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysIssueLogkey) + + // Make a random log key for this session. + logKey := make([]byte, 16) + _, err := rand.Read(logKey) + if err != nil { + panic(err) + } + + // TODO(Andoryuuta): In the offical client, the log key index is off by one, + // cutting off the last byte in _most uses_. Find and document these accordingly. + s.Lock() + s.logKey = logKey + s.Unlock() + + // Issue it. + resp := byteframe.NewByteFrame() + resp.WriteBytes(logKey) + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) +} + +func handleMsgSysRecordLog(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysRecordLog) + if _config.ErupeConfig.RealClientMode == _config.ZZ { + bf := byteframe.NewByteFrameFromBytes(pkt.Data) + _, _ = bf.Seek(32, 0) + var val uint8 + for i := 0; i < 176; i++ { + val = bf.ReadUint8() + if val > 0 && mhfmon.Monsters[i].Large { + if _, err := s.server.db.Exec(`INSERT INTO kill_logs (character_id, monster, quantity, timestamp) VALUES ($1, $2, $3, $4)`, s.charID, i, val, TimeAdjusted()); err != nil { + s.logger.Error("Failed to insert kill log", zap.Error(err)) + } + } + } + } + // remove a client returning to town from reserved slots to make sure the stage is hidden from board + delete(s.stage.reservedClientSlots, s.charID) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + +func handleMsgSysEcho(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysLockGlobalSema(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysLockGlobalSema) + var sgid string + for _, channel := range s.server.Channels { + for id := range channel.stages { + if strings.HasSuffix(id, pkt.UserIDString) { + sgid = channel.GlobalID + } + } + } + bf := byteframe.NewByteFrame() + if len(sgid) > 0 && sgid != s.server.GlobalID { + bf.WriteUint8(0) + bf.WriteUint8(0) + ps.Uint16(bf, sgid, false) + } else { + bf.WriteUint8(2) + bf.WriteUint8(0) + ps.Uint16(bf, pkt.ServerChannelIDString, false) + } + doAckBufSucceed(s, pkt.AckHandle, bf.Data()) +} + +func handleMsgSysUnlockGlobalSema(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysUnlockGlobalSema) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + +func handleMsgSysUpdateRight(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysAuthQuery(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysAuthTerminal(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysRightsReload(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgSysRightsReload) + updateRights(s) + doAckSimpleSucceed(s, pkt.AckHandle, []byte{0x00, 0x00, 0x00, 0x00}) +} + +func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfTransitMessage) + + local := strings.Split(s.rawConn.RemoteAddr().String(), ":")[0] == "127.0.0.1" + + var maxResults, port, count uint16 + var cid uint32 + var term, ip string + bf := byteframe.NewByteFrameFromBytes(pkt.MessageData) + switch pkt.SearchType { + case 1: + maxResults = 1 + cid = bf.ReadUint32() + case 2: + bf.ReadUint16() // term length + maxResults = bf.ReadUint16() + bf.ReadUint8() // Unk + term = stringsupport.SJISToUTF8(bf.ReadNullTerminatedBytes()) + case 3: + _ip := bf.ReadBytes(4) + ip = fmt.Sprintf("%d.%d.%d.%d", _ip[3], _ip[2], _ip[1], _ip[0]) + port = bf.ReadUint16() + bf.ReadUint16() // term length + maxResults = bf.ReadUint16() + bf.ReadUint8() + term = string(bf.ReadNullTerminatedBytes()) + } + + resp := byteframe.NewByteFrame() + resp.WriteUint16(0) + switch pkt.SearchType { + case 1, 2, 3: // usersearchidx, usersearchname, lobbysearchname + for _, c := range s.server.Channels { + for _, session := range c.sessions { + if count == maxResults { + break + } + if pkt.SearchType == 1 && session.charID != cid { + continue + } + if pkt.SearchType == 2 && !strings.Contains(session.Name, term) { + continue + } + if pkt.SearchType == 3 && session.server.IP != ip && session.server.Port != port && session.stage.id != term { + continue + } + count++ + sessionName := stringsupport.UTF8ToSJIS(session.Name) + sessionStage := stringsupport.UTF8ToSJIS(session.stage.id) + if !local { + resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4())) + } else { + resp.WriteUint32(0x0100007F) + } + resp.WriteUint16(c.Port) + resp.WriteUint32(session.charID) + resp.WriteUint8(uint8(len(sessionStage) + 1)) + resp.WriteUint8(uint8(len(sessionName) + 1)) + resp.WriteUint16(uint16(len(c.userBinaryParts[userBinaryPartID{charID: session.charID, index: 3}]))) + + // TODO: This case might be <=G2 + if _config.ErupeConfig.RealClientMode <= _config.G1 { + resp.WriteBytes(make([]byte, 8)) + } else { + resp.WriteBytes(make([]byte, 40)) + } + resp.WriteBytes(make([]byte, 8)) + + resp.WriteNullTerminatedBytes(sessionStage) + resp.WriteNullTerminatedBytes(sessionName) + resp.WriteBytes(c.userBinaryParts[userBinaryPartID{session.charID, 3}]) + } + } + case 4: // lobbysearch + type FindPartyParams struct { + StagePrefix string + RankRestriction int16 + Targets []int16 + Unk0 []int16 + Unk1 []int16 + QuestID []int16 + } + findPartyParams := FindPartyParams{ + StagePrefix: "sl2Ls210", + } + numParams := bf.ReadUint8() + maxResults = bf.ReadUint16() + for i := uint8(0); i < numParams; i++ { + switch bf.ReadUint8() { + case 0: + values := bf.ReadUint8() + for i := uint8(0); i < values; i++ { + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + findPartyParams.RankRestriction = bf.ReadInt16() + } else { + findPartyParams.RankRestriction = int16(bf.ReadInt8()) + } + } + case 1: + values := bf.ReadUint8() + for i := uint8(0); i < values; i++ { + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + findPartyParams.Targets = append(findPartyParams.Targets, bf.ReadInt16()) + } else { + findPartyParams.Targets = append(findPartyParams.Targets, int16(bf.ReadInt8())) + } + } + case 2: + values := bf.ReadUint8() + for i := uint8(0); i < values; i++ { + var value int16 + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + value = bf.ReadInt16() + } else { + value = int16(bf.ReadInt8()) + } + switch value { + case 0: // Public Bar + findPartyParams.StagePrefix = "sl2Ls210" + case 1: // Tokotoko Partnya + findPartyParams.StagePrefix = "sl2Ls463" + case 2: // Hunting Prowess Match + findPartyParams.StagePrefix = "sl2Ls286" + case 3: // Volpakkun Together + findPartyParams.StagePrefix = "sl2Ls465" + case 5: // Quick Party + // Unk + } + } + case 3: // Unknown + values := bf.ReadUint8() + for i := uint8(0); i < values; i++ { + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + findPartyParams.Unk0 = append(findPartyParams.Unk0, bf.ReadInt16()) + } else { + findPartyParams.Unk0 = append(findPartyParams.Unk0, int16(bf.ReadInt8())) + } + } + case 4: // Looking for n or already have n + values := bf.ReadUint8() + for i := uint8(0); i < values; i++ { + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + findPartyParams.Unk1 = append(findPartyParams.Unk1, bf.ReadInt16()) + } else { + findPartyParams.Unk1 = append(findPartyParams.Unk1, int16(bf.ReadInt8())) + } + } + case 5: + values := bf.ReadUint8() + for i := uint8(0); i < values; i++ { + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + findPartyParams.QuestID = append(findPartyParams.QuestID, bf.ReadInt16()) + } else { + findPartyParams.QuestID = append(findPartyParams.QuestID, int16(bf.ReadInt8())) + } + } + } + } + for _, c := range s.server.Channels { + for _, stage := range c.stages { + if count == maxResults { + break + } + if strings.HasPrefix(stage.id, findPartyParams.StagePrefix) { + sb3 := byteframe.NewByteFrameFromBytes(stage.rawBinaryData[stageBinaryKey{1, 3}]) + _, _ = sb3.Seek(4, 0) + + stageDataParams := 7 + if _config.ErupeConfig.RealClientMode <= _config.G10 { + stageDataParams = 4 + } else if _config.ErupeConfig.RealClientMode <= _config.Z1 { + stageDataParams = 6 + } + + var stageData []int16 + for i := 0; i < stageDataParams; i++ { + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + stageData = append(stageData, sb3.ReadInt16()) + } else { + stageData = append(stageData, int16(sb3.ReadInt8())) + } + } + + if findPartyParams.RankRestriction >= 0 { + if stageData[0] > findPartyParams.RankRestriction { + continue + } + } + + var hasTarget bool + if len(findPartyParams.Targets) > 0 { + for _, target := range findPartyParams.Targets { + if target == stageData[1] { + hasTarget = true + break + } + } + if !hasTarget { + continue + } + } + + count++ + if !local { + resp.WriteUint32(binary.LittleEndian.Uint32(net.ParseIP(c.IP).To4())) + } else { + resp.WriteUint32(0x0100007F) + } + resp.WriteUint16(c.Port) + + resp.WriteUint16(0) // Static? + resp.WriteUint16(0) // Unk, [0 1 2] + resp.WriteUint16(uint16(len(stage.clients) + len(stage.reservedClientSlots))) + resp.WriteUint16(stage.maxPlayers) + // TODO: Retail returned the number of clients in quests, not workshop/my series + resp.WriteUint16(uint16(len(stage.reservedClientSlots))) + + resp.WriteUint8(0) // Static? + resp.WriteUint8(uint8(stage.maxPlayers)) + resp.WriteUint8(1) // Static? + resp.WriteUint8(uint8(len(stage.id) + 1)) + resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 0}]))) + resp.WriteUint8(uint8(len(stage.rawBinaryData[stageBinaryKey{1, 1}]))) + + for i := range stageData { + if _config.ErupeConfig.RealClientMode >= _config.Z1 { + resp.WriteInt16(stageData[i]) + } else { + resp.WriteInt8(int8(stageData[i])) + } + } + resp.WriteUint8(0) // Unk + resp.WriteUint8(0) // Unk + + resp.WriteNullTerminatedBytes([]byte(stage.id)) + resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 0}]) + resp.WriteBytes(stage.rawBinaryData[stageBinaryKey{1, 1}]) + } + } + } + } + _, _ = resp.Seek(0, io.SeekStart) + resp.WriteUint16(count) + doAckBufSucceed(s, pkt.AckHandle, resp.Data()) +} + +func handleMsgCaExchangeItem(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgMhfServerCommand(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgMhfAnnounce(s *Session, p mhfpacket.MHFPacket) { + pkt := p.(*mhfpacket.MsgMhfAnnounce) + s.server.BroadcastRaviente(pkt.IPAddress, pkt.Port, pkt.StageID, pkt.Data.ReadUint8()) + doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) +} + +func handleMsgMhfSetLoginwindow(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysTransBinary(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysCollectBinary(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysGetState(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysSerialize(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysEnumlobby(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysEnumuser(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgSysInfokyserver(s *Session, p mhfpacket.MHFPacket) {} + +func handleMsgMhfGetCaUniqueID(s *Session, p mhfpacket.MHFPacket) {}