fix(challenge,scene): challenge id auto-detect, real ScRsp ids, teleport ownership

Round-2 fixes from the 01:44-01:53 client session:

- StartChallenge (MoC/PF/AS normal start) decoded challenge_id=0 -> client
  retried and aborted. detectChallengeID scans top-level varints for the
  first plausible id (challenge_data + known mode ranges) when the strict
  tag-14 decode misses; payload hexdump logged when still 0
- StartChallengeScRsp now carries stage_info (tag 13) with boss node buff
  echo, per the 4.6.51 proto
- GetSwitchHandData replies 8108 (the real ScRsp) - 8104 is
  SwitchHandStartCsReq, we were sending a request id back at the client
- GetSceneMapInfo: strict proto parse failed on some payloads (layout
  drift) which stalled the map UI ('no pillars / navigation loading') -
  now a raw handler with a lenient wire walk fallback + hexdump logging
- teleport destination ('AA teleport landed in AP'): teleport ids repeat
  on every floor and teleports.json was scanned globally in random map
  order. Map clicks now resolve floor-first via scene_identifier, and the
  global id scan never touches teleports.json
- tierce: arena 3014101 -> 3000101 fallback (client has no MapEntrance
  3014101), same as StartChallenge
This commit is contained in:
CAtlas0784 committed 2026-10-07 02:13:45 +07:00
1 parent d364b9616f
commit 876f20d857
4 files changed
+229 -29

No files matched your search

