feat(all): add resilient server recovery and talent upgrades
This commit is contained in:
@@ -52,6 +52,8 @@ type Response struct {
|
||||
|
||||
type InitialState struct {
|
||||
Platform string `json:"platform"`
|
||||
ClientVersion string `json:"client_version"`
|
||||
GameVersion string `json:"game_version"`
|
||||
LogPath string `json:"log_path"`
|
||||
GameDirectory string `json:"game_directory"`
|
||||
ServerOrigin string `json:"server_origin"`
|
||||
@@ -118,7 +120,7 @@ func (s *Studio) log() *slog.Logger {
|
||||
func (s *Studio) Initialize() InitialState {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
state := InitialState{Platform: runtime.GOOS, LogPath: s.options.LogPath, GameDirectory: s.options.InitialGameDir, ServerOrigin: "http://127.0.0.1:8080", CDNMode: clientconfig.CDNOfficial}
|
||||
state := InitialState{Platform: runtime.GOOS, ClientVersion: s.options.Versions.ClientVersion, GameVersion: s.options.Versions.GameVersion, LogPath: s.options.LogPath, GameDirectory: s.options.InitialGameDir, ServerOrigin: "http://127.0.0.1:8080", CDNMode: clientconfig.CDNOfficial}
|
||||
if state.GameDirectory == "" {
|
||||
return state
|
||||
}
|
||||
@@ -318,7 +320,7 @@ func (s *Studio) Launch(input Request) (Response, error) {
|
||||
}
|
||||
return Response{}, fmt.Errorf("launch Brown Dust II: %w", err)
|
||||
}
|
||||
s.log().Info("game launch requested", "platform", installation.Kind, "client_version", status.ClientVersion)
|
||||
s.log().Info("game launch requested", "platform", installation.Kind, "client_version", s.options.Versions.ClientVersion, "game_version", status.ClientVersion)
|
||||
return success("Brown Dust II started", nil), nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestEmbeddedStudioUsesNativeBindings(t *testing.T) {
|
||||
|
||||
func TestStudioInitializeLoadsSavedSettings(t *testing.T) {
|
||||
dir := makeTestClient(t)
|
||||
studio := NewStudio(Options{InitialGameDir: dir, Versions: clientconfig.ReleaseVersions{ClientVersion: "2.35.10"}}, NativeHost{})
|
||||
studio := NewStudio(Options{InitialGameDir: dir, Versions: clientconfig.ReleaseVersions{GameVersion: "2.35.10", ClientVersion: "2.35.10+client.0.1.0"}}, NativeHost{})
|
||||
if state := studio.Initialize(); state.AutoOpen {
|
||||
t.Fatal("missing settings enabled automatic workspace")
|
||||
}
|
||||
@@ -50,14 +50,14 @@ func TestStudioInitializeLoadsSavedSettings(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
state := studio.Initialize()
|
||||
if !state.AutoOpen || state.GameDirectory != dir || state.ServerOrigin != "https://play.example.com" {
|
||||
if !state.AutoOpen || state.GameDirectory != dir || state.ServerOrigin != "https://play.example.com" || state.ClientVersion != "2.35.10+client.0.1.0" || state.GameVersion != "2.35.10" {
|
||||
t.Fatalf("state=%+v", state)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStudioSaveRemembersOnlyValidatedSettings(t *testing.T) {
|
||||
dir := makeTestClient(t)
|
||||
studio := NewStudio(Options{Versions: clientconfig.ReleaseVersions{ClientVersion: "2.35.10"}}, NativeHost{})
|
||||
studio := NewStudio(Options{Versions: clientconfig.ReleaseVersions{GameVersion: "2.35.10"}}, NativeHost{})
|
||||
var remembered string
|
||||
studio.savePreferences = func(value string) error { remembered = value; return nil }
|
||||
input := Request{GameDirectory: dir, ServerOrigin: "https://play.example.com", CDNMode: clientconfig.CDNOfficial}
|
||||
@@ -116,7 +116,7 @@ func TestStudioInitializeDefaultsWithoutGameDirectory(t *testing.T) {
|
||||
func TestStudioBrowseUsesNativeDirectoryPicker(t *testing.T) {
|
||||
dir := makeTestClient(t)
|
||||
var title string
|
||||
studio := NewStudio(Options{Versions: clientconfig.ReleaseVersions{ClientVersion: "2.35.10"}}, NativeHost{
|
||||
studio := NewStudio(Options{Versions: clientconfig.ReleaseVersions{GameVersion: "2.35.10"}}, NativeHost{
|
||||
BrowseDirectory: func(_ context.Context, requested string) (string, error) {
|
||||
title = requested
|
||||
return dir, nil
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -14,15 +14,18 @@ import (
|
||||
const ReleaseFileName = "versions.json"
|
||||
|
||||
var (
|
||||
clientVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+$`)
|
||||
gameVersionPattern = regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+$`)
|
||||
releaseVersionPattern = regexp.MustCompile(`^([0-9]+\.[0-9]+\.[0-9]+)\+(client|server)\.([0-9]+\.[0-9]+\.[0-9]+)$`)
|
||||
resourceVersionPattern = regexp.MustCompile(`^[0-9]{14}$`)
|
||||
)
|
||||
|
||||
// ReleaseVersions is the exact client/resource tuple supported by one
|
||||
// bd2client distribution. The release package carries the authoritative
|
||||
// versions.json next to bd2client.exe.
|
||||
// ReleaseVersions separates the official game compatibility version from the
|
||||
// independently evolving client and server software release versions. The
|
||||
// release package carries the authoritative versions.json beside bd2client.
|
||||
type ReleaseVersions struct {
|
||||
GameVersion string `json:"game_version"`
|
||||
ClientVersion string `json:"client_version"`
|
||||
ServerVersion string `json:"server_version"`
|
||||
GameDataVersion string `json:"game_data_version"`
|
||||
BundleVersion string `json:"bundle_version"`
|
||||
SeedDirectory string `json:"seed_directory"`
|
||||
@@ -48,9 +51,15 @@ func LoadReleaseVersions(path string) (ReleaseVersions, error) {
|
||||
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
|
||||
return ReleaseVersions{}, errors.New("client release versions must contain exactly one JSON object")
|
||||
}
|
||||
if !clientVersionPattern.MatchString(versions.ClientVersion) {
|
||||
if !gameVersionPattern.MatchString(versions.GameVersion) {
|
||||
return ReleaseVersions{}, fmt.Errorf("invalid game_version %q", versions.GameVersion)
|
||||
}
|
||||
if !validReleaseVersion(versions.ClientVersion, versions.GameVersion, "client") {
|
||||
return ReleaseVersions{}, fmt.Errorf("invalid client_version %q", versions.ClientVersion)
|
||||
}
|
||||
if !validReleaseVersion(versions.ServerVersion, versions.GameVersion, "server") {
|
||||
return ReleaseVersions{}, fmt.Errorf("invalid server_version %q", versions.ServerVersion)
|
||||
}
|
||||
if !resourceVersionPattern.MatchString(versions.BundleVersion) || !resourceVersionPattern.MatchString(versions.GameDataVersion) {
|
||||
return ReleaseVersions{}, errors.New("bundle_version and game_data_version must be 14-digit timestamps")
|
||||
}
|
||||
@@ -61,6 +70,11 @@ func LoadReleaseVersions(path string) (ReleaseVersions, error) {
|
||||
return versions, nil
|
||||
}
|
||||
|
||||
func validReleaseVersion(value, game, component string) bool {
|
||||
match := releaseVersionPattern.FindStringSubmatch(value)
|
||||
return len(match) == 4 && match[1] == game && match[2] == component
|
||||
}
|
||||
|
||||
func ReleaseVersionsBesideExecutable() (ReleaseVersions, error) {
|
||||
executable, err := os.Executable()
|
||||
if err != nil {
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
func TestLoadReleaseVersions(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), ReleaseFileName)
|
||||
data := `{"client_version":"2.35.10","game_data_version":"20260923193640","bundle_version":"20260921135230","seed_directory":"go/seed/v2_35_10","plugins":{"local_identity":"0.6.0","capture_environment":"0.2.0","login_ui":"0.1.0"}}`
|
||||
data := `{"game_version":"2.35.10","client_version":"2.35.10+client.0.1.0","server_version":"2.35.10+server.0.1.0","game_data_version":"20260923193640","bundle_version":"20260921135230","seed_directory":"go/seed/v2_35_10","plugins":{"local_identity":"0.6.0","capture_environment":"0.2.0","login_ui":"0.1.0"}}`
|
||||
if err := os.WriteFile(path, []byte(data), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -16,15 +16,16 @@ func TestLoadReleaseVersions(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.ClientVersion != "2.35.10" || got.BundleVersion != "20260921135230" || got.GameDataVersion != "20260923193640" {
|
||||
if got.GameVersion != "2.35.10" || got.ClientVersion != "2.35.10+client.0.1.0" || got.ServerVersion != "2.35.10+server.0.1.0" || got.BundleVersion != "20260921135230" || got.GameDataVersion != "20260923193640" {
|
||||
t.Fatalf("versions=%+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadReleaseVersionsRejectsUnknownAndTrailingData(t *testing.T) {
|
||||
for name, data := range map[string]string{
|
||||
"unknown": `{"client_version":"2.35.10","unknown":true}`,
|
||||
"trailing": `{}` + `{}`,
|
||||
"unknown": `{"game_version":"2.35.10","unknown":true}`,
|
||||
"wrong-component": `{"game_version":"2.35.10","client_version":"2.35.10+server.0.1.0","server_version":"2.35.10+server.0.1.0"}`,
|
||||
"trailing": `{}` + `{}`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), ReleaseFileName)
|
||||
|
||||
@@ -69,8 +69,8 @@ func Inspect(gameDir string, versions clientconfig.ReleaseVersions) (GameStatus,
|
||||
if err != nil {
|
||||
return status, err
|
||||
}
|
||||
if status.ClientVersion != versions.ClientVersion {
|
||||
return status, fmt.Errorf("unsupported Brown Dust II client version %s; this bd2client release requires %s", status.ClientVersion, versions.ClientVersion)
|
||||
if status.ClientVersion != versions.GameVersion {
|
||||
return status, fmt.Errorf("unsupported Brown Dust II client version %s; this bd2client release requires %s", status.ClientVersion, versions.GameVersion)
|
||||
}
|
||||
if verified, verifyErr := introdb.VerifyClient(installation.Root); verifyErr == nil {
|
||||
status.PatchedURL = verified.URL
|
||||
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
|
||||
func testVersions() clientconfig.ReleaseVersions {
|
||||
return clientconfig.ReleaseVersions{
|
||||
ClientVersion: "2.35.10", BundleVersion: "20260921135230", GameDataVersion: "20260923193640",
|
||||
GameVersion: "2.35.10", ClientVersion: "2.35.10+client.0.1.0", ServerVersion: "2.35.10+server.0.1.0", BundleVersion: "20260921135230", GameDataVersion: "20260923193640",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user