fix: sync current handlers, repair relic data and restore visible launcher consoles
This commit is contained in:
1 parent
163d4433e6
commit
74f3c4411b
46 files changed
+4599
-1402
No files matched your search
@@ -111,7 +111,7 @@ func (m *MultiPathAvatar) UnmarshalJSON(data []byte) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiPathAvatarType in the 4.6.51 proto uses the avatar id itself as the
|
||||
// MultiPathAvatarType in the loaded schema uses the avatar id itself as the
|
||||
// enum value (Mar7thKnightType=1001, BoyWarriorType=8001, ...), so the proto
|
||||
// value is simply the numeric id (0 for Unk).
|
||||
func (m MultiPathAvatar) ProtoType() uint32 { return uint32(m) }
|
||||
|
||||
@@ -44,11 +44,12 @@ func relicAffixes(subs []SubAffix) []*goproto.RelicAffix {
|
||||
}
|
||||
|
||||
func (r *Relic) ToBattleRelic() *goproto.BattleRelic {
|
||||
// Match PearlSR's standalone battle relic format. Inventory UniqueID is
|
||||
// only sent by ToProtoRelic/ToEquipRelic; battle carries the full affixes.
|
||||
return &goproto.BattleRelic{
|
||||
Id: r.RelicID,
|
||||
Level: r.Level,
|
||||
MainAffixId: r.MainAffixID,
|
||||
UniqueId: r.UniqueID(),
|
||||
SubAffixList: relicAffixes(r.SubAffixes),
|
||||
}
|
||||
}
|
||||
@@ -316,19 +317,26 @@ func (m *Monster) ToSceneMonster() *goproto.SceneMonster {
|
||||
return &goproto.SceneMonster{
|
||||
MonsterId: m.MonsterID,
|
||||
MaxHp: hp,
|
||||
CurHp: hp,
|
||||
}
|
||||
}
|
||||
|
||||
func monsterWave(waveID uint32, monsters []Monster) *goproto.SceneMonsterWave {
|
||||
func monsterWave(monsters []Monster) *goproto.SceneMonsterWave {
|
||||
maxLevel := uint32(0)
|
||||
for _, monster := range monsters {
|
||||
if monster.Level > maxLevel {
|
||||
maxLevel = monster.Level
|
||||
}
|
||||
}
|
||||
if maxLevel == 0 {
|
||||
maxLevel = 95
|
||||
}
|
||||
|
||||
wave := &goproto.SceneMonsterWave{
|
||||
WaveId: waveID,
|
||||
MonsterList: make([]*goproto.SceneMonster, 0, len(monsters)),
|
||||
// NOTE: WaveParam MUST be nil! In client 4.6.53 GameAssembly.dll
|
||||
// AdventureModule.CreateBattleServerCheckResult (RVA 0x166b2ef6),
|
||||
// if WaveParam is non-nil it tries to dereference [wave_param + 0x10]
|
||||
// and throws NullReferenceException. When nil, it safely jumps over.
|
||||
WaveParam: nil,
|
||||
// Include a level so the client can validate this wave directly instead
|
||||
// of looking up monster data from the battle stage's static table. This
|
||||
// matters when a Calyx request reuses a custom battle stage ID.
|
||||
WaveParam: &goproto.SceneMonsterWaveParam{Level: maxLevel},
|
||||
}
|
||||
for i := range monsters {
|
||||
wave.MonsterList = append(wave.MonsterList, monsters[i].ToSceneMonster())
|
||||
@@ -338,8 +346,8 @@ func monsterWave(waveID uint32, monsters []Monster) *goproto.SceneMonsterWave {
|
||||
|
||||
func ToSceneMonsterWaves(waves [][]Monster) []*goproto.SceneMonsterWave {
|
||||
out := make([]*goproto.SceneMonsterWave, 0, len(waves))
|
||||
for i, wave := range waves {
|
||||
out = append(out, monsterWave(uint32(i+1), wave))
|
||||
for _, wave := range waves {
|
||||
out = append(out, monsterWave(wave))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
goproto "gac-hkrpg-go/protocol/proto"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
func TestStandaloneBattleRelicWire(t *testing.T) {
|
||||
relic := Relic{
|
||||
RelicID: 61151, Level: 15, MainAffixID: 1, InternalUID: 42,
|
||||
SubAffixes: []SubAffix{{SubAffixID: 7, Count: 3, Step: 3}, {SubAffixID: 8, Count: 3, Step: 0}},
|
||||
}
|
||||
encoded, err := proto.Marshal(relic.ToBattleRelic())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var decoded goproto.BattleRelic
|
||||
if err := proto.Unmarshal(encoded, &decoded); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if decoded.UniqueId != 0 {
|
||||
t.Fatal("standalone battle relic must not use inventory ID")
|
||||
}
|
||||
if decoded.Id != 61151 || decoded.MainAffixId != 1 || len(decoded.SubAffixList) != 2 {
|
||||
t.Fatalf("relic or affixes lost on wire: %v", &decoded)
|
||||
}
|
||||
if decoded.SubAffixList[0].Cnt != 3 || decoded.SubAffixList[0].Step != 3 || decoded.SubAffixList[1].Step != 0 {
|
||||
t.Fatalf("sub-affix values changed on wire: %v", decoded.SubAffixList)
|
||||
}
|
||||
if relic.ToProtoRelic().UniqueId != 42 || relic.ToEquipRelic().RelicUniqueId != 42 {
|
||||
t.Fatal("inventory and equipped relic references must retain their IDs")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// RuntimeConfig is shared by both server processes. Deployment values belong
|
||||
// in configuration rather than in handlers or executable entry points.
|
||||
type RuntimeConfig struct {
|
||||
BattleDiagnostics BattleDiagnosticsConfig `json:"battle_diagnostics"`
|
||||
ClientVersion string `json:"client_version"`
|
||||
GameHost string `json:"game_host"`
|
||||
GamePort uint16 `json:"game_port"`
|
||||
SDKHost string `json:"sdk_host"`
|
||||
SDKPort uint16 `json:"sdk_port"`
|
||||
PublicGameHost string `json:"public_game_host"`
|
||||
PublicSDKURL string `json:"public_sdk_url"`
|
||||
}
|
||||
|
||||
// The obfuscated protobuf class name is version-specific and comes from the
|
||||
// client's stack trace. Empty disables the read-only client wire probe.
|
||||
type BattleDiagnosticsConfig struct {
|
||||
ClientBattleType string `json:"client_battle_type"`
|
||||
}
|
||||
|
||||
func LoadRuntimeConfig() (RuntimeConfig, error) {
|
||||
path := os.Getenv("GAC_CONFIG")
|
||||
if path == "" {
|
||||
path = "server.json"
|
||||
}
|
||||
var cfg RuntimeConfig
|
||||
for i, filename := range []string{path, filepath.Join(filepath.Dir(path), strings.TrimSuffix(filepath.Base(path), filepath.Ext(path))+".local.json")} {
|
||||
raw, err := os.ReadFile(filename)
|
||||
if i == 1 && os.IsNotExist(err) {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
return cfg, fmt.Errorf("read runtime config %s: %w", filename, err)
|
||||
}
|
||||
if err := json.Unmarshal(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf}), &cfg); err != nil {
|
||||
return cfg, fmt.Errorf("parse runtime config %s: %w", filename, err)
|
||||
}
|
||||
}
|
||||
for name, dest := range map[string]*string{
|
||||
"GAC_CLIENT_VERSION": &cfg.ClientVersion, "GAC_GAME_HOST": &cfg.GameHost,
|
||||
"GAC_SDK_HOST": &cfg.SDKHost, "GAC_PUBLIC_GAME_HOST": &cfg.PublicGameHost,
|
||||
"GAC_PUBLIC_SDK_URL": &cfg.PublicSDKURL,
|
||||
} {
|
||||
if value, ok := os.LookupEnv(name); ok {
|
||||
*dest = value
|
||||
}
|
||||
}
|
||||
for name, dest := range map[string]*uint16{"GAC_GAME_PORT": &cfg.GamePort, "GAC_SDK_PORT": &cfg.SDKPort} {
|
||||
if value, ok := os.LookupEnv(name); ok {
|
||||
n, err := strconv.ParseUint(value, 10, 16)
|
||||
if err != nil || n == 0 {
|
||||
return cfg, fmt.Errorf("%s must be a port between 1 and 65535", name)
|
||||
}
|
||||
*dest = uint16(n)
|
||||
}
|
||||
}
|
||||
if cfg.GamePort == 0 || cfg.SDKPort == 0 || strings.TrimSpace(cfg.ClientVersion) == "" {
|
||||
return cfg, fmt.Errorf("runtime config requires client_version, game_port and sdk_port")
|
||||
}
|
||||
for name, host := range map[string]string{"game_host": cfg.GameHost, "sdk_host": cfg.SDKHost, "public_game_host": cfg.PublicGameHost} {
|
||||
if strings.TrimSpace(host) == "" || strings.ContainsAny(host, "/\\ \t\r\n") {
|
||||
return cfg, fmt.Errorf("invalid %s", name)
|
||||
}
|
||||
}
|
||||
u, err := url.Parse(cfg.PublicSDKURL)
|
||||
if err != nil || u.Hostname() == "" || (u.Scheme != "http" && u.Scheme != "https") || u.RawQuery != "" || u.Fragment != "" || u.User != nil {
|
||||
return cfg, fmt.Errorf("public_sdk_url must be an HTTP(S) base URL")
|
||||
}
|
||||
cfg.PublicSDKURL = strings.TrimRight(cfg.PublicSDKURL, "/")
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c RuntimeConfig) SDKAddress() string {
|
||||
return net.JoinHostPort(c.SDKHost, strconv.Itoa(int(c.SDKPort)))
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Addresses and ports below are deliberately different fixture values, not
|
||||
// deployment defaults. These tests must never bind sockets or start servers.
|
||||
func TestRuntimeConfigOverrides(t *testing.T) {
|
||||
t.Chdir(t.TempDir())
|
||||
for _, name := range []string{"GAC_CONFIG", "GAC_CLIENT_VERSION", "GAC_GAME_HOST", "GAC_GAME_PORT", "GAC_SDK_HOST", "GAC_SDK_PORT", "GAC_PUBLIC_GAME_HOST", "GAC_PUBLIC_SDK_URL"} {
|
||||
t.Setenv(name, "")
|
||||
if err := os.Unsetenv(name); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
base := `{"client_version":"fixture","game_host":"localhost","game_port":32101,"sdk_host":"localhost","sdk_port":32102,"public_game_host":"example.invalid","public_sdk_url":"https://example.invalid/base/"}`
|
||||
if err := os.WriteFile("server.json", []byte(base), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
local := append([]byte{0xef, 0xbb, 0xbf}, []byte(`{"game_port":32201,"launcher":{"client_path":"fixture"}}`)...)
|
||||
if err := os.WriteFile("server.local.json", local, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.GamePort != 32201 || cfg.SDKPort != 32102 || cfg.PublicSDKURL != "https://example.invalid/base" {
|
||||
t.Fatalf("merge failed: %+v", cfg)
|
||||
}
|
||||
t.Setenv("GAC_GAME_PORT", "32301")
|
||||
t.Setenv("GAC_PUBLIC_GAME_HOST", "gateway.example.invalid")
|
||||
cfg, err = LoadRuntimeConfig()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if cfg.GamePort != 32301 || cfg.PublicGameHost != "gateway.example.invalid" {
|
||||
t.Fatalf("environment override failed: %+v", cfg)
|
||||
}
|
||||
t.Setenv("GAC_GAME_PORT", "65536")
|
||||
if _, err := LoadRuntimeConfig(); err == nil {
|
||||
t.Fatal("out-of-range port accepted")
|
||||
}
|
||||
t.Setenv("GAC_GAME_PORT", "0")
|
||||
if _, err := LoadRuntimeConfig(); err == nil {
|
||||
t.Fatal("zero port accepted")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRuntimeConfigRequiresFile(t *testing.T) {
|
||||
t.Setenv("GAC_CONFIG", filepath.Join(t.TempDir(), "missing.json"))
|
||||
if _, err := LoadRuntimeConfig(); err == nil {
|
||||
t.Fatal("missing config silently accepted")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user