mirror of
https://github.com/Mezeporta/Erupe.git
synced 2026-03-22 07:32:32 +01:00
Merge pull request #157 from Mezeporta/fix/85-rengoku-skill-reset
fix(rengoku): prevent Sky Corridor race condition from wiping skill data (#85)
This commit is contained in:
@@ -1,20 +1,63 @@
|
|||||||
package channelserver
|
package channelserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/binary"
|
||||||
ps "erupe-ce/common/pascalstring"
|
ps "erupe-ce/common/pascalstring"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/jmoiron/sqlx"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/jmoiron/sqlx"
|
||||||
|
|
||||||
"erupe-ce/common/byteframe"
|
"erupe-ce/common/byteframe"
|
||||||
"erupe-ce/network/mhfpacket"
|
"erupe-ce/network/mhfpacket"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, b := range data[0x1B:0x21] {
|
||||||
|
if b != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, b := range data[0x2E:0x3A] {
|
||||||
|
if b != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// rengokuHasPoints checks if any skill point allocation (offsets 0x3B-0x46) is nonzero.
|
||||||
|
func rengokuHasPoints(data []byte) bool {
|
||||||
|
if len(data) < 0x47 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, b := range data[0x3B:0x47] {
|
||||||
|
if b != 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// rengokuMergeSkills copies skill slot IDs (0x1B-0x20) and equipped skill
|
||||||
|
// values (0x2E-0x39) from existing data into the incoming save payload,
|
||||||
|
// 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])
|
||||||
|
}
|
||||||
|
|
||||||
func handleMsgMhfSaveRengokuData(s *Session, p mhfpacket.MHFPacket) {
|
func handleMsgMhfSaveRengokuData(s *Session, p mhfpacket.MHFPacket) {
|
||||||
// saved every floor on road, holds values such as floors progressed, points etc.
|
// Saved every floor on road, holds values such as floors progressed, points etc.
|
||||||
// can be safely handled by the client
|
// Can be safely handled by the client.
|
||||||
pkt := p.(*mhfpacket.MsgMhfSaveRengokuData)
|
pkt := p.(*mhfpacket.MsgMhfSaveRengokuData)
|
||||||
if len(pkt.RawDataPayload) < 91 || len(pkt.RawDataPayload) > 4096 {
|
if len(pkt.RawDataPayload) < 91 || len(pkt.RawDataPayload) > 4096 {
|
||||||
s.logger.Warn("Rengoku payload size out of range", zap.Int("len", len(pkt.RawDataPayload)))
|
s.logger.Warn("Rengoku payload size out of range", zap.Int("len", len(pkt.RawDataPayload)))
|
||||||
@@ -22,13 +65,47 @@ func handleMsgMhfSaveRengokuData(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
dumpSaveData(s, pkt.RawDataPayload, "rengoku")
|
dumpSaveData(s, pkt.RawDataPayload, "rengoku")
|
||||||
_, err := s.server.db.Exec("UPDATE characters SET rengokudata=$1 WHERE id=$2", pkt.RawDataPayload, s.charID)
|
|
||||||
|
saveData := pkt.RawDataPayload
|
||||||
|
|
||||||
|
// Guard against a client race condition (issue #85): the Sky Corridor init
|
||||||
|
// 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) {
|
||||||
|
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) {
|
||||||
|
s.logger.Info("Rengoku save has zeroed skills with invested points, preserving existing skills",
|
||||||
|
zap.Uint32("charID", s.charID))
|
||||||
|
merged := make([]byte, len(saveData))
|
||||||
|
copy(merged, saveData)
|
||||||
|
rengokuMergeSkills(merged, existing)
|
||||||
|
saveData = merged
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also reject saves where the sentinel is 0 (no data) if valid data already exists.
|
||||||
|
if len(saveData) >= 4 && binary.BigEndian.Uint32(saveData[:4]) == 0 {
|
||||||
|
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) >= 4 && binary.BigEndian.Uint32(existing[:4]) != 0 {
|
||||||
|
s.logger.Warn("Refusing to overwrite valid rengoku data with empty sentinel",
|
||||||
|
zap.Uint32("charID", s.charID))
|
||||||
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := s.server.db.Exec("UPDATE characters SET rengokudata=$1 WHERE id=$2", saveData, s.charID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to save rengokudata", zap.Error(err))
|
s.logger.Error("Failed to save rengokudata", zap.Error(err))
|
||||||
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
doAckSimpleSucceed(s, pkt.AckHandle, make([]byte, 4))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
bf := byteframe.NewByteFrameFromBytes(pkt.RawDataPayload)
|
bf := byteframe.NewByteFrameFromBytes(saveData)
|
||||||
_, _ = bf.Seek(71, 0)
|
_, _ = bf.Seek(71, 0)
|
||||||
maxStageMp := bf.ReadUint32()
|
maxStageMp := bf.ReadUint32()
|
||||||
maxScoreMp := bf.ReadUint32()
|
maxScoreMp := bf.ReadUint32()
|
||||||
@@ -53,7 +130,8 @@ func handleMsgMhfLoadRengokuData(s *Session, p mhfpacket.MHFPacket) {
|
|||||||
var data []byte
|
var data []byte
|
||||||
err := s.server.db.QueryRow("SELECT rengokudata FROM characters WHERE id = $1", s.charID).Scan(&data)
|
err := s.server.db.QueryRow("SELECT rengokudata FROM characters WHERE id = $1", s.charID).Scan(&data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("Failed to load rengokudata", zap.Error(err))
|
s.logger.Error("Failed to load rengokudata", zap.Error(err),
|
||||||
|
zap.Uint32("charID", s.charID))
|
||||||
}
|
}
|
||||||
if len(data) > 0 {
|
if len(data) > 0 {
|
||||||
doAckBufSucceed(s, pkt.AckHandle, data)
|
doAckBufSucceed(s, pkt.AckHandle, data)
|
||||||
|
|||||||
1068
server/channelserver/handlers_rengoku_integration_test.go
Normal file
1068
server/channelserver/handlers_rengoku_integration_test.go
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user