feat(all): add verified gacha flows and native client studio
This commit is contained in:
@@ -3,17 +3,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
clientconfig "bd2server/internal/client/config"
|
||||
clientlayout "bd2server/internal/client/layout"
|
||||
)
|
||||
|
||||
type clientDevelopmentConfig struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
GameDirectory string `json:"game_directory"`
|
||||
}
|
||||
|
||||
func developmentRunOptions(args []string) ([]string, clientRunOptions, error) {
|
||||
if len(args) == 0 || args[0] != "--dev" {
|
||||
return args, clientRunOptions{}, nil
|
||||
@@ -31,10 +37,11 @@ func developmentRunOptions(args []string) ([]string, clientRunOptions, error) {
|
||||
return nil, clientRunOptions{}, err
|
||||
}
|
||||
if gameDir == "" {
|
||||
preferences, preferenceErr := clientconfig.LoadPreferences()
|
||||
if preferenceErr == nil {
|
||||
gameDir = preferences.GameDirectory
|
||||
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 {
|
||||
@@ -49,6 +56,35 @@ func developmentRunOptions(args []string) ([]string, clientRunOptions, error) {
|
||||
}, 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 {
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
@@ -15,7 +17,7 @@ func TestClientDevelopmentGameDirectory(t *testing.T) {
|
||||
}{
|
||||
{name: "separate", args: []string{"--game-dir", filepath.Join("some", "game")}, want: filepath.Join("some", "game")},
|
||||
{name: "equals", args: []string{"--game-dir=" + filepath.Join("other", "game")}, want: filepath.Join("other", "game")},
|
||||
{name: "absent", args: []string{"--no-browser"}},
|
||||
{name: "absent", args: nil},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
got, err := clientDevelopmentGameDirectory(test.args)
|
||||
@@ -37,14 +39,53 @@ func TestClientDevelopmentGameDirectoryRequiresValue(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevelopmentRunOptionsUsesRepositoryFiles(t *testing.T) {
|
||||
t.Setenv("APPDATA", t.TempDir())
|
||||
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
|
||||
args, options, err := developmentRunOptions([]string{"--dev", "run", "--no-browser"})
|
||||
func TestLoadClientDevelopmentConfig(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.json")
|
||||
gameDir := filepath.Join(dir, "BrownDust II")
|
||||
quoted, err := json.Marshal(gameDir)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(args) != 1 || args[0] != "--no-browser" {
|
||||
if err := os.WriteFile(path, []byte(`{"schema_version":1,"game_directory":`+string(quoted)+`}`), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config, err := loadClientDevelopmentConfig(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want, _ := filepath.Abs(gameDir)
|
||||
if config.SchemaVersion != 1 || config.GameDirectory != want {
|
||||
t.Fatalf("config=%+v want directory %q", config, want)
|
||||
}
|
||||
if _, err := loadClientDevelopmentConfig(filepath.Join(dir, "missing.json")); err == nil {
|
||||
t.Fatal("missing development config accepted")
|
||||
}
|
||||
for name, body := range map[string]string{
|
||||
"unknown": `{"schema_version":1,"game_directory":"x","extra":true}`,
|
||||
"version": `{"schema_version":2,"game_directory":"x"}`,
|
||||
"empty": `{"schema_version":1,"game_directory":""}`,
|
||||
"trailing": `{"schema_version":1,"game_directory":"x"}{}`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
bad := filepath.Join(dir, name+".json")
|
||||
if err := os.WriteFile(bad, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := loadClientDevelopmentConfig(bad); err == nil {
|
||||
t.Fatal("invalid development config accepted")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDevelopmentRunOptionsUsesRepositoryFiles(t *testing.T) {
|
||||
gameDir := t.TempDir()
|
||||
args, options, err := developmentRunOptions([]string{"--dev", "run", "--game-dir", gameDir})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(args) != 2 || args[0] != "--game-dir" || args[1] != gameDir {
|
||||
t.Fatalf("client args = %v", args)
|
||||
}
|
||||
for name, path := range map[string]string{
|
||||
|
||||
@@ -30,8 +30,6 @@ func main() {
|
||||
|
||||
func runClient(args []string, options clientRunOptions) error {
|
||||
fs := flag.NewFlagSet("bd2client", flag.ContinueOnError)
|
||||
listen := fs.String("listen", "127.0.0.1:0", "loopback address for the local setup interface")
|
||||
noBrowser := fs.Bool("no-browser", false, "print the interface URL without opening a browser")
|
||||
gameDir := fs.String("game-dir", "", "initial Brown Dust II installation directory")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
@@ -66,8 +64,6 @@ func runClient(args []string, options clientRunOptions) error {
|
||||
}
|
||||
}
|
||||
if err := clientapp.Run(clientapp.Options{
|
||||
Listen: *listen,
|
||||
NoBrowser: *noBrowser,
|
||||
InitialGameDir: *gameDir,
|
||||
Logger: logger,
|
||||
LogPath: logPath,
|
||||
|
||||
@@ -213,6 +213,10 @@ func serve(args []string) (serveErr error) {
|
||||
if err != nil {
|
||||
return fmt.Errorf("load active gacha GameData: %w", err)
|
||||
}
|
||||
firstGacha, err := gamedata.LoadFirstGacha(gameData, *gameDataVersion)
|
||||
if err != nil {
|
||||
return fmt.Errorf("load first gacha GameData: %w", err)
|
||||
}
|
||||
stateRepository, err := accountstate.Open(filepath.Clean(*stateFile))
|
||||
if err != nil {
|
||||
return fmt.Errorf("open account state database: %w", err)
|
||||
@@ -426,7 +430,12 @@ func serve(args []string) (serveErr error) {
|
||||
}
|
||||
// The mapped client property is IsDoneFirstGachaPick. Its authoritative
|
||||
// local state is the explicit GachaSubType=3 completion marker.
|
||||
login.SetFirstGacha(gachaService.FirstGachaCompleted())
|
||||
if err := login.AttachFirstGacha(gachaService); err != nil {
|
||||
return fmt.Errorf("attach first gacha status to login: %w", err)
|
||||
}
|
||||
if err := gachaService.AttachFirstGacha(firstGacha); err != nil {
|
||||
return fmt.Errorf("attach first gacha GameData: %w", err)
|
||||
}
|
||||
gachaService.AttachInventory(ownedItems)
|
||||
gachaService.AttachEquipmentGacha(equipmentGacha, ownedEquipment)
|
||||
gachaService.AttachPreviewMission(func() error {
|
||||
|
||||
Reference in New Issue
Block a user