From 7c444b023bd1308b39cf00e04b47b508e504bd0c Mon Sep 17 00:00:00 2001 From: Houmgaor Date: Fri, 20 Feb 2026 19:50:28 +0100 Subject: [PATCH] refactor(channelserver): replace magic numbers with named protocol constants Extract numeric literals into named constants across quest handling, save data parsing, rengoku skill layout, diva event timing, guild info, achievement trophies, RP accrual rates, and semaphore IDs. Adds constants_quest.go for quest-related constants shared across functions. Pure rename/extract with zero behavior change. --- server/channelserver/constants_quest.go | 4 +- server/channelserver/handlers_guild_info.go | 10 ++- server/channelserver/handlers_quest.go | 60 ++++++++--------- server/channelserver/handlers_rengoku.go | 35 ++++++---- server/channelserver/handlers_session.go | 30 ++++----- server/channelserver/model_character.go | 73 +++++++++++++-------- 6 files changed, 124 insertions(+), 88 deletions(-) diff --git a/server/channelserver/constants_quest.go b/server/channelserver/constants_quest.go index 0e0e8c5f0..4fe81b1d7 100644 --- a/server/channelserver/constants_quest.go +++ b/server/channelserver/constants_quest.go @@ -12,8 +12,8 @@ const ( // Event quest binary frame offsets const ( - questFrameTimeFlagOffset = 25 - questFrameVariant3Offset = 175 + questFrameTimeFlagOffset = 25 + questFrameVariant3Offset = 175 ) // Quest body lengths per game version diff --git a/server/channelserver/handlers_guild_info.go b/server/channelserver/handlers_guild_info.go index dea46ab29..4dafab021 100644 --- a/server/channelserver/handlers_guild_info.go +++ b/server/channelserver/handlers_guild_info.go @@ -12,6 +12,12 @@ import ( "github.com/jmoiron/sqlx" ) +// Guild sentinel and cost constants +const ( + guildNotJoinedSentinel = uint32(0xFFFFFFFF) + guildRoomMaxRP = uint32(55000) +) + func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { pkt := p.(*mhfpacket.MsgMhfInfoGuild) @@ -32,7 +38,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { guildLeaderName := stringsupport.UTF8ToSJIS(guild.LeaderName) characterGuildData, err := GetCharacterGuildData(s, s.charID) - characterJoinedAt := uint32(0xFFFFFFFF) + characterJoinedAt := guildNotJoinedSentinel if characterGuildData != nil && characterGuildData.JoinedAt != nil { characterJoinedAt = uint32(characterGuildData.JoinedAt.Unix()) @@ -123,7 +129,7 @@ func handleMsgMhfInfoGuild(s *Session, p mhfpacket.MHFPacket) { } bf.WriteUint8(limit) - bf.WriteUint32(55000) + bf.WriteUint32(guildRoomMaxRP) bf.WriteUint32(uint32(guild.RoomExpiry.Unix())) bf.WriteUint16(guild.RoomRP) bf.WriteUint16(0) // Ignored diff --git a/server/channelserver/handlers_quest.go b/server/channelserver/handlers_quest.go index f4b81608d..f52b14868 100644 --- a/server/channelserver/handlers_quest.go +++ b/server/channelserver/handlers_quest.go @@ -223,24 +223,24 @@ func loadQuestFile(s *Session, questId int) []byte { fileBytes.SetLE() _, _ = fileBytes.Seek(int64(fileBytes.ReadUint32()), 0) - bodyLength := 320 + bodyLength := questBodyLenZZ if s.server.erupeConfig.RealClientMode <= _config.S6 { - bodyLength = 160 + bodyLength = questBodyLenS6 } else if s.server.erupeConfig.RealClientMode <= _config.F5 { - bodyLength = 168 + bodyLength = questBodyLenF5 } else if s.server.erupeConfig.RealClientMode <= _config.G101 { - bodyLength = 192 + bodyLength = questBodyLenG101 } else if s.server.erupeConfig.RealClientMode <= _config.Z1 { - bodyLength = 224 + bodyLength = questBodyLenZ1 } // The n bytes directly following the data pointer must go directly into the event's body, after the header and before the string pointers. questBody := byteframe.NewByteFrameFromBytes(fileBytes.ReadBytes(uint(bodyLength))) questBody.SetLE() // Find the master quest string pointer - _, _ = questBody.Seek(40, 0) + _, _ = questBody.Seek(questStringPointerOff, 0) _, _ = fileBytes.Seek(int64(questBody.ReadUint32()), 0) - _, _ = questBody.Seek(40, 0) + _, _ = questBody.Seek(questStringPointerOff, 0) // Overwrite it questBody.WriteUint32(uint32(bodyLength)) _, _ = questBody.Seek(0, 2) @@ -248,8 +248,8 @@ func loadQuestFile(s *Session, questId int) []byte { // Rewrite the quest strings and their pointers var tempString []byte newStrings := byteframe.NewByteFrame() - tempPointer := bodyLength + 32 - for i := 0; i < 8; i++ { + tempPointer := bodyLength + questStringTablePadding + for i := 0; i < questStringCount; i++ { questBody.WriteUint32(uint32(tempPointer)) temp := int64(fileBytes.Index()) _, _ = fileBytes.Seek(int64(fileBytes.ReadUint32()), 0) @@ -284,21 +284,21 @@ func makeEventQuest(s *Session, rows *sql.Rows) ([]byte, error) { bf.WriteUint32(0) // Unk bf.WriteUint8(0) // Unk switch questType { - case 16: + case QuestTypeRegularRaviente: bf.WriteUint8(s.server.erupeConfig.GameplayOptions.RegularRavienteMaxPlayers) - case 22: + case QuestTypeViolentRaviente: bf.WriteUint8(s.server.erupeConfig.GameplayOptions.ViolentRavienteMaxPlayers) - case 40: + case QuestTypeBerserkRaviente: bf.WriteUint8(s.server.erupeConfig.GameplayOptions.BerserkRavienteMaxPlayers) - case 50: + case QuestTypeExtremeRaviente: bf.WriteUint8(s.server.erupeConfig.GameplayOptions.ExtremeRavienteMaxPlayers) - case 51: + case QuestTypeSmallBerserkRavi: bf.WriteUint8(s.server.erupeConfig.GameplayOptions.SmallBerserkRavienteMaxPlayers) default: bf.WriteUint8(maxPlayers) } bf.WriteUint8(questType) - if questType == 9 { + if questType == QuestTypeSpecialTool { bf.WriteBool(false) } else { bf.WriteBool(true) @@ -314,9 +314,9 @@ func makeEventQuest(s *Session, rows *sql.Rows) ([]byte, error) { // Time Flag Replacement // Bitset Structure: b8 UNK, b7 Required Objective, b6 UNK, b5 Night, b4 Day, b3 Cold, b2 Warm, b1 Spring // if the byte is set to 0 the game choses the quest file corresponding to whatever season the game is on - _, _ = bf.Seek(25, 0) + _, _ = bf.Seek(questFrameTimeFlagOffset, 0) flagByte := bf.ReadUint8() - _, _ = bf.Seek(25, 0) + _, _ = bf.Seek(questFrameTimeFlagOffset, 0) if s.server.erupeConfig.GameplayOptions.SeasonOverride { bf.WriteUint8(flagByte & 0b11100000) } else { @@ -332,10 +332,10 @@ func makeEventQuest(s *Session, rows *sql.Rows) ([]byte, error) { // Bitset Structure Quest Variant 2: b8 Road, b7 High Conquest, b6 Fixed Difficulty, b5 No Active Feature, b4 Timer, b3 No Cuff, b2 No Halk Pots, b1 Low Conquest // Bitset Structure Quest Variant 3: b8 No Sigils, b7 UNK, b6 Interception, b5 Zenith, b4 No GP Skills, b3 No Simple Mode?, b2 GSR to GR, b1 No Reward Skills - _, _ = bf.Seek(175, 0) + _, _ = bf.Seek(questFrameVariant3Offset, 0) questVariant3 := bf.ReadUint8() questVariant3 &= 0b11011111 // disable Interception flag - _, _ = bf.Seek(175, 0) + _, _ = bf.Seek(questFrameVariant3Offset, 0) bf.WriteUint8(questVariant3) _, _ = bf.Seek(0, 2) @@ -400,7 +400,7 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { s.logger.Error("Failed to make event quest", zap.Error(err)) continue } else { - if len(data) > 896 || len(data) < 352 { + if len(data) > questDataMaxLen || len(data) < questDataMinLen { s.logger.Error("Invalid quest data length", zap.Int("len", len(data))) continue } else { @@ -601,25 +601,25 @@ func handleMsgMhfEnumerateQuest(s *Session, p mhfpacket.MHFPacket) { } tuneValues = temp - tuneLimit := 770 + tuneLimit := tuneLimitZZ if s.server.erupeConfig.RealClientMode <= _config.G1 { - tuneLimit = 256 + tuneLimit = tuneLimitG1 } else if s.server.erupeConfig.RealClientMode <= _config.G3 { - tuneLimit = 283 + tuneLimit = tuneLimitG3 } else if s.server.erupeConfig.RealClientMode <= _config.GG { - tuneLimit = 315 + tuneLimit = tuneLimitGG } else if s.server.erupeConfig.RealClientMode <= _config.G61 { - tuneLimit = 332 + tuneLimit = tuneLimitG61 } else if s.server.erupeConfig.RealClientMode <= _config.G7 { - tuneLimit = 339 + tuneLimit = tuneLimitG7 } else if s.server.erupeConfig.RealClientMode <= _config.G81 { - tuneLimit = 396 + tuneLimit = tuneLimitG81 } else if s.server.erupeConfig.RealClientMode <= _config.G91 { - tuneLimit = 694 + tuneLimit = tuneLimitG91 } else if s.server.erupeConfig.RealClientMode <= _config.G101 { - tuneLimit = 704 + tuneLimit = tuneLimitG101 } else if s.server.erupeConfig.RealClientMode <= _config.Z2 { - tuneLimit = 750 + tuneLimit = tuneLimitZ2 } if len(tuneValues) > tuneLimit { tuneValues = tuneValues[:tuneLimit] diff --git a/server/channelserver/handlers_rengoku.go b/server/channelserver/handlers_rengoku.go index 207589de0..d34ff71eb 100644 --- a/server/channelserver/handlers_rengoku.go +++ b/server/channelserver/handlers_rengoku.go @@ -14,18 +14,31 @@ import ( "go.uber.org/zap" ) +// Rengoku save blob layout offsets +const ( + rengokuSkillSlotsStart = 0x1B + rengokuSkillSlotsEnd = 0x21 + rengokuSkillValuesStart = 0x2E + rengokuSkillValuesEnd = 0x3A + rengokuPointsStart = 0x3B + rengokuPointsEnd = 0x47 + rengokuMaxStageMpOffset = 71 + rengokuMinPayloadSize = 91 + rengokuMaxPayloadSize = 4096 +) + // rengokuSkillsZeroed checks if the skill slot IDs (offsets 0x1B-0x20) and // equipped skill values (offsets 0x2E-0x39) are all zero in a rengoku save blob. func rengokuSkillsZeroed(data []byte) bool { - if len(data) < 0x3A { + if len(data) < rengokuSkillValuesEnd { return true } - for _, b := range data[0x1B:0x21] { + for _, b := range data[rengokuSkillSlotsStart:rengokuSkillSlotsEnd] { if b != 0 { return false } } - for _, b := range data[0x2E:0x3A] { + for _, b := range data[rengokuSkillValuesStart:rengokuSkillValuesEnd] { if b != 0 { return false } @@ -35,10 +48,10 @@ func rengokuSkillsZeroed(data []byte) bool { // rengokuHasPoints checks if any skill point allocation (offsets 0x3B-0x46) is nonzero. func rengokuHasPoints(data []byte) bool { - if len(data) < 0x47 { + if len(data) < rengokuPointsEnd { return false } - for _, b := range data[0x3B:0x47] { + for _, b := range data[rengokuPointsStart:rengokuPointsEnd] { if b != 0 { return true } @@ -51,15 +64,15 @@ func rengokuHasPoints(data []byte) bool { // preserving the skills that the client failed to populate due to a race // condition during area transitions (see issue #85). func rengokuMergeSkills(dst, src []byte) { - copy(dst[0x1B:0x21], src[0x1B:0x21]) - copy(dst[0x2E:0x3A], src[0x2E:0x3A]) + copy(dst[rengokuSkillSlotsStart:rengokuSkillSlotsEnd], src[rengokuSkillSlotsStart:rengokuSkillSlotsEnd]) + copy(dst[rengokuSkillValuesStart:rengokuSkillValuesEnd], src[rengokuSkillValuesStart:rengokuSkillValuesEnd]) } func handleMsgMhfSaveRengokuData(s *Session, p mhfpacket.MHFPacket) { // Saved every floor on road, holds values such as floors progressed, points etc. // Can be safely handled by the client. pkt := p.(*mhfpacket.MsgMhfSaveRengokuData) - if len(pkt.RawDataPayload) < 91 || len(pkt.RawDataPayload) > 4096 { + if len(pkt.RawDataPayload) < rengokuMinPayloadSize || len(pkt.RawDataPayload) > rengokuMaxPayloadSize { s.logger.Warn("Rengoku payload size out of range", zap.Int("len", len(pkt.RawDataPayload))) doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4)) return @@ -72,10 +85,10 @@ func handleMsgMhfSaveRengokuData(s *Session, p mhfpacket.MHFPacket) { // path triggers a rengoku save BEFORE the load response has been parsed into // the character data area. This produces a save with zeroed skill fields but // preserved point totals. Detect this pattern and merge existing skill data. - if len(saveData) >= 0x47 && rengokuSkillsZeroed(saveData) && rengokuHasPoints(saveData) { + if len(saveData) >= rengokuPointsEnd && rengokuSkillsZeroed(saveData) && rengokuHasPoints(saveData) { var existing []byte if err := s.server.db.QueryRow("SELECT rengokudata FROM characters WHERE id=$1", s.charID).Scan(&existing); err == nil { - if len(existing) >= 0x47 && !rengokuSkillsZeroed(existing) { + if len(existing) >= rengokuPointsEnd && !rengokuSkillsZeroed(existing) { s.logger.Info("Rengoku save has zeroed skills with invested points, preserving existing skills", zap.Uint32("charID", s.charID)) merged := make([]byte, len(saveData)) @@ -106,7 +119,7 @@ func handleMsgMhfSaveRengokuData(s *Session, p mhfpacket.MHFPacket) { return } bf := byteframe.NewByteFrameFromBytes(saveData) - _, _ = bf.Seek(71, 0) + _, _ = bf.Seek(rengokuMaxStageMpOffset, 0) maxStageMp := bf.ReadUint32() maxScoreMp := bf.ReadUint32() _, _ = bf.Seek(4, 1) diff --git a/server/channelserver/handlers_session.go b/server/channelserver/handlers_session.go index 8353e70b3..4a2571800 100644 --- a/server/channelserver/handlers_session.go +++ b/server/channelserver/handlers_session.go @@ -500,12 +500,12 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { case 1, 2, 3: // usersearchidx, usersearchname, lobbysearchname // Snapshot matching sessions under lock, then build response outside locks. type sessionResult struct { - charID uint32 - name []byte - stageID []byte - ip net.IP - port uint16 - userBin3 []byte + charID uint32 + name []byte + stageID []byte + ip net.IP + port uint16 + userBin3 []byte } var results []sessionResult @@ -656,15 +656,15 @@ func handleMsgMhfTransitMessage(s *Session, p mhfpacket.MHFPacket) { } // Snapshot matching stages under lock, then build response outside locks. type stageResult struct { - ip net.IP - port uint16 - clientCount int - reserved int - maxPlayers uint16 - stageID string - stageData []int16 - rawBinData0 []byte - rawBinData1 []byte + ip net.IP + port uint16 + clientCount int + reserved int + maxPlayers uint16 + stageID string + stageData []int16 + rawBinData0 []byte + rawBinData1 []byte } var stageResults []stageResult diff --git a/server/channelserver/model_character.go b/server/channelserver/model_character.go index f84d02875..f732b1042 100644 --- a/server/channelserver/model_character.go +++ b/server/channelserver/model_character.go @@ -13,20 +13,20 @@ import ( type SavePointer int const ( - pGender = iota // +1 - pRP // +2 - pHouseTier // +5 - pHouseData // +195 - pBookshelfData // +lBookshelfData - pGalleryData // +1748 - pToreData // +240 - pGardenData // +68 - pPlaytime // +4 - pWeaponType // +1 - pWeaponID // +2 - pHR // +2 - pGRP // +4 - pKQF // +8 + pGender = iota + pRP + pHouseTier + pHouseData + pBookshelfData + pGalleryData + pToreData + pGardenData + pPlaytime + pWeaponType + pWeaponID + pHR + pGRP + pKQF lBookshelfData ) @@ -146,16 +146,33 @@ func (save *CharacterSaveData) updateSaveDataWithStruct() { rpBytes := make([]byte, 2) binary.LittleEndian.PutUint16(rpBytes, save.RP) if save.Mode >= _config.F4 { - copy(save.decompSave[save.Pointers[pRP]:save.Pointers[pRP]+2], rpBytes) + copy(save.decompSave[save.Pointers[pRP]:save.Pointers[pRP]+saveFieldRP], rpBytes) } if save.Mode >= _config.G10 { - copy(save.decompSave[save.Pointers[pKQF]:save.Pointers[pKQF]+8], save.KQF) + copy(save.decompSave[save.Pointers[pKQF]:save.Pointers[pKQF]+saveFieldKQF], save.KQF) } } // This will update the save struct with the values stored in the character save +// Save data field sizes +const ( + saveFieldRP = 2 + saveFieldHouseTier = 5 + saveFieldHouseData = 195 + saveFieldGallery = 1748 + saveFieldTore = 240 + saveFieldGarden = 68 + saveFieldPlaytime = 4 + saveFieldWeaponID = 2 + saveFieldHR = 2 + saveFieldGRP = 4 + saveFieldKQF = 8 + saveFieldNameOffset = 88 + saveFieldNameLen = 12 +) + func (save *CharacterSaveData) updateStructWithSaveData() { - save.Name, _ = stringsupport.SJISToUTF8(bfutil.UpToNull(save.decompSave[88:100])) + save.Name, _ = stringsupport.SJISToUTF8(bfutil.UpToNull(save.decompSave[saveFieldNameOffset : saveFieldNameOffset+saveFieldNameLen])) if save.decompSave[save.Pointers[pGender]] == 1 { save.Gender = true } else { @@ -163,24 +180,24 @@ func (save *CharacterSaveData) updateStructWithSaveData() { } if !save.IsNewCharacter { if save.Mode >= _config.S6 { - save.RP = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pRP] : save.Pointers[pRP]+2]) - save.HouseTier = save.decompSave[save.Pointers[pHouseTier] : save.Pointers[pHouseTier]+5] - save.HouseData = save.decompSave[save.Pointers[pHouseData] : save.Pointers[pHouseData]+195] + save.RP = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pRP] : save.Pointers[pRP]+saveFieldRP]) + save.HouseTier = save.decompSave[save.Pointers[pHouseTier] : save.Pointers[pHouseTier]+saveFieldHouseTier] + save.HouseData = save.decompSave[save.Pointers[pHouseData] : save.Pointers[pHouseData]+saveFieldHouseData] save.BookshelfData = save.decompSave[save.Pointers[pBookshelfData] : save.Pointers[pBookshelfData]+save.Pointers[lBookshelfData]] - save.GalleryData = save.decompSave[save.Pointers[pGalleryData] : save.Pointers[pGalleryData]+1748] - save.ToreData = save.decompSave[save.Pointers[pToreData] : save.Pointers[pToreData]+240] - save.GardenData = save.decompSave[save.Pointers[pGardenData] : save.Pointers[pGardenData]+68] - save.Playtime = binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pPlaytime] : save.Pointers[pPlaytime]+4]) + save.GalleryData = save.decompSave[save.Pointers[pGalleryData] : save.Pointers[pGalleryData]+saveFieldGallery] + save.ToreData = save.decompSave[save.Pointers[pToreData] : save.Pointers[pToreData]+saveFieldTore] + save.GardenData = save.decompSave[save.Pointers[pGardenData] : save.Pointers[pGardenData]+saveFieldGarden] + save.Playtime = binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pPlaytime] : save.Pointers[pPlaytime]+saveFieldPlaytime]) save.WeaponType = save.decompSave[save.Pointers[pWeaponType]] - save.WeaponID = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pWeaponID] : save.Pointers[pWeaponID]+2]) - save.HR = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pHR] : save.Pointers[pHR]+2]) + save.WeaponID = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pWeaponID] : save.Pointers[pWeaponID]+saveFieldWeaponID]) + save.HR = binary.LittleEndian.Uint16(save.decompSave[save.Pointers[pHR] : save.Pointers[pHR]+saveFieldHR]) if save.Mode >= _config.G1 { if save.HR == uint16(999) { - save.GR = grpToGR(int(binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pGRP] : save.Pointers[pGRP]+4]))) + save.GR = grpToGR(int(binary.LittleEndian.Uint32(save.decompSave[save.Pointers[pGRP] : save.Pointers[pGRP]+saveFieldGRP]))) } } if save.Mode >= _config.G10 { - save.KQF = save.decompSave[save.Pointers[pKQF] : save.Pointers[pKQF]+8] + save.KQF = save.decompSave[save.Pointers[pKQF] : save.Pointers[pKQF]+saveFieldKQF] } } }