Files
bd2/go/cmd/bd2client/dev_command.go
T
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata.

Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks.

Improve automatic login and recovery diagnostics; document configuration and release flows.
2026-10-05 11:50:31 +08:00

209 lines
7.2 KiB
Go

//go:build !release
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"strings"
clientlayout "bd2server/internal/client/layout"
)
type clientDevelopmentConfig struct {
SchemaVersion int `json:"schema_version"`
GameDirectory string `json:"game_directory"`
}
var runDevelopmentChild = func(root string, args []string) error {
command := exec.Command("go", args...)
command.Dir = filepath.Join(root, "go")
command.Stdin = os.Stdin
command.Stdout = os.Stdout
command.Stderr = os.Stderr
return command.Run()
}
// relaunchDevelopmentIfNeeded preserves the documented plain `go run`
// command. Wails selects its real desktop backend with the `dev` or
// `production` build tag; without either it deliberately links a diagnostic
// placeholder. The outer process therefore rebuilds itself once with
// `production`, retaining the embedded frontend and normal native host while
// the project-level --dev option supplies development paths and plugin builds.
func relaunchDevelopmentIfNeeded(args []string) (bool, error) {
if len(args) == 0 || args[0] != "--dev" || wailsDevelopmentBuild {
return false, nil
}
if len(args) < 2 || args[1] != "run" {
return false, nil
}
root, err := findClientDevelopmentRoot()
if err != nil {
return false, err
}
childArgs := []string{"run", "-tags", "production", "./cmd/bd2client", "--dev", "run"}
childArgs = append(childArgs, args[2:]...)
if err := runDevelopmentChild(root, childArgs); err != nil {
return false, fmt.Errorf("run Wails development client: %w", err)
}
return true, nil
}
func developmentRunOptions(args []string) ([]string, clientRunOptions, error) {
if len(args) == 0 || args[0] != "--dev" {
return args, clientRunOptions{}, nil
}
if len(args) < 2 || args[1] != "run" {
return nil, clientRunOptions{}, errors.New("usage: bd2client --dev run [client options]")
}
root, err := findClientDevelopmentRoot()
if err != nil {
return nil, clientRunOptions{}, err
}
clientArgs := append([]string(nil), args[2:]...)
gameDir, err := clientDevelopmentGameDirectory(clientArgs)
if err != nil {
return nil, clientRunOptions{}, err
}
if gameDir == "" {
config, configErr := loadClientDevelopmentConfig(filepath.Join(root, "go", "config.json"))
if configErr != nil {
return nil, clientRunOptions{}, configErr
}
gameDir = config.GameDirectory
}
if gameDir != "" {
if err := buildDevelopmentPlugins(root, gameDir); err != nil {
return nil, clientRunOptions{}, err
}
}
return clientArgs, clientRunOptions{
versionConfigPath: filepath.Join(root, "versions.json"),
logExecutablePath: filepath.Join(root, "data", "bd2client-dev"),
localIdentityPlugin: filepath.Join(root, "plugins", "LocalIdentity", "bin", "Release", "netstandard2.1", "BD2LocalIdentity.dll"),
loginUIPlugin: filepath.Join(root, "plugins", "LoginUI", "bin", "Release", "netstandard2.1", "BD2LoginUI.dll"),
cashShopPlugin: filepath.Join(root, "plugins", "CashShop", "bin", "Release", "netstandard2.1", "BD2CashShop.dll"),
}, nil
}
func loadClientDevelopmentConfig(path string) (clientDevelopmentConfig, error) {
data, err := os.ReadFile(filepath.Clean(path))
if errors.Is(err, os.ErrNotExist) {
return clientDevelopmentConfig{}, fmt.Errorf("development config %s is missing; copy go/config.example.json to go/config.json or pass --game-dir", path)
}
if err != nil {
return clientDevelopmentConfig{}, fmt.Errorf("read development config: %w", err)
}
decoder := json.NewDecoder(strings.NewReader(string(data)))
decoder.DisallowUnknownFields()
var config clientDevelopmentConfig
if err := decoder.Decode(&config); err != nil {
return clientDevelopmentConfig{}, fmt.Errorf("decode development config: %w", err)
}
var trailing any
if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
return clientDevelopmentConfig{}, errors.New("development config must contain exactly one JSON object")
}
if config.SchemaVersion != 1 || strings.TrimSpace(config.GameDirectory) == "" {
return clientDevelopmentConfig{}, errors.New("development config requires schema_version 1 and game_directory")
}
absolute, err := filepath.Abs(filepath.Clean(config.GameDirectory))
if err != nil {
return clientDevelopmentConfig{}, fmt.Errorf("resolve development game directory: %w", err)
}
config.GameDirectory = absolute
return config, nil
}
func findClientDevelopmentRoot() (string, error) {
working, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("resolve development working directory: %w", err)
}
for directory := filepath.Clean(working); ; directory = filepath.Dir(directory) {
if clientDevelopmentFile(filepath.Join(directory, "versions.json")) &&
clientDevelopmentFile(filepath.Join(directory, "go", "go.mod")) &&
clientDevelopmentFile(filepath.Join(directory, "plugins", "LocalIdentity", "LocalIdentity.csproj")) &&
clientDevelopmentFile(filepath.Join(directory, "plugins", "LoginUI", "LoginUI.csproj")) {
return directory, nil
}
parent := filepath.Dir(directory)
if parent == directory {
break
}
}
return "", errors.New("development repository root not found; run from the bd2 repository")
}
func clientDevelopmentFile(path string) bool {
info, err := os.Stat(path)
return err == nil && info.Mode().IsRegular()
}
func clientDevelopmentGameDirectory(args []string) (string, error) {
for index := 0; index < len(args); index++ {
arg := args[index]
if arg == "--game-dir" {
if index+1 >= len(args) || strings.TrimSpace(args[index+1]) == "" {
return "", errors.New("--game-dir requires a directory")
}
return filepath.Clean(args[index+1]), nil
}
if strings.HasPrefix(arg, "--game-dir=") {
value := strings.TrimSpace(strings.TrimPrefix(arg, "--game-dir="))
if value == "" {
return "", errors.New("--game-dir requires a directory")
}
return filepath.Clean(value), nil
}
}
return "", nil
}
func buildDevelopmentPlugins(root, gameDir string) error {
installation, err := clientlayout.Resolve(gameDir)
if err != nil {
return nil
}
if !clientDevelopmentFile(filepath.Join(installation.BepInEx, "core", "BepInEx.dll")) {
return nil
}
projects := []string{
filepath.Join(root, "plugins", "LocalIdentity", "LocalIdentity.csproj"),
filepath.Join(root, "plugins", "LoginUI", "LoginUI.csproj"),
filepath.Join(root, "plugins", "CashShop", "CashShop.csproj"),
}
for _, project := range projects {
fmt.Fprintf(os.Stderr, "bd2client: preparing development plugin %s; first-time game source generation may take several minutes\n", filepath.Base(project))
command := exec.Command(
"dotnet", "build", project, "-c", "Release",
"-p:GameDir="+filepath.Clean(gameDir),
"-p:BD2ManagedDir="+filepath.Join(installation.Data, "Managed"),
"-p:BD2BepInExDir="+installation.BepInEx,
"--nologo",
)
command.Dir = root
// SDK preparation can decompile the entire game before the window opens.
// Stream progress immediately, while retaining diagnostics for failures.
var output bytes.Buffer
writer := io.MultiWriter(os.Stderr, &output)
command.Stdout = writer
command.Stderr = writer
err := command.Run()
if err != nil {
message := strings.TrimSpace(output.String())
if message == "" {
message = err.Error()
}
return fmt.Errorf("build development plugin %s: %s", filepath.Base(project), message)
}
}
return nil
}