+125 -14
View File
@@ -126,11 +126,24 @@ func init() {
// req.EntryId doubles as a floor id in some map flows
resolvedEntry = findEntryForFloor(res, req.EntryId)
case req.TeleportId != 0:
resolvedEntry = findEntryByTeleport(res, req.TeleportId, currentEntry())
floorID := uint32(0)
if sceneIdent != nil {
floorID = sceneIdent.FloorId
}
if entry := findEntryByTeleportInFloor(res, req.TeleportId, floorID); entry != 0 {
resolvedEntry = entry
} else {
resolvedEntry = findEntryByTeleport(res, req.TeleportId, currentEntry())
}
case sceneIdent != nil && sceneIdent.TeleportNigger != nil:
if t, okc := sceneIdent.TeleportNigger.(*goproto.SceneIdentifier_MDAIDPPKOPO); okc && t.MDAIDPPKOPO != nil && t.MDAIDPPKOPO.PLIDNBMCIJH != 0 {
// map teleport click: teleport id arrives via scene_identifier
resolvedEntry = findEntryByTeleport(res, t.MDAIDPPKOPO.PLIDNBMCIJH, currentEntry())
// map teleport click: teleport id + floor arrive via scene_identifier
tpID := t.MDAIDPPKOPO.PLIDNBMCIJH
if entry := findEntryByTeleportInFloor(res, tpID, sceneIdent.FloorId); entry != 0 {
resolvedEntry = entry
} else {
resolvedEntry = findEntryByTeleport(res, tpID, currentEntry())
}
}
case interactID != 0:
resolvedEntry = findEntryByInteract(res, interactID, currentEntry())
@@ -221,13 +234,33 @@ func init() {
return nil
})
handle("GetSceneMapInfo", func(s *Session, req *goproto.GetSceneMapInfoCsReq, rsp *goproto.GetSceneMapInfoScRsp) error {
// decodeSceneIdentifiersLenient: the 4.6.51 client's cmd-1461 payload can
// fail strict proto parsing (field layout drift between builds) — walk the
// wire format manually and pull floor_id/content_id out of field 2.
// GetSceneMapInfo as a raw handler: the 4.6.51 client's cmd-1461 payload
// can fail strict proto parsing (layout drift), so fall back to a manual
// wire walk instead of dropping the request (the map UI stalls without
// this reply - no pillars, "navigation loading").
rawHandlers[1461] = func(s *Session, payload []byte) error {
res, err := common.GameRes()
if err != nil {
return err
}
rsp := &goproto.GetSceneMapInfoScRsp{}
for _, si := range req.SceneIdentifiers {
var identifiers []*goproto.SceneIdentifier
var strictReq goproto.GetSceneMapInfoCsReq
if err := proto.Unmarshal(payload, &strictReq); err == nil {
identifiers = strictReq.SceneIdentifiers
}
if len(identifiers) == 0 {
identifiers = decodeSceneIdentifiersLenient(payload)
if len(identifiers) > 0 {
PacketLog("[SCENE] GetSceneMapInfo: used lenient decode (strict parse failed)")
}
}
for _, si := range identifiers {
floorID := si.FloorId
mapInfo := &goproto.SceneMapInfo{
FloorId: floorID,
@@ -283,8 +316,9 @@ func init() {
rsp.SceneMapInfo = append(rsp.SceneMapInfo, mapInfo)
}
return nil
})
return s.Send(rsp)
}
handle("SceneEntityMove", func(s *Session, req *goproto.SceneEntityMoveCsReq, rsp *goproto.SceneEntityMoveScRsp) error {
player := s.JSON()
@@ -413,16 +447,35 @@ func mapHasEntry(res *common.JsonConfig, entryID uint32) bool {
return ok
}
func findEntryByTeleport(res *common.JsonConfig, teleportID uint32, fallback uint32) uint32 {
// teleports.json covers floors whose res config has no inline teleports:
// teleport id -> floor -> entry (via the floor name search)
for floorID, m := range common.Teleports() {
if _, ok := m[teleportID]; ok {
if entry := findEntryForFloor(res, floorID); entry != 0 {
return entry
// findEntryByTeleportInFloor resolves a teleport inside a KNOWN floor
// (map-teleport clicks always carry the floor in scene_identifier).
func findEntryByTeleportInFloor(res *common.JsonConfig, teleportID, floorID uint32) uint32 {
if floorID != 0 {
suffix := fmt.Sprintf("_F%d", floorID)
entryIDs := sortedU32Keys2(res.LevelOutputConfigs)
for _, eid := range entryIDs {
for name, cfg := range res.LevelOutputConfigs[eid] {
if len(name) <= len(suffix) || name[len(name)-len(suffix):] != suffix {
continue
}
for _, sc := range cfg.Scenes {
if _, ok := sc.Teleports[teleportID]; ok {
return eid
}
}
}
}
// floor known but teleport not in its res config (teleports.json-only
// floors): the entry owning the floor is still the right destination
return findEntryForFloor(res, floorID)
}
return 0
}
func findEntryByTeleport(res *common.JsonConfig, teleportID uint32, fallback uint32) uint32 {
// NOTE: never scan teleports.json globally by id alone - teleport ids
// (1001, 1002, ...) repeat on every floor, which sent players to a random
// region ("AA teleport landed in AP").
entryIDs := sortedU32Keys2(res.LevelOutputConfigs)
for _, eid := range entryIDs {
names := make([]string, 0, len(res.LevelOutputConfigs[eid]))
@@ -466,6 +519,64 @@ func findEntryByInteract(res *common.JsonConfig, interactID uint32, fallback uin
return fallback
}
// decodeSceneIdentifiersLenient walks the raw wire format of a
// GetSceneMapInfoCsReq payload: field 2 = repeated SceneIdentifier;
// inside each: floor_id = 13 (varint), content_id = 7 (varint).
func decodeSceneIdentifiersLenient(payload []byte) []*goproto.SceneIdentifier {
var out []*goproto.SceneIdentifier
buf := payload
for len(buf) > 0 {
tag, n := readUvarint(buf)
if n == 0 {
break
}
buf = buf[n:]
fieldNumber := uint32(tag >> 3)
wireType := uint32(tag & 0x7)
if fieldNumber == 2 && wireType == 2 {
l, n2 := readUvarint(buf)
if n2 == 0 || int(l) > len(buf[n2:]) {
break
}
sub := buf[n2 : n2+int(l)]
buf = buf[n2+int(l):]
si := &goproto.SceneIdentifier{}
for len(sub) > 0 {
stag, n3 := readUvarint(sub)
if n3 == 0 {
break
}
sfn := uint32(stag >> 3)
swt := uint32(stag & 0x7)
sub2 := sub[n3:]
if swt == 0 && sfn == 13 {
v, n4 := readUvarint(sub2)
if n4 == 0 {
break
}
sub = sub2[n4:]
si.FloorId = uint32(v)
} else if swt == 0 && sfn == 7 {
v, n4 := readUvarint(sub2)
if n4 == 0 {
break
}
sub = sub2[n4:]
si.ContentId = uint32(v)
} else {
skipWireField(swt, &sub)
}
}
out = append(out, si)
} else {
skipWireField(wireType, &buf)
}
}
return out
}
func sortedU32Keys2[V any](m map[uint32]V) []uint32 {
keys := make([]uint32, 0, len(m))
for k := range m {