chore(all): resolve lint findings, update example plugin, refine docs and add LGPL licenses

This commit is contained in:
2026-10-07 00:57:35 +08:00
parent e8e9d77974
commit 8e8ddeddb6
388 changed files with 4638 additions and 3321 deletions
+2
View File
@@ -13,3 +13,5 @@ Commit messages must follow this format:
- `<description>` must be **concise but complete**, clearly covering **all changes made in the commit**. Do not omit important changes for the sake of brevity.
Before pushing, run the code checks for each language you modified.
Not every lint suggestion needs to be applied. Evaluate each finding, and suppress its warning locally if the code is intentionally left unchanged after review.
+12
View File
@@ -32,6 +32,18 @@
默认执行测试与静态检查;`-SkipTests` 跳过这些检查;`-SchedulesOnly` 只导出活动日历编排信息。
## 其他命令
在仓库根目录运行:
```powershell
.\bd2w check-csharp
.\bd2w check-csharp LoginUI GameNames
.\bd2w sdk pack --game-dir '<客户端目录>'
.\bd2w sdk verify --game-dir '<客户端目录>'
.\bd2w sdk update-names --game-dir '<客户端目录>' --game-mapping '<映射文件>'
```
## 客户端插件项目
- `plugins/LocalIdentity/`:本地服务端客户端专用插件。
+49 -9
View File
@@ -1,22 +1,62 @@
#!/bin/sh
set -eu
root=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd -P)
case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) suffix=.exe ;; *) suffix= ;; esac
tool_dir="$root/.build/tools"
tool="$tool_dir/bd2w$suffix"
# Bootstrap only when the cached executable is absent. Build/run live in Go.
if [ ! -f "$tool" ]; then
if [ "${1-}" = check-csharp ]; then
shift
(
cd "$root"
if [ "$#" -eq 0 ]; then
set -- GameSdk GameNames LocalIdentity LoginUI CashShop CaptureEnvironment
fi
for project do
case "$project" in
GameSdk|GameNames|LocalIdentity|LoginUI|CashShop|CaptureEnvironment) ;;
*) printf 'bd2w: unknown C# project: %s\n' "$project" >&2; exit 1 ;;
esac
done
Configuration=Release
export Configuration
failed=0
for project do
printf 'Checking %s\n' "$project"
project_file="$root/plugins/$project/$project.csproj"
if ! dotnet build "$project_file" -c Release -t:Rebuild --nologo -warnaserror; then
failed=1
continue
fi
if ! dotnet format style "$project_file" --verify-no-changes --severity info --no-restore; then
failed=1
fi
done
exit "$failed"
)
exit $?
fi
tool_dir="$root/.build/tools"
tool="$tool_dir/bd2w"
tool_is_current() {
[ -f "$tool" ] || return 1
newer_sources=$(find "$root/go/build" "$root/go/go.mod" "$root/go/go.sum" -type f -newer "$tool" -print) || {
printf 'bd2w: cannot check build tool source timestamps.\n' >&2
exit 1
}
[ -z "$newer_sources" ]
}
if ! tool_is_current; then
mkdir -p "$tool_dir"
lock="$tool_dir/.bootstrap.lock"
attempts=0
while [ ! -f "$tool" ]; do
while ! tool_is_current; do
if mkdir "$lock" 2>/dev/null; then
temporary="$tool_dir/.bd2w-$$$suffix"
temporary="$tool_dir/.bd2w-$$"
trap 'rm -f "$temporary"; rmdir "$lock" 2>/dev/null || true' 0
trap 'exit 130' INT TERM
if [ ! -f "$tool" ]; then
printf 'bd2w: compiling the build tool for first use...\n'
if ! tool_is_current; then
printf 'bd2w: compiling the build tool...\n'
(
cd "$root/go"
GOCACHE="$root/go/.cache/go-build"
+67 -23
View File
@@ -1,43 +1,33 @@
@echo off
setlocal DisableDelayedExpansion
set "BD2W_ROOT=%~dp0"
if /I "%~1"=="check-csharp" goto check_csharp
set "BD2W_TOOL_DIR=%BD2W_ROOT%.build\tools"
set "BD2W_TOOL=%BD2W_TOOL_DIR%\bd2w.exe"
if exist "%BD2W_TOOL%" goto run
if not exist "%BD2W_TOOL_DIR%" mkdir "%BD2W_TOOL_DIR%"
if errorlevel 1 exit /b 1
if not exist "%BD2W_TOOL_DIR%" (
mkdir "%BD2W_TOOL_DIR%"
if errorlevel 1 exit /b 1
)
set "BD2W_LOCK=%BD2W_TOOL_DIR%\.bootstrap.lock"
set "BD2W_ATTEMPTS=0"
:lock
if exist "%BD2W_TOOL%" goto run
mkdir "%BD2W_LOCK%" 2>nul
if errorlevel 1 goto wait
set "BD2W_TEMP=%BD2W_TOOL_DIR%\.bd2w-%RANDOM%-%RANDOM%.exe"
if exist "%BD2W_TOOL%" goto unlock
pushd "%BD2W_ROOT%go"
call :compile_tool
if errorlevel 1 goto failed
set "BD2W_PUSHED=1"
echo bd2w: compiling the build tool for first use...
set "BD2W_PREVIOUS_CGO=%CGO_ENABLED%"
set "GOCACHE=%BD2W_ROOT%go\.cache\go-build"
set "GOOS="
set "GOARCH="
for /f "delims=" %%A in ('go env GOHOSTOS') do set "GOOS=%%A"
for /f "delims=" %%A in ('go env GOHOSTARCH') do set "GOARCH=%%A"
if not defined GOOS goto failed
if not defined GOARCH goto failed
set "CGO_ENABLED=0"
go build -buildvcs=false -trimpath -o "%BD2W_TEMP%" ./build
if not exist "%BD2W_TOOL%" goto replace_tool
fc /b "%BD2W_TEMP%" "%BD2W_TOOL%" >nul 2>nul
if errorlevel 1 goto replace_tool
del /q "%BD2W_TEMP%"
if errorlevel 1 goto failed
goto unlock
:replace_tool
move /y "%BD2W_TEMP%" "%BD2W_TOOL%" >nul
if errorlevel 1 goto failed
:unlock
if defined BD2W_PUSHED set "CGO_ENABLED=%BD2W_PREVIOUS_CGO%"
if defined BD2W_PUSHED popd
rmdir "%BD2W_LOCK%"
if errorlevel 1 exit /b 1
:run
"%BD2W_TOOL%" %*
exit /b %errorlevel%
@@ -51,8 +41,62 @@ ping -n 2 127.0.0.1 >nul
goto lock
:failed
if defined BD2W_PUSHED popd
if defined BD2W_TEMP if exist "%BD2W_TEMP%" del /q "%BD2W_TEMP%"
rmdir "%BD2W_LOCK%"
echo bd2w: Build tool compilation failed. 1>&2
exit /b 1
:compile_tool
setlocal
pushd "%BD2W_ROOT%go"
if errorlevel 1 exit /b 1
set "GOCACHE=%BD2W_ROOT%go\.cache\go-build"
set "GOOS="
set "GOARCH="
for /f "delims=" %%A in ('go env GOHOSTOS') do set "GOOS=%%A"
for /f "delims=" %%A in ('go env GOHOSTARCH') do set "GOARCH=%%A"
if not defined GOOS goto compile_failed
if not defined GOARCH goto compile_failed
set "CGO_ENABLED=0"
go build -buildvcs=false -trimpath -o "%BD2W_TEMP%" ./build
if errorlevel 1 goto compile_failed
popd
exit /b 0
:compile_failed
popd
exit /b 1
:check_csharp
shift
set "BD2W_PROJECTS="
:check_arguments
if "%~1"=="" goto check_start
set "BD2W_PROJECT="
for %%P in (GameSdk GameNames LocalIdentity LoginUI CashShop CaptureEnvironment) do if "%~1"=="%%P" set "BD2W_PROJECT=%%P"
if not defined BD2W_PROJECT (
echo bd2w: Unknown C# project: "%~1" 1>&2
exit /b 1
)
set "BD2W_PROJECTS=%BD2W_PROJECTS% %BD2W_PROJECT%"
shift
goto check_arguments
:check_start
if not defined BD2W_PROJECTS set "BD2W_PROJECTS=GameSdk GameNames LocalIdentity LoginUI CashShop CaptureEnvironment"
pushd "%BD2W_ROOT%"
if errorlevel 1 exit /b 1
set "Configuration=Release"
set "BD2W_FAILED=0"
for %%P in (%BD2W_PROJECTS%) do call :check_project %%P
popd
exit /b %BD2W_FAILED%
:check_project
echo Checking %~1
call dotnet build "%BD2W_ROOT%plugins\%~1\%~1.csproj" -c Release -t:Rebuild --nologo -warnaserror
if errorlevel 1 (
set "BD2W_FAILED=1"
exit /b 0
)
call dotnet format style "%BD2W_ROOT%plugins\%~1\%~1.csproj" --verify-no-changes --severity info --no-restore
if errorlevel 1 set "BD2W_FAILED=1"
exit /b 0
+26 -507
View File
@@ -1,43 +1,13 @@
// Command build is the shared implementation behind the repository wrappers.
// It uses only the standard library and caches no game or account state.
package main
import (
"archive/zip"
"bytes"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"runtime"
"sort"
"strings"
)
type buildOptions struct {
gameDir string
skipTests, schedulesOnly bool
}
type releaseVersions struct {
Game string `json:"game_version"`
Server string `json:"server_version"`
Client string `json:"client_version"`
}
type target struct{ platform, goos, architecture, goarch, goarm string }
type task struct {
root string
target target
}
func main() {
if err := run(os.Args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "bd2w:", err)
@@ -54,11 +24,16 @@ func usage() {
bd2w build [-GameDir directory] [-SkipTests] [-SchedulesOnly]
bd2w runClient [client options]
bd2w runServer [server options]
bd2w check-csharp [GameSdk GameNames LocalIdentity LoginUI CashShop CaptureEnvironment]
bd2w sdk pack [--game-dir directory] [--package-version version] [--output-directory directory]
bd2w sdk verify [--game-dir directory] [--package-version version] [--package-directory directory]
bd2w sdk update-names --game-mapping file [--game-dir directory] [--version-config file]
bd2w version-source --config file --output file --plugin name
Windows: .\bd2w build
Linux/macOS: ./bd2w build
Build automatically selects the native platform and architecture.
The wrappers compile go/build/build.go only when their cached executable is absent.`)
The wrappers cache the build tool and refresh it when its sources change.`)
}
func run(args []string) error {
@@ -66,10 +41,10 @@ func run(args []string) error {
usage()
return nil
}
if args[0] != "build" && args[0] != "runClient" && args[0] != "runServer" {
return fmt.Errorf("unknown task %q; use build, runClient or runServer", args[0])
if args[0] != "build" && args[0] != "runClient" && args[0] != "runServer" && args[0] != "sdk" && args[0] != "version-source" {
return fmt.Errorf("unknown task %q; use --help for available commands", args[0])
}
if args[0] == "build" && len(args) == 2 && isHelp(args[1]) {
if len(args) == 2 && isHelp(args[1]) {
usage()
return nil
}
@@ -87,6 +62,23 @@ func run(args []string) error {
return t.command("go", append([]string{"run", "-tags", "production", "./cmd/bd2client", "--dev", "run"}, args[1:]...)...)
case "runServer":
return t.command("go", append([]string{"run", "./cmd/bd2server", "--dev", "run"}, args[1:]...)...)
case "version-source":
return t.generateVersionSource(args[1:])
case "sdk":
if len(args) < 2 || isHelp(args[1]) || (len(args) == 3 && isHelp(args[2])) {
usage()
return nil
}
switch args[1] {
case "pack":
return t.sdkPack(args[2:])
case "verify":
return t.sdkVerify(args[2:])
case "update-names":
return t.sdkUpdateNames(args[2:])
default:
return fmt.Errorf("unknown SDK task %q; use sdk pack, verify or update-names", args[1])
}
default:
opts, err := parseOptions(args[1:])
if err != nil {
@@ -99,476 +91,3 @@ func run(args []string) error {
func isHelp(s string) bool {
return s == "help" || s == "-h" || s == "--help" || strings.EqualFold(s, "-Help")
}
func repositoryRoot() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
for dir := filepath.Dir(exe); ; dir = filepath.Dir(dir) {
if regular(filepath.Join(dir, "versions.json")) && regular(filepath.Join(dir, "go", "go.mod")) {
return dir, nil
}
if filepath.Dir(dir) == dir {
break
}
}
return "", errors.New("repository root not found beside the cached build tool; launch through bd2w")
}
func nativeTarget() (target, error) {
arch := runtime.GOARCH
if runtime.GOOS == "windows" {
machine := os.Getenv("PROCESSOR_ARCHITEW6432")
if machine == "" {
machine = os.Getenv("PROCESSOR_ARCHITECTURE")
}
switch strings.ToLower(machine) {
case "amd64":
arch = "amd64"
case "arm64":
arch = "arm64"
case "x86":
arch = "386"
}
} else {
if out, err := exec.Command("uname", "-m").Output(); err == nil {
arch = strings.TrimSpace(string(out))
}
}
return platformTarget(runtime.GOOS, arch)
}
func platformTarget(goos, arch string) (target, error) {
t := target{goos: goos}
switch goos {
case "windows", "linux":
t.platform = goos
case "darwin":
t.platform = "macos"
default:
return t, fmt.Errorf("unsupported platform %q", goos)
}
switch strings.ToLower(arch) {
case "amd64", "x86_64", "x64":
t.architecture = "x64"
t.goarch = "amd64"
case "aarch64", "arm64":
t.architecture = "arm64"
t.goarch = "arm64"
case "386", "i386", "i486", "i586", "i686", "x86":
t.architecture = "x86"
t.goarch = "386"
case "arm", "armv7", "armv7l":
t.architecture = "armv7"
t.goarch = "arm"
t.goarm = "7"
default:
return t, fmt.Errorf("unsupported architecture %q", arch)
}
return t, nil
}
func parseOptions(args []string) (buildOptions, error) {
var opts buildOptions
for i := 0; i < len(args); i++ {
name, value, assigned := strings.Cut(args[i], "=")
switch strings.ToLower(name) {
case "-gamedir", "--game-dir":
if !assigned {
i++
if i >= len(args) {
return opts, errors.New("-GameDir requires a directory")
}
value = args[i]
}
if strings.TrimSpace(value) == "" {
return opts, errors.New("-GameDir requires a directory")
}
opts.gameDir = value
case "-skiptests", "--skip-tests":
if assigned {
return opts, fmt.Errorf("unexpected value for %s", name)
}
opts.skipTests = true
case "-schedulesonly", "--schedules-only":
if assigned {
return opts, fmt.Errorf("unexpected value for %s", name)
}
opts.schedulesOnly = true
default:
return opts, fmt.Errorf("unknown build option %q", args[i])
}
}
return opts, nil
}
// Every child receives a process-local native target and repository-local cache.
func (t task) command(program string, args ...string) error {
cmd := exec.Command(program, args...)
cmd.Dir = filepath.Join(t.root, "go")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = replaceEnvironment(os.Environ(), map[string]string{"GOCACHE": filepath.Join(t.root, "go", ".cache", "go-build"), "GOOS": t.target.goos, "GOARCH": t.target.goarch, "GOARM": t.target.goarm})
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s %s: %w", program, strings.Join(args, " "), err)
}
return nil
}
func replaceEnvironment(env []string, values map[string]string) []string {
out := make([]string, 0, len(env)+len(values))
for _, entry := range env {
key, _, _ := strings.Cut(entry, "=")
remove := false
for wanted := range values {
if strings.EqualFold(key, wanted) {
remove = true
break
}
}
if !remove {
out = append(out, entry)
}
}
for key, value := range values {
if value != "" {
out = append(out, key+"="+value)
}
}
return out
}
func readJSON(path string, v any, strict bool) error {
raw, err := os.ReadFile(path)
if err != nil {
return err
}
d := json.NewDecoder(bytes.NewReader(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf})))
if strict {
d.DisallowUnknownFields()
}
if err = d.Decode(v); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
if err = d.Decode(new(any)); err != io.EOF {
return fmt.Errorf("%s must contain one JSON object", path)
}
return nil
}
func (t task) versions() (releaseVersions, error) {
var v releaseVersions
if err := readJSON(filepath.Join(t.root, "versions.json"), &v, false); err != nil {
return v, err
}
valid := regexp.MustCompile(`^[A-Za-z0-9.+_-]+$`)
for _, s := range []string{v.Game, v.Server, v.Client} {
if !valid.MatchString(s) {
return v, fmt.Errorf("invalid release version %q", s)
}
}
return v, nil
}
func (t task) schedules(gameVersion string) ([]string, string, error) {
files, err := filepath.Glob(filepath.Join(t.root, "schedules", "*.bd2schedule"))
if err != nil {
return nil, "", err
}
if len(files) == 0 {
return nil, "", errors.New("no project schedule files found")
}
sort.Slice(files, func(i, j int) bool {
return strings.ToLower(filepath.Base(files[i])) < strings.ToLower(filepath.Base(files[j]))
})
var lines []string
for _, file := range files {
raw, err := os.ReadFile(file)
if err != nil {
return nil, "", err
}
lines = append(lines, fmt.Sprintf("%s:%X", filepath.Base(file), sha256.Sum256(raw)))
}
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(lines, "\n"))))[:12]
return files, "bd2schedules-" + gameVersion + "-" + hash, nil
}
func (t task) gameDirectory(explicit string) (string, string, string, error) {
if explicit == "" {
var c struct {
SchemaVersion int `json:"schema_version"`
GameDirectory string `json:"game_directory"`
}
if err := readJSON(filepath.Join(t.root, "go", "config.json"), &c, true); err != nil {
return "", "", "", fmt.Errorf("read go/config.json or pass -GameDir: %w", err)
}
if c.SchemaVersion != 1 || strings.TrimSpace(c.GameDirectory) == "" {
return "", "", "", errors.New("go/config.json requires schema_version 1 and game_directory")
}
explicit = c.GameDirectory
}
game, err := filepath.Abs(explicit)
if err != nil {
return "", "", "", err
}
managed := filepath.Join(game, "BrownDust II_Data", "Managed")
bep := filepath.Join(game, "BepInEx")
if !regular(filepath.Join(managed, "Assembly-CSharp.dll")) {
app := game
if !strings.EqualFold(filepath.Ext(app), ".app") {
app = filepath.Join(game, "BrownDust II.app")
}
managed = filepath.Join(app, "Contents", "Resources", "Data", "Managed")
bep = filepath.Join(filepath.Dir(app), "BepInEx")
inside := filepath.Join(app, "Contents", "BepInEx")
if directory(inside) && !directory(bep) {
bep = inside
}
}
if !regular(filepath.Join(managed, "Assembly-CSharp.dll")) {
return "", "", "", fmt.Errorf("Assembly-CSharp.dll missing in %s", game)
}
if !regular(filepath.Join(bep, "core", "BepInEx.dll")) {
return "", "", "", fmt.Errorf("BepInEx/core/BepInEx.dll missing in %s", game)
}
return game, managed, bep, nil
}
func (t task) build(opts buildOptions) error {
v, err := t.versions()
if err != nil {
return err
}
schedules, scheduleName, err := t.schedules(v.Game)
if err != nil {
return err
}
buildRoot := filepath.Join(t.root, ".build")
if opts.schedulesOnly {
out := filepath.Join(buildRoot, scheduleName)
if err := os.MkdirAll(out, 0755); err != nil {
return err
}
for _, file := range schedules {
if err := copyFile(file, filepath.Join(out, filepath.Base(file))); err != nil {
return err
}
}
fmt.Println("Built schedule release:", out)
return nil
}
game, managed, bep, err := t.gameDirectory(opts.gameDir)
if err != nil {
return err
}
packageRoot := filepath.Join(buildRoot, "package")
server := filepath.Join(packageRoot, "bd2server")
client := filepath.Join(packageRoot, "bd2client")
suffix := "-" + t.target.platform + "-" + t.target.architecture + ".zip"
serverZip := filepath.Join(buildRoot, "bd2server-"+v.Server+suffix)
clientZip := filepath.Join(buildRoot, "bd2client-"+v.Client+suffix)
if err := os.MkdirAll(buildRoot, 0755); err != nil {
return err
}
for _, path := range []string{packageRoot, serverZip, clientZip} {
if err := removeBuildOutput(buildRoot, path); err != nil {
return err
}
}
for _, path := range []string{filepath.Join(server, "go"), filepath.Join(server, "data", "state"), filepath.Join(server, "schedules"), filepath.Join(client, "plugins")} {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
}
if !opts.skipTests {
for _, args := range [][]string{{"test", "./..."}, {"vet", "./..."}, {"test", "-tags", "release,production", "./..."}, {"vet", "-tags", "release,production", "./..."}} {
if err := t.command("go", args...); err != nil {
return err
}
}
}
ext := ""
clientFlags := "-s -w"
if t.target.goos == "windows" {
ext = ".exe"
clientFlags = "-H windowsgui -s -w"
}
if err := t.command("go", "build", "-tags", "release", "-trimpath", "-ldflags", "-s -w", "-o", filepath.Join(server, "bd2server"+ext), "./cmd/bd2server"); err != nil {
return err
}
if err := t.command("go", "build", "-tags", "release,production", "-trimpath", "-ldflags", clientFlags, "-o", filepath.Join(client, "bd2client"+ext), "./cmd/bd2client"); err != nil {
return err
}
var shared []byte
for _, plugin := range []string{"LocalIdentity", "LoginUI", "CashShop"} {
project := filepath.Join(t.root, "plugins", plugin, plugin+".csproj")
if err := t.command("dotnet", "build", project, "-c", "Release", "-p:GameDir="+game, "-p:BD2ManagedDir="+managed, "-p:BD2BepInExDir="+bep, "--nologo"); err != nil {
return err
}
out := filepath.Join(t.root, "plugins", plugin, "bin", "Release", "netstandard2.1")
raw, err := os.ReadFile(filepath.Join(out, "BD2.GameNames.dll"))
if err != nil {
return err
}
if shared != nil && !bytes.Equal(shared, raw) {
return errors.New("plugins were built with different BD2.GameNames libraries")
}
shared = raw
for _, name := range []string{"BD2" + plugin + ".dll", "BD2.GameNames.dll"} {
if err := copyFile(filepath.Join(out, name), filepath.Join(client, "plugins", name)); err != nil {
return err
}
}
}
if err := copyTree(filepath.Join(t.root, "go", "seed"), filepath.Join(server, "go", "seed")); err != nil {
return err
}
for _, file := range schedules {
if err := copyFile(file, filepath.Join(server, "schedules", filepath.Base(file))); err != nil {
return err
}
}
for _, name := range []string{"versions.json", "README.md", "LICENSE"} {
for _, out := range []string{server, client} {
if err := copyFile(filepath.Join(t.root, name), filepath.Join(out, name)); err != nil {
return err
}
}
}
for _, name := range []string{"AUTHENTICATION.md", "RESOURCES.md", "GAME_CONFIGURATION.md"} {
if err := copyFile(filepath.Join(t.root, name), filepath.Join(server, name)); err != nil {
return err
}
}
if err := archiveDirectory(server, serverZip); err != nil {
return err
}
if err := archiveDirectory(client, clientZip); err != nil {
return err
}
fmt.Println("Built server archive:", serverZip)
fmt.Println("Built client archive:", clientZip)
return nil
}
func regular(path string) bool { s, err := os.Stat(path); return err == nil && s.Mode().IsRegular() }
func directory(path string) bool { s, err := os.Stat(path); return err == nil && s.IsDir() }
func removeBuildOutput(root, path string) error {
base, err := filepath.EvalSymlinks(root)
if err != nil {
return err
}
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
return nil
} else if err != nil {
return err
}
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
rel, err := filepath.Rel(base, resolved)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("refusing to remove output outside .build: %s", path)
}
return os.RemoveAll(path)
}
func copyFile(source, destination string) error {
in, err := os.Open(source)
if err != nil {
return err
}
defer in.Close()
info, err := in.Stat()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("not a regular file: %s", source)
}
if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
return err
}
out, err := os.OpenFile(destination, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode().Perm())
if err != nil {
return err
}
_, writeErr := io.Copy(out, in)
return errors.Join(writeErr, out.Close())
}
func copyTree(source, destination string) error {
return filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(source, path)
if err != nil {
return err
}
out := filepath.Join(destination, rel)
if entry.IsDir() {
return os.MkdirAll(out, 0755)
}
if entry.Type()&os.ModeSymlink != 0 {
return fmt.Errorf("unexpected seed symlink: %s", path)
}
return copyFile(path, out)
})
}
func archiveDirectory(source, destination string) error {
file, err := os.CreateTemp(filepath.Dir(destination), ".bd2-archive-*.tmp")
if err != nil {
return err
}
temp := file.Name()
defer os.Remove(temp)
archive := zip.NewWriter(file)
err = filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.Type()&os.ModeSymlink != 0 {
return fmt.Errorf("unexpected package symlink: %s", path)
}
info, err := entry.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
rel, err := filepath.Rel(filepath.Dir(source), path)
if err != nil {
return err
}
header.Name = filepath.ToSlash(rel)
if entry.IsDir() {
header.Name += "/"
_, err = archive.CreateHeader(header)
return err
}
header.Method = zip.Deflate
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
in, err := os.Open(path)
if err != nil {
return err
}
_, writeErr := io.Copy(writer, in)
return errors.Join(writeErr, in.Close())
})
err = errors.Join(err, archive.Close(), file.Close())
if err != nil {
return err
}
return os.Rename(temp, destination)
}
+70
View File
@@ -0,0 +1,70 @@
package main
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
func readJSON(path string, v any, strict bool) error {
raw, err := os.ReadFile(path)
if err != nil {
return err
}
d := json.NewDecoder(bytes.NewReader(bytes.TrimPrefix(raw, []byte{0xef, 0xbb, 0xbf})))
if strict {
d.DisallowUnknownFields()
}
if err = d.Decode(v); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
if err = d.Decode(new(any)); err != io.EOF {
return fmt.Errorf("%s must contain one JSON object", path)
}
return nil
}
func (t task) gameDirectory(explicit string) (string, string, string, error) {
if explicit == "" {
var c struct {
SchemaVersion int `json:"schema_version"`
GameDirectory string `json:"game_directory"`
}
if err := readJSON(filepath.Join(t.root, "go", "config.json"), &c, true); err != nil {
return "", "", "", fmt.Errorf("read go/config.json or pass -GameDir: %w", err)
}
if c.SchemaVersion != 1 || strings.TrimSpace(c.GameDirectory) == "" {
return "", "", "", errors.New("go/config.json requires schema_version 1 and game_directory")
}
explicit = c.GameDirectory
}
game, err := filepath.Abs(explicit)
if err != nil {
return "", "", "", err
}
managed := filepath.Join(game, "BrownDust II_Data", "Managed")
bep := filepath.Join(game, "BepInEx")
if !regular(filepath.Join(managed, "Assembly-CSharp.dll")) {
app := game
if !strings.EqualFold(filepath.Ext(app), ".app") {
app = filepath.Join(game, "BrownDust II.app")
}
managed = filepath.Join(app, "Contents", "Resources", "Data", "Managed")
bep = filepath.Join(filepath.Dir(app), "BepInEx")
inside := filepath.Join(app, "Contents", "BepInEx")
if directory(inside) && !directory(bep) {
bep = inside
}
}
if !regular(filepath.Join(managed, "Assembly-CSharp.dll")) {
return "", "", "", fmt.Errorf("Assembly-CSharp.dll missing in %s", game)
}
if !regular(filepath.Join(bep, "core", "BepInEx.dll")) {
return "", "", "", fmt.Errorf("BepInEx/core/BepInEx.dll missing in %s", game)
}
return game, managed, bep, nil
}
+132
View File
@@ -0,0 +1,132 @@
package main
import (
"archive/zip"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"strings"
)
func regular(path string) bool { s, err := os.Stat(path); return err == nil && s.Mode().IsRegular() }
func directory(path string) bool { s, err := os.Stat(path); return err == nil && s.IsDir() }
func removeBuildOutput(root, path string) error {
base, err := filepath.EvalSymlinks(root)
if err != nil {
return err
}
if _, err := os.Lstat(path); errors.Is(err, os.ErrNotExist) {
return nil
} else if err != nil {
return err
}
resolved, err := filepath.EvalSymlinks(path)
if err != nil {
return err
}
rel, err := filepath.Rel(base, resolved)
if err != nil || rel == "." || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
return fmt.Errorf("refusing to remove output outside .build: %s", path)
}
return os.RemoveAll(path)
}
func copyFile(source, destination string) error {
in, err := os.Open(source)
if err != nil {
return err
}
defer func() { _ = in.Close() }()
info, err := in.Stat()
if err != nil {
return err
}
if !info.Mode().IsRegular() {
return fmt.Errorf("not a regular file: %s", source)
}
if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
return err
}
out, err := os.OpenFile(destination, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode().Perm())
if err != nil {
return err
}
_, writeErr := io.Copy(out, in)
return errors.Join(writeErr, out.Close())
}
func copyTree(source, destination string) error {
return filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
rel, err := filepath.Rel(source, path)
if err != nil {
return err
}
out := filepath.Join(destination, rel)
if entry.IsDir() {
return os.MkdirAll(out, 0755)
}
if entry.Type()&os.ModeSymlink != 0 {
return fmt.Errorf("unexpected seed symlink: %s", path)
}
return copyFile(path, out)
})
}
func archiveDirectory(source, destination string) error {
file, err := os.CreateTemp(filepath.Dir(destination), ".bd2-archive-*.tmp")
if err != nil {
return err
}
temp := file.Name()
defer func() { _ = os.Remove(temp) }()
archive := zip.NewWriter(file)
err = filepath.WalkDir(source, func(path string, entry fs.DirEntry, err error) error {
if err != nil {
return err
}
if entry.Type()&os.ModeSymlink != 0 {
return fmt.Errorf("unexpected package symlink: %s", path)
}
info, err := entry.Info()
if err != nil {
return err
}
header, err := zip.FileInfoHeader(info)
if err != nil {
return err
}
rel, err := filepath.Rel(filepath.Dir(source), path)
if err != nil {
return err
}
header.Name = filepath.ToSlash(rel)
if entry.IsDir() {
header.Name += "/"
_, err = archive.CreateHeader(header)
return err
}
header.Method = zip.Deflate
writer, err := archive.CreateHeader(header)
if err != nil {
return err
}
in, err := os.Open(path)
if err != nil {
return err
}
_, writeErr := io.Copy(writer, in)
return errors.Join(writeErr, in.Close())
})
err = errors.Join(err, archive.Close(), file.Close())
if err != nil {
return err
}
return os.Rename(temp, destination)
}
+64
View File
@@ -0,0 +1,64 @@
package main
import (
"fmt"
"os"
"os/exec"
"runtime"
"strings"
)
type target struct{ platform, goos, architecture, goarch, goarm string }
func nativeTarget() (target, error) {
arch := runtime.GOARCH
if runtime.GOOS == "windows" {
machine := os.Getenv("PROCESSOR_ARCHITEW6432")
if machine == "" {
machine = os.Getenv("PROCESSOR_ARCHITECTURE")
}
switch strings.ToLower(machine) {
case "amd64":
arch = "amd64"
case "arm64":
arch = "arm64"
case "x86":
arch = "386"
}
} else {
if out, err := exec.Command("uname", "-m").Output(); err == nil {
arch = strings.TrimSpace(string(out))
}
}
return platformTarget(runtime.GOOS, arch)
}
func platformTarget(goos, arch string) (target, error) {
t := target{goos: goos}
switch goos {
case "windows", "linux":
t.platform = goos
case "darwin":
t.platform = "macos"
default:
return t, fmt.Errorf("unsupported platform %q", goos)
}
switch strings.ToLower(arch) {
case "amd64", "x86_64", "x64":
t.architecture = "x64"
t.goarch = "amd64"
case "aarch64", "arm64":
t.architecture = "arm64"
t.goarch = "arm64"
case "386", "i386", "i486", "i586", "i686", "x86":
t.architecture = "x86"
t.goarch = "386"
case "arm", "armv7", "armv7l":
t.architecture = "armv7"
t.goarch = "arm"
t.goarm = "7"
default:
return t, fmt.Errorf("unsupported architecture %q", arch)
}
return t, nil
}
+68
View File
@@ -0,0 +1,68 @@
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
)
type task struct {
root string
target target
}
func repositoryRoot() (string, error) {
exe, err := os.Executable()
if err != nil {
return "", err
}
for dir := filepath.Dir(exe); ; dir = filepath.Dir(dir) {
if regular(filepath.Join(dir, "versions.json")) && regular(filepath.Join(dir, "go", "go.mod")) {
return dir, nil
}
if filepath.Dir(dir) == dir {
break
}
}
return "", errors.New("repository root not found beside the cached build tool; launch through bd2w")
}
// Every child receives a process-local native target and repository-local cache.
func (t task) command(program string, args ...string) error {
cmd := exec.Command(program, args...)
cmd.Dir = filepath.Join(t.root, "go")
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = replaceEnvironment(os.Environ(), map[string]string{"GOCACHE": filepath.Join(t.root, "go", ".cache", "go-build"), "GOOS": t.target.goos, "GOARCH": t.target.goarch, "GOARM": t.target.goarm})
if err := cmd.Run(); err != nil {
return fmt.Errorf("%s %s: %w", program, strings.Join(args, " "), err)
}
return nil
}
func replaceEnvironment(env []string, values map[string]string) []string {
out := make([]string, 0, len(env)+len(values))
for _, entry := range env {
key, _, _ := strings.Cut(entry, "=")
remove := false
for wanted := range values {
if strings.EqualFold(key, wanted) {
remove = true
break
}
}
if !remove {
out = append(out, entry)
}
}
for key, value := range values {
if value != "" {
out = append(out, key+"="+value)
}
}
return out
}
+210
View File
@@ -0,0 +1,210 @@
package main
import (
"bytes"
"crypto/sha256"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"sort"
"strings"
)
type buildOptions struct {
gameDir string
skipTests, schedulesOnly bool
}
type releaseVersions struct {
Game string `json:"game_version"`
Server string `json:"server_version"`
Client string `json:"client_version"`
}
func parseOptions(args []string) (buildOptions, error) {
var opts buildOptions
for i := 0; i < len(args); i++ {
name, value, assigned := strings.Cut(args[i], "=")
switch strings.ToLower(name) {
case "-gamedir", "--game-dir":
if !assigned {
i++
if i >= len(args) {
return opts, errors.New("-GameDir requires a directory")
}
value = args[i]
}
if strings.TrimSpace(value) == "" {
return opts, errors.New("-GameDir requires a directory")
}
opts.gameDir = value
case "-skiptests", "--skip-tests":
if assigned {
return opts, fmt.Errorf("unexpected value for %s", name)
}
opts.skipTests = true
case "-schedulesonly", "--schedules-only":
if assigned {
return opts, fmt.Errorf("unexpected value for %s", name)
}
opts.schedulesOnly = true
default:
return opts, fmt.Errorf("unknown build option %q", args[i])
}
}
return opts, nil
}
func (t task) versions() (releaseVersions, error) {
var v releaseVersions
if err := readJSON(filepath.Join(t.root, "versions.json"), &v, false); err != nil {
return v, err
}
valid := regexp.MustCompile(`^[A-Za-z0-9.+_-]+$`)
for _, s := range []string{v.Game, v.Server, v.Client} {
if !valid.MatchString(s) {
return v, fmt.Errorf("invalid release version %q", s)
}
}
return v, nil
}
func (t task) schedules(gameVersion string) ([]string, string, error) {
files, err := filepath.Glob(filepath.Join(t.root, "schedules", "*.bd2schedule"))
if err != nil {
return nil, "", err
}
if len(files) == 0 {
return nil, "", errors.New("no project schedule files found")
}
sort.Slice(files, func(i, j int) bool {
return strings.ToLower(filepath.Base(files[i])) < strings.ToLower(filepath.Base(files[j]))
})
var lines []string
for _, file := range files {
raw, err := os.ReadFile(file)
if err != nil {
return nil, "", err
}
lines = append(lines, fmt.Sprintf("%s:%X", filepath.Base(file), sha256.Sum256(raw)))
}
hash := fmt.Sprintf("%x", sha256.Sum256([]byte(strings.Join(lines, "\n"))))[:12]
return files, "bd2schedules-" + gameVersion + "-" + hash, nil
}
func (t task) build(opts buildOptions) error {
v, err := t.versions()
if err != nil {
return err
}
schedules, scheduleName, err := t.schedules(v.Game)
if err != nil {
return err
}
buildRoot := filepath.Join(t.root, ".build")
if opts.schedulesOnly {
out := filepath.Join(buildRoot, scheduleName)
if err := os.MkdirAll(out, 0755); err != nil {
return err
}
for _, file := range schedules {
if err := copyFile(file, filepath.Join(out, filepath.Base(file))); err != nil {
return err
}
}
fmt.Println("Built schedule release:", out)
return nil
}
game, managed, bep, err := t.gameDirectory(opts.gameDir)
if err != nil {
return err
}
packageRoot := filepath.Join(buildRoot, "package")
server := filepath.Join(packageRoot, "bd2server")
client := filepath.Join(packageRoot, "bd2client")
suffix := "-" + t.target.platform + "-" + t.target.architecture + ".zip"
serverZip := filepath.Join(buildRoot, "bd2server-"+v.Server+suffix)
clientZip := filepath.Join(buildRoot, "bd2client-"+v.Client+suffix)
if err := os.MkdirAll(buildRoot, 0755); err != nil {
return err
}
for _, path := range []string{packageRoot, serverZip, clientZip} {
if err := removeBuildOutput(buildRoot, path); err != nil {
return err
}
}
for _, path := range []string{filepath.Join(server, "go"), filepath.Join(server, "data", "state"), filepath.Join(server, "schedules"), filepath.Join(client, "plugins")} {
if err := os.MkdirAll(path, 0755); err != nil {
return err
}
}
if !opts.skipTests {
for _, args := range [][]string{{"test", "./..."}, {"vet", "./..."}, {"test", "-tags", "release,production", "./..."}, {"vet", "-tags", "release,production", "./..."}} {
if err := t.command("go", args...); err != nil {
return err
}
}
}
ext := ""
clientFlags := "-s -w"
if t.target.goos == "windows" {
ext = ".exe"
clientFlags = "-H windowsgui -s -w"
}
if err := t.command("go", "build", "-tags", "release", "-trimpath", "-ldflags", "-s -w", "-o", filepath.Join(server, "bd2server"+ext), "./cmd/bd2server"); err != nil {
return err
}
if err := t.command("go", "build", "-tags", "release,production", "-trimpath", "-ldflags", clientFlags, "-o", filepath.Join(client, "bd2client"+ext), "./cmd/bd2client"); err != nil {
return err
}
var shared []byte
for _, plugin := range []string{"LocalIdentity", "LoginUI", "CashShop"} {
project := filepath.Join(t.root, "plugins", plugin, plugin+".csproj")
if err := t.command("dotnet", "build", project, "-c", "Release", "-p:GameDir="+game, "-p:BD2ManagedDir="+managed, "-p:BD2BepInExDir="+bep, "--nologo"); err != nil {
return err
}
out := filepath.Join(t.root, "plugins", plugin, "bin", "Release", "netstandard2.1")
raw, err := os.ReadFile(filepath.Join(out, "BD2.GameNames.dll"))
if err != nil {
return err
}
if shared != nil && !bytes.Equal(shared, raw) {
return errors.New("plugins were built with different BD2.GameNames libraries")
}
shared = raw
for _, name := range []string{"BD2" + plugin + ".dll", "BD2.GameNames.dll"} {
if err := copyFile(filepath.Join(out, name), filepath.Join(client, "plugins", name)); err != nil {
return err
}
}
}
if err := copyTree(filepath.Join(t.root, "go", "seed"), filepath.Join(server, "go", "seed")); err != nil {
return err
}
for _, file := range schedules {
if err := copyFile(file, filepath.Join(server, "schedules", filepath.Base(file))); err != nil {
return err
}
}
for _, name := range []string{"versions.json", "README.md", "LICENSE"} {
for _, out := range []string{server, client} {
if err := copyFile(filepath.Join(t.root, name), filepath.Join(out, name)); err != nil {
return err
}
}
}
for _, name := range []string{"AUTHENTICATION.md", "RESOURCES.md", "GAME_CONFIGURATION.md"} {
if err := copyFile(filepath.Join(t.root, name), filepath.Join(server, name)); err != nil {
return err
}
}
if err := archiveDirectory(server, serverZip); err != nil {
return err
}
if err := archiveDirectory(client, clientZip); err != nil {
return err
}
fmt.Println("Built server archive:", serverZip)
fmt.Println("Built client archive:", clientZip)
return nil
}
+184
View File
@@ -0,0 +1,184 @@
package main
import (
"bytes"
"encoding/xml"
"errors"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
type sdkOptions struct{ gameDir, versionConfig, packageVersion, outputDirectory, packageDirectory, gameMapping string }
func (t task) sdkOptions(args []string, mode string) (sdkOptions, error) {
var o sdkOptions
fs := flag.NewFlagSet("sdk "+mode, flag.ContinueOnError)
fs.StringVar(&o.gameDir, "game-dir", "", "game installation directory")
fs.StringVar(&o.versionConfig, "version-config", filepath.Join(t.root, "versions.json"), "version configuration")
switch mode {
case "pack":
fs.StringVar(&o.packageVersion, "package-version", "", "version-locked NuGet version")
fs.StringVar(&o.outputDirectory, "output-directory", filepath.Join(t.root, ".build", "nuget"), "package output directory")
case "verify":
fs.StringVar(&o.packageVersion, "package-version", "", "version-locked NuGet version")
fs.StringVar(&o.packageDirectory, "package-directory", filepath.Join(t.root, ".build", "nuget"), "local package feed")
case "update-names":
fs.StringVar(&o.gameMapping, "game-mapping", "", "official obfuscation mapping")
default:
return o, fmt.Errorf("unknown SDK mode %q", mode)
}
if err := fs.Parse(args); err != nil {
return o, err
}
if fs.NArg() != 0 {
return o, fmt.Errorf("unexpected SDK argument %q", fs.Arg(0))
}
var err error
for _, p := range []*string{&o.versionConfig, &o.outputDirectory, &o.packageDirectory} {
if *p == "" {
continue
}
*p, err = filepath.Abs(*p)
if err != nil {
return o, err
}
}
return o, nil
}
func (t task) sdkPackageVersion(o sdkOptions) (string, string, error) {
var versions releaseVersions
if err := readJSON(o.versionConfig, &versions, false); err != nil {
return "", "", err
}
if !regexp.MustCompile(`^\d+\.\d+\.\d+$`).MatchString(versions.Game) {
return "", "", fmt.Errorf("invalid game version %q", versions.Game)
}
version := o.packageVersion
if version == "" {
raw, err := os.ReadFile(filepath.Join(t.root, "plugins", "PackageMetadata.props"))
if err != nil {
return "", "", err
}
var metadata struct {
Groups []struct {
Version string `xml:"BD2PackageVersion"`
} `xml:"PropertyGroup"`
}
if err = xml.Unmarshal(raw, &metadata); err != nil {
return "", "", err
}
for _, g := range metadata.Groups {
if g.Version != "" {
if version != "" {
return "", "", errors.New("duplicate BD2PackageVersion")
}
version = strings.TrimSpace(g.Version)
}
}
if version == "" {
return "", "", errors.New("PackageMetadata.props requires BD2PackageVersion")
}
version += "-game." + versions.Game
}
if !regexp.MustCompile(`^\d+\.\d+\.\d+-game\.\d+\.\d+\.\d+(?:\.[A-Za-z0-9-]+)*$`).MatchString(version) {
return "", "", fmt.Errorf("invalid SDK package version %q", version)
}
if !strings.Contains(version, "-game."+versions.Game+".") && !strings.HasSuffix(version, "-game."+versions.Game) {
return "", "", errors.New("package game suffix must match version configuration")
}
return version, versions.Game, nil
}
func (t task) sdkCommand(workdir string, env map[string]string, capture bool, args ...string) (string, error) {
cmd := exec.Command("dotnet", args...)
cmd.Dir = workdir
cmd.Stdin = os.Stdin
cmd.Env = replaceEnvironment(os.Environ(), env)
if capture {
out, err := cmd.CombinedOutput()
return string(out), err
}
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("dotnet %s: %w", strings.Join(args, " "), err)
}
return "", nil
}
func (t task) sdkWorkspace(prefix string) (string, error) {
build := filepath.Join(t.root, ".build")
if err := os.MkdirAll(build, 0755); err != nil {
return "", err
}
return os.MkdirTemp(build, prefix)
}
func (t task) sdkCleanup(path string, result *error) {
if err := removeBuildOutput(filepath.Join(t.root, ".build"), path); err != nil {
*result = errors.Join(*result, err)
}
}
func sdkReadTrimmed(path string) (string, error) {
raw, err := os.ReadFile(path)
return strings.TrimSpace(string(raw)), err
}
func sdkXML(value string) string {
var out bytes.Buffer
_ = xml.EscapeText(&out, []byte(value))
return out.String()
}
func sdkNugetConfig(path, feed, cache string) error {
config := `<configuration><packageSources><clear/><add key="bd2-local" value="` + sdkXML(feed) + `"/><add key="nuget.org" value="https://api.nuget.org/v3/index.json"/></packageSources>`
if cache != "" {
config += `<config><add key="globalPackagesFolder" value="` + sdkXML(cache) + `"/></config>`
}
config += `</configuration>`
return os.WriteFile(path, []byte(config), 0644)
}
func sdkDirectorySize(path string) (int64, error) {
var size int64
err := filepath.WalkDir(path, func(_ string, d os.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
info, err := d.Info()
if err != nil {
return err
}
size += info.Size()
}
return nil
})
return size, err
}
func sdkAtomicCopy(source, destination string) (result error) {
in, err := os.Open(source)
if err != nil {
return err
}
defer func() { _ = in.Close() }()
out, err := os.CreateTemp(filepath.Dir(destination), ".names-*.gz")
if err != nil {
return err
}
temp := out.Name()
defer func() { _ = out.Close(); _ = os.Remove(temp) }()
if _, err = io.Copy(out, in); err != nil {
return err
}
if err = out.Sync(); err != nil {
return err
}
if err = out.Close(); err != nil {
return err
}
return os.Rename(temp, destination)
}
+119
View File
@@ -0,0 +1,119 @@
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
)
func (t task) sdkPack(args []string) (result error) {
o, err := t.sdkOptions(args, "pack")
if err != nil {
return err
}
version, gameVersion, err := t.sdkPackageVersion(o)
if err != nil {
return err
}
_, managed, _, err := t.gameDirectory(o.gameDir)
if err != nil {
return err
}
workspace, err := t.sdkWorkspace("sdk-pack-")
if err != nil {
return err
}
defer t.sdkCleanup(workspace, &result)
if err = os.MkdirAll(o.outputDirectory, 0755); err != nil {
return err
}
tool := filepath.Join(workspace, "tool")
sdk := filepath.Join(workspace, "sdk")
command := func(args ...string) error { _, err := t.sdkCommand(t.root, nil, false, args...); return err }
if err = command("publish", filepath.Join(t.root, "plugins", "GameSdk", "GameSdk.csproj"), "-c", "Release", "--nologo", "-o", tool, "-p:Version="+strings.SplitN(version, "-", 2)[0], "-p:UseAppHost=false"); err != nil {
return err
}
toolDLL := filepath.Join(tool, "GameSdk.dll")
cache := os.Getenv("BD2_GAME_SDK_CACHE")
if cache == "" {
cache = filepath.Join(t.root, ".build", "game-sdk")
}
if _, err = t.sdkCommand(t.root, map[string]string{"BD2_GAME_SDK_CACHE": cache}, false, toolDLL, "prepare-embedded", filepath.Join(managed, "Assembly-CSharp.dll"), sdk, "--game-version", gameVersion); err != nil {
return err
}
shared, err := sdkReadTrimmed(filepath.Join(sdk, "shared-sdk.txt"))
if err != nil {
return err
}
table := filepath.Join(shared, "names.json")
runtimeObj := filepath.Join(workspace, "runtime-obj") + string(filepath.Separator)
runtimeBin := filepath.Join(workspace, "runtime-bin") + string(filepath.Separator)
if err = command("pack", filepath.Join(t.root, "plugins", "GameNames", "GameNames.csproj"), "-c", "Release", "--nologo", "-o", o.outputDirectory, "-p:Version="+version, "-p:GameNamesTable="+table, "-p:BaseIntermediateOutputPath="+runtimeObj, "-p:OutputPath="+runtimeBin); err != nil {
return err
}
if err = command(toolDLL, "verify-runtime", table, filepath.Join(runtimeBin, "BD2.GameNames.dll")); err != nil {
return err
}
project := filepath.Join(t.root, "plugins", "GameSdk", "Package", "BD2.GameSdk.Package.csproj")
config := filepath.Join(workspace, "NuGet.Config")
if err = sdkNugetConfig(config, o.outputDirectory, ""); err != nil {
return err
}
packageProps := []string{"-p:Version=" + version, "-p:BD2Packaging=true", "-p:BaseIntermediateOutputPath=" + filepath.Join(workspace, "package-obj") + string(filepath.Separator), "-p:OutputPath=" + filepath.Join(workspace, "package-bin") + string(filepath.Separator)}
if err = command(append([]string{"restore", project, "--configfile", config}, packageProps...)...); err != nil {
return err
}
packArgs := []string{"pack", project, "-c", "Release", "--no-restore", "--nologo", "-o", o.outputDirectory, "-p:BD2ToolPublishDir=" + tool}
if err = command(append(packArgs, packageProps...)...); err != nil {
return err
}
fmt.Printf("Created BD2.GameNames and BD2.GameSdk %s in %s\n", version, o.outputDirectory)
return nil
}
func (t task) sdkUpdateNames(args []string) (result error) {
o, err := t.sdkOptions(args, "update-names")
if err != nil {
return err
}
if strings.TrimSpace(o.gameMapping) == "" {
return fmt.Errorf("sdk update-names requires --game-mapping")
}
mapping, err := filepath.Abs(o.gameMapping)
if err != nil {
return err
}
if !regular(mapping) {
return fmt.Errorf("mapping file missing: %s", mapping)
}
_, managed, _, err := t.gameDirectory(o.gameDir)
if err != nil {
return err
}
workspace, err := t.sdkWorkspace("sdk-names-")
if err != nil {
return err
}
defer t.sdkCleanup(workspace, &result)
toolDir := filepath.Join(workspace, "tool")
command := func(args ...string) error { _, err := t.sdkCommand(t.root, nil, false, args...); return err }
if err = command("publish", filepath.Join(t.root, "plugins", "GameSdk", "GameSdk.csproj"), "-c", "Release", "--nologo", "-o", toolDir, "-p:UseAppHost=false"); err != nil {
return err
}
tool := filepath.Join(toolDir, "GameSdk.dll")
assembly := filepath.Join(managed, "Assembly-CSharp.dll")
table := filepath.Join(workspace, "names.json")
if err = command(tool, "names", assembly, mapping, o.versionConfig, table); err != nil {
return err
}
if err = command(tool, "shell", table, assembly, filepath.Join(workspace, "Assembly-CSharp.Readable.dll")); err != nil {
return err
}
destination := filepath.Join(t.root, "plugins", "GameNames", "Mappings", "names.json.gz")
if err = sdkAtomicCopy(table+".gz", destination); err != nil {
return err
}
fmt.Printf("Updated SDK/runtime shared table: %s. Rebuild SDK and plugins, then pack a new version.\n", destination)
return nil
}
+169
View File
@@ -0,0 +1,169 @@
package main
import (
"encoding/xml"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)
func (t task) sdkVerify(args []string) (result error) {
o, err := t.sdkOptions(args, "verify")
if err != nil {
return err
}
version, gameVersion, err := t.sdkPackageVersion(o)
if err != nil {
return err
}
game, managed, bep, err := t.gameDirectory(o.gameDir)
if err != nil {
return err
}
workspace, err := t.sdkWorkspace("sdk-verify-")
if err != nil {
return err
}
defer t.sdkCleanup(workspace, &result)
for _, name := range []string{"ExamplePlugin.csproj", "Plugin.cs"} {
if err = copyFile(filepath.Join(t.root, "plugins", "GameSdk", "samples", "ExamplePlugin", name), filepath.Join(workspace, name)); err != nil {
return err
}
}
project := filepath.Join(workspace, "ExamplePlugin.csproj")
raw, err := os.ReadFile(project)
if err != nil {
return err
}
packageReference := regexp.MustCompile(`(<PackageReference\s+Include="BD2\.GameSdk"\s+Version=")[^"]+("[^>]*>)`)
if !packageReference.Match(raw) {
return fmt.Errorf("sample has no BD2.GameSdk PackageReference")
}
text := packageReference.ReplaceAllString(string(raw), `${1}`+version+`${2}`)
text = regexp.MustCompile(`<BD2GameVersion>[^<]*</BD2GameVersion>`).ReplaceAllString(text, "<BD2GameVersion>"+gameVersion+"</BD2GameVersion>")
if err = os.WriteFile(project, []byte(text), 0644); err != nil {
return err
}
for _, name := range []string{"Directory.Build.props", "Directory.Build.targets"} {
if err = os.WriteFile(filepath.Join(workspace, name), []byte("<Project />\n"), 0644); err != nil {
return err
}
}
if err = sdkNugetConfig(filepath.Join(workspace, "NuGet.Config"), o.packageDirectory, filepath.Join(workspace, "packages")); err != nil {
return err
}
sharedCache := filepath.Join(t.root, ".build", "game-sdk")
properties := []string{"-p:GameDir=" + game, "-p:BD2ManagedDir=" + managed, "-p:BD2BepInExDir=" + bep, "-p:BD2GameSdkCache=" + sharedCache}
build := func(noRestore bool) error {
args := []string{"build", project, "-c", "Release", "--nologo"}
if noRestore {
args = append(args, "--no-restore")
}
_, err := t.sdkCommand(workspace, nil, false, append(args, properties...)...)
return err
}
if err = build(false); err != nil {
return err
}
output := filepath.Join(workspace, "bin", "Release", "netstandard2.1")
for _, name := range []string{"BD2.GameNames.dll", "ExamplePlugin.dll"} {
if !regular(filepath.Join(output, name)) {
return fmt.Errorf("missing output: %s", name)
}
}
for _, name := range []string{"Assembly-CSharp.Readable.dll", "Assembly-CSharp.Readable.pdb", "Assembly-CSharp.dll", "GameSdk.dll", "Mono.Cecil.dll", "ICSharpCode.Decompiler.dll", "BepInEx.dll", "0Harmony.dll", "UnityEngine.dll", "ExamplePlugin.pdb", "navigation.json", "sources", "ref", "lib"} {
if _, err = os.Lstat(filepath.Join(output, name)); err == nil {
return fmt.Errorf("unexpected deployment artifact: %s", name)
} else if !os.IsNotExist(err) {
return err
}
}
tool := filepath.Join(workspace, "packages", "bd2.gamesdk", strings.ToLower(version), "tools", "net8.0", "GameSdk.dll")
sdk := filepath.Join(workspace, "obj", "Release", "netstandard2.1", "bd2-game-sdk")
shared, err := sdkReadTrimmed(filepath.Join(sdk, "shared-sdk.txt"))
if err != nil {
return err
}
table := filepath.Join(shared, "names.json")
size, err := sdkDirectorySize(sdk)
if err != nil {
return err
}
if size > 65536 {
return fmt.Errorf("SDK obj contains %d bytes instead of shared references", size)
}
var example struct {
Group struct {
Version string `xml:"BD2GameVersion"`
} `xml:"PropertyGroup"`
}
if err = xml.Unmarshal([]byte(text), &example); err != nil {
return err
}
if filepath.Base(filepath.Dir(shared)) != example.Group.Version {
return fmt.Errorf("shared SDK directory is not grouped by plugin-declared game version")
}
readyPath := filepath.Join(shared, "ready.txt")
navigationPath := filepath.Join(sdk, "GameSourceNavigation.props")
stamp := func(path string) (time.Time, error) {
info, err := os.Stat(path)
if err != nil {
return time.Time{}, err
}
return info.ModTime(), nil
}
readyTime, err := stamp(readyPath)
if err != nil {
return err
}
navigationTime, err := stamp(navigationPath)
if err != nil {
return err
}
command := func(args ...string) error { _, err := t.sdkCommand(workspace, nil, false, args...); return err }
runtime := filepath.Join(output, "BD2.GameNames.dll")
plugin := filepath.Join(output, "ExamplePlugin.dll")
assembly := filepath.Join(managed, "Assembly-CSharp.dll")
if err = command(tool, "verify-runtime", table, runtime); err != nil {
return err
}
if err = command(tool, "verify-navigation", sdk); err != nil {
return err
}
if err = command(tool, "verify", table, plugin, assembly); err != nil {
return err
}
if err = build(true); err != nil {
return err
}
after, err := stamp(navigationPath)
if err != nil {
return err
}
if !after.Equal(navigationTime) {
return fmt.Errorf("unchanged navigation props were rewritten")
}
after, err = stamp(readyPath)
if err != nil {
return err
}
if !after.Equal(readyTime) {
return fmt.Errorf("repeated build unexpectedly regenerated shared SDK")
}
if err = command(tool, "verify", table, plugin, assembly); err != nil {
return err
}
for _, invalid := range []string{"", "0.0.0"} {
args := append([]string{"build", project, "-c", "Release", "--no-restore", "--nologo"}, properties...)
args = append(args, "-p:BD2GameVersion="+invalid)
rejected, err := t.sdkCommand(workspace, nil, true, args...)
if err == nil || (invalid == "" && !strings.Contains(rejected, "BD2GameVersion")) || (invalid != "" && !strings.Contains(rejected, "Plugin requires game 0.0.0")) {
return fmt.Errorf("missing/mismatched game version was not rejected: %q\n%s", invalid, rejected)
}
}
fmt.Println("Verified external PackageReference consumer, source navigation, shared runtime and incremental rebuild")
return nil
}
+149
View File
@@ -0,0 +1,149 @@
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"io"
"os"
"path/filepath"
"regexp"
"strings"
)
type versionSourceConfig 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"`
Plugins struct {
LocalIdentity string `json:"local_identity"`
CaptureEnvironment string `json:"capture_environment"`
LoginUI string `json:"login_ui"`
CashShop string `json:"cash_shop"`
} `json:"plugins"`
}
func (t task) generateVersionSource(args []string) error {
options := flag.NewFlagSet("version-source", flag.ContinueOnError)
options.SetOutput(io.Discard)
config := options.String("config", "", "version configuration")
output := options.String("output", "", "generated C# source")
plugin := options.String("plugin", "", "plugin version key")
if err := options.Parse(args); err != nil {
return err
}
if options.NArg() != 0 || *config == "" || *output == "" || *plugin == "" {
return errors.New("version-source requires --config, --output and --plugin, with no positional arguments")
}
var versions versionSourceConfig
if err := readJSON(*config, &versions, true); err != nil {
return err
}
pluginVersion, err := versions.validate(*plugin)
if err != nil {
return fmt.Errorf("%s: %w", *config, err)
}
configPath, err := filepath.Abs(*config)
if err != nil {
return err
}
outputPath, err := filepath.Abs(*output)
if err != nil {
return err
}
if outputPath == configPath {
return errors.New("version-source output must not replace the version configuration")
}
configInfo, err := os.Stat(configPath)
if err != nil {
return err
}
if outputInfo, statErr := os.Stat(outputPath); statErr == nil && os.SameFile(configInfo, outputInfo) {
return errors.New("version-source output must not replace the version configuration")
} else if statErr != nil && !errors.Is(statErr, os.ErrNotExist) {
return statErr
}
source := fmt.Sprintf(`// Generated from versions.json. Do not edit.
namespace Bd2Build
{
internal static class Versions
{
internal const string Game = "%s";
internal const string ClientRelease = "%s";
internal const string GameData = "%s";
internal const string Bundle = "%s";
internal const string Plugin = "%s";
}
}
`, versions.GameVersion, versions.ClientVersion, versions.GameDataVersion, versions.BundleVersion, pluginVersion)
return writeVersionSource(outputPath, []byte(source))
}
func (c versionSourceConfig) validate(plugin string) (string, error) {
plugins := map[string]string{"local_identity": c.Plugins.LocalIdentity, "capture_environment": c.Plugins.CaptureEnvironment, "login_ui": c.Plugins.LoginUI, "cash_shop": c.Plugins.CashShop}
selected, found := plugins[plugin]
if !found {
return "", fmt.Errorf("unsupported plugin %q", plugin)
}
semver := regexp.MustCompile(`^[0-9]+\.[0-9]+\.[0-9]+$`)
resource := regexp.MustCompile(`^[0-9]{14}$`)
if !semver.MatchString(c.GameVersion) {
return "", errors.New("game_version must be a numeric three-part version")
}
for _, key := range []string{"local_identity", "capture_environment", "login_ui", "cash_shop"} {
if !semver.MatchString(plugins[key]) {
return "", fmt.Errorf("plugins.%s must be a numeric three-part version", key)
}
}
for _, release := range []struct{ name, value, component string }{{"client_version", c.ClientVersion, "client"}, {"server_version", c.ServerVersion, "server"}} {
prefix := c.GameVersion + "+" + release.component + "."
if suffix, found := strings.CutPrefix(release.value, prefix); !found || !semver.MatchString(suffix) {
return "", fmt.Errorf("%s must be %sX.Y.Z", release.name, prefix)
}
}
for _, version := range []struct{ name, value string }{{"game_data_version", c.GameDataVersion}, {"bundle_version", c.BundleVersion}} {
if !resource.MatchString(version.value) {
return "", fmt.Errorf("%s must be a 14-digit version", version.name)
}
}
seed := filepath.Clean(filepath.FromSlash(strings.ReplaceAll(c.SeedDirectory, `\`, "/")))
if c.SeedDirectory == "" || filepath.IsAbs(seed) || strings.Contains(c.SeedDirectory, ":") || seed == ".." || strings.HasPrefix(seed, ".."+string(filepath.Separator)) {
return "", errors.New("seed_directory must be a non-empty relative path below the version file")
}
return selected, nil
}
func writeVersionSource(path string, source []byte) (err error) {
current, err := os.ReadFile(path)
if err == nil && bytes.Equal(current, source) {
return nil
}
if err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
return err
}
file, err := os.CreateTemp(filepath.Dir(path), ".version-source-*")
if err != nil {
return err
}
temporary := file.Name()
defer func() {
if removeErr := os.Remove(temporary); removeErr != nil && !errors.Is(removeErr, os.ErrNotExist) {
err = errors.Join(err, removeErr)
}
}()
_, writeErr := file.Write(source)
if writeErr == nil {
writeErr = file.Sync()
}
if closeErr := file.Close(); writeErr != nil || closeErr != nil {
return errors.Join(writeErr, closeErr)
}
return os.Rename(temporary, path)
}
+106
View File
@@ -0,0 +1,106 @@
package main
import (
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"
"time"
)
func versionSourceFixture(t *testing.T) map[string]any {
t.Helper()
return map[string]any{
"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": map[string]any{"local_identity": "0.6.1", "capture_environment": "0.2.0", "login_ui": "0.2.0", "cash_shop": "0.1.2"},
}
}
func writeVersionFixture(t *testing.T, path string, config map[string]any) {
t.Helper()
raw, err := json.Marshal(config)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, raw, 0o600); err != nil {
t.Fatal(err)
}
}
func TestGenerateVersionSourcePreservesUnchangedFile(t *testing.T) {
dir := t.TempDir()
config := filepath.Join(dir, "versions with spaces.json")
output := filepath.Join(dir, "generated with spaces", "Version.cs")
writeVersionFixture(t, config, versionSourceFixture(t))
args := []string{"--config", config, "--output", output, "--plugin", "cash_shop"}
if err := (task{root: dir}).generateVersionSource(args); err != nil {
t.Fatal(err)
}
raw, err := os.ReadFile(output)
if err != nil {
t.Fatal(err)
}
if !strings.Contains(string(raw), `Plugin = "0.1.2";`) || !strings.Contains(string(raw), `ClientRelease = "2.35.10+client.0.1.0";`) {
t.Fatalf("incorrect generated versions: %s", raw)
}
stamp := time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC)
if err := os.Chtimes(output, stamp, stamp); err != nil {
t.Fatal(err)
}
if err := (task{root: dir}).generateVersionSource(args); err != nil {
t.Fatal(err)
}
info, err := os.Stat(output)
if err != nil || !info.ModTime().Equal(stamp) {
t.Fatalf("unchanged source rewritten: info=%v err=%v", info, err)
}
changed := versionSourceFixture(t)
changed["plugins"].(map[string]any)["cash_shop"] = "0.1.3"
writeVersionFixture(t, config, changed)
if err := (task{root: dir}).generateVersionSource(args); err != nil {
t.Fatal(err)
}
raw, err = os.ReadFile(output)
if err != nil || !strings.Contains(string(raw), `Plugin = "0.1.3";`) {
t.Fatalf("changed source not installed: %s err=%v", raw, err)
}
}
func TestGenerateVersionSourceRejectsInvalidConfigWithoutWriting(t *testing.T) {
for _, test := range []struct {
name string
change func(map[string]any)
plugin string
}{
{"cross-game-client", func(c map[string]any) { c["client_version"] = "2.34.13+client.0.1.0" }, "login_ui"},
{"wrong-server-component", func(c map[string]any) { c["server_version"] = "2.35.10+client.0.1.0" }, "login_ui"},
{"invalid-resource", func(c map[string]any) { c["bundle_version"] = "2026092113523" }, "login_ui"},
{"injected-plugin-source", func(c map[string]any) { c["plugins"].(map[string]any)["cash_shop"] = "1.0.0\";" }, "cash_shop"},
{"invalid-unselected-plugin", func(c map[string]any) { c["plugins"].(map[string]any)["cash_shop"] = "invalid" }, "login_ui"},
{"unknown-field", func(c map[string]any) { c["game_versoin"] = "2.35.10" }, "login_ui"},
{"escaping-seed", func(c map[string]any) { c["seed_directory"] = `..\outside` }, "login_ui"},
{"unknown-plugin", func(map[string]any) {}, "game_names"},
} {
t.Run(test.name, func(t *testing.T) {
dir := t.TempDir()
config := filepath.Join(dir, "versions.json")
output := filepath.Join(dir, "Version.cs")
c := versionSourceFixture(t)
test.change(c)
writeVersionFixture(t, config, c)
prior := []byte("existing generated source")
if err := os.WriteFile(output, prior, 0o600); err != nil {
t.Fatal(err)
}
if err := (task{}).generateVersionSource([]string{"--config", config, "--output", output, "--plugin", test.plugin}); err == nil {
t.Fatal("invalid configuration accepted")
}
raw, err := os.ReadFile(output)
if err != nil || string(raw) != string(prior) {
t.Fatalf("validation failure modified output=%s err=%v", raw, err)
}
})
}
}
+3 -4
View File
@@ -147,16 +147,15 @@ func clientDevelopmentFile(path string) bool {
}
func clientDevelopmentGameDirectory(args []string) (string, error) {
for index := 0; index < len(args); index++ {
arg := args[index]
for index, arg := range args {
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, ok := strings.CutPrefix(arg, "--game-dir="); ok {
value = strings.TrimSpace(value)
if value == "" {
return "", errors.New("--game-dir requires a directory")
}
+1 -1
View File
@@ -52,7 +52,7 @@ func runClient(args []string, options clientRunOptions) error {
if err != nil {
return err
}
defer logCloser.Close()
defer func() { _ = logCloser.Close() }()
logger.Info("bd2client starting", "log_path", logPath)
var versions clientconfig.ReleaseVersions
+5 -3
View File
@@ -289,14 +289,14 @@ func serve(args []string) (serveErr error) {
if err != nil {
return fmt.Errorf("open account state database: %w", err)
}
defer stateRepository.Close()
defer func() { _ = stateRepository.Close() }()
var authService *auth.Service
if authentication.Mode == "oauth" {
authStore, err := auth.Open(filepath.Join(filepath.Dir(filepath.Clean(*stateFile)), "auth.db"), authRuntime.MasterKey)
if err != nil {
return fmt.Errorf("open authentication database: %w", err)
}
defer authStore.Close()
defer func() { _ = authStore.Close() }()
authService, err = auth.New(authRuntime, authStore)
if err != nil {
return err
@@ -1041,7 +1041,9 @@ func serve(args []string) (serveErr error) {
}
loginPasses.SetClock(time.Now, eventAPReset.ResetSeconds-9*3600)
eventTasksService.AttachUnlockResolver(missionUnlocked)
missionService.AttachEventHandler(eventTasksService)
if err = missionService.AttachEventHandler(eventTasksService); err != nil {
return fmt.Errorf("attach mission event handler: %w", err)
}
if err = missionService.RecordLogin(missionUnlocked); err != nil {
return err
}
+1 -1
View File
@@ -26,7 +26,7 @@ func stateCheckCommand(args []string) error {
if err != nil {
return err
}
defer repository.Close()
defer func() { _ = repository.Close() }()
version, err := repository.SchemaVersion()
if err != nil {
return err
-3
View File
@@ -1,3 +0,0 @@
# Dot-source in an external PowerShell terminal: . .\env.ps1
# Applies only to this terminal and child processes; no user-level Go settings.
$env:GOCACHE = Join-Path $PSScriptRoot '.cache\go-build'
+2 -2
View File
@@ -21,7 +21,7 @@ import (
//go:embed web/index.html
var webFS embed.FS
var errGameAlreadyRunning = errors.New("Brown Dust II is already running")
var errGameAlreadyRunning = errors.New("Brown Dust II is already running") //nolint:staticcheck // ST1005
type Options struct {
InitialGameDir string
@@ -226,7 +226,7 @@ func (s *Studio) Resources(input Request) (Response, error) {
defer cancel()
policy, err := clientsetup.FetchResourcePolicy(ctx, nil, input.settings(), s.options.Versions)
message := "The client will use the release-locked official CDN"
if policy.Mode == clientconfig.CDNLocal {
if policy.Mode == clientconfig.CDNLocal { //nolint:staticcheck // QF1003
message = "Local resources verified"
} else if policy.Mode == clientconfig.CDNServer {
message = "Server resource policy verified"
+1 -1
View File
@@ -45,7 +45,7 @@ func Run(runOptions Options) error {
studio.Shutdown()
studio.log().Info("native client window closed")
},
Bind: []interface{}{studio},
Bind: []any{studio},
EnableDefaultContextMenu: false,
DragAndDrop: &options.DragAndDrop{
DisableWebViewDrop: true,
+26 -7
View File
@@ -4,6 +4,7 @@ package app
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
@@ -42,7 +43,9 @@ func ShowFatalError(err error) {
if conversionErr != nil {
return
}
messageBoxW.Call(0, uintptr(unsafe.Pointer(message)), uintptr(unsafe.Pointer(title)), 0x10)
if result, _, callErr := messageBoxW.Call(0, uintptr(unsafe.Pointer(message)), uintptr(unsafe.Pointer(title)), 0x10); result == 0 {
log.Printf("Fatal error dialog could not be displayed: %v", callErr)
}
}
// CREATE_NO_WINDOW prevents console-subsystem helpers such as powershell.exe
@@ -74,7 +77,7 @@ func launchGame(target, proxyURL string) error {
return err
} else if running {
if !activateProcessWindow(processID, 5*time.Second) {
return fmt.Errorf("Brown Dust II is running, but its window could not be restored")
return fmt.Errorf("Brown Dust II is running, but its window could not be restored") //nolint:staticcheck // ST1005
}
return errGameAlreadyRunning
}
@@ -95,7 +98,11 @@ func windowsExecutableProcessID(name string) (uint32, bool, error) {
if err != nil {
return 0, false, err
}
defer windows.CloseHandle(snapshot)
defer func() {
if err := windows.CloseHandle(snapshot); err != nil {
log.Printf("Process snapshot cleanup failed: %v", err)
}
}()
entry := windows.ProcessEntry32{Size: uint32(unsafe.Sizeof(windows.ProcessEntry32{}))}
if err := windows.Process32First(snapshot, &entry); err != nil {
return 0, false, err
@@ -120,9 +127,14 @@ func activateProcessWindow(processID uint32, timeout time.Duration) bool {
iconic, _, _ := isIconicProc.Call(window)
if iconic != 0 {
const swRestore = 9
showWindowAsyncProc.Call(window, swRestore)
if result, _, callErr := showWindowAsyncProc.Call(window, swRestore); result == 0 {
log.Printf("Game window restore request failed: %v", callErr)
}
}
if result, _, _ := setForegroundWindowProc.Call(window); result == 0 {
// Windows may deny foreground activation even for a valid game window.
log.Print("Windows declined foreground activation of the game window")
}
setForegroundWindowProc.Call(window)
return true
}
if time.Now().After(deadline) {
@@ -136,7 +148,9 @@ func topLevelWindowForProcess(processID uint32) uintptr {
var found uintptr
callback := syscall.NewCallback(func(window uintptr, _ uintptr) uintptr {
var owner uint32
getWindowThreadProcessIDProc.Call(window, uintptr(unsafe.Pointer(&owner)))
if thread, _, _ := getWindowThreadProcessIDProc.Call(window, uintptr(unsafe.Pointer(&owner))); thread == 0 {
return 1 // The window disappeared during enumeration.
}
visible, _, _ := isWindowVisibleProc.Call(window)
if owner == processID && visible != 0 {
found = window
@@ -144,6 +158,11 @@ func topLevelWindowForProcess(processID uint32) uintptr {
}
return 1
})
enumWindowsProc.Call(callback, 0)
if result, _, callErr := enumWindowsProc.Call(callback, 0); result == 0 && found == 0 {
// A successful match deliberately stops enumeration and also returns zero.
if callErr != syscall.Errno(0) {
log.Printf("Game window enumeration failed: %v", callErr)
}
}
return found
}
+1 -1
View File
@@ -162,7 +162,7 @@ func Save(gameDir string, in Settings) (Settings, error) {
return Settings{}, fmt.Errorf("client config: create temporary config: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
defer func() { _ = os.Remove(temporaryPath) }()
if err = temporary.Chmod(0o600); err == nil {
_, err = temporary.Write(data)
}
+1 -1
View File
@@ -76,7 +76,7 @@ func SavePreferences(gameDirectory string) error {
return fmt.Errorf("stage client preferences: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
defer func() { _ = os.Remove(temporaryPath) }()
if err = temporary.Chmod(0o600); err == nil {
_, err = temporary.Write(data)
}
+1 -1
View File
@@ -25,7 +25,7 @@ func cryptPages(in []byte, encrypt bool) ([]byte, error) {
}
out := make([]byte, len(in))
for start := 0; start < len(in); start += PageSize {
var mode cipher.BlockMode = cipher.NewCBCEncrypter(block, Header)
mode := cipher.NewCBCEncrypter(block, Header)
if !encrypt {
mode = cipher.NewCBCDecrypter(block, Header)
}
+3 -6
View File
@@ -199,10 +199,7 @@ func urlInDB(p []byte) (string, error) {
break
}
i += start + len(marker)
limit := i + 128
if limit > len(p) {
limit = len(p)
}
limit := min(i+128, len(p))
if j := bytes.Index(p[i:limit], []byte("http")); j >= 0 {
at := i + j
if at+len(oldURL) <= len(p) {
@@ -229,7 +226,7 @@ func copyFile(src, dst string) error {
if err != nil {
return err
}
defer in.Close()
defer func() { _ = in.Close() }()
out, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
if err != nil {
return err
@@ -433,7 +430,7 @@ func findIntro(file []byte) (textAsset, []byte, error) {
return textAsset{}, nil, fmt.Errorf("introdb: invalid object count %d", count)
}
var candidates []textAsset
for i := int32(0); i < count; i++ {
for i := range count {
// Since serialized version 14, Unity aligns object records to four bytes
// before their 64-bit path ID (not to an eight-byte boundary).
r.off = (r.off + 48 + 3) &^ 3
+1 -1
View File
@@ -125,7 +125,7 @@ func installData(spec Spec, pluginDir string, sourceData []byte) (Result, error)
return Result{}, fmt.Errorf("clientplugin: create temporary plugin: %w", err)
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
defer func() { _ = os.Remove(temporaryPath) }()
if _, err = io.Copy(temporary, bytes.NewReader(sourceData)); err == nil {
err = temporary.Sync()
}
+3 -4
View File
@@ -126,7 +126,7 @@ func readCatalog(path string) ([]byte, error) {
if err != nil {
return nil, fmt.Errorf("open local ServerData catalog: %w", err)
}
defer file.Close()
defer func() { _ = file.Close() }()
raw, err := io.ReadAll(io.LimitReader(file, maximumCatalogBytes+1))
if err != nil {
return nil, fmt.Errorf("read local ServerData catalog: %w", err)
@@ -163,8 +163,7 @@ func localizeCatalog(raw []byte, release, aa string) ([]byte, error) {
remoteCount++
continue
}
if strings.HasPrefix(internalID, localCatalogPrefix) {
relative := strings.TrimPrefix(internalID, localCatalogPrefix)
if relative, ok := strings.CutPrefix(internalID, localCatalogPrefix); ok {
relative = filepath.FromSlash(strings.ReplaceAll(relative, "\\", "/"))
if info, err := os.Stat(filepath.Join(aa, relative)); err != nil || !info.Mode().IsRegular() {
return nil, fmt.Errorf("built-in Addressables catalog references a missing local bundle: %s", relative)
@@ -288,7 +287,7 @@ func replaceCatalog(path string, data []byte) error {
return err
}
temporaryPath := temporary.Name()
defer os.Remove(temporaryPath)
defer func() { _ = os.Remove(temporaryPath) }()
if err = temporary.Chmod(0o600); err == nil {
_, err = temporary.Write(data)
}
+3 -3
View File
@@ -209,7 +209,7 @@ func FetchResourcePolicy(ctx context.Context, client *http.Client, settings clie
if err != nil {
return ResourcePolicy{}, fmt.Errorf("request server resource policy: %w", err)
}
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
limited := io.LimitReader(response.Body, 64<<10)
responseBody, err := io.ReadAll(limited)
if err != nil {
@@ -272,10 +272,10 @@ func detectClientVersion(path string) (string, error) {
return "", fmt.Errorf("read Brown Dust II client version metadata: %w", err)
}
if !info.Mode().IsRegular() {
return "", errors.New("Brown Dust II client version metadata is not a regular file")
return "", errors.New("Brown Dust II client version metadata is not a regular file") //nolint:staticcheck // ST1005
}
if info.Size() <= 0 || info.Size() > 64<<20 {
return "", fmt.Errorf("Brown Dust II client version metadata has an invalid size: %d", info.Size())
return "", fmt.Errorf("Brown Dust II client version metadata has an invalid size: %d", info.Size()) //nolint:staticcheck // ST1005
}
data, err := os.ReadFile(path)
if err != nil {
+5 -1
View File
@@ -33,7 +33,11 @@ func TestLoginPortraitUsesSQLiteSelectionAfterRestart(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer func() {
if err := r.Close(); err != nil {
t.Error(err)
}
}()
d, err = deck.OpenStore(r, seed)
if err != nil {
t.Fatal(err)
@@ -27,7 +27,7 @@ func (r *Repository) SaveWithEntries(domain string, core []byte, changes []state
if err != nil {
return err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
if err := saveWithEntries(tx, domain, core, changes); err != nil {
return err
}
+5 -1
View File
@@ -40,11 +40,15 @@ func (r *Repository) InitializationState(accountDomains ...string) (Initializati
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
rows.Close()
_ = rows.Close()
return InitializationCorrupt, err
}
found = append(found, name)
}
if err := rows.Err(); err != nil {
_ = rows.Close()
return InitializationCorrupt, err
}
if err := rows.Close(); err != nil {
return InitializationCorrupt, err
}
@@ -38,7 +38,11 @@ func TestInitializationMarkerMakesSchemaOnlyRollbackRetryable(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer repository.Close()
defer func() {
if err := repository.Close(); err != nil {
t.Error(err)
}
}()
if repository.IsNew() {
t.Fatal("schema-only retry incorrectly depends on file creation")
}
@@ -76,7 +80,11 @@ func TestInitializationMarkerCommitsCompleteAccountExactlyOnce(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer repository.Close()
defer func() {
if err := repository.Close(); err != nil {
t.Error(err)
}
}()
if state, err := repository.InitializationState("mail", "wallet"); err != nil || state != InitializationComplete {
t.Fatalf("committed state=%d err=%v", state, err)
}
@@ -87,7 +95,11 @@ func TestInitializationStateRejectsPartialAccount(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer repository.Close()
defer func() {
if err := repository.Close(); err != nil {
t.Error(err)
}
}()
if err := repository.Save("mail", []byte(`{"version":1}`)); err != nil {
t.Fatal(err)
}
+5 -5
View File
@@ -65,7 +65,7 @@ func (t *Tx) ListEntries(domain, bucket string) (map[string][]byte, error) {
if err != nil {
return nil, fmt.Errorf("accountstate: list entries: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
entries := make(map[string][]byte)
for rows.Next() {
var key string
@@ -144,7 +144,7 @@ func (r *Repository) LoadEntry(domain, bucket, key string) ([]byte, bool, error)
if err != nil {
return nil, false, err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
return tx.LoadEntry(domain, bucket, key)
}
@@ -160,7 +160,7 @@ func (r *Repository) ListEntries(domain, bucket string) (map[string][]byte, erro
if err != nil {
return nil, err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
return tx.ListEntries(domain, bucket)
}
@@ -176,7 +176,7 @@ func (r *Repository) PutEntry(domain, bucket, key string, payload []byte) error
if err != nil {
return err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
if err := tx.PutEntry(domain, bucket, key, payload); err != nil {
return err
}
@@ -195,7 +195,7 @@ func (r *Repository) DeleteEntry(domain, bucket, key string) (bool, error) {
if err != nil {
return false, err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
deleted, err := tx.DeleteEntry(domain, bucket, key)
if err != nil {
return false, err
@@ -147,7 +147,11 @@ func TestOldSchemaIsRejectedWithoutMutation(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
var version string
if err := db.QueryRow(`SELECT value FROM metadata WHERE key = 'schema_version'`).Scan(&version); err != nil || version != "0" {
t.Fatalf("changed unsupported version %q: %v", version, err)
@@ -107,7 +107,7 @@ func claimWriterEpoch(ctx context.Context, db *sql.DB) (int64, error) {
if err != nil {
return 0, fmt.Errorf("accountstate: begin writer claim: %w", err)
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
var raw string
if err := tx.QueryRowContext(ctx, `SELECT value FROM metadata WHERE key='writer_epoch'`).Scan(&raw); err != nil {
return 0, fmt.Errorf("accountstate: read writer epoch: %w", err)
@@ -148,7 +148,7 @@ func (r *Repository) RequireDomains(required ...string) error {
if err != nil {
return fmt.Errorf("accountstate: list domains: %w", err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var found []string
for rows.Next() {
var name string
@@ -225,7 +225,7 @@ func (r *Repository) LoadContext(ctx context.Context, name string) ([]byte, int6
if err != nil {
return nil, 0, false, err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
return tx.Load(name)
}
@@ -236,7 +236,7 @@ func (r *Repository) SaveContext(ctx context.Context, name string, payload []byt
if err != nil {
return 0, err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
generation, err := tx.Save(name, payload)
if err != nil {
return 0, err
@@ -94,7 +94,11 @@ func TestRollbackAndReopen(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer reopened.Close()
defer func() {
if err := reopened.Close(); err != nil {
t.Error(err)
}
}()
requireState(t, reopened, "wallet", []byte("committed"), 2)
var mode string
if err := reopened.db.QueryRow("PRAGMA journal_mode").Scan(&mode); err != nil || mode != "wal" {
@@ -241,7 +245,11 @@ func TestSecondRepositoryIsRejectedUntilWriterCloses(t *testing.T) {
if err != nil {
t.Fatalf("Open after writer Close: %v", err)
}
defer second.Close()
defer func() {
if err := second.Close(); err != nil {
t.Error(err)
}
}()
}
func TestWriterEpochStillFencesStaleRepositoryBeforeMutation(t *testing.T) {
@@ -333,7 +341,11 @@ func BenchmarkRequestTransaction(b *testing.B) {
if err != nil {
b.Fatal(err)
}
defer r.Close()
defer func() {
if err := r.Close(); err != nil {
b.Error(err)
}
}()
ctx := context.Background()
payload := bytes.Repeat([]byte("x"), 4096)
b.ResetTimer()
+11 -4
View File
@@ -36,8 +36,7 @@ func initialize(ctx context.Context, db *sql.DB, fresh bool) error {
if err != nil {
return fmt.Errorf("accountstate: begin schema transaction: %w", err)
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
if fresh {
if err := createV1(ctx, tx); err != nil {
return err
@@ -178,11 +177,15 @@ func validateSchemaTables(tx *sql.Tx, version int) error {
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
rows.Close()
_ = rows.Close()
return err
}
names = append(names, name)
}
if err := rows.Err(); err != nil {
_ = rows.Close()
return err
}
if err := rows.Close(); err != nil {
return err
}
@@ -222,11 +225,15 @@ func tableColumns(tx *sql.Tx, table string) ([]schemaColumn, error) {
var entry schemaColumn
var defaultValue any
if err := columns.Scan(&ordinal, &entry.name, &entry.kind, &entry.notNull, &defaultValue, &entry.pk); err != nil {
columns.Close()
_ = columns.Close()
return nil, err
}
actual = append(actual, entry)
}
if err := columns.Err(); err != nil {
_ = columns.Close()
return nil, err
}
if err := columns.Close(); err != nil {
return nil, err
}
+41 -11
View File
@@ -15,7 +15,11 @@ func createV1Database(t *testing.T, path string, domain, payload string) {
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
@@ -52,12 +56,16 @@ func TestMigrationV1ToV2IsRepeatSafe(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
for range 2 {
if err := migrateV1ToV2(context.Background(), tx); err != nil {
t.Fatal(err)
@@ -75,12 +83,16 @@ func TestMigrationV2ToV3IsRepeatSafe(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
if err := migrateV1ToV2(context.Background(), tx); err != nil {
t.Fatal(err)
}
@@ -102,7 +114,11 @@ func TestOpenMigratesV1ToCurrent(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer func() {
if err := r.Close(); err != nil {
t.Error(err)
}
}()
version, err := r.SchemaVersion()
if err != nil || version != schemaVersion {
t.Fatalf("schema version %d, error %v", version, err)
@@ -116,7 +132,11 @@ func TestMigrationsRejectMissingAndNonAdjacentSteps(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
for _, test := range []struct {
name string
steps []migration
@@ -130,7 +150,7 @@ func TestMigrationsRejectMissingAndNonAdjacentSteps(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
err = runMigrations(context.Background(), tx, 1, 3, test.steps)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("error %v, want %q", err, test.want)
@@ -143,14 +163,20 @@ func TestValidationFailureRollsBackMigrationAndVersion(t *testing.T) {
path := filepath.Join(t.TempDir(), "state.db")
createV1Database(t, path, "progress", `{"quests":{"0:1":{"QuestID":1,"PackID":0}},"cleared_quests":{}}`)
if r, err := Open(path); err == nil {
r.Close()
if err := r.Close(); err != nil {
t.Error(err)
}
t.Fatal("opened state rejected by final validation")
}
db, err := sql.Open("sqlite", path)
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
var version string
if err := db.QueryRow(`SELECT value FROM metadata WHERE key = 'schema_version'`).Scan(&version); err != nil || version != "1" {
t.Fatalf("schema version %q after rollback: %v", version, err)
@@ -168,7 +194,11 @@ func TestMigrationFailureRollsBackEarlierStepWork(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer db.Close()
defer func() {
if err := db.Close(); err != nil {
t.Error(err)
}
}()
tx, err := db.Begin()
if err != nil {
t.Fatal(err)
@@ -29,7 +29,11 @@ func TestStartingPackPolicyPersistsAndRejectsChanges(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer func() {
if err := r.Close(); err != nil {
t.Error(err)
}
}()
op, err = r.BeginOperation()
if err != nil {
t.Fatal(err)
@@ -72,7 +76,11 @@ func TestStartingPackPolicyRollbackAllowsAnotherChoice(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer r.Close()
defer func() {
if err := r.Close(); err != nil {
t.Error(err)
}
}()
op, err = r.BeginOperation()
if err != nil {
t.Fatal(err)
@@ -94,7 +102,7 @@ func TestStartingPackPolicyMissingExistingRejectsInitialization(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer op.Rollback()
defer func() { _ = op.Rollback() }()
for _, initialize := range []bool{false, true} {
if _, err := r.LockStartingPack(1, initialize); err == nil {
t.Fatalf("accepted missing existing policy initialize=%v", initialize)
@@ -113,7 +121,7 @@ func TestStartingPackPolicyRejectsInvalidValues(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer op.Rollback()
defer func() { _ = op.Rollback() }()
if _, err := r.LockStartingPack(1, false); err == nil {
t.Fatal("accepted invalid stored value")
}
@@ -124,7 +132,7 @@ func TestStartingPackPolicyRejectsInvalidValues(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer op.Rollback()
defer func() { _ = op.Rollback() }()
for _, configured := range []int{0, 2, -1} {
if _, err := r.LockStartingPack(configured, true); err == nil {
t.Fatalf("accepted invalid configured value %d", configured)
+7 -6
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strconv"
"strings"
@@ -131,7 +132,7 @@ func duplicateIndexProblems(code string, path []string, values []uint64) []Probl
duplicates = append(duplicates, value)
}
}
sort.Slice(duplicates, func(i, j int) bool { return duplicates[i] < duplicates[j] })
slices.Sort(duplicates)
problems := make([]Problem, 0, len(duplicates))
for _, value := range duplicates {
problems = append(problems, Problem{Code: code, Path: path, Message: "duplicate inventory identity", RelatedIDs: []uint64{value}})
@@ -206,7 +207,7 @@ func (r *Repository) Validate() ([]Problem, error) {
if err != nil {
return nil, err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
return tx.Validate()
}
@@ -313,7 +314,7 @@ func indexedEntries(tx *sql.Tx, domain, bucket string, readUseChar bool) ([]uint
if err != nil {
return nil, nil, fmt.Errorf("accountstate: list %s.%s for validation: %w", domain, bucket, err)
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var indices []uint64
var users []namedIndex
for rows.Next() {
@@ -349,7 +350,7 @@ func namedUintEntries(tx *sql.Tx, domain, bucket string) ([]namedIndex, error) {
if err != nil {
return nil, err
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var entries []namedIndex
for rows.Next() {
var entry namedIndex
@@ -370,7 +371,7 @@ func boolEntries(tx *sql.Tx, domain, bucket string) (map[string]bool, error) {
if err != nil {
return nil, err
}
defer rows.Close()
defer func() { _ = rows.Close() }()
entries := make(map[string]bool)
for rows.Next() {
var key string
@@ -391,7 +392,7 @@ func indexedGrantEntries(tx *sql.Tx) ([]indexedGrant, error) {
if err != nil {
return nil, err
}
defer rows.Close()
defer func() { _ = rows.Close() }()
var grants []indexedGrant
for rows.Next() {
var grant indexedGrant
+11 -17
View File
@@ -92,7 +92,7 @@ func securityHeaders(next http.Handler) http.Handler {
}
func decodeJSON(w http.ResponseWriter, r *http.Request, target any) bool {
defer r.Body.Close()
defer func() { _ = r.Body.Close() }()
data, err := io.ReadAll(io.LimitReader(r.Body, 16<<10+1))
if err != nil || len(data) > 16<<10 {
http.Error(w, "request too large", http.StatusRequestEntityTooLarge)
@@ -157,7 +157,7 @@ func (s *Service) createDevice(w http.ResponseWriter, r *http.Request) {
http.Error(w, "could not create transaction", http.StatusInternalServerError)
return
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
if err := cleanupExpired(tx, now.Unix()); err != nil {
http.Error(w, "could not create transaction", http.StatusInternalServerError)
return
@@ -396,7 +396,7 @@ func (s *Service) exchangeIdentity(ctx context.Context, provider, code, verifier
if err != nil {
return providerIdentity{}, networkProviderFailure(ctx, provider, "token_exchange", err)
}
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
if response.StatusCode != http.StatusOK {
return providerIdentity{}, rejectedProviderFailure(provider, "token_exchange", response)
}
@@ -422,7 +422,7 @@ func (s *Service) exchangeIdentity(ctx context.Context, provider, code, verifier
if err != nil {
return providerIdentity{}, networkProviderFailure(ctx, provider, "userinfo", err)
}
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
if response.StatusCode != http.StatusOK {
return providerIdentity{}, rejectedProviderFailure(provider, "userinfo", response)
}
@@ -449,7 +449,7 @@ func (s *Service) verifyGoogleIDToken(ctx context.Context, idToken, nonce string
if err != nil {
return providerIdentity{}, networkProviderFailure(ctx, "google", "id_token_verify", err)
}
defer response.Body.Close()
defer func() { _ = response.Body.Close() }()
if response.StatusCode != http.StatusOK {
return providerIdentity{}, rejectedProviderFailure("google", "id_token_verify", response)
}
@@ -499,7 +499,7 @@ func (s *Service) completeDevice(deviceID, provider string, identity providerIde
if err != nil {
return err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
var accountID, status string
subjectHash := s.store.identityDigest(identity.issuer, identity.subject)
err = tx.QueryRow(`SELECT i.account_id,a.status FROM identities i JOIN accounts a ON a.id=i.account_id WHERE i.issuer=? AND i.subject_hash=?`, identity.issuer, subjectHash).Scan(&accountID, &status)
@@ -646,7 +646,7 @@ func (s *Service) poll(w http.ResponseWriter, r *http.Request) {
http.Error(w, "login result unavailable", http.StatusInternalServerError)
return
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
var storedHash, sealed []byte
var status, errorCode string
var expires int64
@@ -773,7 +773,7 @@ func (s *Service) refresh(w http.ResponseWriter, r *http.Request) {
http.Error(w, "refresh unavailable", http.StatusInternalServerError)
return
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
if err := cleanupExpired(tx, now.Unix()); err != nil {
http.Error(w, "refresh unavailable", http.StatusInternalServerError)
return
@@ -914,14 +914,8 @@ func refreshAttemptSealID(familyID string, attemptHash []byte) string {
}
func (r refreshAttemptResult) deviceResult(now int64) deviceResult {
accessTTL := r.AccessExpiresAt - now
if accessTTL < 0 {
accessTTL = 0
}
refreshTTL := r.RefreshExpiresAt - now
if refreshTTL < 0 {
refreshTTL = 0
}
accessTTL := max(r.AccessExpiresAt-now, 0)
refreshTTL := max(r.RefreshExpiresAt-now, 0)
return deviceResult{
Provider: r.Provider, AccessToken: r.AccessToken, AccessExpiresIn: accessTTL,
RefreshToken: r.RefreshToken, RefreshExpiresIn: refreshTTL,
@@ -945,7 +939,7 @@ func (s *Service) revoke(w http.ResponseWriter, r *http.Request) {
http.Error(w, "revocation unavailable", http.StatusInternalServerError)
return
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
var familyID, accountStatus string
var expires int64
var revoked sql.NullInt64
+2 -2
View File
@@ -401,7 +401,7 @@ func TestJSONLimitsAndSecurityHeaders(t *testing.T) {
func TestCreateDeviceLimitsPendingTransactionsPerClient(t *testing.T) {
service, store := testService(t)
handler := service.Handler()
for i := 0; i < 5; i++ {
for i := range 5 {
response := postJSON(handler, "/auth/device", map[string]string{"provider": "discord"})
if response.Code != http.StatusCreated {
t.Fatalf("create %d status=%d body=%q", i, response.Code, response.Body.String())
@@ -434,7 +434,7 @@ func TestDecodeProviderJSONRejectsOversizeAndTrailingValues(t *testing.T) {
func TestRequestLimiterIsBoundedAndExpiresWindows(t *testing.T) {
limiter := requestLimiter{windows: make(map[string]limitWindow)}
now := time.Unix(testNowUnix, 0)
for i := 0; i < 4096; i++ {
for i := range 4096 {
if !limiter.allow(strconv.Itoa(i), now, time.Minute, 1) {
t.Fatalf("rejected window %d before capacity", i)
}
+1 -1
View File
@@ -64,7 +64,7 @@ func Open(path string, masterKey []byte) (*Store, error) {
if err != nil {
return nil, err
}
defer tx.Rollback()
defer func() { _ = tx.Rollback() }()
statements := []string{
`CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY NOT NULL, value TEXT NOT NULL) WITHOUT ROWID`,
`CREATE TABLE IF NOT EXISTS accounts (id TEXT PRIMARY KEY NOT NULL, status TEXT NOT NULL, created_at INTEGER NOT NULL, last_login_at INTEGER NOT NULL) WITHOUT ROWID`,
+13 -3
View File
@@ -16,7 +16,11 @@ func TestStoreRequiresAndClearsExactMasterKey(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer store.Close()
defer func() {
if err := store.Close(); err != nil {
t.Error(err)
}
}()
for index, value := range key {
if value != 0 {
t.Fatalf("master key byte %d was retained by the caller buffer", index)
@@ -38,7 +42,9 @@ func TestStoreMigratesSchemaV1ToV2(t *testing.T) {
t.Fatal(err)
}
if _, err := db.Exec(`DROP TABLE refresh_attempts; UPDATE metadata SET value='1' WHERE key='schema_version'`); err != nil {
db.Close()
if err := db.Close(); err != nil {
t.Error(err)
}
t.Fatal(err)
}
if err := db.Close(); err != nil {
@@ -48,7 +54,11 @@ func TestStoreMigratesSchemaV1ToV2(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer reopened.Close()
defer func() {
if err := reopened.Close(); err != nil {
t.Error(err)
}
}()
var version int
if err := reopened.db.QueryRow(`SELECT CAST(value AS INTEGER) FROM metadata WHERE key='schema_version'`).Scan(&version); err != nil {
t.Fatal(err)
+3 -3
View File
@@ -25,7 +25,7 @@ type Config struct {
PublicURL string `json:"public_url,omitempty"`
MasterKeyEnv string `json:"master_key_env,omitempty"`
Providers map[string]ProviderConfig `json:"providers,omitempty"`
Session SessionConfig `json:"session,omitempty"`
Session SessionConfig `json:"session"`
}
type ProviderConfig struct {
@@ -68,7 +68,7 @@ func Load(path string) (Config, error) {
if err != nil {
return Config{}, fmt.Errorf("authconfig: open %s: %w", path, err)
}
defer file.Close()
defer func() { _ = file.Close() }()
var config Config
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields()
@@ -113,7 +113,7 @@ func (c Config) Validate() error {
return errors.New("public_url must be an absolute origin without path, query, fragment, or user info")
}
localhost := publicURL.Hostname() == "127.0.0.1" || publicURL.Hostname() == "localhost" || publicURL.Hostname() == "::1"
if publicURL.Scheme != "https" && !(localhost && publicURL.Scheme == "http") {
if publicURL.Scheme != "https" && !(localhost && publicURL.Scheme == "http") { //nolint:staticcheck // QF1001
return errors.New("public_url must use HTTPS except on localhost")
}
for name, provider := range c.Providers {
+1 -1
View File
@@ -405,7 +405,7 @@ func (s *Service) Handle(path string, request []byte) (int, []byte, bool, error)
if field.Type != 2 {
return nil
}
if field.Number == 4 {
if field.Number == 4 { //nolint:staticcheck // QF1003
response = wire.AppendBytes(response, 1, field.Value)
} else if field.Number == 5 {
response = wire.AppendBytes(response, 2, field.Value)
+1 -1
View File
@@ -245,7 +245,7 @@ func TestBattleStateIsIsolatedPerGameSession(t *testing.T) {
func TestRepeatedCurrentSessionSelectionDoesNotEvictOtherBattles(t *testing.T) {
service := NewService("", "", nil, nil)
for index := 0; index < 1024; index++ {
for index := range 1024 {
service.BeginSession(fmt.Sprintf("session-%d", index))
}
service.BeginSession("session-1023")
+1 -1
View File
@@ -31,7 +31,7 @@ func TestFieldBuffConsumptionAfterValidationAndBeforeBattleActivation(t *testing
t.Fatal("persistence failure activated battle")
}
fail = false
for i := 0; i < 2; i++ {
for range 2 {
if _, _, _, err := s.Handle("/BattleEnter", enter); err != nil {
t.Fatal(err)
}
+1 -1
View File
@@ -80,7 +80,7 @@ func TestBinaryRejectsCorruptionLimitsAndTrailing(t *testing.T) {
if e != nil {
t.Fatal(e)
}
for n := 0; n < len(raw); n++ {
for n := range len(raw) {
if _, e = UnmarshalBinary(raw[:n]); e == nil {
t.Fatalf("accepted truncation %d", n)
}
+2 -3
View File
@@ -3,6 +3,7 @@ package calendar
import (
"bd2server/internal/server/readonly"
"fmt"
"maps"
)
func scalar(n int, v uint64) readonly.Field { return readonly.Field{Number: n, Type: 0, Varint: v} }
@@ -29,9 +30,7 @@ func (s *Set) ApplyReadonly(seed *readonly.Seed) (*readonly.Seed, error) {
return nil, fmt.Errorf("calendar: missing calendar/static seed")
}
result := &readonly.Seed{Version: seed.Version, Responses: make(map[string]readonly.Response, len(seed.Responses))}
for path, response := range seed.Responses {
result.Responses[path] = response
}
maps.Copy(result.Responses, seed.Responses)
var hunt []readonly.Field
if h := s.MonsterHunt; h != nil {
for _, v := range h.Seasons {
@@ -84,10 +84,10 @@ func TestClearPackageRejectsConflictingMalformedAndSpoofedRows(t *testing.T) {
row = wire.AppendVarint(row, 2, 12)
row = wire.AppendVarint(row, 3, 2)
prefix := wire.AppendVarint(nil, 1, 1)
row = row[:len(row):len(row)]
prefix = prefix[:len(prefix):len(prefix)]
row = row[:len(row):len(row)] //nolint:modernize // slicesclip
prefix = prefix[:len(prefix):len(prefix)] //nolint:modernize // slicesclip
active := wire.AppendBytes(append([]byte(nil), prefix...), 3, row)
active = active[:len(active):len(active)]
active = active[:len(active):len(active)] //nolint:modernize // slicesclip
requests := map[string][]byte{
"missing row": prefix,
"empty active": wire.AppendBytes(prefix, 3, nil),
@@ -269,7 +269,7 @@ func TestClearPackageMailSQLiteAtomicRetryAndReconnect(t *testing.T) {
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, s, mailbox, _, _ = open(false)
_, s, mailbox, _, _ = open(false)
// A replay must restore the exact response without requiring progress again
// or issuing a second mail, including after sequence/session changes.
s.AttachProgress(nil, nil)
+11 -4
View File
@@ -139,7 +139,10 @@ func TestCashPurchaseMailSQLiteRollbackRestartBatchAndReplay(t *testing.T) {
t.Fatal(err)
}
}
repo, shop, mailbox, wallet, items := open()
var mailbox *mail.Service
var wallet *player.Wallet
var items *player.Inventory
repo, shop, _, _, _ := open()
shop.AttachPurchaseHook(func(string, gamedata.CashProductDesign, uint64) error {
return fmt.Errorf("injected failure after issuing mail")
})
@@ -222,7 +225,7 @@ func TestCashPurchaseMailSQLiteRollbackRestartBatchAndReplay(t *testing.T) {
t.Fatal("purchase replay selected or charged again")
}
_ = repo.Close()
repo, shop, mailbox, wallet, items = open()
repo, _, mailbox, _, _ = open()
claim := wire.AppendVarint(nil, 1, 3)
claim = wire.AppendVarint(claim, 2, 1)
claim = wire.AppendVarint(claim, 2, 2)
@@ -235,7 +238,7 @@ func TestCashPurchaseMailSQLiteRollbackRestartBatchAndReplay(t *testing.T) {
}
rollback(op)
_ = repo.Close()
repo, shop, mailbox, wallet, items = open()
repo, _, mailbox, wallet, items = open()
if wallet.Snapshot().Gold != 0 || len(items.All()) != 0 {
t.Fatal("claim rollback retained rewards")
}
@@ -267,7 +270,11 @@ func TestCashPurchaseMailSQLiteRollbackRestartBatchAndReplay(t *testing.T) {
}
_ = repo.Close()
repo, _, mailbox, wallet, items = open()
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
op, err = repo.BeginOperation()
if err != nil {
t.Fatal(err)
+4 -10
View File
@@ -126,7 +126,7 @@ func (e *EntitlementEconomy) applyPrepared(identity string, costs, rewards []gam
}
input := make([]gamedata.BattleReward, len(rewards))
for i, r := range rewards {
input[i] = gamedata.BattleReward{Type: r.Type, ID: r.ID, Count: r.Count}
input[i] = gamedata.BattleReward{Type: r.Type, ID: r.ID, Count: r.Count} //nolint:staticcheck // S1016
}
leaves := input
if !resolved {
@@ -184,10 +184,7 @@ func (e *EntitlementEconomy) applyPrepared(identity string, costs, rewards []gam
return nil, fmt.Errorf("commerce: subscription reward schedule missing")
}
now := e.now().UnixMilli()
expiry := int64(e.items.ContentTicketExpiry(r.ID))
if expiry < now {
expiry = now
}
expiry := max(int64(e.items.ContentTicketExpiry(r.ID)), now)
if r.Count > uint64((math.MaxInt64-expiry)/(30*86400000)) {
return nil, fmt.Errorf("commerce: subscription expiry overflow")
}
@@ -201,7 +198,7 @@ func (e *EntitlementEconomy) applyPrepared(identity string, costs, rewards []gam
sub.Expiry = expiry
s.Subscriptions[key] = sub
default:
regular = append(regular, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count})
regular = append(regular, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count}) //nolint:staticcheck // S1016
if r.Type == 19 && e.design.TicketTypes[r.ID] == 3 && len(e.design.Attendance[r.ID]) > 0 {
key := strconv.FormatUint(r.ID, 10)
if _, exists := s.Subscriptions[key]; !exists {
@@ -314,10 +311,7 @@ func (e *EntitlementEconomy) MergeAttendance(response []byte) ([]byte, error) {
}
if typ := e.design.AttendanceTypes[ticket]; typ != 0 {
rewards := []byte{}
n := sub.Claimed
if n > uint64(len(e.design.Attendance[ticket])) {
n = uint64(len(e.design.Attendance[ticket]))
}
n := min(sub.Claimed, uint64(len(e.design.Attendance[ticket])))
for i := uint64(1); i <= n; i++ {
rewards = wire.AppendVarint(rewards, 1, i)
}
@@ -124,12 +124,18 @@ func TestNativeEventShopSQLiteRollsBackDebitGrantAndPurchaseCount(t *testing.T)
if err = op.Rollback(); err != nil && !errors.Is(err, stateio.ErrStateRecoveryRequired) {
t.Fatal(err)
}
repo.Close()
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, err = accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
wallet, err = player.OpenWallet(repo, player.Currency{})
if err != nil {
t.Fatal(err)
@@ -66,7 +66,9 @@ func TestInstalledCashProductRewardCoverage23510(t *testing.T) {
if err := rowsDB.Err(); err != nil {
t.Fatal(err)
}
rowsDB.Close()
if err := rowsDB.Close(); err != nil {
t.Fatal(err)
}
release()
policy, err := NewCatalog("2.35.10", catalog, gameconfig.Default().Purchases)
if err != nil {
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"bd2server/internal/server/wire"
"encoding/json"
"fmt"
"sort"
"slices"
"sync"
"time"
)
@@ -92,7 +92,7 @@ func (s *LoginPasses) ClaimAndInfo(identity string) ([]byte, [][]byte, error) {
for group := range s.design.Groups {
keys = append(keys, group)
}
sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] })
slices.Sort(keys)
r, seen := v.Receipts[identity]
if !seen {
r = loginPassReceipt{Rewarded: map[uint64]bool{}}
@@ -155,12 +155,18 @@ func TestNativeTotalDiamondPurchaseSQLiteRollbackAndRestartReplay(t *testing.T)
if err = op.Rollback(); err != nil && !errors.Is(err, stateio.ErrStateRecoveryRequired) {
t.Fatal(err)
}
repo.Close()
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, err = accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
wallet, err = player.OpenWallet(repo, player.Currency{})
if err != nil {
t.Fatal(err)
+5 -1
View File
@@ -281,7 +281,11 @@ func TestSQLiteAccountTransactionRollsBackCostRewardAndReceipt(t *testing.T) {
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
wallet, err = player.OpenWallet(repo, player.Currency{})
if err != nil {
t.Fatal(err)
+1 -1
View File
@@ -30,7 +30,7 @@ func Ensure(path string, defaults any) error {
if err != nil {
return err
}
defer os.Remove(file.Name())
defer func() { _ = os.Remove(file.Name()) }()
if _, err = file.Write(append(data, '\n')); err == nil {
err = file.Sync()
}
+1 -1
View File
@@ -38,7 +38,7 @@ func SessionKey(userKey string) ([]byte, error) {
return nil, fmt.Errorf("%w: got %d bytes", ErrInvalidKey, len(userKey))
}
for _, c := range []byte(userKey) {
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') {
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') { //nolint:staticcheck // QF1001
return nil, fmt.Errorf("%w: session key is not hexadecimal ASCII", ErrInvalidKey)
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ func cryptPages(in []byte, encrypt bool) ([]byte, error) {
}
out := make([]byte, len(in))
for start := 0; start < len(in); start += PageSize {
var mode cipher.BlockMode = cipher.NewCBCEncrypter(block, Header)
mode := cipher.NewCBCEncrypter(block, Header)
if !encrypt {
mode = cipher.NewCBCDecrypter(block, Header)
}
+1 -1
View File
@@ -110,7 +110,7 @@ func (s *Store) handleAutoRecovery(req []byte) (int, []byte, bool, error) {
}
return nil
}
if n.FieldCharControlDeckType == 0 {
if n.FieldCharControlDeckType == 0 { //nolint:staticcheck // QF1003
for _, v := range n.Deck {
if e = add(v.CharacterInvenIndex); e != nil {
return fail(e)
+19 -7
View File
@@ -14,9 +14,15 @@ func recoveryFixture(t *testing.T) *presetFixture {
if e := f.deck.AttachFieldSettings(&gamedata.FieldSettingsDesign{TalentSlots: 1, CharacterTalentClass: map[uint64]uint64{350: 10, 360: 14}}); e != nil {
t.Fatal(e)
}
f.characters.AttachMaxHealth(func(player.Character) (uint64, error) { return 100, nil })
f.characters.SetCurrentHealth(100, 100)
f.characters.SetCurrentHealth(200, 0)
if err := f.characters.AttachMaxHealth(func(player.Character) (uint64, error) { return 100, nil }); err != nil {
t.Fatal(err)
}
if err := f.characters.SetCurrentHealth(100, 100); err != nil {
t.Fatal(err)
}
if err := f.characters.SetCurrentHealth(200, 0); err != nil {
t.Fatal(err)
}
if _, _, _, e := f.deck.Handle("/DeckSave", req(1, triple(200, 1, 1))); e != nil {
t.Fatal(e)
}
@@ -34,7 +40,9 @@ func TestAutoRecoveryPersistsResponseAndDisabledSettingNeverExecutes(t *testing.
if caster != 100 || len(targets) != 1 || targets[0] != 200 {
t.Fatal("wrong actual fatigue targets")
}
f.characters.SetCurrentHealth(200, 25)
if err := f.characters.SetCurrentHealth(200, 25); err != nil {
return player.AutoRecoveryResult{}, err
}
c, _ := f.characters.Find(200)
c.HP = 25
return player.AutoRecoveryResult{Caster: 100, Characters: []player.Character{c}, Experience: 2, Catalyst: 10}, nil
@@ -61,8 +69,10 @@ func TestAutoRecoveryPersistsResponseAndDisabledSettingNeverExecutes(t *testing.
if _, _, _, e = f.deck.Handle("/CharAutoReviveSet", req(4, wire.AppendVarint(nil, 3, 100))); e != nil {
t.Fatal(e)
}
f.characters.SetCurrentHealth(200, 0)
_, out, _, e = f.deck.Handle("/DeckCharAutoRevive", req(5, wire.AppendVarint(nil, 2, 100)))
if err := f.characters.SetCurrentHealth(200, 0); err != nil {
t.Fatal(err)
}
_, _, _, e = f.deck.Handle("/DeckCharAutoRevive", req(5, wire.AppendVarint(nil, 2, 100)))
if e != nil || calls != 1 {
t.Fatal("disabled automatic recovery executed", e)
}
@@ -88,7 +98,9 @@ func TestAutoRecoveryFailureReportsExhaustionAndStoryCannotRecover(t *testing.T)
if e != nil || mode != 3 || disabled != 2 {
t.Fatalf("failure mode%d disabled%d %v", mode, disabled, e)
}
f.characters.SetCurrentHealth(100, 0)
if err := f.characters.SetCurrentHealth(100, 0); err != nil {
t.Fatal(err)
}
f.deck.state.FieldCharControlDeckType = 2
f.deck.AttachAutoRecovery(func(uint64, uint64, []uint64) (player.AutoRecoveryResult, error) {
t.Fatal("story mode must not recover temporary party")
+2 -3
View File
@@ -3,6 +3,7 @@ package deck
import (
"bd2server/internal/server/wire"
"fmt"
"maps"
"math"
)
@@ -59,8 +60,6 @@ func (s *Store) handleCostumeUse(req []byte) (int, []byte, bool, error) {
}
}
n := clone(s.state)
for char, cost := range assignments {
n.Costumes[char] = cost
}
maps.Copy(n.Costumes, assignments)
return 41, nil, true, s.commit(n)
}
+5 -1
View File
@@ -64,7 +64,11 @@ func TestBatchCostumeUseRestoresCharacterSelectionsFromSQLite(t *testing.T) {
if e != nil {
t.Fatal(e)
}
defer r.Close()
defer func() {
if err := r.Close(); err != nil {
t.Error(err)
}
}()
inv, e = player.OpenInventory(r, &player.Starter{Version: "2.35.10"})
if e != nil {
t.Fatal(e)
+3 -6
View File
@@ -6,6 +6,7 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"sync"
@@ -247,13 +248,9 @@ func clone(x state) state {
y.Waypoints[k] = append([]uint64(nil), v...)
}
y.Costumes = map[uint64]uint64{}
for k, v := range x.Costumes {
y.Costumes[k] = v
}
maps.Copy(y.Costumes, x.Costumes)
y.Packs = map[uint64]uint64{}
for k, v := range x.Packs {
y.Packs[k] = v
}
maps.Copy(y.Packs, x.Packs)
return y
}
func checkSeq(req []byte) error {
+10 -4
View File
@@ -55,12 +55,14 @@ func TestTalentSlotsRestoreOwnershipAndClearSlots(t *testing.T) {
t.Fatal(e)
}
n := 0
wire.Walk(b, func(f wire.Field) error {
if err := wire.Walk(b, func(f wire.Field) error {
if f.Number == 2 {
n++
}
return nil
})
}); err != nil {
t.Fatal(err)
}
if n != 3 {
t.Fatal("DeckInfo missing empty quick slot", n)
}
@@ -85,7 +87,9 @@ func TestCharmFieldAndTalentSlotsExpireWithoutPermanentOwnership(t *testing.T) {
}
install(uint64(time.Now().Add(time.Hour).UnixMilli()))
d := &gamedata.FieldSettingsDesign{TalentSlots: 1, CharacterTalentClass: map[uint64]uint64{9010: 10}, CharacterTemporaryPack: map[uint64]int{9010: 99}}
f.deck.AttachFieldSettingsPack(func() (int, error) { return 4, nil })
if err := f.deck.AttachFieldSettingsPack(func() (int, error) { return 4, nil }); err != nil {
t.Fatal(err)
}
if e := f.deck.AttachFieldSettings(d); e != nil {
t.Fatal(e)
}
@@ -116,7 +120,9 @@ func TestTemporaryFieldAndTalentSlotsFollowPack(t *testing.T) {
}
pack := 4
d := &gamedata.FieldSettingsDesign{TalentSlots: 1, CharacterTalentClass: map[uint64]uint64{9001: 9}, CharacterTemporaryPack: map[uint64]int{9001: 4}}
f.deck.AttachFieldSettingsPack(func() (int, error) { return pack, nil })
if err := f.deck.AttachFieldSettingsPack(func() (int, error) { return pack, nil }); err != nil {
t.Fatal(err)
}
if e := f.deck.AttachFieldSettings(d); e != nil {
t.Fatal(e)
}
+6 -6
View File
@@ -5,6 +5,8 @@ import (
"encoding/json"
"errors"
"fmt"
"maps"
"slices"
"sort"
"strconv"
"strings"
@@ -471,7 +473,7 @@ func (s *Store) handlePresetInfo(request []byte) (int, []byte, bool, error) {
slots = append(slots, slot)
}
}
sort.Slice(slots, func(i, j int) bool { return slots[i] < slots[j] })
slices.Sort(slots)
var response []byte
for _, slot := range slots {
response = wire.AppendBytes(response, 1, presetWire(s.presets[slot]))
@@ -706,9 +708,7 @@ func (s *Store) handlePresetUse(request []byte) (int, []byte, bool, error) {
}
next := clone(s.state)
next.Deck = append([]DeckEntry(nil), deckEntries...)
for character, costume := range assignments {
next.Costumes[character] = costume
}
maps.Copy(next.Costumes, assignments)
if err := s.commit(next); err != nil {
return 0, nil, true, fmt.Errorf("deck: persist applied preset: %w", err)
}
@@ -732,7 +732,7 @@ func (s *Store) handlePresetUse(request []byte) (int, []byte, bool, error) {
for index := range characterSet {
indices = append(indices, index)
}
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
slices.Sort(indices)
for _, index := range indices {
if character, found := s.characters.Find(index); found {
response = wire.AppendBytes(response, 2, player.CharacterWire(character))
@@ -841,7 +841,7 @@ func (s *Store) handleCostumeSettingInfo(request []byte) (int, []byte, bool, err
for index := range s.costumeSettings {
indices = append(indices, index)
}
sort.Slice(indices, func(i, j int) bool { return indices[i] < indices[j] })
slices.Sort(indices)
var response []byte
for _, index := range indices {
response = wire.AppendBytes(response, 1, costumeSettingWire(s.costumeSettings[index]))
+2 -2
View File
@@ -117,7 +117,7 @@ func presetRequest(slot, character, costume, equipment uint64, name string) []by
base = wire.AppendVarint(base, 3, 1)
deck := wire.AppendBytes(nil, 1, base)
deck = wire.AppendVarint(deck, 2, costume)
for equipmentType := uint64(0); equipmentType < 5; equipmentType++ {
for equipmentType := range uint64(5) {
entry := wire.AppendVarint(nil, 1, equipmentType)
if equipmentType == 0 {
entry = wire.AppendVarint(entry, 2, equipment)
@@ -202,7 +202,7 @@ func TestPresetMetadataCanCreateEmptySlot(t *testing.T) {
func TestPresetAddSlotChargesOncePersistsAndCaps(t *testing.T) {
f := newPresetFixture(t)
add := req(20, wire.AppendVarint(nil, 2, 1))
for attempt := 0; attempt < 2; attempt++ {
for attempt := range 2 {
if code, _, _, err := f.deck.Handle("/PresetAddSlot", add); err != nil || code != 180 {
t.Fatalf("add attempt %d code=%d err=%v", attempt, code, err)
}
+3 -8
View File
@@ -5,7 +5,7 @@ import (
"errors"
"fmt"
"math"
"sort"
"slices"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/wire"
@@ -28,12 +28,7 @@ func (s *Store) ActivatedWaypoint(pack, id uint64) bool {
return hasWaypoint(s.state.Waypoints[pack], id)
}
func hasWaypoint(points []uint64, id uint64) bool {
for _, p := range points {
if p == id {
return true
}
}
return false
return slices.Contains(points, id)
}
func validWaypointState(packs map[uint64][]uint64) error {
@@ -75,7 +70,7 @@ func (s *Store) handleWaypoint(path string, req []byte) (int, []byte, bool, erro
}
if path == "/WaypointInfo" {
ids := append([]uint64(nil), s.state.Waypoints[pack]...)
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
slices.Sort(ids)
var packed []byte
for _, id := range ids {
if _, known := design.Points[id]; !known {
+1 -1
View File
@@ -93,7 +93,7 @@ func TestWaypointTravelChargesOnceAndRejectsInvalidBeforeSpending(t *testing.T)
}
}
valid := waypointReq(6, 1, 1, 2)
for i := 0; i < 2; i++ {
for range 2 {
code, _, _, err := s.Handle("/WaypointUse", valid)
if err != nil || code != 33 {
t.Fatalf("travel %d %v", code, err)
+3 -3
View File
@@ -124,9 +124,9 @@ func (s *Service) tacticsLines(uid uint64) uint64 {
n := uint64(5) - g.V(4)
clear := s.state.Tactics[uid]
var lines uint64
for i := uint64(0); i < n; i++ {
for i := range n {
row, col := true, true
for j := uint64(0); j < n; j++ {
for j := range n {
row = row && contains(clear, i*n+j+1)
col = col && contains(clear, j*n+i+1)
}
@@ -138,7 +138,7 @@ func (s *Service) tacticsLines(uid uint64) uint64 {
}
}
a, b := true, true
for i := uint64(0); i < n; i++ {
for i := range n {
a = a && contains(clear, i*n+i+1)
b = b && contains(clear, i*n+(n-i))
}
+1 -4
View File
@@ -39,10 +39,7 @@ func (s *Service) cafeteriaReward(req []byte, identity string) ([]byte, error) {
if last := s.state.CafeteriaLast[receiptKey]; last > 0 && now-last < int64(term)*1000 {
return nil, errors.New("eventactions: cafeteria interaction cooldown")
}
count := row.V(11)
if count > cap-s.state.CafeteriaCurrency {
count = cap - s.state.CafeteriaCurrency
}
count := min(row.V(11), cap-s.state.CafeteriaCurrency)
rewards := []gamedata.Reward{{Type: row.V(13), ID: row.V(12), Count: count}}
bundle, err := s.economy.Apply(identity, nil, rewards)
if err != nil {
@@ -3,7 +3,7 @@ package eventactions
import (
"errors"
"fmt"
"sort"
"slices"
"strconv"
"strings"
@@ -78,7 +78,7 @@ func (s *Service) dailyStory(path string, b []byte, identity string) ([]byte, er
}
}
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
slices.Sort(ids)
var out []byte
for _, id := range ids {
out = wire.AppendVarint(out, 1, id)
@@ -65,7 +65,9 @@ func TestMiniContentStoryAndQuizCompleteReplayAndWindows(t *testing.T) {
}
reopened.now = s.now
reopened.BeginSession("test")
reopened.AttachMiniContent(routes, d)
if err := reopened.AttachMiniContent(routes, d); err != nil {
t.Fatal(err)
}
_, replay, _, err := reopened.Handle("/NpcQuizClear", quiz)
if err != nil || !bytes.Equal(replay, quizReply) || e.calls != 2 {
t.Fatal("quiz restart replay", err)
+4 -5
View File
@@ -13,6 +13,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strings"
"sync"
@@ -544,12 +545,10 @@ func (s *Service) RecordTacticsClear(uid, stage uint64) error {
if _, ok = s.row("TacticsBingoTable", 4, 5, group.V(1), stage); !ok {
return errors.New("eventactions: invalid tactics stage")
}
for _, id := range s.state.Tactics[uid] {
if id == stage {
return nil
}
if slices.Contains(s.state.Tactics[uid], stage) {
return nil
}
s.state.Tactics[uid] = append(s.state.Tactics[uid], stage)
sort.Slice(s.state.Tactics[uid], func(i, j int) bool { return s.state.Tactics[uid][i] < s.state.Tactics[uid][j] })
slices.Sort(s.state.Tactics[uid])
return s.save()
}
+15 -5
View File
@@ -41,12 +41,18 @@ func TestTrackerRewardAndProgressFailureRollbackTogether(t *testing.T) {
if err = op.Rollback(); err != nil && !errors.Is(err, stateio.ErrStateRecoveryRequired) {
t.Fatal(err)
}
repo.Close()
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, err = accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
b, err := repo.Load("tracker-reward")
if err != nil || b != nil {
t.Fatal("reward survived transaction rollback")
@@ -134,12 +140,14 @@ func TestTrackerPrepareCatchResumeAndCrossSequenceRetry(t *testing.T) {
t.Fatal("reconnect lost durable catch progress")
}
caught := 0
wire.Walk(p, func(f wire.Field) error {
if err := wire.Walk(p, func(f wire.Field) error {
if f.Number == 5 {
caught++
}
return nil
})
}); err != nil {
t.Fatal(err)
}
if caught != 1 {
t.Fatal("resume lost caught monster")
}
@@ -155,7 +163,9 @@ func TestTrackerSlotsExpiryAndDailyReset(t *testing.T) {
s.now = func() time.Time { return now }
s.design.Tables["FieldSpawnEventTable"] = append(s.design.Tables["FieldSpawnEventTable"], gamedata.EventActionRow{Values: map[int]uint64{4: 1, 5: 2, 2: 1}, Text: map[int]string{7: "13:00:00", 1: "13:10:00"}})
r := events.NewRegistry()
r.Replace([]events.Schedule{{UID: 3, Type: 21, ID: 1, Start: base.Add(-time.Hour).UnixMilli(), End: base.Add(48 * time.Hour).UnixMilli()}})
if err := r.Replace([]events.Schedule{{UID: 3, Type: 21, ID: 1, Start: base.Add(-time.Hour).UnixMilli(), End: base.Add(48 * time.Hour).UnixMilli()}}); err != nil {
t.Fatal(err)
}
s.registry = r
if _, _, _, err := s.Handle("/FieldEventSpawnStart", spawnStart(1, 1)); err != nil {
t.Fatal(err)
+3 -7
View File
@@ -6,6 +6,7 @@ import (
"bd2server/internal/server/wire"
"errors"
"fmt"
"slices"
"sort"
"time"
)
@@ -41,16 +42,11 @@ func (s *Service) candidates(r gamedata.EventActionRow) []uint64 {
ids = append(ids, x.V(2))
}
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
slices.Sort(ids)
return ids
}
func contains(ids []uint64, id uint64) bool {
for _, v := range ids {
if v == id {
return true
}
}
return false
return slices.Contains(ids, id)
}
func voteWire(v *vote) []byte {
b := wire.AppendVarint(nil, 1, v.Round)
+2 -2
View File
@@ -238,7 +238,7 @@ func (s *Service) HandleSession(path string, req []byte, session string) (int, [
}
var rewards []gamedata.Reward
var changed []byte
for n := uint64(0); n < count; n++ {
for range count {
entries := g.Page(p.Page)
var weights []uint64
var sum uint64
@@ -299,7 +299,7 @@ func (s *Service) HandleSession(path string, req []byte, session string) (int, [
} else {
rs := make([]gamedata.BattleReward, len(rewards))
for i, r := range rewards {
rs[i] = gamedata.BattleReward{Type: r.Type, ID: r.ID, Count: r.Count}
rs[i] = gamedata.BattleReward{Type: r.Type, ID: r.ID, Count: r.Count} //nolint:staticcheck // S1016
}
bundle, err = s.runtime.ConsumeAndGrant("eventexchange:"+key, uses, rs)
}
+2 -6
View File
@@ -14,6 +14,7 @@ import (
"fmt"
"math"
"math/big"
"slices"
"strconv"
"strings"
"sync"
@@ -129,12 +130,7 @@ func nums(b []byte, n int) ([]uint64, error) {
}
func scalar(b []byte, n int) uint64 { v, _, _ := wire.Varint(b, n); return v }
func contains(a []uint64, v uint64) bool {
for _, x := range a {
if x == v {
return true
}
}
return false
return slices.Contains(a, v)
}
func (s *Service) Handle(path string, req []byte) (int, []byte, bool, error) {
return s.HandleSession(path, req, "local")
+3 -3
View File
@@ -48,7 +48,7 @@ func (s *Service) play(path string, req []byte, g *GameState, d *gamedata.EventG
return fail(fmt.Errorf("eventgames: invalid bingo play count"))
}
cost = plays
for i := uint64(0); i < plays; i++ {
for range plays {
var available []uint64
for pos := range g.Board {
if !contains(g.Opened, uint64(pos)) {
@@ -192,7 +192,7 @@ func (s *Service) play(path string, req []byte, g *GameState, d *gamedata.EventG
return fail(fmt.Errorf("eventgames: roulette draw exceeds limit"))
}
}
for i := uint64(0); i < n; i++ {
for range n {
force := d.Pity > 0 && g.SinceSpecial+1 >= d.Pity
var candidates []gamedata.EventGameReward
total := uint64(0)
@@ -270,7 +270,7 @@ func completeLine(g *GameState, n, typ, index uint64) bool {
if n == 0 {
return false
}
for i := uint64(0); i < n; i++ {
for i := range n {
var pos uint64
switch typ {
case 1:
+1 -1
View File
@@ -113,7 +113,7 @@ func (s *Service) CompleteBattle(req []byte, receipt string) ([]byte, error) {
}
rewards := []gamedata.Reward{}
for _, r := range rs {
rewards = append(rewards, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count})
rewards = append(rewards, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count}) //nolint:staticcheck // S1016
}
cost := num(row, 3)
var costs []gamedata.Reward
+4 -4
View File
@@ -12,7 +12,7 @@ import (
"encoding/json"
"fmt"
"math"
"sort"
"slices"
"strings"
"sync"
"time"
@@ -282,7 +282,7 @@ func (s *Service) HandleSession(path string, req []byte, session string) (int, [
func (s *Service) grant(identity string, rewards []gamedata.BattleReward) ([]byte, error) {
rs := make([]gamedata.Reward, 0, len(rewards))
for _, r := range rewards {
rs = append(rs, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count})
rs = append(rs, gamedata.Reward{Type: r.Type, ID: r.ID, Count: r.Count}) //nolint:staticcheck // S1016
}
return s.economy.Apply(identity, nil, rs)
}
@@ -440,7 +440,7 @@ func (s *Service) game(path string, req []byte, session string, next *snapshot,
for id := range d.Objects {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
slices.Sort(ids)
for _, id := range ids {
startReply = wire.AppendVarint(startReply, 1, id)
}
@@ -630,7 +630,7 @@ func (s *Service) game(path string, req []byte, session string, next *snapshot,
return nil, e
}
field := 1
if f == "Run" {
if f == "Run" { //nolint:staticcheck // QF1003
field = 3
} else if f == "Field" {
field = 5
+3 -3
View File
@@ -11,8 +11,6 @@ import (
"time"
)
func nums(b []byte, n int) ([]uint64, error) { return list(b, n) }
type econ struct{ cost, granted uint64 }
func (e *econ) Apply(_ string, c, r []gamedata.Reward) ([]byte, error) {
@@ -188,7 +186,9 @@ func TestEventBattleChallengeUnknownAndDuplicateRejectedBeforeCost(t *testing.T)
s.design.Tables["PackEventBattleTable"] = [][]byte{row}
s.AttachBattleChallenges(gamedata.EventBattleChallenges{88: {{Type: 8, Reward: gamedata.BattleReward{Type: 3, Count: 25}}}})
start := wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(wire.AppendVarint(nil, 5, 17), 8, 1), 9, 1), 4, 88)
s.EnterBattle(start, "sid:x")
if _, err := s.EnterBattle(start, "sid:x"); err != nil {
t.Fatal(err)
}
bad := wire.AppendVarint(wire.AppendVarint(nil, 2, 1), 5, 1)
if _, err := s.CompleteBattle(bad, "sid:x"); err == nil {
t.Fatal("unknownchallenge")
+4 -8
View File
@@ -5,6 +5,7 @@ import (
"bd2server/internal/server/wire"
"fmt"
"math"
"slices"
"strings"
)
@@ -52,7 +53,7 @@ func (s *Service) endScore(f string, req []byte, a Run) (uint64, error) {
return 0, e
}
maxField := 8
if a.Mode == 1 {
if a.Mode == 1 { //nolint:staticcheck // QF1003
maxField = 2
} else if a.Mode == 2 {
maxField = 1
@@ -134,7 +135,7 @@ func (s *Service) progress(path string, req []byte, rk string, next *snapshot, k
return nil, fmt.Errorf("eventplay: object invalid or already scored")
}
a.Objects = append(a.Objects, id)
if v.Type == 1 {
if v.Type == 1 { //nolint:staticcheck // QF1003
a.Score += v.Point
} else if v.Type == 2 {
a.HP += v.Point
@@ -227,11 +228,6 @@ func (s *Service) progress(path string, req []byte, rk string, next *snapshot, k
return out, nil
}
func has(a []uint64, id uint64) bool {
for _, x := range a {
if x == id {
return true
}
}
return false
return slices.Contains(a, id)
}
func seqSeed(n uint64) uint64 { return n & 2147483647 }
@@ -15,7 +15,11 @@ func TestAdditionalRewardCurrenciesAndRankChangeItemPersistAndReplay(t *testing.
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
eco, items, _ := economyFixture(t, repo, &economyGraph{})
eco.AttachOwnedItemDesign(map[uint64]map[uint64]bool{69: {1: true}})
rewards := []gamedata.Reward{{Type: 38, Count: 1}, {Type: 39, Count: 20}, {Type: 40, Count: 30}, {Type: 54, Count: 40}, {Type: 70, Count: 50}, {Type: 69, ID: 1, Count: 2}}
@@ -78,12 +82,18 @@ func TestAdditionalRewardCurrenciesAndRankChangeItemPersistAndReplay(t *testing.
t.Fatal(err)
}
_ = op.Rollback()
repo.Close()
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, err = accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
eco, _, _ = economyFixture(t, repo, &economyGraph{})
assertBalances(eco)
}
+2 -3
View File
@@ -7,6 +7,7 @@ import (
"bd2server/internal/server/wire"
"encoding/json"
"fmt"
"maps"
"math"
"time"
)
@@ -53,9 +54,7 @@ func (e *Economy) refreshAP(s *economySnapshot) error {
if !reset {
return nil
}
for typ, max := range e.apCaps {
s.Balances[typ] = max
}
maps.Copy(s.Balances, e.apCaps)
clock.Next = next
raw, err := json.Marshal(clock)
if err != nil {
@@ -29,12 +29,18 @@ func TestAvatarSetBatchPersistsMembersAndRollsBackWithAccount(t *testing.T) {
t.Fatal(err)
}
_ = op.Rollback()
repo.Close()
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, err = accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
eco, items, wallet := economyFixture(t, repo, &economyGraph{})
eco.AttachAvatarRewards(avatarDesign())
if len(items.All()) != 0 || wallet.Snapshot().Gold != 5 {
@@ -68,12 +68,18 @@ func TestPermanentBuffAndCurrencyAccountRollback(t *testing.T) {
t.Fatal(err)
}
_ = op.Rollback()
repo.Close()
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, err = accountstate.Open(path)
if err != nil {
t.Fatal(err)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
eco, _, wallet := economyFixture(t, repo, &economyGraph{})
buffs, err = OpenBuffRewards(repo, design)
if err != nil {
+2 -3
View File
@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"encoding/json"
"fmt"
"maps"
"math"
"sort"
"strconv"
@@ -99,9 +100,7 @@ func (e *Economy) AdditionalCurrencies() (map[int]uint64, error) {
}
func (e *Economy) load() (economySnapshot, error) {
s := economySnapshot{Balances: map[uint64]uint64{}, Receipts: map[string]economyReceipt{}}
for t, n := range e.initial {
s.Balances[t] = n
}
maps.Copy(s.Balances, e.initial)
b, err := e.store.Load("event_economy")
if err != nil || b == nil {
return s, err
+8 -2
View File
@@ -118,12 +118,18 @@ func TestEconomySQLiteAtomicRollbackAndRetry(t *testing.T) {
t.Fatal("operation not applied")
}
_ = op.Rollback()
repo.Close()
if err := repo.Close(); err != nil {
t.Fatal(err)
}
repo, e = accountstate.Open(path)
if e != nil {
t.Fatal(e)
}
defer repo.Close()
defer func() {
if err := repo.Close(); err != nil {
t.Error(err)
}
}()
eco, items, wallet := economyFixture(t, repo, &economyGraph{})
if wallet.Snapshot().Gold != 5 || len(items.All()) != 0 {
t.Fatal("rollback kept rewards/cost")
@@ -222,7 +222,9 @@ func TestInstalledScheduledTaskAttendanceAndPassRewardCoverage(t *testing.T) {
if err = rows.Err(); err != nil {
t.Fatal(err)
}
rows.Close()
if err := rows.Close(); err != nil {
t.Fatal(err)
}
}
leaves := map[[2]uint64]gamedata.BattleReward{}
var walk func(gamedata.BattleReward, map[uint64]bool)
+1 -4
View File
@@ -20,10 +20,7 @@ func (e *Economy) NativePurchaseCosts(cost gamedata.Reward) ([]gamedata.Reward,
return nil, fmt.Errorf("events: diamond cost has an item ID")
}
balance := e.wallet.Snapshot()
free := cost.Count
if free > balance.FreeJewelry {
free = balance.FreeJewelry
}
free := min(cost.Count, balance.FreeJewelry)
paid := cost.Count - free
if paid > balance.Jewelry {
return nil, fmt.Errorf("events: insufficient total diamonds")
@@ -49,7 +49,9 @@ func TestObserverUsesInventoryProjectionAndRestoresMemoryOnSaveFailure(t *testin
}
s.store = store
p.count = 1
s.BeforeDispatch("/grant", req(1))
if err := s.BeforeDispatch("/grant", req(1)); err != nil {
t.Fatal(err)
}
p.count = 2
b, e := s.AfterDispatch("/grant", req(1), nil)
if e != nil || len(b) == 0 {
@@ -103,7 +105,9 @@ func TestReadOnlyBatchHasNoObserverWritesButRealDeltaNotifiesOnce(t *testing.T)
// Even if an upstream replay temporarily exposes the same before/after
// delta, the committed request receipt must not increment tasks twice.
p.count = 1
s.BeforeDispatch("/grant", req(58))
if err := s.BeforeDispatch("/grant", req(58)); err != nil {
t.Fatal(err)
}
p.count = 2
b, e = s.AfterDispatch("/grant", req(58), nil)
if e != nil || len(b) != 0 || counter.writes != 1 {
+4 -3
View File
@@ -15,6 +15,7 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"sort"
"strconv"
"sync"
@@ -173,7 +174,7 @@ func key(v ...uint64) string {
var k string
for i, n := range v {
if i > 0 {
k += "/"
k += "/" //nolint:modernize // stringsbuilder
}
k += strconv.FormatUint(n, 10)
}
@@ -498,7 +499,7 @@ func (s *Service) handle(path string, b []byte, identity string) ([]byte, error)
}
}
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
slices.Sort(ids)
for _, id := range ids {
rewards := s.attendanceRewards(v, a, group, id)
if len(rewards) == 0 {
@@ -549,7 +550,7 @@ func (s *Service) handle(path string, b []byte, identity string) ([]byte, error)
var u uint64
_, _ = fmt.Sscanf(uid, "%d", &u)
for g, ids := range groups {
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
slices.Sort(ids)
x := wire.AppendVarint(nil, 1, u)
x = wire.AppendVarint(x, 2, g)
for _, id := range ids {
+2 -3
View File
@@ -8,6 +8,7 @@ package feature
import (
"errors"
"fmt"
"maps"
"bd2server/internal/server/wire"
)
@@ -84,8 +85,6 @@ func (Service) Handle(path string, request []byte) (int, []byte, bool, error) {
// EmptyPacketCodes returns a detached copy for diagnostics and protocol tests.
func EmptyPacketCodes() map[string]int {
result := make(map[string]int, len(emptyResponses))
for path, code := range emptyResponses {
result[path] = code
}
maps.Copy(result, emptyResponses)
return result
}
+4 -6
View File
@@ -4,7 +4,7 @@ import (
"encoding/binary"
"fmt"
"math"
"sort"
"slices"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/player"
@@ -23,10 +23,8 @@ func (s *RecipeService) Knows(id uint64) bool {
if !s.design.IDs[id] {
return false
}
for _, known := range s.initial {
if known == id {
return true
}
if slices.Contains(s.initial, id) {
return true
}
for _, item := range s.items.All() {
if item.Type == 7 && item.ID == id && item.Count > 0 {
@@ -75,7 +73,7 @@ func (s *RecipeService) Handle(path string, request []byte) (int, []byte, bool,
for id := range known {
ids = append(ids, id)
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
slices.Sort(ids)
var packed []byte
for _, id := range ids {
packed = binary.AppendUvarint(packed, id)
+1 -1
View File
@@ -14,7 +14,7 @@ func TestFirstGachaPreviewConfirmAndReplay(t *testing.T) {
characters := make(map[uint64]gamedata.CharacterDesign)
equipmentDesign := make(map[uint64]gamedata.EquipmentDesign)
program := &gamedata.FirstGachaRewardGroup{ID: 20, DropCount: 1, DropType: 1}
for i := uint64(0); i < 5; i++ {
for i := range uint64(5) {
costumeID := 61001 + i
equipmentID := 71001 + i
characters[costumeID] = fixtureCharacter(6100+i, 100+i)

Some files were not shown because too many files have changed in this diff Show More