fix: sync current handlers, repair relic data and restore visible launcher consoles

This commit is contained in:
CAtlas0784 committed 2026-10-16 22:19:52 +07:00
1 parent 163d4433e6
commit 74f3c4411b
46 files changed
+4599 -1402

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
package gameserver
import (
"crypto/sha256"
_ "embed"
"encoding/base64"
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"time"
"gac-hkrpg-go/internal/common"
goproto "gac-hkrpg-go/protocol/proto"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/protowire"
"google.golang.org/protobuf/proto"
)
//go:embed battle_wire_probe.lua
var battleWireProbe string
type battleCapture struct {
ResponseName string `json:"response_name"`
CmdID uint16 `json:"cmd_id"`
ResponseSHA256 string `json:"response_sha256"`
BattleSHA256 string `json:"battle_sha256"`
CapturedUTC time.Time `json:"captured_utc"`
StageID uint32 `json:"stage_id"`
}
func saveBattleCapture(msg proto.Message, cmdID uint16, response []byte) ([]byte, *battleCapture, error) {
carrier, ok := msg.(interface {
GetBattleInfo() *goproto.SceneBattleInfo
})
if !ok || carrier.GetBattleInfo() == nil {
return nil, nil, nil
}
battle := carrier.GetBattleInfo()
wire, err := extractBattleWire(msg, response)
if err != nil {
return nil, nil, err
}
pretty, err := (protojson.MarshalOptions{Indent: " "}).Marshal(battle)
if err != nil {
return nil, nil, err
}
meta := &battleCapture{
ResponseName: string(msg.ProtoReflect().Descriptor().Name()), CmdID: cmdID,
ResponseSHA256: fmt.Sprintf("%x", sha256.Sum256(response)),
BattleSHA256: fmt.Sprintf("%x", sha256.Sum256(wire)),
CapturedUTC: time.Now().UTC(), StageID: battle.StageId,
}
info, err := json.MarshalIndent(meta, "", " ")
if err != nil {
return nil, nil, err
}
files := map[string][]byte{"battle_last_response.bin": response, "battle_last.bin": wire, "battle_last.json": pretty, "battle_last_capture.json": info}
// Keep earlier failures intact so consecutive attempts can be compared.
archive := filepath.Join("battle_diagnostics", meta.CapturedUTC.Format("20060102T150405.000000000Z")+"_"+meta.ResponseSHA256[:12])
if err := os.MkdirAll(archive, 0700); err != nil {
return nil, nil, err
}
for name, data := range files {
if err := os.WriteFile(filepath.Join(archive, name), data, 0600); err != nil {
return nil, nil, err
}
if err := os.WriteFile(name, data, 0600); err != nil {
return nil, nil, err
}
}
return wire, meta, nil
}
func extractBattleWire(msg proto.Message, response []byte) ([]byte, error) {
field := msg.ProtoReflect().Descriptor().Fields().ByName("battle_info")
if field == nil {
return nil, fmt.Errorf("response has no battle_info field")
}
for len(response) > 0 {
number, kind, n := protowire.ConsumeTag(response)
if n < 0 {
return nil, protowire.ParseError(n)
}
response = response[n:]
if number == protowire.Number(field.Number()) && kind == protowire.BytesType {
wire, n := protowire.ConsumeBytes(response)
if n < 0 {
return nil, protowire.ParseError(n)
}
return append([]byte(nil), wire...), nil
}
n = protowire.ConsumeFieldValue(number, kind, response)
if n < 0 {
return nil, protowire.ParseError(n)
}
response = response[n:]
}
return nil, fmt.Errorf("battle_info missing from serialized response")
}
func makeBattleProbe(wire []byte, typeName, hash string) []byte {
// Values are quoted as Lua strings; base64 contains no executable syntax.
replacer := strings.NewReplacer("__TYPE_NAME__", strconv.Quote(typeName), "__PAYLOAD__", strconv.Quote(base64.StdEncoding.EncodeToString(wire)), "__CAPTURE_HASH__", strconv.Quote(hash))
return []byte(replacer.Replace(battleWireProbe))
}
func (s *Session) captureBattleResponse(msg proto.Message, cmdID uint16, body []byte) {
wire, meta, err := saveBattleCapture(msg, cmdID, body)
if err != nil {
Log(fmt.Sprintf("[BATTLE CAPTURE] %v", err))
return
}
if meta == nil {
return
}
Log(fmt.Sprintf("[BATTLE CAPTURE] cmd=%d stage=%d response_sha256=%s battle_sha256=%s", cmdID, meta.StageID, meta.ResponseSHA256, meta.BattleSHA256))
cfg, err := common.LoadRuntimeConfig()
if err != nil {
Log(fmt.Sprintf("[BATTLE PROBE] config error: %v", err))
return
}
if cfg.BattleDiagnostics.ClientBattleType == "" {
return
}
s.battleProbeMu.Lock()
s.battleProbe = makeBattleProbe(wire, cfg.BattleDiagnostics.ClientBattleType, meta.BattleSHA256)
s.battleProbeMu.Unlock()
Log("[BATTLE PROBE] queued read-only client parser check for next heartbeat")
}
func (s *Session) takeBattleProbe() []byte {
s.battleProbeMu.Lock()
defer s.battleProbeMu.Unlock()
probe := s.battleProbe
s.battleProbe = nil
return probe
}
@@ -0,0 +1,72 @@
package gameserver
import (
"bytes"
"crypto/sha256"
"fmt"
"os"
"strings"
"testing"
goproto "gac-hkrpg-go/protocol/proto"
"google.golang.org/protobuf/proto"
)
func TestCaptureUsesExactResponseBattleBytes(t *testing.T) {
t.Chdir(t.TempDir())
// Fixture values exercise zero-step affixes and map ordering on the wire.
battle := &goproto.SceneBattleInfo{BattleAvatarList: []*goproto.BattleAvatar{{Id: 1204, RelicList: []*goproto.BattleRelic{{Id: 61151, SubAffixList: []*goproto.RelicAffix{{AffixId: 7, Cnt: 3, Step: 0}}}}}}, BuffList: []*goproto.BattleBuff{{DynamicValues: map[string]float32{"first": 1, "second": 2}}}}
rsp := &goproto.StartQuickCocoonStageRsp{BattleInfo: battle}
body, err := proto.Marshal(rsp)
if err != nil {
t.Fatal(err)
}
wire, meta, err := saveBattleCapture(rsp, 1371, body)
if err != nil {
t.Fatal(err)
}
if meta.ResponseSHA256 != fmt.Sprintf("%x", sha256.Sum256(body)) {
t.Fatal("response checksum mismatch")
}
var decoded goproto.SceneBattleInfo
if err := proto.Unmarshal(wire, &decoded); err != nil {
t.Fatal(err)
}
if !proto.Equal(battle, &decoded) {
t.Fatal("captured battle differs from outgoing battle")
}
written, err := os.ReadFile("battle_last_response.bin")
if err != nil || !bytes.Equal(written, body) {
t.Fatalf("response not preserved: %v", err)
}
written, err = os.ReadFile("battle_last.bin")
if err != nil || !bytes.Equal(written, wire) {
t.Fatalf("battle wire not preserved: %v", err)
}
probe := string(makeBattleProbe(wire, "FixtureBattleType", meta.BattleSHA256))
if strings.Contains(probe, "__TYPE_NAME__") || !strings.Contains(probe, meta.BattleSHA256) {
t.Fatal("probe placeholders not populated")
}
}
func TestProbeDoesNotRunWithoutBattle(t *testing.T) {
t.Chdir(t.TempDir())
_, meta, err := saveBattleCapture(&goproto.GetCurBattleInfoScRsp{}, 171, nil)
if err != nil || meta != nil {
t.Fatalf("empty login response captured: %v", err)
}
if _, err := os.Stat("battle_last.bin"); !os.IsNotExist(err) {
t.Fatal("empty response wrote a capture")
}
s := &Session{}
s.battleProbe = []byte("fixture")
if string(s.takeBattleProbe()) != "fixture" || len(s.takeBattleProbe()) != 0 {
t.Fatal("probe was not consumed exactly once")
}
}
func TestBattleWireRejectsTruncatedResponse(t *testing.T) {
if _, err := extractBattleWire(&goproto.StartQuickCocoonStageRsp{}, []byte{0x2a, 0x7f}); err == nil {
t.Fatal("truncated payload accepted")
}
}
+95
View File
@@ -0,0 +1,95 @@
-- Read-only: decode the captured battle with the client's own protobuf parser.
-- This does not start battles, modify actors, replace handlers, or change saves.
do
local captureHash = __CAPTURE_HASH__
if _G.__gacBattleWireProbeV2Hash ~= captureHash then
_G.__gacBattleWireProbeV2Hash = captureHash
local function log(text)
CS.UnityEngine.Debug.Log('[GAC_BATTLE_WIRE] ' .. captureHash .. ' ' .. tostring(text))
end
local ok, err = pcall(function()
local typeName = __TYPE_NAME__
local assemblies = CS.System.AppDomain.CurrentDomain:GetAssemblies()
local messageType = nil
for i = 0, assemblies.Length - 1 do
messageType = assemblies[i]:GetType(typeName)
if messageType ~= nil then break end
end
if messageType == nil then error('message type not found: ' .. typeName) end
-- IL2CPP builds can expose the parser as a non-public member.
-- Include both visibility levels instead of assuming a public getter.
local flags = CS.System.Reflection.BindingFlags.Public
+ CS.System.Reflection.BindingFlags.NonPublic
+ CS.System.Reflection.BindingFlags.Static
+ CS.System.Reflection.BindingFlags.Instance
local properties = messageType:GetProperties(flags)
local parser, descriptor = nil, nil
log('message_type=' .. tostring(messageType.FullName) .. ' properties=' .. tostring(properties.Length))
for i = 0, properties.Length - 1 do
local property = properties[i]
local getter = property:GetGetMethod(true)
if getter ~= nil and getter.IsStatic then
local propertyType = tostring(property.PropertyType.FullName)
log('static_property=' .. tostring(property.Name) .. ' type=' .. propertyType)
if string.find(propertyType, 'Google.Protobuf.MessageParser', 1, true) then
parser = property:GetValue(nil, nil)
elseif propertyType == 'Google.Protobuf.Reflection.MessageDescriptor' then
descriptor = property:GetValue(nil, nil)
end
end
end
local fields = messageType:GetFields(flags)
for i = 0, fields.Length - 1 do
local field = fields[i]
local fieldType = tostring(field.FieldType.FullName)
log('member=' .. tostring(field.Name) .. ' type=' .. fieldType .. ' static=' .. tostring(field.IsStatic))
if field.IsStatic then
if parser == nil and string.find(fieldType, 'Google.Protobuf.MessageParser', 1, true) then
parser = field:GetValue(nil)
elseif descriptor == nil and fieldType == 'Google.Protobuf.Reflection.MessageDescriptor' then
descriptor = field:GetValue(nil)
end
end
end
local bytes = CS.System.Convert.FromBase64String(__PAYLOAD__)
local message = nil
if parser ~= nil then
message = parser:ParseFrom(bytes)
else
-- A generated message can decode through MergeFrom even when
-- its static parser metadata was stripped from reflection.
message = CS.System.Activator.CreateInstance(messageType)
local methods = messageType:GetMethods(flags)
local merge = nil
for i = 0, methods.Length - 1 do
local method = methods[i]
local parameters = method:GetParameters()
if not method.IsStatic and parameters.Length == 1
and tostring(parameters[0].ParameterType.FullName) == 'Google.Protobuf.CodedInputStream' then
merge = method
break
end
end
if merge == nil then error('no parser or CodedInputStream decoder on ' .. typeName) end
local input = CS.Google.Protobuf.CodedInputStream(bytes)
local arguments = CS.System.Array.CreateInstance(typeof(CS.System.Object), 1)
arguments:SetValue(input, 0)
merge:Invoke(message, arguments)
end
log('type=' .. typeName .. ' bytes=' .. tostring(bytes.Length))
log('decoded=' .. message:ToString())
if descriptor ~= nil then
local fields = descriptor.Fields:InFieldNumberOrder()
local iterator = fields:GetEnumerator()
while iterator:MoveNext() do
local field = iterator.Current
local nested = field.MessageType
log('field=' .. tostring(field.FieldNumber) .. ' name=' .. tostring(field.Name)
.. ' type=' .. tostring(field.FieldType)
.. ' nested=' .. (nested ~= nil and tostring(nested.FullName) or ''))
end
end
end)
if not ok then log('probe_error=' .. tostring(err)) end
end
end
+26 -13
View File
@@ -1,6 +1,7 @@
package gameserver
import (
"errors"
"strings"
"fmt"
@@ -42,6 +43,10 @@ var BaseAvatarIDs = common.BaseAvatarIDs
type handlerFunc func(s *Session, payload []byte) error
// errResponseAlreadySent tells the typed dispatcher that the handler has
// explicitly sent its response early to preserve packet ordering.
var errResponseAlreadySent = errors.New("response already sent")
var (
typedHandlers = map[uint16]handlerFunc{}
dummyResponses = map[uint16]uint16{} // req cmd -> rsp cmd (empty body reply)
@@ -59,12 +64,12 @@ func handle[ReqT any, RspT any](name string, fn func(s *Session, req *ReqT, rsp
func handleNamed[ReqT any, RspT any](name, rspName string, fn func(s *Session, req *ReqT, rsp *RspT) error) {
reqID, ok := goproto.CmdIDByName[name+"CsReq"]
if !ok {
Log("[DISPATCH] proto 4.6.51 has no " + name + "CsReq; handler skipped")
Log("[DISPATCH] loaded protocol has no " + name + "CsReq; handler skipped")
return
}
rspID, ok := goproto.CmdIDByName[rspName]
if !ok {
Log("[DISPATCH] proto 4.6.51 has no " + rspName + "; handler skipped")
Log("[DISPATCH] loaded protocol has no " + rspName + "; handler skipped")
return
}
typedHandlers[uint16(reqID)] = func(s *Session, payload []byte) error {
@@ -78,7 +83,9 @@ func handleNamed[ReqT any, RspT any](name, rspName string, fn func(s *Session, r
}
}
rsp := new(RspT)
if err := fn(s, req, rsp); err != nil {
if err := fn(s, req, rsp); errors.Is(err, errResponseAlreadySent) {
return nil
} else if err != nil {
return err
}
return s.Send(any(rsp).(proto.Message))
@@ -91,12 +98,25 @@ func dummy(name string) {
reqID, ok1 := goproto.CmdIDByName[name+"CsReq"]
rspID, ok2 := goproto.CmdIDByName[name+"ScRsp"]
if !ok1 || !ok2 {
Log("[DISPATCH] proto 4.6.51 has no dummy pair " + name + "; skipped")
Log("[DISPATCH] loaded protocol has no dummy pair " + name + "; skipped")
return
}
dummyResponses[uint16(reqID)] = uint16(rspID)
}
// rawNamed binds only the request declared by the current command table.
// Old numeric aliases can collide with responses after a protocol update.
func rawNamed(request string, fn func(*Session, []byte) error) {
id, ok := goproto.CmdIDByName[request]
if !ok {
panic("raw handler request missing from protocol: " + request)
}
if _, exists := rawHandlers[id]; exists {
panic("duplicate raw handler: " + request)
}
rawHandlers[id] = fn
}
// dangerFamilies are client modules that Sync(nil) -> NRE -> black screen when
// they receive an empty ScRsp (lesson recorded in the Rust packet.rs / HANDOFF):
// never auto-answer these, drop them silently instead.
@@ -112,15 +132,8 @@ func isDangerCmd(cmdID uint16) bool {
return true
}
}
// CycleScore cmd ids (4143/4146) and GridFight range carry generic names
switch cmdID {
case 4143, 4146, 8541, 5410, 5449, 7142, 3174, 9252, 8175, 6995, 6988:
return true
}
// GetMapRotationData: an empty reply NREs the client's map init
// (AdventurePhase._EnterStep1_InitializeMapInfo) - drop until real
// RotaterData is implemented
if cmdID == 6827 {
// This module requires populated rotation state before it can be answered.
if name == "GetMapRotationDataCsReq" || strings.HasPrefix(name, "ChimeraGet") {
return true
}
return false
@@ -1,156 +0,0 @@
package gameserver
// Code generated from the March7th PS ActivityConfig (real 4.6.51 client
// activity ids). DO NOT EDIT manually.
type activityScheduleEntry struct{ activityID, panelID uint32 }
var realActivitySchedule = []activityScheduleEntry{
{1001501, 10015},
{1001701, 10017},
{1002101, 10191},
{1002501, 10025},
{1002601, 10192},
{1004101, 10041},
{1006201, 10066},
{1010801, 10187},
{2000603, 20016},
{2001101, 20011},
{2001102, 20011},
{2001103, 20011},
{2001104, 20011},
{2001105, 20011},
{2001106, 20011},
{2001107, 20011},
{2001108, 20011},
{2001109, 20011},
{2002303, 20024},
{2002502, 20027},
{2006301, 20064},
{2006302, 20064},
{2006400, 20064},
{2008201, 20091},
{2008401, 20092},
{2008601, 20087},
{2008701, 20087},
{2009100, 20091},
{2009200, 20092},
{2009301, 20093},
{2101001, 21010},
{2101101, 21011},
{3000801, 30008},
{4000208, 40002},
{4000301, 40003},
{4000302, 40003},
{4000303, 40003},
{4000304, 40003},
{4000305, 40003},
{4000306, 40003},
{4000307, 40003},
{5000002, 50000},
{5000003, 50000},
{5000004, 50000},
{5000005, 50000},
{5000006, 50000},
{5000007, 50000},
{5000008, 50000},
{5000009, 50000},
{5000010, 50000},
{5000011, 50000},
{5000013, 50000},
{5000105, 50001},
{5000106, 50001},
{5000107, 50001},
{5000201, 50002},
{5000202, 50002},
{5000203, 50002},
{5000204, 50002},
{5000301, 50003},
{5000302, 50003},
{5000303, 50003},
{5000304, 50003},
{5000408, 50004},
{5000501, 50005},
{5000502, 50005},
{5000503, 50005},
{5000504, 50005},
{5000505, 50005},
{5000601, 50006},
{5000602, 50006},
{5000603, 50006},
{5000604, 50006},
{5000605, 50006},
{5000702, 50007},
{5000902, 50009},
{5001202, 50012},
{5001401, 50014},
{5001501, 50015},
{5001802, 50018},
{5002102, 50021},
{5002202, 50022},
{5002402, 50024},
{5002602, 50026},
{5002902, 50029},
{5003402, 50034},
{5003502, 50035},
{5004102, 50041},
{5004202, 50042},
{5004502, 50045},
{5006302, 50063},
{5006802, 50068},
{5007001, 50070},
{5007002, 50070},
{5007003, 50070},
{5007004, 50070},
{5007005, 50070},
{5007007, 50070},
{5007302, 50073},
{5007402, 50074},
{5009202, 50092},
{5009402, 50094},
{5010102, 50101},
{5011102, 50111},
{5011202, 50112},
{5011402, 50114},
{5011602, 50116},
{5012501, 50125},
{5012502, 50125},
{5012503, 50125},
{5012504, 50125},
{5012505, 50125},
{5012601, 50126},
{5012701, 50127},
{5012901, 50129},
{5013201, 50132},
{5013202, 50132},
{5013301, 50133},
{5013401, 50134},
{5013402, 50134},
{5013501, 50135},
{5013502, 50135},
{5013701, 50137},
{5014201, 50142},
{5014202, 50142},
{5014301, 50143},
{5014302, 50143},
{5014303, 50143},
{5014304, 50143},
{5014305, 50143},
{5014311, 50143},
{5014312, 50143},
{5014313, 50143},
{5014314, 50143},
{5014315, 50143},
{5015401, 50154},
{6000101, 60001},
{6000302, 60003},
{6000901, 60009},
{6001402, 60014},
{6001601, 60016},
{6002301, 60023},
{6002401, 60024},
{7100001, 71000},
{7100101, 71001},
{7110001, 71100},
{7110101, 71101},
}
+18 -50
View File
@@ -13,33 +13,25 @@ import (
func init() {
handle("StartCocoonStage", func(s *Session, req *goproto.StartCocoonStageCsReq, rsp *goproto.StartCocoonStageScRsp) error {
Log(fmt.Sprintf("[BATTLE] StartCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d PropEntityId=%d", req.CocoonId, req.Wave, req.WorldLevel, req.PropEntityId))
battleInfo := createBattleInfo(s, 0, 0)
normalizeCalyxBattleInfo(battleInfo, req.Wave, req.WorldLevel)
CURRENT_BATTLE.Store(s.Token, battleInfo)
challengeCnt := req.Wave
if challengeCnt == 0 {
challengeCnt = 1
}
rsp.Retcode = 0
rsp.PropEntityId = req.PropEntityId
rsp.CocoonId = req.CocoonId
rsp.Wave = req.Wave
rsp.ChallengeCnt = challengeCnt
battleInfo := createBattleInfo(s, 0, 0)
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.BattleInfo = battleInfo
return nil
})
// NOTE: 4.6.51 proto names this message StartQuickCocoonStageCsReq/Rsp
// The Go schema names this message StartQuickCocoonStageCsReq/Rsp
handleNamed("StartQuickCocoonStage", "StartQuickCocoonStageRsp", func(s *Session, req *goproto.StartQuickCocoonStageCsReq, rsp *goproto.StartQuickCocoonStageRsp) error {
Log(fmt.Sprintf("[BATTLE] StartQuickCocoonStage: CocoonId=%d Wave=%d WorldLevel=%d", req.CocoonId, req.Wave, req.WorldLevel))
battleInfo := createBattleInfo(s, 0, 0)
normalizeCalyxBattleInfo(battleInfo, req.Wave, req.WorldLevel)
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.Retcode = 0
rsp.CocoonId = req.CocoonId
rsp.Wave = req.Wave
battleInfo := createBattleInfo(s, 0, 0)
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.BattleInfo = battleInfo
return nil
})
@@ -47,7 +39,6 @@ func init() {
handleNamed("StartQuickFarmElement", "StartQuickFarmElementRsp", func(s *Session, req *goproto.StartQuickFarmElementCsReq, rsp *goproto.StartQuickFarmElementRsp) error {
Log(fmt.Sprintf("[BATTLE] StartQuickFarmElement: FarmID=%d WorldLevel=%d", req.PAOFHFLFFHD, req.WorldLevel))
battleInfo := createBattleInfo(s, 0, 0)
normalizeCalyxBattleInfo(battleInfo, 1, req.WorldLevel)
CURRENT_BATTLE.Store(s.Token, battleInfo)
rsp.Retcode = 0
@@ -115,24 +106,24 @@ func init() {
//
// อาการ: กดโจมตีใน overworld แล้วกล้องหลุดออกจากตัวละคร
//
// สาเหตุ (จากโค้ด Rust เดิม): handler ตอบ battle_info + monster_battle_info
// สาเหตุ (จากโค้ด Rust เดิม): handler ตอบ_info + monster_battle_info
// ให้ client เข้าสู่การต่อสู้กับ target ที่ "ผ่าน filter แบบหลวมๆ"
// filter เดิม: id > 30000 (monster ในฉาก) || id < 1000 (avatar entity ของผู้เล่น!)
// ถ้า targets ว่างและอยู่ใน arena (entry >= 3000000) ยัง "หลอก" target
// เป็น 30001 อีกชั้น
// พอ client ได้ battle_info กับ "monster" ที่จริงคือ prop/NPC/ตัวผู้เล่นเอง
// หรือ monster ที่ไม่มี event_id มันจะเริ่ม battle transition ที่โหลดไม่สำเร็จ
// → battle camera ไม่มี target ให้ตาม → กล้องหลุดจากตัวละคร
// พอ client ได้_info กับ "monster" ที่จริงคือ prop/NPC/ตัวผู้เล่นเอง
// หรือ monster ที่ไม่มี event_id มันจะเริ่ม transition ที่โหลดไม่สำเร็จ
// → camera ไม่มี target ให้ตาม → กล้องหลุดจากตัวละคร
//
// FIX: ยอมรับเฉพาะ entity ที่ server spawn เป็น "monster จริง" ในฉากปัจจุบัน
// (เก็บไว้ใน Session.MonsterEntities ตอน load_scene) ถ้าไม่มี monster จริง
// ถูกโจมตี → ตอบ retcode 0 + cast_entity_id เฉยๆ ไม่เข้า battle
// ถูกโจมตี → ตอบ retcode 0 + cast_entity_id เฉยๆ ไม่เข้า
// client จะเล่นแอนิเมชันตีปกติ กล้องค้างกับตัวละคร
// ================================================================
handle("SceneCastSkill", func(s *Session, req *goproto.SceneCastSkillCsReq, rsp *goproto.SceneCastSkillScRsp) error {
rsp.CastEntityId = req.CastEntityId
// เฉพาะ monster entity จริงเท่านั้นที่ trigger battle
// เฉพาะ monster entity จริงเท่านั้นที่ trigger
var targets []uint32
seen := map[uint32]bool{}
addTarget := func(id uint32) {
@@ -149,14 +140,14 @@ func init() {
}
if len(targets) == 0 {
// ไม่มี monster จริงถูกตี — ไม่เข้า battle (นี่คือ fix หลัก)
// ไม่มี monster จริงถูกตี — ไม่เข้า (นี่คือ fix หลัก)
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d skill_index=%d -> no real monster target (battle skipped)",
req.CastEntityId, req.CasterId, req.SkillIndex)
rsp.Retcode = 0
return nil
}
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d -> battle with %d monster(s)",
SceneDebugLog("SCENE_CAST_SKILL cast_entity=%d caster=%d -> with %d monster(s)",
req.CastEntityId, req.CasterId, len(targets))
battleInfo := createBattleInfo(s, req.CasterId, req.SkillIndex)
@@ -179,39 +170,19 @@ func init() {
})
handle("GetCurBattleInfo", func(s *Session, req *goproto.GetCurBattleInfoCsReq, rsp *goproto.GetCurBattleInfoScRsp) error {
// Login queries this even when no battle is active. An absent battle
// is a successful empty result, not RET_BATTLE_STAGE_NOT_MATCH.
rsp.Retcode = 0
if info, ok := CURRENT_BATTLE.Load(s.Token); ok {
rsp.Retcode = 0
rsp.BattleInfo = info
return nil
}
rsp.Retcode = 2000 // RET_BATTLE_STAGE_NOT_MATCH
return nil
})
}
func normalizeCalyxBattleInfo(battleInfo *goproto.SceneBattleInfo, reqWave uint32, worldLevel uint32) {
if worldLevel > 0 {
battleInfo.WorldLevel = worldLevel
}
if reqWave > 0 && int(reqWave) <= len(battleInfo.MonsterWaveList) {
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:reqWave]
} else if len(battleInfo.MonsterWaveList) > 1 {
battleInfo.MonsterWaveList = battleInfo.MonsterWaveList[:1]
}
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
for _, w := range battleInfo.MonsterWaveList {
// NOTE: In client 4.6.53 GameAssembly.dll 0x166b2fdd, client verifies:
// (wave.WaveId & ~8) == 1. If wave_id != 1, it jumps to 0x166b3161
// and throws NullReferenceException in AdventureModule.CreateBattleServerCheckResult.
// Therefore every wave in Calyx / FarmElement MUST have WaveId = 1 and WaveParam = nil.
w.WaveId = 1
w.WaveParam = nil
w.BattleStageId = battleInfo.StageId
}
}
// createBattleInfo is the Go port of battle.rs create_battle_info.
// createBattleInfo is the Go port of.rs create_battle_info.
func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBattleInfo {
player := s.JSON()
if player == nil {
@@ -431,9 +402,6 @@ func createBattleInfo(s *Session, casterID, skillIndex uint32) *goproto.SceneBat
// monsters
battleInfo.MonsterWaveList = common.ToSceneMonsterWaves(player.BattleConfig.Monsters)
battleInfo.JAOIOKBBMFC = uint32(len(battleInfo.MonsterWaveList))
for _, wave := range battleInfo.MonsterWaveList {
wave.BattleStageId = battleInfo.StageId
}
// global buffs
hasCastorice, hasSilverWolf := false, false
+4 -12
View File
@@ -274,12 +274,8 @@ func init() {
})
handle("GetCurChallengePeak", func(s *Session, req *goproto.GetCurChallengePeakCsReq, rsp *goproto.GetCurChallengePeakScRsp) error {
// reverted to the state of the build that reached Pom-Pom:
// empty peak_id (a fabricated peak_id:1 crashed CreateMemberEntityLeader)
// Match upstream's empty current-state response until a Peak is active.
rsp.Retcode = 0
rsp.HasPassed = true
rsp.PeakId = 1
rsp.BossBuffId = 0
return nil
})
@@ -293,13 +289,9 @@ func init() {
EndTime: 1924992000,
})
}
// NOTE: the real 4.6.51 activity schedule (realActivitySchedule, from
// the March7th dump) BLACK-SCREENS the events page: the client renders
// real event panels, then requests each activity's data (GetActivity-
// HotData/GetActiveActivityData/...) which we cannot fill yet - NRE.
// The synthetic list below resolves to nothing and keeps the page open
// (empty but usable). Re-enable the real list only after implementing
// per-activity data handlers.
// Advertise only the supported Endgame schedules. Exposing the full
// ActivityConfig before implementing its activity-data handlers makes
// the Client request unavailable state and can stall the events page.
// Apocalyptic Shadow tab requires an active schedule with panel 21001
for actID := uint32(21001); actID <= 21030; actID++ {
+101 -555
View File
@@ -1,6 +1,7 @@
package gameserver
import (
"fmt"
"sort"
"google.golang.org/protobuf/proto"
@@ -9,8 +10,8 @@ import (
goproto "gac-hkrpg-go/protocol/proto"
)
// Full Go port of challenge.rs: StartChallenge / Tierce / Peak flows with the
// hand-built 4.6.51 response layouts (cmd 1787 / 8973).
// Challenge flows use the generated request and response layouts. Command
// numbers are resolved through the current protocol table at registration/send.
type decodedChallengeStart struct {
challengeID uint32
@@ -106,433 +107,89 @@ func mapChallengeStageID(id uint32) uint32 {
return id
}
func readLenDelimAndAdvance(buf *[]byte) ([]byte, bool) {
b := *buf
l, n := readUvarint(b)
if n == 0 || int(l) > len(b[n:]) {
*buf = nil
return nil, false
}
sub := b[n : n+int(l)]
*buf = b[n+int(l):]
return sub, true
}
func readLenDelim(buf []byte) ([]byte, bool) {
l, n := readUvarint(buf)
if n == 0 || int(l) > len(buf[n:]) {
return nil, false
}
return buf[n : n+int(l)], true
}
func decodeStartChallengeReq(payload []byte) decodedChallengeStart {
var req decodedChallengeStart
var pbReq goproto.StartChallengeCsReq
if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 {
req.challengeID = pbReq.ChallengeId
req.firstAvatars = pbReq.FirstLineup
if len(req.firstAvatars) == 0 && len(pbReq.AvatarLineupFirst) > 0 {
for _, av := range pbReq.AvatarLineupFirst {
if av.Id != 0 {
req.firstAvatars = append(req.firstAvatars, av.Id)
}
}
}
req.secondAvatars = pbReq.SecondLineup
if len(req.secondAvatars) == 0 && len(pbReq.AvatarLineupSecond) > 0 {
for _, av := range pbReq.AvatarLineupSecond {
if av.Id != 0 {
req.secondAvatars = append(req.secondAvatars, av.Id)
}
}
}
if pbReq.StageInfo != nil {
if pbReq.StageInfo.BossInfo != nil && pbReq.StageInfo.BossInfo.BuffOne != 0 {
req.buffID = pbReq.StageInfo.BossInfo.BuffOne
} else if pbReq.StageInfo.StoryInfo != nil && pbReq.StageInfo.StoryInfo.BuffOne != 0 {
req.buffID = pbReq.StageInfo.StoryInfo.BuffOne
}
}
return req
if err := proto.Unmarshal(payload, &pbReq); err != nil {
return decodedChallengeStart{}
}
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)
switch {
case (fieldNumber == 14 || fieldNumber == 12) && wireType == 0:
v, n2 := readUvarint(buf)
if n2 == 0 {
buf = nil
break
req := decodedChallengeStart{challengeID: pbReq.ChallengeId,
firstAvatars: pbReq.FirstLineup, secondAvatars: pbReq.SecondLineup}
if len(req.firstAvatars) == 0 {
for _, avatar := range pbReq.AvatarLineupFirst {
if avatar.Id != 0 {
req.firstAvatars = append(req.firstAvatars, avatar.Id)
}
buf = buf[n2:]
if req.challengeID == 0 {
req.challengeID = uint32(v)
}
case fieldNumber == 6 && wireType == 0:
v, n2 := readUvarint(buf)
if n2 == 0 {
buf = nil
break
}
buf = buf[n2:]
req.firstAvatars = append(req.firstAvatars, uint32(v))
case fieldNumber == 6 && wireType == 2:
sub, ok := readLenDelimAndAdvance(&buf)
if !ok {
break
}
for len(sub) > 0 {
v, n3 := readUvarint(sub)
if n3 == 0 {
break
}
sub = sub[n3:]
req.firstAvatars = append(req.firstAvatars, uint32(v))
}
case fieldNumber == 8 && wireType == 0:
v, n2 := readUvarint(buf)
if n2 == 0 {
buf = nil
break
}
buf = buf[n2:]
req.secondAvatars = append(req.secondAvatars, uint32(v))
case fieldNumber == 8 && wireType == 2:
sub, ok := readLenDelimAndAdvance(&buf)
if !ok {
break
}
for len(sub) > 0 {
v, n3 := readUvarint(sub)
if n3 == 0 {
break
}
sub = sub[n3:]
req.secondAvatars = append(req.secondAvatars, uint32(v))
}
case (fieldNumber == 4 || fieldNumber == 15) && wireType == 2:
sub, ok := readLenDelimAndAdvance(&buf)
if !ok {
break
}
for len(sub) > 0 {
stag, n3 := readUvarint(sub)
if n3 == 0 {
break
}
sfn := uint32(stag >> 3)
swt := uint32(stag & 0x7)
if sfn == 1 && swt == 0 {
v, n4 := readUvarint(sub[n3:])
if n4 == 0 {
break
}
sub = sub[n3+n4:]
if fieldNumber == 4 {
req.firstAvatars = append(req.firstAvatars, uint32(v))
} else {
req.secondAvatars = append(req.secondAvatars, uint32(v))
}
} else {
skipWireField(swt, &sub)
}
}
case fieldNumber == 10 && wireType == 2:
sub, ok := readLenDelimAndAdvance(&buf)
if !ok {
break
}
for len(sub) > 0 {
stag, n3 := readUvarint(sub)
if n3 == 0 {
break
}
sfn := uint32(stag >> 3)
swt := uint32(stag & 0x7)
sub = sub[n3:]
if swt == 2 && (sfn == 9 || sfn == 10) {
inner, ok2 := readLenDelimAndAdvance(&sub)
if !ok2 {
break
}
for len(inner) > 0 {
t2, n4 := readUvarint(inner)
if n4 == 0 {
break
}
f2 := uint32(t2 >> 3)
w2 := uint32(t2 & 0x7)
inner = inner[n4:]
if w2 == 0 && (f2 == 10 || f2 == 15) {
v, n5 := readUvarint(inner)
if n5 == 0 {
break
}
inner = inner[n5:]
if req.buffID == 0 {
req.buffID = uint32(v)
}
} else {
skipWireField(w2, &inner)
}
}
} else {
skipWireField(swt, &sub)
}
}
default:
skipWireField(wireType, &buf)
}
}
if len(req.secondAvatars) == 0 {
for _, avatar := range pbReq.AvatarLineupSecond {
if avatar.Id != 0 {
req.secondAvatars = append(req.secondAvatars, avatar.Id)
}
}
}
if pbReq.StageInfo != nil {
if info := pbReq.StageInfo.BossInfo; info != nil {
req.buffID = info.BuffOne
}
if info := pbReq.StageInfo.StoryInfo; info != nil && req.buffID == 0 {
req.buffID = info.BuffOne
}
}
return req
}
type tierceStageEntry struct {
buff uint32
lineup []uint32
}
func decodeStartChallengeTierceReq(payload []byte) decodedChallengeStart {
var req decodedChallengeStart
var stagesAvatars []tierceStageEntry
var pbReq goproto.StartChallengeTierceCsReq
if err := proto.Unmarshal(payload, &pbReq); err == nil && pbReq.ChallengeId != 0 {
req.challengeID = pbReq.ChallengeId
req.stageIndex = pbReq.FINOCEGDBDI
req.isSingleStage = pbReq.MPKMIKBFEHG
for _, lt := range pbReq.LineupTierceList {
var l []uint32
for _, av := range lt.TierceAvatarIdList {
if av.Id != 0 {
l = append(l, av.Id)
}
}
stagesAvatars = append(stagesAvatars, tierceStageEntry{
buff: lt.BuffId,
lineup: l,
})
}
} else {
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)
switch {
case (fieldNumber == 10 || fieldNumber == 1 || fieldNumber == 5) && wireType == 0:
v, n2 := readUvarint(buf)
if n2 == 0 {
buf = nil
break
}
buf = buf[n2:]
req.stageIndex = uint32(v)
case (fieldNumber == 15 || fieldNumber == 3) && wireType == 0:
v, n2 := readUvarint(buf)
if n2 == 0 {
buf = nil
break
}
buf = buf[n2:]
req.challengeID = uint32(v)
case (fieldNumber == 2 || fieldNumber == 13) && wireType == 0:
v, n2 := readUvarint(buf)
if n2 == 0 {
buf = nil
break
}
buf = buf[n2:]
req.isSingleStage = v != 0
case (fieldNumber == 9 || fieldNumber == 12 || fieldNumber == 14) && wireType == 2:
sub, ok := readLenDelimAndAdvance(&buf)
if !ok {
break
}
var stageBuff uint32
var stageLineup []uint32
for len(sub) > 0 {
subTag, n2 := readUvarint(sub)
if n2 == 0 {
break
}
subFn := uint32(subTag >> 3)
subWt := uint32(subTag & 0x7)
sub = sub[n2:]
switch {
case (subFn == 10 || subFn == 6 || subFn == 5) && subWt == 0:
v, n3 := readUvarint(sub)
if n3 == 0 {
sub = nil
break
}
sub = sub[n3:]
stageBuff = uint32(v)
case (subFn == 9 || subFn == 4) && subWt == 2:
abuf, ok2 := readLenDelimAndAdvance(&sub)
if !ok2 {
break
}
var aid uint32
for len(abuf) > 0 {
atag, n4 := readUvarint(abuf)
if n4 == 0 {
break
}
afn := uint32(atag >> 3)
awt := uint32(atag & 0x7)
abuf = abuf[n4:]
if (afn == 1 || afn == 7 || afn == 9) && awt == 0 {
v, n5 := readUvarint(abuf)
if n5 == 0 {
break
}
abuf = abuf[n5:]
aid = uint32(v)
} else {
skipWireField(awt, &abuf)
}
}
if aid != 0 {
stageLineup = append(stageLineup, aid)
}
default:
skipWireField(subWt, &sub)
}
}
stagesAvatars = append(stagesAvatars, tierceStageEntry{buff: stageBuff, lineup: stageLineup})
default:
skipWireField(wireType, &buf)
if err := proto.Unmarshal(payload, &pbReq); err != nil {
return decodedChallengeStart{}
}
req := decodedChallengeStart{challengeID: pbReq.ChallengeId,
stageIndex: pbReq.FINOCEGDBDI, isSingleStage: pbReq.MPKMIKBFEHG}
for index, stage := range pbReq.LineupTierceList {
var avatars []uint32
for _, avatar := range stage.TierceAvatarIdList {
if avatar.Id != 0 {
avatars = append(avatars, avatar.Id)
}
}
}
for _, st := range stagesAvatars {
req.stageInfoList = append(req.stageInfoList, st.lineup)
}
if len(stagesAvatars) > 0 {
req.firstAvatars = stagesAvatars[0].lineup
if req.stageIndex == 0 && stagesAvatars[0].buff != 0 {
req.buffID = stagesAvatars[0].buff
req.stageInfoList = append(req.stageInfoList, avatars)
if uint32(index) == req.stageIndex {
req.buffID = stage.BuffId
}
}
if len(stagesAvatars) > 1 {
req.secondAvatars = stagesAvatars[1].lineup
if req.stageIndex == 1 && stagesAvatars[1].buff != 0 {
req.buffID = stagesAvatars[1].buff
}
if len(req.stageInfoList) > 0 {
req.firstAvatars = req.stageInfoList[0]
}
if len(stagesAvatars) > 2 {
req.thirdAvatars = stagesAvatars[2].lineup
if req.stageIndex == 2 && stagesAvatars[2].buff != 0 {
req.buffID = stagesAvatars[2].buff
}
if len(req.stageInfoList) > 1 {
req.secondAvatars = req.stageInfoList[1]
}
if len(req.stageInfoList) > 2 {
req.thirdAvatars = req.stageInfoList[2]
}
return req
}
func decodeSetChallengeTierceLineupReq(payload []byte) (uint32, [][]uint32) {
var challengeID uint32
var stages [][]uint32
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)
switch {
case fieldNumber == 7 && wireType == 0:
v, n2 := readUvarint(buf)
if n2 == 0 {
buf = nil
break
}
buf = buf[n2:]
challengeID = uint32(v)
case fieldNumber == 12 && wireType == 2:
sub, ok := readLenDelim(buf)
if !ok {
buf = nil
break
}
buf = buf[len(sub):]
var stageLineup []uint32
for len(sub) > 0 {
subTag, n2 := readUvarint(sub)
if n2 == 0 {
break
}
subFn := uint32(subTag >> 3)
subWt := uint32(subTag & 0x7)
if subFn == 9 && subWt == 2 {
abuf, ok2 := readLenDelim(sub[n2:])
if !ok2 {
sub = nil
break
}
sub = sub[n2+len(abuf):]
var aid uint32
for len(abuf) > 0 {
atag, n4 := readUvarint(abuf)
if n4 == 0 {
break
}
afn := uint32(atag >> 3)
awt := uint32(atag & 0x7)
if afn == 9 && awt == 0 {
v, n5 := readUvarint(abuf[n4:])
if n5 == 0 {
break
}
abuf = abuf[n4+n5:]
aid = uint32(v)
} else {
skipWireField(awt, &abuf)
}
}
if aid != 0 {
stageLineup = append(stageLineup, aid)
}
} else {
skipWireField(subWt, &sub)
}
}
stages = append(stages, stageLineup)
default:
skipWireField(wireType, &buf)
}
var req goproto.SetChallengeTierceLineupCsReq
if err := proto.Unmarshal(payload, &req); err != nil {
return 0, nil
}
return challengeID, stages
var stages [][]uint32
for _, stage := range req.StageInfoList {
var avatars []uint32
for _, avatar := range stage.TierceAvatarIdList {
if avatar.Id != 0 {
avatars = append(avatars, avatar.Id)
}
}
stages = append(stages, avatars)
}
return req.ChallengeId, stages
}
// ---------------------------------------------------------------------------
// battle config setup shared by start handlers
// config setup shared by start handlers
// ---------------------------------------------------------------------------
func setChallengeBattleConfig(player *common.FreesrData, avatars []uint32, challengeID uint32, eventID uint32, buff uint32) {
@@ -618,63 +275,12 @@ func dedupeTruncate4(in []uint32) []uint32 {
return out
}
// looksLikeChallengeID: known challenge-id ranges across endgame modes.
func looksLikeChallengeID(v uint64) bool {
switch {
case v >= 101 && v <= 119, v == 900,
v >= 1001 && v <= 1035,
v >= 2001 && v <= 2025,
v >= 3001 && v <= 3020,
v >= 5000 && v <= 5700, // MoC floors incl. 4.6 phases
v >= 20000 && v <= 20300, // Pure Fiction
v >= 30000 && v <= 30300: // Apocalyptic Shadow
return true
}
cd := getChallengeData()
_, okC := cd.Challenges[uint32(v)]
_, okT := cd.Tierce[uint32(v)]
return okC || okT
}
// detectChallengeID: when the strict decoder misses the challenge id (field
// layout drift between client builds), scan top-level varint fields for the
// first value that looks like a challenge id.
func detectChallengeID(payload []byte) (uint32, int) {
buf := payload
for len(buf) > 0 {
tag, n := readUvarint(buf)
if n == 0 {
break
}
buf = buf[n:]
wireType := uint32(tag & 0x7)
if wireType == 0 {
v, n2 := readUvarint(buf)
if n2 == 0 {
break
}
buf = buf[n2:]
if looksLikeChallengeID(v) {
return uint32(v), int(tag >> 3)
}
} else {
skipWireField(wireType, &buf)
}
}
return 0, 0
}
// handleStartChallenge ports handle_start_challenge: respond cmd 1787 with
// retcode(3) + cur_challenge(5) + lineup_list(6) + scene(10) + stage_info(13).
func handleStartChallenge(s *Session, payload []byte) error {
req := decodeStartChallengeReq(payload)
if req.challengeID == 0 {
if id, field := detectChallengeID(payload); id != 0 {
Log("handle_start_challenge: strict decode missed, detected challenge_id=" + itoa(int(id)) + " at field " + itoa(field))
req.challengeID = id
} else {
PacketLog("[CHALLENGE] challenge_id=0 payload=" + hexDump(payload, 128))
}
return s.Send(&goproto.StartChallengeScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
}
Log("handle_start_challenge: challenge_id=" + itoa(int(req.challengeID)))
@@ -742,12 +348,7 @@ func handleStartChallenge(s *Session, payload []byte) error {
LineupList: lineups,
Scene: sceneInfo,
}
rspBuf, err := proto.Marshal(rsp)
if err != nil {
return err
}
return s.SendRaw(&NetPacket{CmdType: 1787, Body: rspBuf})
return s.Send(rsp)
}
func scoreIDFor(challengeID uint32) uint32 {
@@ -757,9 +358,12 @@ func scoreIDFor(challengeID uint32) uint32 {
return 0
}
// handleStartChallengeTierce ports handle_start_challenge_tierce -> cmd 8998 (4.6.51).
// handleStartChallengeTierce uses the current Tierce request schema.
func handleStartChallengeTierce(s *Session, payload []byte) error {
req := decodeStartChallengeTierceReq(payload)
if req.challengeID == 0 {
return s.Send(&goproto.StartChallengeTierceScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
}
baseID := mapChallengeStageID(req.challengeID)
Log("handle_start_challenge_tierce: challenge_id=" + itoa(int(req.challengeID)) + " (mapped " + itoa(int(baseID)) + ")")
@@ -910,8 +514,8 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
tierceLineupList = append(tierceLineupList, lineupInfoFromAvatars(stageAvatars, sceneInfo.PlaneId, goproto.ExtraLineupType(1), uint32(stIdx)))
}
// 4.6.51 StartChallengeTierceScRsp (CmdID 8998)
rsp46 := &goproto.StartChallengeTierceScRsp{
// Generated response resolves its command ID through Session.Send.
rsp := &goproto.StartChallengeTierceScRsp{
Retcode: 0,
Scene: sceneInfo,
CurTierceChallenge: &goproto.CurTierceChallenge{
@@ -921,110 +525,61 @@ func handleStartChallengeTierce(s *Session, payload []byte) error {
MPKMIKBFEHG: req.isSingleStage,
},
}
rspBuf, err := proto.Marshal(rsp46)
if err != nil {
return err
}
return s.SendRaw(&NetPacket{CmdType: 8998, Body: rspBuf})
return s.Send(rsp)
}
func handleSetChallengeTierceLineup(s *Session, payload []byte) error {
challengeID, stages := decodeSetChallengeTierceLineupReq(payload)
if challengeID == 0 {
return s.Send(&goproto.SetChallengeTierceLineupScRsp{Retcode: uint32(goproto.Retcode_RET_CHALLENGE_NOT_EXIST)})
}
if len(stages) > 0 {
tierceLineupsMu.Lock()
tierceLineups[challengeID] = stages
tierceLineupsMu.Unlock()
}
// 4.6.51 SetChallengeTierceLineupScRsp (cmd 8985, retcode tag 1 = 0)
if err := s.SendRaw(&NetPacket{CmdType: 8985, Body: []byte{0x08, 0x00}}); err != nil {
return err
}
_ = s.SendRaw(&NetPacket{CmdType: 8999, Body: []byte{0x08, 0x00}})
_ = s.SendRaw(&NetPacket{CmdType: 8995, Body: []byte{0x60, 0x00}})
return nil
return s.Send(&goproto.SetChallengeTierceLineupScRsp{})
}
func handleLeaveChallenge(s *Session, _ []byte) error {
var returnEntry uint32 = 102020107
if player := s.JSON(); player != nil {
player.BattleConfig.CustomBattleLineup = nil
// Always return safely to Belobog Admin District 102020107
player.Scene.EntryID = 102020107
player.Scene.PlaneID = 10202
player.Scene.FloorID = 10202001
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
player.SavePersistent()
}
// 4.6.51 LeaveChallengeScRsp (cmd 1771, tag 10 retcode 0)
if err := s.SendRaw(&NetPacket{CmdType: 1771, Body: []byte{0x50, 0x00}}); err != nil {
return err
}
_ = s.SendRaw(&NetPacket{CmdType: 1716, Body: []byte{0x20, 0x00}})
_ = s.SendRaw(&NetPacket{CmdType: 1781, Body: []byte{0x40, 0x00}})
// Restore the player back to the open world
_, _ = loadScene(s, returnEntry, true, nil, nil)
_ = refreshLineup(s)
return nil
return leaveChallengeScene(s, &goproto.LeaveChallengeScRsp{})
}
func handleLeaveChallengeTierce(s *Session, _ []byte) error {
var returnEntry uint32 = 102020107
if player := s.JSON(); player != nil {
player.BattleConfig.CustomBattleLineup = nil
// Always return safely to Belobog Admin District 102020107
player.Scene.EntryID = 102020107
player.Scene.PlaneID = 10202
player.Scene.FloorID = 10202001
player.Position = common.Position{X: -92, Y: 4278, Z: 68083, RotY: 155566}
player.SavePersistent()
return leaveChallengeScene(s, &goproto.LeaveChallengeTierceScRsp{})
}
func leaveChallengeScene(s *Session, response proto.Message) error {
player := s.JSON()
if player == nil || player.Scene.EntryID == 0 {
return fmt.Errorf("challenge return scene is unavailable")
}
// 4.6.51 LeaveChallengeTierceScRsp (cmd 8983, tag 13 retcode 0)
if err := s.SendRaw(&NetPacket{CmdType: 8983, Body: []byte{0x68, 0x00}}); err != nil {
// loadChallengeScene preserves the player's saved overworld scene.
entryID := player.Scene.EntryID
player.BattleConfig.CustomBattleLineup = nil
CURRENT_BATTLE.Delete(s.Token)
if err := s.Send(&goproto.QuitBattleScNotify{}); err != nil {
return err
}
_ = s.SendRaw(&NetPacket{CmdType: 8997, Body: []byte{0x38, 0x00}})
_ = s.SendRaw(&NetPacket{CmdType: 8982, Body: []byte{0x68, 0x00}})
// Restore the player back to the open world
_, _ = loadScene(s, returnEntry, true, nil, nil)
_ = refreshLineup(s)
return nil
if _, err := loadScene(s, entryID, true, true, nil, nil); err != nil {
return err
}
if err := refreshLineup(s); err != nil {
return err
}
return s.Send(response)
}
func handleTakeChallengeReward(s *Session, payload []byte) error {
var groupID uint32
buf := payload
for len(buf) > 0 {
tag, n := readUvarint(buf)
if n == 0 {
break
}
buf = buf[n:]
fnNum := uint32(tag >> 3)
wt := uint32(tag & 0x7)
if fnNum == 4 && wt == 0 {
v, n2 := readUvarint(buf)
if n2 == 0 {
break
}
buf = buf[n2:]
groupID = uint32(v)
break
}
skipWireField(wt, &buf)
var req goproto.TakeChallengeRewardCsReq
if err := proto.Unmarshal(payload, &req); err != nil {
return err
}
var body []byte
body = append(body, 0x10) // tag 2: group_id
body = appendUvarint(body, uint64(groupID))
body = append(body, 0x30, 0x00) // tag 6: retcode = 0
return s.SendRaw(&NetPacket{CmdType: 1704, Body: body})
return s.Send(&goproto.TakeChallengeRewardScRsp{GroupId: req.GroupId})
}
// buildGetChallengeTierceDataScRsp — verbatim port of the Rust builder.
func buildGetChallengeTierceDataScRspReal() []byte {
// buildGetChallengeTierceDataScRspReal builds the generated current response.
func buildGetChallengeTierceDataScRspReal() *goproto.GetChallengeTierceDataScRsp {
cd := getChallengeData()
type tierceEntry struct {
@@ -1066,17 +621,8 @@ func buildGetChallengeTierceDataScRspReal() []byte {
})
}
rspMsg := &goproto.GetChallengeTierceDataScRsp{
return &goproto.GetChallengeTierceDataScRsp{
ChallengeTierceGroups: groups,
Retcode: 0,
}
body, err := proto.Marshal(rspMsg)
if err == nil {
return body
}
var rsp []byte
rsp = append(rsp, 0x40, 0x00) // tag 8: retcode = 0
rsp = append(rsp, 0x60, 0x00) // legacy tag 12: retcode = 0
return rsp
}
+1 -1
View File
@@ -161,7 +161,7 @@ func init() {
}
rsp.RecommendType = &goproto.GetBigDataAllRecommendScRsp_RelicAvatar{RelicAvatar: inner}
case goproto.BigDataRecommendType_BIG_DATA_RECOMMEND_TYPE_AVATAR_RELIC:
// 4.6.51 proto: recomended_relic_info_list is repeated GDFKJKDLEPM
// Loaded schema: recomended_relic_info_list is repeated GDFKJKDLEPM
// (obfuscated, contents unused by this PS) — reply with empty entries
inner := &goproto.BigDataRecommendAvatarRelic{}
for range BaseAvatarIDs {
+6 -2
View File
@@ -11,7 +11,7 @@ import (
func init() {
// The three optional PlayerSettingInfo sub-messages are required by the
// 4.6.51 client: leaving them unset NREs inside _CmdGetBasicInfoScRsp.
// Client requires these when processing _CmdGetBasicInfoScRsp.
handle("GetBasicInfo", func(s *Session, req *goproto.GetBasicInfoCsReq, rsp *goproto.GetBasicInfoScRsp) error {
rsp.Retcode = 0
rsp.Gender = 2
@@ -40,6 +40,10 @@ func init() {
var finalData []byte
finalData = append(finalData, heartbeatLua...)
if probe := s.takeBattleProbe(); len(probe) > 0 {
finalData = append(finalData, '\n')
finalData = append(finalData, probe...)
}
// Load all .lua files from scripts/ directory in deterministic order
if entries, err := os.ReadDir("scripts"); err == nil {
@@ -62,7 +66,7 @@ func init() {
}
rsp.DownloadData = &goproto.ClientDownloadData{
Version: 51,
Version: goproto.ClientDownloadDataLuaVersion,
Time: int64(rsp.ServerTimeMs),
Data: finalData,
}
+35 -162
View File
@@ -39,94 +39,43 @@ func init() {
dummy(name)
}
// ---- fixed-body raw replies (cmd -> rsp cmd + body bytes) ----
simpleReply := func(reqIDs []uint16, rspID uint16, body []byte) {
for _, id := range reqIDs {
id := id
rawHandlers[id] = func(s *Session, payload []byte) error {
return s.SendRaw(&NetPacket{CmdType: rspID, Body: body})
}
}
}
// Bind requests by the current command names; do not retain numeric aliases
// from earlier builds, which may now identify responses or notifications.
rawNamed("GetChallengeGroupStatisticsCsReq", challengeGroupStatsReply)
rawNamed("GetChallengeTierceDataCsReq", challengeTierceReply)
rawNamed("StartChallengeCsReq", handleStartChallenge)
rawNamed("StartChallengeTierceCsReq", handleStartChallengeTierce)
rawNamed("SetChallengeTierceLineupCsReq", handleSetChallengeTierceLineup)
rawNamed("LeaveChallengeCsReq", handleLeaveChallenge)
rawNamed("LeaveChallengeTierceCsReq", handleLeaveChallengeTierce)
rawNamed("TakeChallengeCumulativeRewardCsReq", handleTakeChallengeReward)
// The request/response names remain obfuscated in the upstream schema.
rawNamed("HIGLJHLNODG", handleExRankUnlock)
simpleReply([]uint16{8909}, 8923, []byte{0x60, 0x00}) // tag 12 retcode 0
simpleReply([]uint16{8924, 8968}, 8920, []byte{0x10, 0x00}) // tag 2 retcode 0
simpleReply([]uint16{8935}, 8915, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{8917}, 8929, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{8933}, 8906, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{8919}, 8948, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{8910}, 8921, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{8975}, 8992, []byte{0x40, 0x00}) // tag 8 retcode 0
simpleReply([]uint16{8990}, 8977, []byte{0x40, 0x00}) // tag 8 retcode 0
simpleReply([]uint16{8977}, 8999, []byte{0x50, 0x00}) // tag 10 retcode 0
simpleReply([]uint16{2966}, 2955, []byte{0x30, 0x00}) // tag 6 retcode 0
simpleReply([]uint16{2916}, 2906, []byte{0x18, 0x00}) // tag 3 retcode 0
simpleReply([]uint16{2919}, 2928, []byte{0x48, 0x00}) // tag 9 retcode 0
simpleReply([]uint16{2974}, 2919, []byte{0x18, 0x00}) // GetFriendListInfo tag 3 retcode 0
simpleReply([]uint16{740}, 733, []byte{0x58, 0x00}) // tag 11 retcode 0
simpleReply([]uint16{1748}, 1745, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{1765}, 1730, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{1742}, 1772, []byte{0x08, 0x00}) // tag 1 retcode 0
simpleReply([]uint16{1738}, 1748, nil) // empty body
simpleReply([]uint16{188}, 181, []byte{0x50, 0x00}) // tag 10 retcode 0
simpleReply([]uint16{4130}, 4124, []byte{0x70, 0x00}) // tag 14 retcode 0
simpleReply([]uint16{8110, 8115}, 8108, []byte{0x18, 0x00}) // GetSwitchHandData tag 3 retcode 0 (4.6.51 CmdID: 8108)
simpleReply([]uint16{8993, 8991}, 8981, []byte{0x70, 0x00}) // 4.6.51: tag 14 retcode 0
simpleReply([]uint16{8987}, 8986, []byte{0x38, 0x00}) // ConfirmChallengeTierceStageSettle tag 7 retcode 0
rawNamed("GetSwitchHandDataCsReq", func(s *Session, payload []byte) error {
return s.Send(&goproto.GetSwitchHandDataScRsp{})
})
// ---- 1775 | 1711 -> GetChallengeGroupStatisticsScRsp (cmd 1708) ----
rawHandlers[1775] = challengeGroupStatsReply
rawHandlers[1711] = challengeGroupStatsReply
handle("GetChallengeTierceController", func(s *Session, req *goproto.GetChallengeTierceControllerCsReq, rsp *goproto.GetChallengeTierceControllerScRsp) error {
rsp.Retcode = 0
return nil
})
handle("StartChallengePeak", handleStartChallengePeak)
rawNamed("LeaveChallengePeakCsReq", func(s *Session, payload []byte) error {
return leaveChallengeScene(s, &goproto.LeaveChallengePeakScRsp{})
})
// ---- 8995 | 8979 | 8981 -> tierce data (cmd 8996 + legacy 8994) ----
rawHandlers[8995] = challengeTierceReply
rawHandlers[8979] = challengeTierceReply
rawHandlers[8981] = challengeTierceReply
// ---- challenge flow (StartChallenge family) ----
rawHandlers[1731] = handleStartChallenge
rawHandlers[1793] = handleStartChallenge
rawHandlers[8986] = handleStartChallengeTierce
rawHandlers[8988] = handleStartChallengeTierce
rawHandlers[8978] = handleSetChallengeTierceLineup
rawHandlers[9000] = handleSetChallengeTierceLineup
rawHandlers[1705] = handleLeaveChallenge
rawHandlers[1760] = handleLeaveChallenge
rawHandlers[1788] = handleLeaveChallenge
rawHandlers[8989] = handleLeaveChallengeTierce
rawHandlers[1739] = handleTakeChallengeReward
// ---- 400 -> Ex-Rank / Affinitive Eidolon Unlock ----
rawHandlers[400] = handleExRankUnlock
}
// handleExRankUnlock handles CmdID 400 (Affinitive Eidolon / Ex-Rank unlock request from client).
func handleExRankUnlock(s *Session, payload []byte) error {
Log(fmt.Sprintf("[EX-RANK] Received cmd 400 (len: %d) payload: %x", len(payload), payload))
var parsedVal uint32
buf := payload
for len(buf) > 0 {
tag, n := readUvarint(buf)
if n == 0 {
break
}
buf = buf[n:]
wireType := tag & 0x7
fieldNum := tag >> 3
if wireType == 0 {
v, n2 := readUvarint(buf)
if n2 > 0 {
buf = buf[n2:]
Log(fmt.Sprintf("[EX-RANK] field %d = %d", fieldNum, v))
if v >= 100000 || (v >= 1000 && v < 10000 && parsedVal == 0) {
parsedVal = uint32(v)
}
continue
}
}
skipWireField(uint32(wireType), &buf)
var req goproto.HIGLJHLNODG
if err := proto.Unmarshal(payload, &req); err != nil {
return err
}
parsedVal := req.OJFIGABCKLG
var avatarID uint32
if parsedVal >= 100000 {
@@ -162,99 +111,23 @@ func handleExRankUnlock(s *Session, payload []byte) error {
player.SavePersistent()
}
// Send candidate response packets so client RPC callback resolves without spinning
// Candidates in CmdAvatarType: 395 (CmdAvatarType_ENKEIJOLCNO), 354, 355, 364, 341
for _, rspID := range []uint16{395, 354, 355, 364, 341} {
_ = s.SendRaw(&NetPacket{CmdType: rspID, Body: []byte{}})
}
return nil
// The matching response is declared by the current upstream schema.
return s.Send(&goproto.LPOLGDLAADM{})
}
// challengeGroupStatsReply handles CmdGetChallengeGroupStatisticsCsReq (1775/1711)
// and returns GetChallengeGroupStatisticsScRsp (1708) using standard 4.6.51 proto.
// challengeGroupStatsReply uses the current typed statistics response.
func challengeGroupStatsReply(s *Session, payload []byte) error {
groupID := uint32(100)
var req goproto.GetChallengeGroupStatisticsCsReq
if err := proto.Unmarshal(payload, &req); err == nil && req.GroupId != 0 {
groupID = req.GroupId
} else {
buf := payload
for len(buf) > 0 {
tag, n := readUvarint(buf)
if n == 0 {
break
}
buf = buf[n:]
fieldNumber := tag >> 3
wireType := tag & 0x7
if (fieldNumber == 5 || fieldNumber == 12) && wireType == 0 {
v, n2 := readUvarint(buf)
if n2 > 0 {
groupID = uint32(v)
break
}
}
skipWireField(uint32(wireType), &buf)
}
}
rsp := &goproto.GetChallengeGroupStatisticsScRsp{
GroupId: groupID,
Retcode: 0,
}
switch {
case groupID >= 3000:
// Apocalyptic Shadow: challenge_boss (field 1)
rsp.ChallengeBoss = &goproto.ChallengeBossStatistics{
RecordId: 1,
PPBHLLOJNEK: &goproto.AANLJBLOOFO{
Level: 4,
ScoreId: 8000,
},
}
case groupID >= 2000 && groupID < 3000:
// Pure Fiction: challenge_story (field 3)
rsp.ChallengeStory = &goproto.ChallengeStoryStatistics{
RecordId: 1,
PPBHLLOJNEK: &goproto.EIKPHEMHIOH{
ScoreId: 80000,
},
}
case groupID >= 1000 && groupID < 2000:
// Memory of Chaos: challenge_default (field 11)
rsp.ChallengeDefault = &goproto.ChallengeStatistics{
RecordId: 1,
PPBHLLOJNEK: &goproto.ADKJKMKBFDC{
RoundCount: 20,
Level: 12,
},
}
}
body, err := proto.Marshal(rsp)
if err != nil {
if err := proto.Unmarshal(payload, &req); err != nil {
return err
}
return s.SendRaw(&NetPacket{CmdType: 1708, Body: body})
// Match upstream: echo the group without fabricating historical scores.
return s.Send(&goproto.GetChallengeGroupStatisticsScRsp{GroupId: req.GroupId})
}
// challengeTierceReply ports the 8995/8979/8981 arm: cmd 8996 (4.6.51) plus
// legacy cmd 8994 with the same tierce body.
// challengeTierceReply sends only the response paired with the current request.
func challengeTierceReply(s *Session, payload []byte) error {
body := buildGetChallengeTierceDataScRsp()
if err := s.SendRaw(&NetPacket{CmdType: 8996, Body: body}); err != nil {
return err
}
_ = s.SendRaw(&NetPacket{CmdType: 8994, Body: body})
return nil
}
// buildGetChallengeTierceDataScRsp mirrors challenge::build_get_challenge_tierce_data_sc_rsp
// (hand-built protobuf, verified against the 4.6.51 client).
func buildGetChallengeTierceDataScRsp() []byte {
return buildGetChallengeTierceDataScRspReal()
return s.Send(buildGetChallengeTierceDataScRspReal())
}
func readUvarint(b []byte) (uint64, int) {
+99 -19
View File
@@ -52,6 +52,57 @@ func firstSceneOfEntry(res *common.JsonConfig, entryID uint32) (string, *common.
return "", nil, false
}
// sceneOfEntryForTeleport selects the floor config that actually owns the
// requested teleport. Some resource sets put several floors under one entry;
// choosing the lexicographically first config can then send a valid scene
// transition with the wrong floor and spawn position.
func sceneOfEntryForTeleport(res *common.JsonConfig, entryID, floorID, teleportID uint32) (string, *common.LevelOutputConfig, bool) {
configs := res.LevelOutputConfigs[entryID]
names := make([]string, 0, len(configs))
for name := range configs {
names = append(names, name)
}
sort.Strings(names)
if floorID != 0 {
want := fmt.Sprintf("_F%d", floorID)
for _, name := range names {
cfg := configs[name]
if len(name) > len(want) && name[len(name)-len(want):] == want && sceneHasTeleport(cfg, teleportID) {
return name, cfg, true
}
}
}
if teleportID != 0 {
for _, name := range names {
cfg := configs[name]
if sceneHasTeleport(cfg, teleportID) {
return name, cfg, true
}
}
}
if floorID != 0 {
want := fmt.Sprintf("_F%d", floorID)
for _, name := range names {
if len(name) > len(want) && name[len(name)-len(want):] == want {
return name, configs[name], true
}
}
}
return firstSceneOfEntry(res, entryID)
}
func sceneHasTeleport(cfg *common.LevelOutputConfig, teleportID uint32) bool {
if cfg == nil || teleportID == 0 {
return false
}
for _, group := range cfg.Scenes {
if _, ok := group.Teleports[teleportID]; ok {
return true
}
}
return false
}
// parsePlaneFloor splits "P<plane>_F<floor>".
func parsePlaneFloor(name string) (uint32, uint32, error) {
if len(name) < 2 || name[0] != 'P' {
@@ -108,7 +159,7 @@ func init() {
defaultScene.FloorId = player.Scene.FloorID
}
scene, err := loadScene(s, entryID, false, nil, nil)
scene, err := loadScene(s, entryID, false, false, nil, nil)
rsp.Retcode = 0
if err == nil {
rsp.Scene = scene
@@ -120,7 +171,7 @@ func init() {
handle("EnterScene", func(s *Session, req *goproto.EnterSceneCsReq, rsp *goproto.EnterSceneScRsp) error {
Log(fmt.Sprintf("on_enter_scene_cs_req: entry_id=%d, teleport_id=%d, interact_id=%d", req.EntryId, req.TeleportId, req.InteractId))
SceneDebugLog("ENTER_SCENE entry_id=%d teleport_id=%d interact_id=%d", req.EntryId, req.TeleportId, req.InteractId)
SceneDebugLog("ENTER_SCENE entry_id=%d teleport_id=%d interact_id=%d scene_identifier=%v", req.EntryId, req.TeleportId, req.InteractId, req.SceneIdentifier)
res, err := common.GameRes()
if err != nil {
@@ -222,7 +273,7 @@ func init() {
teleportID = &v
}
sceneInfo, err := loadScene(s, resolvedEntry, true, teleportID, sceneIdent)
sceneInfo, err := loadScene(s, resolvedEntry, true, false, teleportID, sceneIdent)
if err != nil {
SceneDebugLog("ENTER_SCENE_FAILED requested_entry=%d resolved_entry=%d err=%v", req.EntryId, resolvedEntry, err)
Log(fmt.Sprintf("Failed to enter scene %d: %v", resolvedEntry, err))
@@ -230,11 +281,21 @@ func init() {
return nil
}
SceneDebugLog("ENTER_SCENE_RESOLVED requested_entry=%d resolved_entry=%d", req.EntryId, resolvedEntry)
SceneDebugLog("ENTER_SCENE_RESOLVED requested_entry=%d resolved_entry=%d floor_id=%d plane_id=%d teleport_id=%d", req.EntryId, resolvedEntry, sceneInfo.FloorId, sceneInfo.PlaneId, teleportIDValue(teleportID))
rsp.Retcode = 0
rsp.SceneIdentifier = sceneInfo.SceneIdentifier
rsp.IsOverMap = true
rsp.IsCloseMap = req.IsCloseMap
return nil
if err := s.Send(rsp); err != nil {
return err
}
if err := s.Send(&goproto.EnterSceneByServerScNotify{
Scene: sceneInfo,
Lineup: common.ToLineupInfo(player.Lineups),
}); err != nil {
return err
}
return errResponseAlreadySent
})
handle("InteractProp", func(s *Session, req *goproto.InteractPropCsReq, rsp *goproto.InteractPropScRsp) error {
@@ -268,14 +329,14 @@ func init() {
return nil
})
// decodeSceneIdentifiersLenient: the 4.6.51 client's cmd-1461 payload can
// decodeSceneIdentifiersLenient: some Client scene-map payloads 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
// GetSceneMapInfo as a raw handler: some Client scene-map payloads
// 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 {
rawNamed("GetSceneMapInfoCsReq", func(s *Session, payload []byte) error {
res, err := common.GameRes()
if err != nil {
return err
@@ -352,7 +413,7 @@ func init() {
}
return s.Send(rsp)
}
})
handle("SceneEntityMove", func(s *Session, req *goproto.SceneEntityMoveCsReq, rsp *goproto.SceneEntityMoveScRsp) error {
player := s.JSON()
@@ -696,9 +757,9 @@ func sortedU32Keys2[V any](m map[uint32]V) []uint32 {
func sortedU32Keys[V any](m map[uint32]V) []uint32 { return sortedU32Keys2(m) }
// loadScene is the Go port of load_scene. When isEnterScene is true it also
// sends EnterSceneByServerScNotify and persists the new position.
func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32, reqSceneIdent *goproto.SceneIdentifier) (*goproto.SceneInfo, error) {
// loadScene is the Go port of load_scene. When isEnterScene is true it updates
// the player's scene; sendNotify controls whether it also emits the notify.
func loadScene(s *Session, entryID uint32, isEnterScene, sendNotify bool, teleportID *uint32, reqSceneIdent *goproto.SceneIdentifier) (*goproto.SceneInfo, error) {
player := s.JSON()
if player == nil {
return nil, fmt.Errorf("data is not set!")
@@ -708,7 +769,15 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
return nil, err
}
name, scene, ok := firstSceneOfEntry(res, entryID)
floorHint := uint32(0)
if reqSceneIdent != nil {
floorHint = reqSceneIdent.FloorId
}
requestedTeleport := uint32(0)
if teleportID != nil {
requestedTeleport = *teleportID
}
name, scene, ok := sceneOfEntryForTeleport(res, entryID, floorHint, requestedTeleport)
if !ok {
Log(fmt.Sprintf("Map Entrance Not Found %d", entryID))
return nil, fmt.Errorf("Map Entrance Not Found %d", entryID)
@@ -750,6 +819,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
}
}
}
SceneDebugLog("LOAD_SCENE_TELEPORT entry_id=%d config=%s floor_id=%d teleport_id=%d matched=%t pos=(%d,%d,%d)", entryID, name, floorID, *teleportID, found, jsonPos.X, jsonPos.Y, jsonPos.Z)
} else if isEnterScene || jsonPos.Y < -5000 {
// first teleport in the scene, else the teleport DB, else spawn near the first prop
var firstTP *common.TeleportInfo
@@ -861,7 +931,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
for _, groupID := range sortedU32Keys2(scene.Scenes) {
group := scene.Scenes[groupID]
groupInfo := &goproto.SceneEntityGroupInfo{GroupId: groupID}
groupInfo := &goproto.SceneEntityGroupInfo{State: 1, GroupId: groupID}
// props
for i := range group.Props {
@@ -924,6 +994,7 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
})
}
sceneInfo.EntityList = append(sceneInfo.EntityList, groupInfo.EntityList...)
if len(group.Props) == 0 && len(group.Npcs) == 0 && len(group.Monsters) == 0 {
continue
}
@@ -959,17 +1030,26 @@ func loadScene(s *Session, entryID uint32, isEnterScene bool, teleportID *uint32
player.Scene.PlaneID = planeID
player.Position = jsonPos
if err := s.Send(&goproto.EnterSceneByServerScNotify{
Scene: sceneInfo,
Lineup: common.ToLineupInfo(player.Lineups),
}); err != nil {
return nil, err
if sendNotify {
if err := s.Send(&goproto.EnterSceneByServerScNotify{
Scene: sceneInfo,
Lineup: common.ToLineupInfo(player.Lineups),
}); err != nil {
return nil, err
}
}
}
return sceneInfo, nil
}
func teleportIDValue(id *uint32) uint32 {
if id == nil {
return 0
}
return *id
}
func hasAvatar(player *common.FreesrData, id uint32) bool {
_, ok := player.Avatars[id]
return ok
+8
View File
@@ -8,6 +8,10 @@ import (
// tutorial modal (which locks camera + input).
func init() {
handle("GetTutorial", func(s *Session, req *goproto.GetTutorialCsReq, rsp *goproto.GetTutorialScRsp) error {
tutorialIDs, _, err := getTutorialResourceIDs()
if err != nil {
return err
}
rsp.Retcode = 0
for _, id := range tutorialIDs {
rsp.TutorialList = append(rsp.TutorialList, &goproto.Tutorial{
@@ -19,6 +23,10 @@ func init() {
})
handle("GetTutorialGuide", func(s *Session, req *goproto.GetTutorialGuideCsReq, rsp *goproto.GetTutorialGuideScRsp) error {
_, tutorialGuideIDs, err := getTutorialResourceIDs()
if err != nil {
return err
}
rsp.Retcode = 0
for _, id := range tutorialGuideIDs {
rsp.TutorialGuideList = append(rsp.TutorialGuideList, &goproto.TutorialGuide{
+69 -109
View File
@@ -1,115 +1,75 @@
package gameserver
// Generated from the Rust tutorial.rs lists (client 4.6.51).
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"sort"
"sync"
)
var tutorialIDs = []uint32{
1001, 1002, 1003, 1004, 1005, 1007, 1008, 1010, 2000, 2001, 2002, 2003, 2004, 2008, 2010, 2011, 2012, 2013, 3001, 3002,
3003, 3004, 3005, 3006, 3008, 3009, 3010, 3011, 3012, 3013, 3014, 3202, 4002, 4003, 4004, 4005, 4006, 4007, 4008, 4009,
4010, 4011, 4013, 4014, 4015, 4017, 4018, 4019, 4020, 4021, 4022, 4023, 4024, 4025, 4026, 4027, 4028, 4029, 4030, 4031,
4032, 4033, 4034, 4035, 4036, 4037, 4038, 4039, 4040, 4041, 4042, 4043, 4044, 4045, 4046, 4047, 4048, 4049, 4050, 4051,
4052, 4053, 4054, 4055, 4056, 4057, 4058, 4059, 4060, 4061, 4063, 4066, 4067, 4068, 4070, 4071, 4072, 4073, 4074, 4075,
4076, 4077, 4078, 4079, 4081, 4083, 4084, 4085, 4097, 4098, 4112, 5001, 5002, 5003, 5004, 5005, 5006, 5007, 5008, 5009,
5010, 5011, 5012, 5013, 5014, 5015, 5016, 5017, 5018, 5019, 5021, 5022, 5023, 5024, 5025, 5026, 5027, 5028, 5029, 5030,
5031, 5032, 5033, 5034, 5035, 5036, 5038, 5039, 5041, 5044, 5045, 5046, 5047, 5048, 5049, 5050, 5051, 5052, 5053, 5054,
5055, 5056, 5057, 5058, 5059, 5060, 5061, 5062, 5063, 5064, 5065, 5070, 5071, 5072, 5073, 5074, 5075, 5077, 5078, 5079,
5081, 5082, 5083, 5084, 5085, 5086, 5087, 5088, 5089, 5090, 5091, 5092, 5093, 5094, 5095, 5096, 5097, 5098, 5099, 5100,
5101, 5102, 5103, 5104, 5105, 5106, 5107, 5108, 5109, 5110, 5111, 5112, 5113, 5114, 5115, 5116, 5130, 5131, 5132, 5133,
5134, 5135, 5140, 5141, 5142, 5143, 5144, 5145, 5146, 5150, 5151, 5152, 5153, 5154, 5155, 5156, 5157, 5158, 5159, 5160,
5161, 5162, 5163, 5164, 5172, 5173, 5174, 5175, 5176, 5177, 5178, 5179, 5180, 5181, 5182, 5183, 5184, 5185, 5186, 5187,
5188, 5189, 5190, 5191, 5192, 5193, 5194, 5195, 5196, 5197, 5198, 5199, 5200, 5201, 5202, 5210, 5211, 5212, 5213, 5214,
5215, 5216, 5217, 5218, 5219, 5220, 5221, 5222, 5301, 5302, 5303, 5304, 5305, 5306, 5307, 5308, 5309, 5310, 5311, 5312,
5313, 5314, 5316, 5317, 5318, 5319, 5320, 5321, 5322, 5323, 5324, 5325, 5326, 5350, 5351, 5352, 5355, 5356, 5357, 5358,
5359, 5360, 5361, 5365, 5366, 5367, 5368, 5369, 5370, 5371, 5372, 5373, 5374, 5375, 5376, 5377, 5378, 5380, 5381, 5382,
5383, 5384, 5385, 5386, 5387, 5388, 5389, 5390, 5391, 5392, 5393, 5394, 5395, 5397, 5398, 5399, 5400, 5401, 5402, 5403,
5404, 5405, 5406, 5407, 5408, 5409, 5410, 5450, 5451, 5452, 5453, 5454, 5455, 5456, 5457, 5458, 5459, 5460, 5461, 5462,
5463, 5464, 5465, 5510, 5511, 5512, 5513, 5514, 5515, 5517, 5518, 5520, 5521, 5522, 5523, 5524, 5525, 5530, 5531, 5532,
5533, 5534, 5535, 5536, 5537, 5540, 5550, 5551, 5552, 5553, 5554, 5555, 5556, 5560, 5561, 5562, 5563, 5564, 5565, 5566,
5567, 5568, 5569, 5600, 5601, 5602, 5603, 5604, 5605, 5606, 5607, 5608, 5609, 5610, 5611, 5612, 5613, 5614, 5615, 5700,
5701, 5702, 5703, 5704, 5705, 5706, 5707, 5708, 5709, 5710, 5711, 5712, 5713, 5714, 5715, 5716, 5717, 5718, 5750, 5751,
5752, 5753, 5754, 5755, 5756, 5757, 5758, 5759, 5760, 5761, 5762, 5763, 5764, 5765, 5766, 5767, 5768, 5769, 5770, 5771,
5772, 5773, 5774, 5775, 5801, 5802, 5803, 5804, 5805, 5806, 5810, 5811, 5812, 5813, 5814, 5815, 5816, 5817, 5901, 5902,
5903, 5904, 5905, 5906, 5907, 5908, 5909, 5910, 5911, 5912, 5913, 5914, 5915, 5916, 5917, 5918, 5921, 5922, 5923, 5924,
5925, 5926, 5927, 5928, 5929, 5930, 5931, 5933, 5934, 5936, 5937, 5938, 5939, 5940, 5950, 5961, 5962, 5963, 5964, 5965,
5966, 5967, 5968, 5969, 5970, 5971, 5972, 5973, 5974, 5975, 5976, 5977, 5978, 5979, 5980, 6000, 6001, 6002, 6003, 6004,
6005, 6006, 6007, 6008, 6009, 6010, 6011, 6012, 6013, 6014, 6015, 6016, 6017, 6018, 6019, 6020, 6021, 6022, 6023, 6024,
6025, 6026, 6027, 6028, 6029, 6030, 6031, 6050, 6051, 6052, 6053, 6054, 6055, 6100, 6102, 6103, 6104, 6105, 6106, 6107,
6108, 6109, 6110, 6111, 6112, 6113, 6114, 6115, 6116, 6117, 6118, 6119, 6120, 6121, 6122, 6123, 6124, 6125, 6126, 6127,
6128, 6129, 6130, 6131, 6132, 6133, 6134, 6135, 6136, 6137, 6138, 6139, 6141, 6142, 6193, 6194, 6195, 6196, 6197, 6198,
6199, 6200, 6300, 6301, 6302, 6303, 6304, 6305, 6306, 6307, 6308, 6309, 6310, 6311, 6312, 6313, 6314, 6315, 6316, 6317,
6318, 6319, 6320, 6321, 6322, 6323, 6324, 6325, 6326, 6327, 6328, 6329, 6330, 6331, 6332, 6333, 6334, 6335, 6336, 6337,
6338, 6339, 6340, 6341, 6342, 6343, 6400, 6401, 6402, 6403, 6404, 6405, 6406, 6408, 6409, 6410, 6411, 6412, 6450, 6451,
6452, 6453, 6454, 6455, 6456, 6500, 6501, 6503, 6505, 6506, 6507, 6508, 6509, 6510, 6511, 6512, 6513, 6514, 6515, 6516,
6517, 6518, 6519, 6601, 6602, 6603, 6604, 6605, 6606, 6607, 6608, 6609, 6610, 6611, 6612, 6613, 6614, 6700, 6701, 6702,
6703, 6704, 6705, 6706, 6707, 6708, 6709, 6710, 6711, 6712, 6713, 6714, 6715, 6716, 6717, 6718, 6719, 6750, 6751, 6752,
6753, 6754, 6755, 6756, 6757, 6758, 6759, 7001, 7002, 7003, 7004, 7005, 7006, 7007, 7008, 7009, 7010, 7011, 7012, 7013,
7014, 7015, 7016, 7017, 7023, 7024, 7025, 7026, 7027, 7028, 7029, 7030, 7031, 7032, 7033, 7034, 7035, 8001, 8002, 8003,
8005, 8006, 8007, 8009, 8012, 8013, 8014, 8015, 8016, 8017, 8018, 8019, 8020, 8021, 8101, 8102, 8103, 8104, 8105, 8106,
8107, 8108, 8109, 8110, 8111, 8112, 8113, 8114, 8115, 8116, 8117, 8118, 8119, 8120, 8121, 8122, 8123, 8124, 8125, 8126,
8127, 8128, 8129, 8130, 8201, 8202, 8203, 8204, 8205, 8206, 8301, 8302, 8303, 8304, 8305, 8306, 8307, 8308, 8309, 8310,
8311, 8312, 8313, 8314, 8315, 8316, 8317, 8318, 8319, 8320, 8321, 8322, 8323, 8324, 8325, 8326, 8327, 8328, 8401, 8404,
8405, 8406, 8501, 8502, 8503, 8504, 8505, 8506, 8508, 8509, 8601, 8602, 8603, 8604, 8605, 8606, 8607, 8608, 8609, 8611,
8612, 8613, 8614, 8621, 8625, 8626, 8630, 8631, 8632, 8633, 8634, 8635, 8636, 8637, 8638, 8639, 8640, 8641, 8700, 8701,
8702, 8703, 8704, 8705, 8706, 8707, 8708, 8709, 8710, 8711, 8712, 8713, 8714, 8715, 8716, 8717, 8718, 8719, 8720, 8721,
8722, 8723, 8724, 8725, 8802, 8803, 8806, 8807, 8808, 8809, 8811, 8812, 8813, 8905, 8906, 8907, 8908, 8909, 8910, 8911,
8931, 8932, 9001, 9002, 9003, 9004, 9005, 9006, 9007, 9008, 9009, 9010, 9011, 9012, 9013, 9014, 9015, 9016, 9017, 9018,
9019, 9020, 9021, 9022, 9023, 9024, 9025, 9026, 9027, 9028, 9029, 9030, 9031, 9032, 9033, 9034, 9035, 9036, 9037, 9038,
9039, 9040, 9041, 9042, 9043, 9044, 9045, 9046, 9047, 9048, 9049, 9050, 9051, 9052, 9053, 9054, 9055, 9056, 9057, 9058,
9059, 9060, 9061, 9062, 9063, 9065, 9066, 9067, 9068, 9070, 9071, 9072, 9073, 9074, 9075, 9076, 9077, 9078, 9079, 9080,
9081, 9082, 9083, 9084, 9085, 9086, 9087, 9088, 9090, 9091, 9092, 9093, 9094, 9095, 9096, 9097, 9098, 9099, 9100, 9110,
9111, 9112, 9113, 9114, 9115, 9116, 9117, 9118, 9119, 9120, 9121, 9122, 9123, 9124, 9125, 9126, 9127, 9128, 9129, 9130,
9131, 9132, 9133, 9134, 9135, 9136, 9137, 9201, 9202, 9203, 9204, 9205, 9206, 9207, 9208, 9209, 9210, 9211, 9212, 9213,
9214, 9301, 9302, 9303, 9304, 9305, 9306, 9307, 9308, 9309, 9310, 9311, 9313, 9314, 9315, 9316, 9317, 9318, 9319, 9321,
9322, 9323, 9324, 9325, 9401, 9402, 9403, 9404, 9405, 9501, 9502, 9503, 9504, 9505, 9506, 9507, 9508, 9509, 9510, 9511,
9512, 9513, 9514, 9515, 9598, 9599, 9610, 9611, 9612, 9613, 9614, 9615, 9616, 9617, 9618, 9619, 9620, 9621, 9622, 9623,
9624, 9625, 9626, 9627, 9628, 9629, 9630, 9631, 9650, 9651, 9701, 9702, 9703, 9704, 9801, 9802, 9803, 9804, 9805, 9806,
9807, 9808, 9809, 9810, 9811, 9812, 9813, 9814, 9815, 9816, 9817, 9818, 9819, 9820, 9901, 9903, 9904, 9905, 9906, 9908,
10001, 10002, 10003, 10004, 10005, 10006, 10007, 10008, 10009, 10031, 10032, 10033, 10034, 10036, 10037, 10038, 10039, 10040, 10041, 10042,
10043, 10045, 43000, 43001, 43002, 43003, 43004, 43005, 43006, 43007, 43008, 43009, 43010, 43011, 44001, 44002, 44003, 44004, 44005, 44006,
44007, 44008, 44009, 44010, 44011, 44012, 44013, 44014, 45001,
// Tutorial IDs come from the installed Client resource tables, not a list
// copied from an older server build. Restart after replacing these resources.
var tutorialResourceCache struct {
sync.Once
tutorials []uint32
guides []uint32
err error
}
var tutorialGuideIDs = []uint32{
1101, 1102, 1103, 1104, 1105, 1108, 1109, 1110, 1116, 1117, 1118, 1119, 2006, 2007, 2008, 2105, 2106, 2107, 2200, 2201,
2202, 2203, 2204, 2205, 2206, 2207, 2208, 2209, 2210, 2211, 2212, 2213, 2214, 2215, 2216, 2217, 2218, 2219, 2220, 2221,
2222, 2223, 2224, 2225, 2226, 2227, 2228, 2229, 2230, 2231, 2232, 2233, 2234, 2235, 2236, 2237, 2238, 2242, 2243, 2244,
2245, 2246, 2247, 2249, 2251, 2252, 2253, 2254, 2260, 3007, 3105, 3106, 3107, 3108, 3109, 3201, 3202, 3203, 3204, 3205,
3206, 3207, 3208, 3209, 3210, 3211, 3212, 3213, 3214, 3215, 3216, 3217, 3218, 3219, 3221, 3222, 3223, 3225, 3227, 3228,
3229, 3230, 3231, 3232, 4001, 4101, 4102, 4103, 4104, 4105, 4106, 4107, 4109, 4110, 5101, 5102, 5103, 5104, 5105, 5106,
6001, 6002, 6003, 6004, 6008, 6009, 6010, 6011, 6012, 6014, 6015, 6018, 6020, 6021, 6023, 6024, 6025, 6026, 6027, 6028,
6029, 6030, 6031, 6032, 6033, 6034, 6035, 6036, 6037, 6038, 6039, 6040, 6041, 6042, 6043, 6044, 6045, 6046, 6047, 6048,
6049, 6050, 6051, 6052, 6053, 6054, 6055, 6056, 6057, 6058, 6059, 6060, 6061, 6062, 6063, 6064, 6065, 6066, 6067, 6068,
6069, 6070, 6071, 6072, 6073, 6074, 6075, 6076, 6077, 6078, 6079, 6080, 6081, 6082, 6083, 6085, 6086, 6087, 6088, 6089,
6090, 6091, 6092, 6093, 6094, 6095, 6096, 6097, 6098, 6099, 6100, 6101, 6102, 6103, 6104, 6105, 6106, 6107, 6108, 6109,
6110, 6111, 6112, 6113, 6114, 6116, 6117, 6118, 6119, 6120, 6122, 6124, 6125, 6126, 6127, 6128, 6129, 6130, 6301, 6302,
6303, 6304, 6305, 6306, 6307, 6308, 6309, 6310, 6311, 6312, 6314, 6315, 6316, 6317, 6318, 6319, 6320, 6321, 6322, 6323,
6324, 6325, 6326, 6327, 6328, 6329, 6330, 6331, 6332, 6333, 6334, 6335, 6336, 6337, 6338, 6339, 6340, 6341, 6342, 6343,
6345, 6346, 6347, 6348, 6349, 6350, 6351, 6352, 6354, 6355, 6356, 6357, 6358, 6359, 6360, 6361, 6362, 6363, 6364, 6365,
6366, 6367, 6368, 6369, 6371, 6372, 6374, 6375, 6376, 6377, 6378, 6379, 6380, 7090, 7091, 7092, 7100, 7101, 7501, 7502,
7503, 7504, 7506, 7507, 7508, 7509, 7511, 7514, 7515, 8001, 8002, 8003, 8004, 8006, 8007, 8008, 8010, 8011, 8012, 8013,
8014, 8015, 8016, 8017, 8018, 8019, 8020, 8021, 8022, 8023, 8024, 8025, 8026, 8027, 8028, 8038, 8039, 8047, 8050, 8051,
8052, 8055, 8056, 8057, 8058, 8059, 8061, 8062, 8063, 8064, 8065, 8066, 8067, 8069, 8070, 8072, 8073, 8074, 8075, 8076,
8078, 8079, 8080, 8090, 8091, 8092, 8093, 8094, 8095, 8096, 8100, 8101, 8102, 8103, 8104, 8105, 8106, 8107, 8108, 8109,
8110, 8111, 8112, 8113, 8122, 8123, 8124, 8130, 8131, 8132, 8133, 8134, 8135, 8136, 8137, 8138, 8139, 8140, 8141, 8142,
8143, 8144, 8145, 8146, 8147, 8148, 8149, 8150, 8151, 8152, 8153, 8154, 8155, 8156, 8157, 8158, 8160, 8161, 8162, 8163,
8164, 8165, 8166, 8167, 8168, 8170, 8171, 8172, 8173, 8174, 8175, 8176, 8180, 8181, 8182, 8183, 8184, 8185, 8186, 8187,
8188, 8189, 8190, 8191, 8192, 8193, 8194, 8195, 8196, 8197, 8198, 8199, 8200, 8201, 8202, 8203, 8204, 8205, 8206, 8207,
8208, 8209, 8211, 8212, 8213, 8301, 8302, 8303, 8304, 8305, 8306, 8310, 8340, 8341, 8342, 8343, 8344, 8345, 8346, 8701,
8702, 8703, 8704, 8802, 8803, 8804, 8805, 9101, 9102, 9103, 9104, 9105, 9107, 9108, 9109, 9110, 9111, 9112, 9113, 9114,
9115, 9116, 9117, 9118, 9119, 9120, 9201, 9202, 9203, 9204, 9205, 9206, 9207, 9208, 9209, 9210, 9211, 9212, 9301, 9303,
9304, 9305, 9306, 9308, 9309, 9310, 9312, 9313, 9314, 9315, 9316, 9317, 9318, 9319, 9320, 9321, 9323, 9324, 9325, 9326,
9327, 9330, 9331, 9332, 9335, 9336, 9337, 9338, 9339, 9340, 9341, 9342, 9343, 9344, 9345, 9601, 9602, 9603, 9604, 9605,
9701, 9702, 9801, 9802, 9803, 9804, 9811, 9812, 9813, 9814, 9815, 9816, 9817, 9818, 9819, 9820, 9821, 9822, 9823, 9824,
9825, 9826, 9827, 9829, 9830, 9831, 9832, 9833, 9834, 9835, 9836, 9837, 9838, 9839, 9840, 9841, 9842, 9843, 9844, 9845,
9846, 9847, 9848, 9849, 9850, 9851, 9861, 9862, 9863, 9864, 9871, 9872, 9901, 9902, 9903, 9904, 9905, 9911, 9913, 9915,
9920, 9921, 9922, 9923, 9925, 9926, 9931, 9932, 9933, 9935, 9936, 9937, 9938, 9941, 9942, 9943, 9944, 9945, 9946, 9947,
9951, 9952, 9953, 9954, 9955, 9956, 9957, 9958, 9959, 9960, 9961, 9962, 9963, 9964, 9965, 9967, 9968, 9969, 9970, 9971,
9973, 9974, 9975, 9976, 9978, 9979, 9980, 9981, 9982, 9983, 9984, 9985, 9986, 9987, 9988, 9989, 9990, 9991, 9992, 9993,
9994, 9995, 10000, 10001, 10002, 10003, 10005, 10010, 10011, 10012, 10013, 10014, 10015, 10016, 10017, 10020, 10021, 10022, 10030, 10031,
10032, 10033, 10034, 10040, 10041, 10042, 10043, 10044, 10045, 10050, 10051, 10052, 10053, 10054, 10055, 10060, 10061, 10062, 10070, 10071,
10072, 10073, 10080, 10081, 10082, 10083, 10084, 10085, 10086, 10087, 10088, 10090, 10091, 10092, 10093, 10096, 10100, 10101, 10102, 10103,
10111, 10112, 10113, 10201, 10202, 10203, 10204, 10310, 10311, 10312, 10313, 10314, 10315, 10401, 10502, 16505, 16506, 16507, 16508, 16509,
31001, 31102, 31103, 31105, 31106, 31109, 31204, 31206, 45001, 100001, 100002, 100003, 100004, 100005, 100006, 100007, 100008, 100009, 100010, 100011,
100012, 100013, 100014, 100015, 100016, 100017, 100021, 100022, 100023, 100024, 100025, 100026, 100027, 100028, 100029, 100030, 100031, 100032, 100033, 100034,
100035, 100036, 100037, 100038, 100039, 100040, 100041, 100042, 100043, 100044, 100045, 100046, 100047, 100048, 100049, 100050, 100051, 100052, 100053, 100054,
100055, 100056, 100057, 100058, 100059, 100060, 100061, 100062, 100063, 100064, 100065, 100066, 100067, 100068, 100069,
func getTutorialResourceIDs() ([]uint32, []uint32, error) {
tutorialResourceCache.Do(func() {
var tutorialFile struct {
Entries []struct {
ID uint32 `json:"TutorialID"`
} `json:"tutorial_config"`
}
var guideFile struct {
Entries []struct {
ID uint32 `json:"GroupID"`
} `json:"tutorial_guide_config"`
}
load := func(name string, dst any) error {
path := filepath.Join("resources", name)
data, err := os.ReadFile(path)
if err != nil {
return fmt.Errorf("tutorial resource %s: %w", path, err)
}
if err := json.Unmarshal(data, dst); err != nil {
return fmt.Errorf("tutorial resource %s: %w", path, err)
}
return nil
}
if err := load("TutorialData.json", &tutorialFile); err != nil {
tutorialResourceCache.err = err
return
}
if err := load("TutorialGuideGroup.json", &guideFile); err != nil {
tutorialResourceCache.err = err
return
}
collect := func(ids []uint32) []uint32 {
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
out := ids[:0]
for _, id := range ids {
if id != 0 && (len(out) == 0 || id != out[len(out)-1]) {
out = append(out, id)
}
}
return out
}
for _, entry := range tutorialFile.Entries {
tutorialResourceCache.tutorials = append(tutorialResourceCache.tutorials, entry.ID)
}
for _, entry := range guideFile.Entries {
tutorialResourceCache.guides = append(tutorialResourceCache.guides, entry.ID)
}
tutorialResourceCache.tutorials = collect(tutorialResourceCache.tutorials)
tutorialResourceCache.guides = collect(tutorialResourceCache.guides)
if len(tutorialResourceCache.tutorials) == 0 || len(tutorialResourceCache.guides) == 0 {
tutorialResourceCache.err = fmt.Errorf("tutorial resource tables contain no IDs")
}
})
return tutorialResourceCache.tutorials, tutorialResourceCache.guides, tutorialResourceCache.err
}
+6 -9
View File
@@ -4,9 +4,7 @@ import (
goproto "gac-hkrpg-go/protocol/proto"
)
// Handlers for cmds the real 4.6.51 client sends during normal play that were
// dropped in the first test session (2026-10-07 01:00) — several of them gate
// UI state (pause menu, Endgame entry, map rotation) on the server's reply.
// UI requests bound to the command names in the loaded protocol.
func init() {
// client pauses itself when opening menus/map — must be acked
@@ -16,12 +14,11 @@ func init() {
return nil
})
// the Endgame entrance waits for this before opening its UI
// (4.6.51: CsReq 4190 -> ScRsp 4112, retcode = tag 14 — the Rust build
// answered the wrong id 4130 and the client stalled)
rawHandlers[4190] = func(s *Session, payload []byte) error {
return s.SendRaw(&NetPacket{CmdType: 4112, Body: []byte{0x70, 0x00}})
}
// The Endgame entrance waits for this response before opening its UI.
handle("GetUnreleasedBlockInfo", func(s *Session, req *goproto.GetUnreleasedBlockInfoCsReq, rsp *goproto.GetUnreleasedBlockInfoScRsp) error {
rsp.Retcode = 0
return nil
})
handle("TakeQuestReward", func(s *Session, req *goproto.TakeQuestRewardCsReq, rsp *goproto.TakeQuestRewardScRsp) error {
rsp.Retcode = 0
+8 -2
View File
@@ -37,6 +37,8 @@ type Session struct {
startTime time.Time
nextSceneSave int64
battleProbeMu sync.Mutex
battleProbe []byte
PropEntities map[uint32]PropEntry
// MonsterEntities tracks real overworld monster entities (entity_id -> event_id)
// spawned in the current scene. Used by SceneCastSkill to decide whether an
@@ -127,7 +129,7 @@ func (s *Session) Consume(data []byte) error {
s.kcpMu.Unlock()
for _, pkt := range packets {
if pkt.CmdType == 31 { // CmdPlayerLogoutCsReq
if pkt.CmdType == goproto.CmdIDByName["PlayerLogoutCsReq"] {
Log("[PACKET] Player logged out")
s.Close()
return nil
@@ -156,7 +158,11 @@ func (s *Session) Send(msg proto.Message) error {
PacketLog(fmt.Sprintf("[PACKET SEND] cmd_id: %d %s (len: %d)", cmdID, name, len(body)))
Log(fmt.Sprintf("sent packet with CmdID: %d (%s)", cmdID, name))
return s.SendRaw(&NetPacket{CmdType: cmdID, Body: body})
if err := s.SendRaw(&NetPacket{CmdType: cmdID, Body: body}); err != nil {
return err
}
s.captureBattleResponse(msg, cmdID, body)
return nil
}
// SendRaw writes an already-framed packet into the KCP stream.