diff --git a/.gitignore b/.gitignore index 04b5e9b..3581cfa 100644 --- a/.gitignore +++ b/.gitignore @@ -24,7 +24,6 @@ go/*.test go/coverage.out *.prof *.pprof -go/internal/statebridge/tool/bd2-state.exe plugins/**/bin/ plugins/**/obj/ TestResults/ diff --git a/README.md b/README.md index bde3a9f..944a06e 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,13 @@ $env:PYTHONDONTWRITEBYTECODE = '1' python -m unittest discover -s .\tools\python\tests -p 'test_*.py' -v ``` -## 单文件发布 +## 发布包 ```powershell -.\build-release.ps1 +.\build-release.ps1 -GameDir "<客户端目录>" ``` -脚本构建并测试 Haskell CLI,将它嵌入 Go 服务,最终只生成 `.build\bin\bd2server.exe`。运行时 helper 位于临时目录并在进程退出时清理,不需要分发第二个可执行文件。Go 构建缓存只写入 `go\.cache`。运行服务仍需项目的本地配置和资源;“单文件”指两个程序的发布产物合并为一个 exe,不代表把外部资源打进程序。 +脚本生成 `.build\bd2server-windows-x64.zip`。解压后得到一个完整目录,其中包含 Go 服务端、独立 Haskell 状态工具、本地身份插件、运行种子和空存档目录。Haskell 不再嵌入 Go 可执行文件。Go 构建缓存只写入 `go\.cache`。 ## 客户端插件项目 @@ -43,48 +43,39 @@ python -m unittest discover -s .\tools\python\tests -p 'test_*.py' -v ## 首次安装本地客户端 -先设置客户端目录,修改客户端入口,并构建、安装 `BD2LocalIdentity.dll`: +先手动安装 BepInEx:。然后修改客户端入口: ```powershell $clientDir = "<客户端目录>" -.\.build\bin\bd2server.exe patch-client ` +.\.build\package\bd2server\bd2server.exe patch-client ` --game-dir $clientDir -dotnet build .\plugins\LocalIdentity\LocalIdentity.csproj ` - -c Release ` - -p:GameDir="$clientDir" - -$pluginDir = Join-Path $clientDir 'BepInEx\plugins' -New-Item -ItemType Directory -Force $pluginDir | Out-Null -Copy-Item ` - .\plugins\LocalIdentity\bin\Release\netstandard2.1\BD2LocalIdentity.dll ` - (Join-Path $pluginDir 'BD2LocalIdentity.dll') ` - -Force ``` -客户端必须已经安装并加载 `BD2LocalIdentity.dll`,否则不能按本地服流程登录。不要把原版抓包插件 `BD2CaptureEnvironment.dll` 安装进这个客户端。 +启动服务端时会检查 BepInEx,并自动从发布目录安装或更新 `BD2LocalIdentity.dll`。如果未检测到 BepInEx,服务端只提示官方下载链接,不修改客户端,也不会继续启动。不要把原版抓包插件 `BD2CaptureEnvironment.dll` 安装进这个客户端。 ## 启动顺序 顺序必须是: -1. 确认 `BD2LocalIdentity.dll` 已安装在本地客户端的 `BepInEx\plugins`。 -2. 启动服务器。 +1. 确认客户端已经安装 BepInEx。 +2. 启动服务器;服务器自动同步 `BD2LocalIdentity.dll`。 3. 健康检查返回成功后,再启动游戏客户端。 启动服务器: ```powershell -Push-Location .\go -& ..\.build\bin\bd2server.exe serve ` +Push-Location .\.build\package\bd2server\go +& ..\bd2server.exe serve ` + --game-dir "<客户端目录>" ` --cdn "" ` --game-data "" ` --game-data-version "" Pop-Location ``` -启动时先核验资源,Go 再调用内嵌的 Haskell CLI 检查和修复存档;通过后加载账号与玩法服务,最后监听指定地址(默认 `127.0.0.1:8080`)。本地身份插件目前固定连接该默认地址;要更换地址,需要同步修改插件。 +启动时先检查 BepInEx 并同步插件,再核验资源;Go 随后调用同目录的 Haskell CLI 检查和修复存档,通过后加载账号与玩法服务,最后监听指定地址(默认 `127.0.0.1:8080`)。本地身份插件目前固定连接该默认地址;要更换地址,需要同步修改插件。 健康检查: @@ -99,6 +90,6 @@ Invoke-WebRequest http://127.0.0.1:8080/healthz 构建服务后运行: ```powershell -.\.build\bin\bd2server.exe patch-client --game-dir "<客户端目录>" --verify -.\.build\bin\bd2server.exe patch-client --game-dir "<客户端目录>" +.\.build\package\bd2server\bd2server.exe patch-client --game-dir "<客户端目录>" --verify +.\.build\package\bd2server\bd2server.exe patch-client --game-dir "<客户端目录>" ``` diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..57038cd --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,35 @@ +# BD2 本地服务器发布包 + +## 首次使用 + +1. 为游戏手动安装 BepInEx。官方下载: + +2. 修改本地客户端入口: + + ```powershell + .\bd2server.exe patch-client --game-dir "<客户端目录>" + ``` + +3. 进入发布包的 `go` 目录并启动服务器: + + ```powershell + Push-Location .\go + & ..\bd2server.exe serve ` + --game-dir "<客户端目录>" ` + --cdn "" ` + --game-data "" ` + --game-data-version "" + Pop-Location + ``` + +服务器会检查 BepInEx,并自动把包内 `plugins\BD2LocalIdentity.dll` 安装或更新到客户端。如果没有安装 BepInEx,服务器会给出上述下载地址、保持客户端不变并拒绝启动。 + +4. 确认健康检查成功: + + ```powershell + Invoke-WebRequest http://127.0.0.1:8080/healthz + ``` + +5. 最后启动游戏客户端。请勿在服务器启动前运行客户端。 + +`bd2-state.exe` 是服务端启动期使用的 Haskell 状态工具,必须与 `bd2server.exe` 保持在同一目录。 diff --git a/build-release.ps1 b/build-release.ps1 index dfba805..579aab4 100644 --- a/build-release.ps1 +++ b/build-release.ps1 @@ -1,17 +1,43 @@ [CmdletBinding()] -param([switch]$SkipTests) +param( + [Parameter(Mandatory)] + [string]$GameDir, + + [switch]$SkipTests +) $ErrorActionPreference = 'Stop' $root = $PSScriptRoot $goRoot = Join-Path $root 'go' $haskellRoot = Join-Path $root 'haskell' -$outputDir = Join-Path $root '.build\bin' -$embedDir = Join-Path $goRoot 'internal\statebridge\tool' -$embedTool = Join-Path $embedDir 'bd2-state.exe' +$localIdentityProject = Join-Path $root 'plugins\LocalIdentity\LocalIdentity.csproj' +$buildRoot = Join-Path $root '.build' +$packageParent = Join-Path $buildRoot 'package' +$packageDir = Join-Path $packageParent 'bd2server' +$packagePluginDir = Join-Path $packageDir 'plugins' +$packageGoDir = Join-Path $packageDir 'go' +$packageStateDir = Join-Path $packageDir 'data\state' +$archive = Join-Path $buildRoot 'bd2server-windows-x64.zip' + +$GameDir = [IO.Path]::GetFullPath($GameDir) +if (-not (Test-Path -LiteralPath (Join-Path $GameDir 'BrownDust II.exe') -PathType Leaf)) { + throw "GameDir does not contain BrownDust II.exe: $GameDir" +} $env:GOCACHE = Join-Path $goRoot '.cache\go-build' -New-Item -ItemType Directory -Force -Path $env:GOCACHE, $outputDir, $embedDir | Out-Null -if (Test-Path -LiteralPath $embedTool) { Remove-Item -LiteralPath $embedTool -Force } +New-Item -ItemType Directory -Force -Path $env:GOCACHE, $buildRoot | Out-Null + +foreach ($target in @($packageParent, $archive)) { + if (Test-Path -LiteralPath $target) { + $resolved = [IO.Path]::GetFullPath($target) + $expectedRoot = [IO.Path]::GetFullPath($buildRoot) + [IO.Path]::DirectorySeparatorChar + if (-not $resolved.StartsWith($expectedRoot, [StringComparison]::OrdinalIgnoreCase)) { + throw "Refusing to remove unexpected build path: $resolved" + } + Remove-Item -LiteralPath $resolved -Recurse -Force + } +} +New-Item -ItemType Directory -Force -Path $packageDir, $packagePluginDir, $packageGoDir, $packageStateDir | Out-Null Push-Location $root try { @@ -33,25 +59,35 @@ try { } $haskellTool = (cabal list-bin exe:bd2-state).Trim() if ($LASTEXITCODE -ne 0 -or -not (Test-Path -LiteralPath $haskellTool)) { - throw 'cannot locate the Cabal-built bd2-state executable' + throw 'Cannot locate the Cabal-built bd2-state executable.' } - Copy-Item -LiteralPath $haskellTool -Destination $embedTool -Force + Copy-Item -LiteralPath $haskellTool -Destination (Join-Path $packageDir 'bd2-state.exe') -Force } finally { Pop-Location } -try { - Push-Location $goRoot - try { - if (-not $SkipTests) { - go test ./... - if ($LASTEXITCODE -ne 0) { throw "go test failed with exit code $LASTEXITCODE" } - go vet ./... - if ($LASTEXITCODE -ne 0) { throw "go vet failed with exit code $LASTEXITCODE" } - } - $output = Join-Path $outputDir 'bd2server.exe' - go build -tags embedded_state_tool -trimpath -ldflags '-s -w' -o $output .\cmd\bd2server - if ($LASTEXITCODE -ne 0) { throw "go build failed with exit code $LASTEXITCODE" } - Write-Host "Built single-file release: $output" - } finally { Pop-Location } -} finally { - if (Test-Path -LiteralPath $embedTool) { Remove-Item -LiteralPath $embedTool -Force } +dotnet build $localIdentityProject -c Release "-p:GameDir=$GameDir" --nologo +if ($LASTEXITCODE -ne 0) { throw "LocalIdentity build failed with exit code $LASTEXITCODE" } +$identityPlugin = Join-Path $root 'plugins\LocalIdentity\bin\Release\netstandard2.1\BD2LocalIdentity.dll' +if (-not (Test-Path -LiteralPath $identityPlugin -PathType Leaf)) { + throw "LocalIdentity build output is missing: $identityPlugin" } +Copy-Item -LiteralPath $identityPlugin -Destination (Join-Path $packagePluginDir 'BD2LocalIdentity.dll') -Force + +Push-Location $goRoot +try { + if (-not $SkipTests) { + go test ./... + if ($LASTEXITCODE -ne 0) { throw "go test failed with exit code $LASTEXITCODE" } + go vet ./... + if ($LASTEXITCODE -ne 0) { throw "go vet failed with exit code $LASTEXITCODE" } + } + go build -trimpath -ldflags '-s -w' -o (Join-Path $packageDir 'bd2server.exe') .\cmd\bd2server + if ($LASTEXITCODE -ne 0) { throw "go build failed with exit code $LASTEXITCODE" } +} finally { Pop-Location } + +Copy-Item -LiteralPath (Join-Path $goRoot 'seed') -Destination (Join-Path $packageGoDir 'seed') -Recurse -Force +Copy-Item -LiteralPath (Join-Path $root 'RELEASE.md') -Destination (Join-Path $packageDir 'README.md') -Force +Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination (Join-Path $packageDir 'LICENSE') -Force + +Compress-Archive -LiteralPath $packageDir -DestinationPath $archive -CompressionLevel Optimal +Write-Host "Built release directory: $packageDir" +Write-Host "Built release archive: $archive" diff --git a/go/cmd/bd2server/main.go b/go/cmd/bd2server/main.go index 2d44d96..fbbcbcf 100644 --- a/go/cmd/bd2server/main.go +++ b/go/cmd/bd2server/main.go @@ -14,6 +14,7 @@ import ( "bd2server/internal/account" "bd2server/internal/battle" "bd2server/internal/bootstrap" + "bd2server/internal/clientplugin" "bd2server/internal/deck" "bd2server/internal/feature" "bd2server/internal/gacha" @@ -71,12 +72,27 @@ func serve(args []string) error { deckSeed := fs.String("deck-seed", `seed\v2_34_13\decks.json`, "versioned starter deck") deckState := fs.String("deck-state", `..\data\state\deck.json`, "local deck and waypoint save") worldSeed := fs.String("world-seed", `seed\v2_34_13\world.json`, "versioned starter world") - stateTool := fs.String("state-tool", "", "optional Haskell state tool override (release builds embed it)") + stateTool := fs.String("state-tool", "", "optional Haskell state tool override (release package includes it)") + gameDir := fs.String("game-dir", "", "Brown Dust II client directory (required)") + identityPlugin := fs.String("identity-plugin", "", "optional BD2LocalIdentity.dll override for development") if err := fs.Parse(args); err != nil { return err } - if *cdn == "" || *gameData == "" || *gameDataVersion == "" { - return errors.New("serve requires --cdn, --game-data, and --game-data-version") + if *cdn == "" || *gameData == "" || *gameDataVersion == "" || *gameDir == "" { + return errors.New("serve requires --game-dir, --cdn, --game-data, and --game-data-version") + } + packagedPlugin, err := clientplugin.ResolvePackaged(*identityPlugin) + if err != nil { + return err + } + pluginResult, err := clientplugin.Install(*gameDir, packagedPlugin) + if err != nil { + return err + } + if pluginResult.Changed { + slog.Info("installed local identity plugin", "path", pluginResult.Destination) + } else { + slog.Info("local identity plugin is current", "path", pluginResult.Destination) } base := "http://" + *listen + "/game/" cfg := bootstrap.Config{ @@ -337,7 +353,7 @@ func usage() { fmt.Fprintln(os.Stderr, `bd2server - BrownDust II 2.34.13 local development server Usage: - bd2server serve --cdn DIR --game-data DIR --game-data-version VERSION [options] + bd2server serve --game-dir DIR --cdn DIR --game-data DIR --game-data-version VERSION [options] bd2server patch-client --game-dir DIR [options] bd2server state check [options] bd2server state migrate-v1-v2 [options] diff --git a/go/internal/clientplugin/install.go b/go/internal/clientplugin/install.go new file mode 100644 index 0000000..a3b0942 --- /dev/null +++ b/go/internal/clientplugin/install.go @@ -0,0 +1,94 @@ +package clientplugin + +import ( + "bytes" + "crypto/sha256" + "errors" + "fmt" + "io" + "os" + "path/filepath" +) + +const ( + FileName = "BD2LocalIdentity.dll" + BepInExReleasesURL = "https://github.com/BepInEx/BepInEx/releases" +) + +type Result struct { + Destination string + Changed bool +} + +func ResolvePackaged(explicit string) (string, error) { + if explicit != "" { + return filepath.Clean(explicit), nil + } + executable, err := os.Executable() + if err != nil { + return "", fmt.Errorf("clientplugin: resolve server executable: %w", err) + } + return filepath.Join(filepath.Dir(executable), "plugins", FileName), nil +} + +// Install verifies that the user installed BepInEx, then atomically stages the +// packaged local-identity plugin into its plugins directory. It never installs +// or downloads BepInEx itself. +func Install(gameDir, source string) (Result, error) { + if gameDir == "" || source == "" { + return Result{}, errors.New("clientplugin: game directory and plugin source are required") + } + gameDir = filepath.Clean(gameDir) + source = filepath.Clean(source) + gameExecutable := filepath.Join(gameDir, "BrownDust II.exe") + if info, err := os.Stat(gameExecutable); err != nil || info.IsDir() { + return Result{}, fmt.Errorf("clientplugin: game executable is unavailable at %q", gameExecutable) + } + bepInEx := filepath.Join(gameDir, "BepInEx", "core", "BepInEx.dll") + if info, err := os.Stat(bepInEx); err != nil || info.IsDir() { + return Result{}, fmt.Errorf("clientplugin: BepInEx is not installed; install it manually from %s, then restart the server; %s was not copied", BepInExReleasesURL, FileName) + } + sourceData, err := os.ReadFile(source) + if err != nil { + return Result{}, fmt.Errorf("clientplugin: read packaged %s: %w", FileName, err) + } + if len(sourceData) == 0 { + return Result{}, fmt.Errorf("clientplugin: packaged %s is empty", FileName) + } + pluginDir := filepath.Join(gameDir, "BepInEx", "plugins") + destination := filepath.Join(pluginDir, FileName) + if installed, err := os.ReadFile(destination); err == nil { + if bytes.Equal(hash(installed), hash(sourceData)) { + return Result{Destination: destination}, nil + } + } else if !errors.Is(err, os.ErrNotExist) { + return Result{}, fmt.Errorf("clientplugin: inspect installed plugin: %w", err) + } + if err := os.MkdirAll(pluginDir, 0o755); err != nil { + return Result{}, fmt.Errorf("clientplugin: create plugin directory: %w", err) + } + temporary, err := os.CreateTemp(pluginDir, ".BD2LocalIdentity-*.tmp") + if err != nil { + return Result{}, fmt.Errorf("clientplugin: create temporary plugin: %w", err) + } + temporaryPath := temporary.Name() + defer os.Remove(temporaryPath) + if _, err = io.Copy(temporary, bytes.NewReader(sourceData)); err == nil { + err = temporary.Sync() + } + if closeErr := temporary.Close(); err == nil { + err = closeErr + } + if err != nil { + return Result{}, fmt.Errorf("clientplugin: stage plugin: %w", err) + } + if err := replaceFile(temporaryPath, destination); err != nil { + return Result{}, fmt.Errorf("clientplugin: install plugin (close the game client first): %w", err) + } + return Result{Destination: destination, Changed: true}, nil +} + +func hash(data []byte) []byte { + sum := sha256.Sum256(data) + return sum[:] +} diff --git a/go/internal/clientplugin/install_test.go b/go/internal/clientplugin/install_test.go new file mode 100644 index 0000000..fda6077 --- /dev/null +++ b/go/internal/clientplugin/install_test.go @@ -0,0 +1,64 @@ +package clientplugin + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestInstallRequiresBepInExWithoutCopyingPlugin(t *testing.T) { + gameDir := t.TempDir() + if err := os.WriteFile(filepath.Join(gameDir, "BrownDust II.exe"), []byte("game"), 0o600); err != nil { + t.Fatal(err) + } + source := filepath.Join(t.TempDir(), FileName) + if err := os.WriteFile(source, []byte("plugin"), 0o600); err != nil { + t.Fatal(err) + } + _, err := Install(gameDir, source) + if err == nil || !strings.Contains(err.Error(), BepInExReleasesURL) { + t.Fatalf("missing BepInEx error=%v", err) + } + if _, statErr := os.Stat(filepath.Join(gameDir, "BepInEx", "plugins", FileName)); !os.IsNotExist(statErr) { + t.Fatalf("plugin was copied without BepInEx: %v", statErr) + } +} + +func TestInstallCopiesUpdatesAndSkipsIdenticalPlugin(t *testing.T) { + gameDir := t.TempDir() + for path, data := range map[string][]byte{ + filepath.Join(gameDir, "BrownDust II.exe"): []byte("game"), + filepath.Join(gameDir, "BepInEx", "core", "BepInEx.dll"): []byte("bepinex"), + } { + if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(path, data, 0o600); err != nil { + t.Fatal(err) + } + } + source := filepath.Join(t.TempDir(), FileName) + if err := os.WriteFile(source, []byte("v1"), 0o600); err != nil { + t.Fatal(err) + } + first, err := Install(gameDir, source) + if err != nil || !first.Changed { + t.Fatalf("first install=%+v err=%v", first, err) + } + second, err := Install(gameDir, source) + if err != nil || second.Changed { + t.Fatalf("idempotent install=%+v err=%v", second, err) + } + if err := os.WriteFile(source, []byte("v2"), 0o600); err != nil { + t.Fatal(err) + } + third, err := Install(gameDir, source) + if err != nil || !third.Changed { + t.Fatalf("update=%+v err=%v", third, err) + } + got, err := os.ReadFile(third.Destination) + if err != nil || string(got) != "v2" { + t.Fatalf("installed=%q err=%v", got, err) + } +} diff --git a/go/internal/clientplugin/replace_other.go b/go/internal/clientplugin/replace_other.go new file mode 100644 index 0000000..0f064ed --- /dev/null +++ b/go/internal/clientplugin/replace_other.go @@ -0,0 +1,9 @@ +//go:build !windows + +package clientplugin + +import "os" + +func replaceFile(source, destination string) error { + return os.Rename(source, destination) +} diff --git a/go/internal/clientplugin/replace_windows.go b/go/internal/clientplugin/replace_windows.go new file mode 100644 index 0000000..3324733 --- /dev/null +++ b/go/internal/clientplugin/replace_windows.go @@ -0,0 +1,34 @@ +//go:build windows + +package clientplugin + +import ( + "os" + "syscall" + "unsafe" +) + +var moveFileEx = syscall.NewLazyDLL("kernel32.dll").NewProc("MoveFileExW") + +func replaceFile(source, destination string) error { + sourcePtr, err := syscall.UTF16PtrFromString(source) + if err != nil { + return err + } + destinationPtr, err := syscall.UTF16PtrFromString(destination) + if err != nil { + return err + } + result, _, callErr := moveFileEx.Call( + uintptr(unsafe.Pointer(sourcePtr)), + uintptr(unsafe.Pointer(destinationPtr)), + 0x1|0x8, // MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH + ) + if result == 0 { + if callErr != syscall.Errno(0) { + return callErr + } + return os.ErrInvalid + } + return nil +} diff --git a/go/internal/statebridge/tool.go b/go/internal/statebridge/tool.go index dc998fc..2184de8 100644 --- a/go/internal/statebridge/tool.go +++ b/go/internal/statebridge/tool.go @@ -6,8 +6,8 @@ import ( "path/filepath" ) -// ResolveTool prefers an explicit development override. Release builds embed -// the Haskell executable and materialize it into a private temporary directory. +// ResolveTool prefers an explicit development override. Release packages place +// bd2-state.exe beside bd2server.exe so both components remain inspectable. func ResolveTool(explicit string) (string, func(), error) { if explicit != "" { return filepath.Clean(explicit), func() {}, nil @@ -15,16 +15,11 @@ func ResolveTool(explicit string) (string, func(), error) { if env := os.Getenv("BD2_STATE_TOOL"); env != "" { return filepath.Clean(env), func() {}, nil } - if path, cleanup, ok, err := embeddedStateTool(); err != nil { - return "", func() {}, err - } else if ok { - return path, cleanup, nil - } if executable, err := os.Executable(); err == nil { sibling := filepath.Join(filepath.Dir(executable), "bd2-state.exe") if info, statErr := os.Stat(sibling); statErr == nil && !info.IsDir() { return sibling, func() {}, nil } } - return "", func() {}, errors.New("statebridge: no Haskell state tool; use --state-tool/--tool or build the embedded release") + return "", func() {}, errors.New("statebridge: bd2-state.exe is missing beside bd2server.exe; use --state-tool/--tool for a development build") } diff --git a/go/internal/statebridge/tool_embedded.go b/go/internal/statebridge/tool_embedded.go deleted file mode 100644 index ea4058c..0000000 --- a/go/internal/statebridge/tool_embedded.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build embedded_state_tool - -package statebridge - -import ( - "crypto/sha256" - _ "embed" - "encoding/hex" - "os" - "path/filepath" -) - -//go:embed tool/bd2-state.exe -var embeddedStateToolBytes []byte - -func embeddedStateTool() (string, func(), bool, error) { - sum := sha256.Sum256(embeddedStateToolBytes) - dir, err := os.MkdirTemp("", "bd2-state-"+hex.EncodeToString(sum[:6])+"-") - if err != nil { - return "", func() {}, true, err - } - cleanup := func() { _ = os.RemoveAll(dir) } - path := filepath.Join(dir, "bd2-state.exe") - if err := os.WriteFile(path, embeddedStateToolBytes, 0o700); err != nil { - cleanup() - return "", func() {}, true, err - } - return path, cleanup, true, nil -} diff --git a/go/internal/statebridge/tool_external.go b/go/internal/statebridge/tool_external.go deleted file mode 100644 index 6970d77..0000000 --- a/go/internal/statebridge/tool_external.go +++ /dev/null @@ -1,7 +0,0 @@ -//go:build !embedded_state_tool - -package statebridge - -func embeddedStateTool() (string, func(), bool, error) { - return "", func() {}, false, nil -}